@ipv9/tokentracker-cli 0.39.43 → 0.39.45
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/README.md +18 -10
- package/dashboard/dist/assets/{Card-LPizs_gs.js → Card-2-TQg7P7.js} +1 -1
- package/dashboard/dist/assets/DashboardPage-yb9ss9uE.js +60 -0
- package/dashboard/dist/assets/{FadeIn-B8aDegoD.js → FadeIn-BOc6XtOK.js} +1 -1
- package/dashboard/dist/assets/{IpCheckPage-Vo2ZZXov.js → IpCheckPage-C2tIb68P.js} +1 -1
- package/dashboard/dist/assets/{LimitsPage-C5-Q9Q30.js → LimitsPage-BsuLQ9co.js} +1 -1
- package/dashboard/dist/assets/LocalOnlyNotice-C8R9KLef.js +1 -0
- package/dashboard/dist/assets/{PopoverPopup-CJf61ahu.js → PopoverPopup-BpfseoI7.js} +1 -1
- package/dashboard/dist/assets/{Select-BLGoaqgw.js → Select-Ctk8zze5.js} +1 -1
- package/dashboard/dist/assets/{SelectItemText-Bt02Fgwf.js → SelectItemText-BKZlFyFs.js} +1 -1
- package/dashboard/dist/assets/{SettingsPage-BnUJew-8.js → SettingsPage-CQXM8qGU.js} +1 -1
- package/dashboard/dist/assets/{SkillsPage-ImHg3Puy.js → SkillsPage-4EMm0eBX.js} +1 -1
- package/dashboard/dist/assets/{WidgetsPage-qscVE2nO.js → WidgetsPage-C2sdX5g6.js} +1 -1
- package/dashboard/dist/assets/{WrappedPage-qM_7aClE.js → WrappedPage-CLiuEcQZ.js} +1 -1
- package/dashboard/dist/assets/{arrow-up-right-CByq3BPT.js → arrow-up-right-BDkp93DX.js} +1 -1
- package/dashboard/dist/assets/{download-CTwO-YeA.js → download-DZ6SoCSn.js} +1 -1
- package/dashboard/dist/assets/{format-4chvNBjF.js → format-CaW9kvsA.js} +1 -1
- package/dashboard/dist/assets/limitDisplay-DNU_w4O7.js +1 -0
- package/dashboard/dist/assets/{main-CCPcJ7ti.js → main-DgyymGht.js} +16 -3
- package/dashboard/dist/assets/main-t7dbBL4x.css +1 -0
- package/dashboard/dist/assets/{mock-data-DSiJ-9lr.js → mock-data-D6C7Fba3.js} +1 -1
- package/dashboard/dist/assets/{use-limits-display-prefs-Dgd-bQBC.js → use-limits-display-prefs-B7cHBa7Y.js} +1 -1
- package/dashboard/dist/assets/{use-native-settings-CjZRLdFT.js → use-native-settings-BKAzGuxw.js} +1 -1
- package/dashboard/dist/assets/{useCurrency-BJRU0syn.js → useCurrency-BVr6Ajuu.js} +1 -1
- package/dashboard/dist/index.html +2 -2
- package/package.json +5 -3
- package/src/commands/doctor.js +8 -0
- package/src/commands/init.js +1 -1
- package/src/commands/sync.js +67 -0
- package/src/lib/doctor.js +227 -1
- package/src/lib/local-api.js +385 -113
- package/src/lib/pricing/seed-snapshot.json +1 -1
- package/src/lib/process-list.js +91 -0
- package/src/lib/queue-compact.js +220 -0
- package/src/lib/rollout.js +81 -14
- package/src/lib/single-flight.js +59 -0
- package/src/lib/skills-manager.js +2 -2
- package/src/lib/transcript-suppression.js +133 -0
- package/src/lib/usage-limits.js +99 -22
- package/dashboard/dist/assets/DashboardPage-CkhqD3x3.js +0 -60
- package/dashboard/dist/assets/LocalOnlyNotice-DXVmRcyV.js +0 -1
- package/dashboard/dist/assets/limitDisplay-CXlkWhjp.js +0 -1
- package/dashboard/dist/assets/main-ZrWkoMlr.css +0 -1
package/src/lib/doctor.js
CHANGED
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
const fs = require("node:fs/promises");
|
|
2
|
+
const { findRowViolations } = require("./queue-compact");
|
|
2
3
|
const { constants } = require("node:fs");
|
|
3
4
|
const path = require("node:path");
|
|
4
5
|
|
|
5
6
|
const { readJsonStrict } = require("./fs");
|
|
6
7
|
|
|
8
|
+
// Stands in for a check whose `id` is missing or not a usable string, so that such
|
|
9
|
+
// a check can still be counted rather than silently dropped. See rule 2 in
|
|
10
|
+
// `listDegradedChecks`.
|
|
11
|
+
const UNNAMED_CHECK_ID = "(unnamed)";
|
|
12
|
+
|
|
7
13
|
async function buildDoctorReport({
|
|
8
14
|
runtime = {},
|
|
9
15
|
diagnostics = null,
|
|
@@ -11,6 +17,7 @@ async function buildDoctorReport({
|
|
|
11
17
|
now = () => new Date(),
|
|
12
18
|
paths = {},
|
|
13
19
|
system = null,
|
|
20
|
+
ingest = null,
|
|
14
21
|
} = {}) {
|
|
15
22
|
const checks = [];
|
|
16
23
|
|
|
@@ -28,26 +35,140 @@ async function buildDoctorReport({
|
|
|
28
35
|
if (paths.cliPath) {
|
|
29
36
|
checks.push(await checkCliEntrypoint(paths.cliPath));
|
|
30
37
|
}
|
|
38
|
+
if (paths.queuePath) {
|
|
39
|
+
checks.push(await checkQueueRows(paths.queuePath));
|
|
40
|
+
}
|
|
31
41
|
|
|
32
42
|
// No cloud reachability check: TokenTracker is local-only, so there is no
|
|
33
43
|
// remote endpoint whose availability could affect anything here.
|
|
34
44
|
|
|
45
|
+
if (ingest) {
|
|
46
|
+
const suppression = buildTranscriptSuppressionCheck(ingest.transcriptSuppression);
|
|
47
|
+
if (suppression) checks.push(suppression);
|
|
48
|
+
}
|
|
49
|
+
|
|
35
50
|
if (diagnostics) {
|
|
36
51
|
checks.push(...buildDiagnosticsChecks(diagnostics));
|
|
37
52
|
}
|
|
38
53
|
|
|
39
54
|
const summary = summarizeChecks(checks);
|
|
55
|
+
const degradedChecks = listDegradedChecks(checks);
|
|
40
56
|
|
|
41
57
|
return {
|
|
42
58
|
version: 1,
|
|
43
59
|
generated_at: now().toISOString(),
|
|
44
60
|
ok: summary.critical === 0,
|
|
61
|
+
// `ok` answers "should this exit non-zero", and only `critical` moves it.
|
|
62
|
+
// A warning therefore leaves an entirely green-looking report, which is
|
|
63
|
+
// exactly how a source going unrecorded stayed invisible. `degraded` is the
|
|
64
|
+
// machine-readable half of that distinction: automation can alert on it
|
|
65
|
+
// without any existing caller's exit code changing.
|
|
66
|
+
//
|
|
67
|
+
// It counts every warn and fail except a warn whose check marked itself
|
|
68
|
+
// `advisory`. A first version counted every warn, which made it useless on the
|
|
69
|
+
// one machine it was written for: that box carries a standing
|
|
70
|
+
// `queue.row_invariant` warn about two parseable invariant violations, so
|
|
71
|
+
// `degraded` read true
|
|
72
|
+
// on a perfectly healthy day and an alert wired to it could never clear. An
|
|
73
|
+
// always-on alert and an alert that never fires fail the same way.
|
|
74
|
+
// `listDegradedChecks` holds the exact rule including its two fail-closed
|
|
75
|
+
// clauses. Advisory is assigned at the individual warning return site: a
|
|
76
|
+
// standing condition can opt out without muting an actionable warning from
|
|
77
|
+
// the same check id.
|
|
78
|
+
degraded: degradedChecks.length > 0,
|
|
79
|
+
// Which checks put it there. Without this, `degraded: true` is unactionable —
|
|
80
|
+
// a consumer has to re-derive the reason by walking `checks` itself, and a
|
|
81
|
+
// human reading the JSON cannot tell a new problem from the standing one.
|
|
82
|
+
degraded_checks: degradedChecks,
|
|
45
83
|
summary,
|
|
46
84
|
checks,
|
|
47
85
|
diagnostics,
|
|
48
86
|
};
|
|
49
87
|
}
|
|
50
88
|
|
|
89
|
+
// A check is advisory when its warn describes a standing condition the operator
|
|
90
|
+
// cannot act on in the moment. Such a check still reports `warn` and still appears
|
|
91
|
+
// in `summary.warn`: the report does not become quieter, only the alert signal
|
|
92
|
+
// becomes specific. Anything that does not opt in counts, so a new check is
|
|
93
|
+
// alert-worthy by default and has to argue its way out.
|
|
94
|
+
//
|
|
95
|
+
// Two rules here are deliberately fail-CLOSED, because the failure this field
|
|
96
|
+
// exists to prevent is a real problem reading as silence, and both were live
|
|
97
|
+
// holes in the first version of this function:
|
|
98
|
+
//
|
|
99
|
+
// 1. `advisory` suppresses a `warn` and NOTHING ELSE. The rationale for the
|
|
100
|
+
// flag is about standing warnings; nothing argues for muting a `fail` on the
|
|
101
|
+
// same id, so a `fail` degrades the report whatever the flag says.
|
|
102
|
+
// 2. A check with a missing or malformed `id` is still counted, under
|
|
103
|
+
// UNNAMED_CHECK_ID. The earlier version mapped to `check.id` and then
|
|
104
|
+
// dropped non-strings, so an id typo silently removed a genuine warn from
|
|
105
|
+
// `degraded` altogether — it rendered as `[WARN] unknown` to a human and as
|
|
106
|
+
// nothing at all to automation. A placeholder keeps `degraded` and
|
|
107
|
+
// `degraded_checks` honest and in agreement.
|
|
108
|
+
function listDegradedChecks(checks = []) {
|
|
109
|
+
return checks
|
|
110
|
+
.filter((check) => check && (check.status === "warn" || check.status === "fail"))
|
|
111
|
+
// A malformed id overrides advisory suppression. Advisory is an explicit
|
|
112
|
+
// classification made at a known warning call site; if that identity is
|
|
113
|
+
// lost, fail closed under the placeholder rather than silently dropping it.
|
|
114
|
+
.filter((check) => !(
|
|
115
|
+
check.status === "warn"
|
|
116
|
+
&& check.advisory === true
|
|
117
|
+
&& typeof check.id === "string"
|
|
118
|
+
&& check.id.trim().length > 0
|
|
119
|
+
))
|
|
120
|
+
.map((check) =>
|
|
121
|
+
typeof check.id === "string" && check.id.trim().length > 0 ? check.id : UNNAMED_CHECK_ID,
|
|
122
|
+
)
|
|
123
|
+
.sort();
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Reports Claude CLI processes that were started with `--no-session-persistence`.
|
|
127
|
+
// Those sessions write no transcript, and transcripts are the only thing the
|
|
128
|
+
// Claude parser can read, so their tokens are unobservable to TokenTracker.
|
|
129
|
+
//
|
|
130
|
+
// Returns null when the platform cannot answer the question at all. A check that
|
|
131
|
+
// was never run must not be printed as `[OK]` — absence is honest, a green line
|
|
132
|
+
// would not be.
|
|
133
|
+
function buildTranscriptSuppressionCheck(detection) {
|
|
134
|
+
if (!detection || detection.supported === false) return null;
|
|
135
|
+
|
|
136
|
+
const id = "ingest.transcript_suppressed";
|
|
137
|
+
|
|
138
|
+
if (!detection.checked) {
|
|
139
|
+
return {
|
|
140
|
+
id,
|
|
141
|
+
status: "warn",
|
|
142
|
+
detail: "Could not read the process list, so transcript-suppressed sessions were not checked",
|
|
143
|
+
critical: false,
|
|
144
|
+
meta: { checked: false, reason: detection.reason || "process_list_failed" },
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const count = Number(detection.count) || 0;
|
|
149
|
+
if (count === 0) {
|
|
150
|
+
return {
|
|
151
|
+
id,
|
|
152
|
+
status: "ok",
|
|
153
|
+
detail: "No Claude CLI process is running with --no-session-persistence",
|
|
154
|
+
critical: false,
|
|
155
|
+
meta: { checked: true, count: 0, models: [] },
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const models = Array.isArray(detection.models) ? detection.models : [];
|
|
160
|
+
const modelSuffix = models.length ? ` (${models.join(", ")})` : "";
|
|
161
|
+
return {
|
|
162
|
+
id,
|
|
163
|
+
status: "warn",
|
|
164
|
+
detail:
|
|
165
|
+
`${count} Claude CLI process${count === 1 ? "" : "es"} running with --no-session-persistence${modelSuffix}`
|
|
166
|
+
+ " - these sessions write no transcript, so their token usage cannot be recorded",
|
|
167
|
+
critical: false,
|
|
168
|
+
meta: { checked: true, count, models },
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
51
172
|
async function buildSystemChecks({
|
|
52
173
|
nodeVersion = process.version,
|
|
53
174
|
platform = process.platform,
|
|
@@ -78,11 +199,14 @@ function buildNodeVersionCheck(nodeVersion) {
|
|
|
78
199
|
async function buildBrowserOpenerCheck({ platform = process.platform, env = process.env, commandExists }) {
|
|
79
200
|
const headless = isHeadlessEnvironment({ platform, env });
|
|
80
201
|
if (headless) {
|
|
202
|
+
// Standing environment property: neither --no-open nor opening the printed
|
|
203
|
+
// URL manually can make a headless session acquire a browser opener.
|
|
81
204
|
return {
|
|
82
205
|
id: "browser.opener",
|
|
83
206
|
status: "warn",
|
|
84
207
|
detail: "headless/session environment detected; use --no-open or open the printed URL manually",
|
|
85
208
|
critical: false,
|
|
209
|
+
advisory: true,
|
|
86
210
|
meta: { platform, command: null, headless: true },
|
|
87
211
|
};
|
|
88
212
|
}
|
|
@@ -328,14 +452,21 @@ function buildDiagnosticsChecks(diagnostics) {
|
|
|
328
452
|
notify.claude_hook_configured ||
|
|
329
453
|
notify.gemini_hook_configured ||
|
|
330
454
|
notify.opencode_plugin_configured ||
|
|
331
|
-
notify.openclaw_hook_configured
|
|
455
|
+
notify.openclaw_hook_configured ||
|
|
456
|
+
notify.openclaw_session_plugin_configured ||
|
|
457
|
+
notify.grok_hook_configured,
|
|
332
458
|
);
|
|
333
459
|
|
|
460
|
+
// This aggregate describes an optional integration preference, not whether
|
|
461
|
+
// passive log ingestion works. `init` also skips hooks for providers whose
|
|
462
|
+
// config is absent, so "none configured" can be a stable, intentional state.
|
|
463
|
+
// Keep only that warn advisory; this check currently has no fail path.
|
|
334
464
|
checks.push({
|
|
335
465
|
id: "notify.configured",
|
|
336
466
|
status: notifyConfigured ? "ok" : "warn",
|
|
337
467
|
detail: notifyConfigured ? "notify configured" : "notify not configured",
|
|
338
468
|
critical: false,
|
|
469
|
+
...(notifyConfigured ? {} : { advisory: true }),
|
|
339
470
|
meta: { configured: notifyConfigured },
|
|
340
471
|
});
|
|
341
472
|
|
|
@@ -358,8 +489,103 @@ function summarizeChecks(checks = []) {
|
|
|
358
489
|
return summary;
|
|
359
490
|
}
|
|
360
491
|
|
|
492
|
+
// CLAUDE.md states the column invariant in prose:
|
|
493
|
+
//
|
|
494
|
+
// total = input + output + cache_creation + cache_read + reasoning
|
|
495
|
+
//
|
|
496
|
+
// Nothing enforced it at runtime, so a miswritten or corrupt row was aggregated
|
|
497
|
+
// and rendered rather than flagged — and a parser bug of exactly that shape is
|
|
498
|
+
// the class CLAUDE.md records at 1.6-7x magnitude. Same conversion as the
|
|
499
|
+
// curated-expiry and version-lockstep checks: a rule that lived in a document
|
|
500
|
+
// starts running.
|
|
501
|
+
//
|
|
502
|
+
// A warn rather than a fail: the rows are already on disk and already being
|
|
503
|
+
// rendered, so failing the whole health check would be reporting a crisis the
|
|
504
|
+
// user cannot act on in the moment. What they can act on is knowing which rows,
|
|
505
|
+
// and how many.
|
|
506
|
+
const QUEUE_VIOLATIONS_SHOWN = 5;
|
|
507
|
+
|
|
508
|
+
// `advisory: true` keeps a warn out of `degraded` and `degraded_checks` while
|
|
509
|
+
// leaving it a full `warn` in `checks` and in `summary.warn`. The flag is decided
|
|
510
|
+
// PER CALL SITE, not once for this check id, because the two warns this check can
|
|
511
|
+
// emit are not the same kind of thing:
|
|
512
|
+
//
|
|
513
|
+
// - a parseable row-invariant violation IS advisory: the row is already written
|
|
514
|
+
// and rendered, so there is nothing the operator can do at report time.
|
|
515
|
+
// - an unparseable line or unreadable queue is NOT. Those conditions omit usage
|
|
516
|
+
// or can stop ingestion and are actionable (corruption, permissions, disk).
|
|
517
|
+
//
|
|
518
|
+
// An earlier version of this function stamped `advisory: true` on everything it
|
|
519
|
+
// returned, which silenced the unreadable case: `warn` in `checks`,
|
|
520
|
+
// `degraded: false` on the wire. Default is NOT advisory, so a new call site has
|
|
521
|
+
// to argue its way out rather than inherit silence.
|
|
522
|
+
function queueCheck(status, detail, meta, { advisory = false } = {}) {
|
|
523
|
+
const check = { id: "queue.row_invariant", status, detail, critical: false, meta };
|
|
524
|
+
return advisory ? { ...check, advisory: true } : check;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
async function readQueueRowsForDoctor(queuePath) {
|
|
528
|
+
const raw = await fs.readFile(queuePath, "utf8");
|
|
529
|
+
const rows = [];
|
|
530
|
+
let malformed = 0;
|
|
531
|
+
for (const line of raw.split("\n")) {
|
|
532
|
+
if (!line.trim()) continue;
|
|
533
|
+
try {
|
|
534
|
+
rows.push(JSON.parse(line));
|
|
535
|
+
} catch {
|
|
536
|
+
malformed += 1;
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
return { rows, malformed };
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
async function checkQueueRows(queuePath) {
|
|
543
|
+
let rows;
|
|
544
|
+
let malformed;
|
|
545
|
+
try {
|
|
546
|
+
({ rows, malformed } = await readQueueRowsForDoctor(queuePath));
|
|
547
|
+
} catch (err) {
|
|
548
|
+
if (err && err.code === "ENOENT") {
|
|
549
|
+
return queueCheck("ok", "no queue yet", { path: queuePath });
|
|
550
|
+
}
|
|
551
|
+
return queueCheck("warn", `queue unreadable: ${err?.message || err}`, { path: queuePath });
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
const violations = findRowViolations(rows);
|
|
555
|
+
if (violations.length === 0 && malformed === 0) {
|
|
556
|
+
return queueCheck("ok", `${rows.length} rows satisfy the column invariant`, {
|
|
557
|
+
path: queuePath,
|
|
558
|
+
rows: rows.length,
|
|
559
|
+
});
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
const parts = [];
|
|
563
|
+
if (violations.length > 0) parts.push(`${violations.length} row problem(s)`);
|
|
564
|
+
if (malformed > 0) parts.push(`${malformed} unparseable line(s)`);
|
|
565
|
+
// Parseable invariant violations are already on disk and already aggregated
|
|
566
|
+
// into what the dashboard renders, so they are advisory. Malformed lines are
|
|
567
|
+
// skipped by local-api readers and their usage is absent; corruption or a
|
|
568
|
+
// partial write is actionable and must degrade the report.
|
|
569
|
+
return queueCheck(
|
|
570
|
+
"warn",
|
|
571
|
+
`${parts.join(", ")} in ${rows.length + malformed} line(s)`,
|
|
572
|
+
{
|
|
573
|
+
path: queuePath,
|
|
574
|
+
rows: rows.length,
|
|
575
|
+
malformed,
|
|
576
|
+
violations: violations.length,
|
|
577
|
+
examples: violations.slice(0, QUEUE_VIOLATIONS_SHOWN),
|
|
578
|
+
},
|
|
579
|
+
{ advisory: malformed === 0 },
|
|
580
|
+
);
|
|
581
|
+
}
|
|
582
|
+
|
|
361
583
|
module.exports = {
|
|
362
584
|
buildDoctorReport,
|
|
585
|
+
buildTranscriptSuppressionCheck,
|
|
586
|
+
listDegradedChecks,
|
|
587
|
+
UNNAMED_CHECK_ID,
|
|
588
|
+
checkQueueRows,
|
|
363
589
|
buildBrowserOpenerCheck,
|
|
364
590
|
buildNodeVersionCheck,
|
|
365
591
|
isHeadlessEnvironment,
|