Merge pull request #1669 from kaitranntt/kai/docs/1666-pruning-freshness

docs: prune stale guides and enforce freshness
This commit is contained in:
Kai (Tam Nhu) Tran authored and GitHub committed 2026-07-26 22:09:35 -04:00
commit b09f8191f3
25 files changed
+687 -2331

No files matched your search

+2 -2
View File
@@ -1,6 +1,6 @@
name: Documentation improvement
description: Report missing, outdated, or confusing docs.
title: "docs: "
title: 'docs: '
labels:
- documentation
body:
@@ -24,7 +24,7 @@ body:
attributes:
label: File or page
description: Path or URL if you know it.
placeholder: README.md or docs/cursor-integration.md
placeholder: README.md or https://docs.ccs.kaitran.ca/providers/oauth/cursor
- type: textarea
id: problem
attributes:
+35 -14
View File
@@ -1,29 +1,44 @@
name: Docs – Quickstart Snippet Parity
name: Docs – Parity and Freshness
on:
push:
paths:
- "docs/quickstart-snippet.md"
- "README.md"
- "docker/README.md"
- "tests/docs/quickstart-parity.sh"
- ".github/workflows/docs-parity.yml"
- 'docs/quickstart-snippet.md'
- 'README.md'
- 'docker/README.md'
- 'macos-bar/README.md'
- 'docs/**'
- '.github/ISSUE_TEMPLATE/**'
- 'src/**'
- 'scripts/hardening-inventory.js'
- 'scripts/maintainability-metrics.js'
- 'scripts/runtime-source-classifier.js'
- 'tests/docs/quickstart-parity.sh'
- 'tests/docs/documentation-freshness.js'
- '.github/workflows/docs-parity.yml'
pull_request:
paths:
- "docs/quickstart-snippet.md"
- "README.md"
- "docker/README.md"
- "tests/docs/quickstart-parity.sh"
- ".github/workflows/docs-parity.yml"
- 'docs/quickstart-snippet.md'
- 'README.md'
- 'docker/README.md'
- 'macos-bar/README.md'
- 'docs/**'
- '.github/ISSUE_TEMPLATE/**'
- 'src/**'
- 'scripts/hardening-inventory.js'
- 'scripts/maintainability-metrics.js'
- 'scripts/runtime-source-classifier.js'
- 'tests/docs/quickstart-parity.sh'
- 'tests/docs/documentation-freshness.js'
- '.github/workflows/docs-parity.yml'
jobs:
quickstart-parity:
name: Assert quickstart snippet matches in README.md and docker/README.md
docs-parity:
name: Assert documentation parity and freshness
if: >-
contains(fromJSON('["COLLABORATOR","MEMBER","OWNER"]'), github.event.pull_request.author_association)
|| github.event_name == 'push'
runs-on: [self-hosted, linux, x64, cliproxy]
steps:
- name: Checkout
uses: actions/checkout@v4
@@ -32,3 +47,9 @@ jobs:
- name: Run quickstart parity check
run: bash tests/docs/quickstart-parity.sh
- name: Validate documentation pointers and relative links
run: node tests/docs/documentation-freshness.js
- name: Validate generated hardening inventory
run: node scripts/hardening-inventory.js --check
+4 -3
View File
@@ -94,7 +94,8 @@ The proxy also supports request-time `profile:model` selectors, scenario-based
model routing through `proxy.routing`, and explicit activation helpers such as
`ccs proxy activate --fish`.
Guide: [OpenAI-Compatible Provider Routing](./docs/openai-compatible-providers.md)
Guide:
[OpenAI-Compatible Provider Routing](https://docs.ccs.kaitran.ca/features/proxy/openai-compatible-providers).
### Related Project: claude-code-router
@@ -166,7 +167,7 @@ CCS can provision first-class local tools like WebSearch and image analysis for
third-party launches instead of leaving you to wire them by hand. Browser
automation now has a first-class setup path as well. Deep dive:
[WebSearch](https://docs.ccs.kaitran.ca/features/ai/websearch) |
[Browser Automation](./docs/browser-automation.md).
[Browser Automation](https://docs.ccs.kaitran.ca/features/workflow/browser-automation).
## Docs Matrix
@@ -183,7 +184,7 @@ reference material.
| Compare OAuth providers, Claude accounts, and API profiles | [Provider Overview](https://docs.ccs.kaitran.ca/providers/concepts/overview) |
| Learn the dashboard structure and feature pages | [Dashboard Overview](https://docs.ccs.kaitran.ca/features/dashboard/overview) |
| Configure profiles, paths, and environment variables | [Configuration](https://docs.ccs.kaitran.ca/getting-started/configuration) |
| Understand browser attach vs Codex browser tooling | [Browser Automation](./docs/browser-automation.md) |
| Understand browser attach vs Codex browser tooling | [Browser Automation](https://docs.ccs.kaitran.ca/features/workflow/browser-automation) |
| Keep OpenCode aligned with your live CCS setup | [OpenCode Sync Plugin](https://docs.ccs.kaitran.ca/features/workflow/opencode-sync) |
| Browse every command and flag | [CLI Commands](https://docs.ccs.kaitran.ca/reference/cli-commands) |
| Recover from install, auth, or provider failures | [Troubleshooting](https://docs.ccs.kaitran.ca/reference/troubleshooting) |
+46 -258
View File
@@ -1,273 +1,61 @@
# Browser Automation
# Browser Automation Developer Contract
Last Updated: 2026-05-11
User setup and troubleshooting live in the canonical
[Browser Automation guide](https://docs.ccs.kaitran.ca/features/workflow/browser-automation).
This local file retains security-sensitive runtime invariants that contributors
must preserve until the public guide covers them fully.
CCS provides browser automation through two separate runtime paths:
## Exposure And Runtime Ownership
- **Claude Browser Attach**: reuses a running Chrome/Chromium session through the CCS-managed local `ccs-browser` MCP runtime
- **Codex Browser Tools**: injects Playwright MCP tooling into Codex-target launches
- Claude Browser Attach and Codex Browser Tools are separate lanes; they do not
promise a shared browser session.
- New installs and upgrades without saved browser settings default both lanes
to `enabled: false` and `policy: manual`.
- `--browser` and `--no-browser` are one-run exposure overrides. They do not
change saved policy.
- CCS owns `mcpServers.ccs-browser`, the
`~/.ccs/mcp/ccs-browser-server.cjs` runtime, and Codex `ccs_browser`
overrides. Generic MCP editors are not the primary setup surface.
These are related, but they are not the same implementation and they do not promise a shared browser session.
On new installs, and on upgrades that do not already have explicit browser settings, both lanes
start **disabled** and **manual** so browser tooling is not auto-exposed until you opt in.
## How Browser Automation Works
### Claude Browser Attach
Claude-target CCS launches can provision a managed local MCP server named `ccs-browser`.
That path is designed for workflows where you want Claude to interact with a browser session
that already has useful authenticated state.
Claude Browser Attach requires a browser launched in attach mode with remote debugging
enabled. A recent Chrome update alone is not sufficient.
### Codex Browser Tools
Codex-target CCS launches use a separate managed path: CCS injects Playwright MCP overrides
for the `ccs_browser` runtime config entry.
This is configured from the same Browser settings surface, but it is distinct from Claude
Browser Attach.
## Configuration
### Via Dashboard
Open `ccs config` -> `Settings` -> `Browser`.
The Browser screen exposes two sections:
- **Claude Browser Attach**
- enable/disable the Claude attach lane
- choose the Chrome user-data directory
- set the expected DevTools port
- review readiness and next-step guidance
- copy a generated browser launch command
- **Codex Browser Tools**
- enable/disable CCS-managed browser tooling for Codex-target launches
- review whether the detected Codex build supports managed browser overrides
Browser policy controls are CLI-first in this release. The dashboard remains the shared setup and
status surface, while `ccs browser policy` is the authoritative place to decide whether browser
tooling is auto-exposed or kept manual by default. Fresh installs, plus upgrades without an
existing browser section, surface both lanes as off/manual until you explicitly enable them.
### Via CLI
```bash
ccs help browser
ccs browser setup
ccs browser status
ccs browser doctor
ccs browser policy
ccs browser policy --all manual
```
Use `ccs browser setup` for the primary one-command setup path. Use `ccs browser status` for
the current state, `ccs browser doctor` for read-only troubleshooting guidance, and
`ccs browser policy` to control default browser exposure. If you only want browser access for one
run, keep policy manual and add `--browser` to that launch.
### Via Config File
Edit `~/.ccs/config.yaml`:
```yaml
browser:
claude:
enabled: false
policy: manual
user_data_dir: "~/.ccs/browser/chrome-user-data"
devtools_port: 9222
codex:
enabled: false
policy: manual
```
Notes:
- `claude.policy` and `codex.policy` accept `auto` or `manual`
- `claude.user_data_dir` is a **Chrome user-data directory**, not a display-name browser profile
- `claude.devtools_port` is the expected remote debugging port for attach mode
- `codex.enabled` controls whether CCS injects browser tooling into Codex-target launches
- New installs, plus upgrades without saved browser settings, default both lanes to `enabled: false` and `policy: manual`
- `manual` keeps the lane configured but hidden until a launch explicitly opts in with `--browser`
## Runtime Policy Controls
CCS now separates **lane enablement** from **default exposure policy**:
- `enabled: false`
- the lane is off; this is the default for both lanes on new installs and upgrades without saved browser settings
- `enabled: true` + `policy: auto`
- the lane is exposed automatically on matching launches
- `enabled: true` + `policy: manual`
- the lane stays configured, but CCS keeps browser tooling hidden unless the current launch uses
`--browser`
One-run launch overrides:
```bash
ccs browser policy --all manual
ccs glm --browser "inspect the page"
ccs glm --no-browser "summarize the docs"
ccs default --target codex --browser "use the browser tools for this run"
```
- `--browser` forces browser tooling on for the current launch when that lane is enabled
- `--no-browser` suppresses browser tooling for the current launch even when policy is `auto`
## Environment Variable Overrides
CCS still supports environment-variable overrides for backward compatibility.
| Variable | Description |
|----------|-------------|
| `CCS_BROWSER_USER_DATA_DIR` | Preferred override for Claude Browser Attach user-data dir |
| `CCS_BROWSER_PROFILE_DIR` | Legacy alias for the same attach directory |
| `CCS_BROWSER_DEVTOOLS_PORT` | Explicit DevTools port override |
| `CCS_BROWSER_INTERCEPT_FULFILL_MODE=enabled` | Dangerous local-testing opt-in for Browser MCP response fulfillment; disabled by default |
| `CCS_BROWSER_UPLOAD_ROOTS` | Optional `path.delimiter`-separated allowlist for local files that browser upload tools may read |
| `CCS_BROWSER_DOWNLOAD_ROOTS` | Optional `path.delimiter`-separated allowlist for caller-provided browser download directories |
If an override is active, Browser status surfaces should report that the current session is being
managed externally by environment variables.
Browser MCP request interception can continue or fail matched requests by default. Synthetic response
fulfillment (`action: fulfill`) is more sensitive because it can serve caller-supplied response
content inside the attached browser's target origin. CCS therefore hides and blocks fulfillment unless
`CCS_BROWSER_INTERCEPT_FULFILL_MODE=enabled` is set for a trusted local test session.
The saved browser policy still controls default exposure. Env overrides change the effective attach
path/port for the current shell; they do not bypass `policy: manual`.
Override precedence is:
## Attach Override Precedence
1. `CCS_BROWSER_USER_DATA_DIR`
2. `CCS_BROWSER_PROFILE_DIR`
3. the persisted `browser.claude.user_data_dir` config value
2. legacy `CCS_BROWSER_PROFILE_DIR`
3. persisted `browser.claude.user_data_dir`
Config-backed Browser Attach always passes an explicit DevTools port to the runtime, even when the
effective value is the default `9222`. Metadata-based port discovery is preserved only for the
legacy `CCS_BROWSER_PROFILE_DIR` flow when `CCS_BROWSER_DEVTOOLS_PORT` is not set.
Config-backed attach always passes an explicit DevTools port, including the
default `9222`. Metadata-based port discovery exists only for the legacy
profile-dir override when `CCS_BROWSER_DEVTOOLS_PORT` is unset.
### Browser File Transfer Safety
## Interception Safety
Claude Browser Attach file-transfer tools intentionally use a deny-by-default filesystem boundary:
Request interception may continue or fail matched requests by default.
Synthetic fulfillment can serve caller-supplied content inside the target
origin, so it must stay hidden and blocked unless
`CCS_BROWSER_INTERCEPT_FULFILL_MODE=enabled` is explicitly set for a trusted
local test.
- Downloads without an explicit `downloadPath` go to a CCS-created temporary session directory.
- A caller-provided `downloadPath` must be inside that temporary session directory or inside one of
the directories listed in `CCS_BROWSER_DOWNLOAD_ROOTS`.
- Local upload and drag-and-drop files must be inside the temporary session download directory or
inside one of the directories listed in `CCS_BROWSER_UPLOAD_ROOTS`.
- Hidden path segments and common secret locations/files, such as `.ssh`, `.aws`, `.ccs`,
`.claude`, `.env`, and private-key filenames, are rejected even inside an allowed root.
- Each file-transfer call is limited to 10 files, and each local file must be at most 10 MiB.
## Event Observation Safety
Set upload/download roots only to purpose-built scratch directories. Do not point these variables at
your home directory, a source checkout with secrets, or a real cloud/tooling config directory.
- `browser_wait_for_event` requires `urlIncludes` for network-request events.
- Download events require either `urlIncludes` or
`suggestedFilenameIncludes`.
- Returned navigation, request, and download URLs must redact query strings,
fragments, and path-scoped bearer values before reaching the MCP caller.
## Managed Runtime Files
## File Transfer Safety
- `~/.claude.json` -> CCS manages `mcpServers.ccs-browser` for Claude Browser Attach
- `~/.ccs/mcp/ccs-browser-server.cjs` -> local Claude Browser Attach MCP runtime
- `Codex runtime config overrides` -> CCS manages the `ccs_browser` MCP entry for Codex-target launches
Browser file transfer is deny-by-default:
Do not treat the generic Codex MCP editor as the primary browser setup path. CCS-managed browser
entries should be configured from `Settings -> Browser`.
- implicit downloads use a CCS-created temporary session directory;
- explicit download paths must remain inside that session directory or a
`CCS_BROWSER_DOWNLOAD_ROOTS` allowlisted root;
- uploads and drag-and-drop files must remain inside the session directory or a
`CCS_BROWSER_UPLOAD_ROOTS` allowlisted root;
- hidden segments and common secret locations/files (`.ssh`, `.aws`, `.ccs`,
`.claude`, `.env`, private keys) remain denied inside allowed roots;
- each call permits at most 10 files, each no larger than 10 MiB.
## Primary Setup Flow
The shortest supported setup path is:
```bash
ccs browser setup
```
That flow:
1. enables Claude Browser Attach in the saved CCS browser config
2. leaves launch exposure under the saved policy, so `policy: manual` still requires `--browser`
3. keeps the configured DevTools port normalized
4. creates the configured browser user-data directory if needed
5. prints the exact browser launch command for the current platform
6. re-checks readiness and reports the next step if Chrome still needs manual attention
## Launching Chrome For Claude Attach
Claude Browser Attach needs a browser launched with remote debugging.
Typical examples:
```bash
# macOS
open -na "Google Chrome" --args --remote-debugging-port=9222 --user-data-dir="$HOME/.ccs/browser/chrome-user-data"
# Linux
google-chrome --remote-debugging-port=9222 --user-data-dir="$HOME/.ccs/browser/chrome-user-data"
# Windows
chrome.exe --remote-debugging-port=9222 --user-data-dir="%USERPROFILE%\\.ccs\\browser\\chrome-user-data"
```
Using a dedicated CCS browser data dir is recommended. It avoids profile-locking issues and keeps
automation state separate from your daily browser profile.
When Claude Browser Attach uses the recommended managed path (`~/.ccs/browser/chrome-user-data`),
CCS now creates that directory automatically the first time it needs it. After that bootstrap step,
the remaining requirement is a running Chrome session started with `--remote-debugging-port`.
## Troubleshooting
### Browser status says Claude Browser Attach is disabled
Run `ccs browser setup`, enable Claude Browser Attach in `Settings -> Browser`, or edit the
browser config block in `~/.ccs/config.yaml`.
### Browser status says the path is missing
The configured Chrome user-data directory does not exist yet.
1. Run `ccs browser setup`
2. If Chrome still is not ready, use the generated launch command
3. Rerun `ccs browser doctor`
If you are using the CCS-managed default path, this usually means the path could not be created
automatically and now needs manual attention.
### Browser status says no running browser session was found
CCS could not find usable DevTools attach metadata for the configured user-data directory.
1. Run `ccs browser setup`
2. If needed, make sure Chrome was started with `--remote-debugging-port=<port>`
3. Make sure it is using the same `user_data_dir` configured in CCS
4. Rerun `ccs browser doctor`
For the CCS-managed default path, this is the normal first-run state after CCS bootstraps the
directory for you.
### Browser status says the DevTools endpoint is unreachable
CCS found attach metadata, but the endpoint did not answer successfully.
1. Run `ccs browser setup`
2. If needed, restart the attach browser session
3. Confirm the expected port matches the real remote debugging port
4. Rerun `ccs browser status`
### Codex Browser Tools are unavailable
Codex browser tooling depends on a Codex build that supports `--config` overrides.
If CCS reports `unsupported_build`, upgrade Codex and rerun `ccs browser status`.
## Security Notes
- Browser automation may operate inside authenticated browser sessions
- Prefer a dedicated automation user-data dir instead of your everyday browser profile
- Do not commit browser paths, secrets, or generated session state to version control
- Treat `~/.ccs/config.yaml`, `~/.claude.json`, and the browser user-data directory as local machine state
- `browser_wait_for_event` requires explicit scoping for network request events (`urlIncludes`) and download events (`urlIncludes` or `suggestedFilenameIncludes`)
- Event details redact observed navigation, request, and download URLs before returning them to the MCP caller so observed browser metadata does not expose query strings, fragments, or path-scoped bearer values
Allowlist only purpose-built scratch directories. Never allowlist a home
directory, source checkout containing secrets, or real tooling configuration
directory.
-144
View File
@@ -1,144 +0,0 @@
# CCS Bar — Native macOS Menu Bar App
CCS Bar is a native macOS menu-bar app that shows live subscription quota and usage at a glance for your Claude Code, Codex, and CLIProxy accounts, without opening the dashboard.
## What It Is
CCS Bar is a thin client of the CCS local web-server. It never talks to a provider directly: every call goes to `localhost`, and CCS performs any provider fetch server-side. Opening the menu fires a debounced force-refresh so the glance reflects live data without blocking the UI.
It is macOS only.
## What It Shows
- Per-account quota percent and reset countdown
- Account tier
- Today, 7-day, and 30-day cost
- A 30-day usage sparkline
- Account state (active, paused, default)
- Native subscription rows for Claude Code and Codex
## Requirements
- macOS
- CCS CLI installed and configured (`ccs config` works)
- The CCS web-server reachable on loopback. `ccs bar` (or `ccs bar launch`) starts it for you.
## Install
```bash
ccs bar install
```
This downloads `CCS-Bar.app.zip` from the floating `ccs-bar-latest` GitHub release and installs `CCS Bar.app` into `~/Applications`. Downloads are restricted to `github.com` and `objects.githubusercontent.com`, and extraction is guarded against zip-slip.
If `CCS Bar.app` is already installed, the command shows the current version and proceeds as a reinstall.
After installation, CCS reads the app version directly from the bundle's `Info.plist` and pins it to `~/.ccs/bar/.version`. It then performs a reachability check against the bar API (`GET /api/bar/summary`). A 404 response means the running CCS server predates CCS Bar support — update CCS to a version that includes CCS Bar, then restart `ccs bar`.
After a successful install, CCS leaves the macOS Gatekeeper quarantine marker in place so macOS can perform its normal first-launch verification. CCS then asks whether to launch CCS Bar immediately (default: yes). Pass `--launch` to skip the prompt and launch right away, or `--no-launch` to suppress the prompt entirely:
```bash
ccs bar install --launch # install and launch immediately
ccs bar install --no-launch # install, skip launch prompt
```
### Gatekeeper note
The install command does not automatically clear the macOS Gatekeeper quarantine attribute on the downloaded app. This preserves macOS first-launch verification for the floating release download.
If macOS blocks the app on first launch, make an explicit trust decision by right-clicking the app and choosing Open.
## Run
There are two ways to run CCS Bar, and both share one background server.
**Open the app.** Launch `CCS Bar` from Spotlight, Finder, or the Dock. The first time you open the menu, the app looks for a running CCS server and, if it does not find one, starts it in the background using the launch details recorded at install (`~/.ccs/bar/launch.json`). No terminal needed.
**Or use the CLI.**
```bash
ccs bar # alias: ccs bar launch
```
`ccs bar` probes for a running server (ports 3000, 3001, 3002, 8000, 8080, with the previous `bar.json` port first). If one is live it is reused; otherwise `ccs bar` starts the server as a detached background process and returns your prompt right away, then opens the app. The server keeps running after the command exits, so closing the terminal does not take the bar offline.
### Server lifecycle
```bash
ccs bar serve # run the server in the current terminal (advanced; ccs bar runs this detached for you)
ccs bar stop # stop the background server
ccs bar status # report whether the server is running
```
### Files under ~/.ccs
| File | Purpose |
| --- | --- |
| `~/.ccs/bar.json` | Discovery file the app reads to find the live server |
| `~/.ccs/bar/launch.json` | How to start the server (recorded at install) so the app can start it without a shell PATH |
| `~/.ccs/bar/server.pid` | PID of the background server, used by `ccs bar stop` and `ccs bar status` |
| `~/.ccs/bar/serve.log` | stdout and stderr of the background server |
`~/.ccs/bar.json` looks like this:
```json
{ "baseUrl": "http://127.0.0.1:3000", "port": 3000, "authMode": "loopback" }
```
The Swift app reads `~/.ccs/bar.json` to find the server, and falls back to probing the candidate ports if it is missing or stale.
## Loopback / Localhost Requirement
CCS Bar talks only to `http://127.0.0.1:<port>`. v1 supports `authMode: "loopback"` only, meaning dashboard auth disabled on localhost.
If you bind the dashboard beyond localhost (for example `--host` set to a non-loopback address) with dashboard auth disabled, the bar's read endpoints (`GET /api/bar/summary`, `GET /api/bar/analytics`) are refused for non-loopback callers, and the app cannot reach the server. Keep the dashboard on loopback for CCS Bar to work.
## Uninstall
```bash
ccs bar uninstall
```
This removes `~/Applications/CCS Bar.app` and the installed version pin. It is a no-op if the app is not present.
## Troubleshooting
- Install fails with "server predates CCS Bar" or bar API returns 404: the CCS server running does not yet include CCS Bar. Update CCS (`npm i -g ccs@latest` or equivalent), then restart `ccs bar`.
- Server failed to start: `ccs bar` first checks whether a CCS server is already running on the candidate ports (3000, 3001, 3002, 8000, 8080) and reuses it if found. A true failure here means a non-CCS process is occupying all candidate ports. Free one of those ports and re-run `ccs bar`. Check `~/.ccs/bar/serve.log` for the background server's output.
- App won't open (Gatekeeper): right-click the app and choose Open to make an explicit trust decision.
- Menu shows "CCS is not running": open the menu again to let the app start the server, or run `ccs bar status` to check and `ccs bar` to start it.
- Quota not updating: re-open the menu to force a refresh, or confirm the server is still reachable on loopback.
## Development
The source lives in `macos-bar/`. Contributors can build and run the logic checks with a Swift 5.9+ toolchain (CommandLineTools is enough, full Xcode not required):
```bash
swift build # build all targets, including the app
swift run ccs-bar-check # run the logic tests
```
## Releasing
The app ships as a single floating GitHub release asset, `CCS-Bar.app.zip` under the `ccs-bar-latest` tag, which is what `ccs bar install` downloads. The version comes from one file: `macos-bar/VERSION` (a single line of semver). Bump it in the same PR when you want a new number; the asset is always the latest build regardless.
### Automatic (preferred)
The `Bar Release` workflow (`.github/workflows/bar-release.yml`) builds and publishes the asset automatically. It is scoped tightly so it never affects other PRs or CI:
- It runs only on a push to `main` that touches `macos-bar/**`, or a manual run from the Actions tab (`workflow_dispatch`) when the selected ref is `main`.
- It runs only on the dedicated self-hosted macOS runner (label `ccs-bar`); the Linux CI runners never pick it up and it never competes for them.
So bar changes reach users when they land on `main` (the stable cadence). To cut a release without a code change, or to re-publish, trigger the workflow manually with `main` selected as the workflow ref.
### Manual fallback
From a macOS machine with the Swift toolchain and `gh`:
```bash
cd macos-bar
./Scripts/package_app.sh # uses macos-bar/VERSION; pass a version to override
gh release upload ccs-bar-latest dist/CCS-Bar.app.zip --clobber
```
Builds are ad-hoc signed by default. Developer ID notarization (for the no-prompt public install) is available via `CCS_BAR_SIGNING=developer-id` once a paid Apple cert is configured.
+58 -202
View File
@@ -1,217 +1,73 @@
# Codex Auth Profile Isolation (`ccsx auth`)
# Codex Auth Developer Contract
Run two Codex accounts simultaneously — one per terminal — with full auth isolation.
The canonical [Codex Adapter guide](https://docs.ccs.kaitran.ca/features/workflow/codex-adapter)
owns user setup. This local contract documents active `ccsx auth` invariants
that contributors and operators must preserve.
## Why
## Command Surface
Codex stores its OAuth credentials in a single directory (`~/.codex/`). When you run two
`codex` sessions in separate terminals, they both write to the same `auth.json`. A token
refresh in one session overwrites the other's credentials.
`ccsx auth` owns `create`, `login`, `switch`, `use`, `show`, `remove`, and
`import-default`. Keep syntax and option changes sourced from
[`src/codex-auth/codex-auth-help.ts`](../src/codex-auth/codex-auth-help.ts)
rather than copying a long command reference here.
`ccsx auth` solves this by giving each account its own profile directory under
`~/.ccs/codex-instances/<name>/`. Each profile holds its own `auth.json` and
`history.jsonl`, plus its own session data. Shared `config.toml`, `agents/`, `skills/`,
and plugin cache resources come from `~/.codex/` so configuration and installed plugin
skills stay in sync across profiles.
- `create <name>` is idempotent and starts native `codex login` for new
profiles. `--force` repairs shared resources without replacing `auth.json`.
- `switch <name>` changes the persistent registry default.
- `use <name>` emits only shell-evaluable `CODEX_HOME` and
`CCS_CODEX_PROFILE` assignments to stdout. It affects the current shell after
`eval`/`source`; diagnostics stay on stderr.
- `ccsx <name>` launches a named profile directly without changing the
persistent default.
## Quick start (4 commands)
## Import Safety
```bash
# Create and authenticate two profiles
ccsx auth create work # creates ~/.ccs/codex-instances/work/ and prompts for login
ccsx auth create personal # same for personal account
`import-default <name>` imports native `~/.codex/auth.json` without deleting the
source. The implementation in
[`import-default-command.ts`](../src/codex-auth/commands/import-default-command.ts)
must continue to:
# Activate per terminal (ephemeral — only this shell)
# Terminal A:
eval "$(ccsx auth use work)"
codex
- refuse import while a current-user Codex process may be refreshing tokens,
unless the operator explicitly accepts the race with
`--force-while-running`;
- retry and validate JSON/JWT shape, reject CLIProxy auth-file formats, and fail
without registering a profile when a torn write persists;
- write the destination atomically with private permissions;
- omit history and sessions unless `--with-history` is requested;
- refuse an existing profile unless `--force` is used, and preserve its current
`auth.json` as `auth.json.bak-<timestamp>` before overwrite.
# Terminal B:
eval "$(ccsx auth use personal)"
codex
## Storage And Cross-Platform Fallback
# Or launch a named profile directly through ccsx
ccsx work
```
## Two-terminal example
```bash
# Terminal A — work account
eval "$(ccsx auth use work)"
codex # runs with CODEX_HOME=~/.ccs/codex-instances/work
# Terminal B — personal account (simultaneously)
eval "$(ccsx auth use personal)"
codex # runs with CODEX_HOME=~/.ccs/codex-instances/personal
# No token clobbering. Each session refreshes its own auth.json only.
```
## Command reference
| Command | Description |
|---------|-------------|
| `ccsx auth create <name>` | Create profile dir + auto-login |
| `ccsx <name>` | Launch a named Codex auth profile |
| `ccsx auth login <name>` | (Re-)authenticate an existing profile |
| `ccsx auth switch <name>` | Set the persistent default profile for future `ccsx` launches |
| `ccsx auth use <name>` | Emit shell exports for this shell only (use with `eval`) |
| `ccsx auth show [name]` | List all profiles or show details for one |
| `ccsx auth remove <name>` | Delete profile dir + registry entry |
| `ccsx auth import-default <name>` | Migrate legacy `~/.codex/auth.json` into a new profile |
## Persistent vs ephemeral switching
| Method | Scope | How |
|--------|-------|-----|
| `ccsx <name>` | One launch | Resolves `<name>` from the Codex profile registry |
| `ccsx auth switch <name>` | Future `ccsx` launches | Writes to `~/.ccs/codex-profiles.yaml` |
| `eval "$(ccsx auth use <name>)"` | Current shell only | Sets `CODEX_HOME` + `CCS_CODEX_PROFILE` in your shell |
Native `codex` shells only see the persistent default when launched through the `ccsx`
Codex runtime. For an already-open shell or a plain native `codex` binary, use `auth use`.
Do not use `ccs persist codex` for Claude Code or the Claude Code Extension. That path
would persist Claude settings that send Claude traffic through the Codex translator. CCS
blocks Codex CLIProxy profiles from Claude extension setup; use `ccsxp` or
`ccs codex --target codex` for ChatGPT/Codex subscriptions. If old settings were already
persisted, clear them with:
```bash
ccs persist default --yes
```
The command prints a config receipt after writing settings: cleared managed keys,
written managed keys, whether and where any `/api/provider/codex` translator URL
remains, and the native Codex targets to use next.
Shell syntax for `use`:
```bash
# bash / zsh
eval "$(ccsx auth use work)"
# fish
ccsx auth use work | source
# PowerShell
ccsx auth use work | Invoke-Expression
```
## Migration from `~/.codex`
If you already have a logged-in session in `~/.codex/auth.json`, import it without
disturbing the original:
```bash
# Auth only (default — recommended)
ccsx auth import-default legacy
# Auth + history + sessions (opt-in)
ccsx auth import-default legacy --with-history
# Make it the default
ccsx auth switch legacy
```
The source `~/.codex/` directory is **never modified**. If `import-default` is not run,
`codex` continues to work exactly as before.
### Torn-write safety
Codex writes `auth.json` with truncate+write (not atomic rename). Running
`import-default` while a token refresh is in flight can produce a corrupt copy.
The command detects a running `codex` process via `pgrep` and refuses unless you
pass `--force-while-running`. The safest approach is to quit Codex before
importing.
## Dashboard
The CCS dashboard shows active profile metadata at the **Auth Profiles** tab on the
Codex page:
- Profile name and whether it is the current default
- Decoded email address (from `id_token` — no signature verification; display only)
- Plan tier (Plus, Pro, Free) when present in the token
- Last-used timestamp
No OAuth tokens are ever returned by the API endpoint or shown in the UI.
## Profile disk layout
```
```text
~/.ccs/
├── codex-profiles.yaml # Registry: version, default, profiles metadata
└── codex-instances/
└── <name>/
├── auth.json # OAuth credentials (Codex writes here)
├── history.jsonl # Per-profile prompt history (optional)
├── sessions/ # Per-profile chat session dirs (optional)
├── config.toml -> ~/.codex/config.toml (symlink — shared)
├── agents/ -> ~/.codex/agents/ (symlink — shared)
├── skills/ -> ~/.codex/skills/ (symlink — shared)
└── plugins/ # Profile-local parent; may hold local metadata
└── cache/ -> ~/.codex/plugins/cache/ (symlink — shared)
~/.codex/
├── config.toml # Single shared model/provider config
├── agents/ # Shared Codex agent role config files
├── skills/ # Shared Codex skills
└── plugins/
└── cache/ # Shared installed plugin payloads
├── codex-profiles.yaml
└── codex-instances/<name>/
├── auth.json, history.jsonl, sessions/ # profile-local
├── config.toml -> ~/.codex/config.toml
├── agents/ -> ~/.codex/agents/
├── skills/ -> ~/.codex/skills/
└── plugins/
└── cache/ -> ~/.codex/plugins/cache/
```
Only `plugins/cache/` is shared. The profile's parent `plugins/` directory remains a
real local directory so Codex can keep profile-specific plugin metadata beside the
shared cache.
The `plugins/` parent stays profile-local. Shared config, resources, and plugin
cache repair must preserve existing profile-local content. On Windows or other
systems where symlinks are unavailable, CCS copies missing shared content into
the profile and warns that later upstream edits will not propagate
automatically. See
[`codex-config-symlink.ts`](../src/codex-auth/codex-config-symlink.ts),
[`codex-profile-resources.ts`](../src/codex-auth/codex-profile-resources.ts),
and
[`codex-profile-plugin-cache.ts`](../src/codex-auth/codex-profile-plugin-cache.ts).
`ccsx auth create <name>` and direct `ccsx <name>` launches repair these links
idempotently before Codex starts. This keeps relative entries such as
`agents/foo.toml` valid and prevents stale first-launch skill warnings after a plugin
install or update changes the cache.
## `ccsx` And `ccsxp` Isolation
## Caveats
`ccsx auth` applies only to native Codex profiles. `ccsxp` ignores
`CCS_CODEX_PROFILE`, uses native `~/.codex` history by default, and routes
through its separate CLIProxy Codex pool. `CCSXP_CODEX_HOME` is its explicit
home override. Never make a `ccsx auth switch` silently redirect `ccsxp`, merge
their auth stores, or consume `ccsx` import backups as pool credentials.
### Windows symlinks
On Windows, creating symlinks requires Developer Mode or elevated privileges.
If symlink creation fails, CCS falls back to copying `config.toml`, `agents/`,
`skills/`, and the current `plugins/cache/` snapshot. Copies do not update live with
`~/.codex/`; after a plugin update, another profile launch or
`ccsx auth create <name> --force` repair copies newly missing cache entries. Existing
profile-local cache files are preserved.
### Native Codex project-local config warnings
`ccsx` preserves your current working directory. If you launch from your home directory,
native Codex can also see `~/.codex/config.toml` as `./.codex/config.toml`, a
project-local config file. Codex rejects user-level-only keys such as `model_providers`
and `notify` in project-local config. That warning comes from native Codex config
layering, not from the `ccsx auth` profile resource links. Launch from a project
directory or move project-local Codex config out of `$HOME/.codex/config.toml` if the
warning is noisy.
### `ccsx` vs `ccsxp`
`ccsx auth` profiles apply only to the **native `codex`** CLI. They have no effect on
`ccsxp` (the CLIProxy round-robin pool). `ccsxp` unconditionally sets its own
`CODEX_HOME` on startup and ignores `CCS_CODEX_PROFILE`.
If you run `eval "$(ccsx auth use work)"` and then invoke `ccsxp`, a notice is emitted
to stderr:
```
[i] CCS_CODEX_PROFILE is ignored by ccsxp; profile applies to native 'codex' only
```
### cmd.exe
`ccsx auth use` emits `set FOO=bar` syntax for cmd.exe. Native `eval` is not available
in legacy cmd — use PowerShell (`Invoke-Expression`) instead.
### Backup files from `--force`
When re-importing with `--force`, the existing `auth.json` is backed up as
`auth.json.bak-<timestamp>` in the profile directory. These accumulate over time; remove
them manually when no longer needed.
Behavior locks live under `tests/unit/codex-auth/` and
`tests/integration/codex-auth/`.
-165
View File
@@ -1,165 +0,0 @@
# Cursor IDE Integration
This guide covers the deprecated CCS-owned Cursor IDE bridge, including auth import, local daemon lifecycle, live probe checks, and dashboard controls.
`ccs cursor` now belongs to the CLIProxy-backed Cursor provider path.
Use `ccs legacy cursor` for the deprecated local bridge documented here.
## What It Provides
- OpenAI-compatible local endpoint powered by Cursor credentials.
- Anthropic-compatible local endpoint at `/v1/messages` for Claude-native clients.
- Cursor model list and chat completions via the local CCS daemon.
- Dedicated dashboard page: `ccs config` -> `Deprecated` -> `Cursor IDE (Legacy)`.
## What This Runtime Actually Does
`ccs legacy cursor` does not launch Cursor IDE itself.
The current workflow is:
1. import Cursor credentials from local SQLite or manual input
2. run a local CCS daemon on `127.0.0.1:<port>`
3. launch Claude Code against that daemon
4. have CCS translate requests to Cursor upstream
Treat this as a CCS-managed Cursor bridge, not a generic CLIProxy-backed provider path.
## Prerequisites
- Cursor IDE installed and logged in.
- CCS installed and configured (`ccs config` works).
- For auto-detect auth on macOS/Linux: `sqlite3` available in PATH.
## CLI Workflow
### 1) Enable integration
```bash
ccs legacy cursor enable
```
### 2) Import credentials
Auto-detect from Cursor local SQLite state:
```bash
ccs legacy cursor auth
```
Manual fallback:
```bash
ccs legacy cursor auth --manual --token <token> --machine-id <machine-id>
```
### 3) Start daemon
```bash
ccs legacy cursor start
```
### 4) Run a live probe
```bash
ccs legacy cursor probe
```
Use this to verify that the current build can complete one real authenticated request through the local daemon.
### 5) Run Cursor-backed Claude
```bash
ccs legacy cursor "explain this repo"
```
### 6) Verify status
```bash
ccs legacy cursor status
```
Use `ccs legacy cursor` with bare or normal Claude args to run through the local Cursor proxy.
The admin namespace remains available for setup and inspection:
```bash
ccs legacy cursor help
```
### 7) Stop daemon
```bash
ccs legacy cursor stop
```
## Supported Cursor Provider Path
For the supported CLIProxy-backed Cursor provider, use:
```bash
ccs cursor --auth
ccs cursor --accounts
ccs cursor --config
ccs cursor "task"
```
## Runtime Defaults
- Default port: `20129`
- `ghost_mode`: enabled
- `auto_start`: disabled
- Model list resolution: authenticated live fetch when available, with cached/default fallback.
- Request model validation: if a requested model is not present in the available Cursor model catalog, daemon falls back to the resolved default model.
- Daemon API surface: `POST /v1/chat/completions`, `POST /v1/messages`, and `GET /v1/models`.
- Live verification: `ccs legacy cursor probe` or `POST /api/cursor/probe`
These values are managed in unified config and can be updated from CLI or dashboard.
## Dashboard Usage
Open dashboard:
```bash
ccs config
```
Then navigate to `Cursor IDE (Legacy)` in the `Deprecated` section.
Available controls:
- Integration toggle (`enabled`)
- Auth actions (auto-detect, manual import)
- Daemon actions (start/stop)
- Runtime config (port, auto-start, ghost mode)
- Models list with searchable combobox filtering for large catalogs
- Raw editor for `~/.ccs/cursor.settings.json`
## Raw Settings and Unified Config Sync
Raw settings are stored in:
`~/.ccs/cursor.settings.json`
When raw settings include a local `ANTHROPIC_BASE_URL` port override, CCS synchronizes that port back into unified config so CLI and dashboard remain consistent.
## Troubleshooting
### `Not authenticated` or `expired` in `ccs cursor status`
- Re-run `ccs legacy cursor auth` (or manual auth command).
### `ccs legacy cursor probe` fails even though status is green
- `status` proves local config/auth/daemon readiness only.
- `probe` proves the live runtime path.
- If `probe` fails with upstream protocol errors, inspect the current CCS build first rather than assuming the local daemon is healthy.
### Auto-detect fails
- Ensure Cursor is logged in.
- Confirm `sqlite3` is installed or use manual import.
- Use manual auth import if needed.
### Daemon fails to start
- Check if port `20129` is in use.
- Change port in dashboard config tab, then retry `ccs legacy cursor start`.
-267
View File
@@ -1,267 +0,0 @@
# Dashboard Authentication CLI
Last Updated: 2026-05-05
CLI commands for managing CCS dashboard authentication.
## Overview
The CCS dashboard (`ccs config`) can be protected with username/password authentication. This is useful whenever the dashboard is reachable from another device, such as when you explicitly bind it beyond loopback with `ccs config --host 0.0.0.0`.
Authentication is **disabled by default** for backward compatibility, while `ccs config` binds to `localhost` by default. Use the CLI to configure and enable auth before exposing the dashboard to other devices.
CCS does **not** ship a default dashboard username or password. When someone opens the dashboard from a non-loopback/IP address before auth is enabled, the UI now shows a setup state instead of an ambiguous login form. The host owner must run `ccs config auth setup`, or the user should switch back to the localhost URL if they are on the same machine.
Docker note: the integrated `ccs docker` stack stores its config inside the running container volume, not in the outer shell's `~/.ccs`. For Docker deployments, run auth setup inside the container:
```bash
docker exec -it ccs-cliproxy ccs config auth setup
```
When auth stays disabled, CCS now applies a localhost-only fallback on sensitive management endpoints. Remote devices can still open the dashboard UI when you intentionally bind it beyond loopback, but write-capable routes such as AI Provider management and CLIProxy auth/status helpers reject non-loopback requests until you enable dashboard auth.
## Account Context Modes (Related Feature)
Dashboard auth and account context metadata are separate:
- `dashboard_auth`: protects dashboard access with username/password
- `accounts.<name>.context_mode/context_group`: controls isolated vs shared account context
- `accounts.<name>.shared_resource_mode`: controls plugins/commands/skills/agents/settings.json sharing
Account context is isolation-first. The recommended two-account route is:
```bash
ccs auth create work
ccs auth create personal
ccs work
ccs personal
```
Only enable history sync when both accounts should share local continuity while tokens stay separate:
| Mode | Default | Requirement |
|------|---------|-------------|
| `isolated` | Yes | No `context_group` required |
| `shared` | No (opt-in) | Valid non-empty `context_group` |
Shared continuity depth:
- `standard` (default): shares project workspace context only
- `deeper` (advanced opt-in): also syncs `session-env`, `file-history`, `shell-snapshots`, `todos`
`ccs auth show <profile>` reports credential isolation, shared resource mode, settings sync state, history lane, and whether plain `ccs` currently uses the same resume lane.
Non-bare account profiles share Claude-local resources with native Claude:
```text
~/.ccs/instances/<profile>/settings.json -> ~/.ccs/shared/settings.json -> ~/.claude/settings.json
```
This keeps ordinary Claude settings, plugins, commands, skills, and agents in sync without copying account tokens. Existing accounts can opt out or back in:
```bash
ccs auth resources work --mode profile-local
ccs auth resources work --mode shared
```
Local history is separate: if users want future plain `ccs` and `ccs ck` sessions to resume from the same account lane, run `ccs auth default ck` after backing up the current native lane with `ccs auth backup default`.
`context_group` normalization and validation:
- trim + lowercase + collapse internal whitespace to `-`
- allowed characters: lowercase letters, numbers, `_`, `-`
- must start with a letter
- max length: 64
- shared mode requires non-empty value after normalization
- `continuity_mode` is only valid when mode is `shared`
`PUT /api/config` behavior for account context:
- rejects invalid unified payloads
- rejects explicit `context_mode: shared` with invalid/empty `context_group`
- rejects invalid `continuity_mode` values
- normalizes valid shared `context_group` before save
- defaults missing shared `continuity_mode` to `standard`
- rejects `context_group` when mode is not `shared`
- rejects `continuity_mode` when mode is not `shared`
Dashboard accounts context editing:
- `PUT /api/accounts/:name/context` updates context mode/group/continuity for existing auth accounts
- rejects CLIProxy OAuth account keys for this route
- applies normalization/validation rules above
Shared resource editing:
- `PUT /api/accounts/:name/shared-resources` updates `shared_resource_mode` for existing auth accounts
- accepts only `shared` or `profile-local`
- rejects CLIProxy OAuth account keys for this route
- reconciles the account instance after metadata is updated
- Dashboard -> Accounts exposes this as a separate Resources action so it is not confused with History Sync.
- Dashboard -> Shared Resources shows the shared hub inventory for commands, skills, agents, plugins, and `settings.json`.
- The Plugins tab is registry-oriented: installed plugin entries come from `installed_plugins.json`, while internal cache/data/marketplace folders stay hidden unless a real plugin entry exists.
- Shared `settings.json` is read-only in the Shared Resources page and still edited through the settings surfaces that own those values.
## Commands
### `ccs config auth setup`
Interactive wizard to configure dashboard login.
```bash
$ ccs config auth setup
╭─────────────────────────────────╮
│ Dashboard Auth Setup │
╰─────────────────────────────────╯
[i] Configure username and password for dashboard access.
Password will be hashed with bcrypt before storage.
Username
Enter username: admin
Password
Minimum 8 characters
Enter password: ********
Confirm password: ********
[i] Hashing password...
[OK] Dashboard authentication configured
[i] Settings saved to ~/.ccs/config.yaml
[i] Username: admin
[i] Session timeout: 24 hours
Start dashboard: ccs config
Show status: ccs config auth show
Disable auth: ccs config auth disable
```
### `ccs config auth show`
Display current authentication status.
```bash
$ ccs config auth show
╭─────────────────────────────────╮
│ Dashboard Auth Status │
╰─────────────────────────────────╯
Configuration
[OK] Authentication: Enabled
[OK] Username: admin
[i] Session timeout: 24 hours
Commands
ccs config auth setup Configure authentication
ccs config auth disable Disable authentication
ccs config Open dashboard
```
### `ccs config auth disable`
Disable dashboard authentication with confirmation.
```bash
$ ccs config auth disable
╭─────────────────────────────────╮
│ Disable Dashboard Auth │
╰─────────────────────────────────╯
[!] This will disable login protection for the dashboard.
[i] Anyone with network access will be able to view the dashboard.
Disable authentication? [y/N]: y
[OK] Dashboard authentication disabled
[i] Credentials preserved - re-enable with: ccs config auth setup
```
### `ccs config auth --help`
Display usage information.
## Environment Variables
Environment variables override `config.yaml` values:
| Variable | Description |
|----------|-------------|
| `CCS_DASHBOARD_AUTH_ENABLED` | Enable/disable auth (`true`/`false`) |
| `CCS_DASHBOARD_USERNAME` | Username |
| `CCS_DASHBOARD_PASSWORD_HASH` | Bcrypt password hash |
### Generating a Password Hash
Use bcrypt to generate a hash:
```bash
# Using Node.js
node -e "console.log(require('bcrypt').hashSync('your-password', 10))"
# Using npx
npx bcrypt-cli hash "your-password"
```
## Configuration
Settings are stored in `~/.ccs/config.yaml`:
```yaml
# Dashboard Auth: Optional login protection for CCS dashboard
# Generate password hash: npx bcrypt-cli hash "your-password"
# ENV override: CCS_DASHBOARD_AUTH_ENABLED, CCS_DASHBOARD_USERNAME, CCS_DASHBOARD_PASSWORD_HASH
dashboard_auth:
enabled: true
username: "admin"
password_hash: "$2b$10$..."
session_timeout_hours: 24
```
## Security Notes
1. **Bcrypt hashing**: Passwords are hashed with bcrypt (10 rounds) before storage
2. **Session cookies**: Sessions use HTTP-only cookies (not accessible via JavaScript)
3. **Rate limiting**: Login attempts are rate-limited (5 per 15 minutes)
4. **Fail-closed remote writes**: When auth is disabled, sensitive management routes allow localhost only
5. **File permissions**: Config file is created with 0o600 permissions
## Troubleshooting
### "Authentication not configured"
Run `ccs config auth setup` to configure credentials.
If you are using the integrated Docker stack, run that command inside `ccs-cliproxy`. Running it on the outer host shell updates a different config directory and will not unlock the running dashboard.
### Forgot password
Run `ccs config auth setup` again to set a new password.
### ENV override not working
Ensure the variable is exported:
```bash
export CCS_DASHBOARD_AUTH_ENABLED=true
export CCS_DASHBOARD_USERNAME=admin
export CCS_DASHBOARD_PASSWORD_HASH='$2b$10$...'
```
### Session expired immediately
Check `session_timeout_hours` in config. Default is 24 hours.
### "Invalid ... context_group ..."
This error comes from `PUT /api/config` when an account explicitly sets shared mode with an invalid group. Use a canonical group value (for example: `team-alpha`).
## See Also
- [Dashboard Auth Feature](https://ccs.kaitran.ca/features/dashboard-auth) - Full documentation
- [Config Schema](https://ccs.kaitran.ca/reference/config-schema) - All config options
+32 -93
View File
@@ -1,112 +1,51 @@
# Hardening Debt Burndown Tracker
# Hardening Debt Burndown
Last Updated: 2026-06-18
Owner: Stream D (`#542`); maintainability epic owner TBD (open Q5)
This document defines the live measurement method. Completed phase narratives
remain available in Git history and must not be copied forward as current state.
## Scope
Maintainability hardening groundwork with low-risk changes:
The generated inventory measures:
- Inventory legacy shims/compatibility markers
- Inventory sync filesystem usage, especially runtime hotpaths
- Incrementally migrate hotpath sync I/O to async I/O with tests
- synchronous filesystem calls across source and in runtime hotpaths;
- legacy, shim, and compatibility markers;
- typed-error adoption;
- direct `console.error` and `console.warn` use outside CLI-UX exemptions;
- structured logger coverage;
- source files above maintainability size thresholds.
## How to Measure
Runtime-hotpath metrics exclude test, fixture, and mock files. Repository-wide
totals may include them when the metric explicitly says `all`.
Run:
## Generate And Verify
```bash
bun run report:hardening
node scripts/hardening-inventory.js --check
```
Generated artifacts:
- `docs/reports/hardening-inventory.json`
- `docs/reports/hardening-inventory.md`
- `docs/reports/hardening-inventory.json` for tooling;
- `docs/reports/hardening-inventory.md` for review.
## Kickoff Baseline (Issue #542 Stream D)
`--check` rebuilds the expected content in memory and compares both tracked
artifacts byte-for-byte. Any source change that affects a metric requires
regeneration in the same commit.
The current baseline is sourced from `docs/reports/hardening-inventory.json` after running `bun run report:hardening`.
Baseline captured: `2026-02-12`.
## Interpretation
| Metric | Baseline |
|---|---:|
| Sync fs occurrences (all) | 835 |
| Sync fs files affected (all) | 100 |
| Sync fs occurrences (runtime hotpaths) | 724 |
| Sync fs files affected (runtime hotpaths) | 89 |
| Legacy shim markers | 131 |
| Legacy shim files affected | 56 |
- Treat rankings as investigation queues, not automatic refactor mandates.
- Characterize behavior before splitting large or compatibility-sensitive code.
- Separate user-facing terminal output from operational diagnostics before
migrating console calls.
- Preserve intentional shims until their compatibility window and callers are
verified.
- Prefer trend comparisons from Git history when historical context is needed;
do not turn this live method into a dated progress ledger.
## Initial Async I/O Migration Log
## Review Contract
| Date | Area | Change | Safety Notes |
|---|---|---|---|
| 2026-02-12 | `src/web-server/jsonl-parser.ts` | Migrated `parseProjectDirectory()` directory listing from sync `readdirSync` to async `fs.promises.readdir` | Existing behavior kept (same filtering/fallback); covered by `tests/unit/jsonl-parser.test.ts` |
## Maintainability & Traceability Baseline (2026-06-18)
Baseline for the maintainability/traceability epic (`plans/260618-1346-maintainability-traceability-epic`). Sourced from `docs/reports/hardening-inventory.json` -> `maintainability` block after `bun run report:hardening`. Baseline captured: `2026-06-18`.
| Metric | Baseline | Epic target | Owner phase |
|---|---:|---:|---|
| typed-error adoption (typed / total throws) | 0.9% (4 / 431) | >40% in locked subdomains | P4 |
| typed-error adoption (locked: cliproxy/quota, cliproxy/auth, web-server/routes, auth) | 0.0% (0 / 23) | >40% | P4 |
| hotpath `console.error`/`warn` occurrences (non-exempt) | 931 (1091 total, 160 CLI-UX exempt) | < 10 | P3 |
| hotpath `console.error`/`warn` files (non-exempt) | 134 | minimal | P3 |
| files with `createLogger` | 35 / 685 (5.1%) | rise across all subdomains | P2/P3 |
| subdomains with zero `createLogger` | 20 (incl. api, channels, config, delegation, dispatcher, docker, shared) | 0 in the named set | P2 |
| files > 400 LOC | 95 | < 60 after P5+P6 | P5/P6 |
| files > 600 LOC | 45 | drop | P5/P6 |
| ESLint `no-new-throw-error` gate | not enforced | error + allowlist | P7 |
| ESLint `max-lines` gate | not enforced | warn at 400 | P7 |
| hardening report freshness | stale (2026-02-12) | < 30d gate in `validate:ci-parity` | P1 |
### Method
Metrics are grep-based and approximate (not a contract). Comments and string/template/regex literals are stripped before matching (`scripts/hardening-inventory.js#stripComments`). Subdomain granularity is 2-level under `src/cliproxy/` (`cliproxy/quota`, `cliproxy/auth`, ...) and 1-level elsewhere (`auth`, `config`, ...). The hotpath `console.error` count excludes CLI-UX print surfaces (`src/commands/`, `src/management/`, `src/utils/ui/`) which are legitimate user-facing terminal output. The typed-error denominator for the P4 target is LOCKED to the four named subdomains so the >40% goal cannot be gamed by narrowing scope. Re-baseline whenever the schema or method changes.
### Largest hotpath console.error offenders (2026-06-18)
| File | `console.error`/`warn` |
|---|---:|
| `src/utils/error-manager.ts` | 142 |
| `src/cliproxy/accounts/account-safety.ts` | 56 |
| `src/cliproxy/config/model-config.ts` | 32 |
| `src/cliproxy/executor/arg-parser.ts` | 26 |
| `src/dispatcher/flows/settings-flow.ts` | 26 |
## Progress Log
| Date | Phase | Change | Metric movement |
|---|---|---|---|
| 2026-06-18 | P2 | Express `withRequestContext` wrap; `CCS_REQUEST_ID` daemon forwarding + child re-anchor; logger toe-holds in delegation/docker. | zero-createLogger subdomains 20 -> 18 |
| 2026-06-18 | P3 | Redaction gate (token-shape scrubbing in context + `Error.message` + message string). `tool-sanitization-proxy` private log subsystem deleted (13 sites -> existing `createLogger`). ~120 diagnostic `console.error` -> structured `createLogger` across proxy, web-server/routes, glmt, quota-fetchers, executors, delegation. User-facing `console.error` (CLI flows, arg-parser usage, installers, prompts, adapter launch errors, error display) migrated to `process.stderr.write` (preserves stderr output). `error-manager.ts` reclassified CLI-UX-exempt (user-facing display). | hotpath `console.error` 928 -> 267 (71%); createLogger files 35 -> 64 |
| 2026-06-18 | P4 | `throw new Error` -> typed subclasses (ProfileError/AuthError/ConfigError/ProviderError/ValidationError) in cliproxy/auth, web-server/routes, auth. Error taxonomy made erasable (enum -> const, param-props -> fields) so the UI build accepts it. | typed adoption (locked subdomains) 0/23 -> 21/23 (91.3%); overall 0.9% -> 8.6% |
| 2026-06-18 | P5 | Split 4 test-backed god-files into submodule dirs + barrels (shared-manager, cliproxy-stats-routes, persist-command, quota-subcommand). Public API preserved. | files > 400 LOC 95 -> 91 |
| 2026-06-18 | P6 | Split 2 of 6 characterization-first god-files (quota-fetcher, quota-fetcher-gemini-cli; both had strong per-provider test coverage). 4 deferred. | files > 400 LOC 91 -> 89 |
| 2026-06-18 | P7 | ESLint gates: `ccs/no-new-throw-error` (error, baseline-allowlisted) + `max-lines` (warn, 400). code-standards.md + logging-contract.md (error.code table). | gates enforced (0 lint errors on baseline) |
## Epic outcome (2026-06-18)
| Metric | Baseline | Final | Target | Status |
|---|---:|---:|---:|---|
| typed-error adoption (locked subdomains) | 0.0% (0/23) | 91.3% (21/23) | >40% | met |
| typed-error adoption (overall) | 0.9% (5/431) | 8.6% (37/431) | rise | met |
| hotpath `console.error`/`warn` | 928 | 267 | <10 | partial; high-risk diagnostics migrated + redaction gate closed, but metric target unmet (see P3 note) |
| files with `createLogger` | 35 | 64 | rise | met |
| subdomains with zero `createLogger` | 20 | 15 | 0 in named set | partial; logger toe-holds improved coverage but P2 zero-subdomain target unmet |
| files > 400 LOC | 95 | 89 | <60 | partial (P6 deferred 4 targets) |
| hardening report freshness | stale | <30d gate | gate | met (P1) |
| ESLint `no-new-throw-error` | not enforced | error + allowlist | enforced | met (P7) |
| ESLint `max-lines` | not enforced | warn at 400 | enforced | met (P7) |
### Deferred follow-ups
- P2 remaining zero-logger subdomains (`api`, `channels`, `config`, `dispatcher`, `shared`, and others): add lightweight logger toe-holds when those subdomains next receive behavior work. This epic improved coverage but did not satisfy the original `0` target.
- P3 remaining 267 non-exempt `console.error`/`warn` call sites: split into true user-facing terminal output vs diagnostics, then either migrate diagnostics to structured logs or update the metric method to exempt confirmed CLI display helpers.
- P6 remaining 4 targets (`oauth-handler.ts`, `cursor-executor.ts`, `tool-sanitization-proxy.ts`, +1): need dedicated characterization-test work before splitting (the plan's characterization-first hard gate). Each has a clean seam identified in the epic plan.
### P3 residual note (2026-06-18)
The remaining 267 non-exempt `console.error`/`warn` occurrences are not fully resolved by this epic. Many are user-facing terminal display paths (interactive flows, arg-parser usage errors, installers, prompts, adapter launch failures, error-display helpers), but the generated metric still counts them because its exemption list is intentionally conservative. Treat P3 as partial until the residual list is classified file-by-file and either migrated to structured logs or explicitly moved into the CLI-UX exemption method. The redaction gate now covers structured message, context, `Error.message`, and `StageOptions.error` metadata so further diagnostic migration is safe.
A hardening change is complete only when focused tests pass, both inventory
artifacts match the current source tree, and the diff explains any material
metric movement.
+6 -117
View File
@@ -1,119 +1,8 @@
# Image Analysis Configuration Guide
# Image Analysis
CCS provides first-class image and PDF analysis for third-party Claude launches that do not have reliable native vision support.
User setup, routing behavior, and troubleshooting live in the canonical
[Image Analysis guide](https://docs.ccs.kaitran.ca/features/ai/image-analysis).
## How Image Analysis Works
Native Claude accounts keep Anthropic's own vision flow.
Third-party profiles now use a CCS-managed local MCP tool named `ImageAnalysis` when the runtime is available. CCS also appends a short steering hint so Claude prefers that tool over `Read` for local image and PDF files.
Healthy Claude-target launches suppress the legacy CCS `Read` hook so MCP stays authoritative. If the managed runtime cannot be provisioned, CCS keeps the old `Read` hook available only as a compatibility fallback when that path is still viable. If runtime/auth/proxy readiness is degraded beyond that, CCS falls back to native `Read` instead of failing the whole launch.
## Routing Model
ImageAnalysis requests go straight to the CCS-managed provider route:
```text
Claude -> ccs-image-analysis MCP -> CCS provider route -> /api/provider/<backend>/v1/messages
```
Important:
- CCS does not relay image analysis through Claude Code, another CLI, or a second model wrapper.
- For bridge-backed settings profiles, CCS resolves the backend and provider path before launch.
- CCS avoids leaking a profile's ordinary third-party `ANTHROPIC_BASE_URL` or token into image analysis unless that profile is explicitly using a CLIProxy bridge.
## Profile Behavior
| Profile Type | Image Method |
|--------------|--------------|
| Claude `default` / `account` | Native Claude vision / native `Read` |
| Third-party settings / CLIProxy / Copilot | CCS local `ImageAnalysis` MCP tool when ready |
| Third-party when MCP provisioning fails but provider-backed analysis is still viable | Legacy CCS `Read` hook fallback |
| Third-party when runtime/auth/proxy is unavailable | Native `Read` fallback |
## Configuration
Configure via dashboard (`Settings -> Image`) or `~/.ccs/config.yaml`:
```yaml
image_analysis:
enabled: true
timeout: 60
fallback_backend: agy
provider_models:
agy: gemini-3-1-flash-preview
codex: gpt-5.1-codex-mini
ghcp: claude-haiku-4.5
```
Useful commands:
```bash
ccs config image-analysis
ccs config image-analysis --enable
ccs config image-analysis --disable
ccs config image-analysis --set-fallback agy
ccs config image-analysis --set-profile-backend glm agy
ccs config image-analysis --clear-profile-backend glm
```
## Prompt Templates
CCS installs editable prompt templates at:
```text
~/.ccs/prompts/image-analysis/
```
Templates:
- `default.txt`
- `screenshot.txt`
- `document.txt`
CCS automatically selects `screenshot` for screenshot-like filenames, `document` for PDFs, and `default` otherwise.
## Runtime Environment
Key runtime env vars:
| Variable | Purpose |
|----------|---------|
| `CCS_IMAGE_ANALYSIS_SKIP` | Disable image analysis for the current launch |
| `CCS_IMAGE_ANALYSIS_SKIP_HOOK` | Suppress only the legacy CCS `Read` hook while keeping MCP ImageAnalysis available |
| `CCS_IMAGE_ANALYSIS_RUNTIME_BASE_URL` | Explicit CCS runtime base URL |
| `CCS_IMAGE_ANALYSIS_RUNTIME_PATH` | Provider route such as `/api/provider/agy` |
| `CCS_IMAGE_ANALYSIS_RUNTIME_API_KEY` | Explicit CCS runtime auth key |
| `CCS_IMAGE_ANALYSIS_MODEL` | Force a single image-analysis model |
| `CCS_DEBUG` | Verbose runtime logging |
## Self-Heal
CCS now auto-heals stale managed image-analysis state in three places:
- Healthy Claude launches remove stale CCS-managed image `Read` hooks from the active profile settings before launch.
- `Settings -> Image` save/provisioning repairs managed MCP runtime files, syncs managed MCP entries into isolated Claude config dirs, and cleans stale CCS-managed image hooks from `~/.ccs/*.settings.json`.
- `ccs doctor --fix` repairs invalid image-analysis config, removes stale CCS-managed image hooks, and resyncs managed `ccs-image-analysis` MCP entries into isolated configs.
## Troubleshooting
### Claude still uses `Read`
- Confirm `ccs config image-analysis` shows `enabled: true`
- Check the active profile resolves to a configured backend
- Run `ccs doctor --fix` to repair stale managed hooks or missing managed MCP sync
- Run with `CCS_DEBUG=1` to see runtime preparation details
### ImageAnalysis is not exposed
- Verify CLIProxy auth for the resolved backend
- Verify the local or remote CLIProxy target is reachable
- Check `~/.claude.json` and inherited account configs for `ccs-image-analysis`
### I need to prove requests are going directly to the provider route
Run with `CCS_DEBUG=1` and inspect the resolved runtime path. The request target should be provider-scoped, for example:
```text
/api/provider/agy/v1/messages
```
Contributor implementation contracts are enforced by the source and focused
tests under `src/utils/image-analysis/`, `src/utils/hooks/`, and
`src/web-server/routes/`.
+33 -367
View File
@@ -1,378 +1,44 @@
# OpenAI-Compatible Provider Routing
# OpenAI-Compatible Proxy Developer Contract
CCS can route Claude Code traffic through a local Anthropic-compatible proxy when
your API profile points at an OpenAI-compatible chat completions endpoint.
The canonical
[OpenAI-Compatible Provider Routing guide](https://docs.ccs.kaitran.ca/features/proxy/openai-compatible-providers)
owns user setup and workflows. This local contract retains runtime and
configuration invariants used by source, tests, and operators.
This is useful for providers such as:
## Profile-Scoped Insecure TLS
- Hugging Face Inference Providers
- Tuning Engines
- OpenRouter
- Ollama
- llama.cpp servers
- OpenAI-compatible self-hosted gateways
`CCS_OPENAI_PROXY_INSECURE` is read from an OpenAI-compatible profile env.
Truthy values are `1`, `true`, `yes`, and `on` (case-insensitive). When enabled,
the local proxy disables upstream certificate verification for that profile,
including request-time routing to another insecure profile.
## Related Project: claude-code-router
This flag weakens TLS verification. Keep it explicit and profile-scoped; never
make it a global default or infer it from a failed certificate check. The live
resolution and dispatcher contracts are in
[`profile-router.ts`](../src/proxy/profile-router.ts) and
[`proxy-server.ts`](../src/proxy/server/proxy-server.ts).
[claude-code-router](https://github.com/musistudio/claude-code-router) is the
main external reference that informed this CCS work. Their Anthropic/OpenAI
transformer design helped shape the routing approach here.
## Request Timeout
When to use CCR:
`CCS_OPENAI_PROXY_REQUEST_TIMEOUT_MS` controls the upstream request timeout in
milliseconds:
- you want a standalone router without CCS profile integration
- you do not need CCS account/runtime management around the request flow
- default: `600000` (10 minutes);
- accepted: values whose `Number.parseInt` result is positive;
- missing, invalid, zero, or negative values: fall back to the default.
When to use CCS:
Upstream Undici header/body timeouts must stay above the request timeout so they
do not terminate slow self-hosted inference first. The current implementation
adds a 30-second grace ceiling. Source of truth:
[`messages-route.ts`](../src/proxy/server/messages-route.ts).
- you already use CCS API profiles or runtime bridges
- you want the proxy flow available through `ccs <profile>` and `ccs proxy ...`
- you want the routing behavior documented and tested inside the CCS workflow
## Compatibility Boundary
## What CCS Does
These variables configure the local Anthropic-to-OpenAI proxy. They do not
convert a non-compatible profile into a compatible one, bypass local proxy
authentication, or authorize remote binding. Keep profile detection, adaptive
port selection, passthrough mode, request-time routing, and scenario routing
documented in the public guide.
When you launch a compatible settings profile with the Claude target, CCS now:
1. Starts a local proxy on `127.0.0.1` using the resolved local port for that profile
2. Accepts Anthropic `/v1/messages` traffic from Claude Code
3. Translates requests into OpenAI chat-completions format
4. Forwards them to your configured upstream provider
5. Translates streaming responses back into Anthropic SSE
You do not need to rewrite your profile by hand each time.
## Quick Start
Create or reuse an API profile that points at an OpenAI-compatible endpoint:
```bash
ccs api create --preset hf
```
For Tuning Engines:
```bash
ccs api create --preset te
```
Then you can use the profile directly:
```bash
ccs hf
# or
ccs te
```
CCS detects that the profile is OpenAI-compatible and auto-routes Claude Code
through the local proxy.
## Manual Proxy Lifecycle
If you want to manage the proxy explicitly:
```bash
ccs proxy start hf
eval "$(ccs proxy activate)"
ccs proxy status
ccs proxy stop
```
Useful variants:
```bash
ccs proxy start hf --host 127.0.0.1
ccs proxy start hf --port 3460
ccs proxy activate hf
ccs proxy activate --fish
ccs proxy status hf
ccs proxy stop hf
```
Port selection precedence is:
1. CLI `--port` for an exact one-off pin
2. `proxy.profile_ports[profile]` for an exact per-profile pin
3. `proxy.port` for a shared preferred starting port
4. adaptive per-profile fallback when nothing is pinned
Legacy shared `proxy.port: 3456` values are treated as unset so older configs
move onto the adaptive path instead of staying on the hot legacy default. If
you need an exact `3456` binding now, pin it via `--port` or `proxy.profile_ports`.
`ccs proxy activate` now prints the full local runtime contract:
- `ANTHROPIC_BASE_URL`
- `ANTHROPIC_AUTH_TOKEN`
- `ANTHROPIC_MODEL` plus tier defaults when present
- `DISABLE_TELEMETRY`
- `DISABLE_COST_WARNINGS`
- `API_TIMEOUT_MS`
- `NO_PROXY`
## Multiple Active Proxy Profiles
CCS now stores OpenAI-compatible proxy state per profile instead of treating the
runtime as a singleton.
- Different compatible profiles can run at the same time on separate local ports
- `ccs proxy activate` without a profile stays convenient when only one proxy is
running
- When multiple proxies are running, pass the profile explicitly to
`activate`, `status`, or `stop`
- `status` and `activate` always reflect the actual running port instead of an
assumed default
If you want to pin or guide ports explicitly, configure them in `~/.ccs/config.yaml`:
```yaml
proxy:
port: 45000
profile_ports:
hf: 3460
openai: 3461
```
## Request-Time Routing
The proxy is no longer limited to the startup profile's default model.
Supported request-time selectors:
- `profile:model`
Example: `deepseek:deepseek-reasoner`
- `profile`
Example: `openrouter`
- plain model ids
Example: `deepseek-chat`
Plain model ids use exact string equality against the configured profile model
slots (`model`, `opusModel`, `sonnetModel`, `haikuModel`). CCS does not apply
fuzzy matching or prefix matching here. If no exact match is found, the request
stays on the active profile with the requested model id unchanged.
Routing behavior:
1. `profile:model` wins immediately.
2. Scenario routing may override the active profile when configured.
3. Plain model ids are matched against the configured OpenAI-compatible
profiles before falling back to the active profile.
This means a Claude session launched through one compatible profile can still
request another compatible profile/model when the proxy can resolve it safely.
## Scenario Routing
Scenario routing is now supported through `proxy.routing` in your CCS config.
Example `~/.ccs/config.yaml`:
```yaml
proxy:
routing:
default: "deepseek:deepseek-chat"
background: "ollama:qwen2.5-coder:0.5b"
think: "deepseek:deepseek-reasoner"
longContext: "openrouter:google/gemini-2.5-pro"
longContextThreshold: 60000
webSearch: "openrouter:perplexity/sonar-pro"
```
Current scenario detection:
- `background`: requested model contains `haiku`
- `think`: Anthropic `thinking` is enabled
- `longContext`: estimated request tokens exceed `longContextThreshold`
- `webSearch`: tool list includes `web_search`
- `default`: fallback selector when the above do not apply
Routing decisions are logged through CCS structured logs.
`longContextThreshold` uses an intentionally approximate token estimate based on
message characters, tool payload size, and a `chars / 4` heuristic. Tune the
threshold conservatively if your routing decision needs a sharper cutoff near
the boundary.
## How Profile Detection Works
CCS keeps these profiles in the normal API/settings-profile flow.
Anthropic-compatible endpoints such as:
- `https://api.anthropic.com`
- `https://api.z.ai/api/anthropic`
- `https://api.deepseek.com/anthropic`
continue to launch directly.
OpenAI-compatible endpoints such as:
- `https://router.huggingface.co/v1`
- `https://api.openai.com/v1`
- `http://localhost:11434`
are routed through the local proxy for Claude-target launches.
## Provider Setup
### DeepSeek
Use a settings profile whose env looks like:
```json
{
"env": {
"ANTHROPIC_BASE_URL": "https://api.deepseek.com/v1",
"ANTHROPIC_AUTH_TOKEN": "sk-...",
"ANTHROPIC_MODEL": "deepseek-chat",
"CCS_DROID_PROVIDER": "generic-chat-completion-api"
}
}
```
Typical override target:
- `deepseek:deepseek-reasoner`
### OpenRouter
```json
{
"env": {
"ANTHROPIC_BASE_URL": "https://openrouter.ai/api/v1",
"ANTHROPIC_AUTH_TOKEN": "sk-or-...",
"ANTHROPIC_MODEL": "openai/gpt-4.1-mini",
"CCS_DROID_PROVIDER": "generic-chat-completion-api"
}
}
```
Useful when you want:
- model fan-out behind one provider profile
- long-context or web-search scenario targets
### Ollama / Local Gateways
```json
{
"env": {
"ANTHROPIC_BASE_URL": "http://127.0.0.1:11434",
"ANTHROPIC_AUTH_TOKEN": "ollama",
"ANTHROPIC_MODEL": "qwen3-coder",
"CCS_DROID_PROVIDER": "generic-chat-completion-api"
}
}
```
For self-signed HTTPS gateways, add `CCS_OPENAI_PROXY_INSECURE=1`.
### DashScope / Qwen Compatible Mode
DashScope's compatible endpoint works even when older settings files still
carry a stale Anthropic-style provider hint:
```json
{
"env": {
"ANTHROPIC_BASE_URL": "https://dashscope-us.aliyuncs.com/compatible-mode/v1",
"ANTHROPIC_AUTH_TOKEN": "sk-...",
"ANTHROPIC_MODEL": "qwen3.6-plus",
"CCS_DROID_PROVIDER": "anthropic"
}
}
```
CCS now infers the OpenAI-compatible route from the base URL and does not let
that stale provider hint block proxy routing.
## Self-Signed TLS
If your upstream gateway uses a self-signed or privately issued certificate,
set this in the profile settings JSON:
```json
{
"env": {
"CCS_OPENAI_PROXY_INSECURE": "1"
}
}
```
That flag is respected by both:
- `ccs <profile>` auto-routing
- `ccs proxy start <profile>`
## Supported Runtime Paths
- `ccs <profile>` with Claude target: auto-starts the local proxy when needed
- `ccs proxy start <profile>`: starts the proxy explicitly
- `GET /`: proxy info and bound profile details
- `GET /health`: proxy liveness check
- `GET /v1/models`: local view of the configured model mapping
- `POST /v1/messages`: Anthropic-compatible request entrypoint
## Troubleshooting
### Missing or invalid local proxy token
- Re-run `eval "$(ccs proxy activate)"`
- Check `ccs proxy status` and confirm the expected profile is running
### Self-signed or private CA upstream
- Add `CCS_OPENAI_PROXY_INSECURE=1` to the profile settings
- Restart the proxy after changing the setting
### Need to pin or verify the local port
- Check the active binding with `ccs proxy status hf`
- Pin a one-off port with `ccs proxy start hf --port 3460`
- Reserve a stable profile port with `proxy.profile_ports`
- Re-run `ccs proxy activate hf` after changing the port
### Provider returns `429` or empty upstream output
- CCS now preserves upstream rate-limit errors and retry headers
- Empty or malformed provider JSON is returned as Anthropic-style `api_error`
### Slow upstreams: `socket connection was closed unexpectedly`
- Long-running upstreams (self-hosted LLMs with queue/prefill phases) can stay
silent for minutes before the first or next token. The proxy already allows up
to 10 minutes per request; set `CCS_OPENAI_PROXY_REQUEST_TIMEOUT_MS` in the
profile settings to raise or lower that ceiling.
- Restart the proxy after changing the setting.
### Requests route to the wrong model/profile
- Use an explicit selector such as `profile:model`
- Review `proxy.routing` if scenario routing is enabled
- Check CCS structured logs in `~/.ccs/logs/current.jsonl` for routing decisions
## Validation
The shipped coverage includes:
- unit tests for OpenAI-compatible profile detection
- unit tests for Anthropic -> OpenAI request translation
- unit tests for request-time profile/model routing and scenario routing
- unit tests for multi-line SSE parsing
- integration tests for `/v1/messages` request/response translation
- integration tests for rate limits, empty upstream responses, timeout handling,
thinking/tool-call chunk streaming, and request-time routing
- integration tests for daemon lifecycle and `/health` / `/v1/models`
- e2e tests for `ccs proxy` lifecycle
- e2e tests for `ccs <profile>` auto-routing through a mock upstream
Focused verification command:
```bash
bun test tests/e2e/proxy-command.e2e.test.ts tests/integration/proxy/request-routing.test.ts --coverage
```
Pre-merge gate:
```bash
bun run validate
```
Focused behavior locks live in `tests/unit/proxy/` and
`tests/integration/proxy/`.
+35 -268
View File
@@ -1,284 +1,51 @@
# CCS Project Roadmap
Last Updated: 2026-05-22
This repository does not maintain a hand-copied backlog snapshot. Current work
lives in GitHub so status, ownership, and dependencies stay live.
Forward-looking roadmap documenting current priorities, GitHub issues, and future feature plans.
## Find Current Work
---
- [Open issues](https://github.com/kaitranntt/ccs/issues)
- [Project boards](https://github.com/users/kaitranntt/projects)
- [Open pull requests](https://github.com/kaitranntt/ccs/pulls)
- [Releases](https://github.com/kaitranntt/ccs/releases)
## Completed Modularization Summary
Filter issues by area and type labels before selecting work. Verify the issue
body, recent comments, and linked pull requests rather than relying on an old
roadmap copy.
All major modularization work is complete. The codebase evolved from monolithic files to a well-structured modular architecture.
## Planning Guidance
| Phase | Description | Key Result |
|-------|-------------|------------|
| 1 | Type System | `src/types/` with barrel exports |
| 2 | CLI Commands | `src/commands/` (8 handlers extracted) |
| 3 | CLIProxy | `src/cliproxy/` with auth/, binary/, services/ subdirs |
| 4 | Utils/Errors | `src/utils/ui/`, `src/errors/`, `src/management/` |
| 5 | UI Components | 5 monster files split into modular dirs (54+ modules) |
| 6 | Settings Page | `pages/settings/` (1,781->20 files) |
| 7 | Analytics Page | `pages/analytics/` (420->8 files) |
| 8 | Auth Monitor | `monitoring/auth-monitor/` (465->8 files) |
| 9 | Test Infrastructure | 1407 tests, 90% coverage |
| 10 | Remote CLIProxy | `proxy-config-resolver.ts`, `remote-proxy-client.ts` |
| 11 | Kiro + legacy ghcp Providers | OAuth support via CLIProxyAPIPlus (v7.2) |
| 12 | Hybrid Quota Management | `quota-manager.ts`, `quota-fetcher.ts` (v7.14) |
| 13 | Docker Support | `docker/` directory with Dockerfile, Compose, entrypoint |
| 14 | Image Analysis Hook | Vision proxying via CLIProxy transformers (v7.34) |
| 15 | Third-Party Tool Integration | `ccs env` command with multi-format export (v7.39) |
1. Confirm the issue still reproduces against `origin/dev`.
2. Read `CLAUDE.md` and the affected source, tests, help, and public docs.
3. Keep the implementation and compatibility boundary explicit.
4. Run focused validation, then the repository quality gate appropriate to the
change.
5. State public-doc impact before handoff.
**Metrics Achieved**:
- Files >500 lines: 12 -> 5 (-58%)
- UI files >200 lines: 28 -> 8 (-71%)
- Barrel exports: 5 -> 39 (+680%)
- Test coverage: 0% -> 90%
- Total tests: 1440 (6 skipped)
Plans are workspace-only and belong under ignored `plans/`; they are not product
roadmap truth.
---
## Maintainability Work
## Current Status
Use [Hardening Debt Burndown](./hardening-debt-burndown.md) for measurement
methodology. Its generated inventory is an exact source-tree snapshot, not a
historical progress log.
### Recent Fixes
## Stable Product Direction
- **2026-05-22**: **#1337** CLIProxy quota preflight now reconciles the entire
active provider account pool before local proxy startup. Known-exhausted
non-default accounts move out of live `auth/` rotation when another healthy
account exists, preventing round-robin requests from stalling on already
exhausted accounts while preserving manual pause/resume and cooldown
auto-resume behavior.
- **2026-05-22**: **#1332** `ccsx auth` profiles now link native Codex
`agents/` and `skills/` resources alongside `config.toml`, and `ccsx <profile>`
repairs missing resource links at launch so relative agent role `config_file`
entries do not warn on existing profiles.
- **2026-05-20**: **#1285** Codex model picker aliases now include `minimal` and `low` reasoning effort suffixes alongside `medium`, `high`, and `xhigh`, and the dashboard separates base recommendations, reasoning variants, and fast variants so low-effort selections such as `gpt-5.5-low` are visible.
- **2026-05-19**: **#1252** Claude extension persistence now rejects Codex CLIProxy profiles instead of writing `ANTHROPIC_BASE_URL=/api/provider/codex` into Claude Code settings. Native Codex subscription use stays on `ccsxp` or `ccs codex --target codex`, and `ccs doctor` warns when stale Claude settings still point at the Codex translator.
- **2026-05-18**: **#1287** `ccsx resume` now passes through to the native Codex `resume` subcommand before CCS profile detection, preserving Codex session continuation while keeping `ccsx auth` on the managed Codex profile namespace.
- **2026-05-09**: **#1199** Existing Claude auth accounts now have dashboard-visible Shared Resources controls separate from History Sync. Accounts exposes a dedicated Resources action for `shared` vs `profile-local`, `/shared` now inventories commands, skills, agents, plugins, and `settings.json`, plugin directories without docs show factual directory contents, and shared settings content is inspectable read-only through the localhost-gated shared-content API.
- **2026-05-07**: **#760** Codex GPT fast mode is now a first-class CLIProxy model tuning suffix. CCS accepts `gpt-5.4-fast`, `gpt-5.4-high-fast`, and equivalent canonicalized forms in raw env configs, CLI variant creation, and the dashboard model picker; runtime requests now send the base upstream model with `reasoning.effort` plus `service_tier: "priority"` instead of leaking the suffixed alias to CLIProxy upstream routing.
- **2026-05-07**: **#1103** GitHub Copilot is now treated as a deprecated compatibility bridge. The dashboard moves Copilot out of the active Identity & Access section and into Deprecated, quick setup no longer offers `ghcp` for new onboarding, CLI/help/config copy marks Copilot as deprecated, and existing `ccs copilot` / `ghcp` compatibility paths remain available for current setups.
- **2026-05-07**: **#1189** Headless settings-profile delegation now preserves native Claude passthrough args without a Claude flag allowlist. Explicit `--channels` values reach Claude Code, future native flags can carry multiple adjacent values, malformed CCS-owned flags no longer swallow the next native flag, and `--prompt=<text>` routes through headless delegation consistently with `--prompt <text>`.
- **2026-05-03**: **#1172** Local CLIProxy config generation now keeps the CPAMC management dashboard aligned with backend selection. `backend: original` points to the upstream dashboard, `backend: plus` points to the CCS-maintained CPAMC fork, `cliproxy.management_panel_repository` lets advanced users override the panel repository, and stale generated configs are regenerated when the expected panel source changes.
- **2026-04-30**: **#1153** Native Claude launches now accept session-scoped `--effort low|medium|high|xhigh|max` overrides through CCS without mutating global Claude settings. CCS validates invalid or missing effort values before spawning Claude, normalizes accepted values, keeps default headless `-p/--prompt` launches on native Claude instead of delegation parsing, and preserves CLIProxy/Codex/Droid effort aliases.
- **2026-04-28**: **#1123** CLIProxy quota failover now uses the dashboard/manual pause mechanism for all quota-visible OAuth providers with CCS quota fetchers: Antigravity, Claude, Codex, Gemini CLI, and GitHub Copilot. When a healthy fallback exists, CCS moves the exhausted account token out of the live `auth/` folder into `auth-paused/`, marks the account paused for dashboard visibility, persists the cooldown for auto-resume, and still avoids self-pausing the last usable account.
- **2026-04-28**: **#1115** CCS now exposes upstream CLIProxy session affinity as a first-class local managed setting. Users can inspect and toggle local `session-affinity` plus TTL from `ccs cliproxy routing affinity`, from the `/cliproxy` dashboard routing card, and through the local dashboard API. The generated local CLIProxy config now persists `routing.session-affinity` and `routing.session-affinity-ttl`, help/copy explains that CLIProxy prefers explicit session or thread identifiers before falling back to prompt-history hashing, and remote session-affinity management stays explicitly unsupported until upstream management APIs expose more than `routing.strategy`.
- **2026-04-24**: **#1065** Local CLIProxy Plus is available again as an explicit opt-in backend through the community-maintained `kaitranntt/CLIProxyAPIPlus` fork. CCS keeps `original` as the default backend, no longer downgrades saved `backend: plus` configs to `original`, updates Plus release lookups to the maintained fork, and documents Plus as a targeted path for plus-only providers.
- **2026-04-21**: CLIProxy quota failover now quarantines exhausted Claude and Antigravity accounts out of live rotation when a healthy fallback exists. CCS persists those quota-triggered pauses across launches, automatically resumes them after the configured cooldown window, and deliberately avoids auto-pausing the last available account so single-account setups still degrade gracefully instead of hard-locking themselves.
- **2026-04-20**: **#1051** Browser automation now defaults safe-off for new installs and upgrades that do not already carry explicit browser settings. CCS changes both Claude Browser Attach and Codex Browser Tools to start with `enabled: false` and `policy: manual`, normalizes missing browser policies on upgrade back to `manual`, preserves explicit existing enablement, and updates status/help/docs so browser tooling is never implied to auto-expose unless users opt in.
- **2026-04-19**: **#1051** Browser tooling now has an explicit exposure policy instead of only coarse enablement toggles. CCS adds `browser.<lane>.policy` (`auto` or `manual`) for both Claude Browser Attach and Codex Browser Tools, exposes CLI-first policy controls through `ccs browser policy`, `ccs browser enable`, and `ccs browser disable`, and adds one-run launch overrides `--browser` and `--no-browser` so users can force browser tooling on or off without editing saved config.
- **2026-04-19**: **#1049** Browser setup now has a real remediation path instead of status/doctor-only guidance. CCS adds `ccs browser setup` as the primary one-command flow for Claude Browser Attach, shortens managed browser-path output to home-relative display paths where appropriate, and updates browser readiness guidance to point users at setup first while keeping browser doctor read-only by default.
- **2026-04-18**: **#1038** Legacy OpenAI-compatible provider writes no longer self-destruct on the next `ccs cliproxy restart`. CCS now preserves AI-provider-managed top-level sections such as `openai-compatibility` during CLIProxy config regeneration, and the legacy `openai-compat` manager now rewrites only its own YAML section instead of dumping the whole file and stripping the generated version header. Regression coverage now proves the legacy helper keeps the generated header intact and that OpenAI-compatible connectors survive regeneration.
- **2026-04-16**: **#1030** Browser automation is now a first-class CCS surface instead of an env-only/runtime-only feature. CCS adds `ccs help browser`, `ccs browser status`, and `ccs browser doctor`; a dedicated `Settings -> Browser` dashboard tab for Claude Browser Attach and Codex Browser Tools; a new `browser` section in `~/.ccs/config.yaml`; explicit readiness/next-step messaging for attach-mode Chrome sessions; and Codex UI guidance that marks the managed `ccs_browser` entry as CCS-owned and redirects browser setup away from the generic MCP editor.
- **2026-04-15**: **#969** Local CLIProxy bootstrap no longer depends on live GitHub reachability during normal dashboard and runtime startup. CCS now skips hidden auto-update lookups on standard CLIProxy bootstrap paths, fails fast with explicit `ccs cliproxy install` guidance when a service start needs a binary that is not installed locally, and keeps `ccs config` able to open the dashboard in limited mode instead of stalling behind blocked release downloads.
- **2026-04-15**: **#1010** Remote dashboard auth guidance now explains the Docker boundary explicitly. The readonly banner, remote login/setup card, and dashboard-auth docs now tell users that integrated Docker deployments keep config inside the running `ccs-cliproxy` container volume, so `ccs config auth setup` must run there rather than in the outer host shell.
- **2026-04-14**: **#991** CCS now auto-routes Claude-target settings profiles that use OpenAI-compatible endpoints through a local Anthropic-compatible proxy instead of sending raw Anthropic `/v1/messages` traffic directly to chat-completions backends. The `ccs proxy` command now supports `start`, `status`, `activate`, and `stop` with explicit host binding, shell-aware activation helpers, and a fuller local runtime env contract. The proxy surface now exposes `GET /`, `/health`, `/v1/models`, and `/v1/messages`, logs routing decisions into CCS structured logs, supports Anthropic image blocks plus request-time `profile:model` overrides, and adds config-driven scenario routing (`background`, `think`, `longContext`, `webSearch`) on top of the compatible-profile path. Coverage now includes request routing, rate-limit/timeout/empty-upstream failures, chunked tool-call streaming, and disconnect cleanup alongside the existing unit, integration, and e2e suites.
- **2026-04-10**: **#765** `/providers` now includes a first-class Hugging Face preset for API Profiles. CCS exposes Hugging Face Inference Providers through the existing OpenAI-compatible profile flow with the official router endpoint `https://router.huggingface.co/v1`, a short `hf` default profile name, and `hf` preset alias support for both the dashboard chooser and `ccs api create --preset hf`.
- **2026-04-10**: **#944** Image Analysis auth readiness no longer collapses to native Read when merged runtime-status dependency overrides include a missing initializer value. CCS now preserves default dependency functions when override entries are `undefined`, still reads token-backed auth status directly in the local readiness path, and includes regression coverage for the missing-initializer case that previously surfaced as `deps.initializeAccounts is not a function`.
- **2026-04-10**: **#945** CCS now normalizes Gemini CLI and Antigravity tier signals around an explicit `free / pro / ultra / unknown` model, preserves raw tier ids such as `g1-pro-tier`, enriches Gemini quota responses with provider entitlement evidence, classifies `MODEL_CAPACITY_EXHAUSTED` separately from auth/entitlement failures, fixes the Antigravity CLI quota table so live quota-derived tiers no longer collapse back to stale `unknown`, adds Gemini tier ids to CLI quota output, extends Gemini Flash Lite grouping to cover `gemini-3.1-flash-lite-preview`, and allows Gemini account surfaces to render the same tier badge semantics as Antigravity.
- **2026-04-09**: **#938** Cliproxy model routing now exposes backend-pinned short prefixes for overlapping OAuth backends. CCS repairs managed OAuth auth-file prefixes for Gemini CLI (`gcli`) and Antigravity (`agy`), enriches `/api/cliproxy/catalog` with routing hints that show whether an unprefixed model is safe, shadowed, or prefix-only, upgrades `ccs cliproxy catalog` plus interactive variant model pickers to surface the pinned names, and updates the `ccs config` Cliproxy model selection UI so users can see the preferred call name and current effective backend before saving settings.
- **2026-04-08**: **#931** `/cliproxy` model pickers now source their provider catalogs from CLIProxy management model definitions instead of treating the UI catalog file as the dropdown source of truth. CCS now refreshes live model definitions for Gemini, Codex, Claude, Antigravity, Qwen, iFlow, Kiro, GitHub Copilot, and Kimi through `/api/cliproxy/catalog`, overlays CCS-only preset/default metadata on top of those upstream models, keeps `/api/cliproxy/models` as the live availability feed, and falls back to cached/static catalogs when the proxy is unavailable so the dashboard never goes blank.
- **2026-04-08**: **#929** Image Analysis hardening now makes the managed `ccs-image-analysis` MCP path authoritative on healthy Claude-target launches, suppresses stale CCS-managed image `Read` hooks instead of letting them compete with MCP, keeps the legacy hook available only as compatibility fallback when MCP provisioning fails, and extends self-heal to dashboard provisioning plus `ccs doctor --fix` so stale hook files and missing isolated MCP sync are repaired automatically.
- **2026-04-07**: CLIProxy routing strategy is now a first-class CCS surface. Users can inspect and explicitly change `round-robin` vs `fill-first` from `ccs cliproxy routing` and from a native `/cliproxy` dashboard card. Local mode now persists the chosen startup default into CCS-managed CLIProxy config generation, while untouched installs remain on `round-robin`. CCS deliberately does not infer strategy from account composition.
- **2026-04-07**: **#926** CCS now has a first-class structured logging layer under `src/services/logging/`, a bounded top-level `logging` config section in `~/.ccs/config.yaml`, automatic rotation/retention for CCS-owned logs under `~/.ccs/logs/`, native `/api/logs` dashboard endpoints, request tracing for the dashboard backend, and a dedicated `System -> Logs` dashboard route for browsing recent entries and editing retention settings. Legacy CLIProxy error files remain available as a labeled legacy source instead of acting as the primary logging model.
- **2026-04-06**: The dashboard login surface now distinguishes a real sign-in from a host-setup requirement. Remote/IP visitors no longer see a misleading blank credential form when dashboard auth is disabled or incomplete; they now get explicit guidance that CCS has no default credentials, should be enabled on the host with `ccs config auth setup`, or should be reopened via localhost when used on the same machine. The password field now includes a show/hide toggle, and the page exposes an explicit light/dark theme switch before sign-in.
- **2026-04-04**: The GitHub README was reduced from a wall-of-text reference dump into a shorter conversion surface that keeps the hero, proof screenshots, and fast-start commands while delegating deeper installation, provider, feature, and CLI-reference content to `docs.ccs.kaitran.ca`. The docs site now includes a dedicated `Product Tour` page for the screenshot-led walkthrough.
- **2026-04-05**: **#912 #913 #914** Kiro auth is now aligned with the current CLIProxyAPIPlus contract. CCS auto-selects the Builder ID path for the default `ccs kiro --auth` flow instead of stalling on the upstream Builder ID vs IDC chooser, callback-based Kiro auth methods can use `--paste-callback` by replaying the pasted redirect URL back into the local callback server, and the CLI now supports IDC auth via `--kiro-auth-method idc` plus `--kiro-idc-start-url`, `--kiro-idc-region`, and `--kiro-idc-flow`.
- **2026-04-03**: CCS CLI help and completion UX was refreshed. Root help is now shorter and task-oriented, `ccs help <topic|command>` routes to topic-aware help, and shell completions now delegate to the hidden `ccs __complete` backend.
- **2026-04-02**: Third-party image and PDF analysis now follows the same first-class local-tool model as WebSearch. CCS provisions `ccs-image-analysis` as a managed MCP tool, routes requests directly to provider-scoped CCS endpoints such as `/api/provider/agy/v1/messages`, keeps editable prompt templates under `~/.ccs/prompts/image-analysis/`, and demotes the old `Read` hook to a best-effort compatibility fallback. Launches now stay non-fatal and fall back to native `Read` when the managed runtime cannot be prepared.
- **2026-04-01**: The `Compatible -> Codex CLI` dashboard now exposes manual long-context controls for `model_context_window` and `model_auto_compact_token_limit`. CCS reads and patches those upstream Codex config keys directly, adds official guidance that GPT-5.4 long context is experimental and opt-in, and keeps the behavior manual-only so the dashboard never auto-fills or auto-saves long-context values for the user.
- **2026-03-30**: **#862** Third-party WebSearch now uses a first-class CCS-managed MCP tool path instead of relying on a denied native Anthropic `WebSearch` call as the normal UX. CCS provisions `ccs-websearch` into `~/.claude.json`, syncs it into isolated account configs when needed, suppresses native `WebSearch` on third-party launches, preserves the provider order `Exa -> Tavily -> Brave -> DuckDuckGo -> legacy CLI fallback`, and keeps the old hook runtime only as shared provider plumbing plus compatibility fallback. Uninstall cleanup now also removes the managed WebSearch MCP runtime.
- **2026-03-28**: **#773** CCS now ships a dedicated `Compatible -> Codex CLI` dashboard route with a real split-view control center. The page detects the local Codex binary, keeps overview/docs guidance, and adds guided editors for the user-owned `~/.codex/config.toml` layer: top-level runtime defaults, project trust, profiles, model providers, MCP servers, and supported feature flags. Structured saves intentionally normalize TOML formatting and drop comments, so the raw editor remains the fidelity escape hatch. Follow-up fixes added immediate raw snapshot refresh, refresh/discard recovery for stale raw drafts, dirty raw-editor guarding for structured controls, project-trust path validation, read-only handling for unreadable config files, preservation of unsupported upstream values such as granular `approval_policy`, and feature reset-to-default support. CCS still warns that transient runtime overrides such as `codex -c key=value` and `CCS_CODEX_API_KEY` may change effective behavior without persisting into the file.
- **2026-03-27**: WebSearch dashboard cards now manage Exa, Tavily, and Brave API keys inline instead of relying on a separate manual env step. CCS stores those secrets through `global_env`, reflects masked key state in `/api/websearch`, and counts dashboard-managed keys as ready in the WebSearch status flow.
- **2026-03-27**: **#812** CCS now includes a first-class `ccs docker` command suite for self-hosting the integrated Dashboard + CLIProxy stack. The CLI can stage bundled Docker assets locally or to a remote `--host` over SSH, report compose/supervisor status, stream CCS or CLIProxy logs, and run in-container update flows without relying on ad-hoc deployment scripts.
- **2026-03-24**: Official Claude Channels now follow Anthropic's actual runtime contract. CCS blocks auto-enable unless Bun is available, Claude Code is verified at v2.1.80+, and `claude.ai` auth is verified; treats `--allow-dangerously-skip-permissions` as an explicit override; keeps Telegram/Discord bot tokens in Claude's shared `~/.claude/channels/` state (or official `*_STATE_DIR` overrides); and upgrades the dashboard/CLI status flow with Bun/version/auth/state-scope guidance, safer token draft retention on refresh failures, and a non-macOS iMessage toggle that can still be turned off when already selected.
- **2026-03-23**: CLIProxy providers that do not expose an email no longer require a user-supplied nickname on first auth. CCS now derives a stable internal account identifier for Kiro/Copilot-style flows, preserves later rename support, hardens account discovery/registry sync around that identifier, and updates AI Provider CRUD to use stable entry IDs instead of dashboard list indexes.
- **2026-03-23**: Sensitive dashboard management routes now fail closed to localhost-only access whenever dashboard auth is disabled. Remote access remains available after `ccs config auth setup`, but AI Provider management, CLIProxy auth/status helpers, and other write-capable settings endpoints no longer trust unauthenticated non-loopback requests.
- **2026-03-19**: **#649** CCS splits CLIProxy provider-key authoring into a dedicated `CLIProxy -> AI Providers` dashboard route. `/cliproxy` now stays focused on OAuth accounts and variants, `/cliproxy/ai-providers` owns Gemini/Codex/Claude/Vertex/OpenAI-compatible key management, and `/providers` stays reserved for CCS-native API Profiles.
- **2026-03-18**: **#755** Marketplace refresh no longer reuses one shared `known_marketplaces.json` across isolated instances. CCS now keeps marketplace payload directories shared while reconciling per-instance marketplace metadata so Claude Code validation succeeds for alternating or concurrent profiles, including Windows copy fallback.
- **2026-03-17**: Deprecated user-facing GLMT discovery across CLI help, completions, presets, and docs. Existing `glmt` profiles now run through a compatibility path that normalizes legacy proxy settings to the direct GLM endpoint.
- **#748**: API profile creation now keeps provider selection compact by collapsing advanced presets behind an explicit toggle, shrinking chooser cards so the form fields stay visually primary, and giving `llama.cpp` a dedicated provider logo.
- **#744**: API profile creation now keeps featured providers in a horizontal rail with scroll fallback, moves Anthropic Direct API to the end, reuses the shared Claude logo, and separates the custom-endpoint entry point from advanced template discovery.
- **#724**: Codex startup is now free-plan safe. CCS defaults new Codex sessions to a cross-plan model and uses runtime fallback handling for unsupported paid-only models without rewriting the saved dashboard settings.
- **#737**: Dashboard model pickers in Cursor, Copilot, and CLIProxy now use a searchable combobox with autofocus and explicit no-results states for large model catalogs.
- **#736**: `ccs config` now supports explicit dashboard bind hosts via `--host`, and surfaces remote-access warnings plus reachable URLs when the effective bind is non-loopback.
- **#1121**: Usage analytics pricing now refreshes cached models.dev metadata before cost derivation, keeps CCS static pricing as the offline fallback, and carries provider identity through CLIProxy/native runtime breakdowns so subscription-backed providers do not inherit paid API pricing.
CCS remains a CLI-first profile and runtime manager with dashboard parity for
configuration. New work should preserve:
### Maintainability Hardening Kickoff
- Issue owner: Stream D for **#542**
- Automated inventory command: `bun run report:hardening`
- Generated report artifacts:
- `docs/reports/hardening-inventory.json`
- `docs/reports/hardening-inventory.md`
- Debt burndown tracker: [Hardening Debt Burndown Tracker](./hardening-debt-burndown.md)
### Remaining Large Files (Acceptable)
**CLI** (complex core logic):
- `model-pricing.ts` (676 lines) - Data file
- `glmt-proxy.ts` (675 lines) - Legacy internal compatibility proxy
- `cliproxy-executor.ts` (666 lines) - Core execution
- `ccs.ts` (596 lines) - Entry point
**UI** (external/shadcn):
- `components/ui/sidebar.tsx` (674 lines) - shadcn component
---
## GitHub Issues Backlog
### Critical (Blocking Users)
| Issue | Title | Type |
|-------|-------|------|
| #158 | AGY not working - Missing API Key - Run /login | bug |
| #155 | Invalid JSON payload error with Gemini/Antigravity | bug |
| #124 | Incorrect model ID for Claude 3.5 Sonnet (Thinking) | bug |
### High Priority (Features)
| Issue | Title | Type | Status |
|-------|-------|------|--------|
| #142 | Configure with available CLIProxyAPI | enhancement | **COMPLETE** (v7.1) |
| #157 | Support for Kiro auth from CLIProxyAPIPlus | enhancement | **COMPLETE** (v7.2) |
| #123 | Add More Models | enhancement | Ongoing |
| #114 | OpenCode Zen Free model + Auto Rotation API Key | enhancement | - |
### Medium Priority
| Issue | Title | Type |
|-------|-------|------|
| #137 | CCS Cannot Connect to IDE, but Native Claude Works | support |
| #89 | Add Claude Code CLI flag passthrough for delegation | enhancement |
| #659 | Comprehensive Vietnamese i18n for dashboard | enhancement |
### Low Priority / Questions
| Issue | Title | Type |
|-------|-------|------|
| #156 | Configure API for Zed IDE | docs |
| #140 | Do we support ampcode? | question |
| #111 | Factory droid CLI support | enhancement |
| #103 | /context command returns incorrect context | invalid |
---
## Future Roadmap
### Priority 1: Multiple CLIProxyAPI Instances
Support connecting to multiple CLIProxyAPI servers simultaneously.
**Use Cases**:
- Load balancing across multiple proxy servers
- Failover when primary server unavailable
- Geographic distribution for latency optimization
- Separate proxies for different provider groups
**Proposed Config**:
```yaml
cliproxy:
instances:
primary:
url: http://localhost:8000
providers: [gemini, codex]
weight: 80
secondary:
url: http://192.168.1.100:8000
providers: [agy]
weight: 20
failover:
url: http://backup.example.com:8000
priority: 2 # Only if primary/secondary fail
strategy: weighted-round-robin
```
### Priority 2: Native Git Worktree Support
Opt-in automatic git worktree management for features/issues.
**Use Cases**:
- Automatic worktree creation when starting issue
- Isolation of feature development
- Easy cleanup after merge
- Integration with GitHub issues
**Proposed Settings**:
```yaml
worktrees:
enabled: true
base_path: ~/.ccs/worktrees
auto_create: true
auto_cleanup: true
naming: "{issue-number}-{short-title}"
```
### Priority 3: Enhanced Model Support
- **#123**: Expand model catalog with new releases
- **#124**: Fix Claude 3.5 Sonnet (Thinking) model ID
- **#114**: OpenCode Zen free model + API key rotation
### Priority 4: IDE Integration
- **#137**: Debug CCS-to-IDE connection issues
- **#156**: Zed IDE configuration documentation
- **#140**: Investigate ampcode compatibility
- **#111**: Factory droid CLI support assessment
### Priority 5: Authentication Enhancements
- **#158**: Fix AGY OAuth flow
- **#157**: ~~Add Kiro auth support from CLIProxyAPIPlus~~ **COMPLETE** (v7.2)
- GitHub Copilot (ghcp) Device Code flow **COMPLETE** (v7.2, now deprecated compatibility)
- Hybrid quota management **COMPLETE** (v7.14)
---
## Milestones
| Milestone | Status | Target |
|-----------|--------|--------|
| Modularization (Phases 1-9) | COMPLETE | - |
| Remote CLIProxy Support (#142) | COMPLETE | v7.1 |
| Kiro + GitHub Copilot OAuth (#157) | COMPLETE, Copilot now deprecated compatibility | v7.2 |
| Hybrid Quota Management | COMPLETE | v7.14 |
| Docker Support (PR #345) | COMPLETE | v7.23 |
| Image Analysis Hook | COMPLETE | v7.34 |
| Third-Party Tool Integration | COMPLETE | v7.39 |
| Critical Bug Fixes (#158, #155, #124) | PLANNED | Q1 2026 |
| Multiple CLIProxyAPI Instances | PLANNED | Q1 2026 |
| Git Worktree Support | PLANNED | Q2 2026 |
| Enhanced Model Support | PLANNED | Q2 2026 |
---
## Success Criteria
All criteria achieved:
- [x] Files under 200 lines (except documented exceptions)
- [x] Every directory has barrel export
- [x] No circular dependencies
- [x] TypeScript strict mode passing
- [x] 90%+ test coverage
- [x] Clear domain boundaries
- [x] Consistent naming conventions
## Historical Maintainability Gate (Retired)
This section is preserved as historical context from the original Issue `#539` work.
- The maintainability baseline gate is no longer part of the active CCS workflow.
- Current contributor and CI gates are documented in `CLAUDE.md`, `CONTRIBUTING.md`, and the GitHub workflow files.
- Do not assume `maintainability:baseline` or `maintainability:check` exist unless they are reintroduced in a future follow-up.
---
- isolated account/profile state;
- explicit provider and runtime selection;
- actionable, ASCII-safe terminal errors;
- compatible CLI, dashboard, and public-doc contracts;
- safe local defaults for credentials, browser access, and proxy routing.
## Related Documentation
- [Codebase Summary](./codebase-summary.md) - Current structure
- [Code Standards](./code-standards.md) - Patterns and conventions
- [System Architecture](./system-architecture/index.md) - Architecture diagrams
- [Hardening Debt Burndown Tracker](./hardening-debt-burndown.md) - Legacy shim + sync-fs debt tracking
- [CLAUDE.md](../CLAUDE.md) - AI development guidance
- [Codebase Summary](./codebase-summary.md)
- [Code Standards](./code-standards.md)
- [System Architecture](./system-architecture/index.md)
- [CLAUDE.md](../CLAUDE.md)
+141 -126
View File
@@ -1,118 +1,11 @@
{
"scope": "src/**/*.{ts,tsx,js,jsx,mjs,cjs}",
"syncFs": {
"totalOccurrences": 2358,
"filesAffected": 253,
"hotpathOccurrences": 1991,
"hotpathFilesAffected": 202,
"totalOccurrences": 2378,
"filesAffected": 254,
"hotpathOccurrences": 1102,
"hotpathFilesAffected": 150,
"topHotpathFiles": [
{
"file": "src/cliproxy/__tests__/pool-routing-phase3.test.ts",
"count": 96,
"calls": [
"existsSync",
"mkdirSync",
"mkdtempSync",
"readFileSync",
"rmSync",
"writeFileSync"
],
"markers": []
},
{
"file": "src/cliproxy/config/__tests__/config-generator.test.js",
"count": 88,
"calls": [
"existsSync",
"mkdirSync",
"mkdtempSync",
"readFileSync",
"rmSync",
"writeFileSync"
],
"markers": []
},
{
"file": "src/cliproxy/config/__tests__/claude-model-neutral.test.ts",
"count": 63,
"calls": [
"existsSync",
"mkdirSync",
"mkdtempSync",
"readFileSync",
"rmSync",
"statSync",
"writeFileSync"
],
"markers": []
},
{
"file": "src/cliproxy/accounts/__tests__/account-safety-quota-exhaustion.test.ts",
"count": 48,
"calls": [
"existsSync",
"mkdirSync",
"mkdtempSync",
"readFileSync",
"rmSync",
"writeFileSync"
],
"markers": []
},
{
"file": "src/cliproxy/accounts/__tests__/account-registry-integrity.test.ts",
"count": 45,
"calls": [
"existsSync",
"mkdirSync",
"mkdtempSync",
"readdirSync",
"readFileSync",
"rmSync",
"writeFileSync"
],
"markers": []
},
{
"file": "src/cliproxy/executor/__tests__/variant-port-integration.test.js",
"count": 36,
"calls": [
"existsSync",
"mkdirSync",
"readdirSync",
"readFileSync",
"rmSync",
"unlinkSync",
"writeFileSync"
],
"markers": []
},
{
"file": "src/cliproxy/executor/__tests__/composite-variant-service.test.ts",
"count": 33,
"calls": [
"existsSync",
"mkdirSync",
"mkdtempSync",
"readFileSync",
"rmSync",
"writeFileSync"
],
"markers": []
},
{
"file": "src/cliproxy/executor/__tests__/variant-port-edge-cases.test.js",
"count": 33,
"calls": [
"existsSync",
"mkdirSync",
"readdirSync",
"rmSync",
"unlinkSync",
"writeFileSync"
],
"markers": []
},
{
"file": "src/utils/browser/mcp-installer.ts",
"count": 32,
@@ -130,14 +23,136 @@
"markers": []
},
{
"file": "src/cliproxy/__tests__/session-tracker-port.test.js",
"count": 31,
"file": "src/utils/image-analysis/mcp-installer.ts",
"count": 30,
"calls": [
"chmodSync",
"copyFileSync",
"existsSync",
"mkdirSync",
"readFileSync",
"renameSync",
"statSync",
"unlinkSync",
"writeFileSync"
],
"markers": []
},
{
"file": "src/utils/claude-symlink-manager.ts",
"count": 27,
"calls": [
"copyFileSync",
"existsSync",
"lstatSync",
"mkdirSync",
"readdirSync",
"readlinkSync",
"renameSync",
"rmSync",
"statSync",
"symlinkSync",
"unlinkSync"
],
"markers": []
},
{
"file": "src/cliproxy/config/env-builder.ts",
"count": 25,
"calls": [
"existsSync",
"mkdirSync",
"readdirSync",
"readFileSync",
"writeFileSync"
],
"markers": []
},
{
"file": "src/management/shared-manager/migrations.ts",
"count": 25,
"calls": [
"copyFileSync",
"cpSync",
"existsSync",
"lstatSync",
"mkdirSync",
"readdirSync",
"symlinkSync",
"unlinkSync",
"writeFileSync"
],
"markers": []
},
{
"file": "src/utils/websearch/mcp-installer.ts",
"count": 25,
"calls": [
"chmodSync",
"copyFileSync",
"existsSync",
"mkdirSync",
"readFileSync",
"renameSync",
"statSync",
"unlinkSync",
"writeFileSync"
],
"markers": []
},
{
"file": "src/cliproxy/services/variant-settings.ts",
"count": 23,
"calls": [
"existsSync",
"mkdirSync",
"readFileSync",
"renameSync",
"unlinkSync",
"writeFileSync"
],
"markers": []
},
{
"file": "src/management/recovery-manager.ts",
"count": 23,
"calls": [
"copyFileSync",
"existsSync",
"lstatSync",
"mkdirSync",
"renameSync",
"statSync",
"unlinkSync",
"writeFileSync"
],
"markers": []
},
{
"file": "src/utils/shell-completion.ts",
"count": 23,
"calls": [
"appendFileSync",
"copyFileSync",
"existsSync",
"mkdirSync",
"readFileSync",
"statSync"
],
"markers": []
},
{
"file": "src/utils/claude-dir-installer.ts",
"count": 21,
"calls": [
"copyFileSync",
"cpSync",
"existsSync",
"lstatSync",
"mkdirSync",
"readdirSync",
"renameSync",
"rmSync",
"statSync",
"unlinkSync",
"writeFileSync"
],
@@ -285,8 +300,8 @@
]
},
"legacyShim": {
"totalMarkers": 454,
"filesAffected": 170,
"totalMarkers": 456,
"filesAffected": 171,
"topFiles": [
{
"file": "src/auth/profile-detector.ts",
@@ -476,11 +491,11 @@
},
"maintainability": {
"typedErrors": {
"totalThrows": 440,
"typedThrows": 68,
"totalThrows": 446,
"typedThrows": 74,
"plainThrows": 312,
"otherThrows": 60,
"adoptionRatio": 0.1545,
"adoptionRatio": 0.1659,
"topSubdomainsByThrows": [
{
"subdomain": "web-server",
@@ -558,8 +573,8 @@
},
"loggerCoverage": {
"filesWithCreateLogger": 65,
"totalSourceFiles": 751,
"coverageRatio": 0.0866,
"totalSourceFiles": 752,
"coverageRatio": 0.0864,
"subdomainsWithZeroCreateLogger": [
"api",
"bin",
@@ -728,7 +743,11 @@
},
{
"file": "src/web-server/routes/settings-routes.ts",
"loc": 1041
"loc": 1042
},
{
"file": "src/cliproxy/config/generator.ts",
"loc": 1034
},
{
"file": "src/cliproxy/proxy/tool-sanitization-proxy.ts",
@@ -738,10 +757,6 @@
"file": "src/cliproxy/config/env-builder.ts",
"loc": 1017
},
{
"file": "src/cliproxy/config/generator.ts",
"loc": 1012
},
{
"file": "src/commands/cliproxy/variant-subcommand.ts",
"loc": 997
+19 -19
View File
@@ -6,27 +6,27 @@ Scope: `src/**/*.{ts,tsx,js,jsx,mjs,cjs}`
| Metric | Value |
|---|---:|
| Sync fs occurrences (all) | 2358 |
| Sync fs files affected (all) | 253 |
| Sync fs occurrences (runtime hotpaths) | 1991 |
| Sync fs files affected (runtime hotpaths) | 202 |
| Legacy shim markers | 454 |
| Legacy shim files affected | 170 |
| Sync fs occurrences (all) | 2378 |
| Sync fs files affected (all) | 254 |
| Sync fs occurrences (runtime hotpaths) | 1102 |
| Sync fs files affected (runtime hotpaths) | 150 |
| Legacy shim markers | 456 |
| Legacy shim files affected | 171 |
## Top Runtime Hotpath Sync fs Files
| File | Sync Calls | API Names |
|---|---:|---|
| `src/cliproxy/__tests__/pool-routing-phase3.test.ts` | 96 | existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync |
| `src/cliproxy/config/__tests__/config-generator.test.js` | 88 | existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync |
| `src/cliproxy/config/__tests__/claude-model-neutral.test.ts` | 63 | existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, statSync, writeFileSync |
| `src/cliproxy/accounts/__tests__/account-safety-quota-exhaustion.test.ts` | 48 | existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync |
| `src/cliproxy/accounts/__tests__/account-registry-integrity.test.ts` | 45 | existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, writeFileSync |
| `src/cliproxy/executor/__tests__/variant-port-integration.test.js` | 36 | existsSync, mkdirSync, readdirSync, readFileSync, rmSync, unlinkSync, writeFileSync |
| `src/cliproxy/executor/__tests__/composite-variant-service.test.ts` | 33 | existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync |
| `src/cliproxy/executor/__tests__/variant-port-edge-cases.test.js` | 33 | existsSync, mkdirSync, readdirSync, rmSync, unlinkSync, writeFileSync |
| `src/utils/browser/mcp-installer.ts` | 32 | chmodSync, copyFileSync, existsSync, mkdirSync, readFileSync, renameSync, statSync, unlinkSync, writeFileSync |
| `src/cliproxy/__tests__/session-tracker-port.test.js` | 31 | existsSync, mkdirSync, readdirSync, readFileSync, rmSync, unlinkSync, writeFileSync |
| `src/utils/image-analysis/mcp-installer.ts` | 30 | chmodSync, copyFileSync, existsSync, mkdirSync, readFileSync, renameSync, statSync, unlinkSync, writeFileSync |
| `src/utils/claude-symlink-manager.ts` | 27 | copyFileSync, existsSync, lstatSync, mkdirSync, readdirSync, readlinkSync, renameSync, rmSync, statSync, symlinkSync, unlinkSync |
| `src/cliproxy/config/env-builder.ts` | 25 | existsSync, mkdirSync, readFileSync, writeFileSync |
| `src/management/shared-manager/migrations.ts` | 25 | copyFileSync, cpSync, existsSync, lstatSync, mkdirSync, readdirSync, symlinkSync, unlinkSync, writeFileSync |
| `src/utils/websearch/mcp-installer.ts` | 25 | chmodSync, copyFileSync, existsSync, mkdirSync, readFileSync, renameSync, statSync, unlinkSync, writeFileSync |
| `src/cliproxy/services/variant-settings.ts` | 23 | existsSync, mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync |
| `src/management/recovery-manager.ts` | 23 | copyFileSync, existsSync, lstatSync, mkdirSync, renameSync, statSync, unlinkSync, writeFileSync |
| `src/utils/shell-completion.ts` | 23 | appendFileSync, copyFileSync, existsSync, mkdirSync, readFileSync, statSync |
| `src/utils/claude-dir-installer.ts` | 21 | copyFileSync, cpSync, existsSync, lstatSync, mkdirSync, readdirSync, renameSync, rmSync, statSync, unlinkSync, writeFileSync |
## Top Legacy Shim Marker Files
@@ -56,11 +56,11 @@ Scope: `src/**/*.{ts,tsx,js,jsx,mjs,cjs}`
| Metric | Value |
|---|---:|
| typed-error adoption (typed/total throws) | 15.4% (68/440) |
| typed-error adoption (typed/total throws) | 16.6% (74/446) |
| typed-error adoption (P4 locked subdomains) | 91.7% (22/24), target 40% |
| hotpath console.error/warn occurrences | 266 (571 total, 305 CLI-UX exempt) |
| hotpath console.error/warn files | 82 |
| files with createLogger | 65/751 |
| files with createLogger | 65/752 |
| subdomains with zero createLogger | 15 (api, bin, channels, cliproxy, cliproxy/accounts, cliproxy/ai-providers, cliproxy/binary, cliproxy/config, cliproxy/management, cliproxy/sync, cliproxy/types, config, dispatcher, shared, types) |
| files > 400 LOC | 91 |
| files > 600 LOC | 42 |
@@ -95,10 +95,10 @@ Scope: `src/**/*.{ts,tsx,js,jsx,mjs,cjs}`
| `src/cursor/cursor-executor.ts` | 1234 |
| `src/web-server/model-pricing.ts` | 1105 |
| `src/cliproxy/auth/oauth-process.ts` | 1048 |
| `src/web-server/routes/settings-routes.ts` | 1041 |
| `src/web-server/routes/settings-routes.ts` | 1042 |
| `src/cliproxy/config/generator.ts` | 1034 |
| `src/cliproxy/proxy/tool-sanitization-proxy.ts` | 1020 |
| `src/cliproxy/config/env-builder.ts` | 1017 |
| `src/cliproxy/config/generator.ts` | 1012 |
| `src/commands/cliproxy/variant-subcommand.ts` | 997 |
| `src/cliproxy/quota/quota-manager.ts` | 954 |
| `src/web-server/services/codex-dashboard-service.ts` | 940 |
@@ -1,26 +1,36 @@
# Typed-Error Exit-Code Compat Audit (P4)
# Typed-Error Exit-Code Compatibility Audit
Date: 2026-06-18. Phase 4 of the maintainability/traceability epic.
Status: resolved contract.
## Question (open Q1)
## Question
Are CCS CLI exit codes a documented public contract that users or CI scripts depend on? This determines whether migrating `throw new Error(...)` to typed errors (which changes the exit code) is safe.
## Finding
Typed exit codes are **already wired end-to-end**. `handleError` -> `getExitCode` extracts `error.code` from any `CCSError` and passes it to `process.exit` (`src/errors/error-handler.ts`, `src/ccs.ts:145`). The `ExitCode` enum and the class-to-code mapping in `src/errors/error-types.ts` are complete and pre-date this epic.
Typed exit codes are wired end-to-end. `handleError` calls the local
`getExitCode` helper and passes the resulting code to `process.exit` in
`src/errors/error-handler.ts`. The CLI entry point delegates uncaught errors to
`handleError` in `src/ccs.ts`. `src/errors/exit-codes.ts` and
`src/errors/error-types.ts` define the stable mapping.
**Only documented public contract:** `ccs doctor` documents exit codes 0 (healthy) / 1 (unhealthy) in `src/commands/doctor-command.ts:55-58`. `ccs doctor` is OUTSIDE the P4 priority subdomains and is NOT touched by P4. Its 0/1 contract is preserved.
`ccs doctor` separately documents exit codes 0 (healthy) and 1 (unhealthy) in
`src/commands/doctor-command.ts`. Preserve that command contract.
**No CI/scripts assert on ccs exit codes.** `scripts/ci-parity-gate.sh` uses `set -euo pipefail` but performs no `ccs` exit-code branching. `.github/workflows/*` perform no ccs exit-code assertions. Prior exit-code changes in CHANGELOG are treated as bugfixes; no documented breaking changes.
The typed taxonomy is enforced by
`src/errors/__tests__/typed-error-migration-exit-codes.test.ts`; do not treat
the mapping as an untested migration note.
## Decision
**Migrate freely** in the P4 priority subdomains (`cliproxy/quota`, `cliproxy/auth`, `web-server/routes`, `auth`). Preserve `GENERAL_ERROR(1)` only where no clear subclass applies. Behavior-lock tests assert the new typed codes.
Use the most specific typed error in `cliproxy/quota`, `cliproxy/auth`,
`web-server/routes`, and `auth`. Preserve `GENERAL_ERROR(1)` only where no clear
subclass applies. Update the behavior-lock test with any intentional taxonomy
change.
## Exit-code mapping (the contract this audit locks)
| Typed class | ExitCode | Value | Used for (P4 sites) |
| Typed class | ExitCode | Value | Typical use |
|---|---|---:|---|
| `ProfileError` | `PROFILE_ERROR` | 7 | profile/account/variant not found, already exists |
| `AuthError` | `AUTH_ERROR` | 4 | OAuth/token/Kiro/GitLab auth flow failures, refresh ownership |
@@ -31,15 +41,5 @@ Typed exit codes are **already wired end-to-end**. `handleError` -> `getExitCode
| `ProxyError` | `PROXY_ERROR` | 8 | |
| `MigrationError` | `MIGRATION_ERROR` | 9 | |
## Per-site decisions
See the P4 commit for the full site list. Summary by subclass chosen:
- `ProfileError`: profile/account not-found + already-exists (`src/auth/profile-registry.ts`, `src/cliproxy/auth/auth-token-manager.ts`, `src/cliproxy/auth/auth-types.ts`).
- `AuthError`: OAuth start failed, paste-callback unavailable, Kiro auth method unsupported, token refresh ownership (`src/cliproxy/auth/oauth-handler.ts`, `provider-refreshers/index.ts`).
- `ConfigError`: invalid settings path, settings not found, CLIProxy config not initialized, GitLab URL format, failed read/write profiles, copilot sync failure (`src/web-server/routes/*`, `src/cliproxy/auth/oauth-handler.ts`, `src/auth/profile-registry.ts`).
- `ValidationError`: invalid profile name, invalid target, invalid host, Kiro IDC start-url (`src/web-server/routes/*`, `src/cliproxy/auth/auth-types.ts`).
- `ProviderError`: unsupported provider backend (`src/web-server/routes/image-analysis-routes.ts`).
## Outcome
Typed-error adoption in the locked subdomains: 0/23 -> 21/23 (91.3%), well above the 40% target. Exit-code changes are intentional and documented here; release notes for the epic PR should mention the differentiated exit codes.
Current adoption counts belong in the generated hardening inventory. Git history
contains the completed migration narrative.
-216
View File
@@ -1,216 +0,0 @@
# Session Sharing Technical Analysis
Last Updated: 2026-05-05
## Summary
CCS supports practical cross-account continuity by sharing workspace context files between selected accounts, while keeping credentials isolated per account.
This is implemented as a context policy per account:
- `isolated` (default): account keeps its own workspace context
- `shared` + `standard` (default): account workspace context is linked to a shared context group
- `shared` + `deeper` (advanced opt-in): account also shares continuity artifacts
## Recommended Two-Account Route
Use `ccs auth` account profiles when you want two real Claude accounts and want to choose which one runs each session:
```bash
ccs auth create work
ccs auth create personal
ccs work
ccs personal
```
This keeps usage and credentials isolated. Each account owns its own Claude config directory, login state, and `.anthropic` credentials.
Shared Resources are separate from History Sync. By default, non-bare account profiles inherit Claude-local resources from native Claude:
```text
~/.ccs/instances/<account>/settings.json
-> ~/.ccs/shared/settings.json
-> ~/.claude/settings.json
```
This covers ordinary Claude Code `settings.json`, commands, skills, agents, and plugins. It is not token sharing. `ccs auth show <account>` reports `Resources`, `Settings`, `History`, and `Plain ccs` lanes so users can see whether shared resources and resume history are aligned.
For existing accounts, change Shared Resources from the CLI:
```bash
ccs auth resources work --mode profile-local
ccs auth resources work --mode shared
```
- `shared`: link plugins, commands, skills, agents, and `settings.json` from the shared Claude resource layout.
- `profile-local`: detach those shared resources for the account. This is the existing `--bare` behavior exposed as an existing-account setting.
Only opt in to shared history when both accounts should see the same local continuity:
```bash
ccs auth create work2 --share-context --context-group daily --deeper-continuity
```
For existing History Sync, use Dashboard -> Accounts -> Sync on both accounts, set both to `shared`, and use the same `History Sync Group`. Use `deeper` only when users expect stronger local handoff beyond project context. History Sync does not control plugins or `settings.json`; use `ccs auth resources` for that.
## Why This Is Safe Enough
CCS only shares workspace context paths (project/session context files). It does **not** merge or copy authentication credentials between accounts.
Credential storage remains per account instance.
## Implementation Model
Account metadata is stored in `~/.ccs/config.yaml`:
```yaml
accounts:
work:
created: "2026-02-24T00:00:00.000Z"
last_used: null
shared_resource_mode: "shared"
context_mode: "shared"
context_group: "team-alpha"
continuity_mode: "deeper"
```
Rules:
- `shared_resource_mode` controls commands, skills, agents, plugins, and `settings.json` (`shared` or `profile-local`)
- `context_mode` must be `isolated` or `shared`
- `context_group` is required when `context_mode=shared`
- `continuity_mode` is valid only when `context_mode=shared` (`standard` or `deeper`)
- group normalization: trim, lowercase, internal spaces -> `-`
- group must start with a letter and only include `[a-zA-Z0-9_-]`
- max length: `64`
Deeper continuity links these directories per context group:
- `session-env`
- `file-history`
- `shell-snapshots`
- `todos`
`.anthropic` and account credentials remain isolated.
## Cross-Profile Inheritance (API / CLIProxy / Copilot)
You can explicitly map non-account profiles (including `default`) to reuse continuity artifacts from an account profile:
```yaml
continuity:
inherit_from_account:
glm: pro
gemini: pro
copilot: pro
```
Behavior:
- Applies only when running Claude target (`ccs <profile>` or `--target claude`)
- Does not change provider credentials or API routing
- Reuses `CLAUDE_CONFIG_DIR` from mapped account profile after normal account context policy resolution
- Invalid/missing mapped accounts are skipped safely
### Resume Lane Note
Resume follows the active `CLAUDE_CONFIG_DIR`, not just the continuity group:
- plain `ccs -r` resumes the lane plain `ccs` is using right now
- `ccs <account> -r` resumes only that account lane
- those two commands can point at different continuity inventories
That means `shared + deeper` on an account does **not** automatically make old plain-`ccs` resume history appear inside `ccs <account> -r`.
If you want future plain `ccs` sessions to use an account lane, either:
```bash
ccs auth default work
```
or map the default profile explicitly:
```yaml
continuity:
inherit_from_account:
default: work
```
Example with an existing `ck` account:
```bash
ccs auth show ck
ccs auth backup default
ccs auth default ck
```
`ccs auth default ck` makes future plain `ccs` sessions use the `ck` account lane, so future `ccs` and `ccs ck` resume from the same local inventory. It does not automatically import old native `~/.claude/projects` history into `ck`; keep using `ccs -r` for the old native lane until you intentionally migrate that local history.
## User Workflows
### New account with shared context
```bash
ccs auth create work2 --share-context
ccs auth create backup --share-context --context-group sprint-a
ccs auth create backup2 --share-context --context-group sprint-a --deeper-continuity
```
### Existing account
History Sync:
- Open `ccs config`
- Go to `Accounts`
- Click the pencil icon (`Edit History Sync`)
- Choose `isolated` or `shared`, set group, and (optionally) choose deeper continuity
Shared Resources:
```bash
ccs auth resources work --mode profile-local
ccs auth resources work --mode shared
```
Dashboard:
- Open `ccs config`
- Go to `Accounts`
- Use `Resources` to switch an existing account between `shared` and `profile-local`
- Go to `Shared Resources` to inspect the shared commands, skills, agents, plugins, and `settings.json` hub
No account recreation required for this workflow.
### Backup Before Changing Sync
CCS can back up local continuity artifacts before you change settings:
```bash
ccs auth backup work
ccs auth backup default
```
- `ccs auth backup work` backs up the selected account lane
- `ccs auth backup default` backs up the lane plain `ccs` would use right now
- this is a local continuity backup, not a guaranteed export of all upstream Claude-hosted resume state
## Current Limitations
- Shared context is local filesystem sharing. It does not bypass remote provider permission models.
- Session continuity still depends on what the upstream tool/provider stores and allows.
- Context sharing should only be enabled for accounts you intentionally trust to share workspace history.
- Shared Resources inspection is read-only in the dashboard. Editing individual files still belongs to the owning command, skill, plugin, or settings surface.
## Alternative: CLIProxy Claude Pool
For users who prefer lower manual account switching, use CLIProxy Claude pool instead:
- Authenticate pool accounts via `ccs cliproxy auth claude`
- Manage account pool behavior in `ccs config` -> `CLIProxy Plus`
## Validation Checklist
- Confirm account row shows `shared (<group>)` in Dashboard Accounts table
- Switch between accounts in the same group and verify workspace continuity
- Run `ccs doctor` if symlink/context health looks inconsistent
+26
View File
@@ -44,3 +44,29 @@ the first time via right-click then Open, or clear quarantine with
`xattr -dr com.apple.quarantine "/Applications/CCS Bar.app"`. Developer ID
signing + notarization (`CCS_BAR_SIGNING=developer-id`) is the public-launch
path and is not required for ad-hoc distribution.
The app ships as `CCS-Bar.app.zip` on the floating `ccs-bar-latest` GitHub
release. `macos-bar/VERSION` is the single semver source; bump it in the same PR
when a new app version is required.
### Automatic release
`.github/workflows/bar-release.yml` publishes the asset after a push to `main`
that changes `macos-bar/**`, or from a manual `workflow_dispatch` run whose
selected ref is `main`. It runs on the dedicated self-hosted macOS runner
labelled `ccs-bar`.
### Manual fallback
From a macOS machine with Swift and `gh`:
```bash
cd macos-bar
./Scripts/package_app.sh
gh release upload ccs-bar-latest dist/CCS-Bar.app.zip --clobber
```
Pass a version to `package_app.sh` only when intentionally overriding
`macos-bar/VERSION`. Ad-hoc signing is the default; use
`CCS_BAR_SIGNING=developer-id` only with the configured Apple signing and
notarization credentials.
+3 -30
View File
@@ -57,36 +57,9 @@ if git show-ref --verify --quiet "refs/remotes/origin/$BASE_BRANCH"; then
fi
fi
# Hardening inventory freshness: the maintainability metrics artifact must be
# regenerated within 30 days so the burndown stays current. Runs only after the
# skip conditions above (CCS_SKIP_PREPUSH_GATE, detached HEAD, behind origin).
HARDENING_JSON="docs/reports/hardening-inventory.json"
if [[ ! -f "$HARDENING_JSON" ]]; then
echo "[X] Missing $HARDENING_JSON."
echo " Regenerate with: bun run report:hardening"
exit 1
fi
HARDENING_TS=""
# If the working-tree copy differs from HEAD (contributor regenerated but not
# yet committed), use filesystem mtime; otherwise use the last commit time,
# which is stable across CI clones (checkout resets mtimes) and so correctly
# flags a stale committed artifact.
if git diff --quiet -- "$HARDENING_JSON" 2>/dev/null && git diff --cached --quiet -- "$HARDENING_JSON" 2>/dev/null; then
HARDENING_TS=$(git log -1 --format=%ct -- "$HARDENING_JSON" 2>/dev/null)
else
HARDENING_TS=$(stat -f %m "$HARDENING_JSON" 2>/dev/null || stat -c %Y "$HARDENING_JSON" 2>/dev/null)
fi
if [[ -n "$HARDENING_TS" ]]; then
NOW_TS=$(date +%s)
AGE_DAYS=$(( (NOW_TS - HARDENING_TS) / 86400 ))
if (( AGE_DAYS > 30 )); then
echo "[X] Hardening inventory is stale (${AGE_DAYS}d old; max 30d)."
echo " Regenerate with: bun run report:hardening"
echo " Then commit docs/reports/hardening-inventory.{json,md}."
exit 1
fi
echo "[i] Hardening inventory fresh (${AGE_DAYS}d old; max 30d)."
fi
# Age does not prove that generated metrics match the checked-out source tree.
# Compare both inventory artifacts byte-for-byte before expensive parity checks.
node scripts/hardening-inventory.js --check
echo "[i] Running CI-parity local checks..."
# `set -euo pipefail` above makes every step fail fast. Keep these commands
+37 -4
View File
@@ -4,6 +4,7 @@ const fs = require('fs');
const path = require('path');
const { collectMaintainabilityMetrics } = require('./maintainability-metrics.js');
const { isTestFile } = require('./runtime-source-classifier.js');
const ROOT_DIR = path.resolve(__dirname, '..');
const SRC_DIR = path.join(ROOT_DIR, 'src');
@@ -98,7 +99,7 @@ function isSourceFile(filePath) {
}
function isHotpath(filePath) {
return HOTPATH_PATTERNS.some((pattern) => pattern.test(filePath));
return !isTestFile(filePath) && HOTPATH_PATTERNS.some((pattern) => pattern.test(filePath));
}
function walkFiles(dirPath) {
@@ -450,7 +451,9 @@ function renderMarkdown(report) {
lines.push(`| Sync fs occurrences (all) | ${report.syncFs.totalOccurrences} |`);
lines.push(`| Sync fs files affected (all) | ${report.syncFs.filesAffected} |`);
lines.push(`| Sync fs occurrences (runtime hotpaths) | ${report.syncFs.hotpathOccurrences} |`);
lines.push(`| Sync fs files affected (runtime hotpaths) | ${report.syncFs.hotpathFilesAffected} |`);
lines.push(
`| Sync fs files affected (runtime hotpaths) | ${report.syncFs.hotpathFilesAffected} |`
);
lines.push(`| Legacy shim markers | ${report.legacyShim.totalMarkers} |`);
lines.push(`| Legacy shim files affected | ${report.legacyShim.filesAffected} |`);
lines.push('');
@@ -547,10 +550,39 @@ function renderMarkdown(report) {
function main() {
const report = buildReport();
const jsonContent = JSON.stringify(report, null, 2) + '\n';
const markdownContent = renderMarkdown(report);
const checkOnly = process.argv.includes('--check');
if (checkOnly) {
const staleReports = [
[JSON_REPORT_PATH, jsonContent],
[MD_REPORT_PATH, markdownContent],
].filter(([reportPath, expected]) => {
try {
return fs.readFileSync(reportPath, 'utf8') !== expected;
} catch {
return true;
}
});
if (staleReports.length > 0) {
console.error('[X] Hardening inventory does not match the current source tree:');
for (const [reportPath] of staleReports) {
console.error(` ${relativePath(reportPath)}`);
}
console.error(' Regenerate with: bun run report:hardening');
process.exitCode = 1;
return;
}
console.log('[OK] Hardening inventory matches the current source tree.');
return;
}
fs.mkdirSync(REPORT_DIR, { recursive: true });
fs.writeFileSync(JSON_REPORT_PATH, JSON.stringify(report, null, 2) + '\n', 'utf8');
fs.writeFileSync(MD_REPORT_PATH, renderMarkdown(report), 'utf8');
fs.writeFileSync(JSON_REPORT_PATH, jsonContent, 'utf8');
fs.writeFileSync(MD_REPORT_PATH, markdownContent, 'utf8');
const relJson = relativePath(JSON_REPORT_PATH);
const relMd = relativePath(MD_REPORT_PATH);
@@ -575,6 +607,7 @@ function main() {
module.exports = {
buildReport,
collectSyncCallSites,
isTestFile,
renderMarkdown,
stripComments,
};
+2 -8
View File
@@ -24,6 +24,7 @@
const fs = require('fs');
const path = require('path');
const { isTestFile } = require('./runtime-source-classifier.js');
const SOURCE_EXTENSIONS = new Set(['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs']);
@@ -79,14 +80,6 @@ function isSourceFile(filePath) {
return SOURCE_EXTENSIONS.has(path.extname(filePath));
}
function isTestFile(relPath) {
return (
/(?:^|\/)(?:__tests__|tests?)\//.test(relPath) ||
/\.test\./.test(relPath) ||
/\.spec\./.test(relPath)
);
}
function isCliUxExempt(relPath) {
return CLI_UX_EXEMPT_PREFIXES.some(function (prefix) {
return relPath.startsWith(prefix);
@@ -350,6 +343,7 @@ module.exports = {
classifyThrows: classifyThrows,
countConsoleErrors: countConsoleErrors,
hasCreateLogger: hasCreateLogger,
isTestFile: isTestFile,
countLoc: countLoc,
TYPED_ERROR_CLASSES: TYPED_ERROR_CLASSES,
TYPED_ADOPTION_SUBDOMAINS: TYPED_ADOPTION_SUBDOMAINS,
+12
View File
@@ -0,0 +1,12 @@
function isTestFile(filePath) {
const normalizedPath = filePath.replace(/\\/g, '/');
return (
/(^|\/)(__tests__|__mocks__|fixtures)(\/|$)/.test(normalizedPath) ||
/(^|\/)(tests?|test-fixtures)(\/|$)/.test(normalizedPath) ||
/\.(test|spec)\.[^.]+$/.test(normalizedPath)
);
}
module.exports = {
isTestFile,
};
@@ -17,13 +17,13 @@ import {
import { ExitCode } from '../exit-codes';
/**
* P4 behavior-lock: the typed-error -> exit-code mapping is the contract this
* epic relies on. Migrating `throw new Error` to typed subclasses changes the
* process exit code (via handleError -> getExitCode); these tests lock the
* mapping so a future change is caught. See
* The typed-error -> exit-code mapping is a runtime compatibility contract.
* Migrating `throw new Error` to typed subclasses changes the process exit code
* through handleError; these tests lock the mapping so a future change is
* caught. See
* docs/reports/typed-error-exit-code-compat-audit.md.
*/
describe('typed-error taxonomy -> exit-code mapping (P4 contract)', () => {
describe('typed-error taxonomy -> exit-code compatibility contract', () => {
test('each typed class carries its documented ExitCode', () => {
expect(new ConfigError('m').code).toBe(ExitCode.CONFIG_ERROR);
expect(new NetworkError('m').code).toBe(ExitCode.NETWORK_ERROR);
+124
View File
@@ -0,0 +1,124 @@
#!/usr/bin/env node
const fs = require('fs');
const path = require('path');
const root = path.resolve(__dirname, '../..');
const failures = [];
function read(relativePath) {
return fs.readFileSync(path.join(root, relativePath), 'utf8');
}
function requireText(relativePath, expected) {
if (!read(relativePath).includes(expected)) {
failures.push(`${relativePath} is missing: ${expected}`);
}
}
function collectFiles(directory, filePattern) {
const files = [];
for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
const absolutePath = path.join(directory, entry.name);
if (entry.isDirectory()) {
files.push(...collectFiles(absolutePath, filePattern));
} else if (entry.isFile() && filePattern.test(entry.name)) {
files.push(absolutePath);
}
}
return files;
}
const removedGuides = [
'docs/ccs-bar.md',
'docs/cursor-integration.md',
'docs/dashboard-auth-cli.md',
'docs/session-sharing-technical-analysis.md',
];
for (const relativePath of removedGuides) {
if (fs.existsSync(path.join(root, relativePath))) {
failures.push(`${relativePath} should use its canonical public-doc replacement`);
}
}
requireText('README.md', 'https://docs.ccs.kaitran.ca/features/proxy/openai-compatible-providers');
requireText('README.md', 'https://docs.ccs.kaitran.ca/features/workflow/browser-automation');
requireText('docs/browser-automation.md', 'CCS_BROWSER_INTERCEPT_FULFILL_MODE=enabled');
requireText('docs/browser-automation.md', 'CCS_BROWSER_UPLOAD_ROOTS');
requireText('docs/browser-automation.md', 'CCS_BROWSER_DOWNLOAD_ROOTS');
requireText('docs/browser-automation.md', 'browser_wait_for_event');
requireText('docs/browser-automation.md', 'path-scoped bearer values');
requireText('docs/codex-auth.md', 'src/codex-auth/codex-auth-help.ts');
requireText('docs/codex-auth.md', 'CCSXP_CODEX_HOME');
requireText('docs/image-analysis.md', 'https://docs.ccs.kaitran.ca/features/ai/image-analysis');
requireText('docs/openai-compatible-providers.md', 'CCS_OPENAI_PROXY_INSECURE');
requireText('docs/openai-compatible-providers.md', 'CCS_OPENAI_PROXY_REQUEST_TIMEOUT_MS');
requireText('macos-bar/README.md', 'macos-bar/VERSION');
requireText('macos-bar/README.md', '.github/workflows/bar-release.yml');
requireText(
'.github/ISSUE_TEMPLATE/documentation.yml',
'https://docs.ccs.kaitran.ca/providers/oauth/cursor'
);
const practicalGuidanceFiles = [
path.join(root, 'README.md'),
path.join(root, 'CLAUDE.md'),
path.join(root, 'CONTRIBUTING.md'),
path.join(root, 'SECURITY.md'),
path.join(root, 'docker', 'README.md'),
path.join(root, 'macos-bar', 'README.md'),
...collectFiles(path.join(root, 'docs'), /\.mdx?$/),
...collectFiles(path.join(root, '.github', 'ISSUE_TEMPLATE'), /\.(md|ya?ml)$/),
];
for (const staleGuidePath of removedGuides) {
for (const guidancePath of practicalGuidanceFiles) {
if (read(path.relative(root, guidancePath)).includes(staleGuidePath)) {
failures.push(
`${path.relative(root, guidancePath)} references deleted guide: ${staleGuidePath}`
);
}
}
}
const markdownFiles = [
path.join(root, 'README.md'),
path.join(root, 'docker', 'README.md'),
path.join(root, 'macos-bar', 'README.md'),
...collectFiles(path.join(root, 'docs'), /\.mdx?$/),
];
const linkPattern = /!?\[[^\]]*]\(([^)]+)\)/g;
for (const markdownPath of markdownFiles) {
const source = fs.readFileSync(markdownPath, 'utf8');
for (const match of source.matchAll(linkPattern)) {
let target = match[1].trim().replace(/^<|>$/g, '');
if (!target || target.startsWith('#') || /^(https?:|mailto:|tel:)/i.test(target)) {
continue;
}
target = target.split('#', 1)[0].split('?', 1)[0];
try {
target = decodeURIComponent(target);
} catch {
failures.push(`${path.relative(root, markdownPath)} has invalid link encoding: ${match[1]}`);
continue;
}
const resolved = path.resolve(path.dirname(markdownPath), target);
if (!fs.existsSync(resolved)) {
failures.push(`${path.relative(root, markdownPath)} has missing relative link: ${match[1]}`);
}
}
}
if (failures.length > 0) {
console.error('[X] Documentation freshness checks failed:');
for (const failure of failures) {
console.error(` ${failure}`);
}
process.exit(1);
}
console.log('[OK] Documentation pointers, retained contracts, and relative links are current.');
+20 -1
View File
@@ -1,6 +1,6 @@
import { describe, expect, test } from 'bun:test';
const { collectSyncCallSites } = require('../../../scripts/hardening-inventory.js');
const { collectSyncCallSites, isTestFile } = require('../../../scripts/hardening-inventory.js');
describe('hardening-inventory sync call scanning', () => {
test('ignores sync-call names inside regex literals after else', () => {
@@ -34,3 +34,22 @@ describe('hardening-inventory sync call scanning', () => {
expect(result.calls).toEqual(['readFileSync']);
});
});
describe('hardening-inventory runtime file classification', () => {
test.each([
'src/cliproxy/__tests__/routing.test.ts',
'src/commands/fixtures/help-output.ts',
'src/auth/profile.spec.ts',
'tests/unit/commands/profile.test.ts',
])('excludes %s from runtime hotpaths', (filePath) => {
expect(isTestFile(filePath)).toBe(true);
});
test.each([
'src/cliproxy/routing/retry-settings.ts',
'src/commands/help-command.ts',
'src/utils/browser/mcp-installer.ts',
])('keeps %s eligible for runtime hotpaths', (filePath) => {
expect(isTestFile(filePath)).toBe(false);
});
});
@@ -8,10 +8,31 @@ const {
classifyThrows,
countConsoleErrors,
hasCreateLogger,
isTestFile,
countLoc,
collectMaintainabilityMetrics,
} = require('../../../scripts/maintainability-metrics.js');
describe('maintainability-metrics runtime file classification', () => {
test.each([
'src/cliproxy/__tests__/routing.test.ts',
'src/commands/fixtures/help-output.ts',
'src/auth/profile.spec.ts',
'tests/unit/commands/profile.test.ts',
'src\\auth\\__mocks__\\profile.ts',
])('excludes %s from runtime metrics', (filePath) => {
expect(isTestFile(filePath)).toBe(true);
});
test.each([
'src/cliproxy/routing/retry-settings.ts',
'src/commands/help-command.ts',
'src/utils/browser/mcp-installer.ts',
])('keeps %s in runtime metrics', (filePath) => {
expect(isTestFile(filePath)).toBe(false);
});
});
describe('maintainability-metrics.classifyThrows', () => {
test('counts plain Error, typed, and other throws; ignores re-throws', () => {
const source = [
@@ -94,14 +115,18 @@ describe('maintainability-metrics.collectMaintainabilityMetrics (fixtures tree)'
fs.mkdirSync(path.join(root, 'src', 'commands'), { recursive: true });
fs.writeFileSync(
path.join(root, 'src', 'commands', 'b.ts'),
['console.error(\'cli print\');', "throw new Error('plain');"].join('\n')
["console.error('cli print');", "throw new Error('plain');"].join('\n')
);
// src/cliproxy/quota/q.ts: createLogger present, 1 plain throw, 0 console.error
fs.mkdirSync(path.join(root, 'src', 'cliproxy', 'quota'), { recursive: true });
fs.writeFileSync(
path.join(root, 'src', 'cliproxy', 'quota', 'q.ts'),
["import { createLogger } from '../../../services/logging';", 'const logger = createLogger();', "throw new Error('quota plain');"].join('\n')
[
"import { createLogger } from '../../../services/logging';",
'const logger = createLogger();',
"throw new Error('quota plain');",
].join('\n')
);
// non-source file is ignored