docs(architecture): reconcile provider and target contracts

This commit is contained in:
Tam Nhu Tran committed 2026-07-26 09:35:00 -04:00
1 parent b918783293
commit ebe1746459
3 files changed
+597 -1832

No files matched your search

+224 -443
View File
@@ -1,465 +1,246 @@
# CCS System Architecture # CCS System Architecture
Last Updated: 2026-04-14 CCS separates profile resolution, provider routing, and target execution. This
document describes stable boundaries; source registries and tests own mutable
provider and command inventories.
High-level architecture overview for the CCS (Claude Codex Switch) system. ## System context
--- ```text
User or automation
## System Overview
CCS is a multi-provider profile and runtime manager that enables seamless switching between multiple Claude accounts, alternative AI providers, and multiple CLI targets (Claude Code, Factory Droid, Codex CLI) for credential delivery.
The system consists of two main components:
1. **CLI Application** (`src/`) - Node.js TypeScript CLI
2. **Dashboard UI** (`ui/`) - React web application served by Express
Dashboard localization (i18n) architecture and contributor workflow are documented in [Dashboard i18n Guide](../i18n-dashboard.md).
CCS v7.34 adds Image Analysis Hook for vision model proxying through CLIProxy with automatic injection for all profile types.
CCS v7.67 adds a native structured logging lane for CCS-owned runtime events, backed by `src/services/logging/`, bounded JSONL files under `~/.ccs/logs/`, and a dedicated dashboard `/logs` route.
CCS PR review now uses PR-Agent in GitHub Actions, with reviews running on the self-hosted `cliproxy` runner, existing `AI_REVIEW_*` workflow variables and secrets preserved as the runtime contract, and repo-level guidance stored in `.pr_agent.toml`.
```
+===========================================================================+
| CCS System |
+===========================================================================+
| |
| +------------------+ +-----------------+ +----------------+ |
| | User Terminal | ---> | CCS CLI | ---> | Target CLI | |
| | (ccs command) | | (src/ccs.ts) | | (claude/droid/codex) | |
| +------------------+ +-----------------+ +----------------+ |
| | | |
| v v |
| +------------------+ +-----------------+ +----------------+ |
| | Dashboard UI | <--> | Express | ---> | Provider APIs | |
| | (React SPA) | | Web Server | | (Claude/GLM/ | |
| +------------------+ +-----------------+ | Gemini/etc) | |
| | +----------------+ |
| v |
| +---------------------+ |
| | CLIProxyAPI | |
| | (Local or Remote) | |
| +---------------------+ |
| |
+===========================================================================+
```
---
## Component Architecture
### Multi-Target Adapter System
CCS v7.45 introduces the Target Adapter pattern, enabling seamless integration with different CLI implementations.
**Key architecture:**
```
Profile Resolution (CLIProxy, Settings/API, Account-based)
| |
v v
Target Resolution (--target flag > runtime entrypoint / argv[0] > config > default) CCS CLI ------------------------> Target CLI
|
v
Get Target Adapter (Claude, Droid, or Codex)
|
+---> detectBinary() (find CLI on system)
|
+---> prepareCredentials() (write config or set env)
|
+---> buildArgs() (construct CLI arguments)
|
+---> buildEnv() (prepare environment variables)
|
v
Spawn Target Process
```
**Each target adapter implements different credential delivery:**
- **Claude Adapter**: Env var delivery (existing behavior)
- `ANTHROPIC_BASE_URL`, `ANTHROPIC_AUTH_TOKEN`, `ANTHROPIC_MODEL`
- No config files needed
- **Droid Adapter**: Config file delivery to `~/.factory/settings.json`
- Writes custom model entry: `custom:ccs-<profile>`
- Spawns: `droid -m custom:ccs-<profile> <args>`
- Model config includes baseUrl, apiKey, provider
- **Codex Adapter**: Transient runtime overrides plus user-layer dashboard inspection
- Uses `codex -c key=value` only for CCS-routed launches
- Preserves native `~/.codex/config.toml` ownership
- Dashboard page reads/writes only the user config layer with explicit runtime-vs-provider warnings
**Runtime entrypoints (built-in bins) and argv[0]-style aliases:**
```
ccs → Target: claude (default)
ccs-droid → Target: droid (explicit alias)
ccsd → Target: droid (legacy shortcut)
ccs-codex → Target: codex (explicit alias)
ccsx → Target: codex (short alias)
ccsxp → Target: codex (native cliproxy shortcut; prepends `--config model_provider="cliproxy"`)
```
For details on the adapter architecture, see [Target Adapters](./target-adapters.md).
### CLI Layer
```
+===========================================================================+
| CLI Architecture |
+===========================================================================+
User Input (ccs [--target <cli>] <profile> [args])
|
v
+-------------+
| ccs.ts | Entry point, command routing
+-------------+
|
+---> [Version/Help/Doctor/etc.] ---> Exit
|
v
+------------------+
| Target Resolution | Determine which CLI to use
+------------------+
|
v
+-------------+
| Profile | Determines execution path
| Detection |
+-------------+
|
+---> [Native Claude Account] ---> execClaude()
| |
+---> [CLIProxy Provider] ---> execClaudeWithCLIProxy()
| |
+---> [Settings/API Profile] ---> normalize legacy glmt if needed
|
v
+------------------+
| Target Adapter | Get appropriate adapter
+------------------+
|
v
+------------------+
| Prepare Creds | Deliver credentials
+------------------+
|
v
+------------------+
| Target CLI | Claude Code or Droid
+------------------+
```
---
## Data Flow Architecture
### CLI Execution Flow
```
+===========================================================================+
| CLI Execution Flow |
+===========================================================================+
1. Parse Arguments
|
v
2. Resolve Target Type
|
v
3. Detect Profile Type
|
+---> Native Claude ---> 3a. Load Account Settings
| |
| v
| 4a. Set CLAUDE_CONFIG_DIR
| |
| v
| 5a. Get Claude Target Adapter
|
+---> CLIProxy -------> 3b. Ensure Binary Installed
| |
| v
| 4b. Generate Config
| |
| v
| 5b. Resolve Target Adapter
| |
| v
| 6b. Prepare Credentials
| |
| v
| 7b. Spawn via Adapter
|
+---> Settings/API ---> 3c. Load settings env
|
v
4c. Normalize legacy glmt if needed
|
v
5c. Resolve Target Adapter
|
v
6c. Spawn via Adapter
```
---
## Provider Integration Architecture
For detailed provider flows (CLIProxyAPI, legacy GLMT compatibility, quota management), see [Provider Flows](./provider-flows.md).
---
## Configuration Architecture
### CCS Logging Architecture
- Shared logging contract lives in `src/services/logging/` and is used for CCS-owned runtime diagnostics, request tracing, and bounded recent-entry reads.
- Config lives at top-level `logging.*` in `~/.ccs/config.yaml`; `cliproxy.logging.*` still controls upstream CLIProxy runtime files only.
- CCS-owned runtime logs write to `~/.ccs/logs/current.jsonl` and rotate into `~/.ccs/logs/archive/` based on policy.
- Dashboard exposure uses native `/api/logs/config`, `/api/logs/sources`, and `/api/logs/entries` endpoints plus the `System -> Logs` React page.
- Request logging explicitly skips `/api/logs` reads so the log viewer does not recursively log itself.
### Config File Hierarchy
```
+===========================================================================+
| Configuration Hierarchy |
+===========================================================================+
~/.ccs/
|
+---> config.yaml # Main CCS config (unified)
|
+---> profiles.json # Claude account registry
|
+---> <profile>.settings.json # Per-profile settings
|
+---> cliproxy/
| |
| +---> config.yaml # CLIProxy configuration
| +---> auth/ # OAuth tokens
| +---> bin/ # CLIProxy binary
|
+---> shared/ # Symlinked resources
|
+---> commands/ # Claude Code commands
+---> skills/ # Custom skills
+---> agents/ # Agent configurations
+---> plugins/
|
+---> cache/ # Shared plugin payload/cache data
+---> marketplaces/ # Shared marketplace payload directories
+---> installed_plugins.json
~/.ccs/instances/<profile>/
|
+---> plugins/
|
+---> known_marketplaces.json # Instance-local registry for active CLAUDE_CONFIG_DIR validation
~/.factory/ (Droid CLI)
|
+---> settings.json # Droid config (custom models)
```
Plugin ownership note:
- `commands/`, `skills/`, `agents/`, and `settings.json` remain shared through the existing symlink/copy flow.
- Marketplace payload directories stay shared, but `known_marketplaces.json` is reconciled per instance so Claude Code can validate `installLocation` against that instance's `CLAUDE_CONFIG_DIR/plugins/marketplaces`.
### Config Loading Order
```
1. Environment Variables (highest priority)
|
v
2. CLI Arguments (including --target)
|
v
3. Profile-specific settings (~/.ccs/<profile>.settings.json)
|
v
4. Main config (~/.ccs/config.yaml)
|
v
5. Default values (lowest priority)
```
---
## WebSocket Architecture
### Real-time Communication
```
+===========================================================================+
| WebSocket Communication |
+===========================================================================+
Dashboard (React) Server (Express)
| |
|<------ Connection Established ------>|
| |
|<------ health:update ----------------| Health status
| |
|<------ auth:status ------------------| Auth changes
| |
|<------ usage:update -----------------| Usage stats
| |
|------- action:refresh -------------->| User requests
| |
```
---
## Security Architecture
### Authentication Flow
See [Provider Flows](./provider-flows.md) → Authentication Flow section.
### Security Boundaries
```
+------------------+
| User Terminal |
+------------------+
|
| Local only (no network exposure)
v
+------------------+
| CCS CLI |
+------------------+
|
| Localhost only (127.0.0.1)
v
+------------------+
| CLIProxy/Legacy | Binds to localhost only
+------------------+
|
| TLS encrypted
v
+------------------+
| Target CLI | Spawned locally (claude/droid)
+------------------+
|
| TLS encrypted
v
+------------------+
| Provider APIs | External endpoints
+------------------+
```
---
## Build and Distribution
### Build Pipeline
```
+===========================================================================+
| Build Pipeline |
+===========================================================================+
src/ (TypeScript) ui/src/ (React TSX)
| | | |
| profile/provider state | provider protocol
v v v v
TypeScript Compiler Vite Build CCS local server <-----------> CLIProxy or direct API
| |
v v
dist/ (JavaScript) dist/ui/ (Static assets)
| |
+---------------+---------------------+
| |
v v
npm package (@kaitranntt/ccs) React dashboard
```
The main implementation surfaces are:
| Surface | Ownership |
| --- | --- |
| `src/` | CLI, dispatch, server, provider integration, target adapters |
| `ui/src/` | Dashboard application |
| `dist/` and `dist/ui/` | Build outputs |
| `docker/` | Integrated and legacy container definitions |
| `tests/` | Unit, integration, end-to-end, native, and Docker contracts |
Dashboard localization is documented in the
[Dashboard i18n Guide](../i18n-dashboard.md).
## Execution pipeline
```text
Parse command
|
Resolve command vs launch
|
Resolve target
|
Resolve profile type
+-- account profile ------> isolated target config root
+-- settings profile -----> profile environment
+-- CLIProxy provider ----> local or remote proxy route
|
Prepare target credentials
|
Spawn target and forward lifecycle signals
```
Command routing stops before profile execution for management commands such as
configuration, diagnostics, proxy management, and environment export.
### Profile resolution
Profile resolution distinguishes:
1. built-in CLIProxy provider shortcuts;
2. user-defined CLIProxy profiles;
3. settings/API profiles; and
4. registered account profiles.
Canonical provider IDs and aliases come from
[`src/cliproxy/provider-capabilities.ts`](../../src/cliproxy/provider-capabilities.ts).
The detector and dispatcher consume those registries; documentation must not
maintain a second provider list.
### Target resolution
Provider and target are independent axes. The selected target is resolved from
explicit flags and runtime entry points before falling back to configuration
and the default target.
Each adapter owns credential delivery:
- **Claude Code:** launch environment and optional isolated
`CLAUDE_CONFIG_DIR`;
- **Factory Droid:** CCS-managed custom-model entries in
`~/.factory/settings.json`; and
- **Codex CLI:** transient `-c` overrides for CCS-routed launches while native
user configuration remains separately owned.
See [Target Adapters](./target-adapters.md) for the detailed compatibility
contract.
## Provider routing
CCS supports three routing boundaries:
| Route | Credential and transport owner |
| --- | --- |
| Direct settings/API profile | Target receives the selected provider's environment |
| Local CLIProxy | CCS manages a local proxy binary, config, and auth directory |
| Remote CLIProxy | CCS connects to the configured remote service and applies the selected fallback policy |
Local backend choice is explicit. `original` is the default. `plus` is an
opt-in backend for provider capabilities unavailable in the original backend.
Compatibility restrictions are defined in
[`src/cliproxy/types/provider-types.ts`](../../src/cliproxy/types/provider-types.ts)
and enforced before local execution.
See [Provider Flows](./provider-flows.md).
## Configuration ownership
The effective CCS directory is resolved by
[`src/utils/config-manager.ts`](../../src/utils/config-manager.ts). The normal
default is `~/.ccs`; tests and scoped workflows can override it.
```text
CCS directory
├── config.yaml
├── profiles.json
├── <profile>.settings.json
├── instances/
├── logs/
└── cliproxy/
├── config.yaml
├── auth/
└── bin/
```
### Settings-write contract
Normal launches do not rewrite shared Claude settings. API profiles store
string-valued launch environment in CCS-owned per-profile settings.
Persistent shared configuration is explicit:
- `ccs persist` reads and validates `~/.claude/settings.json`;
- it refuses unsafe symlink targets;
- it preserves unrelated settings while updating the requested managed fields;
- it creates a backup when an existing file is present; and
- it writes the replacement atomically under a settings-directory lock.
Target-owned writers follow their own boundary. For example, the Droid adapter
manages CCS custom-model entries in `~/.factory/settings.json`, not arbitrary
user settings.
## Local server and dashboard
The Express server exposes APIs used by the React dashboard for supported
configuration, auth, usage, health, and logging workflows. The dashboard is a
management surface over shared services; it must not implement a competing
configuration model.
Real-time updates use server-owned WebSocket messages. Event names and payloads
are code contracts and should be read from the server and UI implementations
rather than copied into this overview.
## Logging
CCS-owned structured runtime logging lives under `src/services/logging/`.
Top-level `logging.*` configuration controls CCS JSONL logs under the CCS
directory. `cliproxy.logging.*` controls upstream CLIProxy files and is a
separate contract.
The dashboard log reader excludes its own log-read requests from request
logging to prevent recursive noise. See [Logging Contract](../logging-contract.md).
## Managed tool preparation
WebSearch and image analysis are prepared before the target launch when the
selected profile needs CCS-managed tooling. They use provider-aware routes but
have different failure contracts: enabled third-party WebSearch fails closed if
its managed MCP replacement cannot be prepared, while image analysis can use a
compatible native path when available.
- [WebSearch](../websearch.md)
- [Provider Flows](./provider-flows.md)
## Security and trust boundaries
### Local host
CCS reads and writes user-authorized configuration and starts target processes.
That local filesystem access is more privileged than a provider API request.
Sensitive values must not enter logs or dashboard responses.
### Local proxy
The host CLI uses loopback for locally managed CLIProxy traffic. Local auth
files and the management API remain sensitive even when the transport never
leaves the machine.
### Remote proxy
A remote CLIProxy crosses a network and administrative boundary. TLS,
authentication, certificate policy, reachability, and local fallback are
explicit configuration choices. Remote-only mode must not silently start a
local proxy.
### Container deployment
The integrated container exposes dashboard and proxy ports through the
operator's port mappings. The image therefore does not inherit the host
installation's loopback-only assumption. Network exposure and access control
belong to the deployment operator.
## Build and distribution
```text
src/ -------- TypeScript --------> dist/
ui/src/ ----- Vite --------------> dist/ui/
| |
v v
npm registry / GitHub releases npm package
```
### Package Contents
```
@kaitranntt/ccs
|
+---> dist/ # Compiled CLI
+---> dist/ui/ # Built dashboard
+---> lib/ # Native scripts
| +---> ccs # Bash bootstrap
| +---> ccs.ps1 # PowerShell bootstrap
+---> package.json
```
---
## Deployment Architecture
### Local Installation
```
npm install -g @kaitranntt/ccs
| |
v v
Global node_modules integrated Docker image
|
+---> Creates symlink: ccs --> dist/ccs.js
|
+---> Runtime aliases: ccs-droid / ccsd → ccs (auto-select droid target)
|
+---> First run creates: ~/.ccs/
``` ```
### PR Review Lane The package requires Node.js 18 or newer. Repository development supports Bun
1.0 or newer. CI can pin newer tool versions independently; those workflow pins
are not the minimum consumer runtime contract.
Automated pull request review stays in `.github/workflows/ai-review.yml`, but the workflow now runs PR-Agent instead of the old Claude action. Reviews run on the existing self-hosted `cliproxy` runner, while the workflow preserves the existing `AI_REVIEW_BASE_URL`, `AI_REVIEW_MODEL`, and `AI_REVIEW_API_KEY` contract by mapping those values into PR-Agent env keys such as `OPENAI.*`, `config.*`, and `github_action_config.*`. Repo-specific reviewer guidance lives in `.pr_agent.toml`. The integrated Docker image is built from
[`docker/Dockerfile.integrated`](../../docker/Dockerfile.integrated). It layers
CCS onto a digest-pinned CLIProxy base, runs CLIProxy and the dashboard under
supervision, and health-checks both services. It does not bundle Claude Code,
Gemini CLI, Codex CLI, Droid, or other target CLIs.
``` Release lane details are in [Release Process](../release-process.md).
GitHub Actions `ai-review.yml`
|
v
Self-hosted `cliproxy` runner
|
v
PR-Agent action
|
v
CLIProxy
|
v
Configured model from `.pr_agent.toml`
```
- `ai-review.yml` owns automation wiring such as runner selection, PR-Agent action usage, and runtime values mapped from `AI_REVIEW_*` into `OPENAI.*`, `config.*`, and `github_action_config.*`. ## Architecture invariants
- `.pr_agent.toml` in the repo root owns review instructions for this repository.
- Contributors should treat PR-Agent comments and trusted `/review` reruns as the primary AI review path for PRs targeting CCS.
### Runtime Dependencies - Provider identity comes from the provider registry.
- Target compatibility is enforced at the adapter boundary.
- Environment values persisted in settings are strings.
- Shared target configuration changes require an explicit workflow.
- Local and remote proxy modes do not silently cross trust boundaries.
- Dashboard and CLI use the same domain services and configuration schema.
- Volatile capability details remain source-owned.
``` ## Related documentation
+------------------+ +------------------+
| Node.js 14+ | | Claude CLI |
| (required) | | (required) |
+------------------+ +------------------+
+------------------+ +------------------+ - [Codebase Summary](../codebase-summary.md)
| CLIProxyAPI | | Droid CLI | - [Code Standards](../code-standards.md)
| (auto-managed) | | (optional) | - [Target Adapters](./target-adapters.md)
+------------------+ +------------------+ - [Provider Flows](./provider-flows.md)
``` - [Release Process](../release-process.md)
- [Project Roadmap](../project-roadmap.md)
---
## Related Documentation
- [Codebase Summary](../codebase-summary.md) - Detailed directory structure
- [Code Standards](../code-standards.md) - Coding conventions & patterns
- [Target Adapters](./target-adapters.md) - Multi-CLI adapter architecture
- [Provider Flows](./provider-flows.md) - CLIProxy, legacy GLMT compatibility, authentication flows
- [Project Roadmap](../project-roadmap.md) - Development phases
+204 -598
View File
@@ -1,641 +1,247 @@
# Provider Integration Flows # Provider Integration Flows
Last Updated: 2026-07-16 CCS routes profiles through direct target configuration or CLIProxy. This
document describes the durable flow and trust boundaries. Mutable provider
capabilities are linked to their source registries.
Detailed provider integration flows including CLIProxyAPI, legacy GLMT compatibility transforms, remote CLIProxy, quota management, and authentication. ## Sources of truth
--- | Detail | Authoritative source |
| --- | --- |
| Canonical provider IDs, aliases, auth-flow type, callback ports, refresh ownership | [`src/cliproxy/provider-capabilities.ts`](../../src/cliproxy/provider-capabilities.ts) |
| Original vs Plus backend restrictions | [`src/cliproxy/types/provider-types.ts`](../../src/cliproxy/types/provider-types.ts) |
| Provider/backend enforcement | [`src/cliproxy/services/variant-service.ts`](../../src/cliproxy/services/variant-service.ts) |
| Remote proxy precedence and fallback | [`src/cliproxy/proxy/proxy-config-resolver.ts`](../../src/cliproxy/proxy/proxy-config-resolver.ts) |
| Provider capability regression tests | [`src/cliproxy/__tests__/provider-capabilities.test.ts`](../../src/cliproxy/__tests__/provider-capabilities.test.ts) |
## CLIProxyAPI Flow Do not copy provider inventories, callback ports, or quota-supported lists into
this document. They change independently of the architecture.
### Overview ## Route selection
CLIProxyAPI is a local OAuth proxy binary that enables seamless integration with multiple AI providers. CCS manages the binary and configuration automatically.
### Local Backend Choice
CCS defaults to the original `router-for-me/CLIProxyAPI` backend because it is the stable MIT upstream. The `plus` backend is an explicit opt-in path that downloads the community-maintained `kaitranntt/CLIProxyAPIPlus` fork for providers that still require Plus-only support, such as Kiro, Cursor, GitLab, CodeBuddy, Kilo, and deprecated GitHub Copilot compatibility. CCS does not silently downgrade `backend: plus` to `original`; users choose that backend deliberately when they need those providers.
Generated local CLIProxy configs also keep the management dashboard aligned with the selected backend. `backend: original` uses upstream CPAMC (`router-for-me/Cli-Proxy-API-Management-Center`), while `backend: plus` uses the CCS-maintained dashboard fork (`kaitranntt/Cli-Proxy-API-Management-Center`). Advanced users can override the generated `remote-management.panel-github-repository` value by setting `cliproxy.management_panel_repository` in `~/.ccs/config.yaml`; CCS will regenerate stale local CLIProxy configs when the expected dashboard repository changes.
```text
Resolved profile
|
+-- account profile
| └── target-native authenticated context
|
+-- settings/API profile
| └── direct or compatible upstream URL and credential environment
|
└-- CLIProxy provider
|
+-- reachable configured remote proxy
|
└-- local proxy, when selected or fallback is allowed
|
└── original or Plus backend
``` ```
+===========================================================================+
| CLIProxyAPI Integration |
+===========================================================================+
Claude CLI The provider route is resolved before the target adapter prepares credentials.
This keeps provider selection independent from whether Claude Code, Droid, or
Codex receives the route.
## Local CLIProxy
CCS manages the local binary, generated configuration, auth files, lifecycle,
and provider-specific launch environment.
The local backend contract is:
- `original` is the default upstream CLIProxy backend;
- `plus` is an explicit opt-in community-maintained distribution;
- providers declared Plus-only fail clearly on the original backend; and
- CCS does not silently downgrade a configured Plus backend.
The management-panel repository is generated to match the selected backend
unless the user supplies the supported override.
Local target traffic uses a provider-compatible endpoint and an internal
credential. CLIProxy owns upstream OAuth token use and request translation
according to the selected provider.
## Remote CLIProxy
Remote mode delegates proxy lifecycle and stored authentication to another
CLIProxy installation.
```text
CLI flags and environment
| |
| ANTHROPIC_BASE_URL = localhost:XXXX
v v
+------------------+ CCS proxy configuration
| CLIProxyAPI | Local proxy binary (Plus fork opt-in for plus-only providers)
| (binary) |
+------------------+
| |
+---> OAuth Authentication v
Reachability check
+-----+-----+
| | | |
| +---> Authorization Code Flow (port-based) reachable unreachable
| | - Gemini, Codex, Antigravity, iFlow, Claude, GitLab
| | - Opens browser for user auth
| | - Callback to localhost:PORT
| | | |
| +---> Device Code Flow (no port needed) remote remote-only? ---- yes ---> fail
| - xAI/Grok, Kimi, Kiro, Copilot, CodeBuddy, Kilo, and Qoder
| - User enters a code on the provider verification page
| - Polls for token completion
| |
| +---> Browser URL Polling (no callback port)
| - Cursor
| - Opens provider login URL returned by CLIProxyAPIPlus
| - Polls auth state until token is saved
| |
| v
| +------------------+
| | OAuth Server | Browser-based auth
| +------------------+
| |
+---> Request Transformation no
| |
| v
| Anthropic Format --> Provider Format
| |
+---> Image Analysis Hook (v7.34) fallback enabled? -- no ----> fail
| |
| v
| Vision Model Proxying (gemini, codex, agy, clipproxy)
| - Auto-injected via claude-hooks
| - Skip for Claude Sub accounts (native vision)
| - Fallback with deprecated block-image-read
| |
+---> Provider APIs yes
| |
+---> Google (Gemini) local proxy
+---> OpenAI (Codex)
+---> xAI (Grok)
+---> Antigravity (AGY)
+---> AWS Kiro (Claude-powered)
+---> GitHub Copilot (ghcp, deprecated compatibility)
+---> OpenAI-compatible endpoints
``` ```
### Supported Built-In Providers Configuration precedence and accepted environment names are implemented in
[`proxy-config-resolver.ts`](../../src/cliproxy/proxy/proxy-config-resolver.ts).
The stable behavioral contract is:
| Provider | ID | Auth Method | Callback Port | Backend | - CLI flags override other sources where supported;
|----------|----|----|------|--------| - environment can select a remote host;
| Gemini | `gemini` | Authorization Code | 8085 | Original | - configuration supplies persistent remote settings;
| Codex | `codex` | Authorization Code | 1455 | Original | - HTTPS and HTTP defaults remain protocol-aware;
| xAI (Grok) | `xai` (`grok` CLI alias) | Device Code | none | Original | - `--remote-only` disables local fallback; and
| Antigravity | `agy` | Authorization Code | 51121 | Original | - an unreachable remote proxy falls back only when fallback is enabled.
| Qwen | `qwen` | Account linking unavailable in bundled runtime | none | Original |
| iFlow | `iflow` | Authorization Code | 11451 | Original |
| Claude | `claude` | Authorization Code | 54545 | Original |
| Kimi | `kimi` | Device Code | none | Original |
| Kiro (AWS) | `kiro` | Method-aware (default: Device Code) | none by default | Plus |
| GitHub Copilot (deprecated) | `ghcp` | Device Code | none | Plus |
| Cursor | `cursor` | Browser URL polling | none | Plus |
| GitLab Duo | `gitlab` | Authorization Code or PAT | 17171 | Plus |
| CodeBuddy | `codebuddy` | Device-style polling | none | Plus |
| Kilo AI | `kilo` | Device Code | none | Plus |
| Qoder | `qoder` | Device Code | none | Plus |
xAI uses CLIProxyAPI's `--xai-login` device flow. CCS stores and discovers the resulting A remote proxy is a separate trust boundary. Its operator can receive provider
`xai-*.json` credentials under the canonical `xai` provider. `ccs grok` is a command alias only, traffic and owns its stored OAuth state. Use TLS and authentication appropriate
so it shares the same accounts, settings, live model catalog, and routing as `ccs xai`. to that boundary.
### Codex Duplicate-Email Account Identity ## Authentication
Codex can legitimately produce multiple auth files for the same email when the user has both a team/business login and a personal/free login. CCS now treats those as separate accounts instead of collapsing them by email. CCS asks the selected CLIProxy backend to start the provider's supported auth
flow. The provider registry identifies whether that is an authorization-code
flow, device-code flow, browser polling flow, or currently unsupported account
linking.
- Internal account IDs stay duplicate-aware for Codex only: `email#variant` ```text
- Variant keys are derived from the auth filename, for example `kaidu.kd@gmail.com#04a0f049-team` and `kaidu.kd@gmail.com#free` Select canonical provider
- Dashboard surfaces continue to show the canonical email, with a compact variant badge such as `Team` or `Free` |
- Quota fetch resolves the exact registry `tokenFile` for the selected account instead of scanning by email and taking the first match Validate backend and auth-start support
- Live usage/account monitor stats key by `provider + account identity`, so duplicate Codex emails no longer merge into one runtime bucket |
Start backend-owned auth flow
This preserves the user-visible distinction between business and personal Codex sessions while keeping other providers on their existing email-backed identity model. |
User completes provider interaction
### Built-In Provider Detection |
Backend writes provider auth file
CCS derives canonical providers from `provider-capabilities.ts`. `profile-detector.ts` resolves only |
explicit CLI aliases before routing through the CLIProxy execution flow. CCS reconciles account identity and readiness
|
```typescript Launch through provider route
const provider = resolveCLIProxyProviderShortcut(profileName);
if (provider) {
return execClaudeWithCLIProxy(claudeCli, provider, args);
}
``` ```
--- Provider tokens live under the configured CLIProxy auth directory. Documentation
and examples must use neutral account labels; raw email addresses, token
filenames, access tokens, and refresh tokens are not architecture data.
## Legacy GLMT Compatibility Flow Some providers can produce more than one account with the same display email.
CCS keeps runtime identity tied to the exact registered auth file rather than
collapsing accounts by display text. The dashboard can show a safe variant
label without exposing the internal identifier.
### Overview ## API-key profiles
GLMT is no longer a marketed runtime surface in CCS. Existing `glmt` profiles are kept as a compatibility path and normalized at launch to the direct GLM endpoint. The `src/glmt/` module remains because Cursor response translation still imports its transformer pipeline. API-key profiles store string-valued environment under the CCS profile
settings file.
``` ```text
+===========================================================================+ Create or edit API profile
| Legacy GLMT Compatibility + Internal Transforms |
+===========================================================================+
Claude CLI
| |
| legacy glmt settings detected Write CCS-owned <profile>.settings.json
v
+------------------+
| Compatibility | normalizeDeprecatedGlmtEnv()
| Layer | (src/utils/glmt-deprecation.ts)
+------------------+
| |
v Resolve direct/native vs compatible proxy-style auth
+------------------+
| Direct GLM API | https://api.z.ai/api/anthropic
+------------------+
| |
v Target adapter prepares credentials
+------------------+ |
| src/glmt/* | retained for Cursor translation Spawn target
+------------------+
``` ```
### Supported Migration Targets Native Anthropic profiles use `ANTHROPIC_API_KEY` without forcing a proxy base
URL. Compatible providers typically use `ANTHROPIC_BASE_URL` and
`ANTHROPIC_AUTH_TOKEN`. The profile writer owns that distinction; callers
should not infer it from documentation examples.
| Provider | Config Key | Endpoint | Auth | Normal launches do not write `~/.claude/settings.json`. Users who want shared
|----------|------------|----------|------| Claude settings must select the explicit `ccs persist` workflow.
| Z.AI (GLM) | `glm` | https://api.z.ai/api/anthropic | API key |
| Kimi API | `km` | https://api.kimi.com/coding/ | API key |
| Legacy compatibility | `glmt` | normalized to direct GLM at runtime | existing profile only |
Use `ccs glm` for Z.AI profiles and `ccs km` for reasoning-first Kimi API profiles. Keep `glmt` only when migrating an existing settings file. ## Legacy GLMT compatibility
### Runtime Handling `glmt` is a compatibility input, not a current provider architecture. Existing
legacy settings are normalized to the direct GLM path before normal
settings-profile dispatch. Internal GLMT transformer modules can remain in use
by other compatibility surfaces without making GLMT a supported standalone
runtime.
CCS detects the deprecated `glmt` profile name and normalizes legacy proxy-only settings before dispatching through the normal settings-profile flow: New configuration should use current API profile commands and provider names.
```typescript ## Quota and account-pool flow
if (isDeprecatedGlmtProfileName(profileName)) {
const normalized = normalizeDeprecatedGlmtEnv(settingsEnv); Quota support is provider-specific and intentionally sourced from
// warn user, validate against direct GLM endpoint, continue through settings flow [`provider-capabilities.ts`](../../src/cliproxy/provider-capabilities.ts).
}
The stable pool contract is:
1. Reconcile registered accounts with live auth files.
2. Exclude manually paused accounts from rotation.
3. Fetch quota only for providers with an implemented quota fetcher.
4. Distinguish an exhausted account from a provider-wide or transient failure.
5. When a healthy fallback exists, CCS can create a temporary quota pause for
an exhausted account.
6. Persist the cooldown so later launches observe the same state.
7. Auto-resume only pauses created by CCS quota management; never override a
user's manual pause.
Routing strategy remains an explicit user choice. CCS must not infer
round-robin or fill-first from account count, plan tier, or quota state.
## Image analysis
For profiles that need managed vision support, CCS prepares the image-analysis
route before launch:
```text
Resolve profile and provider
|
Resolve supported provider backend
|
Provision managed MCP/runtime configuration
|
Target invokes ImageAnalysis
|
CCS sends provider-scoped request
|
Return text result
``` ```
--- The managed route must not expose its internal credential in logs or user
settings. If managed preparation, authentication, or proxy readiness is
## Remote CLIProxy Flow (v7.1) unavailable, CCS falls back to compatible native behavior where possible
instead of failing the entire target launch.
### Overview
Provider-specific vision mappings are implementation data and should be read
Remote CLIProxy enables CCS to delegate authentication to a central proxy server instead of spawning a local binary. from the image-analysis routing services and tests.
``` ## Session and observability boundary
+===========================================================================+
| Remote CLIProxy Architecture (v7.1) | Execution paths can record non-secret metadata such as profile identity,
+===========================================================================+ profile type, canonical provider, target type, timestamps, duration, and
numeric process exit status. Logs must not contain provider tokens, API keys,
Config Resolution (proxy-config-resolver.ts) raw auth payloads, or personal account identifiers.
|
+---> Priority: CLI flags > ENV vars > config.yaml > defaults Textual CCS log codes and numeric child-process exit statuses are different
| contracts. See [Logging Contract](../logging-contract.md).
v
+------------------+ ## Invariants
| ResolvedProxyConfig |
| mode: local|remote | - Canonical provider identity is registry-driven.
+------------------+ - Provider aliases normalize before execution.
| - Backend restrictions are validated before starting local auth or routing.
+---> [mode = local] ---> Spawn local CLIProxyAPI binary - Remote-only mode never starts a local fallback.
| | - Persistent environment values are strings.
| v - Account display text is not a unique runtime identity.
| localhost:8317 - Manual pauses are never auto-resumed by quota management.
| - Provider credentials never appear in architecture examples.
+---> [mode = remote] ---> Connect to remote server
| ## Related documentation
v
+------------------+ - [System Architecture](./index.md)
| Health Check | remote-proxy-client.ts - [Target Adapters](./target-adapters.md)
| /v1/models | 2s timeout - [Logging Contract](../logging-contract.md)
+------------------+ - [Codebase Summary](../codebase-summary.md)
| - [Code Standards](../code-standards.md)
+---> [reachable] ---> Use remote
| |
| v
| protocol://host:port
|
+---> [unreachable] ---> Fallback decision
|
+-----------------------------+
|
+---> [fallbackEnabled] ---> Start local
|
+---> [remoteOnly] ---> Fail with error
CLI Flags:
--proxy-host <host> Remote hostname/IP
--proxy-port <port> Port (default: 8317 HTTP, 443 HTTPS)
--proxy-protocol <proto> http or https
--proxy-auth-token <token> Bearer authentication
--local-proxy Force local mode
--remote-only Fail if remote unreachable
Environment Variables:
CCS_PROXY_HOST Remote hostname
CCS_PROXY_PORT Remote port
CCS_PROXY_PROTOCOL Protocol (http/https)
CCS_PROXY_AUTH_TOKEN Auth token
CCS_PROXY_FALLBACK_ENABLED Enable fallback (true/false)
```
### Configuration Resolution
```typescript
// proxy-config-resolver.ts: Priority order
const resolved = {
...DEFAULT_CONFIG, // 4. Defaults (lowest)
...yamlConfig, // 3. config.yaml
...envConfig, // 2. Environment variables
...cliFlags, // 1. CLI flags (highest)
};
```
### Health Check
```typescript
// remote-proxy-client.ts
async function checkRemoteProxyHealth(config: ResolvedProxyConfig): Promise<boolean> {
try {
const url = `${config.protocol}://${config.host}:${config.port}/v1/models`;
const response = await fetch(url, {
headers: config.authToken ? { Authorization: `Bearer ${config.authToken}` } : {},
timeout: 2000,
});
return response.ok;
} catch {
return false;
}
}
```
---
## Quota Management Flow (v7.14)
### Overview
Hybrid quota management enables automatic detection of exhausted accounts and failover to next available account.
Before local CLIProxy startup, CCS reconciles the whole active account pool for the provider, not only the default account. When CCS detects any quota-exhausted account and a healthy fallback exists, it temporarily pauses the exhausted account out of CLIProxy rotation and automatically resumes that pause after the configured cooldown expires. This durable self-pause uses the same account registry and token movement path as dashboard/manual pause, so the dashboard shows the account as paused and CLIProxy cannot rediscover its token from the live `auth/` folder.
```
+===========================================================================+
| Quota Management Architecture (v7.14) |
+===========================================================================+
Pre-Flight Check (before session start)
|
v
+------------------+
| quota-manager.ts | Hybrid quota management
+------------------+
|
+---> Get all active accounts for provider
|
+---> For each account:
| |
| v
| +------------------+
| | quota-fetcher.ts | Provider-specific API calls
| +------------------+
| |
| +---> Check isPaused flag --> Skip if paused
| |
| +---> Fetch quota from provider API
| | - Antigravity: fetchAvailableModels
| | - Claude: policy limits endpoint
| | - Codex: ChatGPT usage windows
| | - Gemini CLI: Code Assist quota buckets
| | - GitHub Copilot: copilot_internal/user snapshots (deprecated compatibility)
| |
| +---> Detect tier (free/paid/unknown)
| |
| +---> Check exhaustion status
|
+---> Pause exhausted non-default accounts when another healthy account exists
|
+---> Select best account (not paused, not exhausted)
|
+---> Auto-failover to next account if current exhausted
|
+---> Temporarily pause exhausted account when fallback exists
| - move token out of live auth discovery
| - persist cooldown expiry across launches
| - auto-resume only CCS-created quota pauses
CLI Commands:
ccs cliproxy pause <account> --> Set isPaused=true in account-manager
ccs cliproxy resume <account> --> Set isPaused=false
ccs cliproxy status [account] --> Display quota + tier info
Dashboard UI:
- Pause/Resume toggle per account
- Tier badge (free/paid/unknown)
- Quota usage display
```
### Account Selection Algorithm
```typescript
// quota-manager.ts: Best account selection
function selectBestAccount(accounts: AccountInfo[]): AccountInfo | null {
// Priority:
// 1. Not paused
// 2. Not exhausted
// 3. Paid tier over free tier
// 4. Highest remaining quota
return accounts
.filter(acc => !acc.isPaused && !acc.isExhausted)
.sort((a, b) => {
if (a.tier !== b.tier) return (a.tier === 'paid' ? -1 : 1);
return (b.remainingQuota || 0) - (a.remainingQuota || 0);
})[0] || null;
}
```
---
## Authentication Flow
### OAuth Providers - Authorization Code Flow
**Providers**: Gemini, Codex, Antigravity, Kiro (aws method)
```
+===========================================================================+
| OAuth - Authorization Code Flow (Port-based) |
+===========================================================================+
1. User runs: ccs codex
|
v
2. Check token cache (~/.ccs/cliproxy/auth/)
|
+---> [Valid token] ---> Use cached token
|
+---> [No/Expired token]
|
v
3. Start local OAuth server (localhost:9876)
|
v
4. Open browser with OAuth request
| https://oauth-provider/authorize?redirect_uri=http://localhost:9876/callback
v
5. User authorizes in browser
|
v
6. OAuth provider redirects to localhost:9876/callback?code=XXXX
|
v
7. Exchange auth code for access token
|
v
8. Cache token locally (~/.ccs/cliproxy/auth/gemini.json)
|
v
9. Proceed with Claude CLI
```
### OAuth Providers - Device Code Flow
**Providers**: GitHub Copilot (ghcp, deprecated compatibility)
Provider identity note:
- Providers that do not expose a reliable email no longer require a manual nickname during first auth.
- CCS derives a stable internal account identifier from the token/cache context and still allows the user to rename the account later.
```
+===========================================================================+
| OAuth - Device Code Flow (No Port Needed) |
+===========================================================================+
1. User runs: ccs ghcp
|
v
2. Check token cache (~/.ccs/cliproxy/auth/)
|
+---> [Valid token] ---> Use cached token
|
+---> [No/Expired token]
|
v
3. Request device code from GitHub
|
v
4. Display user code + verification URL
| "Enter code XXXX-XXXX at github.com/login/device"
v
5. User opens URL in browser and enters code
|
v
6. Poll GitHub for token completion
|
v
7. Receive and cache token locally
|
v
8. Proceed with Claude CLI
```
### Kiro OAuth - Method-Aware Flow
**Supported methods**:
- `aws`: Device Code (default, AWS org friendly)
- `aws-authcode`: Authorization Code via CLI flow
- `google`: Social OAuth via management API
- `github`: Social OAuth via management API (Dashboard flow)
```
+===========================================================================+
| Kiro OAuth - Method-Aware Flow |
+===========================================================================+
Configuration:
ccs_profile:
target: claude
cliproxy:
provider: kiro
kiro_method: aws # or aws-authcode, google, github
Flow:
Device Code (aws)
→ /start endpoint (no callback port)
→ Opens browser
→ User enters code
→ Poll /status
Authorization Code (aws-authcode, google, github)
→ /start-url endpoint
→ Returns auth_url
→ User visits URL
→ Callback handled
→ Poll /status for completion
Key behavior:
- Device Code method uses /start route (no callback port)
- Callback/social methods use /start-url + status polling
- Some management flows return state first, auth_url later
- Manual nicknames are optional when the upstream provider does not return an email
- Account storage uses a stable internal identifier so reauth/update flows do not depend on dashboard list order
```
### API Key Profiles (GLM, Kimi)
```
+===========================================================================+
| API Key Profile (Non-OAuth) |
+===========================================================================+
1. User configures API key in settings
|
v
2. Key stored in ~/.ccs/<profile>.settings.json
|
v
3. Profile detection: APIKeyProfile
|
v
4. Key passed via ANTHROPIC_AUTH_TOKEN env var
|
v
5. Target adapter (Claude/Droid) handles delivery
|
└─ Claude: env var
└─ Droid: config file (~/.factory/settings.json)
```
### Anthropic Direct API Key
```
+===========================================================================+
| Anthropic Direct API Key (Native Auth) |
+===========================================================================+
1. User creates profile: ccs api create --preset anthropic
|
v
2. Key stored in ~/.ccs/<profile>.settings.json
| env: { ANTHROPIC_API_KEY: "sk-ant-..." }
| (NO ANTHROPIC_BASE_URL, NO ANTHROPIC_AUTH_TOKEN)
v
3. Profile detection: settings-based
|
v
4. Key passed via ANTHROPIC_API_KEY env var
| Claude CLI uses native endpoint (api.anthropic.com)
v
5. Claude CLI authenticates with x-api-key header
Detection logic (profile-writer.ts):
- apiKey.startsWith('sk-ant-') -> native mode
- baseUrl.includes('api.anthropic.com') -> native mode
- Otherwise -> proxy mode (existing behavior)
```
---
## Image Analysis Hook Flow (v7.34)
### Overview
Image Analysis Hook enables vision model proxying through CLIProxy with automatic injection for all profile types.
```
+===========================================================================+
| Image Analysis Hook Flow (v7.34) |
+===========================================================================+
Claude CLI with image input
|
v
Hook Installer (ensureProfileHooks)
|
+---> Check ~/.claude/hooks/openai-vision-hook.cjs exists
|
+---> If missing: auto-install via image-analyzer-hook-installer
|
v
Hook Configuration
|
+---> Set ANTHROPIC_IMAGE_HOOK_URL
| (proxy endpoint URL)
|
v
Claude CLI processes image request
|
v
Claude prefers ImageAnalysis MCP tool
|
v
CCS provider-backed image analysis
|
+---> Provider route resolved before launch
|
+---> Direct request to /api/provider/<backend>/v1/messages
|
+---> Native Read fallback if runtime/auth/proxy is unavailable
|
v
Text description returned to Claude CLI
```
### Runtime Environment
```typescript
// getImageAnalysisHookEnv()
{
CCS_IMAGE_ANALYSIS_RUNTIME_BASE_URL: 'http://127.0.0.1:8317',
CCS_IMAGE_ANALYSIS_RUNTIME_PATH: '/api/provider/agy',
CCS_IMAGE_ANALYSIS_RUNTIME_API_KEY: 'ccs-internal-managed',
}
```
### Provider Support
| Provider | Vision Support | Notes |
|----------|---|---|
| Gemini | ✓ | Via CCS ImageAnalysis provider route |
| Codex | ✓ | Via CCS ImageAnalysis provider route |
| Antigravity | ✓ | Via CCS ImageAnalysis provider route |
| Kiro | ✓ | Via mapped CCS provider route when configured |
| Copilot | ✓ | Deprecated compatibility route via mapped ghcp provider |
| GLM/Kimi | ✓ | Via explicit or fallback backend mapping |
---
## Session Tracking
All execution paths record session metadata including target CLI used:
```typescript
{
profileName: 'gemini',
profileType: 'clipproxy',
provider: 'google-gemini',
targetCli: 'claude', // NEW: which target was used
timestamp: '2026-02-16T10:40:00Z',
duration: 12345,
exitCode: 0,
model: 'claude-opus-4-6',
}
```
This enables analytics on target CLI usage and adoption.
---
## Related Documentation
- [System Architecture Index](./index.md) — Overall system design
- [Target Adapters](./target-adapters.md) — Multi-CLI adapter pattern
- [Codebase Summary](../codebase-summary.md) — Module structure
- [Code Standards](../code-standards.md) — Implementation guidelines
+169 -791
View File
@@ -1,803 +1,181 @@
# Target Adapters # Target Adapters
Last Updated: 2026-05-18 Target adapters are the last-mile boundary between CCS profile resolution and a
supported CLI runtime. Profile discovery and credential resolution happen
before this boundary; the selected adapter owns binary detection, target-native
credential delivery, argument and environment construction, and child-process
execution.
Detailed documentation of the target adapter pattern and implementations. Related architecture:
--- - [System architecture](./index.md)
- [Provider flows](./provider-flows.md)
## Overview ## Contract
The target adapter system enables CCS to dispatch credential-resolved profiles to different CLI implementations while maintaining a unified configuration and profile system. The canonical interface and data types live in
[`src/targets/target-adapter.ts`](../../src/targets/target-adapter.ts). Each
adapter must:
**Key insight**: Profile resolution (detecting provider, loading auth, building credentials) is target-agnostic. Only the final credential delivery and process spawning differ per target. 1. detect its runtime binary without changing user configuration;
2. reject unsupported profile types before launch;
3. prepare only the target-owned configuration needed for the launch;
4. construct an argument vector and environment without exposing credentials in
arguments;
5. spawn the target with inherited stdio and forward process signals.
--- The registry in
[`src/targets/target-registry.ts`](../../src/targets/target-registry.ts) maps a
target type to its adapter. Target names, built-in aliases, legacy alias
environment variables, and persistence eligibility are centralized in
[`src/targets/target-metadata.ts`](../../src/targets/target-metadata.ts).
## Target Adapter Interface Do not duplicate the TypeScript interface in this guide. Interface signatures
and credential fields change with runtime requirements; the source files are
Each CLI target implements the `TargetAdapter` contract: the contract checked by the compiler.
```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<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
```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 ## Target Resolution
CCS resolves which adapter to use via priority-ordered checks: [`src/targets/target-resolver.ts`](../../src/targets/target-resolver.ts) selects
the runtime in this order:
### Resolution Priority
1. `--target <name>` or `--target=<name>`; if repeated, the last flag wins.
``` 2. A trusted package runtime entrypoint identified by
1. --target flag (CLI argument) — highest priority `CCS_INTERNAL_ENTRY_TARGET`.
└─ ccs --target droid glm 3. The invoked binary name, including built-in or configured aliases.
└─ ccs --target codex 4. A persisted per-profile target.
5. `claude`.
2. Explicit runtime entrypoint (`CCS_INTERNAL_ENTRY_TARGET`) — dedicated bin shims
└─ ccs-droid / ccsd → droid All `--target` flags are removed before the remaining arguments reach the
└─ ccs-codex / ccsx → codex runtime. Parsing stops at the `--` option terminator.
└─ ccsxp → codex, then prepends `--config model_provider="cliproxy"`
The built-in runtime aliases are derived from target metadata:
3. argv[0] detection (runtime alias pattern) — binary name mapping for same-binary/custom aliases
└─ ccs-droid (explicit alias) → droid | Target | Built-in aliases |
└─ ccsd (legacy shortcut) → droid | --- | --- |
└─ ccs-codex (explicit alias) → codex | Claude Code | Base `ccs` command |
└─ ccsx (short alias) → codex | Factory Droid | `ccs-droid`, `ccsd` |
└─ ccs (regular command) → default | Codex CLI | `ccs-codex`, `ccsx`, `ccsxp` |
4. Per-profile config (from ~/.ccs/config.yaml or settings.json) Custom aliases use `CCS_TARGET_ALIASES` with entries such as
└─ persisted targets are currently only `claude` and `droid` `droid=team-droid;codex=team-codex`. The target-specific legacy environment
└─ profiles: variables remain compatibility inputs. Alias values are validated and cannot
glm: replace reserved package binary names.
target: droid
## Runtime Compatibility
5. Fallback: 'claude' — lowest priority
``` Adapter-level checks are deliberately conservative. Flow-specific compatibility
is evaluated in
### Implementation [`src/targets/target-runtime-compatibility.ts`](../../src/targets/target-runtime-compatibility.ts),
which has the provider and bridge context needed for an accurate decision.
```typescript
// src/targets/target-resolver.ts | Profile flow | Claude | Droid | Codex |
| --- | --- | --- | --- |
export function resolveTargetType( | Native default | Supported | Conditional: requires resolved `ANTHROPIC_BASE_URL` and `ANTHROPIC_AUTH_TOKEN` | Supported |
args: string[], | Settings/API profile | Supported | Supported | Codex CLIProxy bridge only |
profileConfig?: { target?: TargetType } | CLIProxy profile | Supported | Supported | Non-composite `codex` provider only |
): TargetType { | Claude account | Supported | Not supported | Not supported |
// 1. Parse --target flags (supports --target value and --target=value) | Copilot | Supported | Not supported | Not supported |
// Repeated flags: last one wins. | Cursor local proxy | Supported | Not supported | Not supported |
const parsed = parseTargetFlags(args);
if (parsed.targetOverride) { When changing compatibility, update both the runtime evaluator and its focused
return parsed.targetOverride; tests. The authoritative coverage is in
} [`tests/unit/targets/target-runtime-compatibility.test.ts`](../../tests/unit/targets/target-runtime-compatibility.test.ts).
// 2. Check explicit runtime entrypoint shim The compatibility evaluator accepts Droid's native-default flow, but adapter
const entrypointTarget = resolveEntrypointTarget(); preparation still validates the resolved credentials. A default Droid launch
if (entrypointTarget) { cannot proceed without both a non-empty base URL and auth token.
return entrypointTarget;
} ## Credential and Configuration Boundaries
// 3. Check argv[0] (binary name / custom alias map) ### Claude Code
const binName = path.basename(process.argv[1] || process.argv0 || '').replace(/\.(cmd|bat|ps1|exe)$/i, '');
if (ARGV0_TARGET_MAP[binName]) { [`src/targets/claude-adapter.ts`](../../src/targets/claude-adapter.ts) delivers
return ARGV0_TARGET_MAP[binName]; resolved provider values through the child environment. Native account and
} default launches remove stale Anthropic routing variables before execution.
Browser and WebSearch launch preparation may add runtime-specific arguments and
// 4. Check profile config environment values.
if (profileConfig?.target) {
// Persisted targets are validated before profile configuration is saved. The adapter does not persist provider credentials to Claude settings.
return profileConfig.target;
} ### Factory Droid
// 5. Default to claude [`src/targets/droid-adapter.ts`](../../src/targets/droid-adapter.ts) validates
return 'claude'; the resolved base URL and token, then delegates the target-owned write to
} [`src/targets/droid-config-manager.ts`](../../src/targets/droid-config-manager.ts).
``` That manager updates the CCS custom model and active model in Factory settings.
The adapter passes user arguments through; it does not inject a `-m` selector.
---
Factory settings are a persistent user-owned surface. Writes must preserve
## Claude Adapter unrelated settings and use the configuration manager rather than direct JSON
replacement.
### Implementation
### Codex CLI
```typescript
// src/targets/claude-adapter.ts [`src/targets/codex-adapter.ts`](../../src/targets/codex-adapter.ts) keeps normal
CCS-backed launches transient:
export class ClaudeAdapter implements TargetAdapter {
readonly type: TargetType = 'claude'; - native default sessions keep native Codex authentication and configuration;
readonly displayName = 'Claude Code'; - CCS-backed sessions use `-c key=value` overrides for a Responses-compatible
runtime provider;
detectBinary(): TargetBinaryInfo | null { - the resolved API key is supplied through the provider's environment key, not
const info = getClaudeCliInfo(); on the command line;
if (!info) return null; - stale Anthropic and nested Codex-session variables are removed before spawn.
return { path: info.path, needsShell: info.needsShell };
} The `ccsxp` shortcut is the explicit exception. It may repair the dedicated
`cliproxy` provider block in the active Codex configuration through
async prepareCredentials(_creds: TargetCredentials): Promise<void> { [`src/targets/codex-cliproxy-provider-config.ts`](../../src/targets/codex-cliproxy-provider-config.ts).
// No-op: Claude receives credentials via environment variables That repair preserves a valid custom base URL and the configured environment-key
} name. General Codex launches must not rewrite `config.toml`.
buildArgs(_profile: string, userArgs: string[]): string[] { ## Execution Invariants
return userArgs; // Pass through user arguments unchanged
} The dispatcher owns the order of operations:
buildEnv(creds: TargetCredentials, profileType: string): NodeJS.ProcessEnv { 1. parse CCS-owned arguments and resolve the target;
const webSearchEnv = getWebSearchHookEnv(); 2. resolve the profile and provider credentials;
3. evaluate target/profile/provider compatibility;
// For native profiles, strip stale proxy env to prevent interference 4. detect the target binary;
const baseEnv = 5. call `prepareCredentials`;
profileType === 'account' || profileType === 'default' 6. build target arguments and environment;
? stripAnthropicEnv(process.env) 7. execute the child runtime.
: process.env;
Flow implementations live under
const env: NodeJS.ProcessEnv = { ...baseEnv, ...webSearchEnv }; [`src/dispatcher/flows/`](../../src/dispatcher/flows/). Shared target execution
logic lives in
if (creds.envVars) { [`src/dispatcher/target-executor.ts`](../../src/dispatcher/target-executor.ts).
Object.assign(env, creds.envVars);
} All adapters must preserve these invariants:
// Deliver credentials via environment variables - user credentials never appear in documented examples, logs, or spawn
if (creds.baseUrl) env['ANTHROPIC_BASE_URL'] = creds.baseUrl; arguments;
if (creds.apiKey) env['ANTHROPIC_AUTH_TOKEN'] = creds.apiKey; - target-owned persistent writes are explicit and scoped;
if (creds.model) env['ANTHROPIC_MODEL'] = creds.model; - stale routing variables from another runtime do not leak into the child;
- Windows wrapper handling does not use a shell unless the wrapper format
return env; requires one;
} - child signals and exit behavior propagate to the CCS process;
- binary and launch failures run registered cleanup before exit.
exec(args: string[], env: NodeJS.ProcessEnv, _options?: { cwd?: string }): void {
const claudeCli = detectClaudeCli(); ## Adding or Changing a Target
if (!claudeCli) {
void ErrorManager.showClaudeNotFound(); Change the smallest complete set:
process.exit(1);
return; 1. update `TargetType` and the adapter contract only when required;
} 2. add target metadata and aliases in `target-metadata.ts`;
3. implement and register the adapter through `src/targets/index.ts`;
// Handle Windows shell requirements 4. add flow-aware compatibility rules;
const isWindows = process.platform === 'win32'; 5. add focused resolver, adapter, compatibility, and integration tests;
const needsShell = isWindows && /\.(cmd|bat|ps1)$/i.test(claudeCli); 6. update CLI help and dashboard controls if the target becomes user
configurable.
let child: ChildProcess;
if (needsShell) { Start with these test suites:
const cmdString = [claudeCli, ...args].map(escapeShellArg).join(' ');
child = spawn(cmdString, { shell: true, stdio: 'inherit', env }); - [`tests/unit/targets/target-resolver.test.ts`](../../tests/unit/targets/target-resolver.test.ts)
} else { - [`tests/unit/targets/target-registry.test.ts`](../../tests/unit/targets/target-registry.test.ts)
child = spawn(claudeCli, args, { stdio: 'inherit', env }); - [`tests/unit/targets/target-runtime-compatibility.test.ts`](../../tests/unit/targets/target-runtime-compatibility.test.ts)
} - target-specific tests under
[`tests/unit/targets/`](../../tests/unit/targets/)
// Handle process termination
const onSigInt = () => child.kill('SIGINT'); Do not describe a target as supported from adapter registration alone. Support
const onSigTerm = () => child.kill('SIGTERM'); requires a compatible profile flow, safe credential delivery, binary detection,
process.once('SIGINT', onSigInt); execution behavior, and tests for the full combination.
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
```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<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`)
```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 <key=value>` 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<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.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
→ passes native Codex diagnostics plus known upstream Codex subcommands and aliases through before CCS profile detection
→ reserves CCS-owned `auth`, `doctor`, and `update`
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`
→ preserves valid custom `base_url` values for remote or non-default CLIProxy endpoints
→ 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`:
```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<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:
```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<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
```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