@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 +12 -0
- package/README.md +32 -3
- package/dist/cli/cmd-accounts.js +91 -13
- package/dist/cli/cmd-cli-targets.js +177 -0
- package/dist/cli/cmd-client.js +3 -2
- package/dist/cli/cmd-configure.js +18 -4
- package/dist/cli/cmd-status.js +4 -1
- package/dist/cli/cmd-stop.js +18 -0
- package/dist/cli/index.js +10 -3
- package/dist/config/manager.js +19 -3
- package/dist/config/paths.js +1 -0
- package/dist/protocol/model-ref.js +1 -1
- package/dist/providers/openai/account-state.js +4 -1
- package/dist/providers/openai/usage.js +14 -1
- package/dist/providers/xai/account-record.js +42 -0
- package/dist/providers/xai/device-oauth.js +116 -0
- package/dist/providers/xai/import-auth.js +70 -0
- package/dist/providers/xai/overview.js +279 -0
- package/dist/providers/xai/subscription-fetch.js +56 -0
- package/dist/proxy/allowance.js +142 -0
- package/dist/proxy/logger.js +4 -3
- package/dist/proxy/models-server.js +30 -2
- package/dist/proxy/openai-ingress.js +7 -1
- package/dist/proxy/openai-routing.js +12 -7
- package/dist/proxy/server.js +110 -6
- package/dist/ui/Dashboard.js +383 -124
- package/dist/ui/accountsApi.js +3 -1
- package/dist/utils/cli-routing.js +80 -0
- package/dist/utils/codex-config.js +337 -15
- package/package.json +3 -2
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
|
|
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
|
-
|
|
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` |
|
|
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:
|
package/dist/cli/cmd-accounts.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
:
|
|
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
|
|
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
|
-
|
|
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
|
+
}
|
package/dist/cli/cmd-client.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
|
|
50
|
+
if (result.tokenEnvKey)
|
|
51
|
+
console.log(chalk.gray(` env_key = ${result.tokenEnvKey}`));
|
|
38
52
|
return;
|
|
39
53
|
}
|
|
40
54
|
if (target === "models") {
|
package/dist/cli/cmd-status.js
CHANGED
|
@@ -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
|
-
|
|
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}`));
|
package/dist/cli/cmd-stop.js
CHANGED
|
@@ -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
|
|
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
|
|
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);
|