42861ea7fa
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
87 lines
2.9 KiB
TypeScript
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();
|
|
}
|
|
}
|