22 KiB
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:
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<void>;
/** 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
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 prepends `--config model_provider="cliproxy"`
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
// 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
// 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<void> {
// 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;
}
}
Native Claude launches keep user arguments session-scoped. The launch layer validates and normalizes
--effort low|medium|high|xhigh|max before spawning Claude, then passes it through without writing
to Claude or CCS configuration. CLIProxy-backed Claude launches still treat --effort as the CCS
thinking alias handled by CLIProxy.
Credential Delivery
Method: Environment variables
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
# 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
// 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<void> {
// 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 <model> 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)
{
"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
# 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
# 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:
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 <key=value>support - injects CCS-backed provider credentials through temporary
-coverrides - stores the routed API key only in process env via
CCS_CODEX_API_KEY
// 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<void> {
// 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.tomlor$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.tomleditor 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=valueandCCS_CODEX_API_KEYcan 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
# 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 native `model_provider="cliproxy"` override
→ pins CODEX_HOME to native `~/.codex` unless `CCSXP_CODEX_HOME` is set
→ repairs `[model_providers.cliproxy]` in the active Codex `config.toml`
→ injects the effective CCS CLIProxy auth token into the provider's configured `env_key`
→ ignores the configured CCS default account/profile and stays in native Codex default mode
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:
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:
// src/targets/target-registry.ts
const adapters = new Map<TargetType, TargetAdapter>();
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:
// 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
// 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<void> {
// 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
// src/targets/target-adapter.ts
export type TargetType = 'claude' | 'droid' | 'codex' | 'myai';
3. Register in ccs.ts
registerTarget(new MyAiAdapter());
4. Update Documentation
- Add to Codebase Summary
- Update Code Standards adapter examples
- Document CLI-specific behavior
Cross-Platform Considerations
Windows Shell Detection
Both adapters check for shell-requiring binaries:
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:
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:
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
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
# 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 — Module structure
- Code Standards — Adapter pattern guidelines
- System Architecture Index — Overall system design