@timo972/cc-router 0.7.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/CHANGELOG.md +96 -0
- package/Dockerfile +42 -0
- package/LICENSE +21 -0
- package/README.md +716 -0
- package/accounts.example.json +25 -0
- package/dist/cli/cmd-accounts.js +248 -0
- package/dist/cli/cmd-client.js +612 -0
- package/dist/cli/cmd-configure.js +145 -0
- package/dist/cli/cmd-docker.js +140 -0
- package/dist/cli/cmd-logs.js +85 -0
- package/dist/cli/cmd-models.js +125 -0
- package/dist/cli/cmd-service.js +193 -0
- package/dist/cli/cmd-setup.js +501 -0
- package/dist/cli/cmd-start.js +318 -0
- package/dist/cli/cmd-status.js +177 -0
- package/dist/cli/cmd-stop.js +100 -0
- package/dist/cli/cmd-telemetry.js +58 -0
- package/dist/cli/cmd-update.js +37 -0
- package/dist/cli/index.js +59 -0
- package/dist/config/manager.js +262 -0
- package/dist/config/paths.js +21 -0
- package/dist/config/telemetry.js +64 -0
- package/dist/daemon/launcher.js +163 -0
- package/dist/daemon/pid.js +98 -0
- package/dist/daemon/service.js +260 -0
- package/dist/interceptor/mitmproxy-manager.js +616 -0
- package/dist/protocol/anthropic-to-openai.js +51 -0
- package/dist/protocol/anthropic-types.js +1 -0
- package/dist/protocol/model-ref.js +36 -0
- package/dist/protocol/model-routing-config.js +30 -0
- package/dist/protocol/openai-response-to-anthropic.js +20 -0
- package/dist/protocol/openai-responses-types.js +1 -0
- package/dist/protocol/openai-stream-to-anthropic.js +75 -0
- package/dist/protocol/openai-to-anthropic.js +61 -0
- package/dist/protocol/sse.js +17 -0
- package/dist/providers/model-discovery.js +71 -0
- package/dist/providers/openai/account-pool.js +11 -0
- package/dist/providers/openai/account-record.js +33 -0
- package/dist/providers/openai/codex-transport.js +36 -0
- package/dist/providers/openai/device-oauth.js +116 -0
- package/dist/providers/openai/token-refresher.js +56 -0
- package/dist/providers/route-selector.js +8 -0
- package/dist/providers/types.js +1 -0
- package/dist/proxy/account-deletion.js +44 -0
- package/dist/proxy/anthropic-proxy.js +26 -0
- package/dist/proxy/anthropic-routing.js +90 -0
- package/dist/proxy/lease-lifecycle.js +68 -0
- package/dist/proxy/logger.js +39 -0
- package/dist/proxy/messages-cross-route.js +179 -0
- package/dist/proxy/models-server.js +150 -0
- package/dist/proxy/provider-routing.js +14 -0
- package/dist/proxy/responses-server.js +91 -0
- package/dist/proxy/server.js +875 -0
- package/dist/proxy/session-router.js +171 -0
- package/dist/proxy/stats.js +25 -0
- package/dist/proxy/stream-lifecycle.js +83 -0
- package/dist/proxy/token-pool.js +407 -0
- package/dist/proxy/token-refresher.js +209 -0
- package/dist/proxy/types.js +29 -0
- package/dist/ui/Dashboard.js +640 -0
- package/dist/ui/accountsApi.js +48 -0
- package/dist/ui/modelsApi.js +47 -0
- package/dist/utils/claude-config.js +185 -0
- package/dist/utils/codex-config.js +62 -0
- package/dist/utils/network.js +16 -0
- package/dist/utils/platform.js +13 -0
- package/dist/utils/self-update.js +239 -0
- package/dist/utils/telemetry.js +88 -0
- package/dist/utils/token-extractor.js +95 -0
- package/dist/utils/token-validator.js +26 -0
- package/docker-compose.yml +63 -0
- package/litellm-config.yaml +44 -0
- package/package.json +69 -0
- package/src/interceptor/addon.py +78 -0
|
@@ -0,0 +1,501 @@
|
|
|
1
|
+
import { select, input, confirm, password } from "@inquirer/prompts";
|
|
2
|
+
import chalk from "chalk";
|
|
3
|
+
import { detectPlatform, isMacos } from "../utils/platform.js";
|
|
4
|
+
import { extractFromKeychain, extractFromCredentialsFile, formatExpiry, redactToken, } from "../utils/token-extractor.js";
|
|
5
|
+
import { validateToken } from "../utils/token-validator.js";
|
|
6
|
+
import { writeClaudeSettings, readClaudeProxySettings } from "../utils/claude-config.js";
|
|
7
|
+
import { saveAccounts } from "../proxy/token-refresher.js";
|
|
8
|
+
import { loadAccounts, accountsFileExists, readConfig, writeConfig, generateProxySecret } from "../config/manager.js";
|
|
9
|
+
import { PROXY_PORT } from "../config/paths.js";
|
|
10
|
+
import { DEFAULT_RATE_LIMITS, ACCOUNT_USER_DEFAULTS } from "../proxy/types.js";
|
|
11
|
+
import { existsSync } from "fs";
|
|
12
|
+
import { checkMitmproxyInstalled, isCaCertInstalled, generateCaCert, installCaCert, writeAddonScript, getNetworkExtensionStatus, openNetworkExtensionSettings, } from "../interceptor/mitmproxy-manager.js";
|
|
13
|
+
import { printDesktopSupportExplainer, printNetworkExtensionInstructions } from "./cmd-client.js";
|
|
14
|
+
import { trackEvent } from "../utils/telemetry.js";
|
|
15
|
+
// ─── Public registration ──────────────────────────────────────────────────────
|
|
16
|
+
export function registerSetup(program) {
|
|
17
|
+
program
|
|
18
|
+
.command("setup")
|
|
19
|
+
.description("Interactive wizard: extract tokens and configure Claude Code automatically")
|
|
20
|
+
.option("--add", "Add a new account to an existing configuration (skip intro questions)")
|
|
21
|
+
.action(async (opts) => {
|
|
22
|
+
await runSetupWizard({ addMode: opts.add ?? false });
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
// ─── Shared single-account setup (also used by `accounts add`) ───────────────
|
|
26
|
+
export async function setupSingleAccount(index) {
|
|
27
|
+
const choices = [];
|
|
28
|
+
if (isMacos()) {
|
|
29
|
+
choices.push({ name: "Extract automatically from macOS Keychain (recommended)", value: "keychain" });
|
|
30
|
+
}
|
|
31
|
+
choices.push({ name: "Read from ~/.claude/.credentials.json", value: "credentials" });
|
|
32
|
+
choices.push({ name: "Paste tokens manually", value: "manual" });
|
|
33
|
+
const method = await select({
|
|
34
|
+
message: "How do you want to add the tokens?",
|
|
35
|
+
choices,
|
|
36
|
+
});
|
|
37
|
+
let tokens = null;
|
|
38
|
+
if (method === "keychain") {
|
|
39
|
+
process.stdout.write(chalk.gray(" Extracting from Keychain... "));
|
|
40
|
+
tokens = await extractFromKeychain();
|
|
41
|
+
if (tokens) {
|
|
42
|
+
console.log(chalk.green("✓"));
|
|
43
|
+
console.log(chalk.gray(` Token: ${redactToken(tokens.accessToken)}`));
|
|
44
|
+
console.log(chalk.gray(` Expiry: ${formatExpiry(tokens.expiresAt)}`));
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
console.log(chalk.red("✗"));
|
|
48
|
+
console.log(chalk.yellow(" Could not find credentials in Keychain."));
|
|
49
|
+
console.log(chalk.gray(" Make sure Claude Code is logged in: run `claude login` first."));
|
|
50
|
+
const retry = await confirm({ message: "Try another extraction method?", default: true });
|
|
51
|
+
if (!retry)
|
|
52
|
+
return null;
|
|
53
|
+
return setupSingleAccount(index);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
if (method === "credentials") {
|
|
57
|
+
tokens = extractFromCredentialsFile();
|
|
58
|
+
if (tokens) {
|
|
59
|
+
console.log(chalk.green(` ✓ Found credentials in ~/.claude/.credentials.json`));
|
|
60
|
+
console.log(chalk.gray(` Token: ${redactToken(tokens.accessToken)}`));
|
|
61
|
+
console.log(chalk.gray(` Expiry: ${formatExpiry(tokens.expiresAt)}`));
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
console.log(chalk.red(" ✗ ~/.claude/.credentials.json not found or unreadable."));
|
|
65
|
+
console.log(chalk.gray(" Make sure Claude Code is installed and you've run `claude login`."));
|
|
66
|
+
const retry = await confirm({ message: "Paste tokens manually instead?", default: true });
|
|
67
|
+
if (!retry)
|
|
68
|
+
return null;
|
|
69
|
+
tokens = await promptManualTokens();
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
if (method === "manual") {
|
|
73
|
+
tokens = await promptManualTokens();
|
|
74
|
+
}
|
|
75
|
+
if (!tokens)
|
|
76
|
+
return null;
|
|
77
|
+
const defaultId = `max-account-${index}`;
|
|
78
|
+
const accountId = await input({
|
|
79
|
+
message: "Account ID (press Enter to accept default):",
|
|
80
|
+
default: defaultId,
|
|
81
|
+
validate: (v) => /^[a-zA-Z0-9_-]+$/.test(v) || "Only letters, numbers, _ and - allowed",
|
|
82
|
+
});
|
|
83
|
+
process.stdout.write(chalk.gray(" Validating tokens against Anthropic... "));
|
|
84
|
+
const validation = await validateToken(tokens.accessToken);
|
|
85
|
+
if (validation.valid) {
|
|
86
|
+
console.log(chalk.green("✓ Valid"));
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
console.log(chalk.red("✗ Invalid"));
|
|
90
|
+
console.log(chalk.yellow(` Reason: ${validation.reason}`));
|
|
91
|
+
console.log(chalk.gray(" The token will be saved but may not work until refreshed."));
|
|
92
|
+
const keepAnyway = await confirm({ message: "Save this account anyway?", default: false });
|
|
93
|
+
if (!keepAnyway)
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
return {
|
|
97
|
+
id: accountId,
|
|
98
|
+
tokens,
|
|
99
|
+
healthy: validation.valid,
|
|
100
|
+
busy: false,
|
|
101
|
+
requestCount: 0,
|
|
102
|
+
errorCount: 0,
|
|
103
|
+
lastUsed: 0,
|
|
104
|
+
lastRefresh: 0,
|
|
105
|
+
consecutiveErrors: 0,
|
|
106
|
+
rateLimits: { ...DEFAULT_RATE_LIMITS },
|
|
107
|
+
...ACCOUNT_USER_DEFAULTS,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
// ─── Full wizard ──────────────────────────────────────────────────────────────
|
|
111
|
+
export async function runSetupWizard({ addMode }) {
|
|
112
|
+
const platform = detectPlatform();
|
|
113
|
+
const hasExisting = accountsFileExists();
|
|
114
|
+
const existingClient = readConfig().client;
|
|
115
|
+
printBanner();
|
|
116
|
+
console.log(chalk.gray(`Platform: ${platform}\n`));
|
|
117
|
+
// ── Mode selection (only when nothing is configured yet) ─────────────────
|
|
118
|
+
// If there are no accounts and no existing client config, ask whether the
|
|
119
|
+
// user wants to host cc-router (server mode) or connect to an existing one
|
|
120
|
+
// (client mode). In client mode we skip account setup entirely.
|
|
121
|
+
if (!hasExisting && !existingClient && !addMode) {
|
|
122
|
+
const mode = await select({
|
|
123
|
+
message: "What do you want to do?",
|
|
124
|
+
choices: [
|
|
125
|
+
{
|
|
126
|
+
name: "Host CC-Router on this machine (manage tokens and accounts here)",
|
|
127
|
+
value: "server",
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
name: "Connect to an existing CC-Router server (client mode)",
|
|
131
|
+
value: "client",
|
|
132
|
+
},
|
|
133
|
+
],
|
|
134
|
+
});
|
|
135
|
+
if (mode === "client") {
|
|
136
|
+
await runClientSetupFromWizard();
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
if (hasExisting && !addMode) {
|
|
141
|
+
const existing = loadAccounts();
|
|
142
|
+
console.log(chalk.yellow(` Found ${existing.length} existing account(s).\n`));
|
|
143
|
+
const action = await select({
|
|
144
|
+
message: "What do you want to do?",
|
|
145
|
+
choices: [
|
|
146
|
+
{ name: "Add more accounts to the existing configuration", value: "add" },
|
|
147
|
+
{ name: "Start fresh (replace all accounts)", value: "replace" },
|
|
148
|
+
{ name: "Cancel", value: "cancel" },
|
|
149
|
+
],
|
|
150
|
+
});
|
|
151
|
+
if (action === "cancel") {
|
|
152
|
+
console.log(chalk.gray("\nCancelled.\n"));
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
if (action === "replace") {
|
|
156
|
+
const sure = await confirm({
|
|
157
|
+
message: chalk.red("This will delete all existing accounts. Are you sure?"),
|
|
158
|
+
default: false,
|
|
159
|
+
});
|
|
160
|
+
if (!sure) {
|
|
161
|
+
console.log(chalk.gray("\nCancelled.\n"));
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
if (!addMode && isMacos()) {
|
|
167
|
+
console.log(chalk.cyan(" Tip: to add multiple accounts, you need to:"));
|
|
168
|
+
console.log(chalk.gray(" 1. Log in to Claude Code with account 1 (already done if you use CC normally)"));
|
|
169
|
+
console.log(chalk.gray(" 2. Extract tokens → log out → log in with account 2 → extract → repeat\n"));
|
|
170
|
+
}
|
|
171
|
+
let numAccounts = 1;
|
|
172
|
+
if (!addMode) {
|
|
173
|
+
const { number } = await import("@inquirer/prompts");
|
|
174
|
+
numAccounts = await number({
|
|
175
|
+
message: "How many accounts do you want to configure now?",
|
|
176
|
+
default: 1,
|
|
177
|
+
min: 1,
|
|
178
|
+
max: 20,
|
|
179
|
+
}) ?? 1;
|
|
180
|
+
}
|
|
181
|
+
const newAccounts = [];
|
|
182
|
+
for (let i = 0; i < numAccounts; i++) {
|
|
183
|
+
const label = numAccounts > 1 ? `${i + 1}/${numAccounts}` : "";
|
|
184
|
+
console.log(chalk.bold(`\n${"━".repeat(40)}\n Account ${label}\n${"━".repeat(40)}\n`));
|
|
185
|
+
if (i > 0 && isMacos()) {
|
|
186
|
+
console.log(chalk.yellow(` Before extracting account ${i + 1}:\n` +
|
|
187
|
+
` 1. Run: ${chalk.white("claude logout")}\n` +
|
|
188
|
+
` 2. Run: ${chalk.white("claude login")} (log in with your next Max account)\n`));
|
|
189
|
+
await confirm({ message: "Ready?", default: true });
|
|
190
|
+
}
|
|
191
|
+
const existingCount = hasExisting ? loadAccounts().length : 0;
|
|
192
|
+
const account = await setupSingleAccount(i + 1 + existingCount);
|
|
193
|
+
if (account) {
|
|
194
|
+
newAccounts.push(account);
|
|
195
|
+
console.log(chalk.green(`\n ✓ Account "${account.id}" ready.\n`));
|
|
196
|
+
}
|
|
197
|
+
else {
|
|
198
|
+
console.log(chalk.yellow(` ↷ Skipped account ${i + 1}.\n`));
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
if (newAccounts.length === 0) {
|
|
202
|
+
console.log(chalk.red("\n✗ No accounts configured. Run cc-router setup again.\n"));
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
// Merge: existing accounts minus any overwritten by ID, plus new ones
|
|
206
|
+
const existingAccounts = (hasExisting && !addMode) ? [] : (hasExisting ? loadAccounts() : []);
|
|
207
|
+
const merged = [
|
|
208
|
+
...existingAccounts.filter(a => !newAccounts.some(n => n.id === a.id)),
|
|
209
|
+
...newAccounts,
|
|
210
|
+
];
|
|
211
|
+
console.log(chalk.bold(`\n${"━".repeat(40)}\n Saving\n${"━".repeat(40)}\n`));
|
|
212
|
+
saveAccounts(merged);
|
|
213
|
+
console.log(chalk.green(` ✓ ${merged.length} account(s) saved to ~/.cc-router/accounts.json`));
|
|
214
|
+
void trackEvent("setup_completed", { account_count: merged.length });
|
|
215
|
+
// ─── Post-setup interactive flow ─────────────────────────────────────────
|
|
216
|
+
await runPostSetupFlow(merged.length);
|
|
217
|
+
}
|
|
218
|
+
// ─── Post-setup interactive flow ─────────────────────────────────────────────
|
|
219
|
+
async function runPostSetupFlow(accountCount) {
|
|
220
|
+
console.log(chalk.bold(`\n${"━".repeat(40)}\n Configure this machine\n${"━".repeat(40)}\n`));
|
|
221
|
+
// 1. Configure Claude Code on this machine
|
|
222
|
+
const currentSettings = readClaudeProxySettings();
|
|
223
|
+
const alreadyConfigured = currentSettings.baseUrl?.includes("localhost");
|
|
224
|
+
const configureLocal = await confirm({
|
|
225
|
+
message: alreadyConfigured
|
|
226
|
+
? `Claude Code is already pointing to ${currentSettings.baseUrl}. Reconfigure?`
|
|
227
|
+
: "Configure Claude Code on this machine to use the proxy?",
|
|
228
|
+
default: true,
|
|
229
|
+
});
|
|
230
|
+
if (configureLocal) {
|
|
231
|
+
// Ask if this is a local proxy or a remote one
|
|
232
|
+
const proxyLocation = await select({
|
|
233
|
+
message: "Where will cc-router run?",
|
|
234
|
+
choices: [
|
|
235
|
+
{ name: `On this machine (localhost:${PROXY_PORT})`, value: "local" },
|
|
236
|
+
{ name: "On another machine / VPS (I'll enter the address)", value: "remote" },
|
|
237
|
+
],
|
|
238
|
+
});
|
|
239
|
+
let proxyHost = `http://localhost:${PROXY_PORT}`;
|
|
240
|
+
if (proxyLocation === "remote") {
|
|
241
|
+
const remoteHost = await input({
|
|
242
|
+
message: "Proxy URL (e.g. http://192.168.1.50:3456 or https://cc-router.example.com):",
|
|
243
|
+
validate: (v) => {
|
|
244
|
+
try {
|
|
245
|
+
new URL(v);
|
|
246
|
+
return true;
|
|
247
|
+
}
|
|
248
|
+
catch {
|
|
249
|
+
return "Enter a valid URL (http:// or https://)";
|
|
250
|
+
}
|
|
251
|
+
},
|
|
252
|
+
});
|
|
253
|
+
proxyHost = remoteHost.replace(/\/$/, ""); // strip trailing slash
|
|
254
|
+
}
|
|
255
|
+
const port = proxyLocation === "local"
|
|
256
|
+
? PROXY_PORT
|
|
257
|
+
: parseInt(new URL(proxyHost).port || "80", 10);
|
|
258
|
+
// ── Password setup for remote proxy ───────────────────────────────────────
|
|
259
|
+
if (proxyLocation === "remote") {
|
|
260
|
+
const pwChoice = await select({
|
|
261
|
+
message: "Set a proxy password? (strongly recommended for internet-exposed proxies)",
|
|
262
|
+
choices: [
|
|
263
|
+
{ name: "Generate automatically (recommended)", value: "generate" },
|
|
264
|
+
{ name: "Enter my own password", value: "manual" },
|
|
265
|
+
{ name: "Skip — no password protection", value: "skip" },
|
|
266
|
+
],
|
|
267
|
+
});
|
|
268
|
+
let chosenSecret;
|
|
269
|
+
if (pwChoice === "generate") {
|
|
270
|
+
chosenSecret = generateProxySecret();
|
|
271
|
+
writeConfig({ ...readConfig(), proxySecret: chosenSecret });
|
|
272
|
+
}
|
|
273
|
+
else if (pwChoice === "manual") {
|
|
274
|
+
const raw = await password({
|
|
275
|
+
message: "Enter proxy password:",
|
|
276
|
+
validate: (v) => v.trim().length >= 8 || "Minimum 8 characters",
|
|
277
|
+
});
|
|
278
|
+
chosenSecret = raw.trim();
|
|
279
|
+
writeConfig({ ...readConfig(), proxySecret: chosenSecret });
|
|
280
|
+
}
|
|
281
|
+
writeClaudeSettings(port, proxyHost);
|
|
282
|
+
if (chosenSecret) {
|
|
283
|
+
console.log(chalk.yellow("\n *** Save this password — you cannot recover it later ***"));
|
|
284
|
+
console.log(" " + chalk.bold(chosenSecret));
|
|
285
|
+
console.log(chalk.gray(" Claude Code has been configured to use it automatically."));
|
|
286
|
+
console.log(chalk.gray(" Other machines: cc-router configure --set-password <value>"));
|
|
287
|
+
}
|
|
288
|
+
else {
|
|
289
|
+
console.log(chalk.green(`\n ✓ ~/.claude/settings.json updated`));
|
|
290
|
+
console.log(chalk.gray(` ANTHROPIC_BASE_URL = ${proxyHost}`));
|
|
291
|
+
console.log(chalk.gray(` ANTHROPIC_AUTH_TOKEN = proxy-managed`));
|
|
292
|
+
}
|
|
293
|
+
console.log(chalk.cyan(`\n On the remote machine, start cc-router with:`));
|
|
294
|
+
console.log(chalk.white(` HOST=0.0.0.0 cc-router start`));
|
|
295
|
+
console.log(chalk.cyan(` Or as a service:`));
|
|
296
|
+
console.log(chalk.white(` cc-router service install\n`));
|
|
297
|
+
// Nothing more to do on this machine
|
|
298
|
+
printDone(accountCount);
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
writeClaudeSettings(port, proxyHost);
|
|
302
|
+
console.log(chalk.green(`\n ✓ ~/.claude/settings.json updated`));
|
|
303
|
+
console.log(chalk.gray(` ANTHROPIC_BASE_URL = ${proxyHost}`));
|
|
304
|
+
console.log(chalk.gray(` ANTHROPIC_AUTH_TOKEN = proxy-managed`));
|
|
305
|
+
}
|
|
306
|
+
printDone(accountCount);
|
|
307
|
+
}
|
|
308
|
+
// ─── Done banner ──────────────────────────────────────────────────────────────
|
|
309
|
+
function printDone(accountCount) {
|
|
310
|
+
console.log(chalk.bold(`\n${"━".repeat(40)}\n All done — ${accountCount} account(s) ready\n${"━".repeat(40)}\n`));
|
|
311
|
+
console.log(` Start the proxy: ${chalk.cyan("cc-router start")}`);
|
|
312
|
+
console.log(` Add more accounts: ${chalk.cyan("cc-router setup --add")}`);
|
|
313
|
+
console.log(` Dashboard: ${chalk.cyan("cc-router status")}\n`);
|
|
314
|
+
}
|
|
315
|
+
// ─── Manual token input ───────────────────────────────────────────────────────
|
|
316
|
+
async function promptManualTokens() {
|
|
317
|
+
console.log(chalk.gray("\n You can find your tokens by running:\n" +
|
|
318
|
+
" macOS: security find-generic-password -s 'Claude Code-credentials' -w\n" +
|
|
319
|
+
" Linux/Windows: cat ~/.claude/.credentials.json\n"));
|
|
320
|
+
const accessToken = await password({
|
|
321
|
+
message: "Paste accessToken (sk-ant-oat01-...):",
|
|
322
|
+
mask: "•",
|
|
323
|
+
validate: (v) => v.startsWith("sk-ant-oat01-") || v.startsWith("sk-ant-")
|
|
324
|
+
? true
|
|
325
|
+
: "Must start with sk-ant-oat01-",
|
|
326
|
+
});
|
|
327
|
+
const refreshToken = await password({
|
|
328
|
+
message: "Paste refreshToken (sk-ant-ort01-...):",
|
|
329
|
+
mask: "•",
|
|
330
|
+
validate: (v) => v.startsWith("sk-ant-ort01-") || v.startsWith("sk-ant-")
|
|
331
|
+
? true
|
|
332
|
+
: "Must start with sk-ant-ort01-",
|
|
333
|
+
});
|
|
334
|
+
const useDefaultExpiry = await confirm({
|
|
335
|
+
message: "Use default expiry (8 hours from now)?",
|
|
336
|
+
default: true,
|
|
337
|
+
});
|
|
338
|
+
const expiresAt = useDefaultExpiry
|
|
339
|
+
? Date.now() + 8 * 60 * 60 * 1000
|
|
340
|
+
: new Date(await input({ message: "Paste expiresAt (ISO date or ms timestamp):" })).getTime();
|
|
341
|
+
return {
|
|
342
|
+
accessToken,
|
|
343
|
+
refreshToken,
|
|
344
|
+
expiresAt,
|
|
345
|
+
scopes: ["user:inference", "user:profile"],
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
// ─── Client-mode setup (from wizard) ─────────────────────────────────────────
|
|
349
|
+
async function runClientSetupFromWizard() {
|
|
350
|
+
console.log(chalk.bold("\n🔗 Client Mode — Connect to a CC-Router server\n"));
|
|
351
|
+
const rawUrl = await input({
|
|
352
|
+
message: "CC-Router server URL (e.g. 192.168.1.50:3456):",
|
|
353
|
+
});
|
|
354
|
+
let url = rawUrl.trim().replace(/\/+$/, "");
|
|
355
|
+
if (!url.startsWith("http://") && !url.startsWith("https://"))
|
|
356
|
+
url = `http://${url}`;
|
|
357
|
+
const secret = (await input({
|
|
358
|
+
message: "Proxy secret (leave empty if none):",
|
|
359
|
+
transformer: (v) => (v ? "•".repeat(v.length) : ""),
|
|
360
|
+
})) || undefined;
|
|
361
|
+
// Test connection
|
|
362
|
+
console.log(chalk.gray(`\nTesting connection to ${url}...`));
|
|
363
|
+
let accounts;
|
|
364
|
+
try {
|
|
365
|
+
const headers = {};
|
|
366
|
+
if (secret)
|
|
367
|
+
headers["authorization"] = `Bearer ${secret}`;
|
|
368
|
+
const res = await fetch(`${url}/cc-router/health`, {
|
|
369
|
+
headers,
|
|
370
|
+
signal: AbortSignal.timeout(8_000),
|
|
371
|
+
});
|
|
372
|
+
if (!res.ok)
|
|
373
|
+
throw new Error(`HTTP ${res.status}`);
|
|
374
|
+
const data = (await res.json());
|
|
375
|
+
accounts = data.accounts?.length;
|
|
376
|
+
console.log(chalk.green(`✓ Connected — ${accounts ?? "?"} accounts on server\n`));
|
|
377
|
+
}
|
|
378
|
+
catch (e) {
|
|
379
|
+
console.error(chalk.red(`\n✗ Cannot reach CC-Router at ${url}`));
|
|
380
|
+
console.error(chalk.yellow(` Error: ${e.message}`));
|
|
381
|
+
console.error(chalk.gray(" Make sure the server is running and the URL is correct.\n"));
|
|
382
|
+
process.exit(1);
|
|
383
|
+
}
|
|
384
|
+
// Save config
|
|
385
|
+
const clientCfg = { remoteUrl: url };
|
|
386
|
+
if (secret)
|
|
387
|
+
clientCfg.remoteSecret = secret;
|
|
388
|
+
writeConfig({ ...readConfig(), client: clientCfg });
|
|
389
|
+
// Configure Claude Code
|
|
390
|
+
writeClaudeSettings(0, url, secret ?? "proxy-managed");
|
|
391
|
+
console.log(chalk.green("✓ Claude Code configured"));
|
|
392
|
+
console.log(chalk.gray(` ANTHROPIC_BASE_URL → ${url}\n`));
|
|
393
|
+
// ── Claude Desktop (Cowork / Agent mode) ─────────────────────────────────
|
|
394
|
+
const desktopInstalled = isMacos() && existsSync("/Applications/Claude.app");
|
|
395
|
+
if (desktopInstalled) {
|
|
396
|
+
printDesktopSupportExplainer();
|
|
397
|
+
const wantsDesktop = await confirm({
|
|
398
|
+
message: "Route Claude Desktop's Cowork / Agent-mode traffic through CC-Router?",
|
|
399
|
+
default: false,
|
|
400
|
+
});
|
|
401
|
+
if (wantsDesktop) {
|
|
402
|
+
await setupDesktopFromWizard(url, secret);
|
|
403
|
+
const current = readConfig();
|
|
404
|
+
if (current.client) {
|
|
405
|
+
current.client = { ...current.client, desktopEnabled: true };
|
|
406
|
+
writeConfig(current);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
console.log(chalk.bold.green(`\n${"━".repeat(40)}\n Client mode active\n${"━".repeat(40)}\n`));
|
|
411
|
+
console.log(` Check status: ${chalk.cyan("cc-router client status")}`);
|
|
412
|
+
console.log(` Disconnect: ${chalk.cyan("cc-router client disconnect")}`);
|
|
413
|
+
if (readConfig().client?.desktopEnabled) {
|
|
414
|
+
console.log(` Start Desktop: ${chalk.cyan("cc-router client start-desktop")}`);
|
|
415
|
+
}
|
|
416
|
+
console.log();
|
|
417
|
+
}
|
|
418
|
+
async function setupDesktopFromWizard(target, secret) {
|
|
419
|
+
console.log(chalk.bold("\n🖥 Claude Desktop — Cowork / Agent Setup\n"));
|
|
420
|
+
// 1. Check mitmproxy
|
|
421
|
+
if (!(await checkMitmproxyInstalled())) {
|
|
422
|
+
console.log(chalk.yellow("mitmproxy is required but not installed."));
|
|
423
|
+
if (isMacos()) {
|
|
424
|
+
console.log(chalk.cyan(" Install: brew install mitmproxy\n"));
|
|
425
|
+
}
|
|
426
|
+
else {
|
|
427
|
+
console.log(chalk.cyan(" Install: pip install mitmproxy\n"));
|
|
428
|
+
}
|
|
429
|
+
const proceed = await confirm({ message: "Have you installed mitmproxy now?", default: false });
|
|
430
|
+
if (!proceed || !(await checkMitmproxyInstalled())) {
|
|
431
|
+
console.log(chalk.red("Skipping Desktop setup. Re-run with: cc-router client start-desktop\n"));
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
console.log(chalk.green("✓ mitmproxy found"));
|
|
436
|
+
// 2. CA cert
|
|
437
|
+
if (!isCaCertInstalled()) {
|
|
438
|
+
console.log(chalk.gray("Generating mitmproxy CA certificate (one-time)..."));
|
|
439
|
+
try {
|
|
440
|
+
await generateCaCert();
|
|
441
|
+
console.log(chalk.green("✓ CA certificate generated"));
|
|
442
|
+
}
|
|
443
|
+
catch (e) {
|
|
444
|
+
console.log(chalk.red(`✗ CA generation failed: ${e.message}`));
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
else {
|
|
449
|
+
console.log(chalk.green("✓ CA certificate already present"));
|
|
450
|
+
}
|
|
451
|
+
console.log(chalk.yellow("\nThe CA certificate must be installed in your OS trust store (requires admin)."));
|
|
452
|
+
const doInstall = await confirm({ message: "Install CA certificate now?", default: true });
|
|
453
|
+
if (doInstall) {
|
|
454
|
+
const ok = await installCaCert();
|
|
455
|
+
if (ok) {
|
|
456
|
+
console.log(chalk.green("✓ CA certificate installed in system trust store"));
|
|
457
|
+
}
|
|
458
|
+
else {
|
|
459
|
+
console.log(chalk.red("✗ CA install failed."));
|
|
460
|
+
console.log(chalk.gray(" Install manually: sudo security add-trusted-cert -d -r trustRoot \\"));
|
|
461
|
+
console.log(chalk.gray(" -k /Library/Keychains/System.keychain ~/.mitmproxy/mitmproxy-ca-cert.pem"));
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
// 3. Addon (with secret so intercepted requests authenticate against the proxy)
|
|
465
|
+
writeAddonScript(target, secret);
|
|
466
|
+
console.log(chalk.green("✓ Redirect addon configured"));
|
|
467
|
+
// 4. Network Extension walkthrough (macOS)
|
|
468
|
+
if (isMacos()) {
|
|
469
|
+
printNetworkExtensionInstructions();
|
|
470
|
+
const status = await getNetworkExtensionStatus();
|
|
471
|
+
if (status === "not_installed") {
|
|
472
|
+
console.log(chalk.gray(" The extension will be installed on first `cc-router client start-desktop`.\n" +
|
|
473
|
+
" macOS will show a popup — follow the steps above to approve it.\n"));
|
|
474
|
+
}
|
|
475
|
+
else if (status === "waiting") {
|
|
476
|
+
console.log(chalk.red(" ⚠ Extension is installed but NOT approved.\n"));
|
|
477
|
+
const openNow = await confirm({ message: "Open System Settings to approve it now?", default: true });
|
|
478
|
+
if (openNow) {
|
|
479
|
+
await openNetworkExtensionSettings();
|
|
480
|
+
console.log(chalk.gray(" System Settings should be open. Toggle 'Mitmproxy Redirector' ON.\n"));
|
|
481
|
+
await confirm({ message: "Done? Press Enter when the toggle is ON", default: true });
|
|
482
|
+
const newStatus = await getNetworkExtensionStatus();
|
|
483
|
+
console.log(newStatus === "enabled"
|
|
484
|
+
? chalk.green(" ✓ Network Extension enabled")
|
|
485
|
+
: chalk.yellow(` Still not enabled (status: ${newStatus}) — you can fix later`));
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
else if (status === "enabled") {
|
|
489
|
+
console.log(chalk.green(" ✓ Network Extension already enabled — you're all set\n"));
|
|
490
|
+
}
|
|
491
|
+
// Remind to restart Claude Desktop
|
|
492
|
+
console.log(chalk.bold.yellow(" Remember:"));
|
|
493
|
+
console.log(chalk.gray(" After starting the interceptor, " + chalk.bold("quit and relaunch Claude Desktop") + " (⌘Q)"));
|
|
494
|
+
console.log(chalk.gray(" so mitmproxy can hook into the new process.\n"));
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
function printBanner() {
|
|
498
|
+
console.log(chalk.cyan("\n╔══════════════════════════════════════════╗\n" +
|
|
499
|
+
"║ CC-Router — Setup ║\n" +
|
|
500
|
+
"╚══════════════════════════════════════════╝\n"));
|
|
501
|
+
}
|