@timo972/cc-router 0.11.0 → 0.12.0-rc.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -10,6 +10,18 @@ This project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
10
10
 
11
11
  ### Added
12
12
 
13
+ - `cc-router cli claude` and `cc-router cli codex` toggle Claude Code / Codex
14
+ CLI routing without stopping the proxy: `start`, `stop`, `resume` (same as
15
+ start), and `status`. `cc-router cli` shows both. Short aliases
16
+ `cc-router claude` / `cc-router codex` still work. `configure --remove` now
17
+ also works with `configure codex --remove`. `stop --full` / `revert` and
18
+ `client disconnect` strip the Codex managed block as well as Claude Code
19
+ settings.
20
+ - Dashboard `[c]` / `[x]` toggle Claude Code / Codex CLI routing without
21
+ stopping the proxy (`[c]` still sets the Claude model default while MODELS
22
+ is focused).
23
+ - Status dashboard `rst` column shows Codex banked usage-limit reset count
24
+ (`0` = none available); Claude cells are `—`.
13
25
  - Automatic upstream failover and retry on both providers. A 429 or 5xx
14
26
  received before any response byte is relayed no longer passes straight
15
27
  through to the client: the router applies the existing cooldown/affinity
package/README.md CHANGED
@@ -220,7 +220,17 @@ cc-router configure codex (Re)write ~/.codex/config.toml for Codex CLI
220
220
  cc-router configure codex --model openai/gpt-5-codex
221
221
  cc-router configure models --claude-model claude-sonnet-4-6 --openai-model gpt-5-codex
222
222
  cc-router configure --show Show current Claude Code proxy settings
223
- cc-router configure --remove Remove cc-router settings (same as revert without stopping)
223
+ cc-router configure --remove Remove cc-router settings from Claude Code (proxy stays up)
224
+ cc-router configure codex --remove Remove the Codex managed block (proxy stays up)
225
+
226
+ cc-router cli Show whether Claude Code and Codex are routing through the proxy
227
+ cc-router cli claude start Point Claude Code at the running proxy
228
+ cc-router cli claude stop Restore Claude Code to native Anthropic auth (proxy stays up)
229
+ cc-router cli claude resume Same as cli claude start
230
+ cc-router cli codex start Point Codex CLI at the running proxy
231
+ cc-router cli codex stop Restore Codex CLI to native OpenAI auth (proxy stays up)
232
+ cc-router cli codex resume Same as cli codex start
233
+ cc-router claude … / cc-router codex … Hidden shortcuts for the same commands
224
234
 
225
235
  cc-router client connect <url> Connect Claude Code to a remote CC-Router
226
236
  cc-router client connect --desktop Also configure Claude Desktop interception
@@ -502,6 +512,24 @@ mitmproxy's local mode is *process-scoped* — it only intercepts traffic from t
502
512
 
503
513
  ---
504
514
 
515
+ ## Toggle Claude Code or Codex while the proxy stays up
516
+
517
+ `cc-router start` / `stop` control the proxy process. To send only one CLI back to native auth (or point it at the proxy again) without tearing the router down:
518
+
519
+ ```bash
520
+ cc-router cli # Claude Code + Codex routing state
521
+ cc-router cli claude stop # Claude Code → native Anthropic auth
522
+ cc-router cli claude resume # Claude Code → running proxy (alias of start)
523
+ cc-router cli codex start # Codex CLI → running proxy
524
+ cc-router cli codex stop # Codex CLI → native OpenAI auth
525
+ ```
526
+
527
+ `cc-router claude …` and `cc-router codex …` are shortcuts for the same commands. `cli` is the grouping — not `provider`, which already means the Anthropic/OpenAI account pool. `cc-router client` remains remote client-mode (connect this machine to another CC-Router).
528
+
529
+ These rewrite `~/.claude/settings.json` or the managed block in `~/.codex/config.toml`. The proxy keeps listening. Restart any already-running Claude Code or Codex process so it picks up the new config. From `cc-router status`, `[c]` / `[x]` do the same toggles.
530
+
531
+ ---
532
+
505
533
  ## Reverting to normal Claude Code
506
534
 
507
535
  To stop using cc-router and go back to normal Claude Code authentication:
@@ -529,7 +557,7 @@ cc-router status
529
557
  Claude 2/2 healthy OpenAI 1/1 healthy · cross-route ready
530
558
  endpoints /v1/messages /v1/responses /v1/models /cc-router/accounts
531
559
  routing claude=claude-sonnet-4-6 aliases[sonnet] openai=gpt-5-codex aliases[codex]
532
- models [m] list/select change [c] Claude [o] OpenAI
560
+ cli Claude on Codex off [c]/[x] · models [m] then [c]/[o] defaults
533
561
 
534
562
  MODELS [m/r] refresh [↑/↓] select [c] Claude default [o] OpenAI default
535
563
  current claude=claude-sonnet-4-6 openai=gpt-5-codex
@@ -561,7 +589,8 @@ The dashboard is also a control surface. In local mode it controls the local pro
561
589
  | `w` / `s` | Change selected Claude account weekly/session cap |
562
590
  | `d` | Delete selected Claude account |
563
591
  | `m` / `r` | Load or refresh discovered provider models |
564
- | `c` | Set selected `anthropic/*` model as Claude default |
592
+ | `c` | Toggle Claude Code routing (or set Claude model default when MODELS is focused) |
593
+ | `x` | Toggle Codex CLI routing (proxy stays up) |
565
594
  | `o` | Set selected `openai/*` model as OpenAI default |
566
595
 
567
596
  List and change models without waiting for a package update:
@@ -1,15 +1,17 @@
1
1
  import chalk from "chalk";
2
- import { loadAccounts, loadOpenAIAccounts, accountsFileExists, upsertAccountRecord, removeAccountRecordById, renameAccountRecordById, readConfig, serialize } from "../config/manager.js";
2
+ import { loadAccounts, loadOpenAIAccounts, loadXaiAccounts, accountsFileExists, upsertAccountRecord, removeAccountRecordById, renameAccountRecordById, readConfig, serialize } from "../config/manager.js";
3
3
  import { saveAccounts } from "../proxy/token-refresher.js";
4
4
  import { formatExpiry, redactToken } from "../utils/token-extractor.js";
5
5
  import { PROXY_PORT } from "../config/paths.js";
6
6
  import { createOpenAIAccountRecord } from "../providers/openai/account-record.js";
7
7
  import { loginOpenAIWithDeviceCode } from "../providers/openai/device-oauth.js";
8
+ import { importGrokCliAuth } from "../providers/xai/import-auth.js";
9
+ import { loginXaiWithDeviceCode } from "../providers/xai/device-oauth.js";
8
10
  import { isValidAccountId } from "../proxy/account-rename.js";
9
11
  export function registerAccounts(program) {
10
12
  const accounts = program
11
13
  .command("accounts")
12
- .description("Manage Claude Max accounts in the token pool");
14
+ .description("Manage Claude Max, ChatGPT/Codex, and Grok accounts");
13
15
  // ── accounts list ────────────────────────────────────────────────────────
14
16
  accounts
15
17
  .command("list")
@@ -24,12 +26,13 @@ export function registerAccounts(program) {
24
26
  }
25
27
  const stored = loadAccounts();
26
28
  const openAIStored = loadOpenAIAccounts();
27
- if (stored.length === 0 && openAIStored.length === 0) {
29
+ const xaiStored = loadXaiAccounts();
30
+ if (stored.length === 0 && openAIStored.length === 0 && xaiStored.length === 0) {
28
31
  console.log(chalk.yellow("accounts.json is empty. Run: cc-router setup"));
29
32
  return;
30
33
  }
31
34
  if (opts.json) {
32
- console.log(JSON.stringify(liveStats ?? buildStoredAccountsJson(stored, openAIStored), null, 2));
35
+ console.log(JSON.stringify(liveStats ?? buildStoredAccountsJson(stored, openAIStored, xaiStored), null, 2));
33
36
  return;
34
37
  }
35
38
  // The count has to describe the rows printed below it. Taking it from
@@ -37,13 +40,15 @@ export function registerAccounts(program) {
37
40
  // header claimed four accounts above six rows.
38
41
  console.log(chalk.bold(liveStats
39
42
  ? `\n Accounts (${liveStats.length} in the running proxy)\n`
40
- : `\n Accounts (${stored.length + openAIStored.length} configured)\n`));
43
+ : `\n Accounts (${stored.length + openAIStored.length + xaiStored.length} configured)\n`));
41
44
  if (liveStats) {
42
45
  console.log(chalk.green(" ● Proxy is running — showing live stats\n"));
43
46
  for (const s of liveStats) {
44
47
  const provider = s.provider === "openai_subscription"
45
48
  ? chalk.cyan("openai".padEnd(9))
46
- : chalk.gray("claude".padEnd(9));
49
+ : s.provider === "xai_subscription"
50
+ ? chalk.magenta("grok".padEnd(9))
51
+ : chalk.gray("claude".padEnd(9));
47
52
  const status = s.healthy
48
53
  ? chalk.green("✓ healthy")
49
54
  : chalk.red("✗ unhealthy");
@@ -63,7 +68,7 @@ export function registerAccounts(program) {
63
68
  // here is how an account can be routing live while its refresh token
64
69
  // exists nowhere on disk — one restart from having to authenticate it
65
70
  // again.
66
- const { unpersisted, unloaded } = accountDrift(liveStats.map(s => s.id), [...stored.map(a => a.id), ...openAIStored.map(a => a.id)]);
71
+ const { unpersisted, unloaded } = accountDrift(liveStats.map(s => s.id), [...stored.map(a => a.id), ...openAIStored.map(a => a.id), ...xaiStored.map(a => a.id)]);
67
72
  if (unpersisted.length > 0) {
68
73
  console.log(chalk.red(`\n ⚠ Not in accounts.json: ${unpersisted.join(", ")}`));
69
74
  console.log(chalk.gray(" These live only in the running proxy. Restarting it loses their\n"
@@ -96,6 +101,15 @@ export function registerAccounts(program) {
96
101
  ` ${redactToken(a.accessToken).padEnd(26)}` +
97
102
  ` expires: ${exp}`);
98
103
  }
104
+ for (const a of xaiStored) {
105
+ const exp = a.expiresAt > Date.now()
106
+ ? chalk.yellow(formatExpiry(a.expiresAt))
107
+ : chalk.red("EXPIRED");
108
+ console.log(` ${chalk.bold(a.id.padEnd(24))}` +
109
+ ` ${chalk.magenta("grok".padEnd(10))}` +
110
+ ` ${redactToken(a.accessToken).padEnd(26)}` +
111
+ ` expires: ${exp}`);
112
+ }
99
113
  }
100
114
  console.log();
101
115
  });
@@ -192,6 +206,59 @@ export function registerAccounts(program) {
192
206
  console.log(chalk.green(`\n✓ OpenAI account "${record.id}" saved via device login.\n`));
193
207
  printAddOutcome(mode);
194
208
  });
209
+ // ── accounts add-grok ────────────────────────────────────────────────────
210
+ accounts
211
+ .command("add-grok")
212
+ .description("Import the Grok CLI login from ~/.grok/auth.json")
213
+ .action(async () => {
214
+ const { input } = await import("@inquirer/prompts");
215
+ let imported;
216
+ try {
217
+ imported = importGrokCliAuth();
218
+ }
219
+ catch (err) {
220
+ const message = err instanceof Error ? err.message : String(err);
221
+ console.log(chalk.red(`\n✗ ${message}\n`));
222
+ console.log(chalk.gray(" Or sign in here: cc-router accounts login-grok\n"));
223
+ process.exit(1);
224
+ }
225
+ const id = await input({
226
+ message: "Grok account ID:",
227
+ default: imported.id,
228
+ validate: (v) => /^[a-zA-Z0-9_-]+$/.test(v) || "Only letters, numbers, _ and - allowed",
229
+ });
230
+ const record = { ...imported, id };
231
+ upsertAccountRecord(record);
232
+ console.log(chalk.green(`\n✓ Grok account "${record.id}" imported from ~/.grok.\n`));
233
+ printAddOutcome("stored");
234
+ });
235
+ // ── accounts login-grok ──────────────────────────────────────────────────
236
+ accounts
237
+ .command("login-grok")
238
+ .description("Sign in to a Grok / xAI account with device code")
239
+ .action(async () => {
240
+ const { input } = await import("@inquirer/prompts");
241
+ const accountId = await input({
242
+ message: "Grok account ID:",
243
+ default: loadXaiAccounts().length === 0 ? "grok" : `grok-${loadXaiAccounts().length + 1}`,
244
+ validate: (v) => /^[a-zA-Z0-9_-]+$/.test(v) || "Only letters, numbers, _ and - allowed",
245
+ });
246
+ console.log(chalk.cyan("\nGrok device login"));
247
+ console.log(chalk.gray("Approve the login in your browser. No local callback server is used.\n"));
248
+ const record = await loginXaiWithDeviceCode({
249
+ accountId,
250
+ onDeviceCode: (code) => {
251
+ console.log(chalk.bold("1. Open this URL:"));
252
+ console.log(` ${chalk.cyan(code.verificationUrl)}`);
253
+ console.log(chalk.bold("2. Enter this code if the page does not fill it in:"));
254
+ console.log(` ${chalk.cyan(code.userCode)}\n`);
255
+ console.log(chalk.gray("Waiting for authorization..."));
256
+ },
257
+ });
258
+ upsertAccountRecord(record);
259
+ console.log(chalk.green(`\n✓ Grok account "${record.id}" saved via device login.\n`));
260
+ printAddOutcome("stored");
261
+ });
195
262
  // ── accounts remove ───────────────────────────────────────────────────────
196
263
  accounts
197
264
  .command("remove <id>")
@@ -203,7 +270,8 @@ export function registerAccounts(program) {
203
270
  }
204
271
  const anthropicAccounts = loadAccounts();
205
272
  const openAIAccounts = loadOpenAIAccounts();
206
- const { ids: existingIds, openAIIds } = mergeAccountInventory(anthropicAccounts.map(a => a.id), openAIAccounts.map(a => a.id), await fetchLiveStats());
273
+ const xaiAccounts = loadXaiAccounts();
274
+ const { ids: existingIds, openAIIds } = mergeAccountInventory(anthropicAccounts.map(a => a.id), openAIAccounts.map(a => a.id), await fetchLiveStats(), xaiAccounts.map(a => a.id));
207
275
  if (!existingIds.includes(id)) {
208
276
  console.log(chalk.red(`✗ Account "${id}" not found.`));
209
277
  console.log(chalk.gray(` Available: ${existingIds.join(", ")}`));
@@ -227,7 +295,7 @@ export function registerAccounts(program) {
227
295
  console.log(chalk.red(`✗ Could not remove "${id}": ${message}`));
228
296
  process.exit(1);
229
297
  }
230
- const remaining = loadAccounts().length + loadOpenAIAccounts().length;
298
+ const remaining = loadAccounts().length + loadOpenAIAccounts().length + loadXaiAccounts().length;
231
299
  const providerLabel = isOpenAI ? "OpenAI account" : "Account";
232
300
  console.log(chalk.green(`✓ Removed ${providerLabel} "${id}". ${remaining} account(s) remaining.`));
233
301
  if (remaining === 0) {
@@ -247,7 +315,7 @@ export function registerAccounts(program) {
247
315
  console.log(chalk.gray(" 1-64 characters: alphanumeric start, then letters, digits, dots, underscores, or dashes."));
248
316
  process.exit(1);
249
317
  }
250
- const { ids: existingIds } = mergeAccountInventory(loadAccounts().map(a => a.id), loadOpenAIAccounts().map(a => a.id), await fetchLiveStats());
318
+ const { ids: existingIds } = mergeAccountInventory(loadAccounts().map(a => a.id), loadOpenAIAccounts().map(a => a.id), await fetchLiveStats(), loadXaiAccounts().map(a => a.id));
251
319
  if (!existingIds.includes(id)) {
252
320
  console.log(chalk.red(`✗ Account "${id}" not found.`));
253
321
  console.log(chalk.gray(` Available: ${existingIds.join(", ")}`));
@@ -292,7 +360,7 @@ function printAddOutcome(mode) {
292
360
  * (see `removeAccountRuntimeAware`), so validating an id against disk alone
293
361
  * rejected accounts that existed and were perfectly removable.
294
362
  */
295
- export function mergeAccountInventory(storedAnthropicIds, storedOpenAIIds, live) {
363
+ export function mergeAccountInventory(storedAnthropicIds, storedOpenAIIds, live, storedXaiIds = []) {
296
364
  const openAIIds = new Set(storedOpenAIIds);
297
365
  for (const account of live ?? []) {
298
366
  if (account.provider === "openai_subscription")
@@ -302,6 +370,7 @@ export function mergeAccountInventory(storedAnthropicIds, storedOpenAIIds, live)
302
370
  ids: [...new Set([
303
371
  ...storedAnthropicIds,
304
372
  ...storedOpenAIIds,
373
+ ...storedXaiIds,
305
374
  ...(live ?? []).map(account => account.id),
306
375
  ])],
307
376
  openAIIds,
@@ -323,7 +392,7 @@ export function accountDrift(liveIds, storedIds) {
323
392
  unloaded: storedIds.filter(id => !live.has(id)),
324
393
  };
325
394
  }
326
- export function buildStoredAccountsJson(anthropicAccounts, openAIAccounts) {
395
+ export function buildStoredAccountsJson(anthropicAccounts, openAIAccounts, xaiAccounts = []) {
327
396
  return [
328
397
  ...anthropicAccounts.map(a => ({
329
398
  id: a.id,
@@ -338,6 +407,12 @@ export function buildStoredAccountsJson(anthropicAccounts, openAIAccounts) {
338
407
  enabled: a.enabled !== false,
339
408
  expiresAt: a.expiresAt,
340
409
  })),
410
+ ...xaiAccounts.map(a => ({
411
+ id: a.id,
412
+ provider: "xai_subscription",
413
+ enabled: a.enabled !== false,
414
+ expiresAt: a.expiresAt,
415
+ })),
341
416
  ];
342
417
  }
343
418
  /** Return false only when no running proxy can be reached; HTTP errors remain authoritative. */
@@ -498,7 +573,10 @@ async function fetchLiveStats() {
498
573
  if (!res.ok)
499
574
  return null;
500
575
  const data = await res.json();
501
- return data.accounts;
576
+ if (!Array.isArray(data.accounts))
577
+ return null;
578
+ const { mergeGrokIntoHealth } = await import("../providers/xai/overview.js");
579
+ return mergeGrokIntoHealth(data).accounts;
502
580
  }
503
581
  catch {
504
582
  return null;
@@ -0,0 +1,177 @@
1
+ import chalk from "chalk";
2
+ import { readConfig } from "../config/manager.js";
3
+ import { PROXY_PORT } from "../config/paths.js";
4
+ import { isProxyRunning } from "../daemon/pid.js";
5
+ import { readClaudeRouting, readCodexRouting, setClaudeRouting, setCodexRouting, } from "../utils/cli-routing.js";
6
+ function configuredPort() {
7
+ return readConfig().runPreferences?.port ?? PROXY_PORT;
8
+ }
9
+ function parsePort(opts) {
10
+ return opts.port ? parseInt(opts.port, 10) : configuredPort();
11
+ }
12
+ async function warnIfProxyDown(port) {
13
+ if (readConfig().client?.remoteUrl)
14
+ return;
15
+ if (await isProxyRunning(port))
16
+ return;
17
+ console.log(chalk.yellow(` Proxy is not running on port ${port}.`));
18
+ console.log(chalk.gray(" Start it with: cc-router start"));
19
+ }
20
+ function printRestartHint(cliName) {
21
+ console.log(chalk.gray(` Restart any running ${cliName} process to pick this up.`));
22
+ }
23
+ function printClaudeStatus(opts = {}) {
24
+ const current = readClaudeRouting();
25
+ if (opts.json) {
26
+ console.log(JSON.stringify(current, null, 2));
27
+ return;
28
+ }
29
+ if (current.enabled) {
30
+ console.log(chalk.green(" Claude Code is routing through cc-router"));
31
+ console.log(` ANTHROPIC_BASE_URL = ${chalk.cyan(current.baseUrl)}`);
32
+ if (current.model)
33
+ console.log(` model = ${current.model}`);
34
+ }
35
+ else {
36
+ console.log(chalk.yellow(" Claude Code is using native Anthropic auth"));
37
+ console.log(chalk.gray(" Enable with: cc-router cli claude start"));
38
+ }
39
+ }
40
+ function printCodexStatus(opts = {}) {
41
+ const current = readCodexRouting();
42
+ if (opts.json) {
43
+ console.log(JSON.stringify(current, null, 2));
44
+ return;
45
+ }
46
+ if (current.enabled) {
47
+ console.log(chalk.green(" Codex CLI is routing through cc-router"));
48
+ if (current.baseUrl)
49
+ console.log(` base_url = ${chalk.cyan(current.baseUrl)}`);
50
+ console.log(` model_provider = ${current.modelProvider ?? "cc-router"}`);
51
+ if (current.model)
52
+ console.log(` model = ${current.model}`);
53
+ }
54
+ else {
55
+ console.log(chalk.yellow(" Codex CLI is using native OpenAI auth"));
56
+ console.log(chalk.gray(" Enable with: cc-router cli codex start"));
57
+ }
58
+ }
59
+ function printBothStatus(opts = {}) {
60
+ if (opts.json) {
61
+ console.log(JSON.stringify({
62
+ claude: readClaudeRouting(),
63
+ codex: readCodexRouting(),
64
+ }, null, 2));
65
+ return;
66
+ }
67
+ printClaudeStatus();
68
+ console.log("");
69
+ printCodexStatus();
70
+ }
71
+ async function enableClaude(opts) {
72
+ const port = parsePort(opts);
73
+ const result = setClaudeRouting(true, { port, model: opts.model });
74
+ console.log(chalk.green(`✓ Claude Code → ${result.baseUrl ?? "proxy"}`));
75
+ if (opts.model)
76
+ console.log(chalk.gray(` model = ${opts.model}`));
77
+ printRestartHint("Claude Code");
78
+ await warnIfProxyDown(port);
79
+ }
80
+ function disableClaude() {
81
+ const result = setClaudeRouting(false);
82
+ if (!result.changed) {
83
+ console.log(chalk.gray(" Claude Code is already using native Anthropic auth."));
84
+ return;
85
+ }
86
+ console.log(chalk.green("✓ Claude Code → native Anthropic auth"));
87
+ printRestartHint("Claude Code");
88
+ }
89
+ async function enableCodex(opts) {
90
+ const port = parsePort(opts);
91
+ const result = setCodexRouting(true, { port, model: opts.model });
92
+ console.log(chalk.green(`✓ Codex CLI → ${result.baseUrl ?? "proxy"}`));
93
+ if (result.path)
94
+ console.log(chalk.gray(` config = ${result.path}`));
95
+ if (opts.model)
96
+ console.log(chalk.gray(` model = ${opts.model}`));
97
+ printRestartHint("Codex");
98
+ await warnIfProxyDown(port);
99
+ }
100
+ function disableCodex() {
101
+ const result = setCodexRouting(false);
102
+ if (!result.changed) {
103
+ console.log(chalk.gray(" Codex CLI is already using native OpenAI auth."));
104
+ return;
105
+ }
106
+ console.log(chalk.green("✓ Codex CLI → native OpenAI auth"));
107
+ if (result.path)
108
+ console.log(chalk.gray(` removed managed block from ${result.path}`));
109
+ printRestartHint("Codex");
110
+ }
111
+ function registerTargetCommands(parent, spec) {
112
+ const startOpts = (command) => command
113
+ .option("--port <port>", "Proxy port to point at")
114
+ .option("--model <model>", "Optional default model for this CLI");
115
+ startOpts(parent
116
+ .command("start")
117
+ .description(spec.startDescription)).action(async (opts) => {
118
+ await spec.enable(opts);
119
+ });
120
+ parent
121
+ .command("stop")
122
+ .description(spec.stopDescription)
123
+ .action(() => {
124
+ spec.disable();
125
+ });
126
+ startOpts(parent
127
+ .command("resume")
128
+ .description(spec.resumeDescription)).action(async (opts) => {
129
+ await spec.enable(opts);
130
+ });
131
+ parent
132
+ .command("status", { isDefault: true })
133
+ .description(spec.statusDescription)
134
+ .option("--json", "Output current routing state as JSON")
135
+ .action((opts) => {
136
+ spec.status(opts);
137
+ });
138
+ }
139
+ const CLAUDE_SPEC = {
140
+ startDescription: "Point Claude Code at the running proxy",
141
+ stopDescription: "Restore Claude Code to native Anthropic auth (proxy keeps running)",
142
+ resumeDescription: "Re-enable Claude Code routing (same as start)",
143
+ statusDescription: "Show whether Claude Code is routing through the proxy",
144
+ enable: enableClaude,
145
+ disable: disableClaude,
146
+ status: printClaudeStatus,
147
+ };
148
+ const CODEX_SPEC = {
149
+ startDescription: "Point Codex CLI at the running proxy",
150
+ stopDescription: "Restore Codex CLI to native OpenAI auth (proxy keeps running)",
151
+ resumeDescription: "Re-enable Codex CLI routing (same as start)",
152
+ statusDescription: "Show whether Codex CLI is routing through the proxy",
153
+ enable: enableCodex,
154
+ disable: disableCodex,
155
+ status: printCodexStatus,
156
+ };
157
+ function registerClaudeAndCodex(parent) {
158
+ registerTargetCommands(parent.command("claude").description("Claude Code routing"), CLAUDE_SPEC);
159
+ registerTargetCommands(parent.command("codex").description("Codex CLI routing"), CODEX_SPEC);
160
+ }
161
+ export function registerCliTargets(program) {
162
+ const cli = program
163
+ .command("cli")
164
+ .description("Enable or disable Claude Code / Codex routing without stopping the proxy");
165
+ cli
166
+ .command("status", { isDefault: true })
167
+ .description("Show routing state for Claude Code and Codex CLI")
168
+ .option("--json", "Output current routing state as JSON")
169
+ .action((opts) => {
170
+ printBothStatus(opts);
171
+ });
172
+ registerClaudeAndCodex(cli);
173
+ // Short aliases for on-the-fly use. Hidden so top-level help stays
174
+ // `start`/`stop` = proxy and `cli` = per-CLI routing.
175
+ registerTargetCommands(program.command("claude", { hidden: true }).description("Alias of cc-router cli claude"), CLAUDE_SPEC);
176
+ registerTargetCommands(program.command("codex", { hidden: true }).description("Alias of cc-router cli codex"), CODEX_SPEC);
177
+ }
@@ -3,7 +3,7 @@ import { existsSync } from "fs";
3
3
  import { input, confirm } from "@inquirer/prompts";
4
4
  import { readConfig, writeConfig } from "../config/manager.js";
5
5
  import { writeClaudeSettings, removeClaudeSettings, readClaudeProxySettings } from "../utils/claude-config.js";
6
- import { codexBaseUrlFromRouterUrl, writeCodexRouterConfigFromClient } from "../utils/codex-config.js";
6
+ import { codexBaseUrlFromRouterUrl, writeCodexRouterConfigFromClient, removeCodexRouterConfig } from "../utils/codex-config.js";
7
7
  import { isMacos, isWindows } from "../utils/platform.js";
8
8
  import { checkMitmproxyInstalled, isCaCertInstalled, generateCaCert, installCaCert, writeAddonScript, startInterceptor, stopInterceptor, isInterceptorRunning, getProcessName, getNetworkExtensionStatus, openNetworkExtensionSettings, installInterceptorService, uninstallInterceptorService, isInterceptorServiceInstalled, removeCaCert, } from "../interceptor/mitmproxy-manager.js";
9
9
  // ─── Helpers ──────────────────────────────────────────────────────────────────
@@ -230,11 +230,12 @@ export function registerClient(program) {
230
230
  }
231
231
  }
232
232
  removeClaudeSettings();
233
+ removeCodexRouterConfig();
233
234
  const current = readConfig();
234
235
  delete current.client;
235
236
  writeConfig(current);
236
237
  console.log(chalk.green("\n✓ Disconnected from CC-Router"));
237
- console.log(chalk.gray(" Claude Code will use direct Anthropic connection on next restart.\n"));
238
+ console.log(chalk.gray(" Claude Code and Codex CLI will use their native auth on next restart.\n"));
238
239
  });
239
240
  // ── cc-router client status ─────────────────────────────────────────────────
240
241
  client
@@ -1,6 +1,6 @@
1
1
  import chalk from "chalk";
2
2
  import { writeClaudeSettings, removeClaudeSettings, readClaudeProxySettings } from "../utils/claude-config.js";
3
- import { writeCodexRouterConfig } from "../utils/codex-config.js";
3
+ import { writeCodexRouterConfig, removeCodexRouterConfig } from "../utils/codex-config.js";
4
4
  import { readConfig, writeConfig, generateProxySecret } from "../config/manager.js";
5
5
  import { PROXY_PORT, CLAUDE_SETTINGS_PATH } from "../config/paths.js";
6
6
  import { buildModelRoutingUpdate } from "../protocol/model-routing-config.js";
@@ -9,7 +9,7 @@ export function registerConfigure(program) {
9
9
  .command("configure")
10
10
  .description("Configure Claude Code or Codex to point to the proxy")
11
11
  .argument("[target]", "Optional target to configure: codex, models")
12
- .option("--remove", "Remove cc-router settings from ~/.claude/settings.json")
12
+ .option("--remove", "Remove cc-router settings from Claude Code or Codex")
13
13
  .option("--port <port>", "Proxy port to configure", String(PROXY_PORT))
14
14
  .option("--model <model>", "Default model for the configured target")
15
15
  .option("--claude-model <model>", "Default Claude/Anthropic model for router aliases")
@@ -22,10 +22,23 @@ export function registerConfigure(program) {
22
22
  .option("--disable-auto-update", "Disable automatic updates for the proxy")
23
23
  .action((target, opts) => {
24
24
  if (target === "codex") {
25
+ if (opts.remove) {
26
+ const result = removeCodexRouterConfig();
27
+ if (result.removed) {
28
+ console.log(chalk.green(`✓ Removed cc-router settings from ${result.path}`));
29
+ console.log(chalk.gray(" Codex CLI will use its native OpenAI auth on next launch."));
30
+ }
31
+ else {
32
+ console.log(chalk.gray(` No cc-router block in ${result.path}`));
33
+ }
34
+ return;
35
+ }
25
36
  const port = parseInt(opts.port, 10);
37
+ // Local routing has no auth in front of the proxy, so no env_key is
38
+ // written — Codex would otherwise hard-abort on startup if that env
39
+ // var isn't set.
26
40
  const result = writeCodexRouterConfig({
27
41
  baseUrl: `http://localhost:${port}/v1`,
28
- tokenEnvKey: "CC_ROUTER_TOKEN",
29
42
  defaultModel: opts.model,
30
43
  });
31
44
  console.log(chalk.green(`✓ Updated ${result.path}`));
@@ -34,7 +47,8 @@ export function registerConfigure(program) {
34
47
  console.log(chalk.gray(` model = ${opts.model}`));
35
48
  console.log(chalk.gray(" model_provider = cc-router"));
36
49
  console.log(chalk.gray(` base_url = http://localhost:${port}/v1`));
37
- console.log(chalk.gray(" env_key = CC_ROUTER_TOKEN"));
50
+ if (result.tokenEnvKey)
51
+ console.log(chalk.gray(` env_key = ${result.tokenEnvKey}`));
38
52
  return;
39
53
  }
40
54
  if (target === "models") {
@@ -52,7 +52,10 @@ async function jsonOutput(port) {
52
52
  console.error(chalk.red(`Proxy returned HTTP ${res.status}`));
53
53
  process.exit(1);
54
54
  }
55
- console.log(JSON.stringify(await res.json(), null, 2));
55
+ const { mergeGrokIntoHealth, loadGrokHealthSnapshotsWithSubscription } = await import("../providers/xai/overview.js");
56
+ const health = await res.json();
57
+ const grokSnapshots = await loadGrokHealthSnapshotsWithSubscription();
58
+ console.log(JSON.stringify(mergeGrokIntoHealth(health, grokSnapshots), null, 2));
56
59
  }
57
60
  catch {
58
61
  console.error(chalk.red(`Cannot connect to proxy at ${healthUrl}`));
@@ -1,6 +1,7 @@
1
1
  import { confirm } from "@inquirer/prompts";
2
2
  import chalk from "chalk";
3
3
  import { removeClaudeSettings, readClaudeProxySettings } from "../utils/claude-config.js";
4
+ import { removeCodexRouterConfig, readCodexRouterConfig } from "../utils/codex-config.js";
4
5
  import { PROXY_PORT } from "../config/paths.js";
5
6
  import { stopDaemon } from "../daemon/launcher.js";
6
7
  import { isProxyRunning } from "../daemon/pid.js";
@@ -86,6 +87,23 @@ async function stopProxy(opts) {
86
87
  anythingDone = true;
87
88
  }
88
89
  }
90
+ // 4. Codex CLI config cleanup
91
+ const codex = readCodexRouterConfig();
92
+ if (codex.configured) {
93
+ let removeCodex = opts.full ?? false;
94
+ if (!opts.full) {
95
+ removeCodex = await confirm({
96
+ message: "Remove proxy settings from Codex CLI? (Codex will use native OpenAI auth)",
97
+ default: false,
98
+ });
99
+ }
100
+ if (removeCodex) {
101
+ removeCodexRouterConfig();
102
+ console.log(chalk.green(`✓ Removed proxy settings from ${codex.path}`));
103
+ console.log(chalk.gray(" Codex CLI will use its native OpenAI authentication on next launch."));
104
+ anythingDone = true;
105
+ }
106
+ }
89
107
  printDone(anythingDone);
90
108
  }
91
109
  function printDone(anythingDone) {
package/dist/cli/index.js CHANGED
@@ -12,12 +12,12 @@ import { registerClient } from "./cmd-client.js";
12
12
  import { registerTelemetry } from "./cmd-telemetry.js";
13
13
  import { registerLogs } from "./cmd-logs.js";
14
14
  import { registerModels } from "./cmd-models.js";
15
+ import { registerCliTargets } from "./cmd-cli-targets.js";
15
16
  import { getCurrentVersion, checkForUpdate, printUpdateBanner } from "../utils/self-update.js";
16
17
  const program = new Command();
17
18
  program
18
19
  .name("cc-router")
19
- .description("Round-robin proxy for Claude Max OAuth tokens.\n" +
20
- "Distributes Claude Code requests across multiple Claude Max accounts.")
20
+ .description("Round-robin proxy for Claude Max and ChatGPT/Codex subscriptions, with a Grok CLI overview.")
21
21
  .version(getCurrentVersion())
22
22
  .addHelpText("after", `
23
23
  Examples:
@@ -29,10 +29,16 @@ Examples:
29
29
  $ cc-router status # Live dashboard with account stats
30
30
  $ cc-router models list # List dynamically discovered provider models
31
31
  $ cc-router logs # View proxy logs (background mode)
32
- $ cc-router accounts list # Show all configured accounts
32
+ $ cc-router accounts list # Show Claude, ChatGPT, and Grok accounts
33
+ $ cc-router accounts add-grok # Import the Grok CLI login
34
+ $ cc-router accounts login-grok# Sign in to Grok with device code
33
35
  $ cc-router revert # Restore Claude Code to normal (remove all proxy config)
34
36
  $ cc-router docker up # Full stack: cc-router + LiteLLM in Docker
35
37
  $ cc-router client connect <url> # Route Claude Code through a remote CC-Router
38
+ $ cc-router cli claude stop # Point Claude Code at native auth (proxy stays up)
39
+ $ cc-router cli claude resume # Point Claude Code back at the running proxy
40
+ $ cc-router cli codex start # Point Codex CLI at the running proxy
41
+ $ cc-router cli # Show Claude Code + Codex routing state
36
42
  `);
37
43
  registerSetup(program);
38
44
  registerStart(program);
@@ -42,6 +48,7 @@ registerStatus(program);
42
48
  registerModels(program);
43
49
  registerAccounts(program);
44
50
  registerConfigure(program);
51
+ registerCliTargets(program);
45
52
  registerDocker(program);
46
53
  registerUpdate(program);
47
54
  registerClient(program);