# Target Adapters Last Updated: 2026-03-28 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' | 'codex' readonly displayName: string; // "Claude Code" | "Factory Droid" | "Codex CLI" /** 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[], options?: { creds?: TargetCredentials; profileType?: ProfileType; binaryInfo?: TargetBinaryInfo; } ): 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' | 'codex'; 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? version?: string; // Optional version string features?: readonly string[]; // Capability probes } ``` --- ## 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 └─ ccs --target codex 2. Explicit runtime entrypoint (`CCS_INTERNAL_ENTRY_TARGET`) — dedicated bin shims └─ ccs-droid / ccsd → droid └─ ccs-codex / ccsx → codex └─ ccsxp → codex, then rewrites argv to `ccs codex --target codex ...` 3. argv[0] detection (runtime alias pattern) — binary name mapping for same-binary/custom aliases └─ ccs-droid (explicit alias) → droid └─ ccsd (legacy shortcut) → droid └─ ccs-codex (explicit alias) → codex └─ ccsx (short alias) → codex └─ ccs (regular command) → default 4. Per-profile config (from ~/.ccs/config.yaml or settings.json) └─ persisted targets are currently only `claude` and `droid` └─ profiles: glm: target: droid 5. 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 explicit runtime entrypoint shim const entrypointTarget = resolveEntrypointTarget(); if (entrypointTarget) { return entrypointTarget; } // 3. Check argv[0] (binary name / custom alias map) 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. Check profile config if (profileConfig?.target) { // Persisted targets intentionally exclude runtime-only codex. return profileConfig.target; } // 5. 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 codex → 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 codex → droid -m custom:ccs-codex "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) ``` ### Runtime Alias Pattern ```bash # Built-in package bin aliases ccs-droid glm → Target: droid (forced by runtime alias) → droid -m custom:ccs-glm "args..." # Legacy shortcut still works ccsd glm → Target: droid (forced by runtime alias) → droid -m custom:ccs-glm "args..." ``` On Windows, `ccs-droid.cmd`, `ccsd.cmd`, `ccsd.bat`, `ccsd.ps1`, and `ccsd.exe` wrappers are also recognized. Additional alias names can be configured at runtime after you create a matching symlink or another launcher that preserves the invoked basename. Use `CCS_TARGET_ALIASES` (preferred, `target=alias1,alias2;...`) or legacy `CCS_DROID_ALIASES` (comma-separated). Example: ```bash ln -s /path/to/ccs /path/to/mydroid CCS_TARGET_ALIASES=droid=mydroid ``` --- ## Codex Adapter ### Implementation The Codex adapter keeps CCS-backed Codex launches transient. It does not rewrite `~/.codex/config.toml`. Instead it: - passes through native default Codex sessions unchanged - probes the installed Codex binary for `--config ` support - injects CCS-backed provider credentials through temporary `-c` overrides - stores the routed API key only in process env via `CCS_CODEX_API_KEY` ```typescript // src/targets/codex-adapter.ts export class CodexAdapter implements TargetAdapter { readonly type: TargetType = 'codex'; readonly displayName = 'Codex CLI'; detectBinary(): TargetBinaryInfo | null { return getCodexBinaryInfo(); } async prepareCredentials(_creds: TargetCredentials): Promise { // No file writes. Codex uses transient -c overrides plus env_key injection. } buildArgs(profile: string, userArgs: string[], options?: BuildOptions): string[] { if ((options?.profileType || 'default') === 'default') { return userArgs; } if (!codexBinarySupportsConfigOverrides(options?.binaryInfo)) { throw new Error('Upgrade Codex before using CCS-backed Codex profiles.'); } return [ '-c', 'model_provider=\"ccs_runtime\"', '-c', 'model_providers.ccs_runtime.base_url=\"http://127.0.0.1:8317/api/provider/codex\"', '-c', 'model_providers.ccs_runtime.env_key=\"CCS_CODEX_API_KEY\"', '-c', 'model_providers.ccs_runtime.wire_api=\"responses\"', ...userArgs, ]; } buildEnv(creds: TargetCredentials, profileType: string): NodeJS.ProcessEnv { const env = { ...stripAnthropicEnv(process.env) }; if (profileType !== 'default') { env['CCS_CODEX_API_KEY'] = creds.apiKey; } return env; } } ``` ### Support Matrix Codex is a real runtime target, but it is intentionally narrower than Claude or Droid in v1: | Profile Type | Codex Target | Notes | |--------------|--------------|-------| | `default` | Yes | Uses existing native Codex auth/config | | `cliproxy` provider=`codex` | Yes | Routed through CLIProxy Codex Responses bridge | | `cliproxy` composite | No | Not proven native-Codex-safe | | `settings` with Codex bridge metadata | Yes | Only when the API profile resolves to a Codex CLIProxy bridge | | `settings` generic API profile | No | Claude/Droid only | | `account` | No | Claude-only account isolation concept | | `copilot` | No | Not a native Codex provider path | ### Codex Dashboard Surface CCS also exposes a dedicated dashboard route at `ccs config` -> `Compatible` -> `Codex CLI`. That page is intentionally narrower than the Droid dashboard in overall scope, but it is no longer read-mostly: - reads and writes only the user config layer: `~/.codex/config.toml` or `$CODEX_HOME/config.toml` - provides guided controls for top-level settings, project trust, profiles, model providers, MCP servers, and supported feature flags - keeps a raw `config.toml` editor as the escape hatch for unsupported or fidelity-sensitive edits - shows binary detection, user-layer config summaries, support-matrix guidance, and upstream docs - normalizes TOML formatting and drops comments on structured saves - keeps structured controls disabled while raw TOML is dirty or invalid, validates project trust paths as absolute or `~/...`, and lets feature flags reset back to Codex defaults - warns that transient CCS runtime overrides such as `codex -c key=value` and `CCS_CODEX_API_KEY` can change the effective runtime without persisting into the file editor This keeps the dashboard honest about Codex's merged configuration model while still giving users one place to inspect and manage the user-owned layer safely. ### Runtime Entrypoints and argv[0] Fallback ```bash # Built-in package bin entrypoints ccs-codex → dist/bin/codex-runtime.js → CCS_INTERNAL_ENTRY_TARGET=codex ccsx → dist/bin/codex-runtime.js → CCS_INTERNAL_ENTRY_TARGET=codex ccsxp → dist/bin/ccsxp-runtime.js → CCS_INTERNAL_ENTRY_TARGET=codex → injects built-in codex profile shortcut ``` If a user launches CCS through a custom shim instead of the built-in package bins, target resolution falls back to `argv[0]` aliases from `CCS_TARGET_ALIASES` or legacy `CCS_CODEX_ALIASES`: ```bash ln -s /path/to/ccs /path/to/mycodex CCS_TARGET_ALIASES='codex=mycodex' # Legacy fallback: CCS_CODEX_ALIASES='mycodex' ``` --- ## 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()); registerTarget(new CodexAdapter()); ``` --- ## 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' | 'codex' | '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 Codex adapter (if installed) ccs --target codex ccs-codex ccsxp # Test argv[0] detection ccs-droid help ccsx ``` --- ## 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