# Target Adapters Last Updated: 2026-02-16 Detailed documentation of the target adapter pattern and implementations. --- ## Overview The target adapter system enables CCS to dispatch credential-resolved profiles to different CLI implementations while maintaining a unified configuration and profile system. **Key insight**: Profile resolution (detecting provider, loading auth, building credentials) is target-agnostic. Only the final credential delivery and process spawning differ per target. --- ## Target Adapter Interface Each CLI target implements the `TargetAdapter` contract: ```typescript export interface TargetAdapter { readonly type: TargetType; // 'claude' | 'droid' readonly displayName: string; // "Claude Code" | "Factory Droid" /** Detect if the target CLI binary exists on system */ detectBinary(): TargetBinaryInfo | null; /** Prepare credentials for delivery to target CLI */ prepareCredentials(creds: TargetCredentials): Promise; /** Build spawn arguments for the target CLI */ buildArgs(profile: string, userArgs: string[]): string[]; /** Build environment variables for the target CLI */ buildEnv(creds: TargetCredentials, profileType: string): NodeJS.ProcessEnv; /** Spawn the target CLI process (replaces current process flow) */ exec(args: string[], env: NodeJS.ProcessEnv, options?: { cwd?: string }): void; /** Check if a profile type is supported by this target */ supportsProfileType(profileType: string): boolean; } ``` ### Type Definitions ```typescript export type TargetType = 'claude' | 'droid'; export interface TargetCredentials { baseUrl: string; // API endpoint apiKey: string; // Auth token model?: string; // Model ID provider?: 'anthropic' | 'openai' | 'generic-chat-completion-api'; envVars?: NodeJS.ProcessEnv; // Additional env vars } export interface TargetBinaryInfo { path: string; // Full path to binary needsShell: boolean; // Windows .cmd/.bat/.ps1? } ``` --- ## Target Resolution CCS resolves which adapter to use via priority-ordered checks: ### Resolution Priority ``` 1. --target flag (CLI argument) — highest priority └─ ccs --target droid glm 2. Per-profile config (from ~/.ccs/config.yaml or settings.json) └─ profiles: glm: target: droid 3. argv[0] detection (busybox pattern) — binary name mapping └─ ccsd (symlink/batch file) → droid └─ ccs (regular command) → default 4. Fallback: 'claude' — lowest priority ``` ### Implementation ```typescript // src/targets/target-resolver.ts export function resolveTargetType( args: string[], profileConfig?: { target?: TargetType } ): TargetType { // 1. Parse --target flags (supports --target value and --target=value) // Repeated flags: last one wins. const parsed = parseTargetFlags(args); if (parsed.targetOverride) { return parsed.targetOverride; } // 2. Check profile config if (profileConfig?.target) { return profileConfig.target; } // 3. Check argv[0] (binary name) const binName = path.basename(process.argv[1] || process.argv0 || '').replace(/\.(cmd|bat|ps1|exe)$/i, ''); if (ARGV0_TARGET_MAP[binName]) { return ARGV0_TARGET_MAP[binName]; } // 4. Default to claude return 'claude'; } ``` --- ## Claude Adapter ### Implementation ```typescript // src/targets/claude-adapter.ts export class ClaudeAdapter implements TargetAdapter { readonly type: TargetType = 'claude'; readonly displayName = 'Claude Code'; detectBinary(): TargetBinaryInfo | null { const info = getClaudeCliInfo(); if (!info) return null; return { path: info.path, needsShell: info.needsShell }; } async prepareCredentials(_creds: TargetCredentials): Promise { // No-op: Claude receives credentials via environment variables } buildArgs(_profile: string, userArgs: string[]): string[] { return userArgs; // Pass through user arguments unchanged } buildEnv(creds: TargetCredentials, profileType: string): NodeJS.ProcessEnv { const webSearchEnv = getWebSearchHookEnv(); // For native profiles, strip stale proxy env to prevent interference const baseEnv = profileType === 'account' || profileType === 'default' ? stripAnthropicEnv(process.env) : process.env; const env: NodeJS.ProcessEnv = { ...baseEnv, ...webSearchEnv }; if (creds.envVars) { Object.assign(env, creds.envVars); } // Deliver credentials via environment variables if (creds.baseUrl) env['ANTHROPIC_BASE_URL'] = creds.baseUrl; if (creds.apiKey) env['ANTHROPIC_AUTH_TOKEN'] = creds.apiKey; if (creds.model) env['ANTHROPIC_MODEL'] = creds.model; return env; } exec(args: string[], env: NodeJS.ProcessEnv, _options?: { cwd?: string }): void { const claudeCli = detectClaudeCli(); if (!claudeCli) { void ErrorManager.showClaudeNotFound(); process.exit(1); return; } // Handle Windows shell requirements const isWindows = process.platform === 'win32'; const needsShell = isWindows && /\.(cmd|bat|ps1)$/i.test(claudeCli); let child: ChildProcess; if (needsShell) { const cmdString = [claudeCli, ...args].map(escapeShellArg).join(' '); child = spawn(cmdString, { shell: true, stdio: 'inherit', env }); } else { child = spawn(claudeCli, args, { stdio: 'inherit', env }); } // Handle process termination const onSigInt = () => child.kill('SIGINT'); const onSigTerm = () => child.kill('SIGTERM'); process.once('SIGINT', onSigInt); process.once('SIGTERM', onSigTerm); child.on('exit', () => { process.removeListener('SIGINT', onSigInt); process.removeListener('SIGTERM', onSigTerm); }); } supportsProfileType(profileType: string): boolean { // Claude supports all profile types return true; } } ``` ### Credential Delivery **Method**: Environment variables ```bash export ANTHROPIC_BASE_URL=https://api.anthropic.com export ANTHROPIC_AUTH_TOKEN=sk-ant-... export ANTHROPIC_MODEL=claude-opus-4-6 export WEBSEARCH_HOOK_ENV=... # Image analysis, websearch ``` ### Execution ```bash # Direct invocation ccs gemini → claude "args..." with ANTHROPIC_BASE_URL, ANTHROPIC_AUTH_TOKEN set # With --target override ccs --target claude glm → claude "args..." with ANTHROPIC_BASE_URL, ANTHROPIC_AUTH_TOKEN set ``` --- ## Droid Adapter ### Implementation ```typescript // src/targets/droid-adapter.ts export class DroidAdapter implements TargetAdapter { readonly type: TargetType = 'droid'; readonly displayName = 'Factory Droid'; detectBinary(): TargetBinaryInfo | null { const info = getDroidBinaryInfo(); if (!info) return null; // Non-blocking version compatibility check checkDroidVersion(info.path); return info; } async prepareCredentials(creds: TargetCredentials): Promise { // Write custom model entry to ~/.factory/settings.json await upsertCcsModel(creds.profile, { model: creds.model || 'claude-opus-4-6', displayName: `CCS ${creds.profile}`, baseUrl: creds.baseUrl, apiKey: creds.apiKey, provider: creds.provider || 'anthropic', }); } buildArgs(profile: string, userArgs: string[]): string[] { // Droid uses -m syntax for model selection return ['-m', `custom:ccs-${profile}`, ...userArgs]; } buildEnv(_creds: TargetCredentials, _profileType: string): NodeJS.ProcessEnv { // Droid reads from config file — minimal env needed return { ...process.env }; } exec(args: string[], env: NodeJS.ProcessEnv, _options?: { cwd?: string }): void { const droidPath = detectDroidCli(); if (!droidPath) { console.error('[X] Droid CLI not found. Install: npm i -g @factory/cli'); process.exit(1); return; } // Handle Windows shell requirements const isWindows = process.platform === 'win32'; const needsShell = isWindows && /\.(cmd|bat|ps1)$/i.test(droidPath); let child: ChildProcess; if (needsShell) { const cmdString = [droidPath, ...args].map(escapeShellArg).join(' '); child = spawn(cmdString, { shell: true, stdio: 'inherit', env }); } else { child = spawn(droidPath, args, { stdio: 'inherit', env }); } // Handle process termination const onSigInt = () => child.kill('SIGINT'); const onSigTerm = () => child.kill('SIGTERM'); process.once('SIGINT', onSigInt); process.once('SIGTERM', onSigTerm); child.on('exit', () => { process.removeListener('SIGINT', onSigInt); process.removeListener('SIGTERM', onSigTerm); }); } supportsProfileType(profileType: string): boolean { // Droid currently supports direct settings/default paths only return profileType === 'settings' || profileType === 'default'; } } ``` ### Credential Delivery **Method**: Config file (`~/.factory/settings.json`) ```json { "customModels": [ { "model": "claude-opus-4-6", "displayName": "CCS gemini", "baseUrl": "https://generativelanguage.googleapis.com/v1beta/openai/", "apiKey": "AIza...", "provider": "openai" }, { "model": "glm-4", "displayName": "CCS glm", "baseUrl": "https://open.bigmodel.cn/api/paas/v4/", "apiKey": "your-glm-key", "provider": "openai" } ] } ``` ### Execution ```bash # Direct invocation ccs gemini → droid -m custom:ccs-gemini "args..." (credentials loaded from ~/.factory/settings.json) # With --target override ccs --target droid glm → droid -m custom:ccs-glm "args..." (credentials loaded from ~/.factory/settings.json) ``` ### Binary Alias Pattern ```bash # Create alias/symlink to auto-select droid target # Built-in alias: ccsd ln -s /path/to/ccs /path/to/ccsd # Usage ccsd glm → Target: droid (detected from argv[0]) → droid -m custom:ccs-glm "args..." ``` On Windows, `ccsd.cmd`, `ccsd.bat`, `ccsd.ps1`, and `ccsd.exe` wrappers are also recognized. Additional alias names can be configured at runtime via `CCS_DROID_ALIASES` (comma-separated). Example: `CCS_DROID_ALIASES=ccs-droid,mydroid`. --- ## Registry and Lookup The target registry is a simple map-based store for adapters: ```typescript // src/targets/target-registry.ts const adapters = new Map(); export function registerTarget(adapter: TargetAdapter): void { adapters.set(adapter.type, adapter); } export function getTarget(type: TargetType): TargetAdapter { const adapter = adapters.get(type); if (!adapter) { throw new Error(`Unknown target "${type}"`); } return adapter; } export function getDefaultTarget(): TargetAdapter { return getTarget('claude'); } ``` ### Adapter Registration At startup, adapters self-register: ```typescript // src/ccs.ts (initialization) registerTarget(new ClaudeAdapter()); registerTarget(new DroidAdapter()); ``` --- ## Execution Flow ### Step-by-Step ``` 1. Parse command-line arguments └─ args: ['--target', 'droid', 'glm'] 2. Resolve target type └─ resolveTargetType(args) → 'droid' └─ stripTargetFlag(args) → ['glm'] 3. Detect and resolve profile └─ detectProfile(['glm']) → { profile: 'glm', ... } └─ Load credentials from config/CLIProxy/env 4. Build credentials object └─ TargetCredentials { baseUrl: '...', apiKey: '...', model: 'claude-opus-4-6', envVars: { CCS_PROFILE_NAME: 'glm', ... } } 5. Get target adapter └─ getTarget('droid') → DroidAdapter instance 6. Prepare credentials └─ adapter.prepareCredentials(creds) └─ DroidAdapter: writes to ~/.factory/settings.json 7. Build spawn arguments └─ adapter.buildArgs('glm', []) → ['-m', 'custom:ccs-glm'] 8. Build environment └─ adapter.buildEnv(creds, profileType) → process.env 9. Spawn target CLI └─ adapter.exec(spawnArgs, env) └─ exec spawn('droid', ['-m', 'custom:ccs-glm', ...]) 10. Replace current process └─ Child process inherits stdio └─ Signal handlers propagate to child ``` --- ## Adding a New Target To support a new CLI (e.g., MyAI CLI), follow this pattern: ### 1. Create Adapter Class ```typescript // src/targets/myai-adapter.ts export class MyAiAdapter implements TargetAdapter { readonly type: TargetType = 'myai'; readonly displayName = 'MyAI CLI'; detectBinary(): TargetBinaryInfo | null { const path = which.sync('myai', { nothrow: true }); if (!path) return null; return { path, needsShell: process.platform === 'win32' }; } async prepareCredentials(creds: TargetCredentials): Promise { // Write to ~/.myai/config or similar } buildArgs(profile: string, userArgs: string[]): string[] { return ['-p', profile, ...userArgs]; } buildEnv(creds: TargetCredentials, _profileType: string): NodeJS.ProcessEnv { return { ...process.env, MYAI_API_KEY: creds.apiKey, MYAI_API_URL: creds.baseUrl, }; } exec(args: string[], env: NodeJS.ProcessEnv): void { const myaiPath = this.detectBinary()?.path; if (!myaiPath) { console.error('[X] MyAI CLI not found'); process.exit(1); } spawn(myaiPath, args, { stdio: 'inherit', env }); } supportsProfileType(profileType: string): boolean { return true; // or implement specific logic } } ``` ### 2. Update Type Definition ```typescript // src/targets/target-adapter.ts export type TargetType = 'claude' | 'droid' | 'myai'; ``` ### 3. Register in ccs.ts ```typescript registerTarget(new MyAiAdapter()); ``` ### 4. Update Documentation - Add to [Codebase Summary](../codebase-summary.md) - Update Code Standards adapter examples - Document CLI-specific behavior --- ## Cross-Platform Considerations ### Windows Shell Detection Both adapters check for shell-requiring binaries: ```typescript const needsShell = isWindows && /\.(cmd|bat|ps1)$/i.test(binaryPath); if (needsShell) { const cmdString = [binaryPath, ...args].map(escapeShellArg).join(' '); spawn(cmdString, { shell: true, stdio: 'inherit' }); } else { spawn(binaryPath, args, { stdio: 'inherit' }); } ``` ### Environment Variable Escaping Arguments passed to shell are escaped to prevent injection: ```typescript export function escapeShellArg(arg: string): string { // Wrap in quotes and escape internal quotes return `"${arg.replace(/"/g, '\\"')}"`; } ``` ### Signal Handling Both adapters propagate signals from parent to child: ```typescript const onSigInt = () => child.kill('SIGINT'); const onSigTerm = () => child.kill('SIGTERM'); process.once('SIGINT', onSigInt); process.once('SIGTERM', onSigTerm); child.on('exit', () => { process.removeListener('SIGINT', onSigInt); process.removeListener('SIGTERM', onSigTerm); }); ``` This ensures CTRL+C and graceful shutdowns work correctly. --- ## Testing Target Adapters ### Unit Tests ```typescript describe('ClaudeAdapter', () => { it('detects Claude CLI', () => { const adapter = new ClaudeAdapter(); const binary = adapter.detectBinary(); expect(binary).not.toBeNull(); }); it('builds env with credentials', () => { const adapter = new ClaudeAdapter(); const env = adapter.buildEnv({ baseUrl: 'https://api.anthropic.com', apiKey: 'sk-ant-...', model: 'claude-opus-4-6', }, 'cliproxy'); expect(env['ANTHROPIC_AUTH_TOKEN']).toBe('sk-ant-...'); }); }); ``` ### Integration Tests ```bash # Test Claude adapter ccs --target claude help # Test Droid adapter (if installed) ccs --target droid help # Test argv[0] detection ccsd help ``` --- ## Related Documentation - [Codebase Summary](../codebase-summary.md) — Module structure - [Code Standards](../code-standards.md) — Adapter pattern guidelines - [System Architecture Index](./index.md) — Overall system design