ai-cc-router 0.2.7 → 0.3.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/README.md CHANGED
@@ -3,10 +3,11 @@
3
3
  **Round-robin proxy for multiple Claude Max accounts.**
4
4
  Distribute Claude Code requests across N subscriptions to multiply your throughput.
5
5
 
6
- [![CI](https://github.com/VictorMinemu/CC-Router/actions/workflows/ci.yml/badge.svg)](https://github.com/VictorMinemu/CC-Router/actions/workflows/ci.yml)
7
6
  [![npm](https://img.shields.io/npm/v/ai-cc-router)](https://www.npmjs.com/package/ai-cc-router)
8
7
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
9
8
 
9
+ ![CC-Router Dashboard](assets/dashboard.png)
10
+
10
11
  ### Features
11
12
 
12
13
  - **Round-robin token rotation** — distribute requests across 2-20 Claude Max accounts automatically
@@ -19,7 +20,7 @@ Distribute Claude Code requests across N subscriptions to multiply your throughp
19
20
  - **Live dashboard** — real-time terminal UI showing account health, request counts, token usage, recent activity
20
21
  - **Proxy authentication** — optional Bearer / x-api-key secret for internet-exposed deployments
21
22
  - **Auto-update** — patch/minor releases install automatically (opt-out available)
22
- - **Multiple deployment modes** — foreground, PM2 daemon, system service, Docker Compose (with LiteLLM)
23
+ - **Multiple deployment modes** — background daemon, native OS auto-start (launchd/systemd), foreground, Docker Compose
23
24
  - **Cross-platform** — macOS, Linux, Windows; Node.js 20+
24
25
 
25
26
  ---
@@ -114,17 +115,10 @@ Run cc-router on a machine everyone on the team can reach — a home server, a V
114
115
  ```bash
115
116
  npm install -g ai-cc-router
116
117
  cc-router setup # configure the 3 shared accounts
117
- cc-router service install # auto-start on boot
118
+ cc-router start # first run asks: background/boot/server mode — choose "server mode"
118
119
  ```
119
120
 
120
- By default cc-router binds to `localhost`. To accept connections from other machines, set the `HOST` environment variable:
121
-
122
- ```bash
123
- # Listen on all interfaces (team LAN or VPS)
124
- HOST=0.0.0.0 cc-router start
125
-
126
- # Or configure it permanently in the service
127
- ```
121
+ When you enable server mode during `cc-router start`, the proxy automatically binds to all interfaces (`0.0.0.0`) and prints instructions for connecting clients.
128
122
 
129
123
  #### On each developer's machine
130
124
 
@@ -204,9 +198,9 @@ claude
204
198
 
205
199
  That's it. Claude Code will route through the proxy without any further changes.
206
200
 
207
- **Optional:** install as a system service so it starts automatically on boot:
201
+ On first run, `cc-router start` asks how you want to run (background/foreground, auto-start on boot, server mode) and remembers your choice. Next time, it just starts. To change preferences later:
208
202
  ```bash
209
- cc-router service install
203
+ cc-router start --reconfigure
210
204
  ```
211
205
 
212
206
  ---
@@ -274,26 +268,27 @@ cc-router setup
274
268
  cc-router setup Interactive wizard: extract tokens + configure Claude Code
275
269
  cc-router setup --add Add another account to an existing configuration
276
270
 
277
- cc-router start Start proxy on localhost:3456 (foreground)
278
- cc-router start --daemon Start in background via PM2
271
+ cc-router start Start proxy (asks preferences on first run, then remembers)
272
+ cc-router start --foreground Run in the foreground (stays in terminal)
273
+ cc-router start --reconfigure Re-ask run preferences (background/service/server mode)
279
274
  cc-router start --litellm Start with LiteLLM in Docker (advanced mode)
280
275
 
281
- cc-router stop Stop proxy + restore Claude Code to normal auth
276
+ cc-router stop Stop proxy (offers to remove auto-start / config)
282
277
  cc-router stop --keep-config Stop proxy only (keep settings.json)
283
- cc-router revert Restore Claude Code to normal authentication
278
+ cc-router stop --full Stop + remove auto-start + revert Claude Code (no prompts)
279
+ cc-router revert Same as stop --full
284
280
 
285
281
  cc-router status Live dashboard (updates every 2s, press q to quit)
286
282
  cc-router status --json Print current stats as JSON and exit
287
283
 
284
+ cc-router logs View proxy logs (background mode)
285
+ cc-router logs -f Follow log output in real time
286
+ cc-router logs --lines 100 Show last 100 lines
287
+
288
288
  cc-router accounts list List configured accounts (live stats if proxy is running)
289
289
  cc-router accounts add Add an account interactively
290
290
  cc-router accounts remove <id> Remove an account
291
291
 
292
- cc-router service install Register cc-router to start on system boot (PM2)
293
- cc-router service uninstall Remove from system startup
294
- cc-router service status Show PM2 service status
295
- cc-router service logs Tail proxy logs from PM2
296
-
297
292
  cc-router configure (Re)write ~/.claude/settings.json
298
293
  cc-router configure --show Show current Claude Code proxy settings
299
294
  cc-router configure --remove Remove cc-router settings (same as revert without stopping)
@@ -323,7 +318,7 @@ cc-router docker restart [service] Restart a service
323
318
  Claude Code → cc-router:3456 → api.anthropic.com
324
319
  ```
325
320
 
326
- Best for personal use. No Docker required.
321
+ Best for personal use. No Docker required. Runs in the background by default, auto-starts on boot if you choose.
327
322
 
328
323
  ```bash
329
324
  cc-router start
@@ -477,7 +472,9 @@ To stop using cc-router and go back to normal Claude Code authentication:
477
472
  cc-router revert
478
473
  ```
479
474
 
480
- This stops the proxy process and removes cc-router's settings from `~/.claude/settings.json`. Claude Code will use its own authentication on the next launch.
475
+ This stops the proxy process, removes the auto-start service (if installed), and removes cc-router's settings from `~/.claude/settings.json`. Claude Code will use its own authentication on the next launch.
476
+
477
+ For a gentler approach, `cc-router stop` interactively asks what you want to clean up.
481
478
 
482
479
  ---
483
480
 
@@ -530,7 +527,7 @@ CC-Router sends a handful of anonymous lifecycle events to [Aptabase](https://ap
530
527
  | `app_started` | First proxy start after install | `first_run: true` |
531
528
  | `setup_completed` | Setup wizard finishes successfully | `account_count` |
532
529
  | `proxy_started` | Each `cc-router start` | `account_count`, `mode` |
533
- | `proxy_heartbeat` | Every 6h while the proxy is running | `uptime_hours`, `account_count` |
530
+ | `proxy_heartbeat` | Every hour while the proxy is running | `uptime_minutes`, `account_count` |
534
531
  | `telemetry_disabled` | When you run `cc-router telemetry off` | — |
535
532
 
536
533
  Plus anonymous system props with every event: `appVersion`, `osName` (macOS/Linux/Windows), `osVersion`, `locale`, `engineVersion` (Node), and an anonymous `installId` (random UUID generated on first run, stored in `~/.cc-router/telemetry.json`).
@@ -0,0 +1,85 @@
1
+ import { existsSync, openSync, readSync, closeSync, statSync, watch } from "fs";
2
+ import chalk from "chalk";
3
+ import { LOG_PATH } from "../config/paths.js";
4
+ export function registerLogs(program) {
5
+ program
6
+ .command("logs")
7
+ .description("View proxy logs")
8
+ .option("--lines <n>", "Number of lines to show (default: 50)", "50")
9
+ .option("-f, --follow", "Follow log output in real time")
10
+ .action(async (opts) => {
11
+ if (!existsSync(LOG_PATH)) {
12
+ console.log(chalk.yellow("No log file found."));
13
+ console.log(chalk.gray(` Expected: ${LOG_PATH}`));
14
+ console.log(chalk.gray(" Logs are created when running in background mode."));
15
+ console.log(chalk.gray(" Foreground mode prints directly to this terminal.\n"));
16
+ return;
17
+ }
18
+ const numLines = parseInt(opts.lines, 10) || 50;
19
+ if (opts.follow) {
20
+ await tailFollow(numLines);
21
+ }
22
+ else {
23
+ tailStatic(numLines);
24
+ }
25
+ });
26
+ }
27
+ /** Print last N lines of the log file using byte-based seeking. */
28
+ function tailStatic(n) {
29
+ if (!existsSync(LOG_PATH)) {
30
+ console.log(chalk.gray("No log file found. Is the daemon running?"));
31
+ return;
32
+ }
33
+ const stats = statSync(LOG_PATH);
34
+ if (stats.size === 0) {
35
+ console.log(chalk.gray("Log file is empty."));
36
+ return;
37
+ }
38
+ // Read last chunk (up to 64KB should be enough for most tail operations)
39
+ const CHUNK = Math.min(stats.size, 64 * 1024);
40
+ const buf = Buffer.alloc(CHUNK);
41
+ const fd = openSync(LOG_PATH, "r");
42
+ readSync(fd, buf, 0, CHUNK, stats.size - CHUNK);
43
+ closeSync(fd);
44
+ const text = buf.toString("utf-8");
45
+ const lines = text.split("\n");
46
+ // Remove first potentially partial line if we didn't read from start
47
+ if (CHUNK < stats.size)
48
+ lines.shift();
49
+ const tail = lines.slice(-n).join("\n");
50
+ if (tail)
51
+ process.stdout.write(tail + "\n");
52
+ }
53
+ /** Stream log file and print new lines as they appear. */
54
+ async function tailFollow(initialLines) {
55
+ // Show initial tail
56
+ tailStatic(initialLines);
57
+ console.log(chalk.gray("\n── Following log output (Ctrl+C to stop) ──\n"));
58
+ if (!existsSync(LOG_PATH)) {
59
+ console.log(chalk.gray("Waiting for log file..."));
60
+ }
61
+ let bytePosition = existsSync(LOG_PATH) ? statSync(LOG_PATH).size : 0;
62
+ const watcher = watch(LOG_PATH, () => {
63
+ try {
64
+ const currentSize = statSync(LOG_PATH).size;
65
+ if (currentSize > bytePosition) {
66
+ const buf = Buffer.alloc(currentSize - bytePosition);
67
+ const fd = openSync(LOG_PATH, "r");
68
+ readSync(fd, buf, 0, buf.length, bytePosition);
69
+ closeSync(fd);
70
+ process.stdout.write(buf.toString("utf-8"));
71
+ bytePosition = currentSize;
72
+ }
73
+ else if (currentSize < bytePosition) {
74
+ // File was truncated/rotated
75
+ bytePosition = 0;
76
+ }
77
+ }
78
+ catch { /* file may have been removed */ }
79
+ });
80
+ process.on("SIGINT", () => {
81
+ watcher.close();
82
+ process.exit(0);
83
+ });
84
+ await new Promise(() => { }); // never resolves
85
+ }
@@ -1,6 +1,4 @@
1
1
  import { select, input, confirm, password } from "@inquirer/prompts";
2
- import { execFile, spawn } from "child_process";
3
- import { promisify } from "util";
4
2
  import chalk from "chalk";
5
3
  import { detectPlatform, isMacos } from "../utils/platform.js";
6
4
  import { extractFromKeychain, extractFromCredentialsFile, formatExpiry, redactToken, } from "../utils/token-extractor.js";
@@ -14,7 +12,6 @@ import { existsSync } from "fs";
14
12
  import { checkMitmproxyInstalled, isCaCertInstalled, generateCaCert, installCaCert, writeAddonScript, getNetworkExtensionStatus, openNetworkExtensionSettings, } from "../interceptor/mitmproxy-manager.js";
15
13
  import { printDesktopSupportExplainer, printNetworkExtensionInstructions } from "./cmd-client.js";
16
14
  import { trackEvent } from "../utils/telemetry.js";
17
- const execFileAsync = promisify(execFile);
18
15
  // ─── Public registration ──────────────────────────────────────────────────────
19
16
  export function registerSetup(program) {
20
17
  program
@@ -110,7 +107,7 @@ export async function setupSingleAccount(index) {
110
107
  };
111
108
  }
112
109
  // ─── Full wizard ──────────────────────────────────────────────────────────────
113
- async function runSetupWizard({ addMode }) {
110
+ export async function runSetupWizard({ addMode }) {
114
111
  const platform = detectPlatform();
115
112
  const hasExisting = accountsFileExists();
116
113
  const existingClient = readConfig().client;
@@ -305,190 +302,14 @@ async function runPostSetupFlow(accountCount) {
305
302
  console.log(chalk.gray(` ANTHROPIC_BASE_URL = ${proxyHost}`));
306
303
  console.log(chalk.gray(` ANTHROPIC_AUTH_TOKEN = proxy-managed`));
307
304
  }
308
- // ── Auto-update preference ──────────────────────────────────────────────
309
- const existingCfg = readConfig();
310
- if (existingCfg.autoUpdate === undefined) {
311
- const enableAutoUpdate = await confirm({
312
- message: "Enable auto-updates? (proxy will install patch/minor releases automatically)",
313
- default: true,
314
- });
315
- writeConfig({ ...existingCfg, autoUpdate: enableAutoUpdate });
316
- console.log(chalk.gray(` Auto-update: ${enableAutoUpdate ? chalk.green("enabled") : chalk.gray("disabled")}`));
317
- console.log(chalk.gray(" Change later with: cc-router configure --enable-auto-update / --disable-auto-update"));
318
- }
319
- // 2. Only ask about starting the proxy if it's local
320
- console.log(chalk.bold(`\n${"━".repeat(40)}\n Start the proxy\n${"━".repeat(40)}\n`));
321
- // Check if it's already running
322
- const alreadyRunning = await isProxyRunning();
323
- if (alreadyRunning) {
324
- console.log(chalk.green(` ✓ Proxy is already running on http://localhost:${PROXY_PORT}`));
325
- printDone(accountCount);
326
- return;
327
- }
328
- const startChoice = await select({
329
- message: "How do you want to run the proxy?",
330
- choices: [
331
- { name: "Install as system service (auto-start on boot — recommended)", value: "service" },
332
- { name: "Start in background now (current session only, via PM2)", value: "daemon" },
333
- { name: "Start in foreground now (this terminal, Ctrl+C to stop)", value: "foreground" },
334
- { name: "I'll start it manually later", value: "skip" },
335
- ],
336
- });
337
- if (startChoice === "service") {
338
- await installService();
339
- }
340
- else if (startChoice === "daemon") {
341
- await startDaemon();
342
- }
343
- else if (startChoice === "foreground") {
344
- printDone(accountCount);
345
- console.log(chalk.cyan("\nStarting proxy in foreground...\n"));
346
- // Launch start as child — it blocks until Ctrl+C
347
- await startForeground();
348
- return; // startForeground never returns normally
349
- }
350
305
  printDone(accountCount);
351
306
  }
352
- // ─── Proxy launch helpers ─────────────────────────────────────────────────────
353
- async function isProxyRunning() {
354
- try {
355
- const res = await fetch(`http://localhost:${PROXY_PORT}/cc-router/health`, {
356
- signal: AbortSignal.timeout(800),
357
- });
358
- return res.ok;
359
- }
360
- catch {
361
- return false;
362
- }
363
- }
364
- async function installService() {
365
- console.log(chalk.cyan("\n Installing as system service via PM2..."));
366
- try {
367
- // Ensure PM2 is installed
368
- await execFileAsync("pm2", ["--version"]).catch(async () => {
369
- console.log(chalk.gray(" Installing PM2..."));
370
- await execFileAsync("npm", ["install", "-g", "pm2"]);
371
- });
372
- const { fileURLToPath } = await import("url");
373
- const { dirname, join } = await import("path");
374
- const __dirname = dirname(fileURLToPath(import.meta.url));
375
- const cliEntry = join(__dirname, "index.js");
376
- // Start in PM2
377
- await execFileAsync("pm2", [
378
- "start", cliEntry,
379
- "--name", "cc-router",
380
- "--interpreter", process.execPath,
381
- "--max-memory-restart", "500M",
382
- "--", "start",
383
- ]).catch(async (err) => {
384
- // Already registered — restart instead
385
- if (err.message?.includes("already")) {
386
- await execFileAsync("pm2", ["restart", "cc-router"]);
387
- }
388
- else {
389
- throw err;
390
- }
391
- });
392
- await execFileAsync("pm2", ["save"]);
393
- console.log(chalk.green(" ✓ cc-router registered in PM2 and saved"));
394
- // Generate startup hook
395
- try {
396
- const { stdout, stderr } = await execFileAsync("pm2", ["startup"]);
397
- const combined = stdout + stderr;
398
- const sudoMatch = combined.match(/sudo\s+\S.+/);
399
- if (sudoMatch) {
400
- console.log(chalk.yellow("\n Run this command to complete auto-start setup:"));
401
- console.log(chalk.white(` ${sudoMatch[0]}`));
402
- console.log(chalk.gray(" Then run: pm2 save"));
403
- }
404
- else {
405
- console.log(chalk.green(" ✓ Auto-start on boot configured"));
406
- }
407
- }
408
- catch (err) {
409
- const combined = (err.stdout ?? "") +
410
- (err.stderr ?? "");
411
- const sudoMatch = combined.match(/sudo\s+\S.+/);
412
- if (sudoMatch) {
413
- console.log(chalk.yellow("\n Run this command to complete auto-start setup:"));
414
- console.log(chalk.white(` ${sudoMatch[0]}`));
415
- console.log(chalk.gray(" Then run: pm2 save"));
416
- }
417
- }
418
- // Wait a moment and confirm it started
419
- await new Promise(r => setTimeout(r, 1500));
420
- const running = await isProxyRunning();
421
- if (running) {
422
- console.log(chalk.green(` ✓ Proxy is running on http://localhost:${PROXY_PORT}`));
423
- }
424
- else {
425
- console.log(chalk.yellow(" ⚠ Service registered but proxy not yet responding — it may still be starting."));
426
- console.log(chalk.gray(" Check: cc-router service status"));
427
- }
428
- }
429
- catch (err) {
430
- console.log(chalk.red(` ✗ Service install failed: ${err.message}`));
431
- console.log(chalk.gray(" Try manually: cc-router service install"));
432
- }
433
- }
434
- async function startDaemon() {
435
- console.log(chalk.cyan("\n Starting in background via PM2..."));
436
- try {
437
- const { fileURLToPath } = await import("url");
438
- const { dirname, join } = await import("path");
439
- const __dirname = dirname(fileURLToPath(import.meta.url));
440
- const cliEntry = join(__dirname, "index.js");
441
- await execFileAsync("pm2", [
442
- "start", cliEntry,
443
- "--name", "cc-router",
444
- "--interpreter", process.execPath,
445
- "--max-memory-restart", "500M",
446
- "--", "start",
447
- ]).catch(async (err) => {
448
- if (err.message?.includes("already")) {
449
- await execFileAsync("pm2", ["restart", "cc-router"]);
450
- }
451
- else {
452
- throw err;
453
- }
454
- });
455
- await new Promise(r => setTimeout(r, 1500));
456
- const running = await isProxyRunning();
457
- if (running) {
458
- console.log(chalk.green(` ✓ Proxy running in background on http://localhost:${PROXY_PORT}`));
459
- console.log(chalk.gray(" Logs: pm2 logs cc-router | Stop: cc-router stop"));
460
- }
461
- else {
462
- console.log(chalk.yellow(" ⚠ PM2 registered but proxy not yet responding."));
463
- console.log(chalk.gray(" Check: pm2 logs cc-router"));
464
- }
465
- }
466
- catch (err) {
467
- console.log(chalk.red(` ✗ Failed to start via PM2: ${err.message}`));
468
- console.log(chalk.gray(" PM2 not installed? Run: npm install -g pm2"));
469
- console.log(chalk.gray(" Or start manually: cc-router start"));
470
- }
471
- }
472
- async function startForeground() {
473
- const { fileURLToPath } = await import("url");
474
- const { dirname, join } = await import("path");
475
- const __dirname = dirname(fileURLToPath(import.meta.url));
476
- const cliEntry = join(__dirname, "index.js");
477
- const child = spawn(process.execPath, [cliEntry, "start"], { stdio: "inherit" });
478
- await new Promise((resolve) => {
479
- child.on("close", resolve);
480
- child.on("error", (err) => {
481
- console.error(chalk.red(` ✗ ${err.message}`));
482
- resolve();
483
- });
484
- });
485
- }
486
307
  // ─── Done banner ──────────────────────────────────────────────────────────────
487
308
  function printDone(accountCount) {
488
309
  console.log(chalk.bold(`\n${"━".repeat(40)}\n All done — ${accountCount} account(s) ready\n${"━".repeat(40)}\n`));
489
- console.log(` Dashboard: ${chalk.cyan("cc-router status")}`);
310
+ console.log(` Start the proxy: ${chalk.cyan("cc-router start")}`);
490
311
  console.log(` Add more accounts: ${chalk.cyan("cc-router setup --add")}`);
491
- console.log(` Stop & revert: ${chalk.cyan("cc-router revert")}\n`);
312
+ console.log(` Dashboard: ${chalk.cyan("cc-router status")}\n`);
492
313
  }
493
314
  // ─── Manual token input ───────────────────────────────────────────────────────
494
315
  async function promptManualTokens() {