Closes#1218.
When the user invokes `ccs <profile> agents` (and other Claude subcommands
like `mcp`, `doctor`, `plugin`, ...), CCS was unconditionally injecting
session-only Claude flags (`--append-system-prompt`, `--disallowedTools`,
`--settings`, official-channels plugin specs) and forwarding the
`DISABLE_TELEMETRY=1` env var from profile settings. Each of those
either errored out the subcommand (`error: unknown option
'--append-system-prompt'`) or silently flipped Claude into
non-interactive list mode, so the new `claude agents` agent view never
opened under CCS.
Add a small `claude-subcommand-detector` that recognizes the documented
Claude subcommand set after skipping known value-taking flags, then have
the three steering-prompt injectors (websearch, image-analysis, browser),
the cliproxy and settings launchers, and the official-channels plan
short-circuit when a subcommand invocation is detected. Also strip
`DISABLE_TELEMETRY` from the spawned env only for subcommand
invocations — upstream Claude Code uses that var as a kill switch for
the subcommand TUIs, and the user's telemetry preference still applies
to every normal interactive session.
Verified end-to-end against `ccs glm agents` and `ccs ck agents`: the
agent view opens correctly, no `unknown option` error, and all 1938
existing tests pass.
The dashboard Add Account flow hits POST /api/cliproxy/auth/:provider/start-url
which fetches the OAuth consent URL directly from the CLIProxy Plus
management API. With cliproxy.backend=plus and unset
CLIPROXY_{GEMINI,ANTIGRAVITY}_OAUTH_CLIENT_ID/SECRET, Plus returns a URL
with empty client_id= and Google rejects with 400 invalid_request. PR
#1131 only guarded the CLI /start path; /start-url was unguarded.
- Pre-fetch guard via getPlusOAuthCredentialError -> 400
plus_oauth_credentials_missing before contacting the Plus binary.
- Post-fetch guard via getPlusAuthUrlCredentialError -> 502
plus_oauth_url_missing_client_id if Plus still emits a URL without
client_id (logged with query string redacted).
- New integration test (cliproxy-auth-routes-oauth-guard.test.ts) covers
gemini/agy guard firing, non-table providers passing through, and both
error body contracts. 15 cases pass.
Closes#1208
Refactor getGeminiPlusOAuthCredentialError / getGeminiAuthUrlCredentialError
behind a provider-table-driven helper (PLUS_OAUTH_ENV_BY_PROVIDER) and add
getPlusOAuthCredentialError / getPlusAuthUrlCredentialError exports covering
both gemini and agy. Existing gemini-named exports preserved as aliases so
PR #1131 test surface remains unchanged. New AGY-parity test cases mirror
the original gemini diagnostics. CLI call sites in triggerOAuth /
handlePasteCallbackMode stay gemini-only; dashboard handler in the
follow-up commits will use the generalized exports for both providers.
Refs #1208
* fix(dispatcher): preserve anthropic auth env for settings profiles on non-proxy path
API profiles whose `ANTHROPIC_BASE_URL` is classified as `'anthropic'`
(anthropic.com, paths containing `/anthropic`, ollama.com) skip the
local OpenAI-compat proxy. The non-proxy launch path stripped
`ANTHROPIC_BASE_URL` / `ANTHROPIC_AUTH_TOKEN` / `ANTHROPIC_API_KEY`
from the subprocess env without re-injecting them, so Claude Code
launched with no routing/auth in `process.env` and failed with
`Not logged in - Please run /login`. The `--settings` env block does
not satisfy Claude Code's auth check.
Pre-existing for `anthropic.com` and `/anthropic` profiles. Newly
broken in v7.77.0 for Ollama Cloud profiles - PR #1175 reclassified
`ollama.com` from `generic-chat-completion-api` to `anthropic`,
moving it from the proxy path onto this broken non-proxy path.
Fix by extending `stripAnthropicRoutingEnv` with an optional
`preserveFrom` parameter. Routing keys present in `preserveFrom`
survive the strip (with values from `preserveFrom`). Settings-type
profiles pass their own `settings.env` as the preserve source so
routing they explicitly supplied is kept while routing leaked from
the parent shell or `global.env` is dropped.
Wired into both call sites:
- `headless-executor.ts` (the `-p` headless executor)
- `settings-flow.ts` (the interactive flow, which then calls
`execClaude` - whose own `stripAnthropicRoutingEnv` pass on the
merged env now also takes `envVars` as the preserve source so the
caller-supplied routing is not stripped a second time before spawn)
Native Anthropic / Bedrock / Vertex profiles are unaffected (they
don't put routing keys in `settings.env`). The OpenAI-compat proxy
path is unaffected because `buildOpenAICompatProxyEnv` overrides
`BASE_URL` / `AUTH_TOKEN` with localhost values and explicitly deletes
`API_KEY` after this strip.
Replaces the Apr 21 defensive double-strip test (which was the
mechanism causing this regression on the interactive path) with a
test asserting the new contract: caller-supplied routing in `envVars`
survives, parent-process routing is still stripped.
* test: harden anthropic settings env preservation coverage
* fix: preserve explicit blank anthropic routing env
---------
Co-authored-by: Tam Nhu Tran <kaitran.ntt@gmail.com>
Resolve the dispatcher and dashboard conflicts from origin/dev while preserving the configured local CLIProxy port behavior. Also harden lifecycle port fallback and wait for the actual stopped port during binary installs.
The normalizers extraction in #1168 introduced a circular module-load
chain that crashed the built CLI:
ccs.js -> errors -> services/logging -> log-config -> config-loader-facade
-> unified-config-loader -> loader/normalizers -> channels/official-channels-runtime
-> utils/claude-detector -> utils/shell-executor -> utils/websearch-manager
-> utils/websearch/hook-env -> utils/websearch/trace
-> services/logging (mid-load - createLogger undefined) -> CRASH
normalizers only needed three pure helpers from official-channels-runtime
(isOfficialChannelId, normalizeOfficialChannelIds, resolveLegacyDiscord
Selection) but pulled in the whole file's claude-detector / shell-
executor / websearch chain.
Fix: extract those three helpers + OFFICIAL_CHANNEL_IDS into a leaf
module src/channels/official-channels-ids.ts with no runtime deps.
Update normalizers.ts and config-getters.ts to import from the leaf.
official-channels-runtime.ts re-exports from the leaf for callers that
still want the bundled API.
Also revert the over-eager facade-import migration in
src/utils/config-manager.ts (it was importing from config-loader-facade
which re-exports from itself, creating a direct cycle).
Verified: dist/ccs.js boots cleanly (--version returns); test:all
1828/1828 pass; typecheck/lint/format clean.
Refs #1135
All call sites that spawn or probe CLIProxyApiPlus now read
cliproxy_server.local.port from config via resolveLifecyclePort()
instead of using the hardcoded CLIPROXY_DEFAULT_PORT constant.
- Move resolveLifecyclePort helper to src/cliproxy/config/port-manager.ts
- Fix 7 call sites: ccs.ts, config-command.ts, copilot-executor.ts,
lifecycle.ts, binary-manager.ts, cliproxy-stats-routes.ts,
cliproxy-local-proxy.ts
- Remove duplicate resolveLocalCliproxyPort helper
- Cache port resolution in /proxy-status handler to avoid repeated I/O
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Issue #1161. Sweeps 127 files to import from
src/config/config-loader-facade.ts instead of unified-config-loader or
utils/config-manager directly.
WRITE callers (32 files): replaced raw saveUnifiedConfig /
mutateUnifiedConfig / updateUnifiedConfig calls with the facade's
cache-coherent wrappers saveConfig / mutateConfig / updateConfig. This
fixes a latent stale-cache window where direct writes through the
underlying loader bypassed the facade's memoization.
READ callers (95 files): mechanical import-path migration only —
function names unchanged because the facade re-exports them. No
behavior change.
Also updated:
- tests/unit/utils/browser/browser-setup.test.ts (DI interface rename)
- src/management/checks/image-analysis-check.ts (dynamic import rename)
- src/web-server/health-service.ts (dynamic require rename)
- src/ccs.ts (path prefix fix from sweep script)
After sweep: zero raw write callers remain outside src/config/. Direct
imports of config-manager remain only for symbols not in the facade
(getConfigPath, getCcsDirSource, etc). Behavior unchanged; full suite
passes 1824/1824.
Out of scope: switching loadOrCreateUnifiedConfig() callers to
getCachedConfig() — needs per-callsite cache-safety analysis. Tracked
as follow-up.
Refs #1161
Phases 4-6 of #1164. Final extractions and orchestrator cleanup:
- src/config/loader/defaults-merger.ts (323 LOC):
mergeWithDefaults — pure transform that fills defaults across browser,
websearch, dashboard auth, image analysis, logging, cursor, continuity,
official channels.
- src/config/loader/config-getters.ts (339 LOC):
getWebSearchConfig, getGlobalEnvConfig, getContinuityInheritanceMap,
getCliproxySafetyConfig, getThinkingConfig, getOfficialChannelsConfig,
isDashboardAuthEnabled, getDashboardAuthConfig, getBrowserConfig,
getImageAnalysisConfig, getLoggingConfig, getCursorConfig,
GeminiWebSearchInfo. Lazy require('../unified-config-loader') call-time
resolution preserves spy-based test compatibility while breaking the
config-getters -> orchestrator cycle.
- unified-config-loader.ts: rewritten as 268-LOC orchestrator. Hosts core
load/save/mutate logic + re-exports of all moved symbols for backwards
compat with codebase imports.
Forward-reference callbacks in io-locks.ts kept as-is — defaults-merger
and normalizers don't import from io-locks (no cycle), but simplifying
io-locks's internal callback API risks lock-semantics regression.
Deferred per KISS.
unified-config-loader.ts: 1508 -> 268 LOC (-1240, -82%). Behavior
unchanged; full suite passes 1824/1824.
Refs #1164
Phases 5+6 of #1165. Final big extraction: collapses the 6-branch
profileInfo.type switch in main() into a single dispatchProfile call.
- src/dispatcher/dispatcher-context.ts (40 LOC): ProfileDispatchContext type
- src/dispatcher/flows/cliproxy-flow.ts (211 LOC)
- src/dispatcher/flows/copilot-flow.ts (66 LOC)
- src/dispatcher/flows/cursor-flow.ts (54 LOC)
- src/dispatcher/flows/settings-flow.ts (400 LOC)
- src/dispatcher/flows/settings-image-analysis-prep.ts (126 LOC):
split out per plan to keep settings-flow control flow whole
- src/dispatcher/flows/account-flow.ts (63 LOC)
- src/dispatcher/flows/default-flow.ts (136 LOC)
- src/dispatcher/target-executor.ts (+38 LOC): dispatchProfile switch
settings-flow stays at 400 LOC — flow control (3 pre-flight blocks +
env construction + 2 dispatch paths) is maximally cohesive. Further
splitting would fragment dispatch logic per plan note.
process.exit parity preserved exactly (cliproxy non-claude path uses
process.exitCode=1; return; others use process.exit(1)).
ccs.ts: 1040 -> 170 LOC (-870). Full reduction across phases 1-6:
1775 -> 170 (-1605, -90%). main() body now ~100 LOC of awaited phase
calls. Behavior unchanged; full suite passes 1824/1824.
Refs #1165
Phase 4 of #1165. Extracts the largest remaining concern in main():
profile detection, target resolution, Claude CLI detection, adapter
lookup, compatibility preflight, binary detection, droid prune, and
per-target arg normalization.
- src/dispatcher/profile-resolver.ts (358 LOC):
resolveProfileAndTarget + ProfileResolutionContext / ResolvedProfile
types. Includes droid + codex per-target arg normalization (cohesive
with profile resolution; splitting would create incomplete extraction).
- 7 unit tests covering default, gemini, codex, unknown profile, droid,
and compatibility preflight runs.
Compatibility preflight duplication between this phase and the settings
flow (Phase E) is preserved exactly per plan risk note 5 — do not dedupe
in this PR. Module is 358 LOC (over 300 ideal) — kept whole because the
profile resolution + per-target normalization form a single dependency
chain.
ccs.ts: 1262 -> 1040 LOC (-222). Behavior unchanged; full suite passes
1824/1824.
Refs #1165