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 CHANGED
@@ -7,6 +7,21 @@ Distribute Claude Code requests across N subscriptions to multiply your throughp
7
7
  [![npm](https://img.shields.io/npm/v/ai-cc-router)](https://www.npmjs.com/package/ai-cc-router)
8
8
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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**
@@ -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
- console.log(` ${chalk.gray(formatTime(log.ts))} ${log.accountId.padEnd(18)} ` +
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
  }
@@ -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));
@@ -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 = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai-cc-router",
3
- "version": "0.2.3",
3
+ "version": "0.2.4",
4
4
  "description": "Round-robin proxy for Claude Max OAuth tokens — use multiple Claude Max accounts with Claude Code",
5
5
  "type": "module",
6
6
  "bin": {