0fcf66fc22
Interactive Prompts: - Unified InteractivePrompt type across all 3 adapters (Claude/Codex/Gemini) - InteractivePromptOverlay component with options, text input, countdown - Gemini + Codex pane monitors detect tool confirmation, ask user, plan approval - respondInteractivePrompt routing: permission → respondPermission, options → _selectOption - Claude AskUserQuestion nested questions[0] structure parsing Cross-AI Review: - Client-generated reviewId, removed pendingReview state - FloatingReviewPanel uses CSS display:none instead of unmount (keeps hooks alive) - Child review sessions default to YOLO/bypass permission mode - Send back to parent, send to existing/new review, tab switching, end review - Collapsed review cards with read-only panel for ended reviews - Full reconnect support: active + ended reviews restore correctly AskUserQuestion Tool Card UI: - Dedicated renderer replaces raw JSON display - Options shown with selected (green) / unselected (gray) indicators - Free text answers shown in quoted format with green border - Collapsed summary: question → answer - Shared parseAskQuestionInput utility (client + server) - Historical tool results attached via _result on tool_use blocks Adapter Fixes: - Session→adapter mapping persisted in SQLite (survives server restart) - SESSION_CREATED deferred for pendingRekey adapters (Codex/Gemini) - session-rekeyed handler sends complete SESSION_CREATED with adapter + cwd - Gemini: auto-accept folder trust, privacy notice, IDE nudge, YOLO * prompt - Claude: auto-accept bypass permissions confirmation (v2.1.85+) - Port fallback (EADDRINUSE → try +1), statusLine shell script wrapper Other: - Desktop Enter sends / Shift+Enter newline; Mobile Enter newline - Strip CLAWTAP_REF marker from session list - Active sessions tab shows adapter badge - Rename CLAUDE_UI_PASSWORD → CLAWTAP_PASSWORD Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
95 lines
3.1 KiB
TypeScript
95 lines
3.1 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;
|
|
}
|
|
|
|
/** Install hooks with confirmed port (called after server.listen succeeds) */
|
|
export function installAllHooks(port: number | string): void {
|
|
for (const [, adapter] of adapters) {
|
|
adapter.setHookPort(port);
|
|
adapter.installHooks();
|
|
}
|
|
}
|
|
|
|
export function getAll(): Map<string, IAdapter> {
|
|
return adapters;
|
|
}
|
|
|
|
export async function cleanupAll(): Promise<void> {
|
|
for (const [, adapter] of adapters) {
|
|
await adapter.cleanup();
|
|
}
|
|
}
|