ai-cc-router 0.2.2 → 0.2.3

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.
@@ -4,7 +4,7 @@ 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
6
  import { isMacos, isWindows } from "../utils/platform.js";
7
- import { checkMitmproxyInstalled, isCaCertInstalled, generateCaCert, installCaCert, writeAddonScript, startInterceptor, stopInterceptor, isInterceptorRunning, getProcessName, } from "../interceptor/mitmproxy-manager.js";
7
+ import { checkMitmproxyInstalled, isCaCertInstalled, generateCaCert, installCaCert, writeAddonScript, startInterceptor, stopInterceptor, isInterceptorRunning, getProcessName, getNetworkExtensionStatus, openNetworkExtensionSettings, } from "../interceptor/mitmproxy-manager.js";
8
8
  // ─── Helpers ──────────────────────────────────────────────────────────────────
9
9
  function isClaudeDesktopInstalled() {
10
10
  if (isMacos()) {
@@ -112,9 +112,15 @@ export function registerClient(program) {
112
112
  writeClaudeSettings(0, url, secret ?? "proxy-managed");
113
113
  console.log(chalk.green("✓ Claude Code configured to route through CC-Router"));
114
114
  console.log(chalk.gray(` ANTHROPIC_BASE_URL → ${url}`));
115
- // 6. Optionally configure Claude Desktop
116
- const wantsDesktop = opts?.desktop ?? (isClaudeDesktopInstalled() &&
117
- await confirm({ message: "Also route Claude Desktop (chat + Cowork) through the proxy?", default: false }));
115
+ // 6. Optionally configure Claude Desktop (Cowork / Agent mode only)
116
+ let wantsDesktop = opts?.desktop ?? false;
117
+ if (!opts?.desktop && isClaudeDesktopInstalled()) {
118
+ printDesktopSupportExplainer();
119
+ wantsDesktop = await confirm({
120
+ message: "Route Claude Desktop's Cowork / Agent-mode traffic through CC-Router?",
121
+ default: false,
122
+ });
123
+ }
118
124
  if (wantsDesktop) {
119
125
  await setupDesktopInterception(url);
120
126
  cfg.client.desktopEnabled = true;
@@ -207,16 +213,34 @@ export function registerClient(program) {
207
213
  console.log(chalk.gray("\n No recent activity on the remote proxy."));
208
214
  }
209
215
  // ── Desktop status ─────────────────────────────────────────────────
210
- console.log(chalk.bold("\n DESKTOP INTERCEPTOR"));
216
+ console.log(chalk.bold("\n DESKTOP INTERCEPTOR (Cowork / Agent mode)"));
211
217
  if (cfg.client.desktopEnabled) {
212
218
  const running = await isInterceptorRunning();
213
- console.log(` ${running ? chalk.green("● running") : chalk.yellow("○ configured but stopped")}`);
214
- if (!running) {
219
+ if (running) {
220
+ console.log(` ${chalk.green("● running")}`);
221
+ }
222
+ else {
223
+ console.log(` ${chalk.yellow("○ configured but stopped")}`);
215
224
  console.log(chalk.gray(" Start with: cc-router client start-desktop"));
216
225
  }
226
+ // Check Network Extension on macOS
227
+ if (isMacos()) {
228
+ const extStatus = await getNetworkExtensionStatus();
229
+ if (extStatus === "waiting") {
230
+ console.log(chalk.red(" ⚠ Network Extension NOT approved — interceptor won't capture traffic!"));
231
+ console.log(chalk.gray(" Fix: System Settings → General → Login Items & Extensions → Network Extensions"));
232
+ }
233
+ else if (extStatus === "not_installed") {
234
+ console.log(chalk.yellow(" ⚠ Network Extension not installed — will be triggered on first start"));
235
+ }
236
+ else if (extStatus === "enabled") {
237
+ console.log(` ${chalk.green("✓")} ${chalk.gray("Network Extension: enabled")}`);
238
+ }
239
+ }
240
+ console.log(chalk.gray(" Scope: /v1/messages + /v1/models (normal chat NOT routed)"));
217
241
  }
218
242
  else {
219
- console.log(` ${chalk.gray("not configured")}`);
243
+ console.log(` ${chalk.gray("not configured — enable with: cc-router client connect --desktop")}`);
220
244
  }
221
245
  console.log();
222
246
  console.log(chalk.gray(" Live dashboard: cc-router status\n"));
@@ -224,16 +248,17 @@ export function registerClient(program) {
224
248
  // ── cc-router client start-desktop ──────────────────────────────────────────
225
249
  client
226
250
  .command("start-desktop")
227
- .description("Start mitmproxy interceptor for Claude Desktop")
251
+ .description("Start mitmproxy interceptor for Claude Desktop (Cowork / Agent mode)")
228
252
  .action(async () => {
229
253
  const cfg = readConfig();
230
254
  if (!cfg.client) {
231
255
  console.error(chalk.red("Not connected. Run: cc-router client connect <url>"));
232
256
  process.exit(1);
233
257
  }
234
- if (!await checkMitmproxyInstalled()) {
235
- console.error(chalk.red("mitmproxy not found. Install it first:"));
236
- console.error(chalk.yellow(isMacos() ? " brew install mitmproxy" : " pip install mitmproxy"));
258
+ if (!(await checkMitmproxyInstalled())) {
259
+ console.error(chalk.red("\n✗ mitmproxy not found. Install it first:"));
260
+ console.error(chalk.cyan(isMacos() ? " brew install mitmproxy" : " pip install mitmproxy"));
261
+ console.error();
237
262
  process.exit(1);
238
263
  }
239
264
  if (!cfg.client.desktopEnabled) {
@@ -241,13 +266,52 @@ export function registerClient(program) {
241
266
  cfg.client.desktopEnabled = true;
242
267
  writeConfig(cfg);
243
268
  }
269
+ // Pre-flight check: verify Network Extension is ready on macOS.
270
+ // startInterceptor does the same check and throws; we catch and show
271
+ // a friendlier block here with the open-settings shortcut.
272
+ if (isMacos()) {
273
+ const status = await getNetworkExtensionStatus();
274
+ if (status === "waiting") {
275
+ console.error(chalk.red("\n✗ Mitmproxy Network Extension is NOT yet approved.\n"));
276
+ printNetworkExtensionInstructions();
277
+ const openNow = await confirm({
278
+ message: "Open System Settings now?",
279
+ default: true,
280
+ });
281
+ if (openNow)
282
+ await openNetworkExtensionSettings();
283
+ console.error(chalk.yellow("\n Re-run `cc-router client start-desktop` after approving.\n"));
284
+ process.exit(1);
285
+ }
286
+ if (status === "not_installed") {
287
+ console.error(chalk.yellow("\n⚠ Mitmproxy Network Extension is not installed yet."));
288
+ console.error(chalk.gray(" The first mitmdump run will trigger installation."));
289
+ console.error(chalk.gray(" Approve it in System Settings when macOS prompts you, then re-run this command.\n"));
290
+ }
291
+ }
244
292
  const target = cfg.client.remoteUrl;
245
293
  const processName = getProcessName();
246
294
  console.log(chalk.cyan(`\nStarting mitmproxy interceptor for "${processName}"...`));
247
- console.log(chalk.gray(` Redirecting api.anthropic.com → ${target}\n`));
248
- await startInterceptor(target);
249
- console.log(chalk.green("✓ Claude Desktop interceptor running"));
250
- console.log(chalk.gray(" Open Claude Desktop and send a message to test.\n"));
295
+ console.log(chalk.gray(` Redirecting api.anthropic.com/v1/messages → ${target}`));
296
+ try {
297
+ await startInterceptor(target);
298
+ }
299
+ catch (e) {
300
+ console.error(chalk.red(`\n✗ Failed to start interceptor:\n`));
301
+ console.error(chalk.yellow(" " + e.message.split("\n").join("\n ")));
302
+ console.error();
303
+ process.exit(1);
304
+ }
305
+ console.log(chalk.green("\n✓ Claude Desktop interceptor running"));
306
+ console.log();
307
+ console.log(chalk.bold.yellow(" Next steps:"));
308
+ console.log(" " + chalk.cyan("1.") + " Quit Claude Desktop completely (⌘Q)");
309
+ console.log(" " + chalk.cyan("2.") + " Reopen Claude Desktop");
310
+ console.log(" " + chalk.cyan("3.") + " Use Cowork / Agent mode (Claude Code in Desktop)");
311
+ console.log();
312
+ console.log(chalk.gray(" Check routing with: ") + chalk.cyan("cc-router client status"));
313
+ console.log(chalk.gray(" Stop interceptor: ") + chalk.cyan("cc-router client stop-desktop"));
314
+ console.log();
251
315
  });
252
316
  // ── cc-router client stop-desktop ───────────────────────────────────────────
253
317
  client
@@ -259,53 +323,154 @@ export function registerClient(program) {
259
323
  });
260
324
  }
261
325
  // ─── Desktop setup flow ───────────────────────────────────────────────────────
326
+ /**
327
+ * Printed before asking the user whether to enable Desktop interception.
328
+ * The copy is deliberately explicit about WHAT works and WHAT doesn't — users
329
+ * who expect the normal chat to go through CC-Router will hit confusion fast,
330
+ * and we can head it off here by framing this as a "Cowork / Agent mode" feature.
331
+ */
332
+ export function printDesktopSupportExplainer() {
333
+ console.log(chalk.bold.cyan("\n 🖥 Claude Desktop — what CC-Router can route\n"));
334
+ console.log(" Claude Desktop does NOT expose ANTHROPIC_BASE_URL, so CC-Router uses\n" +
335
+ " mitmproxy to selectively intercept only the traffic it can handle:\n");
336
+ console.log(chalk.green(" ✓ Cowork / Agent mode ") + chalk.gray("— /v1/messages (this is what gets routed)"));
337
+ console.log(chalk.green(" ✓ Claude Code inside Desktop") + chalk.gray("— /v1/messages (same as CLI)"));
338
+ console.log(chalk.red(" ✗ Normal chat ") + chalk.gray("— goes to claude.ai webview, NOT redirectable"));
339
+ console.log();
340
+ console.log(chalk.gray(" TL;DR: Your LLM-heavy workflows (Cowork, agent tasks, in-Desktop\n" +
341
+ " Claude Code) will rotate across your Max accounts via CC-Router.\n" +
342
+ " The regular chat sidebar keeps going directly through claude.ai."));
343
+ console.log();
344
+ }
345
+ /**
346
+ * Prints the macOS Network Extension approval walkthrough.
347
+ * This is the #1 gotcha — mitmdump starts silently but captures nothing
348
+ * until the user flips the toggle in System Settings.
349
+ */
350
+ export function printNetworkExtensionInstructions() {
351
+ if (!isMacos())
352
+ return;
353
+ console.log(chalk.bold.yellow("\n ⚠ IMPORTANT — macOS Network Extension approval\n"));
354
+ console.log(" The first time mitmproxy runs in local mode, macOS installs a");
355
+ console.log(" Network Extension (" + chalk.cyan("Mitmproxy Redirector") + ") that must be approved");
356
+ console.log(" manually. " + chalk.red("Without this step, mitmproxy captures ZERO traffic.") + "\n");
357
+ console.log(chalk.bold(" Steps:"));
358
+ console.log(" " + chalk.cyan("1.") + " Open " + chalk.bold("System Settings"));
359
+ console.log(" " + chalk.cyan("2.") + " Go to " + chalk.bold("General → Login Items & Extensions"));
360
+ console.log(" " + chalk.cyan("3.") + " Scroll to " + chalk.bold("Network Extensions") + " and click the " + chalk.bold("ⓘ") + " button");
361
+ console.log(" " + chalk.cyan("4.") + " Toggle " + chalk.bold("Mitmproxy Redirector") + " ON");
362
+ console.log(" " + chalk.cyan("5.") + " Enter your Mac admin password when prompted\n");
363
+ console.log(chalk.gray(" You only need to do this ONCE per machine.\n"));
364
+ }
262
365
  async function setupDesktopInterception(target) {
263
366
  console.log(chalk.bold("\n🖥 Claude Desktop Setup\n"));
367
+ // 0. Explain what actually works before anything else
368
+ printDesktopSupportExplainer();
369
+ const proceedWithSetup = await confirm({
370
+ message: "Continue with Cowork / Agent-mode interception setup?",
371
+ default: true,
372
+ });
373
+ if (!proceedWithSetup) {
374
+ console.log(chalk.gray("Skipping Desktop setup. You can run it later with: cc-router client start-desktop\n"));
375
+ return;
376
+ }
264
377
  // 1. Check mitmproxy
265
- if (!await checkMitmproxyInstalled()) {
266
- console.log(chalk.yellow("mitmproxy is required but not installed."));
378
+ if (!(await checkMitmproxyInstalled())) {
379
+ console.log(chalk.yellow("\nmitmproxy is required but not installed."));
267
380
  if (isMacos()) {
268
- console.log(chalk.cyan(" Install: brew install mitmproxy"));
381
+ console.log(chalk.cyan(" Install: brew install mitmproxy"));
269
382
  }
270
383
  else if (isWindows()) {
271
- console.log(chalk.cyan(" Install: pip install mitmproxy"));
384
+ console.log(chalk.cyan(" Install: pip install mitmproxy (or download the installer from mitmproxy.org)"));
272
385
  }
273
386
  else {
274
- console.log(chalk.cyan(" Install: pip install mitmproxy (requires kernel ≥ 6.8)"));
387
+ console.log(chalk.cyan(" Install: pip install mitmproxy (Linux local mode requires kernel ≥ 6.8)"));
275
388
  }
276
389
  console.log();
277
- const proceed = await confirm({ message: "Have you installed mitmproxy?", default: false });
278
- if (!proceed || !await checkMitmproxyInstalled()) {
279
- console.log(chalk.red("mitmproxy not found. Skipping Desktop setup.\n"));
390
+ const proceed = await confirm({ message: "Have you installed mitmproxy now?", default: false });
391
+ if (!proceed || !(await checkMitmproxyInstalled())) {
392
+ console.log(chalk.red("\nmitmproxy still not found. Skipping Desktop setup.\n"));
393
+ console.log(chalk.gray("Re-run later with: cc-router client start-desktop\n"));
280
394
  return;
281
395
  }
282
396
  }
283
397
  console.log(chalk.green("✓ mitmproxy found"));
284
- // 2. Generate CA cert if needed
398
+ // 2. Generate CA cert if missing
285
399
  if (!isCaCertInstalled()) {
286
- console.log(chalk.gray("Generating mitmproxy CA certificate..."));
287
- await generateCaCert();
400
+ console.log(chalk.gray("Generating mitmproxy CA certificate (one-time)..."));
401
+ try {
402
+ await generateCaCert();
403
+ console.log(chalk.green("✓ CA certificate generated"));
404
+ }
405
+ catch (e) {
406
+ console.log(chalk.red(`✗ CA generation failed: ${e.message}`));
407
+ return;
408
+ }
409
+ }
410
+ else {
411
+ console.log(chalk.green("✓ CA certificate already present"));
288
412
  }
289
413
  // 3. Install CA cert (requires sudo)
290
- console.log(chalk.yellow("\nInstalling the mitmproxy CA certificate requires admin access."));
291
- console.log(chalk.gray("This is needed so Claude Desktop trusts the local interceptor."));
292
- const installCa = await confirm({ message: "Install CA certificate now? (requires password)", default: true });
414
+ console.log();
415
+ console.log(chalk.yellow("The mitmproxy CA certificate must be trusted by your OS so that"));
416
+ console.log(chalk.yellow("Claude Desktop accepts the local interceptor. This requires sudo."));
417
+ const installCa = await confirm({ message: "Install CA certificate now? (asks for admin password)", default: true });
293
418
  if (installCa) {
294
419
  const ok = await installCaCert();
295
420
  if (ok) {
296
- console.log(chalk.green("✓ CA certificate installed"));
421
+ console.log(chalk.green("✓ CA certificate installed in system trust store"));
297
422
  }
298
423
  else {
299
- console.log(chalk.red("✗ CA certificate install failed. You may need to install it manually."));
424
+ console.log(chalk.red("✗ CA certificate install failed."));
425
+ console.log(chalk.gray(" Install manually later with:"));
426
+ console.log(chalk.gray(" sudo security add-trusted-cert -d -r trustRoot \\"));
427
+ console.log(chalk.gray(" -k /Library/Keychains/System.keychain \\"));
428
+ console.log(chalk.gray(" ~/.mitmproxy/mitmproxy-ca-cert.pem"));
300
429
  }
301
430
  }
302
431
  // 4. Write addon script
303
432
  writeAddonScript(target);
304
433
  console.log(chalk.green("✓ Redirect addon configured"));
305
- // 5. macOS Network Extension note
434
+ // 5. macOS Network Extension — THIS is the step people miss
306
435
  if (isMacos()) {
307
- console.log(chalk.yellow("\n⚠ On first run, macOS will ask to approve mitmproxy's Network Extension."));
308
- console.log(chalk.gray(" Go to System Settings General Login Items & Extensions → Network Extensions"));
309
- console.log(chalk.gray(" and toggle 'Mitmproxy Redirector' on.\n"));
436
+ printNetworkExtensionInstructions();
437
+ // Check current status and guide the user if it's not enabled
438
+ const status = await getNetworkExtensionStatus();
439
+ if (status === "not_installed") {
440
+ console.log(chalk.gray(" The Network Extension hasn't been installed yet — it'll be triggered\n" +
441
+ " automatically the first time you run `cc-router client start-desktop`.\n" +
442
+ " macOS will show a popup — approve it and follow the steps above.\n"));
443
+ }
444
+ else if (status === "waiting") {
445
+ console.log(chalk.red(" ⚠ Network Extension is installed but NOT yet approved.\n"));
446
+ const openNow = await confirm({
447
+ message: "Open System Settings now so you can approve it?",
448
+ default: true,
449
+ });
450
+ if (openNow) {
451
+ await openNetworkExtensionSettings();
452
+ console.log(chalk.gray("\n System Settings should now be open."));
453
+ console.log(chalk.gray(" Toggle 'Mitmproxy Redirector' ON, then come back here.\n"));
454
+ await confirm({ message: "Done? Press Enter when the toggle is ON", default: true });
455
+ const newStatus = await getNetworkExtensionStatus();
456
+ if (newStatus === "enabled") {
457
+ console.log(chalk.green("✓ Network Extension is enabled"));
458
+ }
459
+ else {
460
+ console.log(chalk.yellow(` Still not enabled (status: ${newStatus})`));
461
+ console.log(chalk.gray(" You can re-check later with: cc-router client status"));
462
+ }
463
+ }
464
+ }
465
+ else if (status === "enabled") {
466
+ console.log(chalk.green(" ✓ Network Extension is already enabled — you're all set"));
467
+ }
310
468
  }
469
+ // 6. Remind that Claude Desktop must be restarted for mitmproxy to hook into it
470
+ console.log();
471
+ console.log(chalk.bold.yellow(" One more thing:"));
472
+ console.log(chalk.gray(" After starting the interceptor, you must " + chalk.bold("quit and relaunch Claude Desktop")));
473
+ console.log(chalk.gray(" (⌘Q in Claude Desktop, then reopen it). mitmproxy only captures"));
474
+ console.log(chalk.gray(" traffic from processes started AFTER it begins listening."));
475
+ console.log();
311
476
  }
@@ -11,7 +11,8 @@ import { loadAccounts, accountsFileExists, readConfig, writeConfig, generateProx
11
11
  import { PROXY_PORT } from "../config/paths.js";
12
12
  import { DEFAULT_RATE_LIMITS } from "../proxy/types.js";
13
13
  import { existsSync } from "fs";
14
- import { checkMitmproxyInstalled, isCaCertInstalled, generateCaCert, installCaCert, writeAddonScript, } from "../interceptor/mitmproxy-manager.js";
14
+ import { checkMitmproxyInstalled, isCaCertInstalled, generateCaCert, installCaCert, writeAddonScript, getNetworkExtensionStatus, openNetworkExtensionSettings, } from "../interceptor/mitmproxy-manager.js";
15
+ import { printDesktopSupportExplainer, printNetworkExtensionInstructions } from "./cmd-client.js";
15
16
  const execFileAsync = promisify(execFile);
16
17
  // ─── Public registration ──────────────────────────────────────────────────────
17
18
  export function registerSetup(program) {
@@ -567,11 +568,12 @@ async function runClientSetupFromWizard() {
567
568
  writeClaudeSettings(0, url, secret ?? "proxy-managed");
568
569
  console.log(chalk.green("✓ Claude Code configured"));
569
570
  console.log(chalk.gray(` ANTHROPIC_BASE_URL → ${url}\n`));
570
- // ── Claude Desktop question ─────────────────────────────────────────────
571
+ // ── Claude Desktop (Cowork / Agent mode) ─────────────────────────────────
571
572
  const desktopInstalled = isMacos() && existsSync("/Applications/Claude.app");
572
573
  if (desktopInstalled) {
574
+ printDesktopSupportExplainer();
573
575
  const wantsDesktop = await confirm({
574
- message: "Also route Claude Desktop (chat + Cowork) through the proxy?",
576
+ message: "Route Claude Desktop's Cowork / Agent-mode traffic through CC-Router?",
575
577
  default: false,
576
578
  });
577
579
  if (wantsDesktop) {
@@ -589,33 +591,82 @@ async function runClientSetupFromWizard() {
589
591
  console.log();
590
592
  }
591
593
  async function setupDesktopFromWizard(target) {
592
- console.log(chalk.bold("\n🖥 Claude Desktop Setup\n"));
594
+ console.log(chalk.bold("\n🖥 Claude Desktop — Cowork / Agent Setup\n"));
595
+ // 1. Check mitmproxy
593
596
  if (!(await checkMitmproxyInstalled())) {
594
597
  console.log(chalk.yellow("mitmproxy is required but not installed."));
595
- console.log(chalk.cyan(" Install: brew install mitmproxy\n"));
596
- const proceed = await confirm({ message: "Have you installed mitmproxy?", default: false });
598
+ if (isMacos()) {
599
+ console.log(chalk.cyan(" Install: brew install mitmproxy\n"));
600
+ }
601
+ else {
602
+ console.log(chalk.cyan(" Install: pip install mitmproxy\n"));
603
+ }
604
+ const proceed = await confirm({ message: "Have you installed mitmproxy now?", default: false });
597
605
  if (!proceed || !(await checkMitmproxyInstalled())) {
598
- console.log(chalk.red("Skipping Desktop setup.\n"));
606
+ console.log(chalk.red("Skipping Desktop setup. Re-run with: cc-router client start-desktop\n"));
599
607
  return;
600
608
  }
601
609
  }
602
610
  console.log(chalk.green("✓ mitmproxy found"));
611
+ // 2. CA cert
603
612
  if (!isCaCertInstalled()) {
604
- console.log(chalk.gray("Generating mitmproxy CA certificate..."));
605
- await generateCaCert();
613
+ console.log(chalk.gray("Generating mitmproxy CA certificate (one-time)..."));
614
+ try {
615
+ await generateCaCert();
616
+ console.log(chalk.green("✓ CA certificate generated"));
617
+ }
618
+ catch (e) {
619
+ console.log(chalk.red(`✗ CA generation failed: ${e.message}`));
620
+ return;
621
+ }
622
+ }
623
+ else {
624
+ console.log(chalk.green("✓ CA certificate already present"));
606
625
  }
607
- console.log(chalk.yellow("\nInstalling the CA certificate requires your admin password."));
626
+ console.log(chalk.yellow("\nThe CA certificate must be installed in your OS trust store (requires admin)."));
608
627
  const doInstall = await confirm({ message: "Install CA certificate now?", default: true });
609
628
  if (doInstall) {
610
629
  const ok = await installCaCert();
611
- console.log(ok ? chalk.green("✓ CA certificate installed") : chalk.red("✗ CA install failed — install manually later"));
630
+ if (ok) {
631
+ console.log(chalk.green("✓ CA certificate installed in system trust store"));
632
+ }
633
+ else {
634
+ console.log(chalk.red("✗ CA install failed."));
635
+ console.log(chalk.gray(" Install manually: sudo security add-trusted-cert -d -r trustRoot \\"));
636
+ console.log(chalk.gray(" -k /Library/Keychains/System.keychain ~/.mitmproxy/mitmproxy-ca-cert.pem"));
637
+ }
612
638
  }
639
+ // 3. Addon
613
640
  writeAddonScript(target);
614
641
  console.log(chalk.green("✓ Redirect addon configured"));
642
+ // 4. Network Extension walkthrough (macOS)
615
643
  if (isMacos()) {
616
- console.log(chalk.yellow("\n⚠ On first run macOS will ask to approve mitmproxy's Network Extension."));
617
- console.log(chalk.gray(" System Settings General → Login Items & Extensions → Network Extensions"));
618
- console.log(chalk.gray(" Toggle 'Mitmproxy Redirector' on.\n"));
644
+ printNetworkExtensionInstructions();
645
+ const status = await getNetworkExtensionStatus();
646
+ if (status === "not_installed") {
647
+ console.log(chalk.gray(" The extension will be installed on first `cc-router client start-desktop`.\n" +
648
+ " macOS will show a popup — follow the steps above to approve it.\n"));
649
+ }
650
+ else if (status === "waiting") {
651
+ console.log(chalk.red(" ⚠ Extension is installed but NOT approved.\n"));
652
+ const openNow = await confirm({ message: "Open System Settings to approve it now?", default: true });
653
+ if (openNow) {
654
+ await openNetworkExtensionSettings();
655
+ console.log(chalk.gray(" System Settings should be open. Toggle 'Mitmproxy Redirector' ON.\n"));
656
+ await confirm({ message: "Done? Press Enter when the toggle is ON", default: true });
657
+ const newStatus = await getNetworkExtensionStatus();
658
+ console.log(newStatus === "enabled"
659
+ ? chalk.green(" ✓ Network Extension enabled")
660
+ : chalk.yellow(` Still not enabled (status: ${newStatus}) — you can fix later`));
661
+ }
662
+ }
663
+ else if (status === "enabled") {
664
+ console.log(chalk.green(" ✓ Network Extension already enabled — you're all set\n"));
665
+ }
666
+ // Remind to restart Claude Desktop
667
+ console.log(chalk.bold.yellow(" Remember:"));
668
+ console.log(chalk.gray(" After starting the interceptor, " + chalk.bold("quit and relaunch Claude Desktop") + " (⌘Q)"));
669
+ console.log(chalk.gray(" so mitmproxy can hook into the new process.\n"));
619
670
  }
620
671
  }
621
672
  function printBanner() {
@@ -60,6 +60,64 @@ export async function checkMitmproxyInstalled() {
60
60
  return false;
61
61
  }
62
62
  }
63
+ /**
64
+ * Check the approval status of the mitmproxy macOS Network Extension.
65
+ * No-op on Windows/Linux (returns "enabled").
66
+ *
67
+ * Parses `systemextensionsctl list` output, looking for the mitmproxy entry.
68
+ * Status comes from the flags column:
69
+ * "* *" → enabled + active
70
+ * " *" → active but waiting for user approval
71
+ */
72
+ export async function getNetworkExtensionStatus() {
73
+ if (!isMacos())
74
+ return "enabled"; // Only macOS needs this check
75
+ try {
76
+ const { stdout } = await execFileP("systemextensionsctl", ["list"]);
77
+ const mitmLine = stdout
78
+ .split("\n")
79
+ .find((l) => l.toLowerCase().includes("mitmproxy"));
80
+ if (!mitmLine)
81
+ return "not_installed";
82
+ // systemextensionsctl flags are the first two columns; "*" means set.
83
+ // Order is "enabled active" — both must be "*" for the extension to work.
84
+ // Example strings seen in the wild:
85
+ // "*\t*\tS8XHQB96PW\torg.mitmproxy.macos-redirector..." → enabled
86
+ // "\t*\tS8XHQB96PW\torg.mitmproxy.macos-redirector..." → waiting
87
+ //
88
+ // We also accept the human-readable "[activated enabled]" / "[activated waiting for user]"
89
+ // suffix that newer macOS versions append.
90
+ if (mitmLine.includes("[activated enabled]"))
91
+ return "enabled";
92
+ if (mitmLine.includes("waiting for user"))
93
+ return "waiting";
94
+ const cols = mitmLine.split("\t").map((s) => s.trim());
95
+ const enabled = cols[0] === "*";
96
+ const active = cols[1] === "*";
97
+ if (enabled && active)
98
+ return "enabled";
99
+ if (!enabled && active)
100
+ return "waiting";
101
+ return "not_installed";
102
+ }
103
+ catch {
104
+ return "unknown";
105
+ }
106
+ }
107
+ /** Open the macOS "Login Items & Extensions" settings pane. Best-effort. */
108
+ export async function openNetworkExtensionSettings() {
109
+ if (!isMacos())
110
+ return;
111
+ try {
112
+ // The x-apple.systempreferences URL opens the right pane in System Settings.
113
+ // Extensions pane is not directly deep-linkable, so we open the closest one.
114
+ await execFileP("open", ["x-apple.systempreferences:com.apple.LoginItems-Settings.extension"]);
115
+ }
116
+ catch {
117
+ // If that fails, fall back to opening plain System Settings
118
+ await execFileP("open", ["/System/Applications/System Settings.app"]).catch(() => { });
119
+ }
120
+ }
63
121
  // ─── CA certificate ───────────────────────────────────────────────────────────
64
122
  export function isCaCertInstalled() {
65
123
  return existsSync(CA_PATH);
@@ -165,6 +223,24 @@ def request(flow: http.HTTPFlow) -> None:
165
223
  * api.anthropic.com traffic to CC-Router via the addon script.
166
224
  */
167
225
  export async function startInterceptor(target) {
226
+ // On macOS, verify the Network Extension is enabled before attempting to start.
227
+ // If it's "waiting", mitmdump starts silently but captures zero traffic.
228
+ if (isMacos()) {
229
+ const status = await getNetworkExtensionStatus();
230
+ if (status === "waiting") {
231
+ throw new Error("Mitmproxy Network Extension is installed but not yet approved.\n" +
232
+ " Open: System Settings → General → Login Items & Extensions → Network Extensions\n" +
233
+ ' Toggle "Mitmproxy Redirector" ON and enter your admin password.\n' +
234
+ " Then re-run this command.");
235
+ }
236
+ if (status === "not_installed") {
237
+ throw new Error("Mitmproxy Network Extension is not installed.\n" +
238
+ " Run mitmdump once manually to trigger the installation:\n" +
239
+ ' mitmdump --mode "local:Claude" --set connection_strategy=lazy\n' +
240
+ " macOS will prompt you to approve it in System Settings.\n" +
241
+ " Then re-run this command.");
242
+ }
243
+ }
168
244
  // Ensure addon exists
169
245
  if (!existsSync(ADDON_PATH))
170
246
  writeAddonScript(target);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai-cc-router",
3
- "version": "0.2.2",
3
+ "version": "0.2.3",
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": {