Files
clawtap/server/adapters/registry.ts
T
kuannnn 42861ea7fa feat: ClawTap v0.1.0 — initial release
Multi-adapter mobile UI for AI coding assistants.
Supports Claude Code, Codex CLI, and Gemini CLI through one interface.

Features:
- Real-time bidirectional sync via tmux + WebSocket
- Cross-AI review (send one AI's output to another for review)
- Multi-review tabs with minimize/expand
- Push notifications (PWA) with smart session-aware filtering
- Three-channel event system (hooks, file watcher, pane monitor)
- Voice input, image paste, draft persistence
- Terminal-native design (JetBrains Mono, dark theme, pixel art claw)
- CLI with --adapter flag on every command
- Zero-overhead fire-and-forget hooks
2026-03-26 10:40:26 +08:00

87 lines
2.9 KiB
TypeScript

// server/adapters/registry.ts
import { execFileSync } from 'child_process';
import fs from 'fs';
import path from 'path';
import os from 'os';
import type { Express } from 'express';
import type { IAdapter } from './interface.js';
import type { AdapterInfo } from '../types/adapter.js';
/** Constructor type for adapter classes that extend IAdapter */
interface AdapterConstructor {
new (): IAdapter;
id: string;
displayName: string;
command: string;
}
const configPath = path.join(os.homedir(), '.clawtap', 'config.json');
let userConfig: { defaultAdapter?: string; adapters?: Record<string, { enabled: boolean }> } = {};
try { userConfig = JSON.parse(fs.readFileSync(configPath, 'utf-8')); } catch {}
export const DEFAULT_ADAPTER: string = userConfig.defaultAdapter || 'claude';
/** Return adapter config parsed from ~/.clawtap/config.json */
export function getAdapterConfig(): { defaultAdapter: string; enabledAdapters: string[] } {
// If no adapters config, enable all known adapters by default.
// Registry's listAvailable() will check `which <command>` for actual availability.
const enabledAdapters = userConfig.adapters
? Object.entries(userConfig.adapters).filter(([, v]) => v.enabled).map(([k]) => k)
: ['claude', 'codex', 'gemini'];
return { defaultAdapter: DEFAULT_ADAPTER, enabledAdapters };
}
const adapters: Map<string, IAdapter> = new Map(); // id → adapter instance
let cachedAvailable: AdapterInfo[] | null = null; // cached result of listAvailable()
export function register(AdapterClass: AdapterConstructor): IAdapter {
const instance = new AdapterClass();
adapters.set(AdapterClass.id, instance);
cachedAvailable = null; // invalidate cache
return instance;
}
export function get(id: string): IAdapter | undefined {
return adapters.get(id);
}
export function getDefault(): IAdapter | null {
return adapters.get(DEFAULT_ADAPTER) || adapters.values().next().value || null;
}
export function listAvailable(): AdapterInfo[] {
if (cachedAvailable) return cachedAvailable;
cachedAvailable = [...adapters.values()].map(adapter => {
const Cls = adapter.constructor as unknown as AdapterConstructor;
let available = false;
try {
execFileSync('which', [Cls.command], { stdio: 'ignore' });
available = true;
} catch {}
return {
id: Cls.id,
displayName: Cls.displayName,
available,
capabilities: adapter.getCapabilities(),
};
});
return cachedAvailable;
}
export function initAll(app: Express): Map<string, IAdapter> {
for (const [, adapter] of adapters) {
adapter.setup(app);
}
listAvailable(); // Pre-cache — sync execFileSync runs once at startup, not per-request
return adapters;
}
export function getAll(): Map<string, IAdapter> {
return adapters;
}
export async function cleanupAll(): Promise<void> {
for (const [, adapter] of adapters) {
await adapter.cleanup();
}
}