Skip to content

Commit 03f4ff5

Browse files
committed
feat(client): improved desktop setup with extension walkthrough and cowork scope
- Explain explicitly that only Cowork/Agent mode is routed, NOT normal chat - Add printDesktopSupportExplainer() showing ✓/✗ scope table before asking - Add printNetworkExtensionInstructions() with step-by-step macOS approval guide - Check Network Extension status and offer to open System Settings if "waiting" - startInterceptor() refuses to start if extension isn't approved (avoids silent zero-capture) - client status shows extension state and warns if not approved - Remind user to quit+relaunch Claude Desktop after starting interceptor
1 parent eea8209 commit 03f4ff5

5 files changed

Lines changed: 362 additions & 54 deletions

File tree

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "ai-cc-router",
3-
"version": "0.2.2",
3+
"version": "0.2.3",
44
"description": "Round-robin proxy for Claude Max OAuth tokens — use multiple Claude Max accounts with Claude Code",
55
"type": "module",
66
"bin": {

src/cli/cmd-client.ts

Lines changed: 205 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ import {
1515
stopInterceptor,
1616
isInterceptorRunning,
1717
getProcessName,
18+
getNetworkExtensionStatus,
19+
openNetworkExtensionSettings,
1820
} from "../interceptor/mitmproxy-manager.js";
1921

2022
// ─── Helpers ──────────────────────────────────────────────────────────────────
@@ -154,11 +156,15 @@ export function registerClient(program: Command): void {
154156
console.log(chalk.green("✓ Claude Code configured to route through CC-Router"));
155157
console.log(chalk.gray(` ANTHROPIC_BASE_URL → ${url}`));
156158

157-
// 6. Optionally configure Claude Desktop
158-
const wantsDesktop = opts?.desktop ?? (
159-
isClaudeDesktopInstalled() &&
160-
await confirm({ message: "Also route Claude Desktop (chat + Cowork) through the proxy?", default: false })
161-
);
159+
// 6. Optionally configure Claude Desktop (Cowork / Agent mode only)
160+
let wantsDesktop = opts?.desktop ?? false;
161+
if (!opts?.desktop && isClaudeDesktopInstalled()) {
162+
printDesktopSupportExplainer();
163+
wantsDesktop = await confirm({
164+
message: "Route Claude Desktop's Cowork / Agent-mode traffic through CC-Router?",
165+
default: false,
166+
});
167+
}
162168

163169
if (wantsDesktop) {
164170
await setupDesktopInterception(url);
@@ -276,15 +282,30 @@ export function registerClient(program: Command): void {
276282
}
277283

278284
// ── Desktop status ─────────────────────────────────────────────────
279-
console.log(chalk.bold("\n DESKTOP INTERCEPTOR"));
285+
console.log(chalk.bold("\n DESKTOP INTERCEPTOR (Cowork / Agent mode)"));
280286
if (cfg.client.desktopEnabled) {
281287
const running = await isInterceptorRunning();
282-
console.log(` ${running ? chalk.green("● running") : chalk.yellow("○ configured but stopped")}`);
283-
if (!running) {
288+
if (running) {
289+
console.log(` ${chalk.green("● running")}`);
290+
} else {
291+
console.log(` ${chalk.yellow("○ configured but stopped")}`);
284292
console.log(chalk.gray(" Start with: cc-router client start-desktop"));
285293
}
294+
// Check Network Extension on macOS
295+
if (isMacos()) {
296+
const extStatus = await getNetworkExtensionStatus();
297+
if (extStatus === "waiting") {
298+
console.log(chalk.red(" ⚠ Network Extension NOT approved — interceptor won't capture traffic!"));
299+
console.log(chalk.gray(" Fix: System Settings → General → Login Items & Extensions → Network Extensions"));
300+
} else if (extStatus === "not_installed") {
301+
console.log(chalk.yellow(" ⚠ Network Extension not installed — will be triggered on first start"));
302+
} else if (extStatus === "enabled") {
303+
console.log(` ${chalk.green("✓")} ${chalk.gray("Network Extension: enabled")}`);
304+
}
305+
}
306+
console.log(chalk.gray(" Scope: /v1/messages + /v1/models (normal chat NOT routed)"));
286307
} else {
287-
console.log(` ${chalk.gray("not configured")}`);
308+
console.log(` ${chalk.gray("not configured — enable with: cc-router client connect --desktop")}`);
288309
}
289310

290311
console.log();
@@ -294,17 +315,18 @@ export function registerClient(program: Command): void {
294315
// ── cc-router client start-desktop ──────────────────────────────────────────
295316
client
296317
.command("start-desktop")
297-
.description("Start mitmproxy interceptor for Claude Desktop")
318+
.description("Start mitmproxy interceptor for Claude Desktop (Cowork / Agent mode)")
298319
.action(async () => {
299320
const cfg = readConfig();
300321
if (!cfg.client) {
301322
console.error(chalk.red("Not connected. Run: cc-router client connect <url>"));
302323
process.exit(1);
303324
}
304325

305-
if (!await checkMitmproxyInstalled()) {
306-
console.error(chalk.red("mitmproxy not found. Install it first:"));
307-
console.error(chalk.yellow(isMacos() ? " brew install mitmproxy" : " pip install mitmproxy"));
326+
if (!(await checkMitmproxyInstalled())) {
327+
console.error(chalk.red("\n✗ mitmproxy not found. Install it first:"));
328+
console.error(chalk.cyan(isMacos() ? " brew install mitmproxy" : " pip install mitmproxy"));
329+
console.error();
308330
process.exit(1);
309331
}
310332

@@ -314,15 +336,53 @@ export function registerClient(program: Command): void {
314336
writeConfig(cfg);
315337
}
316338

339+
// Pre-flight check: verify Network Extension is ready on macOS.
340+
// startInterceptor does the same check and throws; we catch and show
341+
// a friendlier block here with the open-settings shortcut.
342+
if (isMacos()) {
343+
const status = await getNetworkExtensionStatus();
344+
if (status === "waiting") {
345+
console.error(chalk.red("\n✗ Mitmproxy Network Extension is NOT yet approved.\n"));
346+
printNetworkExtensionInstructions();
347+
const openNow = await confirm({
348+
message: "Open System Settings now?",
349+
default: true,
350+
});
351+
if (openNow) await openNetworkExtensionSettings();
352+
console.error(chalk.yellow("\n Re-run `cc-router client start-desktop` after approving.\n"));
353+
process.exit(1);
354+
}
355+
if (status === "not_installed") {
356+
console.error(chalk.yellow("\n⚠ Mitmproxy Network Extension is not installed yet."));
357+
console.error(chalk.gray(" The first mitmdump run will trigger installation."));
358+
console.error(chalk.gray(" Approve it in System Settings when macOS prompts you, then re-run this command.\n"));
359+
}
360+
}
361+
317362
const target = cfg.client.remoteUrl;
318363
const processName = getProcessName();
319364
console.log(chalk.cyan(`\nStarting mitmproxy interceptor for "${processName}"...`));
320-
console.log(chalk.gray(` Redirecting api.anthropic.com → ${target}\n`));
321-
322-
await startInterceptor(target);
365+
console.log(chalk.gray(` Redirecting api.anthropic.com/v1/messages → ${target}`));
366+
367+
try {
368+
await startInterceptor(target);
369+
} catch (e) {
370+
console.error(chalk.red(`\n✗ Failed to start interceptor:\n`));
371+
console.error(chalk.yellow(" " + (e as Error).message.split("\n").join("\n ")));
372+
console.error();
373+
process.exit(1);
374+
}
323375

324-
console.log(chalk.green("✓ Claude Desktop interceptor running"));
325-
console.log(chalk.gray(" Open Claude Desktop and send a message to test.\n"));
376+
console.log(chalk.green("\n✓ Claude Desktop interceptor running"));
377+
console.log();
378+
console.log(chalk.bold.yellow(" Next steps:"));
379+
console.log(" " + chalk.cyan("1.") + " Quit Claude Desktop completely (⌘Q)");
380+
console.log(" " + chalk.cyan("2.") + " Reopen Claude Desktop");
381+
console.log(" " + chalk.cyan("3.") + " Use Cowork / Agent mode (Claude Code in Desktop)");
382+
console.log();
383+
console.log(chalk.gray(" Check routing with: ") + chalk.cyan("cc-router client status"));
384+
console.log(chalk.gray(" Stop interceptor: ") + chalk.cyan("cc-router client stop-desktop"));
385+
console.log();
326386
});
327387

328388
// ── cc-router client stop-desktop ───────────────────────────────────────────
@@ -337,55 +397,162 @@ export function registerClient(program: Command): void {
337397

338398
// ─── Desktop setup flow ───────────────────────────────────────────────────────
339399

400+
/**
401+
* Printed before asking the user whether to enable Desktop interception.
402+
* The copy is deliberately explicit about WHAT works and WHAT doesn't — users
403+
* who expect the normal chat to go through CC-Router will hit confusion fast,
404+
* and we can head it off here by framing this as a "Cowork / Agent mode" feature.
405+
*/
406+
export function printDesktopSupportExplainer(): void {
407+
console.log(chalk.bold.cyan("\n 🖥 Claude Desktop — what CC-Router can route\n"));
408+
console.log(
409+
" Claude Desktop does NOT expose ANTHROPIC_BASE_URL, so CC-Router uses\n" +
410+
" mitmproxy to selectively intercept only the traffic it can handle:\n"
411+
);
412+
console.log(chalk.green(" ✓ Cowork / Agent mode ") + chalk.gray("— /v1/messages (this is what gets routed)"));
413+
console.log(chalk.green(" ✓ Claude Code inside Desktop") + chalk.gray("— /v1/messages (same as CLI)"));
414+
console.log(chalk.red(" ✗ Normal chat ") + chalk.gray("— goes to claude.ai webview, NOT redirectable"));
415+
console.log();
416+
console.log(chalk.gray(
417+
" TL;DR: Your LLM-heavy workflows (Cowork, agent tasks, in-Desktop\n" +
418+
" Claude Code) will rotate across your Max accounts via CC-Router.\n" +
419+
" The regular chat sidebar keeps going directly through claude.ai."
420+
));
421+
console.log();
422+
}
423+
424+
/**
425+
* Prints the macOS Network Extension approval walkthrough.
426+
* This is the #1 gotcha — mitmdump starts silently but captures nothing
427+
* until the user flips the toggle in System Settings.
428+
*/
429+
export function printNetworkExtensionInstructions(): void {
430+
if (!isMacos()) return;
431+
console.log(chalk.bold.yellow("\n ⚠ IMPORTANT — macOS Network Extension approval\n"));
432+
console.log(" The first time mitmproxy runs in local mode, macOS installs a");
433+
console.log(" Network Extension (" + chalk.cyan("Mitmproxy Redirector") + ") that must be approved");
434+
console.log(" manually. " + chalk.red("Without this step, mitmproxy captures ZERO traffic.") + "\n");
435+
console.log(chalk.bold(" Steps:"));
436+
console.log(" " + chalk.cyan("1.") + " Open " + chalk.bold("System Settings"));
437+
console.log(" " + chalk.cyan("2.") + " Go to " + chalk.bold("General → Login Items & Extensions"));
438+
console.log(" " + chalk.cyan("3.") + " Scroll to " + chalk.bold("Network Extensions") + " and click the " + chalk.bold("ⓘ") + " button");
439+
console.log(" " + chalk.cyan("4.") + " Toggle " + chalk.bold("Mitmproxy Redirector") + " ON");
440+
console.log(" " + chalk.cyan("5.") + " Enter your Mac admin password when prompted\n");
441+
console.log(chalk.gray(" You only need to do this ONCE per machine.\n"));
442+
}
443+
340444
async function setupDesktopInterception(target: string): Promise<void> {
341445
console.log(chalk.bold("\n🖥 Claude Desktop Setup\n"));
342446

447+
// 0. Explain what actually works before anything else
448+
printDesktopSupportExplainer();
449+
const proceedWithSetup = await confirm({
450+
message: "Continue with Cowork / Agent-mode interception setup?",
451+
default: true,
452+
});
453+
if (!proceedWithSetup) {
454+
console.log(chalk.gray("Skipping Desktop setup. You can run it later with: cc-router client start-desktop\n"));
455+
return;
456+
}
457+
343458
// 1. Check mitmproxy
344-
if (!await checkMitmproxyInstalled()) {
345-
console.log(chalk.yellow("mitmproxy is required but not installed."));
459+
if (!(await checkMitmproxyInstalled())) {
460+
console.log(chalk.yellow("\nmitmproxy is required but not installed."));
346461
if (isMacos()) {
347-
console.log(chalk.cyan(" Install: brew install mitmproxy"));
462+
console.log(chalk.cyan(" Install: brew install mitmproxy"));
348463
} else if (isWindows()) {
349-
console.log(chalk.cyan(" Install: pip install mitmproxy"));
464+
console.log(chalk.cyan(" Install: pip install mitmproxy (or download the installer from mitmproxy.org)"));
350465
} else {
351-
console.log(chalk.cyan(" Install: pip install mitmproxy (requires kernel ≥ 6.8)"));
466+
console.log(chalk.cyan(" Install: pip install mitmproxy (Linux local mode requires kernel ≥ 6.8)"));
352467
}
353468
console.log();
354-
const proceed = await confirm({ message: "Have you installed mitmproxy?", default: false });
355-
if (!proceed || !await checkMitmproxyInstalled()) {
356-
console.log(chalk.red("mitmproxy not found. Skipping Desktop setup.\n"));
469+
const proceed = await confirm({ message: "Have you installed mitmproxy now?", default: false });
470+
if (!proceed || !(await checkMitmproxyInstalled())) {
471+
console.log(chalk.red("\nmitmproxy still not found. Skipping Desktop setup.\n"));
472+
console.log(chalk.gray("Re-run later with: cc-router client start-desktop\n"));
357473
return;
358474
}
359475
}
360476
console.log(chalk.green("✓ mitmproxy found"));
361477

362-
// 2. Generate CA cert if needed
478+
// 2. Generate CA cert if missing
363479
if (!isCaCertInstalled()) {
364-
console.log(chalk.gray("Generating mitmproxy CA certificate..."));
365-
await generateCaCert();
480+
console.log(chalk.gray("Generating mitmproxy CA certificate (one-time)..."));
481+
try {
482+
await generateCaCert();
483+
console.log(chalk.green("✓ CA certificate generated"));
484+
} catch (e) {
485+
console.log(chalk.red(`✗ CA generation failed: ${(e as Error).message}`));
486+
return;
487+
}
488+
} else {
489+
console.log(chalk.green("✓ CA certificate already present"));
366490
}
367491

368492
// 3. Install CA cert (requires sudo)
369-
console.log(chalk.yellow("\nInstalling the mitmproxy CA certificate requires admin access."));
370-
console.log(chalk.gray("This is needed so Claude Desktop trusts the local interceptor."));
371-
const installCa = await confirm({ message: "Install CA certificate now? (requires password)", default: true });
493+
console.log();
494+
console.log(chalk.yellow("The mitmproxy CA certificate must be trusted by your OS so that"));
495+
console.log(chalk.yellow("Claude Desktop accepts the local interceptor. This requires sudo."));
496+
const installCa = await confirm({ message: "Install CA certificate now? (asks for admin password)", default: true });
372497
if (installCa) {
373498
const ok = await installCaCert();
374499
if (ok) {
375-
console.log(chalk.green("✓ CA certificate installed"));
500+
console.log(chalk.green("✓ CA certificate installed in system trust store"));
376501
} else {
377-
console.log(chalk.red("✗ CA certificate install failed. You may need to install it manually."));
502+
console.log(chalk.red("✗ CA certificate install failed."));
503+
console.log(chalk.gray(" Install manually later with:"));
504+
console.log(chalk.gray(" sudo security add-trusted-cert -d -r trustRoot \\"));
505+
console.log(chalk.gray(" -k /Library/Keychains/System.keychain \\"));
506+
console.log(chalk.gray(" ~/.mitmproxy/mitmproxy-ca-cert.pem"));
378507
}
379508
}
380509

381510
// 4. Write addon script
382511
writeAddonScript(target);
383512
console.log(chalk.green("✓ Redirect addon configured"));
384513

385-
// 5. macOS Network Extension note
514+
// 5. macOS Network Extension — THIS is the step people miss
386515
if (isMacos()) {
387-
console.log(chalk.yellow("\n⚠ On first run, macOS will ask to approve mitmproxy's Network Extension."));
388-
console.log(chalk.gray(" Go to System Settings → General → Login Items & Extensions → Network Extensions"));
389-
console.log(chalk.gray(" and toggle 'Mitmproxy Redirector' on.\n"));
516+
printNetworkExtensionInstructions();
517+
518+
// Check current status and guide the user if it's not enabled
519+
const status = await getNetworkExtensionStatus();
520+
521+
if (status === "not_installed") {
522+
console.log(chalk.gray(
523+
" The Network Extension hasn't been installed yet — it'll be triggered\n" +
524+
" automatically the first time you run `cc-router client start-desktop`.\n" +
525+
" macOS will show a popup — approve it and follow the steps above.\n"
526+
));
527+
} else if (status === "waiting") {
528+
console.log(chalk.red(" ⚠ Network Extension is installed but NOT yet approved.\n"));
529+
const openNow = await confirm({
530+
message: "Open System Settings now so you can approve it?",
531+
default: true,
532+
});
533+
if (openNow) {
534+
await openNetworkExtensionSettings();
535+
console.log(chalk.gray("\n System Settings should now be open."));
536+
console.log(chalk.gray(" Toggle 'Mitmproxy Redirector' ON, then come back here.\n"));
537+
await confirm({ message: "Done? Press Enter when the toggle is ON", default: true });
538+
const newStatus = await getNetworkExtensionStatus();
539+
if (newStatus === "enabled") {
540+
console.log(chalk.green("✓ Network Extension is enabled"));
541+
} else {
542+
console.log(chalk.yellow(` Still not enabled (status: ${newStatus})`));
543+
console.log(chalk.gray(" You can re-check later with: cc-router client status"));
544+
}
545+
}
546+
} else if (status === "enabled") {
547+
console.log(chalk.green(" ✓ Network Extension is already enabled — you're all set"));
548+
}
390549
}
550+
551+
// 6. Remind that Claude Desktop must be restarted for mitmproxy to hook into it
552+
console.log();
553+
console.log(chalk.bold.yellow(" One more thing:"));
554+
console.log(chalk.gray(" After starting the interceptor, you must " + chalk.bold("quit and relaunch Claude Desktop") ));
555+
console.log(chalk.gray(" (⌘Q in Claude Desktop, then reopen it). mitmproxy only captures"));
556+
console.log(chalk.gray(" traffic from processes started AFTER it begins listening."));
557+
console.log();
391558
}

0 commit comments

Comments
 (0)