ai-cc-router 0.2.1 → 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.
- package/dist/cli/cmd-client.js +201 -36
- package/dist/cli/cmd-setup.js +65 -14
- package/dist/interceptor/mitmproxy-manager.js +80 -1
- package/package.json +1 -1
- package/src/interceptor/addon.py +21 -11
package/dist/cli/cmd-client.js
CHANGED
|
@@ -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
|
-
|
|
117
|
-
|
|
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
|
-
|
|
214
|
-
|
|
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.
|
|
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}
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
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("
|
|
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:
|
|
381
|
+
console.log(chalk.cyan(" Install: brew install mitmproxy"));
|
|
269
382
|
}
|
|
270
383
|
else if (isWindows()) {
|
|
271
|
-
console.log(chalk.cyan(" Install:
|
|
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:
|
|
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("
|
|
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
|
|
398
|
+
// 2. Generate CA cert if missing
|
|
285
399
|
if (!isCaCertInstalled()) {
|
|
286
|
-
console.log(chalk.gray("Generating mitmproxy CA certificate..."));
|
|
287
|
-
|
|
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(
|
|
291
|
-
console.log(chalk.
|
|
292
|
-
|
|
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.
|
|
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
|
|
434
|
+
// 5. macOS Network Extension — THIS is the step people miss
|
|
306
435
|
if (isMacos()) {
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
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
|
}
|
package/dist/cli/cmd-setup.js
CHANGED
|
@@ -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
|
|
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: "
|
|
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
|
-
|
|
596
|
-
|
|
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
|
|
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
|
-
|
|
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("\
|
|
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
|
-
|
|
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
|
-
|
|
617
|
-
|
|
618
|
-
|
|
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);
|
|
@@ -136,7 +194,7 @@ export function writeAddonScript(target) {
|
|
|
136
194
|
writeFileSync(ADDON_PATH, src, "utf-8");
|
|
137
195
|
}
|
|
138
196
|
else {
|
|
139
|
-
// Inline fallback — minimal addon
|
|
197
|
+
// Inline fallback — minimal addon (only redirects /v1/messages and /v1/models)
|
|
140
198
|
const script = `
|
|
141
199
|
import os
|
|
142
200
|
from mitmproxy import http
|
|
@@ -144,10 +202,13 @@ from urllib.parse import urlparse
|
|
|
144
202
|
|
|
145
203
|
_target = os.environ.get("CC_ROUTER_TARGET", ${JSON.stringify(target)}).rstrip("/")
|
|
146
204
|
_p = urlparse(_target)
|
|
205
|
+
_REDIRECT_PREFIXES = ("/v1/messages", "/v1/models")
|
|
147
206
|
|
|
148
207
|
def request(flow: http.HTTPFlow) -> None:
|
|
149
208
|
if flow.request.pretty_host != "api.anthropic.com":
|
|
150
209
|
return
|
|
210
|
+
if not flow.request.path.startswith(_REDIRECT_PREFIXES):
|
|
211
|
+
return
|
|
151
212
|
flow.request.scheme = _p.scheme
|
|
152
213
|
flow.request.host = _p.hostname or "localhost"
|
|
153
214
|
flow.request.port = _p.port or (443 if _p.scheme == "https" else 80)
|
|
@@ -162,6 +223,24 @@ def request(flow: http.HTTPFlow) -> None:
|
|
|
162
223
|
* api.anthropic.com traffic to CC-Router via the addon script.
|
|
163
224
|
*/
|
|
164
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
|
+
}
|
|
165
244
|
// Ensure addon exists
|
|
166
245
|
if (!existsSync(ADDON_PATH))
|
|
167
246
|
writeAddonScript(target);
|
package/package.json
CHANGED
package/src/interceptor/addon.py
CHANGED
|
@@ -1,11 +1,16 @@
|
|
|
1
|
-
# mitmproxy addon — redirects
|
|
1
|
+
# mitmproxy addon — redirects ONLY /v1/messages traffic to CC-Router.
|
|
2
2
|
#
|
|
3
|
-
#
|
|
4
|
-
#
|
|
3
|
+
# Claude Desktop sends many types of requests to api.anthropic.com:
|
|
4
|
+
# /v1/messages → LLM inference (this is what we redirect)
|
|
5
|
+
# /v1/messages/count_tokens → token counting (redirect too)
|
|
6
|
+
# /v1/oauth/* → session auth (must NOT redirect)
|
|
7
|
+
# /v1/environments/* → bridge/cowork (must NOT redirect)
|
|
8
|
+
# /v1/models → model listing (redirect — CC-Router proxies this)
|
|
9
|
+
# /api/* → desktop features (must NOT redirect)
|
|
5
10
|
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
11
|
+
# Only /v1/messages* and /v1/models are safe to redirect because CC-Router
|
|
12
|
+
# injects its own OAuth token. Everything else carries the user's own
|
|
13
|
+
# session token for features CC-Router doesn't handle.
|
|
9
14
|
|
|
10
15
|
import os
|
|
11
16
|
from urllib.parse import urlparse
|
|
@@ -16,22 +21,27 @@ _target_raw = os.environ.get("CC_ROUTER_TARGET", "http://localhost:3456")
|
|
|
16
21
|
_target = _target_raw.rstrip("/")
|
|
17
22
|
_target_parsed = urlparse(_target)
|
|
18
23
|
|
|
19
|
-
# Fail closed on boot if the target is unusable — better than silently
|
|
20
|
-
# forwarding to a broken URL and seeing Claude Desktop timeout.
|
|
21
24
|
if not _target_parsed.scheme or not _target_parsed.netloc:
|
|
22
25
|
raise RuntimeError(f"CC_ROUTER_TARGET is not a valid URL: {_target_raw!r}")
|
|
23
26
|
|
|
27
|
+
# Paths that CC-Router can handle (it injects its own OAuth token)
|
|
28
|
+
_REDIRECT_PREFIXES = (
|
|
29
|
+
"/v1/messages",
|
|
30
|
+
"/v1/models",
|
|
31
|
+
)
|
|
32
|
+
|
|
24
33
|
|
|
25
34
|
def request(flow: http.HTTPFlow) -> None:
|
|
26
35
|
if flow.request.pretty_host != "api.anthropic.com":
|
|
27
36
|
return
|
|
28
37
|
|
|
29
|
-
#
|
|
30
|
-
|
|
38
|
+
# Only redirect inference and model-listing paths
|
|
39
|
+
if not flow.request.path.startswith(_REDIRECT_PREFIXES):
|
|
40
|
+
return
|
|
41
|
+
|
|
31
42
|
flow.request.scheme = _target_parsed.scheme
|
|
32
43
|
flow.request.host = _target_parsed.hostname or "localhost"
|
|
33
44
|
flow.request.port = _target_parsed.port or (443 if _target_parsed.scheme == "https" else 80)
|
|
34
|
-
# Rewrite the Host header so CC-Router sees itself, not api.anthropic.com.
|
|
35
45
|
flow.request.headers["host"] = flow.request.host + (
|
|
36
46
|
f":{flow.request.port}"
|
|
37
47
|
if flow.request.port not in (80, 443)
|