@timo972/cc-router 0.12.1 → 0.12.2-rc.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 +18 -0
- package/Dockerfile +1 -0
- package/README.md +1 -1
- package/dist/cli/cmd-accounts.js +116 -65
- package/dist/cli/cmd-setup.js +100 -30
- package/dist/cli/cmd-status.js +14 -2
- package/dist/cli/cmd-telemetry.js +43 -32
- package/dist/cli/index.js +15 -1
- package/dist/config/directory.js +14 -0
- package/dist/config/telemetry.js +192 -41
- package/dist/providers/anthropic/usage-refresher.js +35 -1
- package/dist/providers/model-discovery.js +17 -11
- package/dist/providers/openai/device-oauth.js +88 -31
- package/dist/providers/openai/token-refresher.js +12 -2
- package/dist/providers/openai/usage-fetch.js +19 -2
- package/dist/proxy/anthropic-messages-route.js +144 -5
- package/dist/proxy/anthropic-proxy.js +10 -0
- package/dist/proxy/anthropic-response-capture.js +6 -19
- package/dist/proxy/openai-ingress.js +98 -1
- package/dist/proxy/server.js +35 -22
- package/dist/proxy/token-refresher.js +12 -2
- package/dist/proxy/usage-capture.js +41 -4
- package/dist/telemetry/contracts.js +129 -0
- package/dist/telemetry/facade.js +654 -0
- package/dist/telemetry/otel-exporters.js +289 -0
- package/dist/telemetry/posthog-client.js +398 -0
- package/dist/telemetry/privacy.js +567 -0
- package/dist/telemetry/runtime.js +306 -0
- package/dist/telemetry/setup-diagnostics.js +239 -0
- package/dist/utils/token-extractor.js +79 -11
- package/dist/utils/token-validator.js +25 -9
- package/docs/README.md +34 -0
- package/docs/telemetry.md +167 -0
- package/package.json +12 -2
- package/dist/utils/telemetry.js +0 -88
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,24 @@ This project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
8
8
|
|
|
9
9
|
## [Unreleased]
|
|
10
10
|
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- Privacy-bounded OpenTelemetry and PostHog EU telemetry: 10%-sampled proxy
|
|
14
|
+
traces, closed-schema setup and runtime diagnostics, lifecycle events, and
|
|
15
|
+
sanitized exceptions with diagnostic IDs. Every outbound record is rebuilt
|
|
16
|
+
from an allowlist immediately before export; prompts, bodies, headers,
|
|
17
|
+
URLs, account identifiers, tokens, and raw error messages are never sent.
|
|
18
|
+
Fresh installations default on; `cc-router telemetry off`, `DO_NOT_TRACK=1`,
|
|
19
|
+
and `CC_ROUTER_TELEMETRY=0` disable every signal, and a running daemon stops
|
|
20
|
+
exporting as soon as it observes an explicit opt-out. See
|
|
21
|
+
[docs/telemetry.md](docs/telemetry.md) for the complete inventory.
|
|
22
|
+
|
|
23
|
+
### Changed
|
|
24
|
+
|
|
25
|
+
- Aptabase telemetry has been removed. An existing persisted opt-out remains
|
|
26
|
+
off after upgrade; no PostHog Person profiles are created and GeoIP
|
|
27
|
+
enrichment is disabled.
|
|
28
|
+
|
|
11
29
|
---
|
|
12
30
|
|
|
13
31
|
## [0.12.1] — 2026-09-14
|
package/Dockerfile
CHANGED
package/README.md
CHANGED
|
@@ -125,7 +125,7 @@ Per-platform token extraction, Codex CLI, Docker and everything else lives in
|
|
|
125
125
|
| [LiteLLM](docs/litellm-setup.md) | Optional logging and rate-limiting layer |
|
|
126
126
|
| [OAuth tokens](docs/oauth-tokens.md) | How subscription tokens and refresh rotation work |
|
|
127
127
|
| [Security](docs/security.md) | Token storage, proxy auth, threat model |
|
|
128
|
-
| [Telemetry](docs/telemetry.md) |
|
|
128
|
+
| [Telemetry](docs/telemetry.md) | Privacy-bounded telemetry, on by default: exactly what is sent and how to turn it off |
|
|
129
129
|
| [Troubleshooting](docs/troubleshooting.md) | When something doesn't connect |
|
|
130
130
|
|
|
131
131
|
## Disclaimer
|
package/dist/cli/cmd-accounts.js
CHANGED
|
@@ -8,6 +8,7 @@ import { loginOpenAIWithDeviceCode } from "../providers/openai/device-oauth.js";
|
|
|
8
8
|
import { importGrokCliAuth } from "../providers/xai/import-auth.js";
|
|
9
9
|
import { loginXaiWithDeviceCode } from "../providers/xai/device-oauth.js";
|
|
10
10
|
import { isValidAccountId } from "../proxy/account-rename.js";
|
|
11
|
+
import { createSetupAttempt, failAttemptFromError, withSetupTelemetryFlush, } from "../telemetry/setup-diagnostics.js";
|
|
11
12
|
export function registerAccounts(program) {
|
|
12
13
|
const accounts = program
|
|
13
14
|
.command("accounts")
|
|
@@ -118,9 +119,9 @@ export function registerAccounts(program) {
|
|
|
118
119
|
.command("add")
|
|
119
120
|
.description("Add a new Claude Max account interactively")
|
|
120
121
|
.action(async () => {
|
|
121
|
-
const {
|
|
122
|
+
const { setupSingleAccountWithAttempt } = await import("./cmd-setup.js");
|
|
122
123
|
const existing = accountsFileExists() ? loadAccounts() : [];
|
|
123
|
-
const account = await
|
|
124
|
+
const { account, attempt } = await setupSingleAccountWithAttempt(existing.length + 1);
|
|
124
125
|
if (!account) {
|
|
125
126
|
console.log(chalk.yellow("\nNo account added.\n"));
|
|
126
127
|
return;
|
|
@@ -130,10 +131,19 @@ export function registerAccounts(program) {
|
|
|
130
131
|
...existing.filter(a => a.id !== account.id),
|
|
131
132
|
account,
|
|
132
133
|
];
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
134
|
+
let mode;
|
|
135
|
+
try {
|
|
136
|
+
({ mode } = await addAccountRuntimeAware(serialize([account])[0], {
|
|
137
|
+
tryAddLive: tryAddAccountToRunningProxy,
|
|
138
|
+
addStored: () => saveAccounts(merged),
|
|
139
|
+
}));
|
|
140
|
+
}
|
|
141
|
+
catch (error) {
|
|
142
|
+
endFailedAttempt(attempt, error, "persistence");
|
|
143
|
+
throw error;
|
|
144
|
+
}
|
|
145
|
+
attempt.stageCompleted("persistence");
|
|
146
|
+
attempt.succeeded();
|
|
137
147
|
console.log(chalk.green(`\n✓ Account "${account.id}" added (${merged.length} total).\n`));
|
|
138
148
|
printAddOutcome(mode);
|
|
139
149
|
});
|
|
@@ -141,71 +151,101 @@ export function registerAccounts(program) {
|
|
|
141
151
|
accounts
|
|
142
152
|
.command("add-openai")
|
|
143
153
|
.description("Add an OpenAI ChatGPT/Codex subscription account manually")
|
|
144
|
-
.action(async () => {
|
|
154
|
+
.action(async () => withSetupTelemetryFlush(async () => {
|
|
145
155
|
const { input, password } = await import("@inquirer/prompts");
|
|
146
|
-
const
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
156
|
+
const attempt = createSetupAttempt({ provider: "openai", method: "manual_token" });
|
|
157
|
+
attempt.stageCompleted("credential_source_selection");
|
|
158
|
+
let reached = "credential_read";
|
|
159
|
+
try {
|
|
160
|
+
const id = await input({
|
|
161
|
+
message: "OpenAI account ID:",
|
|
162
|
+
default: `openai-account-${loadOpenAIAccounts().length + 1}`,
|
|
163
|
+
validate: (v) => /^[a-zA-Z0-9_-]+$/.test(v) || "Only letters, numbers, _ and - allowed",
|
|
164
|
+
});
|
|
165
|
+
const accessToken = await password({
|
|
166
|
+
message: "OpenAI access token:",
|
|
167
|
+
mask: "*",
|
|
168
|
+
validate: (v) => v.trim().length > 0 || "Access token is required",
|
|
169
|
+
});
|
|
170
|
+
const refreshToken = await password({
|
|
171
|
+
message: "OpenAI refresh token:",
|
|
172
|
+
mask: "*",
|
|
173
|
+
validate: (v) => v.trim().length > 0 || "Refresh token is required",
|
|
174
|
+
});
|
|
175
|
+
const expiresAt = await input({
|
|
176
|
+
message: "Access token expiry (Unix ms):",
|
|
177
|
+
default: String(Date.now() + 60 * 60 * 1000),
|
|
178
|
+
validate: (v) => Number.isFinite(Number(v)) && Number(v) > 0 || "Enter a positive Unix timestamp in milliseconds",
|
|
179
|
+
});
|
|
180
|
+
const scopes = await input({
|
|
181
|
+
message: "Scopes:",
|
|
182
|
+
default: "openid profile email offline_access",
|
|
183
|
+
});
|
|
184
|
+
attempt.stageCompleted("credential_read");
|
|
185
|
+
reached = "credential_parse";
|
|
186
|
+
const record = createOpenAIAccountRecord({
|
|
187
|
+
id,
|
|
188
|
+
accessToken,
|
|
189
|
+
refreshToken,
|
|
190
|
+
expiresAt,
|
|
191
|
+
scopes,
|
|
192
|
+
});
|
|
193
|
+
attempt.stageCompleted("credential_parse");
|
|
194
|
+
reached = "persistence";
|
|
195
|
+
const { mode } = await addAccountRuntimeAware(record);
|
|
196
|
+
attempt.stageCompleted("persistence");
|
|
197
|
+
attempt.succeeded();
|
|
198
|
+
console.log(chalk.green(`\n✓ OpenAI account "${record.id}" saved.\n`));
|
|
199
|
+
printAddOutcome(mode);
|
|
200
|
+
console.log(chalk.yellow(" Treat this as experimental until the OAuth login wizard lands.\n"));
|
|
201
|
+
}
|
|
202
|
+
catch (error) {
|
|
203
|
+
endFailedAttempt(attempt, error, reached);
|
|
204
|
+
throw error;
|
|
205
|
+
}
|
|
206
|
+
}));
|
|
182
207
|
// ── accounts login-openai ────────────────────────────────────────────────
|
|
183
208
|
accounts
|
|
184
209
|
.command("login-openai")
|
|
185
210
|
.description("Sign in to an OpenAI ChatGPT/Codex subscription account with device code")
|
|
186
|
-
.action(async () => {
|
|
211
|
+
.action(async () => withSetupTelemetryFlush(async () => {
|
|
187
212
|
const { input } = await import("@inquirer/prompts");
|
|
188
|
-
const
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
213
|
+
const attempt = createSetupAttempt({ provider: "openai", method: "device_oauth" });
|
|
214
|
+
let reached = "device_code_request";
|
|
215
|
+
try {
|
|
216
|
+
const accountId = await input({
|
|
217
|
+
message: "OpenAI account ID:",
|
|
218
|
+
default: `openai-account-${loadOpenAIAccounts().length + 1}`,
|
|
219
|
+
validate: (v) => /^[a-zA-Z0-9_-]+$/.test(v) || "Only letters, numbers, _ and - allowed",
|
|
220
|
+
});
|
|
221
|
+
console.log(chalk.cyan("\nOpenAI Codex device login"));
|
|
222
|
+
console.log(chalk.gray("This will open no local callback server. You will approve the login in your browser.\n"));
|
|
223
|
+
const record = await loginOpenAIWithDeviceCode({
|
|
224
|
+
accountId,
|
|
225
|
+
onDeviceCode: (code) => {
|
|
226
|
+
console.log(chalk.bold("1. Open this URL:"));
|
|
227
|
+
console.log(` ${chalk.cyan(code.verificationUrl)}`);
|
|
228
|
+
console.log(chalk.bold("2. Enter this code:"));
|
|
229
|
+
console.log(` ${chalk.cyan(code.userCode)}\n`);
|
|
230
|
+
console.log(chalk.gray("Waiting for authorization..."));
|
|
231
|
+
},
|
|
232
|
+
onStageCompleted: (stage) => {
|
|
233
|
+
attempt.stageCompleted(stage);
|
|
234
|
+
reached = stage;
|
|
235
|
+
},
|
|
236
|
+
});
|
|
237
|
+
reached = "persistence";
|
|
238
|
+
const { mode } = await addAccountRuntimeAware(record);
|
|
239
|
+
attempt.stageCompleted("persistence");
|
|
240
|
+
attempt.succeeded();
|
|
241
|
+
console.log(chalk.green(`\n✓ OpenAI account "${record.id}" saved via device login.\n`));
|
|
242
|
+
printAddOutcome(mode);
|
|
243
|
+
}
|
|
244
|
+
catch (error) {
|
|
245
|
+
endFailedAttempt(attempt, error, reached);
|
|
246
|
+
throw error;
|
|
247
|
+
}
|
|
248
|
+
}));
|
|
209
249
|
// ── accounts add-grok ────────────────────────────────────────────────────
|
|
210
250
|
accounts
|
|
211
251
|
.command("add-grok")
|
|
@@ -345,6 +385,17 @@ export function registerAccounts(program) {
|
|
|
345
385
|
});
|
|
346
386
|
}
|
|
347
387
|
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
388
|
+
/**
|
|
389
|
+
* Close a setup attempt that ended in a thrown error. A cancelled prompt is a
|
|
390
|
+
* user decision, not a failure, and only an unexpected failure gets a
|
|
391
|
+
* diagnostic ID worth quoting in a bug report.
|
|
392
|
+
*/
|
|
393
|
+
function endFailedAttempt(attempt, error, fallbackStage) {
|
|
394
|
+
const outcome = failAttemptFromError(attempt, error, fallbackStage);
|
|
395
|
+
if (outcome?.unexpected) {
|
|
396
|
+
console.log(chalk.gray(` Diagnostic ID: ${outcome.diagnosticId}`));
|
|
397
|
+
}
|
|
398
|
+
}
|
|
348
399
|
/** Tell the user whether the new account is already live or needs a restart. */
|
|
349
400
|
function printAddOutcome(mode) {
|
|
350
401
|
console.log(mode === "live"
|
package/dist/cli/cmd-setup.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { select, input, confirm, password } from "@inquirer/prompts";
|
|
2
2
|
import chalk from "chalk";
|
|
3
3
|
import { detectPlatform, isMacos } from "../utils/platform.js";
|
|
4
|
-
import {
|
|
4
|
+
import { extractFromKeychainDetailed, extractFromCredentialsFileDetailed, formatExpiry, redactToken, } from "../utils/token-extractor.js";
|
|
5
5
|
import { validateToken } from "../utils/token-validator.js";
|
|
6
6
|
import { writeClaudeSettings, readClaudeProxySettings } from "../utils/claude-config.js";
|
|
7
7
|
import { saveAccounts } from "../proxy/token-refresher.js";
|
|
@@ -11,7 +11,7 @@ import { DEFAULT_RATE_LIMITS, ACCOUNT_USER_DEFAULTS } from "../proxy/types.js";
|
|
|
11
11
|
import { existsSync } from "fs";
|
|
12
12
|
import { checkMitmproxyInstalled, isCaCertInstalled, generateCaCert, installCaCert, writeAddonScript, getNetworkExtensionStatus, openNetworkExtensionSettings, } from "../interceptor/mitmproxy-manager.js";
|
|
13
13
|
import { printDesktopSupportExplainer, printNetworkExtensionInstructions } from "./cmd-client.js";
|
|
14
|
-
import {
|
|
14
|
+
import { createSetupAttempt, failAttemptFromError, withSetupTelemetryFlush, } from "../telemetry/setup-diagnostics.js";
|
|
15
15
|
// ─── Public registration ──────────────────────────────────────────────────────
|
|
16
16
|
export function registerSetup(program) {
|
|
17
17
|
program
|
|
@@ -19,11 +19,21 @@ export function registerSetup(program) {
|
|
|
19
19
|
.description("Interactive wizard: extract tokens and configure Claude Code automatically")
|
|
20
20
|
.option("--add", "Add a new account to an existing configuration (skip intro questions)")
|
|
21
21
|
.action(async (opts) => {
|
|
22
|
-
await runSetupWizard({ addMode: opts.add ?? false });
|
|
22
|
+
await withSetupTelemetryFlush(() => runSetupWizard({ addMode: opts.add ?? false }));
|
|
23
23
|
});
|
|
24
24
|
}
|
|
25
|
+
/** Only an unexpected failure gets a diagnostic ID worth quoting in a bug report. */
|
|
26
|
+
function printDiagnosticId(outcome) {
|
|
27
|
+
if (!outcome.unexpected)
|
|
28
|
+
return;
|
|
29
|
+
console.log(chalk.gray(` Diagnostic ID: ${outcome.diagnosticId}`));
|
|
30
|
+
}
|
|
25
31
|
// ─── Shared single-account setup (also used by `accounts add`) ───────────────
|
|
26
|
-
|
|
32
|
+
/**
|
|
33
|
+
* Same step, but also hands back the setup attempt so the caller can mark the
|
|
34
|
+
* `persistence` stage and the final outcome once the account is written.
|
|
35
|
+
*/
|
|
36
|
+
export async function setupSingleAccountWithAttempt(index) {
|
|
27
37
|
const choices = [];
|
|
28
38
|
if (isMacos()) {
|
|
29
39
|
choices.push({ name: "Extract automatically from macOS Keychain (recommended)", value: "keychain" });
|
|
@@ -34,11 +44,35 @@ export async function setupSingleAccount(index) {
|
|
|
34
44
|
message: "How do you want to add the tokens?",
|
|
35
45
|
choices,
|
|
36
46
|
});
|
|
47
|
+
const attempt = createSetupAttempt({
|
|
48
|
+
provider: "anthropic",
|
|
49
|
+
method: method === "keychain"
|
|
50
|
+
? "macos_keychain"
|
|
51
|
+
: method === "credentials" ? "claude_credentials_file" : "manual_token",
|
|
52
|
+
});
|
|
53
|
+
attempt.stageCompleted("credential_source_selection");
|
|
54
|
+
// Shared by reference: the file-extraction fallback swaps in a manual-token attempt.
|
|
55
|
+
const current = { attempt };
|
|
56
|
+
let reached = "credential_read";
|
|
57
|
+
try {
|
|
58
|
+
return await collectAnthropicAccount(index, method, current, stage => { reached = stage; });
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
// A thrown prompt or extraction error must still close the funnel record.
|
|
62
|
+
const outcome = failAttemptFromError(current.attempt, error, reached);
|
|
63
|
+
if (outcome)
|
|
64
|
+
printDiagnosticId(outcome);
|
|
65
|
+
throw error;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
async function collectAnthropicAccount(index, method, current, reached) {
|
|
69
|
+
let attempt = current.attempt;
|
|
37
70
|
let tokens = null;
|
|
38
71
|
if (method === "keychain") {
|
|
39
72
|
process.stdout.write(chalk.gray(" Extracting from Keychain... "));
|
|
40
|
-
|
|
41
|
-
if (
|
|
73
|
+
const extraction = await extractFromKeychainDetailed();
|
|
74
|
+
if (extraction.ok) {
|
|
75
|
+
tokens = extraction.tokens;
|
|
42
76
|
console.log(chalk.green("✓"));
|
|
43
77
|
console.log(chalk.gray(` Token: ${redactToken(tokens.accessToken)}`));
|
|
44
78
|
console.log(chalk.gray(` Expiry: ${formatExpiry(tokens.expiresAt)}`));
|
|
@@ -47,15 +81,18 @@ export async function setupSingleAccount(index) {
|
|
|
47
81
|
console.log(chalk.red("✗"));
|
|
48
82
|
console.log(chalk.yellow(" Could not find credentials in Keychain."));
|
|
49
83
|
console.log(chalk.gray(" Make sure Claude Code is logged in: run `claude login` first."));
|
|
84
|
+
printDiagnosticId(attempt.stageFailed(extraction.error, "credential_read"));
|
|
50
85
|
const retry = await confirm({ message: "Try another extraction method?", default: true });
|
|
86
|
+
attempt.cancelled();
|
|
51
87
|
if (!retry)
|
|
52
|
-
return null;
|
|
53
|
-
return
|
|
88
|
+
return { account: null, attempt };
|
|
89
|
+
return setupSingleAccountWithAttempt(index);
|
|
54
90
|
}
|
|
55
91
|
}
|
|
56
92
|
if (method === "credentials") {
|
|
57
|
-
|
|
58
|
-
if (
|
|
93
|
+
const extraction = extractFromCredentialsFileDetailed();
|
|
94
|
+
if (extraction.ok) {
|
|
95
|
+
tokens = extraction.tokens;
|
|
59
96
|
console.log(chalk.green(` ✓ Found credentials in ~/.claude/.credentials.json`));
|
|
60
97
|
console.log(chalk.gray(` Token: ${redactToken(tokens.accessToken)}`));
|
|
61
98
|
console.log(chalk.gray(` Expiry: ${formatExpiry(tokens.expiresAt)}`));
|
|
@@ -64,16 +101,29 @@ export async function setupSingleAccount(index) {
|
|
|
64
101
|
console.log(chalk.red(" ✗ ~/.claude/.credentials.json not found or unreadable."));
|
|
65
102
|
console.log(chalk.gray(" Make sure Claude Code is installed and you've run `claude login`."));
|
|
66
103
|
const retry = await confirm({ message: "Paste tokens manually instead?", default: true });
|
|
67
|
-
if (!retry)
|
|
68
|
-
|
|
104
|
+
if (!retry) {
|
|
105
|
+
printDiagnosticId(attempt.stageFailed(extraction.error, "credential_read"));
|
|
106
|
+
attempt.cancelled();
|
|
107
|
+
return { account: null, attempt };
|
|
108
|
+
}
|
|
109
|
+
// The file-based attempt failed; the pasted tokens are a manual-token setup.
|
|
110
|
+
printDiagnosticId(attempt.failed(extraction.error, "credential_read"));
|
|
111
|
+
attempt = createSetupAttempt({ provider: "anthropic", method: "manual_token" });
|
|
112
|
+
current.attempt = attempt;
|
|
113
|
+
attempt.stageCompleted("credential_source_selection");
|
|
69
114
|
tokens = await promptManualTokens();
|
|
70
115
|
}
|
|
71
116
|
}
|
|
72
117
|
if (method === "manual") {
|
|
73
118
|
tokens = await promptManualTokens();
|
|
74
119
|
}
|
|
75
|
-
if (!tokens)
|
|
76
|
-
|
|
120
|
+
if (!tokens) {
|
|
121
|
+
attempt.cancelled();
|
|
122
|
+
return { account: null, attempt };
|
|
123
|
+
}
|
|
124
|
+
attempt.stageCompleted("credential_read");
|
|
125
|
+
attempt.stageCompleted("credential_parse");
|
|
126
|
+
reached("token_validation");
|
|
77
127
|
const defaultId = `max-account-${index}`;
|
|
78
128
|
const accountId = await input({
|
|
79
129
|
message: "Account ID (press Enter to accept default):",
|
|
@@ -84,27 +134,34 @@ export async function setupSingleAccount(index) {
|
|
|
84
134
|
const validation = await validateToken(tokens.accessToken);
|
|
85
135
|
if (validation.valid) {
|
|
86
136
|
console.log(chalk.green("✓ Valid"));
|
|
137
|
+
attempt.stageCompleted("token_validation");
|
|
87
138
|
}
|
|
88
139
|
else {
|
|
89
140
|
console.log(chalk.red("✗ Invalid"));
|
|
90
141
|
console.log(chalk.yellow(` Reason: ${validation.reason}`));
|
|
142
|
+
printDiagnosticId(attempt.stageFailed(validation.diagnostic, "token_validation"));
|
|
91
143
|
console.log(chalk.gray(" The token will be saved but may not work until refreshed."));
|
|
92
144
|
const keepAnyway = await confirm({ message: "Save this account anyway?", default: false });
|
|
93
|
-
if (!keepAnyway)
|
|
94
|
-
|
|
145
|
+
if (!keepAnyway) {
|
|
146
|
+
attempt.cancelled();
|
|
147
|
+
return { account: null, attempt };
|
|
148
|
+
}
|
|
95
149
|
}
|
|
96
150
|
return {
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
151
|
+
account: {
|
|
152
|
+
id: accountId,
|
|
153
|
+
tokens,
|
|
154
|
+
healthy: validation.valid,
|
|
155
|
+
busy: false,
|
|
156
|
+
requestCount: 0,
|
|
157
|
+
errorCount: 0,
|
|
158
|
+
lastUsed: 0,
|
|
159
|
+
lastRefresh: 0,
|
|
160
|
+
consecutiveErrors: 0,
|
|
161
|
+
rateLimits: { ...DEFAULT_RATE_LIMITS },
|
|
162
|
+
...ACCOUNT_USER_DEFAULTS,
|
|
163
|
+
},
|
|
164
|
+
attempt,
|
|
108
165
|
};
|
|
109
166
|
}
|
|
110
167
|
// ─── Full wizard ──────────────────────────────────────────────────────────────
|
|
@@ -179,6 +236,7 @@ export async function runSetupWizard({ addMode }) {
|
|
|
179
236
|
}) ?? 1;
|
|
180
237
|
}
|
|
181
238
|
const newAccounts = [];
|
|
239
|
+
const savedAttempts = [];
|
|
182
240
|
for (let i = 0; i < numAccounts; i++) {
|
|
183
241
|
const label = numAccounts > 1 ? `${i + 1}/${numAccounts}` : "";
|
|
184
242
|
console.log(chalk.bold(`\n${"━".repeat(40)}\n Account ${label}\n${"━".repeat(40)}\n`));
|
|
@@ -189,9 +247,10 @@ export async function runSetupWizard({ addMode }) {
|
|
|
189
247
|
await confirm({ message: "Ready?", default: true });
|
|
190
248
|
}
|
|
191
249
|
const existingCount = hasExisting ? loadAccounts().length : 0;
|
|
192
|
-
const account = await
|
|
250
|
+
const { account, attempt } = await setupSingleAccountWithAttempt(i + 1 + existingCount);
|
|
193
251
|
if (account) {
|
|
194
252
|
newAccounts.push(account);
|
|
253
|
+
savedAttempts.push(attempt);
|
|
195
254
|
console.log(chalk.green(`\n ✓ Account "${account.id}" ready.\n`));
|
|
196
255
|
}
|
|
197
256
|
else {
|
|
@@ -209,9 +268,20 @@ export async function runSetupWizard({ addMode }) {
|
|
|
209
268
|
...newAccounts,
|
|
210
269
|
];
|
|
211
270
|
console.log(chalk.bold(`\n${"━".repeat(40)}\n Saving\n${"━".repeat(40)}\n`));
|
|
212
|
-
|
|
271
|
+
try {
|
|
272
|
+
saveAccounts(merged);
|
|
273
|
+
}
|
|
274
|
+
catch (error) {
|
|
275
|
+
const outcomes = savedAttempts.map(attempt => attempt.failed(error, "persistence"));
|
|
276
|
+
if (outcomes[0])
|
|
277
|
+
printDiagnosticId(outcomes[0]);
|
|
278
|
+
throw error;
|
|
279
|
+
}
|
|
213
280
|
console.log(chalk.green(` ✓ ${merged.length} account(s) saved to ~/.cc-router/accounts.json`));
|
|
214
|
-
|
|
281
|
+
for (const attempt of savedAttempts) {
|
|
282
|
+
attempt.stageCompleted("persistence");
|
|
283
|
+
attempt.succeeded();
|
|
284
|
+
}
|
|
215
285
|
// ─── Post-setup interactive flow ─────────────────────────────────────────
|
|
216
286
|
await runPostSetupFlow(merged.length);
|
|
217
287
|
}
|
package/dist/cli/cmd-status.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import chalk from "chalk";
|
|
2
2
|
import { PROXY_PORT } from "../config/paths.js";
|
|
3
3
|
import { readConfig } from "../config/manager.js";
|
|
4
|
+
import { classifyHttpSetupFailure, failAttemptFromError, } from "../telemetry/setup-diagnostics.js";
|
|
4
5
|
export function resolveStatusTarget(port) {
|
|
5
6
|
const cfg = readConfig();
|
|
6
7
|
if (cfg.client) {
|
|
@@ -143,10 +144,13 @@ async function dashboardLoop(port) {
|
|
|
143
144
|
* success, or null if the user aborted / an error occurred.
|
|
144
145
|
*/
|
|
145
146
|
async function runAddAccountFlow(target) {
|
|
147
|
+
let attempt;
|
|
146
148
|
try {
|
|
147
|
-
const {
|
|
149
|
+
const { setupSingleAccountWithAttempt } = await import("./cmd-setup.js");
|
|
148
150
|
// The index shown in the flow is just for display, pick something neutral.
|
|
149
|
-
const
|
|
151
|
+
const setup = await setupSingleAccountWithAttempt(1);
|
|
152
|
+
attempt = setup.attempt;
|
|
153
|
+
const account = setup.account;
|
|
150
154
|
if (!account)
|
|
151
155
|
return null;
|
|
152
156
|
const res = await fetch(`${target.baseUrl}/cc-router/accounts`, {
|
|
@@ -169,12 +173,20 @@ async function runAddAccountFlow(target) {
|
|
|
169
173
|
console.error(chalk.red(`\n✗ Server rejected account: HTTP ${res.status}`));
|
|
170
174
|
if (text)
|
|
171
175
|
console.error(chalk.gray(` ${text}`));
|
|
176
|
+
attempt.failed(classifyHttpSetupFailure("persistence", res.status, "dashboard account add rejected"), "persistence");
|
|
172
177
|
return null;
|
|
173
178
|
}
|
|
179
|
+
attempt.stageCompleted("persistence");
|
|
180
|
+
attempt.succeeded();
|
|
174
181
|
return account.id;
|
|
175
182
|
}
|
|
176
183
|
catch (err) {
|
|
177
184
|
console.error(chalk.red(`\n✗ Failed to add account: ${err.message}`));
|
|
185
|
+
if (attempt) {
|
|
186
|
+
const outcome = failAttemptFromError(attempt, err, "persistence");
|
|
187
|
+
if (outcome?.unexpected)
|
|
188
|
+
console.error(chalk.gray(` Diagnostic ID: ${outcome.diagnosticId}`));
|
|
189
|
+
}
|
|
178
190
|
return null;
|
|
179
191
|
}
|
|
180
192
|
}
|
|
@@ -1,57 +1,68 @@
|
|
|
1
1
|
import chalk from "chalk";
|
|
2
|
-
import {
|
|
2
|
+
import { getTelemetrySnapshot, updateTelemetryConsent } from "../config/telemetry.js";
|
|
3
3
|
export function registerTelemetry(program) {
|
|
4
4
|
program
|
|
5
5
|
.command("telemetry [action]")
|
|
6
|
-
.description("Manage
|
|
6
|
+
.description("Manage privacy-safe telemetry: on, off, status (default: status; fresh installs: on)")
|
|
7
7
|
.action(async (action) => {
|
|
8
8
|
const resolved = action ?? "status";
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
return;
|
|
9
|
+
try {
|
|
10
|
+
runTelemetryAction(resolved);
|
|
12
11
|
}
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
console.log(chalk.dim(`Install ID: ${state.installId}`));
|
|
19
|
-
return;
|
|
12
|
+
catch (error) {
|
|
13
|
+
// An unreadable state file keeps telemetry off; never replace it blindly.
|
|
14
|
+
console.error(chalk.red(error instanceof Error ? error.message : "Telemetry state could not be read."));
|
|
15
|
+
console.error(chalk.dim("Telemetry stays disabled until the file is readable again."));
|
|
16
|
+
process.exitCode = 1;
|
|
20
17
|
}
|
|
21
|
-
if (resolved === "off") {
|
|
22
|
-
// Do not beacon on opt-out: an explicit "turn it off" must not send data.
|
|
23
|
-
const state = loadTelemetryState();
|
|
24
|
-
state.enabled = false;
|
|
25
|
-
writeTelemetryState(state);
|
|
26
|
-
console.log(chalk.yellow("Telemetry disabled. No data will be sent."));
|
|
27
|
-
console.log(chalk.dim("Re-enable anytime with: cc-router telemetry on"));
|
|
28
|
-
return;
|
|
29
|
-
}
|
|
30
|
-
console.error(chalk.red(`Unknown action "${resolved}". Use: on, off, status`));
|
|
31
|
-
process.exitCode = 1;
|
|
32
18
|
});
|
|
33
19
|
}
|
|
20
|
+
function runTelemetryAction(resolved) {
|
|
21
|
+
if (resolved === "status") {
|
|
22
|
+
showStatus();
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
if (resolved === "on") {
|
|
26
|
+
const state = updateTelemetryConsent(true);
|
|
27
|
+
console.log(chalk.green("Telemetry enabled for future daemon starts."));
|
|
28
|
+
console.log(chalk.dim("Restart a daemon that started with telemetry disabled to begin sending telemetry."));
|
|
29
|
+
console.log(chalk.dim(`Install ID: ${state.installId}`));
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
if (resolved === "off") {
|
|
33
|
+
// Do not beacon on opt-out: an explicit "turn it off" must not send data.
|
|
34
|
+
updateTelemetryConsent(false);
|
|
35
|
+
console.log(chalk.yellow("Telemetry disabled. New outbound telemetry stops immediately."));
|
|
36
|
+
console.log(chalk.dim("Re-enable anytime with: cc-router telemetry on"));
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
console.error(chalk.red(`Unknown action "${resolved}". Use: on, off, status`));
|
|
40
|
+
process.exitCode = 1;
|
|
41
|
+
}
|
|
34
42
|
function showStatus() {
|
|
35
|
-
const state =
|
|
36
|
-
const envDisabled = process.env["DO_NOT_TRACK"] === "1" || process.env["CC_ROUTER_TELEMETRY"] === "0";
|
|
43
|
+
const { state, environmentDisabled, enabled } = getTelemetrySnapshot();
|
|
37
44
|
console.log(chalk.bold("Telemetry"));
|
|
38
45
|
console.log();
|
|
39
|
-
if (
|
|
46
|
+
if (environmentDisabled) {
|
|
40
47
|
console.log(` Status: ${chalk.yellow("disabled")} (by environment variable)`);
|
|
41
48
|
}
|
|
42
49
|
else if (state.enabled) {
|
|
43
|
-
console.log(` Status: ${chalk.green("enabled")}`);
|
|
50
|
+
console.log(` Status: ${chalk.green("enabled")} (persisted)`);
|
|
44
51
|
}
|
|
45
52
|
else {
|
|
46
|
-
console.log(` Status: ${chalk.yellow("disabled")}`);
|
|
53
|
+
console.log(` Status: ${chalk.yellow("disabled")} (persisted)`);
|
|
47
54
|
}
|
|
48
|
-
console.log(` Active: ${
|
|
55
|
+
console.log(` Active: ${enabled ? chalk.green("yes") : chalk.yellow("no")}`);
|
|
49
56
|
console.log(` Install ID: ${chalk.dim(state.installId)}`);
|
|
50
57
|
console.log(` Since: ${chalk.dim(state.firstRunAt)}`);
|
|
51
58
|
console.log();
|
|
52
|
-
console.log(chalk.dim(" What we send:
|
|
53
|
-
console.log(chalk.dim(" What we DON'T:
|
|
54
|
-
console.log(chalk.dim("
|
|
59
|
+
console.log(chalk.dim(" What we send: sampled traces, safe diagnostics, lifecycle events, sanitized exceptions"));
|
|
60
|
+
console.log(chalk.dim(" What we DON'T: tokens, prompts/content, account/session IDs, raw errors, URLs, headers"));
|
|
61
|
+
console.log(chalk.dim(" Network note: PostHog EU sees the HTTPS source IP; it is not added to the payload"));
|
|
62
|
+
console.log(chalk.dim(" Identity: random install pseudonym; no Person profile or GeoIP enrichment"));
|
|
63
|
+
console.log(chalk.dim(" Source code: src/telemetry/"));
|
|
64
|
+
console.log(chalk.dim(" Inventory: docs/telemetry.md"));
|
|
65
|
+
console.log(chalk.dim(" Default: on for new installs"));
|
|
55
66
|
console.log();
|
|
56
67
|
console.log(chalk.dim(" Disable: cc-router telemetry off"));
|
|
57
68
|
console.log(chalk.dim(" Or set: DO_NOT_TRACK=1 | CC_ROUTER_TELEMETRY=0"));
|