Files
clawtap/server/adapters/registry.ts
T
kuannnn 0fcf66fc22 feat: ClawTap v0.2.0
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>
2026-03-27 14:46:00 +08:00

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();
}
}