mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-12 16:15:13 +00:00
docs(architecture): reconcile provider and target contracts
This commit is contained in:
1 parent
b918783293
commit
ebe1746459
3 files changed
+571
-1806
No files matched your search
+211
-430
@@ -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
|
| profile/provider state | provider protocol
|
||||||
Get Target Adapter (Claude, Droid, or Codex)
|
v v
|
||||||
|
|
CCS local server <-----------> CLIProxy or direct API
|
||||||
+---> detectBinary() (find CLI on system)
|
|
|
||||||
|
|
v
|
||||||
+---> prepareCredentials() (write config or set env)
|
React dashboard
|
||||||
|
|
|
||||||
+---> buildArgs() (construct CLI arguments)
|
|
||||||
|
|
|
||||||
+---> buildEnv() (prepare environment variables)
|
|
||||||
|
|
|
||||||
v
|
|
||||||
Spawn Target Process
|
|
||||||
```
|
```
|
||||||
|
|
||||||
**Each target adapter implements different credential delivery:**
|
The main implementation surfaces are:
|
||||||
|
|
||||||
- **Claude Adapter**: Env var delivery (existing behavior)
|
| Surface | Ownership |
|
||||||
- `ANTHROPIC_BASE_URL`, `ANTHROPIC_AUTH_TOKEN`, `ANTHROPIC_MODEL`
|
| --- | --- |
|
||||||
- No config files needed
|
| `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 |
|
||||||
|
|
||||||
- **Droid Adapter**: Config file delivery to `~/.factory/settings.json`
|
Dashboard localization is documented in the
|
||||||
- Writes custom model entry: `custom:ccs-<profile>`
|
[Dashboard i18n Guide](../i18n-dashboard.md).
|
||||||
- Spawns: `droid -m custom:ccs-<profile> <args>`
|
|
||||||
- Model config includes baseUrl, apiKey, provider
|
|
||||||
|
|
||||||
- **Codex Adapter**: Transient runtime overrides plus user-layer dashboard inspection
|
## Execution pipeline
|
||||||
- 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:**
|
```text
|
||||||
|
Parse command
|
||||||
```
|
|
||||||
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)
|
Resolve command vs launch
|
||||||
|
|
|
|
||||||
+---> profiles.json # Claude account registry
|
Resolve target
|
||||||
|
|
|
|
||||||
+---> <profile>.settings.json # Per-profile settings
|
Resolve profile type
|
||||||
|
+-- account profile ------> isolated target config root
|
||||||
|
+-- settings profile -----> profile environment
|
||||||
|
+-- CLIProxy provider ----> local or remote proxy route
|
||||||
|
|
|
|
||||||
+---> cliproxy/
|
Prepare target credentials
|
||||||
| |
|
|
||||||
| +---> config.yaml # CLIProxy configuration
|
|
||||||
| +---> auth/ # OAuth tokens
|
|
||||||
| +---> bin/ # CLIProxy binary
|
|
||||||
|
|
|
|
||||||
+---> shared/ # Symlinked resources
|
Spawn target and forward lifecycle signals
|
||||||
|
|
|
||||||
+---> 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:
|
Command routing stops before profile execution for management commands such as
|
||||||
- `commands/`, `skills/`, `agents/`, and `settings.json` remain shared through the existing symlink/copy flow.
|
configuration, diagnostics, proxy management, and environment export.
|
||||||
- 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
|
### Profile resolution
|
||||||
|
|
||||||
```
|
Profile resolution distinguishes:
|
||||||
1. Environment Variables (highest priority)
|
|
||||||
|
|
1. built-in CLIProxy provider shortcuts;
|
||||||
v
|
2. user-defined CLIProxy profiles;
|
||||||
2. CLI Arguments (including --target)
|
3. settings/API profiles; and
|
||||||
|
|
4. registered account profiles.
|
||||||
v
|
|
||||||
3. Profile-specific settings (~/.ccs/<profile>.settings.json)
|
Canonical provider IDs and aliases come from
|
||||||
|
|
[`src/cliproxy/provider-capabilities.ts`](../../src/cliproxy/provider-capabilities.ts).
|
||||||
v
|
The detector and dispatcher consume those registries; documentation must not
|
||||||
4. Main config (~/.ccs/config.yaml)
|
maintain a second provider list.
|
||||||
|
|
|
||||||
v
|
### Target resolution
|
||||||
5. Default values (lowest priority)
|
|
||||||
|
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
|
||||||
|
|
||||||
## WebSocket Architecture
|
Normal launches do not rewrite shared Claude settings. API profiles store
|
||||||
|
string-valued launch environment in CCS-owned per-profile settings.
|
||||||
|
|
||||||
### Real-time Communication
|
Persistent shared configuration is explicit:
|
||||||
|
|
||||||
```
|
- `ccs persist` reads and validates `~/.claude/settings.json`;
|
||||||
+===========================================================================+
|
- it refuses unsafe symlink targets;
|
||||||
| WebSocket Communication |
|
- 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.
|
||||||
|
|
||||||
Dashboard (React) Server (Express)
|
Target-owned writers follow their own boundary. For example, the Droid adapter
|
||||||
| |
|
manages CCS custom-model entries in `~/.factory/settings.json`, not arbitrary
|
||||||
|<------ Connection Established ------>|
|
user settings.
|
||||||
| |
|
|
||||||
|<------ health:update ----------------| Health status
|
## Local server and dashboard
|
||||||
| |
|
|
||||||
|<------ auth:status ------------------| Auth changes
|
The Express server exposes APIs used by the React dashboard for supported
|
||||||
| |
|
configuration, auth, usage, health, and logging workflows. The dashboard is a
|
||||||
|<------ usage:update -----------------| Usage stats
|
management surface over shared services; it must not implement a competing
|
||||||
| |
|
configuration model.
|
||||||
|------- action:refresh -------------->| User requests
|
|
||||||
| |
|
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
|
||||||
|
npm package
|
||||||
|
|
|
||||||
|
v
|
||||||
|
integrated Docker image
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
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.
|
||||||
|
|
||||||
## Security Architecture
|
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.
|
||||||
|
|
||||||
### Authentication Flow
|
Release lane details are in [Release Process](../release-process.md).
|
||||||
|
|
||||||
See [Provider Flows](./provider-flows.md) → Authentication Flow section.
|
## Architecture invariants
|
||||||
|
|
||||||
### Security Boundaries
|
- 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
|
||||||
+------------------+
|
|
||||||
| 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
|
|
||||||
+------------------+
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
- [Codebase Summary](../codebase-summary.md)
|
||||||
|
- [Code Standards](../code-standards.md)
|
||||||
## Build and Distribution
|
- [Target Adapters](./target-adapters.md)
|
||||||
|
- [Provider Flows](./provider-flows.md)
|
||||||
### Build Pipeline
|
- [Release Process](../release-process.md)
|
||||||
|
- [Project Roadmap](../project-roadmap.md)
|
||||||
```
|
|
||||||
+===========================================================================+
|
|
||||||
| Build Pipeline |
|
|
||||||
+===========================================================================+
|
|
||||||
|
|
||||||
src/ (TypeScript) ui/src/ (React TSX)
|
|
||||||
| |
|
|
||||||
v v
|
|
||||||
TypeScript Compiler Vite Build
|
|
||||||
| |
|
|
||||||
v v
|
|
||||||
dist/ (JavaScript) dist/ui/ (Static assets)
|
|
||||||
| |
|
|
||||||
+---------------+---------------------+
|
|
||||||
|
|
|
||||||
v
|
|
||||||
npm package (@kaitranntt/ccs)
|
|
||||||
|
|
|
||||||
v
|
|
||||||
npm registry / GitHub releases
|
|
||||||
```
|
|
||||||
|
|
||||||
### 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
|
|
||||||
Global node_modules
|
|
||||||
|
|
|
||||||
+---> Creates symlink: ccs --> dist/ccs.js
|
|
||||||
|
|
|
||||||
+---> Runtime aliases: ccs-droid / ccsd → ccs (auto-select droid target)
|
|
||||||
|
|
|
||||||
+---> First run creates: ~/.ccs/
|
|
||||||
```
|
|
||||||
|
|
||||||
### PR Review Lane
|
|
||||||
|
|
||||||
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`.
|
|
||||||
|
|
||||||
```
|
|
||||||
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.*`.
|
|
||||||
- `.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
|
|
||||||
|
|
||||||
```
|
|
||||||
+------------------+ +------------------+
|
|
||||||
| Node.js 14+ | | Claude CLI |
|
|
||||||
| (required) | | (required) |
|
|
||||||
+------------------+ +------------------+
|
|
||||||
|
|
||||||
+------------------+ +------------------+
|
|
||||||
| CLIProxyAPI | | Droid CLI |
|
|
||||||
| (auto-managed) | | (optional) |
|
|
||||||
+------------------+ +------------------+
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 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
|
|
||||||
@@ -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.
|
```text
|
||||||
|
Resolved profile
|
||||||
### Local Backend Choice
|
|
|
||||||
|
+-- account profile
|
||||||
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.
|
| └── target-native authenticated context
|
||||||
|
|
|
||||||
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.
|
+-- settings/API profile
|
||||||
|
| └── direct or compatible upstream URL and credential environment
|
||||||
```
|
|
|
||||||
+===========================================================================+
|
└-- CLIProxy provider
|
||||||
| CLIProxyAPI Integration |
|
|
|
||||||
+===========================================================================+
|
+-- reachable configured remote proxy
|
||||||
|
|
|
||||||
Claude CLI
|
└-- local proxy, when selected or fallback is allowed
|
||||||
|
|
|
||||||
| ANTHROPIC_BASE_URL = localhost:XXXX
|
|
||||||
v
|
|
||||||
+------------------+
|
|
||||||
| CLIProxyAPI | Local proxy binary (Plus fork opt-in for plus-only providers)
|
|
||||||
| (binary) |
|
|
||||||
+------------------+
|
|
||||||
|
|
|
||||||
+---> OAuth Authentication
|
|
||||||
| |
|
|
||||||
| +---> Authorization Code Flow (port-based)
|
|
||||||
| | - Gemini, Codex, Antigravity, iFlow, Claude, GitLab
|
|
||||||
| | - Opens browser for user auth
|
|
||||||
| | - Callback to localhost:PORT
|
|
||||||
| |
|
|
||||||
| +---> Device Code Flow (no port needed)
|
|
||||||
| - 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
|
|
||||||
| |
|
|
||||||
| v
|
|
||||||
| Anthropic Format --> Provider Format
|
|
||||||
|
|
|
||||||
+---> Image Analysis Hook (v7.34)
|
|
||||||
| |
|
|
||||||
| 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
|
|
||||||
|
|
|
|
||||||
+---> Google (Gemini)
|
└── original or Plus backend
|
||||||
+---> OpenAI (Codex)
|
|
||||||
+---> xAI (Grok)
|
|
||||||
+---> Antigravity (AGY)
|
|
||||||
+---> AWS Kiro (Claude-powered)
|
|
||||||
+---> GitHub Copilot (ghcp, deprecated compatibility)
|
|
||||||
+---> OpenAI-compatible endpoints
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Supported Built-In Providers
|
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.
|
||||||
|
|
||||||
| Provider | ID | Auth Method | Callback Port | Backend |
|
## Local CLIProxy
|
||||||
|----------|----|----|------|--------|
|
|
||||||
| Gemini | `gemini` | Authorization Code | 8085 | Original |
|
|
||||||
| Codex | `codex` | Authorization Code | 1455 | Original |
|
|
||||||
| xAI (Grok) | `xai` (`grok` CLI alias) | Device Code | none | Original |
|
|
||||||
| Antigravity | `agy` | Authorization Code | 51121 | Original |
|
|
||||||
| 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
|
CCS manages the local binary, generated configuration, auth files, lifecycle,
|
||||||
`xai-*.json` credentials under the canonical `xai` provider. `ccs grok` is a command alias only,
|
and provider-specific launch environment.
|
||||||
so it shares the same accounts, settings, live model catalog, and routing as `ccs xai`.
|
|
||||||
|
|
||||||
### Codex Duplicate-Email Account Identity
|
The local backend contract is:
|
||||||
|
|
||||||
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.
|
- `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.
|
||||||
|
|
||||||
- Internal account IDs stay duplicate-aware for Codex only: `email#variant`
|
The management-panel repository is generated to match the selected backend
|
||||||
- Variant keys are derived from the auth filename, for example `kaidu.kd@gmail.com#04a0f049-team` and `kaidu.kd@gmail.com#free`
|
unless the user supplies the supported override.
|
||||||
- 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
|
|
||||||
- Live usage/account monitor stats key by `provider + account identity`, so duplicate Codex emails no longer merge into one runtime bucket
|
|
||||||
|
|
||||||
This preserves the user-visible distinction between business and personal Codex sessions while keeping other providers on their existing email-backed identity model.
|
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.
|
||||||
|
|
||||||
### Built-In Provider Detection
|
## Remote CLIProxy
|
||||||
|
|
||||||
CCS derives canonical providers from `provider-capabilities.ts`. `profile-detector.ts` resolves only
|
Remote mode delegates proxy lifecycle and stored authentication to another
|
||||||
explicit CLI aliases before routing through the CLIProxy execution flow.
|
CLIProxy installation.
|
||||||
|
|
||||||
```typescript
|
```text
|
||||||
const provider = resolveCLIProxyProviderShortcut(profileName);
|
CLI flags and environment
|
||||||
|
|
|
||||||
if (provider) {
|
v
|
||||||
return execClaudeWithCLIProxy(claudeCli, provider, args);
|
CCS proxy configuration
|
||||||
}
|
|
|
||||||
|
v
|
||||||
|
Reachability check
|
||||||
|
+-----+-----+
|
||||||
|
| |
|
||||||
|
reachable unreachable
|
||||||
|
| |
|
||||||
|
remote remote-only? ---- yes ---> fail
|
||||||
|
|
|
||||||
|
no
|
||||||
|
|
|
||||||
|
fallback enabled? -- no ----> fail
|
||||||
|
|
|
||||||
|
yes
|
||||||
|
|
|
||||||
|
local proxy
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
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:
|
||||||
|
|
||||||
## Legacy GLMT Compatibility Flow
|
- CLI flags override other sources where supported;
|
||||||
|
- environment can select a remote host;
|
||||||
|
- configuration supplies persistent remote settings;
|
||||||
|
- HTTPS and HTTP defaults remain protocol-aware;
|
||||||
|
- `--remote-only` disables local fallback; and
|
||||||
|
- an unreachable remote proxy falls back only when fallback is enabled.
|
||||||
|
|
||||||
### Overview
|
A remote proxy is a separate trust boundary. Its operator can receive provider
|
||||||
|
traffic and owns its stored OAuth state. Use TLS and authentication appropriate
|
||||||
|
to that boundary.
|
||||||
|
|
||||||
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.
|
## Authentication
|
||||||
|
|
||||||
```
|
CCS asks the selected CLIProxy backend to start the provider's supported auth
|
||||||
+===========================================================================+
|
flow. The provider registry identifies whether that is an authorization-code
|
||||||
| Legacy GLMT Compatibility + Internal Transforms |
|
flow, device-code flow, browser polling flow, or currently unsupported account
|
||||||
+===========================================================================+
|
linking.
|
||||||
|
|
||||||
Claude CLI
|
```text
|
||||||
|
Select canonical provider
|
||||||
|
|
|
|
||||||
| legacy glmt settings detected
|
Validate backend and auth-start support
|
||||||
v
|
|
||||||
+------------------+
|
|
||||||
| Compatibility | normalizeDeprecatedGlmtEnv()
|
|
||||||
| Layer | (src/utils/glmt-deprecation.ts)
|
|
||||||
+------------------+
|
|
||||||
|
|
|
|
||||||
v
|
Start backend-owned auth flow
|
||||||
+------------------+
|
|
||||||
| Direct GLM API | https://api.z.ai/api/anthropic
|
|
||||||
+------------------+
|
|
||||||
|
|
|
|
||||||
v
|
User completes provider interaction
|
||||||
+------------------+
|
|
|
||||||
| src/glmt/* | retained for Cursor translation
|
Backend writes provider auth file
|
||||||
+------------------+
|
|
|
||||||
|
CCS reconciles account identity and readiness
|
||||||
|
|
|
||||||
|
Launch through provider route
|
||||||
```
|
```
|
||||||
|
|
||||||
### Supported Migration Targets
|
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.
|
||||||
|
|
||||||
| Provider | Config Key | Endpoint | Auth |
|
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
|
||||||
| Z.AI (GLM) | `glm` | https://api.z.ai/api/anthropic | API key |
|
collapsing accounts by display text. The dashboard can show a safe variant
|
||||||
| Kimi API | `km` | https://api.kimi.com/coding/ | API key |
|
label without exposing the internal identifier.
|
||||||
| 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.
|
## API-key profiles
|
||||||
|
|
||||||
### Runtime Handling
|
API-key profiles store string-valued environment under the CCS profile
|
||||||
|
settings file.
|
||||||
|
|
||||||
CCS detects the deprecated `glmt` profile name and normalizes legacy proxy-only settings before dispatching through the normal settings-profile flow:
|
```text
|
||||||
|
Create or edit API profile
|
||||||
```typescript
|
|
|
||||||
if (isDeprecatedGlmtProfileName(profileName)) {
|
Write CCS-owned <profile>.settings.json
|
||||||
const normalized = normalizeDeprecatedGlmtEnv(settingsEnv);
|
|
|
||||||
// warn user, validate against direct GLM endpoint, continue through settings flow
|
Resolve direct/native vs compatible proxy-style auth
|
||||||
}
|
|
|
||||||
|
Target adapter prepares credentials
|
||||||
|
|
|
||||||
|
Spawn target
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
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.
|
||||||
|
|
||||||
## Remote CLIProxy Flow (v7.1)
|
Normal launches do not write `~/.claude/settings.json`. Users who want shared
|
||||||
|
Claude settings must select the explicit `ccs persist` workflow.
|
||||||
|
|
||||||
### Overview
|
## Legacy GLMT compatibility
|
||||||
|
|
||||||
Remote CLIProxy enables CCS to delegate authentication to a central proxy server instead of spawning a local binary.
|
`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.
|
||||||
|
|
||||||
```
|
New configuration should use current API profile commands and provider names.
|
||||||
+===========================================================================+
|
|
||||||
| Remote CLIProxy Architecture (v7.1) |
|
|
||||||
+===========================================================================+
|
|
||||||
|
|
||||||
Config Resolution (proxy-config-resolver.ts)
|
## Quota and account-pool flow
|
||||||
|
|
||||||
|
Quota support is provider-specific and intentionally sourced from
|
||||||
|
[`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
|
||||||
|
|
|
|
||||||
+---> Priority: CLI flags > ENV vars > config.yaml > defaults
|
Resolve supported provider backend
|
||||||
|
|
|
|
||||||
v
|
Provision managed MCP/runtime configuration
|
||||||
+------------------+
|
|
||||||
| ResolvedProxyConfig |
|
|
||||||
| mode: local|remote |
|
|
||||||
+------------------+
|
|
||||||
|
|
|
|
||||||
+---> [mode = local] ---> Spawn local CLIProxyAPI binary
|
Target invokes ImageAnalysis
|
||||||
| |
|
|
||||||
| v
|
|
||||||
| localhost:8317
|
|
||||||
|
|
|
|
||||||
+---> [mode = remote] ---> Connect to remote server
|
CCS sends provider-scoped request
|
||||||
|
|
|
|
||||||
v
|
Return text result
|
||||||
+------------------+
|
|
||||||
| Health Check | remote-proxy-client.ts
|
|
||||||
| /v1/models | 2s timeout
|
|
||||||
+------------------+
|
|
||||||
|
|
|
||||||
+---> [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
|
The managed route must not expose its internal credential in logs or user
|
||||||
|
settings. If managed preparation, authentication, or proxy readiness is
|
||||||
|
unavailable, CCS falls back to compatible native behavior where possible
|
||||||
|
instead of failing the entire target launch.
|
||||||
|
|
||||||
```typescript
|
Provider-specific vision mappings are implementation data and should be read
|
||||||
// proxy-config-resolver.ts: Priority order
|
from the image-analysis routing services and tests.
|
||||||
const resolved = {
|
|
||||||
...DEFAULT_CONFIG, // 4. Defaults (lowest)
|
|
||||||
...yamlConfig, // 3. config.yaml
|
|
||||||
...envConfig, // 2. Environment variables
|
|
||||||
...cliFlags, // 1. CLI flags (highest)
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
### Health Check
|
## Session and observability boundary
|
||||||
|
|
||||||
```typescript
|
Execution paths can record non-secret metadata such as profile identity,
|
||||||
// remote-proxy-client.ts
|
profile type, canonical provider, target type, timestamps, duration, and
|
||||||
async function checkRemoteProxyHealth(config: ResolvedProxyConfig): Promise<boolean> {
|
numeric process exit status. Logs must not contain provider tokens, API keys,
|
||||||
try {
|
raw auth payloads, or personal account identifiers.
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
Textual CCS log codes and numeric child-process exit statuses are different
|
||||||
|
contracts. See [Logging Contract](../logging-contract.md).
|
||||||
|
|
||||||
## Quota Management Flow (v7.14)
|
## Invariants
|
||||||
|
|
||||||
### Overview
|
- Canonical provider identity is registry-driven.
|
||||||
|
- Provider aliases normalize before execution.
|
||||||
|
- Backend restrictions are validated before starting local auth or routing.
|
||||||
|
- Remote-only mode never starts a local fallback.
|
||||||
|
- Persistent environment values are strings.
|
||||||
|
- Account display text is not a unique runtime identity.
|
||||||
|
- Manual pauses are never auto-resumed by quota management.
|
||||||
|
- Provider credentials never appear in architecture examples.
|
||||||
|
|
||||||
Hybrid quota management enables automatic detection of exhausted accounts and failover to next available account.
|
## Related documentation
|
||||||
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.
|
|
||||||
|
|
||||||
```
|
- [System Architecture](./index.md)
|
||||||
+===========================================================================+
|
- [Target Adapters](./target-adapters.md)
|
||||||
| Quota Management Architecture (v7.14) |
|
- [Logging Contract](../logging-contract.md)
|
||||||
+===========================================================================+
|
- [Codebase Summary](../codebase-summary.md)
|
||||||
|
- [Code Standards](../code-standards.md)
|
||||||
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
|
|
||||||
@@ -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
|
|
||||||
Reference in new issue
Block a user