mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-11 12:09:03 +00:00
Merge pull request #1668 from kaitranntt/kai/docs/1665-architecture-contracts
docs: reconcile architecture and engineering contracts
This commit is contained in:
8 files changed
+1099
-2479
No files matched your search
+20
-9
@@ -1,7 +1,5 @@
|
||||
# Dashboard i18n Guide
|
||||
|
||||
Last Updated: 2026-04-14
|
||||
|
||||
This document describes the internationalization (i18n) architecture used by the CCS Dashboard (`ui/`), how locale selection works, and how to add new languages safely.
|
||||
|
||||
---
|
||||
@@ -15,6 +13,7 @@ Dashboard i18n currently covers UI text rendered by React components.
|
||||
- `zh-CN` (Simplified Chinese)
|
||||
- `vi` (Vietnamese)
|
||||
- `ja` (Japanese)
|
||||
- `ko` (Korean)
|
||||
- Locale state is persisted in browser localStorage using `ccs-ui-locale`.
|
||||
- Fallback language is `en`.
|
||||
|
||||
@@ -40,6 +39,7 @@ Out of scope:
|
||||
- `zh-CN.translation`
|
||||
- `vi.translation`
|
||||
- `ja.translation`
|
||||
- `ko.translation`
|
||||
- Uses `initReactI18next` for React integration.
|
||||
|
||||
### Locale utilities
|
||||
@@ -59,8 +59,11 @@ Out of scope:
|
||||
### Test bootstrap
|
||||
|
||||
- File: `ui/tests/setup/vitest-setup.ts`
|
||||
- Test setup must import `ui/src/lib/i18n.ts` so direct `useTranslation()` consumers resolve the same singleton instance as the app.
|
||||
- If a test mocks `useTranslation()`, keep the mocked key surface aligned with the component output or the assertions will drift.
|
||||
- Test setup must import `ui/src/lib/i18n.ts` so components using the
|
||||
react-i18next translation hook resolve the same singleton instance as the
|
||||
app.
|
||||
- If a test mocks the translation hook, keep the mocked key surface aligned
|
||||
with the component output or the assertions will drift.
|
||||
|
||||
---
|
||||
|
||||
@@ -104,11 +107,8 @@ When adding a locale such as Thai (`th`):
|
||||
5. Run UI validation and i18n tests:
|
||||
- `cd ui && bun run validate`
|
||||
- `cd ui && bun run test:run tests/unit/ui/i18n/language-switcher.test.tsx`
|
||||
6. Add or update key-parity tests to catch locale drift.
|
||||
|
||||
Current issue driving the Vietnamese rollout:
|
||||
|
||||
- https://github.com/kaitranntt/ccs/issues/659
|
||||
6. Keep the key-parity test passing. It compares every non-English resource
|
||||
against English and verifies interpolation placeholders match.
|
||||
|
||||
---
|
||||
|
||||
@@ -121,3 +121,14 @@ Before opening a PR that touches i18n:
|
||||
- [ ] No unsafe HTML injection path introduced for translated content.
|
||||
- [ ] `ui` validate/test commands pass.
|
||||
- [ ] This document is updated if architecture or conventions changed.
|
||||
|
||||
## References
|
||||
|
||||
- [`ui/src/lib/locales.ts`](../ui/src/lib/locales.ts) - supported locale ids,
|
||||
normalization, persistence, and formatting locale
|
||||
- [`ui/src/lib/i18n.ts`](../ui/src/lib/i18n.ts) - translation resources and
|
||||
i18next initialization
|
||||
- [`ui/src/components/layout/language-switcher.tsx`](../ui/src/components/layout/language-switcher.tsx)
|
||||
- locale selection UI
|
||||
- [`ui/tests/unit/ui/i18n/language-switcher.test.tsx`](../ui/tests/unit/ui/i18n/language-switcher.test.tsx)
|
||||
- selection, persistence, key-parity, and placeholder coverage
|
||||
+102
-141
@@ -1,178 +1,139 @@
|
||||
# Logging Contract
|
||||
|
||||
Single source of truth for structured backend logging in CCS CLI. Companion to GitHub issues #1138 (umbrella) and #1141 (backend instrumentation).
|
||||
CCS structured logs are a machine-readable JSONL channel for backend and
|
||||
runtime events. They are separate from terminal UX output and must never be
|
||||
used as a substitute for user-facing recovery messages.
|
||||
|
||||
## Overview
|
||||
The canonical schema is
|
||||
[`src/services/logging/log-types.ts`](../src/services/logging/log-types.ts).
|
||||
Defaults are defined in
|
||||
[`src/config/schemas/logging.ts`](../src/config/schemas/logging.ts).
|
||||
|
||||
CCS emits structured JSONL log entries for backend behavior (proxy daemons, OAuth flows, target spawn lifecycle, executor errors, etc.). This document defines the canonical schema, request-correlation pattern, lifecycle stages, and redaction policy.
|
||||
## Entry Schema
|
||||
|
||||
> CLI text output (`ok / info / warn / fail` from `src/utils/ui.ts`) is **NOT** affected by this contract. Logs are a separate channel — never printed to stdout/stderr.
|
||||
| Field | Type | Required | Contract |
|
||||
| --- | --- | --- | --- |
|
||||
| `id` | `string` | yes | Unique entry id. |
|
||||
| `timestamp` | `string` | yes | ISO 8601 emission time. |
|
||||
| `level` | `error`, `warn`, `info`, or `debug` | yes | Severity. |
|
||||
| `source` | `string` | yes | Stable module-scoped producer id. |
|
||||
| `event` | `string` | yes | Stable machine-readable event name. |
|
||||
| `message` | `string` | yes | Short human-readable summary. |
|
||||
| `processId` | `number` | yes | Emitting process id. |
|
||||
| `runId` | `string` | yes | Stable for the current process. |
|
||||
| `context` | object | no | Event fields after configured redaction. |
|
||||
| `requestId` | `string` | no | Cross-stage correlation id. |
|
||||
| `stage` | `LogStage` | no | Canonical lifecycle stage. |
|
||||
| `latencyMs` | `number` | no | Elapsed milliseconds, normally at completion. |
|
||||
| `error` | `LogErrorInfo` | no | Structured error metadata. |
|
||||
|
||||
## Schema (`LogEntry`)
|
||||
|
||||
Defined in `src/services/logging/log-types.ts`.
|
||||
|
||||
| Field | Type | Required | Notes |
|
||||
|-------|------|----------|-------|
|
||||
| `id` | `string` | yes | UUID per entry. |
|
||||
| `timestamp` | `string` | yes | ISO 8601. |
|
||||
| `level` | `'error'\|'warn'\|'info'\|'debug'` | yes | |
|
||||
| `source` | `string` | yes | Module-scoped identifier (e.g. `proxy:openai-compat:messages`). |
|
||||
| `event` | `string` | yes | Dotted machine-readable event name (e.g. `request.received`). |
|
||||
| `message` | `string` | yes | Human-readable summary. |
|
||||
| `processId` | `number` | yes | `process.pid`. |
|
||||
| `runId` | `string` | yes | Stable per-process id. |
|
||||
| `context` | `object` | no | Free-form structured fields (redacted). |
|
||||
| `requestId` | `string` | no | Correlates entries belonging to one inbound request across stages. |
|
||||
| `stage` | `LogStage` | no | Lifecycle stage tag. |
|
||||
| `latencyMs` | `number` | no | Elapsed ms (typically on `respond` / `cleanup`). |
|
||||
| `error` | `{name, message, code?, stack?}` | no | Structured error metadata; never raw token strings. |
|
||||
|
||||
Old free-form entries (no `requestId` / `stage`) are still valid; new fields are additive.
|
||||
|
||||
### Example
|
||||
|
||||
```jsonl
|
||||
{"id":"...","timestamp":"2026-04-30T12:34:56.000Z","level":"info","source":"proxy:openai-compat:messages","event":"request.received","message":"Proxy /v1/messages request received","processId":42,"runId":"r1","requestId":"a1b2...","stage":"intake","context":{"method":"POST"}}
|
||||
```
|
||||
Additive optional fields preserve compatibility with older readers. Consumers
|
||||
must not assume every event has a stage, request id, latency, or structured
|
||||
error.
|
||||
|
||||
## Lifecycle Stages
|
||||
|
||||
`LogStage` is one of:
|
||||
Use `logger.stage()` for events that map to the canonical lifecycle:
|
||||
|
||||
| Stage | When to emit |
|
||||
|-------|--------------|
|
||||
| `intake` | Inbound request received at an entry edge (HTTP handler, CLI dispatch). |
|
||||
| `route` | Destination/profile/target resolution. |
|
||||
| `auth` | Authentication / authorization (token exchange, profile auth). |
|
||||
| `dispatch` | Outbound request prepared / child process spawned. |
|
||||
| `upstream` | Upstream call in flight (provider HTTP / spawned child running). |
|
||||
| `transform` | Payload translation (request/response shape conversion). |
|
||||
| `respond` | Response written / dispatched (`latencyMs` typically populated). |
|
||||
| `cleanup` | Error path, abort, teardown. |
|
||||
| Stage | Meaning |
|
||||
| --- | --- |
|
||||
| `intake` | Request or command entered a CCS boundary. |
|
||||
| `route` | Profile, provider, target, or destination resolution. |
|
||||
| `auth` | Authentication or authorization work. |
|
||||
| `dispatch` | Outbound request or child launch prepared. |
|
||||
| `upstream` | Provider request or child operation in flight. |
|
||||
| `transform` | Request or response translation. |
|
||||
| `respond` | Result dispatched to the caller. |
|
||||
| `cleanup` | Failure, abort, or teardown path. |
|
||||
|
||||
Stages may be skipped or repeated. Streaming responses tag `upstream` only at start/end (NOT per chunk).
|
||||
Stages can be skipped or repeated. Use `logger.info()`, `warn()`, or `error()`
|
||||
for events that do not represent a lifecycle stage. High-volume details,
|
||||
including streaming chunk metrics, belong at `debug`.
|
||||
|
||||
## RequestId Propagation (AsyncLocalStorage)
|
||||
|
||||
`requestId` is propagated implicitly via Node `AsyncLocalStorage`. Entry edges wrap their handler in `withRequestContext`; every `createLogger`-emitted entry inside the context auto-merges `requestId` from the active store.
|
||||
Example:
|
||||
|
||||
```ts
|
||||
import { withRequestContext, createLogger } from './services/logging';
|
||||
|
||||
const logger = createLogger('proxy:my-edge');
|
||||
|
||||
http.createServer((req, res) => {
|
||||
const requestId = req.headers['x-ccs-request-id'] ?? randomUUID();
|
||||
res.setHeader('x-ccs-request-id', requestId);
|
||||
withRequestContext({ requestId }, async () => {
|
||||
logger.stage('intake', 'request.received', 'inbound');
|
||||
// ... downstream work emits with the same requestId
|
||||
});
|
||||
logger.stage('respond', 'request.completed', 'Request completed', {
|
||||
statusCode: 200,
|
||||
}, {
|
||||
latencyMs: 42,
|
||||
});
|
||||
```
|
||||
|
||||
### Cross-daemon header
|
||||
See [`src/services/logging/logger.ts`](../src/services/logging/logger.ts) for the
|
||||
compiler-checked signature.
|
||||
|
||||
`x-ccs-request-id` round-trips across the proxy edge:
|
||||
- Inbound: if the header is present and matches the UUID-ish guard (`/^[A-Za-z0-9._-]{8,128}$/`), it is reused; otherwise a fresh UUID is minted.
|
||||
- Outbound (response): the resolved id is echoed back via `res.setHeader('x-ccs-request-id', ...)`.
|
||||
- When CCS calls another daemon (copilot, cursor, glmt), forward the active id in the same header so that daemon can correlate.
|
||||
## Request Correlation
|
||||
|
||||
### Ordering guarantee
|
||||
[`src/services/logging/log-context.ts`](../src/services/logging/log-context.ts)
|
||||
uses Node async-local storage within a process. Entry edges establish a
|
||||
context; loggers created downstream read its `requestId` automatically.
|
||||
|
||||
Emit-time ordering of entries within a single `requestId` is monotonic — the active context is single-threaded relative to the request, so `timestamp` ordering reflects emit order. The UI layer (#1142) consumes this guarantee.
|
||||
Async-local context does not cross child processes or worker threads. A child
|
||||
process can inherit the active id through `CCS_REQUEST_ID` and establish a new
|
||||
local context. HTTP edges may use `x-ccs-request-id`; each edge owns whether it
|
||||
accepts an incoming id or mints a new one.
|
||||
|
||||
### What NOT to put in the context
|
||||
Only the characters and length accepted by `REQUEST_ID_PATTERN` are valid for
|
||||
forwarded ids. Do not treat request ids as authentication or authorization
|
||||
material.
|
||||
|
||||
The ALS context object is mixed into every downstream entry. Never store:
|
||||
- Raw tokens, API keys, refresh tokens, OAuth codes
|
||||
- Raw request/response bodies
|
||||
- User-supplied secrets
|
||||
There is no global ordering guarantee across concurrent async work or processes.
|
||||
Use `timestamp`, stage, event, and process/run identifiers together when
|
||||
reconstructing a request.
|
||||
|
||||
Only benign correlation metadata: `requestId`, `method`, `path`, `command`, `profile`.
|
||||
## Redaction and Data Minimization
|
||||
|
||||
### Worker threads / spawned children
|
||||
[`src/services/logging/log-redaction.ts`](../src/services/logging/log-redaction.ts)
|
||||
is the implementation source of truth. With the default `logging.redact: true`,
|
||||
the logger:
|
||||
|
||||
ALS context is **not** inherited by worker threads or `child_process.spawn` stdio pipes. At those boundaries, mint a fresh `requestId` at the child entry and pass the parent id explicitly via env var or header for correlation.
|
||||
- replaces values under known credential-bearing keys;
|
||||
- masks common authorization schemes and credential token shapes in strings;
|
||||
- redacts sensitive CLI flag values, including inline assignments;
|
||||
- limits string length and nested object depth;
|
||||
- strips an `Error` object to safe structured fields.
|
||||
|
||||
## Redaction
|
||||
The matcher evolves as credential surfaces change. Link to the implementation
|
||||
instead of copying its complete key or token-pattern list into other docs.
|
||||
|
||||
`src/services/logging/log-redaction.ts` is the single source of truth.
|
||||
Redaction is defense in depth, not permission to log sensitive data. Never log:
|
||||
|
||||
### Sensitive key matcher
|
||||
- tokens, passwords, cookies, OAuth codes, or authorization values;
|
||||
- raw request or response bodies;
|
||||
- raw prompts or prompt-bearing CLI arguments;
|
||||
- personal identifiers when a non-identifying account or profile label works.
|
||||
|
||||
`SENSITIVE_KEY_PATTERN` matches (case-insensitive, with `_` / `-` / camelCase variants):
|
||||
`authorization`, `proxy-authorization`, `cookie`, `set-cookie`, `password`, `password_hash`, `secret`, `client_secret`, `token`, `auth_token`, `access_token`, `refresh_token`, `id_token`, `bearer`, `assertion`, `api_key`, `x-api-key`, `x-goog-api-key`, `management_key`, `copilot_token`, `cursor_session_key`, `oauth_code`, `auth_code`.
|
||||
When a new secret-bearing field or flag is introduced, update the redactor and
|
||||
its tests under
|
||||
[`tests/unit/services/logging/`](../tests/unit/services/logging/).
|
||||
|
||||
String/object values for matching keys are replaced with `[redacted]`. Numeric/boolean values pass through (e.g., `expires_at` epoch numbers stay readable).
|
||||
## Error Codes and Process Exit Codes
|
||||
|
||||
### Auth-scheme value masking
|
||||
These are separate contracts:
|
||||
|
||||
Raw string values whose prefix matches `^(Bearer|Basic|Token)\s+\S+` are rewritten to `<scheme> [redacted]` even when nested under non-sensitive keys.
|
||||
- `LogEntry.error.code` is an optional **string** supplied as part of structured
|
||||
error metadata. It can carry a runtime, system, or provider error identifier.
|
||||
- `CCSError.code` is a numeric `ExitCode`. The centralized CLI error handler
|
||||
writes it as `context.exitCode` and passes it to `process.exit`.
|
||||
|
||||
### Argv redaction
|
||||
Do not put a numeric process exit code in `LogEntry.error.code`, and do not make
|
||||
log consumers derive process status from that string field.
|
||||
|
||||
`redactArgv(argv)` redacts the value following any sensitive flag (`--token`, `--api-key`, `--auth`, `--bearer`, `--secret`, `--client-secret`, `--access-token`, `--refresh-token`, `--id-token`, `--password`).
|
||||
The numeric mapping lives in
|
||||
[`src/errors/exit-codes.ts`](../src/errors/exit-codes.ts). Typed error
|
||||
assignments live in
|
||||
[`src/errors/error-types.ts`](../src/errors/error-types.ts), and propagation is
|
||||
implemented by
|
||||
[`src/errors/error-handler.ts`](../src/errors/error-handler.ts).
|
||||
|
||||
### Adding new sensitive keys
|
||||
## Configuration
|
||||
|
||||
1. Extend `SENSITIVE_KEY_PATTERN` in `src/services/logging/log-redaction.ts`.
|
||||
2. Add a unit test in `tests/unit/services/logging/log-redaction-extended.test.ts`.
|
||||
3. Verify regex stays O(1) per key (no catastrophic backtracking).
|
||||
CCS-owned logging uses the `logging` section in the unified config. Its defaults
|
||||
enable logging and redaction at `info` level with bounded rotation, retention,
|
||||
and live-buffer settings. This is distinct from `cliproxy.logging`, which
|
||||
controls CLIProxy runtime logging.
|
||||
|
||||
## Contributor Guide
|
||||
|
||||
### When to use `logger.stage()` vs `logger.info()`
|
||||
|
||||
Use `stage()` whenever the entry corresponds to one of the canonical lifecycle stages — this is what observability tooling and the dashboard rely on. Use `info()` / `warn()` / `error()` for one-off events that don't fit a stage.
|
||||
|
||||
### What NOT to log
|
||||
|
||||
- Token values (use metadata: `expires_at`, `scopes`, account display name).
|
||||
- Request/response bodies (sample lengths only).
|
||||
- Authorization headers (log header *names* present, not values).
|
||||
|
||||
### Level guidance
|
||||
|
||||
| Level | Use for |
|
||||
|-------|---------|
|
||||
| `error` | Failures requiring action (cleanup stage). |
|
||||
| `warn` | Recoverable issues (auth rejected, route fallback). |
|
||||
| `info` | Lifecycle stage entries by default. |
|
||||
| `debug` | High-volume detail (per-chunk stream metrics, lock acquire/release). |
|
||||
|
||||
### Level config
|
||||
|
||||
Default level is `info`. Configure via `logging.level` in `~/.ccs/config.yaml`. Streaming providers MUST gate per-chunk metrics behind `debug`.
|
||||
|
||||
## `error.code` values (exit codes)
|
||||
|
||||
Typed errors (`src/errors/error-types.ts`) carry an `ExitCode` that `handleError` propagates to `process.exit`. Log readers can branch on `error.code` for differentiated handling. The full mapping lives in `src/errors/exit-codes.ts`; the per-class assignment:
|
||||
|
||||
| Typed class | ExitCode | Value |
|
||||
|---|---|---:|
|
||||
| `ConfigError` | `CONFIG_ERROR` | 2 |
|
||||
| `NetworkError` | `NETWORK_ERROR` | 3 (recoverable) |
|
||||
| `AuthError` | `AUTH_ERROR` | 4 |
|
||||
| `BinaryError` | `BINARY_ERROR` | 5 |
|
||||
| `ProviderError` | `PROVIDER_ERROR` | 6 (recoverable) |
|
||||
| `ProfileError` | `PROFILE_ERROR` | 7 |
|
||||
| `ProxyError` | `PROXY_ERROR` | 8 |
|
||||
| `MigrationError` | `MIGRATION_ERROR` | 9 |
|
||||
| `UserAbortError` | `USER_ABORT` | 130 |
|
||||
| `ValidationError`, `RetryableError` | `GENERAL_ERROR` | 1 |
|
||||
|
||||
New throws must use a typed class (enforced by `ccs/no-new-throw-error`, see `docs/code-standards.md`). Redaction scrubs credential token shapes in both context values and message strings, so routing errors into the logger is safe — but keep messages clean prose and put sensitive data in context under a sensitive key (auto-redacted).
|
||||
|
||||
## Backward Compatibility
|
||||
|
||||
- All new `LogEntry` fields (`requestId`, `stage`, `latencyMs`, `error`) are optional. Old readers ignore them.
|
||||
- Existing `console.*` UX prints in `src/commands/`, `src/utils/ui.ts`, and similar user-facing paths are intentionally **not** converted to logger.
|
||||
- `/api/logs` reader unchanged in this PR; UI surfacing of new fields tracked under #1142.
|
||||
|
||||
## Future Work
|
||||
|
||||
- UI surfacing of `requestId` / `stage` / `latencyMs` in the dashboard (#1142).
|
||||
- `ccs logs` CLI improvements (filter by `requestId` / `stage`).
|
||||
- Per-stage performance budgets (see #1071).
|
||||
When adding a logging setting, update the schema/defaults, configuration loader,
|
||||
dashboard surface if applicable, and focused logging tests. Do not document a
|
||||
default that is not present in `DEFAULT_LOGGING_CONFIG`.
|
||||
+194
-314
@@ -1,374 +1,254 @@
|
||||
# CCS Product Development Requirements (PDR)
|
||||
|
||||
Last Updated: 2026-05-07
|
||||
# CCS Product Development Requirements
|
||||
|
||||
## Product Overview
|
||||
|
||||
**Product Name**: CCS (Claude Codex Switch)
|
||||
**Product name:** CCS (Claude Codex Switch)
|
||||
|
||||
**Tagline**: The multi-provider profile and runtime manager for Claude Code and compatible CLIs
|
||||
**Purpose:** Provide one profile and runtime-management surface for Claude Code,
|
||||
Codex CLI, Factory Droid, CLIProxy-backed OAuth providers, and compatible API
|
||||
profiles.
|
||||
|
||||
**Description**: Multi-provider CLI/runtime manager enabling seamless switching between multiple Claude accounts, OAuth/API providers, and alternate targets such as Claude Code, Factory Droid, and Codex CLI. Includes a React-based dashboard for configuration management, plus support for local and remote CLIProxyAPI instances, hybrid quota management, and official Claude channel runtime setup for Telegram, Discord, and iMessage.
|
||||
CCS includes:
|
||||
|
||||
**Current Version**: v7.34.x+ (First-class ImageAnalysis MCP tooling, WebSearch MCP, performance improvements)
|
||||
- a TypeScript CLI and local server;
|
||||
- a React dashboard for configuration, account, health, and usage workflows;
|
||||
- isolated account contexts and per-profile settings;
|
||||
- local or remote CLIProxy routing;
|
||||
- optional managed tools such as WebSearch and image analysis; and
|
||||
- an integrated Docker image containing CCS, CLIProxy, and the dashboard.
|
||||
|
||||
---
|
||||
Release versions and completed-release inventories belong in
|
||||
[`CHANGELOG.md`](../CHANGELOG.md), not this evergreen requirements document.
|
||||
|
||||
## Problem Statement
|
||||
## Problem
|
||||
|
||||
Developers using Claude Code face these challenges:
|
||||
Developers need to switch between accounts, providers, and compatible CLIs
|
||||
without repeatedly editing credentials or allowing one session's configuration
|
||||
to leak into another. They also need a visible, reversible way to manage local
|
||||
proxy state and diagnose provider readiness.
|
||||
|
||||
1. **Single Account Limitation**: Cannot run multiple Claude subscriptions simultaneously
|
||||
2. **Provider Lock-in**: Stuck with Anthropic's API, cannot use alternatives
|
||||
3. **No Concurrent Sessions**: Cannot work on different projects with different accounts
|
||||
4. **Complex Configuration**: Manual env var and config file management
|
||||
5. **No Usage Analytics**: Lack visibility into token usage and costs across providers
|
||||
## Product Principles
|
||||
|
||||
---
|
||||
1. **CLI first:** Core configuration and launch behavior remains scriptable.
|
||||
2. **Explicit state:** Users can inspect which profile, provider, target, and
|
||||
proxy mode CCS selected.
|
||||
3. **Isolation:** Account sessions use separate configuration roots where the
|
||||
target supports them.
|
||||
4. **Compatibility:** CCS adapts provider credentials to a target without
|
||||
redefining the provider or target protocol.
|
||||
5. **Reversibility:** Persistent writes are explicit and recoverable.
|
||||
6. **Local ownership:** Credentials and profile state remain on infrastructure
|
||||
selected by the user.
|
||||
|
||||
## Solution
|
||||
## Users
|
||||
|
||||
CCS provides:
|
||||
|
||||
1. **Multi-Account Claude**: Isolated instances via `CLAUDE_CONFIG_DIR`
|
||||
2. **OAuth Providers**: Zero-config Gemini, Codex, xAI/Grok, Antigravity, Kiro, and other active OAuth integrations, with deprecated Copilot compatibility for existing setups
|
||||
3. **AI Providers**: Dedicated CLIProxy dashboard for Gemini, Codex, Claude, Vertex, and OpenAI-compatible API-key families
|
||||
4. **API Profiles**: GLM, Kimi, OpenRouter, any Anthropic-compatible API
|
||||
5. **Visual Dashboard**: React SPA for configuration management
|
||||
6. **Automatic WebSearch**: First-class local WebSearch tool with deterministic provider chain for third-party providers
|
||||
7. **Automatic Image Analysis**: First-class local ImageAnalysis tool with direct provider routing for third-party profiles
|
||||
8. **Usage Analytics**: Token tracking, cost analysis, model breakdown
|
||||
9. **Official Claude Channels**: Runtime auto-enable plus dashboard token/config flow for Telegram, Discord, and macOS-only iMessage
|
||||
10. **Routing Strategy Guidance**: First-class `round-robin` vs `fill-first` controls in CLI and dashboard, with explicit opt-in changes and no account-based guessing
|
||||
|
||||
---
|
||||
|
||||
## Target Users
|
||||
|
||||
| User Type | Use Case | Primary Features |
|
||||
|-----------|----------|------------------|
|
||||
| Individual Developer | Work/personal separation | Multi-account Claude |
|
||||
| Agency/Contractor | Client account isolation | Profile switching |
|
||||
| Cost-conscious Dev | GLM for bulk operations | API profiles, analytics |
|
||||
| Enterprise | Custom LLM integration | OpenAI-compatible endpoints |
|
||||
| Power User | Multiple providers | OpenRouter 300+ models |
|
||||
|
||||
---
|
||||
| User | Primary need |
|
||||
| --- | --- |
|
||||
| Individual developer | Separate accounts, projects, and provider profiles |
|
||||
| Consultant or agency | Isolate client contexts |
|
||||
| API consumer | Reuse Anthropic-compatible and OpenAI-compatible providers |
|
||||
| Power user | Manage OAuth accounts, routing, health, and quota state |
|
||||
| Team operator | Run a shared remote CLIProxy or integrated Docker service |
|
||||
|
||||
## Functional Requirements
|
||||
|
||||
### FR-001: Profile Switching
|
||||
- Switch between profiles with `ccs <profile>` command
|
||||
- Support default profile when no argument provided
|
||||
- Pass through all Claude CLI arguments
|
||||
### FR-001: Profile resolution and launch
|
||||
|
||||
### FR-002: Multi-Account Claude
|
||||
- Create isolated Claude instances
|
||||
- Maintain separate sessions, todolists, logs per account
|
||||
- Share commands, skills, agents across accounts
|
||||
- Launch the default profile with `ccs`.
|
||||
- Launch named settings, account, and CLIProxy profiles.
|
||||
- Pass target-specific arguments without losing CCS-owned routing constraints.
|
||||
- Resolve provider and target aliases through canonical registries.
|
||||
|
||||
### FR-003: OAuth Provider Integration
|
||||
- Support Gemini, Codex, xAI/Grok, Antigravity, Kiro, and deprecated Copilot compatibility OAuth flows
|
||||
- Browser-based authentication with provider-specific Authorization Code, Device Code, or polling flows
|
||||
- Token caching and refresh
|
||||
### FR-002: Account isolation
|
||||
|
||||
### FR-004: API Profile Management
|
||||
- Configure custom API endpoints
|
||||
- Support Anthropic-compatible APIs
|
||||
- Model mapping and configuration
|
||||
- OpenRouter integration with 300+ models
|
||||
- Maintain an account registry.
|
||||
- Use isolated `CLAUDE_CONFIG_DIR` roots for Claude account profiles.
|
||||
- Keep shared resources and instance-owned state distinguishable.
|
||||
- Prevent one account's provider overrides from leaking into another launch.
|
||||
|
||||
### FR-004A: CLIProxy AI Provider Management
|
||||
- Configure CLIProxy-managed Gemini, Codex, Claude, Vertex, and OpenAI-compatible API-key entries
|
||||
- Keep provider authoring separate from CCS API Profile creation
|
||||
- Support local config editing and remote CLIProxy management parity where available
|
||||
### FR-003: Provider integration
|
||||
|
||||
### FR-005: Dashboard UI
|
||||
- Visual profile management
|
||||
- Real-time health monitoring
|
||||
- Usage analytics with cost tracking
|
||||
- Modular page architecture (settings, analytics, auth-monitor)
|
||||
- Support API-key profiles and OAuth-backed CLIProxy providers.
|
||||
- Support local and remote CLIProxy operation.
|
||||
- Keep original and Plus CLIProxy backends explicit; do not silently substitute
|
||||
one when a requested provider requires the other.
|
||||
- Treat provider capability metadata as code-owned, not prose-owned. See
|
||||
[`src/cliproxy/provider-capabilities.ts`](../src/cliproxy/provider-capabilities.ts)
|
||||
and
|
||||
[`src/cliproxy/types/provider-types.ts`](../src/cliproxy/types/provider-types.ts).
|
||||
|
||||
### FR-006: Health Diagnostics
|
||||
- Verify Claude CLI installation
|
||||
- Check config file integrity
|
||||
- Validate symlinks and permissions
|
||||
### FR-004: Target adapters
|
||||
|
||||
### FR-007: WebSearch Fallback
|
||||
- Expose a CCS-managed local WebSearch tool for third-party profiles that cannot reach Anthropic's native tool
|
||||
- Suppress native `WebSearch` on third-party launches and steer Claude toward the CCS-owned path when it is available
|
||||
- Support Exa, Tavily, Brave, and DuckDuckGo real search backends
|
||||
- Keep Gemini CLI, OpenCode, and Grok as optional legacy fallback
|
||||
- Graceful fallback chain
|
||||
- Support Claude Code, Factory Droid, and Codex CLI through target adapters.
|
||||
- Deliver credentials in the form owned by each target:
|
||||
environment variables for Claude launches, managed custom-model state for
|
||||
Droid, and transient configuration overrides for CCS-routed Codex launches.
|
||||
- Preserve user-owned target configuration outside the explicitly managed
|
||||
fields.
|
||||
|
||||
### FR-007A: First-Class Image Analysis
|
||||
- Expose a CCS-managed local `ImageAnalysis` MCP tool for third-party profiles that need provider-backed vision
|
||||
- Resolve the provider route before launch and send requests directly to `/api/provider/<backend>/v1/messages`
|
||||
- Use editable prompt templates for `default`, `screenshot`, and `document` analysis modes
|
||||
- Suppress the old CCS-managed `Read` hook during healthy MCP launches so it cannot compete with the primary path
|
||||
- Keep the old `Read` hook as compatibility fallback only when MCP provisioning fails but provider-backed analysis is still viable
|
||||
- Auto-heal stale CCS-managed image hooks and missing isolated MCP sync through launch-time cleanup, dashboard provisioning, and `ccs doctor --fix`
|
||||
- Fall back to native `Read` without failing the whole launch when managed runtime, auth, or proxy readiness is unavailable
|
||||
### FR-005: Configuration management
|
||||
|
||||
### FR-008: Remote CLIProxy Support
|
||||
- Connect to remote CLIProxyAPI instances
|
||||
- CLI flags for proxy configuration (--proxy-host, --proxy-port, etc.)
|
||||
- Environment variable configuration (CCS_PROXY_HOST, etc.)
|
||||
- Fallback to local proxy when remote unreachable
|
||||
- Protocol-based default ports (443 for HTTPS, 8317 for HTTP)
|
||||
- Dashboard UI for remote server configuration and testing
|
||||
- Store CCS configuration under the directory resolved by
|
||||
[`getCcsDir()`](../src/utils/config-manager.ts).
|
||||
- Store API profile launch settings in `<profile>.settings.json` files under
|
||||
that directory.
|
||||
- Require all environment values written to settings files to be strings.
|
||||
- Keep shared Claude settings unchanged during normal profile launches.
|
||||
- Allow the explicit `ccs persist` workflow to merge a profile into
|
||||
`~/.claude/settings.json`; back up the existing file and write atomically.
|
||||
- Reject unsafe settings-file targets such as symlinks.
|
||||
|
||||
### FR-009: Quota Management (v7.14)
|
||||
- Pause/resume individual accounts via `ccs cliproxy pause/resume <account>`
|
||||
- Check quota status via `ccs cliproxy status [account]`
|
||||
- Inspect the current proxy-wide routing strategy via `ccs cliproxy routing`
|
||||
- Explicitly switch `round-robin` vs `fill-first` from CLI or dashboard
|
||||
- Keep `round-robin` as the default until the user explicitly changes it
|
||||
- Never infer routing strategy from account count, tier mix, or paused/default account state
|
||||
- Auto-failover when account exhausted
|
||||
- Tier detection: free/pro/ultra/unknown
|
||||
- Distinguish entitlement failures from temporary capacity exhaustion
|
||||
- Pre-flight quota checks before session start
|
||||
- Dashboard UI with pause/resume toggles, tier badges, and quota-detail guidance
|
||||
### FR-006: Dashboard and diagnostics
|
||||
|
||||
### FR-010: Docker Deployment
|
||||
- Multi-stage Dockerfile with bun 1.2.21 and node:20-bookworm-slim
|
||||
- Docker Compose setup with resource limits and healthcheck
|
||||
- Persistent volumes for config, credentials, and CLI tools
|
||||
- Pre-installed CLIs: claude, gemini, grok, opencode, ccs
|
||||
- Ports: 3000 (Dashboard), 8317 (CLIProxy)
|
||||
- Entrypoint with privilege dropping and usage help
|
||||
- Environment variable configuration support
|
||||
- Provide local APIs and a React UI for supported configuration workflows.
|
||||
- Surface health, authentication, provider, routing, and usage state without
|
||||
exposing credentials.
|
||||
- Keep dashboard changes aligned with the same configuration contracts used by
|
||||
the CLI.
|
||||
|
||||
### FR-011: Third-Party Tool Integration
|
||||
- Export shell-evaluable env vars via `ccs env` command
|
||||
- Support OpenAI, Anthropic, raw output formats
|
||||
- Auto-detect shell (bash/zsh, fish, PowerShell) from $SHELL
|
||||
- Security: single-quoted output, key sanitization, shell-specific escaping
|
||||
- Cross-platform compatibility (macOS, Linux, Windows)
|
||||
### FR-007: Managed tools
|
||||
|
||||
### FR-012: Official Claude Channels
|
||||
- Support Telegram, Discord, and iMessage selection via `ccs config channels` and the dashboard
|
||||
- Auto-inject `--channels` only for native Claude `default` and `account` sessions
|
||||
- Store Telegram/Discord bot tokens in Claude's own `~/.claude/channels/<channel>/.env` state or the official `*_STATE_DIR` override path when one is configured
|
||||
- Treat iMessage as macOS-only, tokenless, and dependent on Claude-side install plus OS permissions
|
||||
- Require Bun, Claude Code v2.1.80+, and verified `claude.ai` auth before runtime auto-enable
|
||||
- Keep `--dangerously-skip-permissions` optional and never add it when the user already made an explicit permission choice
|
||||
- Surface platform/auth/version/setup blockers clearly in both CLI and dashboard flows
|
||||
- Preserve dashboard token drafts when save/refresh fails, and let already-selected unsupported iMessage entries be turned off without allowing re-enable on unsupported platforms
|
||||
- Provide WebSearch and image-analysis integration for profiles that need
|
||||
CCS-managed alternatives.
|
||||
- Prefer explicit provider routes.
|
||||
- Fail closed when enabled third-party WebSearch cannot prepare its constrained
|
||||
MCP replacement.
|
||||
- Allow image analysis to use compatible native behavior when its managed route
|
||||
is unavailable.
|
||||
|
||||
---
|
||||
### FR-008: Remote proxy
|
||||
|
||||
- Resolve remote proxy settings from supported CLI flags, environment
|
||||
variables, and CCS configuration.
|
||||
- Verify reachability before use.
|
||||
- Fall back to a local proxy only when fallback is enabled.
|
||||
- Fail instead of falling back when remote-only mode is selected.
|
||||
|
||||
### FR-009: Quota and account-pool management
|
||||
|
||||
- Display supported quota and account health data.
|
||||
- Let users pause and resume accounts.
|
||||
- Keep routing-strategy changes explicit.
|
||||
- Temporarily remove exhausted accounts from rotation only under the
|
||||
CCS-managed cooldown contract and restore only CCS-created pauses.
|
||||
|
||||
### FR-010: Docker deployment
|
||||
|
||||
- Publish an integrated multi-architecture image containing CCS, CLIProxy, and
|
||||
the dashboard.
|
||||
- Persist CCS state and service logs in declared volumes.
|
||||
- Expose dashboard and CLIProxy service ports.
|
||||
- Health-check both services.
|
||||
- Do not bundle target AI CLIs into the integrated image; consumers that need
|
||||
them run sibling containers or install them separately.
|
||||
|
||||
### FR-011: Shell and editor integration
|
||||
|
||||
- Export shell-safe environment values through `ccs env`.
|
||||
- Support the documented shell output formats.
|
||||
- Keep persistent shared settings behind `ccs persist`.
|
||||
- Keep editor-specific writes scoped to the selected editor and user layer.
|
||||
|
||||
## Non-Functional Requirements
|
||||
|
||||
### NFR-001: Performance
|
||||
- CLI startup < 100ms
|
||||
- Dashboard load < 2s
|
||||
- Minimal memory footprint
|
||||
### NFR-001: Security
|
||||
|
||||
- Do not print or log credentials.
|
||||
- Bind local proxy services to loopback unless the user explicitly selects a
|
||||
deployment that exposes them.
|
||||
- Validate file types and use safe replacement semantics for managed sensitive
|
||||
configuration.
|
||||
- Keep remote transport and authentication choices explicit.
|
||||
|
||||
### NFR-002: Reliability
|
||||
- Idempotent operations
|
||||
- Graceful error handling
|
||||
- Automatic recovery where possible
|
||||
|
||||
### NFR-003: Security
|
||||
- Local-only proxy binding (127.0.0.1)
|
||||
- No credential exposure in logs
|
||||
- Secure token storage
|
||||
- Make setup and repair operations idempotent where practical.
|
||||
- Preserve existing user state when merging managed configuration.
|
||||
- Report recovery actions in actionable error messages.
|
||||
- Clean up child processes and temporary runtime state on exit.
|
||||
|
||||
### NFR-004: Cross-Platform
|
||||
- Support Linux, macOS, Windows
|
||||
- Bash 3.2+, PowerShell 5.1+, Node.js 14+
|
||||
- Identical behavior across platforms
|
||||
### NFR-003: Portability
|
||||
|
||||
### NFR-005: Maintainability
|
||||
- Files < 200 lines (with documented exceptions)
|
||||
- Domain-based organization
|
||||
- Barrel exports for clean imports
|
||||
- 90%+ test coverage
|
||||
- Support macOS, Linux, and Windows for the host CLI where target dependencies
|
||||
allow.
|
||||
- Support Node.js 18 or newer, as declared by
|
||||
[`package.json`](../package.json).
|
||||
- Support Bun 1.0 or newer for development and supported runtime workflows.
|
||||
- Keep terminal output ASCII-only and respect `NO_COLOR` and TTY detection.
|
||||
|
||||
---
|
||||
### NFR-004: Maintainability
|
||||
|
||||
## Technical Requirements
|
||||
- Keep provider metadata centralized.
|
||||
- Use target adapters instead of target checks scattered through dispatch code.
|
||||
- Validate CLI, server, and dashboard contracts with focused tests.
|
||||
- Keep generated or volatile inventories out of evergreen architecture prose.
|
||||
|
||||
### TR-001: Runtime Dependencies
|
||||
- Node.js 14+ or Bun 1.0+
|
||||
- Claude Code CLI installed
|
||||
- Internet access for OAuth/API calls
|
||||
## Runtime and Deployment Requirements
|
||||
|
||||
### TR-002: Optional Dependencies
|
||||
- CLIProxyAPI binary (auto-managed)
|
||||
- Exa/Tavily/Brave API keys for higher-quality WebSearch
|
||||
- Gemini CLI for legacy WebSearch fallback
|
||||
- Bun plus Claude Code v2.1.80+ with `claude.ai` auth for Official Channels auto-enable
|
||||
|
||||
### TR-003: Configuration
|
||||
- YAML-based config (`~/.ccs/config.yaml`)
|
||||
- JSON settings per profile
|
||||
- Environment variable overrides
|
||||
- Official channel bot tokens stored in Claude-managed `~/.claude/channels/<channel>/.env`
|
||||
|
||||
---
|
||||
| Surface | Requirement |
|
||||
| --- | --- |
|
||||
| Host npm install | Node.js 18+ |
|
||||
| Development and repository gates | Bun 1.0+ and Node.js 18+ |
|
||||
| Claude launches | Claude Code installed and authenticated as required by the selected profile |
|
||||
| Droid launches | Factory Droid installed |
|
||||
| Codex launches | Codex CLI installed |
|
||||
| Local OAuth proxy | CCS-managed CLIProxy binary |
|
||||
| Integrated Docker | Docker or compatible container runtime; target CLIs are not bundled |
|
||||
|
||||
## Architecture Constraints
|
||||
|
||||
### AC-001: CLI-First Design
|
||||
- All features accessible via CLI
|
||||
- Dashboard is convenience layer, not required
|
||||
- Scriptable and automatable
|
||||
### AC-001: Profile state and shared settings are separate
|
||||
|
||||
### AC-002: Non-Invasive
|
||||
- Never modify `~/.claude/settings.json`
|
||||
- Use environment variables for configuration
|
||||
- Reversible changes only
|
||||
Normal launches consume CCS-owned profile files and environment state.
|
||||
`~/.claude/settings.json` is written only through an explicit persistence or
|
||||
approved settings-management workflow.
|
||||
|
||||
### AC-003: Proxy Pattern
|
||||
- Use local proxy for provider routing
|
||||
- Claude CLI communicates with localhost
|
||||
- Proxy handles upstream API calls
|
||||
### AC-002: Provider and target are independent axes
|
||||
|
||||
---
|
||||
A provider supplies credentials and routing. A target adapter determines how a
|
||||
compatible CLI receives them. Unsupported combinations must fail clearly.
|
||||
|
||||
## Success Metrics
|
||||
### AC-003: Proxy trust boundary is visible
|
||||
|
||||
| Metric | Target | Current |
|
||||
|--------|--------|---------|
|
||||
| Startup time | < 100ms | Achieved |
|
||||
| Dashboard load | < 2s | Achieved |
|
||||
| Error rate | < 1% | Achieved |
|
||||
| Test coverage | > 90% | 90% (1440 tests, 6 skipped) |
|
||||
| File size compliance | 100% < 200 lines | 95% |
|
||||
Local CLIProxy, remote CLIProxy, and direct API profiles have different
|
||||
transport and credential boundaries. CCS must not present them as equivalent or
|
||||
silently cross those boundaries.
|
||||
|
||||
---
|
||||
### AC-004: Source owns volatile capability data
|
||||
|
||||
## Release Criteria
|
||||
Provider IDs, aliases, OAuth flow types, callback ports, refresh ownership,
|
||||
backend restrictions, and quota support must be read from the provider
|
||||
registries and tests. Documentation describes how to find them rather than
|
||||
copying a second mutable table.
|
||||
|
||||
### v1.0 Release (Complete)
|
||||
- [x] Multi-account Claude support
|
||||
- [x] OAuth provider integration (Gemini, Codex, AGY)
|
||||
- [x] API profile management
|
||||
- [x] Dashboard UI
|
||||
- [x] Health diagnostics
|
||||
- [x] WebSearch fallback
|
||||
- [x] Cross-platform support
|
||||
## Acceptance Criteria
|
||||
|
||||
### v7.0 Release (Complete)
|
||||
- [x] OpenRouter integration with 300+ models
|
||||
- [x] Interactive model picker
|
||||
- [x] Dynamic model discovery
|
||||
- [x] Tier mapping (opus/sonnet/haiku)
|
||||
- [x] Settings page modularization (20 files)
|
||||
- [x] Analytics page modularization (8 files)
|
||||
- [x] Auth monitor modularization (8 files)
|
||||
- [x] Comprehensive test infrastructure (539 CLI + 99 UI tests)
|
||||
|
||||
### v7.1 Release (Complete)
|
||||
- [x] Remote CLIProxy routing support
|
||||
- [x] CLI flags for remote proxy (--proxy-host, --proxy-port, etc.)
|
||||
- [x] Environment variables for proxy config (CCS_PROXY_*)
|
||||
- [x] Dashboard remote proxy configuration UI
|
||||
- [x] Connection testing with latency display
|
||||
- [x] Fallback to local when remote unreachable
|
||||
- [x] Protocol-based default ports (HTTPS:443, HTTP:8317)
|
||||
|
||||
### v7.2 Release (Complete)
|
||||
- [x] Kiro (AWS) OAuth provider support via CLIProxyAPIPlus
|
||||
- [x] GitHub Copilot (ghcp) OAuth provider via Device Code flow (deprecated compatibility)
|
||||
- [x] Authorization Code flow for Kiro (port 9876)
|
||||
- [x] Device Code flow for ghcp (no local port needed)
|
||||
|
||||
### v7.14 Release (Complete)
|
||||
- [x] Hybrid quota management with auto-failover
|
||||
- [x] `ccs cliproxy pause/resume/status` commands
|
||||
- [x] API tier detection (free/pro/ultra/unknown)
|
||||
- [x] Dashboard pause/resume toggles and tier badges
|
||||
- [x] Pre-flight quota checks before session start
|
||||
|
||||
### v7.23 Release (Complete)
|
||||
- [x] Docker deployment support (PR #345)
|
||||
- [x] Multi-stage Dockerfile with bun 1.2.21
|
||||
- [x] Docker Compose with resource limits and healthcheck
|
||||
- [x] Persistent volumes for config and credentials
|
||||
- [x] Pre-installed AI CLI tools (claude, gemini, grok, opencode)
|
||||
- [x] Entrypoint with privilege dropping
|
||||
|
||||
### v7.34 Release (Complete)
|
||||
- [x] First-class `ImageAnalysis` MCP tool for third-party launches
|
||||
- [x] Direct provider-scoped routing for image analysis requests
|
||||
- [x] Prompt template selection for default / screenshot / document flows
|
||||
- [x] Hook fallback retained only for compatibility
|
||||
- [x] Non-fatal native `Read` fallback when managed runtime is unavailable
|
||||
- [x] `ccs config image-analysis` CLI command
|
||||
- [x] Doctor integration for hook validation
|
||||
- [x] 791-line E2E test suite for image analysis
|
||||
- [x] Performance: Replace busy-wait with Atomics.wait in config lock
|
||||
- [x] Network error handling with noRetryPatterns
|
||||
- [x] Quota 429 rate limit handling improvements
|
||||
- [x] WebSocket maxPayload limit (DoS prevention)
|
||||
|
||||
### v7.39 Release (Complete)
|
||||
- [x] `ccs env` command for third-party tool integration (OpenCode, Cursor, Continue)
|
||||
- [x] Multi-format output: openai, anthropic, raw
|
||||
- [x] Multi-shell support: bash/zsh, fish, PowerShell (auto-detected)
|
||||
- [x] CLIProxy profile support (gemini, codex, agy, qwen)
|
||||
- [x] Settings profile support (glm, kimi, custom API)
|
||||
- [x] Security: single-quoted output, key sanitization, shell-specific escaping
|
||||
- [x] Shell completion updated (bash, zsh, fish, PowerShell)
|
||||
- [x] 34 unit tests for env command
|
||||
|
||||
### v8.0 Release (Planned - Q1 2026)
|
||||
- [ ] Multiple CLIProxyAPI instances (load balancing, failover)
|
||||
- [ ] Native git worktree support
|
||||
- [ ] Critical bug fixes (#158, #155, #124)
|
||||
|
||||
### v9.0 Release (Future - Q2 2026)
|
||||
- [ ] Team collaboration features
|
||||
- [ ] Cloud sync for profiles
|
||||
- [ ] Plugin system
|
||||
- [ ] CLI extension framework
|
||||
|
||||
---
|
||||
|
||||
## Dependencies
|
||||
|
||||
### External Services
|
||||
- Anthropic Claude API
|
||||
- Google Gemini API
|
||||
- GitHub Codex API
|
||||
- GitHub Copilot (ghcp - deprecated Device Code OAuth compatibility)
|
||||
- AWS Kiro (Authorization Code OAuth)
|
||||
- Z.AI GLM API
|
||||
- OpenRouter API
|
||||
- Moonshot Kimi API
|
||||
- DeepSeek API
|
||||
- Alibaba Qwen API
|
||||
- Minimax API
|
||||
- Azure Foundry API
|
||||
|
||||
### Third-Party Libraries
|
||||
- Express.js (web server)
|
||||
- React (dashboard)
|
||||
- Vite (build tool)
|
||||
- shadcn/ui (UI components)
|
||||
- CLIProxyAPI (proxy binary)
|
||||
- Vitest (testing)
|
||||
|
||||
---
|
||||
- A user can create or select a supported profile and launch it on a compatible
|
||||
target without manual credential-file editing.
|
||||
- Concurrent account profiles do not share target session state accidentally.
|
||||
- Local and remote proxy failures follow the configured fallback policy.
|
||||
- Persistent settings writes preserve unrelated keys and create a recovery
|
||||
path.
|
||||
- Dashboard operations produce configuration compatible with CLI operations.
|
||||
- Release automation publishes only from the documented branches and lanes.
|
||||
- Documentation links resolve and architecture claims are traceable to source.
|
||||
|
||||
## Risks and Mitigations
|
||||
|
||||
| Risk | Probability | Impact | Mitigation |
|
||||
|------|-------------|--------|------------|
|
||||
| Claude CLI API changes | Medium | High | Version pinning, compatibility layer |
|
||||
| Provider API deprecation | Low | High | Fallback chain, multiple providers |
|
||||
| OAuth token expiry | Medium | Medium | Auto-refresh, clear error messages |
|
||||
| Binary compatibility | Low | Medium | Multi-platform builds, fallback |
|
||||
|
||||
---
|
||||
| Risk | Mitigation |
|
||||
| --- | --- |
|
||||
| Provider auth contracts change | Central capability registry plus provider-specific tests |
|
||||
| Target CLI configuration changes | Adapter boundary and compatibility checks |
|
||||
| Credential leakage | Local storage, redaction, safe file handling, no secret logging |
|
||||
| Remote proxy outage | Reachability checks and explicit fallback policy |
|
||||
| Configuration corruption | Validation, backup, locking, and atomic replacement where supported |
|
||||
| Documentation drift | Link volatile details to code; keep this document version-neutral |
|
||||
|
||||
## Related Documentation
|
||||
|
||||
- [Codebase Summary](./codebase-summary.md) - Technical structure
|
||||
- [Code Standards](./code-standards.md) - Development conventions
|
||||
- [System Architecture](./system-architecture/index.md) - Architecture diagrams
|
||||
- [Project Roadmap](./project-roadmap.md) - Development phases and GitHub issues
|
||||
- [Codebase Summary](./codebase-summary.md)
|
||||
- [Code Standards](./code-standards.md)
|
||||
- [System Architecture](./system-architecture/index.md)
|
||||
- [Provider Flows](./system-architecture/provider-flows.md)
|
||||
- [Release Process](./release-process.md)
|
||||
- [Project Roadmap](./project-roadmap.md)
|
||||
+90
-79
@@ -1,106 +1,117 @@
|
||||
# CCS Release Process
|
||||
|
||||
CCS uses a decoupled release model: every merge to `main` immediately publishes
|
||||
a stable npm `@latest` release and an immutable Docker `:<ver>` tag. Docker
|
||||
mutable tags (`:latest`, `:<MAJOR>`, `:<MINOR>`) require a separate manual
|
||||
promote step after an operator-verified soak window. This decouples the npm
|
||||
ecosystem from the Docker stability gate.
|
||||
CCS has separate development, stable npm, and Docker promotion lanes. A branch
|
||||
push starts the relevant workflow. Eligible `dev` pushes publish the next custom
|
||||
development prerelease after their gates pass; `main` publishes only when
|
||||
semantic-release finds release-worthy commits.
|
||||
|
||||
## Phase 1 — Automatic stable release (on every merge to `main`)
|
||||
## Release lanes
|
||||
|
||||
1. A PR is merged into `main` with a conventional commit (`feat:`, `fix:`, etc.).
|
||||
2. `release.yml` triggers semantic-release, which reads `.releaserc.cjs`.
|
||||
3. Because `main` is a stable channel, semantic-release cuts a GitHub release
|
||||
tagged `vX.Y.Z` and publishes the npm package to the `@latest` dist-tag
|
||||
immediately. No rc channel, no soak delay on npm.
|
||||
4. `docker-release.yml` triggers on the `release: published` event and:
|
||||
- Validates the tag as stable semver (`vX.Y.Z`).
|
||||
- Builds the integrated image for `linux/amd64` and `linux/arm64`.
|
||||
- Pushes **only the immutable** `ghcr.io/kaitranntt/ccs:X.Y.Z` tag.
|
||||
- Signs the image with cosign (keyless OIDC).
|
||||
- Runs smoke tests (`smoke-test` job).
|
||||
- Mutable tags (`:latest`, `:<MAJOR>`, `:<MINOR>`) are **not** added at
|
||||
this stage — `promote-mutable-tags` only runs on explicit
|
||||
`workflow_dispatch` with `promote_to_latest=true`.
|
||||
| Source | Workflow | Result |
|
||||
| --- | --- | --- |
|
||||
| Push to `dev` | [`dev-release.yml`](../.github/workflows/dev-release.yml) | Custom development prerelease and npm `@dev` publication |
|
||||
| Push to `main` | [`release.yml`](../.github/workflows/release.yml) | Semantic-release stable version, npm `@latest`, tag, and GitHub release when commits require a release |
|
||||
| Published stable or `rc` GitHub release | [`docker-release.yml`](../.github/workflows/docker-release.yml) | Immutable integrated Docker version tag, signature, and smoke test |
|
||||
| Manual stable promotion | [`promote-release.yml`](../.github/workflows/promote-release.yml) | Docker `:latest`, major, and minor aliases |
|
||||
| Stable GitHub release | [`sync-dev-after-release.yml`](../.github/workflows/sync-dev-after-release.yml) | Merge released `main` state back into `dev` |
|
||||
|
||||
## Phase 2 — Manual promotion to Docker mutable tags (rc.1 soak window)
|
||||
## Development prereleases
|
||||
|
||||
After the immutable `:<ver>` Docker image has soaked (typically 24 h with no
|
||||
reported issues), the operator promotes mutable tags:
|
||||
`Dev Release` runs on pushes to `dev` and can also be dispatched manually.
|
||||
After build and validation gates, it calls
|
||||
[`scripts/dev-release.sh`](../scripts/dev-release.sh). That script owns the
|
||||
`<stable>-dev.<n>` version sequence and npm `@dev` publication. It is
|
||||
intentionally separate from the production semantic-release configuration.
|
||||
|
||||
1. Verify the immutable image is healthy:
|
||||
Generated `chore(release): ...` pushes are skipped by the workflow guard to
|
||||
prevent release recursion.
|
||||
|
||||
```bash
|
||||
docker pull ghcr.io/kaitranntt/ccs:X.Y.Z
|
||||
docker run --rm -p 3000:3000 -p 8317:8317 ghcr.io/kaitranntt/ccs:X.Y.Z
|
||||
# check http://localhost:3000 and http://localhost:8317
|
||||
```
|
||||
## Stable npm and GitHub releases
|
||||
|
||||
2. Optionally verify the cosign signature:
|
||||
`Release` runs on `main`. It builds the CLI and dashboard, runs the fast, slow,
|
||||
and end-to-end gates, then invokes semantic-release with
|
||||
[`.releaserc.cjs`](../.releaserc.cjs).
|
||||
|
||||
```bash
|
||||
cosign verify \
|
||||
--certificate-identity-regexp "https://github.com/kaitranntt/ccs/.github/workflows/docker-release.yml" \
|
||||
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
|
||||
ghcr.io/kaitranntt/ccs:X.Y.Z
|
||||
```
|
||||
Semantic-release analyzes commits since the previous stable release:
|
||||
|
||||
3. Run the `promote-release` workflow via GitHub Actions UI or CLI:
|
||||
- `feat` produces at least a minor release;
|
||||
- `fix`, `hotfix`, `refactor`, and `style` produce patch releases under the
|
||||
repository rules;
|
||||
- breaking-change notation produces the appropriate major release; and
|
||||
- commits without a matching release rule may produce no release.
|
||||
|
||||
```bash
|
||||
gh workflow run promote-release.yml \
|
||||
--field tag=vX.Y.Z
|
||||
```
|
||||
When a release is required, the lane updates `CHANGELOG.md` and `package.json`,
|
||||
publishes npm `@latest`, creates the stable Git tag and GitHub release, and
|
||||
pushes the generated release commit to `main`. Do not bump versions or create
|
||||
release tags manually.
|
||||
|
||||
This dispatches `docker-release.yml` with `promote_to_latest=true`, which
|
||||
triggers the `promote-mutable-tags` job to add `:latest`, `:<MAJOR>`, and
|
||||
`:<MINOR>` via `docker buildx imagetools create`.
|
||||
## Docker publication and promotion
|
||||
|
||||
Alternatively, dispatch `docker-release.yml` directly:
|
||||
The supported integrated image is `ghcr.io/kaitranntt/ccs`.
|
||||
|
||||
```bash
|
||||
gh workflow run "Publish Docker Image" \
|
||||
--field tag=vX.Y.Z \
|
||||
--field promote_to_latest=true
|
||||
```
|
||||
On a published stable `vX.Y.Z` or release-candidate `vX.Y.Z-rc.N` GitHub
|
||||
release, `Publish Docker Image`:
|
||||
|
||||
## Why npm and Docker have different soak windows
|
||||
1. validates the release tag;
|
||||
2. checks out that tag;
|
||||
3. builds the integrated image for `linux/amd64` and `linux/arm64`;
|
||||
4. publishes only the matching immutable version tag;
|
||||
5. signs the image digest with keyless cosign; and
|
||||
6. smoke-tests the published image.
|
||||
|
||||
- **npm `@latest`**: Published immediately on every `main` merge. npm users who
|
||||
pin a version are unaffected; users who run `npm install -g @kaitranntt/ccs`
|
||||
get the latest immediately. Rollback is `npm install -g @kaitranntt/ccs@X.Y.Z`.
|
||||
- **Docker `:latest`**: Promoted only after operator confirmation. Users who
|
||||
pull `:latest` or run `docker pull` without a pinned tag are shielded from
|
||||
a bad image. The immutable `:<ver>` tag is always available for pinned usage
|
||||
from the moment of release.
|
||||
|
||||
## Verifying the promotion
|
||||
Mutable aliases are a separate operator decision. After verifying the immutable
|
||||
image and allowing the desired soak period, dispatch `promote-release.yml`:
|
||||
|
||||
```bash
|
||||
# Confirm :latest points to the promoted digest
|
||||
docker buildx imagetools inspect ghcr.io/kaitranntt/ccs:latest
|
||||
gh workflow run promote-release.yml --field tag=vX.Y.Z
|
||||
```
|
||||
|
||||
# Confirm npm @latest updated (happens automatically at Phase 1)
|
||||
The promotion workflow verifies that the stable GitHub release and immutable
|
||||
image exist, then dispatches `docker-release.yml` with
|
||||
`promote_to_latest=true`. The promotion job creates `:latest`, `:X`, and
|
||||
`:X.Y` aliases from the immutable image digest.
|
||||
|
||||
The deprecated `ccs-dashboard` image has its own sunset compatibility job.
|
||||
Do not use its tag behavior as the contract for the supported integrated image.
|
||||
|
||||
## Post-release development sync
|
||||
|
||||
A published, non-prerelease `vX.Y.Z` release targeting `main` triggers
|
||||
`Sync Dev After Main Release`. The workflow merges `main` into `dev`, resolves
|
||||
known generated version-file conflicts in favor of the released `main` state,
|
||||
and pushes `dev`. That push intentionally triggers the normal `Push CI` and
|
||||
development-release lanes.
|
||||
|
||||
## Verification
|
||||
|
||||
```bash
|
||||
# npm channels
|
||||
npm view @kaitranntt/ccs dist-tags
|
||||
|
||||
# immutable integrated image
|
||||
docker buildx imagetools inspect ghcr.io/kaitranntt/ccs:X.Y.Z
|
||||
|
||||
# mutable alias after promotion
|
||||
docker buildx imagetools inspect ghcr.io/kaitranntt/ccs:latest
|
||||
```
|
||||
|
||||
## Rollback
|
||||
Verify the GitHub Actions run and tag point to the expected commit before
|
||||
announcing a release.
|
||||
|
||||
If a promoted release is found to be bad:
|
||||
## Recovery
|
||||
|
||||
```bash
|
||||
# Repoint :latest to the previous known-good immutable tag
|
||||
docker buildx imagetools create \
|
||||
--tag ghcr.io/kaitranntt/ccs:latest \
|
||||
ghcr.io/kaitranntt/ccs:PREVIOUS.VERSION
|
||||
- **Bad npm release:** publish a corrected patch. Do not unpublish a version
|
||||
used by downstream consumers.
|
||||
- **Bad immutable Docker image:** leave the immutable tag unchanged and publish
|
||||
a corrected version.
|
||||
- **Bad mutable Docker promotion:** promote a known-good immutable digest back
|
||||
to the mutable aliases through the controlled workflow.
|
||||
- **Failed `dev` sync:** repair the merge against current `main` and `dev`;
|
||||
never overwrite branch history.
|
||||
|
||||
# For npm, publish a fix as a new patch release (do not unpublish)
|
||||
# Unpublishing npm packages causes downstream breakage for pinned consumers.
|
||||
```
|
||||
## Branch and tag summary
|
||||
|
||||
## Branch / tag taxonomy
|
||||
|
||||
| Branch | Semantic-release channel | npm dist-tag | Docker tag (on release event) | Docker mutable (on promote) |
|
||||
|--------|--------------------------|--------------|-------------------------------|------------------------------|
|
||||
| `main` | stable | `@latest` | `:<ver>` (immutable, immediate) | `:latest`, `:<MAJOR>`, `:<MINOR>` (after soak) |
|
||||
| `dev` | `dev` prerelease | `@dev` | not published | not published |
|
||||
| Branch | Package channel | npm dist-tag | Integrated Docker |
|
||||
| --- | --- | --- | --- |
|
||||
| `dev` | Development prerelease | `@dev` | None |
|
||||
| `main` | Stable semantic release | `@latest` | Immutable tag on GitHub release; mutable aliases after manual promotion |
|
||||
+211
-430
@@ -1,465 +1,246 @@
|
||||
# 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
|
||||
|
||||
---
|
||||
|
||||
## 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)
|
||||
```text
|
||||
User or automation
|
||||
|
|
||||
v
|
||||
Target Resolution (--target flag > runtime entrypoint / argv[0] > config > default)
|
||||
|
|
||||
v
|
||||
Get Target Adapter (Claude, Droid, or Codex)
|
||||
|
|
||||
+---> detectBinary() (find CLI on system)
|
||||
|
|
||||
+---> prepareCredentials() (write config or set env)
|
||||
|
|
||||
+---> buildArgs() (construct CLI arguments)
|
||||
|
|
||||
+---> buildEnv() (prepare environment variables)
|
||||
|
|
||||
v
|
||||
Spawn Target Process
|
||||
CCS CLI ------------------------> Target CLI
|
||||
| |
|
||||
| profile/provider state | provider protocol
|
||||
v v
|
||||
CCS local server <-----------> CLIProxy or direct API
|
||||
|
|
||||
v
|
||||
React dashboard
|
||||
```
|
||||
|
||||
**Each target adapter implements different credential delivery:**
|
||||
The main implementation surfaces are:
|
||||
|
||||
- **Claude Adapter**: Env var delivery (existing behavior)
|
||||
- `ANTHROPIC_BASE_URL`, `ANTHROPIC_AUTH_TOKEN`, `ANTHROPIC_MODEL`
|
||||
- No config files needed
|
||||
| Surface | Ownership |
|
||||
| --- | --- |
|
||||
| `src/` | CLI, dispatch, server, provider integration, target adapters |
|
||||
| `ui/src/` | Dashboard application |
|
||||
| `dist/` and `dist/ui/` | Build outputs |
|
||||
| `docker/` | Integrated and legacy container definitions |
|
||||
| `tests/` | Unit, integration, end-to-end, native, and Docker contracts |
|
||||
|
||||
- **Droid Adapter**: Config file delivery to `~/.factory/settings.json`
|
||||
- Writes custom model entry: `custom:ccs-<profile>`
|
||||
- Spawns: `droid -m custom:ccs-<profile> <args>`
|
||||
- Model config includes baseUrl, apiKey, provider
|
||||
Dashboard localization is documented in the
|
||||
[Dashboard i18n Guide](../i18n-dashboard.md).
|
||||
|
||||
- **Codex Adapter**: Transient runtime overrides plus user-layer dashboard inspection
|
||||
- Uses `codex -c key=value` only for CCS-routed launches
|
||||
- Preserves native `~/.codex/config.toml` ownership
|
||||
- Dashboard page reads/writes only the user config layer with explicit runtime-vs-provider warnings
|
||||
## Execution pipeline
|
||||
|
||||
**Runtime entrypoints (built-in bins) and argv[0]-style aliases:**
|
||||
|
||||
```
|
||||
ccs → Target: claude (default)
|
||||
ccs-droid → Target: droid (explicit alias)
|
||||
ccsd → Target: droid (legacy shortcut)
|
||||
ccs-codex → Target: codex (explicit alias)
|
||||
ccsx → Target: codex (short alias)
|
||||
ccsxp → Target: codex (native cliproxy shortcut; prepends `--config model_provider="cliproxy"`)
|
||||
```
|
||||
|
||||
For details on the adapter architecture, see [Target Adapters](./target-adapters.md).
|
||||
|
||||
### CLI Layer
|
||||
|
||||
```
|
||||
+===========================================================================+
|
||||
| CLI Architecture |
|
||||
+===========================================================================+
|
||||
|
||||
User Input (ccs [--target <cli>] <profile> [args])
|
||||
|
|
||||
v
|
||||
+-------------+
|
||||
| ccs.ts | Entry point, command routing
|
||||
+-------------+
|
||||
|
|
||||
+---> [Version/Help/Doctor/etc.] ---> Exit
|
||||
|
|
||||
v
|
||||
+------------------+
|
||||
| Target Resolution | Determine which CLI to use
|
||||
+------------------+
|
||||
|
|
||||
v
|
||||
+-------------+
|
||||
| Profile | Determines execution path
|
||||
| Detection |
|
||||
+-------------+
|
||||
|
|
||||
+---> [Native Claude Account] ---> execClaude()
|
||||
| |
|
||||
+---> [CLIProxy Provider] ---> execClaudeWithCLIProxy()
|
||||
| |
|
||||
+---> [Settings/API Profile] ---> normalize legacy glmt if needed
|
||||
|
|
||||
v
|
||||
+------------------+
|
||||
| Target Adapter | Get appropriate adapter
|
||||
+------------------+
|
||||
|
|
||||
v
|
||||
+------------------+
|
||||
| Prepare Creds | Deliver credentials
|
||||
+------------------+
|
||||
|
|
||||
v
|
||||
+------------------+
|
||||
| Target CLI | Claude Code or Droid
|
||||
+------------------+
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Data Flow Architecture
|
||||
|
||||
### CLI Execution Flow
|
||||
|
||||
```
|
||||
+===========================================================================+
|
||||
| CLI Execution Flow |
|
||||
+===========================================================================+
|
||||
|
||||
1. Parse Arguments
|
||||
|
|
||||
v
|
||||
2. Resolve Target Type
|
||||
|
|
||||
v
|
||||
3. Detect Profile Type
|
||||
|
|
||||
+---> Native Claude ---> 3a. Load Account Settings
|
||||
| |
|
||||
| v
|
||||
| 4a. Set CLAUDE_CONFIG_DIR
|
||||
| |
|
||||
| v
|
||||
| 5a. Get Claude Target Adapter
|
||||
|
|
||||
+---> CLIProxy -------> 3b. Ensure Binary Installed
|
||||
| |
|
||||
| v
|
||||
| 4b. Generate Config
|
||||
| |
|
||||
| v
|
||||
| 5b. Resolve Target Adapter
|
||||
| |
|
||||
| v
|
||||
| 6b. Prepare Credentials
|
||||
| |
|
||||
| v
|
||||
| 7b. Spawn via Adapter
|
||||
|
|
||||
+---> Settings/API ---> 3c. Load settings env
|
||||
|
|
||||
v
|
||||
4c. Normalize legacy glmt if needed
|
||||
|
|
||||
v
|
||||
5c. Resolve Target Adapter
|
||||
|
|
||||
v
|
||||
6c. Spawn via Adapter
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Provider Integration Architecture
|
||||
|
||||
For detailed provider flows (CLIProxyAPI, legacy GLMT compatibility, quota management), see [Provider Flows](./provider-flows.md).
|
||||
|
||||
---
|
||||
|
||||
## Configuration Architecture
|
||||
|
||||
### CCS Logging Architecture
|
||||
|
||||
- Shared logging contract lives in `src/services/logging/` and is used for CCS-owned runtime diagnostics, request tracing, and bounded recent-entry reads.
|
||||
- Config lives at top-level `logging.*` in `~/.ccs/config.yaml`; `cliproxy.logging.*` still controls upstream CLIProxy runtime files only.
|
||||
- CCS-owned runtime logs write to `~/.ccs/logs/current.jsonl` and rotate into `~/.ccs/logs/archive/` based on policy.
|
||||
- Dashboard exposure uses native `/api/logs/config`, `/api/logs/sources`, and `/api/logs/entries` endpoints plus the `System -> Logs` React page.
|
||||
- Request logging explicitly skips `/api/logs` reads so the log viewer does not recursively log itself.
|
||||
|
||||
### Config File Hierarchy
|
||||
|
||||
```
|
||||
+===========================================================================+
|
||||
| Configuration Hierarchy |
|
||||
+===========================================================================+
|
||||
|
||||
~/.ccs/
|
||||
```text
|
||||
Parse command
|
||||
|
|
||||
+---> 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/
|
||||
| |
|
||||
| +---> config.yaml # CLIProxy configuration
|
||||
| +---> auth/ # OAuth tokens
|
||||
| +---> bin/ # CLIProxy binary
|
||||
Prepare target credentials
|
||||
|
|
||||
+---> shared/ # Symlinked resources
|
||||
|
|
||||
+---> commands/ # Claude Code commands
|
||||
+---> skills/ # Custom skills
|
||||
+---> agents/ # Agent configurations
|
||||
+---> plugins/
|
||||
|
|
||||
+---> cache/ # Shared plugin payload/cache data
|
||||
+---> marketplaces/ # Shared marketplace payload directories
|
||||
+---> installed_plugins.json
|
||||
|
||||
~/.ccs/instances/<profile>/
|
||||
|
|
||||
+---> plugins/
|
||||
|
|
||||
+---> known_marketplaces.json # Instance-local registry for active CLAUDE_CONFIG_DIR validation
|
||||
|
||||
~/.factory/ (Droid CLI)
|
||||
|
|
||||
+---> settings.json # Droid config (custom models)
|
||||
Spawn target and forward lifecycle signals
|
||||
```
|
||||
|
||||
Plugin ownership note:
|
||||
- `commands/`, `skills/`, `agents/`, and `settings.json` remain shared through the existing symlink/copy flow.
|
||||
- Marketplace payload directories stay shared, but `known_marketplaces.json` is reconciled per instance so Claude Code can validate `installLocation` against that instance's `CLAUDE_CONFIG_DIR/plugins/marketplaces`.
|
||||
Command routing stops before profile execution for management commands such as
|
||||
configuration, diagnostics, proxy management, and environment export.
|
||||
|
||||
### Config Loading Order
|
||||
### Profile resolution
|
||||
|
||||
```
|
||||
1. Environment Variables (highest priority)
|
||||
|
|
||||
v
|
||||
2. CLI Arguments (including --target)
|
||||
|
|
||||
v
|
||||
3. Profile-specific settings (~/.ccs/<profile>.settings.json)
|
||||
|
|
||||
v
|
||||
4. Main config (~/.ccs/config.yaml)
|
||||
|
|
||||
v
|
||||
5. Default values (lowest priority)
|
||||
Profile resolution distinguishes:
|
||||
|
||||
1. built-in CLIProxy provider shortcuts;
|
||||
2. user-defined CLIProxy profiles;
|
||||
3. settings/API profiles; and
|
||||
4. registered account profiles.
|
||||
|
||||
Canonical provider IDs and aliases come from
|
||||
[`src/cliproxy/provider-capabilities.ts`](../../src/cliproxy/provider-capabilities.ts).
|
||||
The detector and dispatcher consume those registries; documentation must not
|
||||
maintain a second provider list.
|
||||
|
||||
### Target resolution
|
||||
|
||||
Provider and target are independent axes. The selected target is resolved from
|
||||
explicit flags and runtime entry points before falling back to configuration
|
||||
and the default target.
|
||||
|
||||
Each adapter owns credential delivery:
|
||||
|
||||
- **Claude Code:** launch environment and optional isolated
|
||||
`CLAUDE_CONFIG_DIR`;
|
||||
- **Factory Droid:** CCS-managed custom-model entries in
|
||||
`~/.factory/settings.json`; and
|
||||
- **Codex CLI:** transient `-c` overrides for CCS-routed launches while native
|
||||
user configuration remains separately owned.
|
||||
|
||||
See [Target Adapters](./target-adapters.md) for the detailed compatibility
|
||||
contract.
|
||||
|
||||
## Provider routing
|
||||
|
||||
CCS supports three routing boundaries:
|
||||
|
||||
| Route | Credential and transport owner |
|
||||
| --- | --- |
|
||||
| Direct settings/API profile | Target receives the selected provider's environment |
|
||||
| Local CLIProxy | CCS manages a local proxy binary, config, and auth directory |
|
||||
| Remote CLIProxy | CCS connects to the configured remote service and applies the selected fallback policy |
|
||||
|
||||
Local backend choice is explicit. `original` is the default. `plus` is an
|
||||
opt-in backend for provider capabilities unavailable in the original backend.
|
||||
Compatibility restrictions are defined in
|
||||
[`src/cliproxy/types/provider-types.ts`](../../src/cliproxy/types/provider-types.ts)
|
||||
and enforced before local execution.
|
||||
|
||||
See [Provider Flows](./provider-flows.md).
|
||||
|
||||
## Configuration ownership
|
||||
|
||||
The effective CCS directory is resolved by
|
||||
[`src/utils/config-manager.ts`](../../src/utils/config-manager.ts). The normal
|
||||
default is `~/.ccs`; tests and scoped workflows can override it.
|
||||
|
||||
```text
|
||||
CCS directory
|
||||
├── config.yaml
|
||||
├── profiles.json
|
||||
├── <profile>.settings.json
|
||||
├── instances/
|
||||
├── logs/
|
||||
└── cliproxy/
|
||||
├── config.yaml
|
||||
├── auth/
|
||||
└── bin/
|
||||
```
|
||||
|
||||
---
|
||||
### Settings-write contract
|
||||
|
||||
## 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:
|
||||
|
||||
```
|
||||
+===========================================================================+
|
||||
| WebSocket Communication |
|
||||
+===========================================================================+
|
||||
- `ccs persist` reads and validates `~/.claude/settings.json`;
|
||||
- it refuses unsafe symlink targets;
|
||||
- it preserves unrelated settings while updating the requested managed fields;
|
||||
- it creates a backup when an existing file is present; and
|
||||
- it writes the replacement atomically under a settings-directory lock.
|
||||
|
||||
Dashboard (React) Server (Express)
|
||||
| |
|
||||
|<------ Connection Established ------>|
|
||||
| |
|
||||
|<------ health:update ----------------| Health status
|
||||
| |
|
||||
|<------ auth:status ------------------| Auth changes
|
||||
| |
|
||||
|<------ usage:update -----------------| Usage stats
|
||||
| |
|
||||
|------- action:refresh -------------->| User requests
|
||||
| |
|
||||
Target-owned writers follow their own boundary. For example, the Droid adapter
|
||||
manages CCS custom-model entries in `~/.factory/settings.json`, not arbitrary
|
||||
user settings.
|
||||
|
||||
## Local server and dashboard
|
||||
|
||||
The Express server exposes APIs used by the React dashboard for supported
|
||||
configuration, auth, usage, health, and logging workflows. The dashboard is a
|
||||
management surface over shared services; it must not implement a competing
|
||||
configuration model.
|
||||
|
||||
Real-time updates use server-owned WebSocket messages. Event names and payloads
|
||||
are code contracts and should be read from the server and UI implementations
|
||||
rather than copied into this overview.
|
||||
|
||||
## Logging
|
||||
|
||||
CCS-owned structured runtime logging lives under `src/services/logging/`.
|
||||
Top-level `logging.*` configuration controls CCS JSONL logs under the CCS
|
||||
directory. `cliproxy.logging.*` controls upstream CLIProxy files and is a
|
||||
separate contract.
|
||||
|
||||
The dashboard log reader excludes its own log-read requests from request
|
||||
logging to prevent recursive noise. See [Logging Contract](../logging-contract.md).
|
||||
|
||||
## Managed tool preparation
|
||||
|
||||
WebSearch and image analysis are prepared before the target launch when the
|
||||
selected profile needs CCS-managed tooling. They use provider-aware routes but
|
||||
have different failure contracts: enabled third-party WebSearch fails closed if
|
||||
its managed MCP replacement cannot be prepared, while image analysis can use a
|
||||
compatible native path when available.
|
||||
|
||||
- [WebSearch](../websearch.md)
|
||||
- [Provider Flows](./provider-flows.md)
|
||||
|
||||
## Security and trust boundaries
|
||||
|
||||
### Local host
|
||||
|
||||
CCS reads and writes user-authorized configuration and starts target processes.
|
||||
That local filesystem access is more privileged than a provider API request.
|
||||
Sensitive values must not enter logs or dashboard responses.
|
||||
|
||||
### Local proxy
|
||||
|
||||
The host CLI uses loopback for locally managed CLIProxy traffic. Local auth
|
||||
files and the management API remain sensitive even when the transport never
|
||||
leaves the machine.
|
||||
|
||||
### Remote proxy
|
||||
|
||||
A remote CLIProxy crosses a network and administrative boundary. TLS,
|
||||
authentication, certificate policy, reachability, and local fallback are
|
||||
explicit configuration choices. Remote-only mode must not silently start a
|
||||
local proxy.
|
||||
|
||||
### Container deployment
|
||||
|
||||
The integrated container exposes dashboard and proxy ports through the
|
||||
operator's port mappings. The image therefore does not inherit the host
|
||||
installation's loopback-only assumption. Network exposure and access control
|
||||
belong to the deployment operator.
|
||||
|
||||
## Build and distribution
|
||||
|
||||
```text
|
||||
src/ -------- TypeScript --------> dist/
|
||||
ui/src/ ----- Vite --------------> dist/ui/
|
||||
|
|
||||
v
|
||||
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.
|
||||
|
||||
```
|
||||
+------------------+
|
||||
| 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
|
||||
+------------------+
|
||||
```
|
||||
## Related documentation
|
||||
|
||||
---
|
||||
|
||||
## Build and Distribution
|
||||
|
||||
### Build Pipeline
|
||||
|
||||
```
|
||||
+===========================================================================+
|
||||
| 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
|
||||
- [Codebase Summary](../codebase-summary.md)
|
||||
- [Code Standards](../code-standards.md)
|
||||
- [Target Adapters](./target-adapters.md)
|
||||
- [Provider Flows](./provider-flows.md)
|
||||
- [Release Process](../release-process.md)
|
||||
- [Project Roadmap](../project-roadmap.md)
|
||||
@@ -1,641 +1,247 @@
|
||||
# Provider Integration Flows
|
||||
|
||||
Last Updated: 2026-07-16
|
||||
CCS routes profiles through direct target configuration or CLIProxy. This
|
||||
document describes the durable flow and trust boundaries. Mutable provider
|
||||
capabilities are linked to their source registries.
|
||||
|
||||
Detailed provider integration flows including CLIProxyAPI, legacy GLMT compatibility transforms, remote CLIProxy, quota management, and authentication.
|
||||
## Sources of truth
|
||||
|
||||
---
|
||||
| Detail | Authoritative source |
|
||||
| --- | --- |
|
||||
| Canonical provider IDs, aliases, auth-flow type, callback ports, refresh ownership | [`src/cliproxy/provider-capabilities.ts`](../../src/cliproxy/provider-capabilities.ts) |
|
||||
| Original vs Plus backend restrictions | [`src/cliproxy/types/provider-types.ts`](../../src/cliproxy/types/provider-types.ts) |
|
||||
| Provider/backend enforcement | [`src/cliproxy/services/variant-service.ts`](../../src/cliproxy/services/variant-service.ts) |
|
||||
| Remote proxy precedence and fallback | [`src/cliproxy/proxy/proxy-config-resolver.ts`](../../src/cliproxy/proxy/proxy-config-resolver.ts) |
|
||||
| Provider capability regression tests | [`src/cliproxy/__tests__/provider-capabilities.test.ts`](../../src/cliproxy/__tests__/provider-capabilities.test.ts) |
|
||||
|
||||
## CLIProxyAPI Flow
|
||||
Do not copy provider inventories, callback ports, or quota-supported lists into
|
||||
this document. They change independently of the architecture.
|
||||
|
||||
### Overview
|
||||
## Route selection
|
||||
|
||||
CLIProxyAPI is a local OAuth proxy binary that enables seamless integration with multiple AI providers. CCS manages the binary and configuration automatically.
|
||||
|
||||
### Local Backend Choice
|
||||
|
||||
CCS defaults to the original `router-for-me/CLIProxyAPI` backend because it is the stable MIT upstream. The `plus` backend is an explicit opt-in path that downloads the community-maintained `kaitranntt/CLIProxyAPIPlus` fork for providers that still require Plus-only support, such as Kiro, Cursor, GitLab, CodeBuddy, Kilo, and deprecated GitHub Copilot compatibility. CCS does not silently downgrade `backend: plus` to `original`; users choose that backend deliberately when they need those providers.
|
||||
|
||||
Generated local CLIProxy configs also keep the management dashboard aligned with the selected backend. `backend: original` uses upstream CPAMC (`router-for-me/Cli-Proxy-API-Management-Center`), while `backend: plus` uses the CCS-maintained dashboard fork (`kaitranntt/Cli-Proxy-API-Management-Center`). Advanced users can override the generated `remote-management.panel-github-repository` value by setting `cliproxy.management_panel_repository` in `~/.ccs/config.yaml`; CCS will regenerate stale local CLIProxy configs when the expected dashboard repository changes.
|
||||
|
||||
```
|
||||
+===========================================================================+
|
||||
| CLIProxyAPI Integration |
|
||||
+===========================================================================+
|
||||
|
||||
Claude CLI
|
||||
|
|
||||
| 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
|
||||
```text
|
||||
Resolved profile
|
||||
|
|
||||
+-- account profile
|
||||
| └── target-native authenticated context
|
||||
|
|
||||
+-- settings/API profile
|
||||
| └── direct or compatible upstream URL and credential environment
|
||||
|
|
||||
└-- CLIProxy provider
|
||||
|
|
||||
+-- reachable configured remote proxy
|
||||
|
|
||||
└-- local proxy, when selected or fallback is allowed
|
||||
|
|
||||
+---> Google (Gemini)
|
||||
+---> OpenAI (Codex)
|
||||
+---> xAI (Grok)
|
||||
+---> Antigravity (AGY)
|
||||
+---> AWS Kiro (Claude-powered)
|
||||
+---> GitHub Copilot (ghcp, deprecated compatibility)
|
||||
+---> OpenAI-compatible endpoints
|
||||
└── original or Plus backend
|
||||
```
|
||||
|
||||
### 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 |
|
||||
|----------|----|----|------|--------|
|
||||
| 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 |
|
||||
## Local CLIProxy
|
||||
|
||||
xAI uses CLIProxyAPI's `--xai-login` device flow. CCS stores and discovers the resulting
|
||||
`xai-*.json` credentials under the canonical `xai` provider. `ccs grok` is a command alias only,
|
||||
so it shares the same accounts, settings, live model catalog, and routing as `ccs xai`.
|
||||
CCS manages the local binary, generated configuration, auth files, lifecycle,
|
||||
and provider-specific launch environment.
|
||||
|
||||
### 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`
|
||||
- Variant keys are derived from the auth filename, for example `kaidu.kd@gmail.com#04a0f049-team` and `kaidu.kd@gmail.com#free`
|
||||
- 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
|
||||
The management-panel repository is generated to match the selected backend
|
||||
unless the user supplies the supported override.
|
||||
|
||||
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
|
||||
explicit CLI aliases before routing through the CLIProxy execution flow.
|
||||
Remote mode delegates proxy lifecycle and stored authentication to another
|
||||
CLIProxy installation.
|
||||
|
||||
```typescript
|
||||
const provider = resolveCLIProxyProviderShortcut(profileName);
|
||||
|
||||
if (provider) {
|
||||
return execClaudeWithCLIProxy(claudeCli, provider, args);
|
||||
}
|
||||
```text
|
||||
CLI flags and environment
|
||||
|
|
||||
v
|
||||
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
|
||||
|
||||
```
|
||||
+===========================================================================+
|
||||
| Legacy GLMT Compatibility + Internal Transforms |
|
||||
+===========================================================================+
|
||||
CCS asks the selected CLIProxy backend to start the provider's supported auth
|
||||
flow. The provider registry identifies whether that is an authorization-code
|
||||
flow, device-code flow, browser polling flow, or currently unsupported account
|
||||
linking.
|
||||
|
||||
Claude CLI
|
||||
```text
|
||||
Select canonical provider
|
||||
|
|
||||
| legacy glmt settings detected
|
||||
v
|
||||
+------------------+
|
||||
| Compatibility | normalizeDeprecatedGlmtEnv()
|
||||
| Layer | (src/utils/glmt-deprecation.ts)
|
||||
+------------------+
|
||||
Validate backend and auth-start support
|
||||
|
|
||||
v
|
||||
+------------------+
|
||||
| Direct GLM API | https://api.z.ai/api/anthropic
|
||||
+------------------+
|
||||
Start backend-owned auth flow
|
||||
|
|
||||
v
|
||||
+------------------+
|
||||
| src/glmt/* | retained for Cursor translation
|
||||
+------------------+
|
||||
User completes provider interaction
|
||||
|
|
||||
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 |
|
||||
|----------|------------|----------|------|
|
||||
| Z.AI (GLM) | `glm` | https://api.z.ai/api/anthropic | API key |
|
||||
| Kimi API | `km` | https://api.kimi.com/coding/ | API key |
|
||||
| Legacy compatibility | `glmt` | normalized to direct GLM at runtime | existing profile only |
|
||||
Some providers can produce more than one account with the same display email.
|
||||
CCS keeps runtime identity tied to the exact registered auth file rather than
|
||||
collapsing accounts by display text. The dashboard can show a safe variant
|
||||
label without exposing the internal identifier.
|
||||
|
||||
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:
|
||||
|
||||
```typescript
|
||||
if (isDeprecatedGlmtProfileName(profileName)) {
|
||||
const normalized = normalizeDeprecatedGlmtEnv(settingsEnv);
|
||||
// warn user, validate against direct GLM endpoint, continue through settings flow
|
||||
}
|
||||
```text
|
||||
Create or edit API profile
|
||||
|
|
||||
Write CCS-owned <profile>.settings.json
|
||||
|
|
||||
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.
|
||||
|
||||
```
|
||||
+===========================================================================+
|
||||
| Remote CLIProxy Architecture (v7.1) |
|
||||
+===========================================================================+
|
||||
New configuration should use current API profile commands and provider names.
|
||||
|
||||
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
|
||||
+------------------+
|
||||
| ResolvedProxyConfig |
|
||||
| mode: local|remote |
|
||||
+------------------+
|
||||
Provision managed MCP/runtime configuration
|
||||
|
|
||||
+---> [mode = local] ---> Spawn local CLIProxyAPI binary
|
||||
| |
|
||||
| v
|
||||
| localhost:8317
|
||||
Target invokes ImageAnalysis
|
||||
|
|
||||
+---> [mode = remote] ---> Connect to remote server
|
||||
|
|
||||
v
|
||||
+------------------+
|
||||
| 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)
|
||||
CCS sends provider-scoped request
|
||||
|
|
||||
Return text result
|
||||
```
|
||||
|
||||
### 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
|
||||
// proxy-config-resolver.ts: Priority order
|
||||
const resolved = {
|
||||
...DEFAULT_CONFIG, // 4. Defaults (lowest)
|
||||
...yamlConfig, // 3. config.yaml
|
||||
...envConfig, // 2. Environment variables
|
||||
...cliFlags, // 1. CLI flags (highest)
|
||||
};
|
||||
```
|
||||
Provider-specific vision mappings are implementation data and should be read
|
||||
from the image-analysis routing services and tests.
|
||||
|
||||
### Health Check
|
||||
## Session and observability boundary
|
||||
|
||||
```typescript
|
||||
// remote-proxy-client.ts
|
||||
async function checkRemoteProxyHealth(config: ResolvedProxyConfig): Promise<boolean> {
|
||||
try {
|
||||
const url = `${config.protocol}://${config.host}:${config.port}/v1/models`;
|
||||
const response = await fetch(url, {
|
||||
headers: config.authToken ? { Authorization: `Bearer ${config.authToken}` } : {},
|
||||
timeout: 2000,
|
||||
});
|
||||
return response.ok;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
```
|
||||
Execution paths can record non-secret metadata such as profile identity,
|
||||
profile type, canonical provider, target type, timestamps, duration, and
|
||||
numeric process exit status. Logs must not contain provider tokens, API keys,
|
||||
raw auth payloads, or personal account identifiers.
|
||||
|
||||
---
|
||||
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.
|
||||
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.
|
||||
## Related documentation
|
||||
|
||||
```
|
||||
+===========================================================================+
|
||||
| Quota Management Architecture (v7.14) |
|
||||
+===========================================================================+
|
||||
|
||||
Pre-Flight Check (before session start)
|
||||
|
|
||||
v
|
||||
+------------------+
|
||||
| quota-manager.ts | Hybrid quota management
|
||||
+------------------+
|
||||
|
|
||||
+---> Get all active accounts for provider
|
||||
|
|
||||
+---> For each account:
|
||||
| |
|
||||
| v
|
||||
| +------------------+
|
||||
| | quota-fetcher.ts | Provider-specific API calls
|
||||
| +------------------+
|
||||
| |
|
||||
| +---> Check isPaused flag --> Skip if paused
|
||||
| |
|
||||
| +---> Fetch quota from provider API
|
||||
| | - Antigravity: fetchAvailableModels
|
||||
| | - Claude: policy limits endpoint
|
||||
| | - Codex: ChatGPT usage windows
|
||||
| | - Gemini CLI: Code Assist quota buckets
|
||||
| | - GitHub Copilot: copilot_internal/user snapshots (deprecated compatibility)
|
||||
| |
|
||||
| +---> Detect tier (free/paid/unknown)
|
||||
| |
|
||||
| +---> Check exhaustion status
|
||||
|
|
||||
+---> Pause exhausted non-default accounts when another healthy account exists
|
||||
|
|
||||
+---> Select best account (not paused, not exhausted)
|
||||
|
|
||||
+---> Auto-failover to next account if current exhausted
|
||||
|
|
||||
+---> Temporarily pause exhausted account when fallback exists
|
||||
| - move token out of live auth discovery
|
||||
| - persist cooldown expiry across launches
|
||||
| - auto-resume only CCS-created quota pauses
|
||||
|
||||
CLI Commands:
|
||||
ccs cliproxy pause <account> --> Set isPaused=true in account-manager
|
||||
ccs cliproxy resume <account> --> Set isPaused=false
|
||||
ccs cliproxy status [account] --> Display quota + tier info
|
||||
|
||||
Dashboard UI:
|
||||
- Pause/Resume toggle per account
|
||||
- Tier badge (free/paid/unknown)
|
||||
- Quota usage display
|
||||
```
|
||||
|
||||
### Account Selection Algorithm
|
||||
|
||||
```typescript
|
||||
// quota-manager.ts: Best account selection
|
||||
function selectBestAccount(accounts: AccountInfo[]): AccountInfo | null {
|
||||
// Priority:
|
||||
// 1. Not paused
|
||||
// 2. Not exhausted
|
||||
// 3. Paid tier over free tier
|
||||
// 4. Highest remaining quota
|
||||
|
||||
return accounts
|
||||
.filter(acc => !acc.isPaused && !acc.isExhausted)
|
||||
.sort((a, b) => {
|
||||
if (a.tier !== b.tier) return (a.tier === 'paid' ? -1 : 1);
|
||||
return (b.remainingQuota || 0) - (a.remainingQuota || 0);
|
||||
})[0] || null;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Authentication Flow
|
||||
|
||||
### OAuth Providers - Authorization Code Flow
|
||||
|
||||
**Providers**: Gemini, Codex, Antigravity, Kiro (aws method)
|
||||
|
||||
```
|
||||
+===========================================================================+
|
||||
| OAuth - Authorization Code Flow (Port-based) |
|
||||
+===========================================================================+
|
||||
|
||||
1. User runs: ccs codex
|
||||
|
|
||||
v
|
||||
2. Check token cache (~/.ccs/cliproxy/auth/)
|
||||
|
|
||||
+---> [Valid token] ---> Use cached token
|
||||
|
|
||||
+---> [No/Expired token]
|
||||
|
|
||||
v
|
||||
3. Start local OAuth server (localhost:9876)
|
||||
|
|
||||
v
|
||||
4. Open browser with OAuth request
|
||||
| https://oauth-provider/authorize?redirect_uri=http://localhost:9876/callback
|
||||
v
|
||||
5. User authorizes in browser
|
||||
|
|
||||
v
|
||||
6. OAuth provider redirects to localhost:9876/callback?code=XXXX
|
||||
|
|
||||
v
|
||||
7. Exchange auth code for access token
|
||||
|
|
||||
v
|
||||
8. Cache token locally (~/.ccs/cliproxy/auth/gemini.json)
|
||||
|
|
||||
v
|
||||
9. Proceed with Claude CLI
|
||||
```
|
||||
|
||||
### OAuth Providers - Device Code Flow
|
||||
|
||||
**Providers**: GitHub Copilot (ghcp, deprecated compatibility)
|
||||
|
||||
Provider identity note:
|
||||
- Providers that do not expose a reliable email no longer require a manual nickname during first auth.
|
||||
- CCS derives a stable internal account identifier from the token/cache context and still allows the user to rename the account later.
|
||||
|
||||
```
|
||||
+===========================================================================+
|
||||
| OAuth - Device Code Flow (No Port Needed) |
|
||||
+===========================================================================+
|
||||
|
||||
1. User runs: ccs ghcp
|
||||
|
|
||||
v
|
||||
2. Check token cache (~/.ccs/cliproxy/auth/)
|
||||
|
|
||||
+---> [Valid token] ---> Use cached token
|
||||
|
|
||||
+---> [No/Expired token]
|
||||
|
|
||||
v
|
||||
3. Request device code from GitHub
|
||||
|
|
||||
v
|
||||
4. Display user code + verification URL
|
||||
| "Enter code XXXX-XXXX at github.com/login/device"
|
||||
v
|
||||
5. User opens URL in browser and enters code
|
||||
|
|
||||
v
|
||||
6. Poll GitHub for token completion
|
||||
|
|
||||
v
|
||||
7. Receive and cache token locally
|
||||
|
|
||||
v
|
||||
8. Proceed with Claude CLI
|
||||
```
|
||||
|
||||
### Kiro OAuth - Method-Aware Flow
|
||||
|
||||
**Supported methods**:
|
||||
- `aws`: Device Code (default, AWS org friendly)
|
||||
- `aws-authcode`: Authorization Code via CLI flow
|
||||
- `google`: Social OAuth via management API
|
||||
- `github`: Social OAuth via management API (Dashboard flow)
|
||||
|
||||
```
|
||||
+===========================================================================+
|
||||
| Kiro OAuth - Method-Aware Flow |
|
||||
+===========================================================================+
|
||||
|
||||
Configuration:
|
||||
ccs_profile:
|
||||
target: claude
|
||||
cliproxy:
|
||||
provider: kiro
|
||||
kiro_method: aws # or aws-authcode, google, github
|
||||
|
||||
Flow:
|
||||
Device Code (aws)
|
||||
→ /start endpoint (no callback port)
|
||||
→ Opens browser
|
||||
→ User enters code
|
||||
→ Poll /status
|
||||
|
||||
Authorization Code (aws-authcode, google, github)
|
||||
→ /start-url endpoint
|
||||
→ Returns auth_url
|
||||
→ User visits URL
|
||||
→ Callback handled
|
||||
→ Poll /status for completion
|
||||
|
||||
Key behavior:
|
||||
- Device Code method uses /start route (no callback port)
|
||||
- Callback/social methods use /start-url + status polling
|
||||
- Some management flows return state first, auth_url later
|
||||
- Manual nicknames are optional when the upstream provider does not return an email
|
||||
- Account storage uses a stable internal identifier so reauth/update flows do not depend on dashboard list order
|
||||
```
|
||||
|
||||
### API Key Profiles (GLM, Kimi)
|
||||
|
||||
```
|
||||
+===========================================================================+
|
||||
| API Key Profile (Non-OAuth) |
|
||||
+===========================================================================+
|
||||
|
||||
1. User configures API key in settings
|
||||
|
|
||||
v
|
||||
2. Key stored in ~/.ccs/<profile>.settings.json
|
||||
|
|
||||
v
|
||||
3. Profile detection: APIKeyProfile
|
||||
|
|
||||
v
|
||||
4. Key passed via ANTHROPIC_AUTH_TOKEN env var
|
||||
|
|
||||
v
|
||||
5. Target adapter (Claude/Droid) handles delivery
|
||||
|
|
||||
└─ Claude: env var
|
||||
└─ Droid: config file (~/.factory/settings.json)
|
||||
```
|
||||
|
||||
### Anthropic Direct API Key
|
||||
|
||||
```
|
||||
+===========================================================================+
|
||||
| Anthropic Direct API Key (Native Auth) |
|
||||
+===========================================================================+
|
||||
|
||||
1. User creates profile: ccs api create --preset anthropic
|
||||
|
|
||||
v
|
||||
2. Key stored in ~/.ccs/<profile>.settings.json
|
||||
| env: { ANTHROPIC_API_KEY: "sk-ant-..." }
|
||||
| (NO ANTHROPIC_BASE_URL, NO ANTHROPIC_AUTH_TOKEN)
|
||||
v
|
||||
3. Profile detection: settings-based
|
||||
|
|
||||
v
|
||||
4. Key passed via ANTHROPIC_API_KEY env var
|
||||
| Claude CLI uses native endpoint (api.anthropic.com)
|
||||
v
|
||||
5. Claude CLI authenticates with x-api-key header
|
||||
|
||||
Detection logic (profile-writer.ts):
|
||||
- apiKey.startsWith('sk-ant-') -> native mode
|
||||
- baseUrl.includes('api.anthropic.com') -> native mode
|
||||
- Otherwise -> proxy mode (existing behavior)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Image Analysis Hook Flow (v7.34)
|
||||
|
||||
### Overview
|
||||
|
||||
Image Analysis Hook enables vision model proxying through CLIProxy with automatic injection for all profile types.
|
||||
|
||||
```
|
||||
+===========================================================================+
|
||||
| Image Analysis Hook Flow (v7.34) |
|
||||
+===========================================================================+
|
||||
|
||||
Claude CLI with image input
|
||||
|
|
||||
v
|
||||
Hook Installer (ensureProfileHooks)
|
||||
|
|
||||
+---> Check ~/.claude/hooks/openai-vision-hook.cjs exists
|
||||
|
|
||||
+---> If missing: auto-install via image-analyzer-hook-installer
|
||||
|
|
||||
v
|
||||
Hook Configuration
|
||||
|
|
||||
+---> Set ANTHROPIC_IMAGE_HOOK_URL
|
||||
| (proxy endpoint URL)
|
||||
|
|
||||
v
|
||||
Claude CLI processes image request
|
||||
|
|
||||
v
|
||||
Claude prefers ImageAnalysis MCP tool
|
||||
|
|
||||
v
|
||||
CCS provider-backed image analysis
|
||||
|
|
||||
+---> Provider route resolved before launch
|
||||
|
|
||||
+---> Direct request to /api/provider/<backend>/v1/messages
|
||||
|
|
||||
+---> Native Read fallback if runtime/auth/proxy is unavailable
|
||||
|
|
||||
v
|
||||
Text description returned to Claude CLI
|
||||
```
|
||||
|
||||
### Runtime Environment
|
||||
|
||||
```typescript
|
||||
// getImageAnalysisHookEnv()
|
||||
{
|
||||
CCS_IMAGE_ANALYSIS_RUNTIME_BASE_URL: 'http://127.0.0.1:8317',
|
||||
CCS_IMAGE_ANALYSIS_RUNTIME_PATH: '/api/provider/agy',
|
||||
CCS_IMAGE_ANALYSIS_RUNTIME_API_KEY: 'ccs-internal-managed',
|
||||
}
|
||||
```
|
||||
|
||||
### Provider Support
|
||||
|
||||
| Provider | Vision Support | Notes |
|
||||
|----------|---|---|
|
||||
| Gemini | ✓ | Via CCS ImageAnalysis provider route |
|
||||
| Codex | ✓ | Via CCS ImageAnalysis provider route |
|
||||
| Antigravity | ✓ | Via CCS ImageAnalysis provider route |
|
||||
| Kiro | ✓ | Via mapped CCS provider route when configured |
|
||||
| Copilot | ✓ | Deprecated compatibility route via mapped ghcp provider |
|
||||
| GLM/Kimi | ✓ | Via explicit or fallback backend mapping |
|
||||
|
||||
---
|
||||
|
||||
## Session Tracking
|
||||
|
||||
All execution paths record session metadata including target CLI used:
|
||||
|
||||
```typescript
|
||||
{
|
||||
profileName: 'gemini',
|
||||
profileType: 'clipproxy',
|
||||
provider: 'google-gemini',
|
||||
targetCli: 'claude', // NEW: which target was used
|
||||
timestamp: '2026-02-16T10:40:00Z',
|
||||
duration: 12345,
|
||||
exitCode: 0,
|
||||
model: 'claude-opus-4-6',
|
||||
}
|
||||
```
|
||||
|
||||
This enables analytics on target CLI usage and adoption.
|
||||
|
||||
---
|
||||
|
||||
## Related Documentation
|
||||
|
||||
- [System Architecture Index](./index.md) — Overall system design
|
||||
- [Target Adapters](./target-adapters.md) — Multi-CLI adapter pattern
|
||||
- [Codebase Summary](../codebase-summary.md) — Module structure
|
||||
- [Code Standards](../code-standards.md) — Implementation guidelines
|
||||
- [System Architecture](./index.md)
|
||||
- [Target Adapters](./target-adapters.md)
|
||||
- [Logging Contract](../logging-contract.md)
|
||||
- [Codebase Summary](../codebase-summary.md)
|
||||
- [Code Standards](../code-standards.md)
|
||||
@@ -1,803 +1,181 @@
|
||||
# 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
|
||||
|
||||
Each CLI target implements the `TargetAdapter` contract:
|
||||
|
||||
```typescript
|
||||
export interface TargetAdapter {
|
||||
readonly type: TargetType; // 'claude' | 'droid' | 'codex'
|
||||
readonly displayName: string; // "Claude Code" | "Factory Droid" | "Codex CLI"
|
||||
|
||||
/** Detect if the target CLI binary exists on system */
|
||||
detectBinary(): TargetBinaryInfo | null;
|
||||
|
||||
/** Prepare credentials for delivery to target CLI */
|
||||
prepareCredentials(creds: TargetCredentials): Promise<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
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
Do not duplicate the TypeScript interface in this guide. Interface signatures
|
||||
and credential fields change with runtime requirements; the source files are
|
||||
the contract checked by the compiler.
|
||||
|
||||
## Target Resolution
|
||||
|
||||
CCS resolves which adapter to use via priority-ordered checks:
|
||||
|
||||
### Resolution Priority
|
||||
|
||||
```
|
||||
1. --target flag (CLI argument) — highest priority
|
||||
└─ ccs --target droid glm
|
||||
└─ ccs --target codex
|
||||
|
||||
2. Explicit runtime entrypoint (`CCS_INTERNAL_ENTRY_TARGET`) — dedicated bin shims
|
||||
└─ ccs-droid / ccsd → droid
|
||||
└─ ccs-codex / ccsx → codex
|
||||
└─ ccsxp → codex, then prepends `--config model_provider="cliproxy"`
|
||||
|
||||
3. argv[0] detection (runtime alias pattern) — binary name mapping for same-binary/custom aliases
|
||||
└─ ccs-droid (explicit alias) → droid
|
||||
└─ ccsd (legacy shortcut) → droid
|
||||
└─ ccs-codex (explicit alias) → codex
|
||||
└─ ccsx (short alias) → codex
|
||||
└─ ccs (regular command) → default
|
||||
|
||||
4. Per-profile config (from ~/.ccs/config.yaml or settings.json)
|
||||
└─ persisted targets are currently only `claude` and `droid`
|
||||
└─ profiles:
|
||||
glm:
|
||||
target: droid
|
||||
|
||||
5. Fallback: 'claude' — lowest priority
|
||||
```
|
||||
|
||||
### Implementation
|
||||
|
||||
```typescript
|
||||
// src/targets/target-resolver.ts
|
||||
|
||||
export function resolveTargetType(
|
||||
args: string[],
|
||||
profileConfig?: { target?: TargetType }
|
||||
): TargetType {
|
||||
// 1. Parse --target flags (supports --target value and --target=value)
|
||||
// Repeated flags: last one wins.
|
||||
const parsed = parseTargetFlags(args);
|
||||
if (parsed.targetOverride) {
|
||||
return parsed.targetOverride;
|
||||
}
|
||||
|
||||
// 2. Check explicit runtime entrypoint shim
|
||||
const entrypointTarget = resolveEntrypointTarget();
|
||||
if (entrypointTarget) {
|
||||
return entrypointTarget;
|
||||
}
|
||||
|
||||
// 3. Check argv[0] (binary name / custom alias map)
|
||||
const binName = path.basename(process.argv[1] || process.argv0 || '').replace(/\.(cmd|bat|ps1|exe)$/i, '');
|
||||
if (ARGV0_TARGET_MAP[binName]) {
|
||||
return ARGV0_TARGET_MAP[binName];
|
||||
}
|
||||
|
||||
// 4. Check profile config
|
||||
if (profileConfig?.target) {
|
||||
// Persisted targets are validated before profile configuration is saved.
|
||||
return profileConfig.target;
|
||||
}
|
||||
|
||||
// 5. Default to claude
|
||||
return 'claude';
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Claude Adapter
|
||||
|
||||
### Implementation
|
||||
|
||||
```typescript
|
||||
// src/targets/claude-adapter.ts
|
||||
|
||||
export class ClaudeAdapter implements TargetAdapter {
|
||||
readonly type: TargetType = 'claude';
|
||||
readonly displayName = 'Claude Code';
|
||||
|
||||
detectBinary(): TargetBinaryInfo | null {
|
||||
const info = getClaudeCliInfo();
|
||||
if (!info) return null;
|
||||
return { path: info.path, needsShell: info.needsShell };
|
||||
}
|
||||
|
||||
async prepareCredentials(_creds: TargetCredentials): Promise<void> {
|
||||
// No-op: Claude receives credentials via environment variables
|
||||
}
|
||||
|
||||
buildArgs(_profile: string, userArgs: string[]): string[] {
|
||||
return userArgs; // Pass through user arguments unchanged
|
||||
}
|
||||
|
||||
buildEnv(creds: TargetCredentials, profileType: string): NodeJS.ProcessEnv {
|
||||
const webSearchEnv = getWebSearchHookEnv();
|
||||
|
||||
// For native profiles, strip stale proxy env to prevent interference
|
||||
const baseEnv =
|
||||
profileType === 'account' || profileType === 'default'
|
||||
? stripAnthropicEnv(process.env)
|
||||
: process.env;
|
||||
|
||||
const env: NodeJS.ProcessEnv = { ...baseEnv, ...webSearchEnv };
|
||||
|
||||
if (creds.envVars) {
|
||||
Object.assign(env, creds.envVars);
|
||||
}
|
||||
|
||||
// Deliver credentials via environment variables
|
||||
if (creds.baseUrl) env['ANTHROPIC_BASE_URL'] = creds.baseUrl;
|
||||
if (creds.apiKey) env['ANTHROPIC_AUTH_TOKEN'] = creds.apiKey;
|
||||
if (creds.model) env['ANTHROPIC_MODEL'] = creds.model;
|
||||
|
||||
return env;
|
||||
}
|
||||
|
||||
exec(args: string[], env: NodeJS.ProcessEnv, _options?: { cwd?: string }): void {
|
||||
const claudeCli = detectClaudeCli();
|
||||
if (!claudeCli) {
|
||||
void ErrorManager.showClaudeNotFound();
|
||||
process.exit(1);
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle Windows shell requirements
|
||||
const isWindows = process.platform === 'win32';
|
||||
const needsShell = isWindows && /\.(cmd|bat|ps1)$/i.test(claudeCli);
|
||||
|
||||
let child: ChildProcess;
|
||||
if (needsShell) {
|
||||
const cmdString = [claudeCli, ...args].map(escapeShellArg).join(' ');
|
||||
child = spawn(cmdString, { shell: true, stdio: 'inherit', env });
|
||||
} else {
|
||||
child = spawn(claudeCli, args, { stdio: 'inherit', env });
|
||||
}
|
||||
|
||||
// Handle process termination
|
||||
const onSigInt = () => child.kill('SIGINT');
|
||||
const onSigTerm = () => child.kill('SIGTERM');
|
||||
process.once('SIGINT', onSigInt);
|
||||
process.once('SIGTERM', onSigTerm);
|
||||
child.on('exit', () => {
|
||||
process.removeListener('SIGINT', onSigInt);
|
||||
process.removeListener('SIGTERM', onSigTerm);
|
||||
});
|
||||
}
|
||||
|
||||
supportsProfileType(profileType: string): boolean {
|
||||
// Claude supports all profile types
|
||||
return true;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Native Claude launches keep user arguments session-scoped. The launch layer validates and normalizes
|
||||
`--effort low|medium|high|xhigh|max` before spawning Claude, then passes it through without writing
|
||||
to Claude or CCS configuration. CLIProxy-backed Claude launches still treat `--effort` as the CCS
|
||||
thinking alias handled by CLIProxy.
|
||||
|
||||
### Credential Delivery
|
||||
|
||||
**Method**: Environment variables
|
||||
|
||||
```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
|
||||
[`src/targets/target-resolver.ts`](../../src/targets/target-resolver.ts) selects
|
||||
the runtime in this order:
|
||||
|
||||
1. `--target <name>` or `--target=<name>`; if repeated, the last flag wins.
|
||||
2. A trusted package runtime entrypoint identified by
|
||||
`CCS_INTERNAL_ENTRY_TARGET`.
|
||||
3. The invoked binary name, including built-in or configured aliases.
|
||||
4. A persisted per-profile target.
|
||||
5. `claude`.
|
||||
|
||||
All `--target` flags are removed before the remaining arguments reach the
|
||||
runtime. Parsing stops at the `--` option terminator.
|
||||
|
||||
The built-in runtime aliases are derived from target metadata:
|
||||
|
||||
| Target | Built-in aliases |
|
||||
| --- | --- |
|
||||
| Claude Code | Base `ccs` command |
|
||||
| Factory Droid | `ccs-droid`, `ccsd` |
|
||||
| Codex CLI | `ccs-codex`, `ccsx`, `ccsxp` |
|
||||
|
||||
Custom aliases use `CCS_TARGET_ALIASES` with entries such as
|
||||
`droid=team-droid;codex=team-codex`. The target-specific legacy environment
|
||||
variables remain compatibility inputs. Alias values are validated and cannot
|
||||
replace reserved package binary names.
|
||||
|
||||
## Runtime Compatibility
|
||||
|
||||
Adapter-level checks are deliberately conservative. Flow-specific compatibility
|
||||
is evaluated in
|
||||
[`src/targets/target-runtime-compatibility.ts`](../../src/targets/target-runtime-compatibility.ts),
|
||||
which has the provider and bridge context needed for an accurate decision.
|
||||
|
||||
| Profile flow | Claude | Droid | Codex |
|
||||
| --- | --- | --- | --- |
|
||||
| Native default | Supported | Conditional: requires resolved `ANTHROPIC_BASE_URL` and `ANTHROPIC_AUTH_TOKEN` | Supported |
|
||||
| Settings/API profile | Supported | Supported | Codex CLIProxy bridge only |
|
||||
| CLIProxy profile | Supported | Supported | Non-composite `codex` provider only |
|
||||
| Claude account | Supported | Not supported | Not supported |
|
||||
| Copilot | Supported | Not supported | Not supported |
|
||||
| Cursor local proxy | Supported | Not supported | Not supported |
|
||||
|
||||
When changing compatibility, update both the runtime evaluator and its focused
|
||||
tests. The authoritative coverage is in
|
||||
[`tests/unit/targets/target-runtime-compatibility.test.ts`](../../tests/unit/targets/target-runtime-compatibility.test.ts).
|
||||
|
||||
The compatibility evaluator accepts Droid's native-default flow, but adapter
|
||||
preparation still validates the resolved credentials. A default Droid launch
|
||||
cannot proceed without both a non-empty base URL and auth token.
|
||||
|
||||
## Credential and Configuration Boundaries
|
||||
|
||||
### Claude Code
|
||||
|
||||
[`src/targets/claude-adapter.ts`](../../src/targets/claude-adapter.ts) delivers
|
||||
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
|
||||
environment values.
|
||||
|
||||
The adapter does not persist provider credentials to Claude settings.
|
||||
|
||||
### Factory Droid
|
||||
|
||||
[`src/targets/droid-adapter.ts`](../../src/targets/droid-adapter.ts) validates
|
||||
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
|
||||
unrelated settings and use the configuration manager rather than direct JSON
|
||||
replacement.
|
||||
|
||||
### Codex CLI
|
||||
|
||||
[`src/targets/codex-adapter.ts`](../../src/targets/codex-adapter.ts) keeps normal
|
||||
CCS-backed launches transient:
|
||||
|
||||
- native default sessions keep native Codex authentication and configuration;
|
||||
- CCS-backed sessions use `-c key=value` overrides for a Responses-compatible
|
||||
runtime provider;
|
||||
- the resolved API key is supplied through the provider's environment key, not
|
||||
on the command line;
|
||||
- stale Anthropic and nested Codex-session variables are removed before spawn.
|
||||
|
||||
The `ccsxp` shortcut is the explicit exception. It may repair the dedicated
|
||||
`cliproxy` provider block in the active Codex configuration through
|
||||
[`src/targets/codex-cliproxy-provider-config.ts`](../../src/targets/codex-cliproxy-provider-config.ts).
|
||||
That repair preserves a valid custom base URL and the configured environment-key
|
||||
name. General Codex launches must not rewrite `config.toml`.
|
||||
|
||||
## Execution Invariants
|
||||
|
||||
The dispatcher owns the order of operations:
|
||||
|
||||
1. parse CCS-owned arguments and resolve the target;
|
||||
2. resolve the profile and provider credentials;
|
||||
3. evaluate target/profile/provider compatibility;
|
||||
4. detect the target binary;
|
||||
5. call `prepareCredentials`;
|
||||
6. build target arguments and environment;
|
||||
7. execute the child runtime.
|
||||
|
||||
Flow implementations live under
|
||||
[`src/dispatcher/flows/`](../../src/dispatcher/flows/). Shared target execution
|
||||
logic lives in
|
||||
[`src/dispatcher/target-executor.ts`](../../src/dispatcher/target-executor.ts).
|
||||
|
||||
All adapters must preserve these invariants:
|
||||
|
||||
- user credentials never appear in documented examples, logs, or spawn
|
||||
arguments;
|
||||
- target-owned persistent writes are explicit and scoped;
|
||||
- stale routing variables from another runtime do not leak into the child;
|
||||
- Windows wrapper handling does not use a shell unless the wrapper format
|
||||
requires one;
|
||||
- child signals and exit behavior propagate to the CCS process;
|
||||
- binary and launch failures run registered cleanup before exit.
|
||||
|
||||
## Adding or Changing a Target
|
||||
|
||||
Change the smallest complete set:
|
||||
|
||||
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`;
|
||||
4. add flow-aware compatibility rules;
|
||||
5. add focused resolver, adapter, compatibility, and integration tests;
|
||||
6. update CLI help and dashboard controls if the target becomes user
|
||||
configurable.
|
||||
|
||||
Start with these test suites:
|
||||
|
||||
- [`tests/unit/targets/target-resolver.test.ts`](../../tests/unit/targets/target-resolver.test.ts)
|
||||
- [`tests/unit/targets/target-registry.test.ts`](../../tests/unit/targets/target-registry.test.ts)
|
||||
- [`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/)
|
||||
|
||||
Do not describe a target as supported from adapter registration alone. Support
|
||||
requires a compatible profile flow, safe credential delivery, binary detection,
|
||||
execution behavior, and tests for the full combination.
|
||||
+122
-130
@@ -1,96 +1,84 @@
|
||||
# WebSearch Configuration Guide
|
||||
|
||||
Last Updated: 2026-04-11
|
||||
CCS provides a local `WebSearch` MCP tool for managed Claude launches that use a
|
||||
third-party provider. Native Claude sessions keep Anthropic's native search
|
||||
behavior.
|
||||
|
||||
CCS provides automatic web search for third-party profiles that cannot access Anthropic's native WebSearch API.
|
||||
## Launch Contract
|
||||
|
||||
## How WebSearch Works
|
||||
For a third-party Claude launch, CCS:
|
||||
|
||||
### Native Claude Accounts
|
||||
1. suppresses the native `WebSearch` tool because the third-party backend cannot
|
||||
execute it;
|
||||
2. adds a short steering prompt that prefers the CCS MCP `WebSearch` tool;
|
||||
3. when WebSearch is enabled, installs the managed MCP server and adds the
|
||||
`ccs-websearch` entry to the applicable Claude configuration;
|
||||
4. searches enabled and ready providers in deterministic order.
|
||||
|
||||
Native Claude subscription accounts still use Anthropic's server-side WebSearch directly.
|
||||
Enabled launches fail closed if the MCP runtime or configuration cannot be
|
||||
prepared. This prevents a launch where native search is suppressed but the
|
||||
managed replacement is missing. When `websearch.enabled` is `false`, CCS skips
|
||||
MCP provisioning but still suppresses native WebSearch for third-party
|
||||
profiles; the model can use ordinary network or shell tools when allowed.
|
||||
|
||||
### Third-Party Profiles
|
||||
Claude subcommands that reject session flags are passed through without
|
||||
WebSearch argument injection.
|
||||
|
||||
Third-party profiles cannot execute Anthropic's server-side WebSearch because the tool never reaches their backend. CCS now handles that by provisioning a first-class local MCP tool when the managed runtime is available, suppressing native `WebSearch` for those launches, appending a short launch-time steering hint, and running real local search providers directly.
|
||||
Implementation sources:
|
||||
|
||||
## Architecture
|
||||
- [`src/utils/websearch/mcp-installer.ts`](../src/utils/websearch/mcp-installer.ts)
|
||||
- [`src/utils/websearch/claude-tool-args.ts`](../src/utils/websearch/claude-tool-args.ts)
|
||||
- [`src/dispatcher/flows/settings-flow.ts`](../src/dispatcher/flows/settings-flow.ts)
|
||||
- [`lib/mcp/ccs-websearch-server.cjs`](../lib/mcp/ccs-websearch-server.cjs)
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────────────────────────┐
|
||||
│ Claude Code CLI │
|
||||
│ │
|
||||
│ Search Request │
|
||||
│ │ │
|
||||
│ ├── Native Claude Account? → Anthropic WebSearch API │
|
||||
│ │ │
|
||||
│ └── Third-party Profile? → native WebSearch disabled │
|
||||
│ │ │
|
||||
│ ├── CCS MCP tool when ready │
|
||||
│ │ ccs-websearch.WebSearch │
|
||||
│ │ │ │
|
||||
│ │ ├── 1. Exa │
|
||||
│ │ ├── 2. Tavily │
|
||||
│ │ ├── 3. Brave │
|
||||
│ │ ├── 4. SearXNG │
|
||||
│ │ ├── 5. DuckDuckGo│
|
||||
│ │ └── 6. Legacy CLI│
|
||||
│ │ fallback │
|
||||
│ │ (Gemini/ │
|
||||
│ │ OpenCode/│
|
||||
│ │ Grok) │
|
||||
│ └── Bash/network fallback │
|
||||
└──────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
## Provider Order, Runtime Eligibility, and Dashboard Status
|
||||
|
||||
## Why This Changed
|
||||
The managed runtime tries eligible providers sequentially:
|
||||
|
||||
The previous design asked another model CLI to perform web search and summarize the answer. A later compatibility path also depended on a denied native-tool hook. Both were brittle:
|
||||
1. Exa
|
||||
2. Tavily
|
||||
3. Brave Search
|
||||
4. SearXNG
|
||||
5. DuckDuckGo
|
||||
6. Antigravity CLI
|
||||
7. Gemini CLI compatibility fallback
|
||||
8. OpenCode
|
||||
9. Grok CLI
|
||||
|
||||
- CLI syntax changed upstream
|
||||
- auth state varied per tool
|
||||
- prompt/tool behavior drifted across releases
|
||||
- hook-shaped denial output produced awkward host UX
|
||||
The first successful provider wins. Temporarily failing providers can enter a
|
||||
bounded cooldown and be skipped on later calls. Runtime attempt eligibility is
|
||||
defined in
|
||||
[`lib/hooks/websearch-transformer.cjs`](../lib/hooks/websearch-transformer.cjs);
|
||||
do not duplicate that list in other architecture docs.
|
||||
|
||||
The new flow matches the `goclaw` model more closely: web search is treated as a first-class deterministic capability, not an LLM-to-LLM workaround or a denied native tool call.
|
||||
The transformer and dashboard answer related but different questions:
|
||||
|
||||
When provisioned, the managed MCP tool is exposed as `ccs-websearch.WebSearch`, not a generic `search` helper. That naming is deliberate: it gives Claude a tool that matches the native `WebSearch` concept more directly, which should reduce cases where the model reaches for ad hoc Bash or `curl` fetches instead.
|
||||
| Provider | Transformer attempts when | Dashboard reports available when |
|
||||
| --- | --- | --- |
|
||||
| Exa | Enabled and `EXA_API_KEY` is present. | Enabled and the key is available through the active process or enabled Global Env. |
|
||||
| Tavily | Enabled and `TAVILY_API_KEY` is present. | Enabled and the key is available through the active process or enabled Global Env. |
|
||||
| Brave Search | Enabled and `BRAVE_API_KEY` is present. | Enabled and the key is available through the active process or enabled Global Env. |
|
||||
| SearXNG | Enabled and a base URL is present. | Enabled with a valid normalized base URL. |
|
||||
| DuckDuckGo | Enabled. | Enabled. |
|
||||
| Antigravity | Enabled and the `agy` executable is available. | Enabled and the CLI is installed. |
|
||||
| Gemini compatibility | Enabled and the `gemini` executable is available. | Enabled, installed, and authenticated. |
|
||||
| OpenCode | Enabled and the `opencode` executable is available. | Enabled and the CLI is installed. |
|
||||
| Grok | Enabled and the `grok` executable is available. | Enabled, installed, and `GROK_API_KEY` is available. |
|
||||
|
||||
CCS also appends a third-party-only `--append-system-prompt` hint telling Claude to prefer that managed `WebSearch` tool for web lookups and current-information requests. This is soft steering only: if the user explicitly asks for shell commands, or the tool is unavailable, Claude can still fall back to Bash/network tools.
|
||||
That shared launch helper applies to normal third-party settings profiles, CLIProxy/Copilot-backed Claude launches, and CCS headless/delegation runs that execute through a settings profile.
|
||||
|
||||
`websearch.enabled: false` disables the managed local runtime, but CCS still suppresses Anthropic's native `WebSearch` on third-party profiles. That native tool cannot be satisfied by Exa, Tavily, Brave, DuckDuckGo, or other non-Anthropic backends, so CCS avoids sending a broken native-tool request and lets Claude fall back to normal shell/network tools instead.
|
||||
|
||||
## Providers
|
||||
|
||||
| Provider | Type | Setup | Default | Notes |
|
||||
|----------|------|-------|---------|-------|
|
||||
| Exa | HTTP API | `EXA_API_KEY` | No | High-quality API search with extracted content |
|
||||
| Tavily | HTTP API | `TAVILY_API_KEY` | No | Agent-oriented search API |
|
||||
| Brave Search | HTTP API | `BRAVE_API_KEY` | No | Cleaner snippets and metadata |
|
||||
| SearXNG | JSON API | `providers.searxng.url` | No | Self-hosted/public SearXNG backend via `/search?format=json` |
|
||||
| DuckDuckGo | HTML fetch | None | Yes | Built-in zero-setup fallback |
|
||||
| Antigravity (agy) | LLM CLI | `curl -fsSL https://antigravity.google/cli/install.sh \| bash` | No | Recommended LLM CLI fallback (Gemini CLI successor) |
|
||||
| Gemini CLI | LLM CLI | Deprecated, use Antigravity (agy) | No | Deprecated. Google retired the gemini CLI on 2026-06-18 |
|
||||
| OpenCode | LLM CLI | `curl -fsSL https://opencode.ai/install \| bash` | No | Optional compatibility fallback |
|
||||
| Grok CLI | LLM CLI | `npm i -g @vibe-kit/grok-cli` + `GROK_API_KEY` | No | Optional compatibility fallback |
|
||||
DuckDuckGo is the default zero-setup provider. API-backed and CLI fallback
|
||||
providers are disabled by default. Dashboard availability and setup guidance
|
||||
are computed separately by
|
||||
[`src/utils/websearch/status.ts`](../src/utils/websearch/status.ts); they do not
|
||||
change the transformer's attempt predicate.
|
||||
|
||||
## Configuration
|
||||
|
||||
### Via Dashboard
|
||||
Use `ccs config` and open `Settings` → `WebSearch`, or edit the `websearch`
|
||||
section in the unified CCS configuration:
|
||||
|
||||
Open `ccs config` → `Settings` → `WebSearch`.
|
||||
|
||||
- Enable Exa, Tavily, Brave, SearXNG, or DuckDuckGo in the backend chain
|
||||
- Configure the SearXNG base URL (for example `https://search.example.com`) when SearXNG is enabled
|
||||
Do not include `/search`, embedded credentials, query parameters, or URL fragments. CCS appends `/search?format=json`.
|
||||
- Set or rotate Exa, Tavily, and Brave API keys directly inside each provider card
|
||||
- Saved keys are persisted in `global_env` and injected at runtime, so readiness updates from the same screen
|
||||
- Review whether any legacy fallback CLIs are still enabled in config
|
||||
|
||||
### Via Config File
|
||||
|
||||
Edit `~/.ccs/config.yaml`:
|
||||
The runtime schema supports Antigravity through `providers.agy`; the current
|
||||
dashboard editor does not expose that provider, so configure it in
|
||||
`config.yaml`.
|
||||
|
||||
```yaml
|
||||
websearch:
|
||||
@@ -129,80 +117,84 @@ websearch:
|
||||
timeout: 55
|
||||
```
|
||||
|
||||
Note: `enabled: false` stops provisioning the managed local `ccs-websearch.WebSearch` runtime. It does not re-enable Anthropic's native `WebSearch` for third-party backends.
|
||||
The schema is
|
||||
[`src/config/schemas/websearch.ts`](../src/config/schemas/websearch.ts), and
|
||||
defaults are in
|
||||
[`src/config/schemas/unified-config.ts`](../src/config/schemas/unified-config.ts).
|
||||
Deprecated top-level WebSearch fields remain load-compatible but must not be
|
||||
used for new configuration.
|
||||
|
||||
## Environment Variables
|
||||
### SearXNG URL
|
||||
|
||||
| Variable | Description |
|
||||
|----------|-------------|
|
||||
| `EXA_API_KEY` | Enables Exa when `providers.exa.enabled: true` |
|
||||
| `TAVILY_API_KEY` | Enables Tavily when `providers.tavily.enabled: true` |
|
||||
| `BRAVE_API_KEY` | Enables Brave Search when `providers.brave.enabled: true` |
|
||||
| `CCS_WEBSEARCH_SEARXNG_URL` | Runtime URL used when `providers.searxng.enabled: true` |
|
||||
| `CCS_WEBSEARCH_SEARXNG_MAX_RESULTS` | Optional runtime override for SearXNG result count (clamped 1..10) |
|
||||
| `GROK_API_KEY` | Required only for legacy Grok CLI fallback |
|
||||
| `CCS_WEBSEARCH_SKIP` | Disable the CCS local WebSearch runtime for the current process; third-party launches still keep native Anthropic `WebSearch` disabled |
|
||||
| `CCS_DEBUG` | Verbose WebSearch runtime logging |
|
||||
| `CCS_WEBSEARCH_TRACE` | Write opt-in JSONL trace records under `~/.ccs/logs/websearch-trace.jsonl` |
|
||||
| `CCS_WEBSEARCH_TRACE_FILE` | Override the trace file path (must stay inside `~/.ccs/`, your system temp directory, or `/var/log`) |
|
||||
Configure the instance base URL, for example
|
||||
`https://search.example.invalid`. Do not include `/search`, credentials, query
|
||||
parameters, or a URL fragment. CCS normalizes the base and calls the JSON search
|
||||
endpoint.
|
||||
|
||||
## Managed Runtime Files
|
||||
### Dashboard-managed API keys
|
||||
|
||||
- `~/.claude.json` → CCS manages `mcpServers.ccs-websearch`
|
||||
- `~/.ccs/mcp/ccs-websearch-server.cjs` → local MCP server binary
|
||||
- `~/.ccs/hooks/websearch-transformer.cjs` → shared provider runtime plus legacy compatibility fallback
|
||||
The dashboard stores supported provider keys in `global_env`. They are
|
||||
available to WebSearch only when global environment injection is enabled.
|
||||
Shell-provided environment values remain supported. Never place real keys in
|
||||
documentation, tests, or committed configuration.
|
||||
|
||||
## Troubleshooting
|
||||
## Managed Files
|
||||
|
||||
### WebSearch says "Ready (DuckDuckGo)"
|
||||
In the default user layout, CCS manages:
|
||||
|
||||
That is expected. DuckDuckGo is the default zero-setup backend.
|
||||
- `~/.claude.json` → `mcpServers.ccs-websearch`
|
||||
- `~/.ccs/mcp/ccs-websearch-server.cjs`
|
||||
- `~/.ccs/hooks/websearch-transformer.cjs`
|
||||
|
||||
### Exa, Tavily, or Brave is enabled but not ready
|
||||
CCS installation and test paths can differ because configuration helpers honor
|
||||
the active CCS and Claude home locations. Provisioning uses a lock and
|
||||
preserves unrelated MCP server entries. Malformed Claude configuration is not
|
||||
overwritten.
|
||||
|
||||
Set the matching API key in the WebSearch dashboard card, or export it in the environment that launches CCS, then refresh status:
|
||||
## Runtime Environment
|
||||
|
||||
```bash
|
||||
export EXA_API_KEY="your-api-key"
|
||||
# or: export TAVILY_API_KEY="your-api-key"
|
||||
# or: export BRAVE_API_KEY="your-api-key"
|
||||
ccs config
|
||||
```
|
||||
[`src/utils/websearch/hook-env.ts`](../src/utils/websearch/hook-env.ts) converts
|
||||
the resolved configuration into runtime environment values. Provider API keys
|
||||
remain environment inputs; boolean and limit variables describe provider
|
||||
selection.
|
||||
|
||||
If the dashboard says the key is stored but still not ready, check whether `Settings -> Global Env` is disabled. WebSearch reuses that injection path for dashboard-managed keys.
|
||||
Operational overrides:
|
||||
|
||||
### SearXNG is enabled but not ready
|
||||
| Variable | Effect |
|
||||
| --- | --- |
|
||||
| `CCS_DEBUG` | Enables verbose diagnostics and WebSearch trace collection. |
|
||||
| `CCS_WEBSEARCH_TRACE` | Enables WebSearch JSONL trace collection. |
|
||||
| `CCS_WEBSEARCH_TRACE_FILE` | Requests a trace path within an allowed CCS log, system temporary, or `/var/log` boundary. |
|
||||
|
||||
1. Confirm the configured base URL is valid (for example `https://search.example.com`)
|
||||
2. Confirm the instance exposes `GET /search?q=<query>&format=json`
|
||||
3. If the hook reports `SearXNG returned 403: format=json is disabled on this instance`, enable JSON format on that SearXNG deployment or switch to another backend
|
||||
An unsafe trace-file override is ignored. Trace writes are best effort and do
|
||||
not change launch or search results.
|
||||
|
||||
### I still want Gemini/OpenCode/Grok fallback
|
||||
## Diagnostics
|
||||
|
||||
Those providers remain supported, but they are no longer the primary path. Enable them explicitly in `config.yaml` if you want them as last-resort fallback.
|
||||
By default, trace records are written under
|
||||
`~/.ccs/logs/websearch-trace.jsonl`. They correlate launch preparation, MCP
|
||||
exposure, tool calls, provider attempts, provider success/failure, and session
|
||||
summaries.
|
||||
|
||||
### I need to see whether CCS exposed WebSearch or the model bypassed it
|
||||
Normal trace metadata uses a query fingerprint and length instead of the raw
|
||||
query. Provider-failure records can also include `error` detail returned by a
|
||||
provider implementation, including HTTP response excerpts or CLI stderr.
|
||||
That provider-generated detail is not guaranteed to exclude query text or
|
||||
other sensitive content unless the implementation redacts it. Treat the trace
|
||||
file as sensitive operational data.
|
||||
|
||||
Run the launch with `CCS_WEBSEARCH_TRACE=1` (or `CCS_DEBUG=1`). CCS writes a JSONL trace to `~/.ccs/logs/websearch-trace.jsonl` with:
|
||||
For delegated/headless sessions, a likely-bypass summary means the tool was
|
||||
exposed but no WebSearch call occurred and another allowed tool path was used.
|
||||
|
||||
1. source-side launch records from CCS (`ccs_websearch_launch`)
|
||||
2. MCP exposure and call records (`mcp_initialize`, `mcp_tools_list`, `mcp_tool_call_*`)
|
||||
3. provider attempt and winner records (`websearch_provider_attempt`, `websearch_provider_success`)
|
||||
4. session summaries (`mcp_session_summary`, and headless `headless_websearch_summary` when applicable)
|
||||
When search is unavailable:
|
||||
|
||||
Queries are fingerprinted (`queryHash`, `queryLength`) instead of logged raw by default. For headless/delegation runs, `headless_websearch_summary.likelyBypassed=true` means the MCP tool was exposed, no WebSearch call occurred, and Claude fell back to `Bash` or `WebFetch`.
|
||||
1. confirm `websearch.enabled` and at least one provider are enabled;
|
||||
2. check readiness in the dashboard;
|
||||
3. verify API keys or the SearXNG base URL without printing secrets;
|
||||
4. enable `CCS_WEBSEARCH_TRACE=1` for one launch;
|
||||
5. inspect provider attempt and cooldown events.
|
||||
|
||||
### WebSearch returns no results
|
||||
|
||||
1. Check `websearch.enabled: true`
|
||||
2. Keep DuckDuckGo enabled unless you have a strong reason to disable it
|
||||
3. If using Exa, Tavily, or Brave, verify the matching API key
|
||||
4. Run with `CCS_DEBUG=1` for runtime logs, or `CCS_WEBSEARCH_TRACE=1` for correlated launch/MCP/provider traces
|
||||
5. If DuckDuckGo returns a non-result HTML error, retry later or enable another provider. CCS now treats that as a provider failure instead of a false empty result.
|
||||
|
||||
## Security Considerations
|
||||
|
||||
- API keys entered from the dashboard are stored in `~/.ccs/config.yaml` under `global_env` and injected as environment variables at runtime
|
||||
- Shell-exported keys still work and are detected as external environment input
|
||||
- Never commit API keys to version control
|
||||
- Use the dashboard only on trusted machines, and protect `~/.ccs/config.yaml` with normal user-level filesystem permissions
|
||||
Focused coverage lives under
|
||||
[`tests/unit/utils/websearch/`](../tests/unit/utils/websearch/),
|
||||
[`tests/unit/hooks/`](../tests/unit/hooks/), and
|
||||
[`tests/unit/targets/settings-profile-websearch-launch.test.ts`](../tests/unit/targets/settings-profile-websearch-launch.test.ts).
|
||||
Reference in new issue
Block a user