ai-cc-router 0.2.3 → 0.2.4
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/README.md +15 -0
- package/dist/cli/cmd-client.js +5 -1
- package/dist/proxy/server.js +6 -0
- package/dist/ui/Dashboard.js +19 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -7,6 +7,21 @@ Distribute Claude Code requests across N subscriptions to multiply your throughp
|
|
|
7
7
|
[](https://www.npmjs.com/package/ai-cc-router)
|
|
8
8
|
[](LICENSE)
|
|
9
9
|
|
|
10
|
+
### Features
|
|
11
|
+
|
|
12
|
+
- **Round-robin token rotation** — distribute requests across 2-20 Claude Max accounts automatically
|
|
13
|
+
- **Transparent proxy** — Claude Code works normally; streaming, thinking, tool use, prompt caching all pass through
|
|
14
|
+
- **Automatic token refresh** — OAuth tokens are refreshed before they expire, saved atomically to disk
|
|
15
|
+
- **Rate limit awareness** — detects 429/529 responses and coolsdown accounts; picks the least-loaded one
|
|
16
|
+
- **Client mode** — connect to a remote CC-Router from any machine with one command (`cc-router client connect <url>`)
|
|
17
|
+
- **Claude Desktop support** — route Cowork / Agent-mode traffic through CC-Router via mitmproxy interception (macOS, Windows, Linux)
|
|
18
|
+
- **Guided setup wizard** — interactive `cc-router setup` extracts tokens from Keychain or credentials file, configures everything
|
|
19
|
+
- **Live dashboard** — real-time terminal UI showing account health, request counts, token usage, recent activity
|
|
20
|
+
- **Proxy authentication** — optional Bearer / x-api-key secret for internet-exposed deployments
|
|
21
|
+
- **Auto-update** — patch/minor releases install automatically (opt-out available)
|
|
22
|
+
- **Multiple deployment modes** — foreground, PM2 daemon, system service, Docker Compose (with LiteLLM)
|
|
23
|
+
- **Cross-platform** — macOS, Linux, Windows; Node.js 20+
|
|
24
|
+
|
|
10
25
|
---
|
|
11
26
|
|
|
12
27
|
> **Warning**
|
package/dist/cli/cmd-client.js
CHANGED
|
@@ -204,7 +204,11 @@ export function registerClient(program) {
|
|
|
204
204
|
const status = log.statusCode ?? 0;
|
|
205
205
|
const statusColor = status >= 500 || status === 0 ? chalk.red : status >= 400 ? chalk.yellow : chalk.green;
|
|
206
206
|
const duration = log.durationMs ? ` ${chalk.gray(log.durationMs + "ms")}` : "";
|
|
207
|
-
|
|
207
|
+
const src = log.source === "cli" ? chalk.blue("cli")
|
|
208
|
+
: log.source === "desktop" ? chalk.magenta("dsk")
|
|
209
|
+
: log.source === "api" ? chalk.gray("api")
|
|
210
|
+
: chalk.gray(" ");
|
|
211
|
+
console.log(` ${chalk.gray(formatTime(log.ts))} ${src} ${log.accountId.padEnd(18)} ` +
|
|
208
212
|
`${(log.method ?? "?").padEnd(5)} ${(log.path ?? "?").padEnd(22)} ` +
|
|
209
213
|
`${statusColor(String(status))}${duration}`);
|
|
210
214
|
}
|
package/dist/proxy/server.js
CHANGED
|
@@ -317,6 +317,11 @@ export async function startServer(opts = {}) {
|
|
|
317
317
|
}
|
|
318
318
|
req._ccAccount = account;
|
|
319
319
|
req._startTime = Date.now();
|
|
320
|
+
const source = req.headers["x-claude-code-session-id"]
|
|
321
|
+
? "cli"
|
|
322
|
+
: req.headers["x-api-key"]
|
|
323
|
+
? "desktop"
|
|
324
|
+
: "api";
|
|
320
325
|
req._pendingLog = {
|
|
321
326
|
ts: Date.now(),
|
|
322
327
|
accountId: account.id,
|
|
@@ -324,6 +329,7 @@ export async function startServer(opts = {}) {
|
|
|
324
329
|
type: "route",
|
|
325
330
|
method: req.method,
|
|
326
331
|
path: req.path,
|
|
332
|
+
source,
|
|
327
333
|
};
|
|
328
334
|
stats.totalRequests++;
|
|
329
335
|
logRoute(account.id, account.requestCount, Math.round((account.tokens.expiresAt - Date.now()) / 60_000));
|
package/dist/ui/Dashboard.js
CHANGED
|
@@ -146,6 +146,13 @@ function LogRow({ log, selected }) {
|
|
|
146
146
|
: "gray";
|
|
147
147
|
const bg = selected ? "white" : undefined;
|
|
148
148
|
const fg = (c) => selected ? "black" : c;
|
|
149
|
+
const sourceLabel = log.source === "cli" ? "cli"
|
|
150
|
+
: log.source === "desktop" ? "dsk"
|
|
151
|
+
: log.source === "api" ? "api"
|
|
152
|
+
: " ";
|
|
153
|
+
const sourceColor = log.source === "cli" ? "blue"
|
|
154
|
+
: log.source === "desktop" ? "magenta"
|
|
155
|
+
: "gray";
|
|
149
156
|
// Per-request token stats
|
|
150
157
|
const inputTok = (log.cacheReadTokens ?? 0) + (log.cacheCreationTokens ?? 0) + (log.inputTokens ?? 0);
|
|
151
158
|
const outputTok = log.outputTokens ?? 0;
|
|
@@ -154,7 +161,7 @@ function LogRow({ log, selected }) {
|
|
|
154
161
|
: cacheHitPct >= 70 ? "green"
|
|
155
162
|
: cacheHitPct >= 30 ? "yellow"
|
|
156
163
|
: "red";
|
|
157
|
-
return (_jsxs(Box, { children: [_jsxs(Text, { backgroundColor: bg, color: fg(undefined), children: [selected ? "▶" : " ", " ", time, " "] }), _jsxs(Text, { backgroundColor: bg, color: fg(typeColor), children: [typeIcon, " "] }), _jsx(Text, { backgroundColor: bg, color: fg("cyan"), children: log.accountId.slice(0, 22).padEnd(22) }), log.method && log.path
|
|
164
|
+
return (_jsxs(Box, { children: [_jsxs(Text, { backgroundColor: bg, color: fg(undefined), children: [selected ? "▶" : " ", " ", time, " "] }), _jsxs(Text, { backgroundColor: bg, color: fg(typeColor), children: [typeIcon, " "] }), _jsxs(Text, { backgroundColor: bg, color: fg(sourceColor), children: [sourceLabel, " "] }), _jsx(Text, { backgroundColor: bg, color: fg("cyan"), children: log.accountId.slice(0, 22).padEnd(22) }), log.method && log.path
|
|
158
165
|
? _jsxs(Text, { backgroundColor: bg, color: fg("white"), children: [" ", log.method, " ", log.path.padEnd(14)] })
|
|
159
166
|
: _jsxs(Text, { backgroundColor: bg, color: fg(typeColor), children: [" ", log.type.padEnd(9)] }), log.statusCode !== undefined && (_jsxs(Text, { backgroundColor: bg, color: fg(statusColor), children: [" ", log.statusCode] })), log.durationMs !== undefined && (_jsxs(Text, { backgroundColor: bg, color: fg("gray"), children: [" ", log.durationMs, "ms"] })), cacheHitPct !== null && (_jsxs(Text, { backgroundColor: bg, color: fg(cacheColor), children: [" \u2191", cacheHitPct, "%"] })), (inputTok > 0 || outputTok > 0) && (_jsxs(Text, { backgroundColor: bg, color: fg("gray"), children: [" ", fmtTok(inputTok), "\u2191 ", fmtTok(outputTok), "\u2193"] })), log.details && (_jsxs(Text, { backgroundColor: bg, color: fg("gray"), children: [" ", log.details] }))] }));
|
|
160
167
|
}
|
|
@@ -174,7 +181,7 @@ function DetailPanel({ log }) {
|
|
|
174
181
|
: log.statusCode >= 500 ? "red"
|
|
175
182
|
: log.statusCode >= 400 ? "yellow"
|
|
176
183
|
: "green";
|
|
177
|
-
return (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: "gray", paddingX: 1, children: [_jsx(Text, { bold: true, color: isError ? "red" : "cyan", children: " DETAILS " }), _jsxs(Box, { marginTop: 1, flexDirection: "column", gap: 0, children: [_jsxs(Box, { gap: 2, children: [_jsx(Field, { label: "Time", value: time }), _jsx(Field, { label: "Account", value: log.accountId })] }), _jsxs(Box, { gap: 2, children: [_jsx(Field, { label: "Method", value: log.method ?? "—" }), _jsx(Field, { label: "Path", value: log.path ?? "—" })] }), _jsxs(Box, { gap: 2, children: [_jsx(FieldColored, { label: "Status", value: statusLabel, color: statusColor }), _jsx(Field, { label: "Duration", value: log.durationMs !== undefined ? `${log.durationMs}ms` : "—" }), _jsx(Field, { label: "Type", value: log.type })] }), log.details && (_jsx(Box, { children: _jsx(Field, { label: "Details", value: log.details }) })), log.cacheReadTokens !== undefined && (_jsx(Box, { gap: 2, children: _jsx(CacheBreakdown, { read: log.cacheReadTokens, created: log.cacheCreationTokens ?? 0, input: log.inputTokens ?? 0, output: log.outputTokens ?? 0 }) }))] })] }));
|
|
184
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: "gray", paddingX: 1, children: [_jsx(Text, { bold: true, color: isError ? "red" : "cyan", children: " DETAILS " }), _jsxs(Box, { marginTop: 1, flexDirection: "column", gap: 0, children: [_jsxs(Box, { gap: 2, children: [_jsx(Field, { label: "Time", value: time }), _jsx(Field, { label: "Account", value: log.accountId })] }), _jsxs(Box, { gap: 2, children: [_jsx(Field, { label: "Method", value: log.method ?? "—" }), _jsx(Field, { label: "Path", value: log.path ?? "—" })] }), _jsxs(Box, { gap: 2, children: [_jsx(FieldColored, { label: "Status", value: statusLabel, color: statusColor }), _jsx(Field, { label: "Duration", value: log.durationMs !== undefined ? `${log.durationMs}ms` : "—" }), _jsx(Field, { label: "Type", value: log.type }), _jsx(Field, { label: "Source", value: sourceFullLabel(log.source) })] }), log.details && (_jsx(Box, { children: _jsx(Field, { label: "Details", value: log.details }) })), log.cacheReadTokens !== undefined && (_jsx(Box, { gap: 2, children: _jsx(CacheBreakdown, { read: log.cacheReadTokens, created: log.cacheCreationTokens ?? 0, input: log.inputTokens ?? 0, output: log.outputTokens ?? 0 }) }))] })] }));
|
|
178
185
|
}
|
|
179
186
|
function Field({ label, value }) {
|
|
180
187
|
return (_jsxs(Box, { children: [_jsxs(Text, { color: "gray", children: [label, ": "] }), _jsx(Text, { color: "white", children: value })] }));
|
|
@@ -215,6 +222,16 @@ function fmtTok(n) {
|
|
|
215
222
|
return `${(n / 1_000).toFixed(1)}k`;
|
|
216
223
|
return String(n);
|
|
217
224
|
}
|
|
225
|
+
// ─── Source label ─────────────────────────────────────────────────────────────
|
|
226
|
+
function sourceFullLabel(source) {
|
|
227
|
+
if (source === "cli")
|
|
228
|
+
return "Claude Code";
|
|
229
|
+
if (source === "desktop")
|
|
230
|
+
return "Claude Desktop";
|
|
231
|
+
if (source === "api")
|
|
232
|
+
return "API";
|
|
233
|
+
return "—";
|
|
234
|
+
}
|
|
218
235
|
// ─── HTTP status text ─────────────────────────────────────────────────────────
|
|
219
236
|
function httpStatusText(code) {
|
|
220
237
|
const map = {
|