tokentrace 0.10.1 → 0.11.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.
Files changed (50) hide show
  1. package/CHANGELOG.md +21 -1
  2. package/README.md +20 -3
  3. package/SECURITY.md +6 -2
  4. package/app/api/repair-items/route.ts +22 -0
  5. package/app/api/settings/route.ts +3 -1
  6. package/app/diagnostics/page.tsx +13 -1
  7. package/app/guide/page.tsx +8 -3
  8. package/app/page.tsx +38 -0
  9. package/app/projects/page.tsx +13 -0
  10. package/app/repair/page.tsx +91 -91
  11. package/app/sessions/[id]/page.tsx +59 -0
  12. package/bin/tokentrace.js +1 -1
  13. package/components/repair-bulk-actions.tsx +84 -0
  14. package/components/scan-health-summary.tsx +22 -1
  15. package/components/session-explorer.tsx +14 -1
  16. package/components/settings-panel.tsx +103 -1
  17. package/dist/runtime/agent.mjs +2 -2
  18. package/dist/runtime/db-seed.mjs +26 -1
  19. package/dist/runtime/digest.mjs +241 -29
  20. package/dist/runtime/doctor.mjs +258 -47
  21. package/dist/runtime/insights.mjs +241 -29
  22. package/dist/runtime/pricing-refresh.mjs +26 -1
  23. package/dist/runtime/report.mjs +241 -29
  24. package/dist/runtime/reset.mjs +26 -1
  25. package/dist/runtime/review.mjs +241 -29
  26. package/dist/runtime/roadmap.mjs +89 -77
  27. package/dist/runtime/scan.mjs +425 -74
  28. package/package.json +3 -2
  29. package/scripts/roadmap.ts +1 -1
  30. package/scripts/security-ioc.mjs +384 -0
  31. package/scripts/smoke-cli.mjs +2 -2
  32. package/scripts/smoke-packed-install.mjs +2 -2
  33. package/src/db/settings.ts +6 -2
  34. package/src/ingestion/adapters/index.ts +2 -0
  35. package/src/ingestion/adapters/sqlite-history.ts +201 -0
  36. package/src/ingestion/discovery.ts +15 -18
  37. package/src/ingestion/persist.ts +32 -9
  38. package/src/ingestion/scan.ts +3 -1
  39. package/src/ingestion/types.ts +9 -1
  40. package/src/lib/agent-discovery.ts +2 -2
  41. package/src/lib/analytics.ts +102 -15
  42. package/src/lib/cost-recalculation.ts +28 -1
  43. package/src/lib/data-confidence.ts +83 -0
  44. package/src/lib/import-profiles.ts +124 -0
  45. package/src/lib/roadmap-status.ts +90 -78
  46. package/src/lib/scan-health.ts +33 -6
  47. package/src/lib/session-timeline.ts +64 -0
  48. package/src/lib/supply-chain-health.ts +31 -0
  49. package/src/lib/token-estimator.ts +72 -5
  50. package/src/lib/unknown-cost-repair.ts +19 -0
package/CHANGELOG.md CHANGED
@@ -4,7 +4,27 @@ All notable changes to TokenTrace are documented here.
4
4
 
5
5
  ## Unreleased
6
6
 
7
- No unreleased changes yet.
7
+ ## [0.11.0] - 2026-05-18
8
+
9
+ ### Added
10
+
11
+ - `npm run security:ioc` scans project files and local Claude/VS Code hook files for high-signal Mini Shai-Hulud/TanStack supply-chain indicators before release.
12
+ - Tokenizer-backed estimates for recognized OpenAI/Codex and Claude-family model names, with explicit `tokenizer estimate` and `simple estimate` confidence labels.
13
+ - Native SQLite history ingestion for local usage databases with usage-shaped tables, source-file evidence, import-profile metadata, and source-provided cost preservation.
14
+ - Data Confidence scoring on Overview, Projects, Sessions, and Session Timeline views, combining token source, cost coverage, parser confidence, and scan freshness.
15
+ - Import Profiles in Settings so users can define safe local wrapper-log matchers without writing parser code.
16
+ - Repair Workbench bulk actions for marking visible unknown-cost groups verified, parser-review, ignored, or reopened.
17
+ - Supply-chain IOC status in Scan Health, backed by the same local `security:ioc` scanner used by release checks.
18
+
19
+ ### Changed
20
+
21
+ - Session Timeline pages now explain token spikes, model changes, cache activity, cost coverage, confidence drivers, and direct unknown-cost repair links.
22
+ - Scan Health now distinguishes exact, tokenizer-estimated, simple-estimated, high-confidence, low-confidence, and unknown token rows.
23
+ - The roadmap API and CLI now report the 0.11.0 Accuracy & Evidence release contract.
24
+
25
+ ### Fixed
26
+
27
+ - Source-provided interaction costs are no longer overwritten as unknown during post-scan cost recalculation when model rates are missing.
8
28
 
9
29
  ## [0.10.1] - 2026-05-18
10
30
 
package/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  # TokenTrace CLI
6
6
 
7
- Local-first AI CLI usage analytics. TokenTrace scans local CLI logs, normalizes token usage, estimates missing counts, and shows cost, model, project, and session analytics in a browser dashboard.
7
+ Local-first AI CLI usage analytics. TokenTrace scans local CLI logs and local usage databases, normalizes token usage, estimates missing counts with confidence labels, and shows cost, model, project, session, repair, and evidence analytics in a browser dashboard.
8
8
 
9
9
  TokenTrace is designed for local development machines first, with macOS-oriented defaults. It does not require a cloud account and does not send telemetry or logs anywhere.
10
10
 
@@ -40,7 +40,7 @@ tokentrace agent --json # Print machine-readable agent discovery manifest
40
40
  tokentrace capabilities --json
41
41
  # Alias for agent discovery manifest
42
42
  tokentrace roadmap --json
43
- # Print Guided Operator release implementation status
43
+ # Print Accuracy & Evidence release implementation status
44
44
  tokentrace scan # Scan local AI CLI usage logs
45
45
  tokentrace doctor --json
46
46
  # Inspect scan health and repair recommendations
@@ -114,7 +114,7 @@ curl http://127.0.0.1:3030/api/agent
114
114
  curl http://127.0.0.1:3030/api/capabilities
115
115
  ```
116
116
 
117
- The Guided Operator release status is also machine-readable:
117
+ The Accuracy & Evidence release status is also machine-readable:
118
118
 
119
119
  ```bash
120
120
  tokentrace roadmap --json
@@ -149,12 +149,28 @@ npm run verify # Run Vitest, TypeScript, and ESLint checks
149
149
  npm run package:test # Verify, build, and dry-run the npm package
150
150
  npm run package:inspect
151
151
  # Check package transparency guardrails
152
+ npm run security:ioc
153
+ # Scan lockfiles, workflows, and local AI-tool hooks for supply-chain IOCs
152
154
  npm run smoke:packed
153
155
  # Inspect packed tarball and smoke test packed CLI
154
156
  tokentrace roadmap --json
155
157
  # Inspect evidence gates and release status
156
158
  ```
157
159
 
160
+ ## Accuracy And Evidence
161
+
162
+ TokenTrace labels the trust level behind imported numbers:
163
+
164
+ - exact provider token counts
165
+ - tokenizer estimates for recognized OpenAI/Codex and Claude-family model names
166
+ - simple estimates when only text-like content is available
167
+ - source-provided costs from local SQLite histories
168
+ - unknown cost repair groups when model, token, or rate evidence is missing
169
+
170
+ The dashboard surfaces a Data Confidence score on Overview, Projects, Sessions,
171
+ and Session Timeline pages. Scan Health also includes a supply-chain IOC check
172
+ so package trust is visible in the product, not only in release scripts.
173
+
158
174
  Release work uses internal milestone commits until the next public minor
159
175
  release. See [docs/RELEASE_CHECKLIST.md](docs/RELEASE_CHECKLIST.md) before
160
176
  bumping versions, tagging, creating GitHub releases, or publishing npm.
@@ -324,6 +340,7 @@ Stop the server with `Ctrl+C` in the terminal where `tokentrace` is running.
324
340
  - The published package ships readable application source and the compiled CLI runtime, not generated `.next/server` route bundles.
325
341
  - `tokentrace serve` prepares the local dashboard build in the user's TokenTrace app-data directory the first time it is needed.
326
342
  - `npm run package:inspect` fails if generated Next.js build output appears in the published tarball.
343
+ - `npm run security:ioc` scans lockfiles, workflows, and local Claude/VS Code hook files for high-signal supply-chain compromise indicators.
327
344
  - Public npm publishing is configured through GitHub Actions Trusted Publishing and provenance from version tags.
328
345
  - Socket GitHub checks and ProjScan are used as release guardrails, alongside `npm audit --audit-level=moderate`.
329
346
  - Release notes are published directly in GitHub Releases from the relevant changelog section, not as a link-only summary.
package/SECURITY.md CHANGED
@@ -40,8 +40,12 @@ When running from source, the default development database is
40
40
  ## Release Trust
41
41
 
42
42
  Public package releases are built from Git tags. The release flow includes
43
- package inspection, production dependency audit, ProjScan checks, and npm
44
- Trusted Publishing through GitHub Actions.
43
+ package inspection, supply-chain IOC scanning, production dependency audit,
44
+ ProjScan checks, and npm Trusted Publishing through GitHub Actions.
45
+
46
+ Run `npm run security:ioc` to check lockfiles, GitHub Actions workflows, and
47
+ local Claude/VS Code hook files for high-signal Mini Shai-Hulud/TanStack-style
48
+ indicators before release or after a dependency scare.
45
49
 
46
50
  ## Reporting
47
51
 
@@ -1,5 +1,6 @@
1
1
  import { NextResponse } from "next/server";
2
2
  import {
3
+ bulkUpdateUnknownCostRepairs,
3
4
  buildUnknownCostRepairWorkbench,
4
5
  getUnknownCostReview,
5
6
  saveUnknownCostReview,
@@ -40,9 +41,30 @@ export async function PUT(request: Request) {
40
41
  return NextResponse.json({ error: parsed.error }, { status: 400 });
41
42
  }
42
43
  const body = parsed.body;
44
+ const keys = Array.isArray(body.keys)
45
+ ? body.keys.map((value) => text(value, 1000)).filter(Boolean)
46
+ : [];
43
47
  const key = text(body.key, 1000);
44
48
  const status = reviewState(body.status ?? body.state);
45
49
 
50
+ if (keys.length) {
51
+ if (!status) {
52
+ return NextResponse.json({ error: "status must be unresolved, ignored, resolved, or needs-parser-review" }, { status: 400 });
53
+ }
54
+ const currentKeys = new Set(buildUnknownCostRepairWorkbench().groups.map((group) => group.key));
55
+ const missingKey = keys.find((item) => !currentKeys.has(item) && !getUnknownCostReview(item)?.updatedAt);
56
+ if (missingKey) {
57
+ return NextResponse.json({ error: "one or more repair keys were not found in current workbench evidence" }, { status: 404 });
58
+ }
59
+ return NextResponse.json(
60
+ bulkUpdateUnknownCostRepairs({
61
+ keys,
62
+ status,
63
+ notes: text(body.notes ?? body.note, 500)
64
+ })
65
+ );
66
+ }
67
+
46
68
  if (!key) {
47
69
  return NextResponse.json({ error: "key is required" }, { status: 400 });
48
70
  }
@@ -2,6 +2,7 @@ import { NextResponse } from "next/server";
2
2
  import { getDatabasePath } from "@/src/db/client";
3
3
  import { getAppSettings, normalizeUsageGuardrails, saveAppSettings } from "@/src/db/settings";
4
4
  import { jsonBooleanFlag, readJsonObject } from "@/src/lib/api-json";
5
+ import { normalizeImportProfiles } from "@/src/lib/import-profiles";
5
6
 
6
7
  export const dynamic = "force-dynamic";
7
8
 
@@ -27,7 +28,8 @@ export async function PUT(request: Request) {
27
28
  const saved = saveAppSettings({
28
29
  customFolders,
29
30
  storeRawMessageContent: jsonBooleanFlag(body.storeRawMessageContent),
30
- usageGuardrails: normalizeUsageGuardrails(body.usageGuardrails)
31
+ usageGuardrails: normalizeUsageGuardrails(body.usageGuardrails),
32
+ importProfiles: normalizeImportProfiles(body.importProfiles)
31
33
  });
32
34
 
33
35
  return NextResponse.json(saved);
@@ -7,8 +7,10 @@ import { DataValue, FieldLabel, MonoText, PageHeader } from "@/components/ui/typ
7
7
  import { ScanHealthSummary } from "@/components/scan-health-summary";
8
8
  import { getAnalyticsData, getScanTrustData, type DebugScanRun } from "@/src/lib/analytics";
9
9
  import { buildDoctorReport, type DoctorReport } from "@/src/lib/doctor";
10
+ import { buildScanHealth } from "@/src/lib/scan-health";
10
11
  import { getDefaultSearchRoots } from "@/src/ingestion/discovery";
11
12
  import { formatDate } from "@/src/lib/format";
13
+ import { getSupplyChainHealth } from "@/src/lib/supply-chain-health";
12
14
 
13
15
  export const dynamic = "force-dynamic";
14
16
 
@@ -461,7 +463,17 @@ function ScanHistoryPanel({ scanRuns }: { scanRuns: DebugScanRun[] }) {
461
463
  }
462
464
 
463
465
  export default async function DiagnosticsPage() {
464
- const data = getScanTrustData();
466
+ const baseData = getScanTrustData();
467
+ const supplyChain = getSupplyChainHealth();
468
+ const data = {
469
+ ...baseData,
470
+ health: buildScanHealth({
471
+ scanRuns: baseData.scanRuns,
472
+ scanFiles: baseData.scanFiles,
473
+ confidence: baseData.confidence,
474
+ supplyChain
475
+ })
476
+ };
465
477
  const analytics = getAnalyticsData();
466
478
  const roots = await getDefaultSearchRoots();
467
479
  const doctorReport = buildDoctorReport({
@@ -168,6 +168,11 @@ const workflows = [
168
168
  problem: "Parser warnings",
169
169
  path: "Parsers, Discovery, Scan Health",
170
170
  action: "Check parser confidence, unsupported files, and imported-with-errors rows before trusting an adapter."
171
+ },
172
+ {
173
+ problem: "Package trust check",
174
+ path: "Scan Health",
175
+ action: "Review the supply-chain IOC check alongside parser and model-rate health before release or upgrade work."
171
176
  }
172
177
  ];
173
178
 
@@ -175,7 +180,7 @@ const pageMap = [
175
180
  ["Overview", "Top-level totals, trends, repair queue, guardrails, and recommended next actions."],
176
181
  ["Sessions", "Per-session evidence with models, costs, cache activity, parser provenance, and tool calls."],
177
182
  ["Model Rates", "Editable provider model rates used for dashboard cost estimates and unknown-cost repair."],
178
- ["Scan Health", "First-run checklist, scan health, supported file types, and diagnostics for missing data."],
183
+ ["Scan Health", "First-run checklist, scan health, supply-chain IOC check, supported file types, and diagnostics for missing data."],
179
184
  ["Discovery", "Recently scanned files grouped by parser, source family, status, and import yield."],
180
185
  ["Parsers", "Adapter choices, warnings, confidence, and parser repair clues for local files."]
181
186
  ];
@@ -483,8 +488,8 @@ export default function GuidePage() {
483
488
  <h3 className="text-sm font-semibold leading-tight">Release readiness</h3>
484
489
  </div>
485
490
  <p className="mt-2 text-sm leading-6 text-muted-foreground">
486
- Current package version is {roadmapStatus.packageVersion}. The 0.10.0 roadmap is released, and 0.10.1 carries
487
- the current guide, scan, evidence, and overview polish.
491
+ Current package version is {roadmapStatus.packageVersion}. The current roadmap focuses on accuracy, evidence,
492
+ repair workflow, import profiles, and package-trust checks.
488
493
  </p>
489
494
  <div className="mt-4 grid grid-cols-2 gap-3 text-sm">
490
495
  <div>
package/app/page.tsx CHANGED
@@ -26,6 +26,7 @@ import { buildPostSessionReview, type PostSessionReview } from "@/src/lib/post-s
26
26
  export const dynamic = "force-dynamic";
27
27
 
28
28
  type UsageComparison = ReturnType<typeof getAnalyticsData>["comparison"];
29
+ type DataConfidence = ReturnType<typeof getAnalyticsData>["dataConfidence"];
29
30
 
30
31
  function CostSessionsMetricPane({
31
32
  label,
@@ -638,6 +639,41 @@ function UsagePulsePanel({ comparison }: { comparison: UsageComparison }) {
638
639
  );
639
640
  }
640
641
 
642
+ function confidenceVariant(grade: DataConfidence["grade"]) {
643
+ if (grade === "high") return "success";
644
+ if (grade === "medium") return "warning";
645
+ if (grade === "low") return "destructive";
646
+ return "secondary";
647
+ }
648
+
649
+ function DataConfidenceStrip({ confidence }: { confidence: DataConfidence }) {
650
+ return (
651
+ <Card>
652
+ <CardContent className="flex flex-col gap-3 p-4 lg:flex-row lg:items-center lg:justify-between">
653
+ <div className="min-w-0">
654
+ <div className="flex flex-wrap items-center gap-2">
655
+ <div className="text-sm font-semibold">Data Confidence</div>
656
+ <Badge variant={confidenceVariant(confidence.grade)}>{confidence.score}/100 {confidence.grade}</Badge>
657
+ </div>
658
+ <p className="mt-1 text-sm leading-6 text-muted-foreground">
659
+ {confidence.drivers.slice(0, 2).join(" ")}
660
+ </p>
661
+ </div>
662
+ <div className="flex flex-wrap gap-2">
663
+ {confidence.repairHref ? (
664
+ <Button asChild variant="outline" size="sm">
665
+ <Link href={confidence.repairHref}>Open repair <ArrowRight className="h-4 w-4" /></Link>
666
+ </Button>
667
+ ) : null}
668
+ <Button asChild variant="outline" size="sm">
669
+ <Link href="/diagnostics">Open Scan Health <ArrowRight className="h-4 w-4" /></Link>
670
+ </Button>
671
+ </div>
672
+ </CardContent>
673
+ </Card>
674
+ );
675
+ }
676
+
641
677
  function guardrailBadgeVariant(status: UsageGuardrailMetric["status"]) {
642
678
  if (status === "exceeded") return "destructive";
643
679
  if (status === "warning") return "warning";
@@ -934,6 +970,8 @@ export default async function OverviewPage({ searchParams }: OverviewPageProps)
934
970
 
935
971
  <UsagePulsePanel comparison={data.comparison} />
936
972
 
973
+ <DataConfidenceStrip confidence={data.dataConfidence} />
974
+
937
975
  <div className="grid gap-3 md:grid-cols-2 xl:grid-cols-6">
938
976
  <TokenAccountingCard
939
977
  summary={summary}
@@ -12,6 +12,13 @@ import { formatCurrency, formatDate, formatTokens } from "@/src/lib/format";
12
12
 
13
13
  export const dynamic = "force-dynamic";
14
14
 
15
+ function confidenceVariant(grade: string) {
16
+ if (grade === "high") return "success";
17
+ if (grade === "medium") return "warning";
18
+ if (grade === "low") return "destructive";
19
+ return "secondary";
20
+ }
21
+
15
22
  export default function ProjectAnalyticsPage() {
16
23
  const data = getAnalyticsData();
17
24
  const mostExpensive = data.projects[0];
@@ -145,6 +152,7 @@ export default function ProjectAnalyticsPage() {
145
152
  <TableHead>Cost</TableHead>
146
153
  <TableHead>Sessions</TableHead>
147
154
  <TableHead>Interactions</TableHead>
155
+ <TableHead>Confidence</TableHead>
148
156
  <TableHead>Output/Input</TableHead>
149
157
  <TableHead>Last used</TableHead>
150
158
  </TableRow>
@@ -164,6 +172,11 @@ export default function ProjectAnalyticsPage() {
164
172
  <TableCell>{formatCurrency(project.cost)}</TableCell>
165
173
  <TableCell>{project.sessions.toLocaleString()}</TableCell>
166
174
  <TableCell>{project.interactions.toLocaleString()}</TableCell>
175
+ <TableCell>
176
+ <Badge variant={confidenceVariant(project.confidenceGrade ?? "empty")}>
177
+ {project.confidenceScore ?? 0}/100 {project.confidenceGrade ?? "empty"}
178
+ </Badge>
179
+ </TableCell>
167
180
  <TableCell>{project.outputInputRatio.toFixed(2)}x</TableCell>
168
181
  <TableCell>{formatDate(project.lastUsedAt)}</TableCell>
169
182
  </TableRow>
@@ -2,6 +2,7 @@ import Link from "next/link";
2
2
  import { ArrowRight, CheckCircle2, CircleDashed, Search, Settings2 } from "lucide-react";
3
3
  import { EmptyState } from "@/components/empty-state";
4
4
  import { PeriodFilter } from "@/components/period-filter";
5
+ import { RepairBulkActions } from "@/components/repair-bulk-actions";
5
6
  import { RepairStateControl } from "@/components/repair-state-control";
6
7
  import { Badge } from "@/components/ui/badge";
7
8
  import { Button } from "@/components/ui/button";
@@ -257,99 +258,98 @@ export default async function RepairPage({ searchParams }: RepairPageProps) {
257
258
  </CardHeader>
258
259
  <CardContent className="table-scroll overflow-x-auto">
259
260
  {hasGroups ? (
260
- <Table className="min-w-[84rem]">
261
- <TableHeader>
262
- <TableRow>
263
- <TableHead>State</TableHead>
264
- <TableHead>Cause</TableHead>
265
- <TableHead className="min-w-56">Model</TableHead>
266
- <TableHead className="min-w-72">Suggestion</TableHead>
267
- <TableHead className="min-w-80">Source</TableHead>
268
- <TableHead>Interactions</TableHead>
269
- <TableHead>Tokens</TableHead>
270
- <TableHead className="min-w-28">Actions</TableHead>
271
- </TableRow>
272
- </TableHeader>
273
- <TableBody>
274
- {workbench.groups.map((group) => (
275
- <TableRow key={group.key} className={focusKey === group.key ? "bg-muted/40" : undefined}>
276
- <TableCell className="align-top">
277
- <RepairStateControl
278
- repairKey={group.key}
279
- initialStatus={group.review.status}
280
- initialNotes={group.review.notes}
281
- sourceFile={group.sourceFile}
282
- model={group.model}
283
- provider={group.provider}
284
- cause={group.cause}
285
- />
286
- </TableCell>
287
- <TableCell className="align-top">
288
- <Badge variant={causeVariant(group.cause)}>{causeLabel(group.cause)}</Badge>
289
- </TableCell>
290
- <TableCell className="align-top">
291
- <div className="font-medium">{group.model}</div>
292
- <div className="mt-1 text-xs text-muted-foreground">{group.provider} / {group.tool}</div>
293
- </TableCell>
294
- <TableCell className="min-w-72 max-w-80 align-top">
295
- <div className="flex items-center gap-2 text-sm font-medium">
296
- {group.suggestion.suggestedModel ? (
297
- <CheckCircle2 className="h-4 w-4 text-primary" />
298
- ) : (
299
- <CircleDashed className="h-4 w-4 text-muted-foreground" />
300
- )}
301
- {suggestionLabel(group)}
302
- </div>
303
- <div className="mt-1 text-xs leading-relaxed text-muted-foreground">
304
- {group.suggestion.confidence} confidence. {group.suggestion.reason}
305
- </div>
306
- </TableCell>
307
- <TableCell className="max-w-96 align-top">
308
- <Link href={mergeHrefParams(group.sourceHref, rangeLinkParams)} title={group.sourceFile}>
309
- <MonoText className="block truncate text-xs text-muted-foreground underline-offset-4 hover:underline">
310
- {group.sourceFile}
311
- </MonoText>
312
- </Link>
313
- </TableCell>
314
- <TableCell className="align-top">{group.interactions.toLocaleString()}</TableCell>
315
- <TableCell className="min-w-28 align-top">
316
- <div>{formatTokens(group.totalTokens)}</div>
317
- <div className="mt-1 text-xs text-muted-foreground">
318
- {formatTokens(group.inputTokens)} in, {formatTokens(group.outputTokens)} out
319
- </div>
320
- </TableCell>
321
- <TableCell className="align-top">
322
- <div className="flex flex-wrap gap-2">
323
- <Link
324
- href={
325
- group.pricingHref
326
- ? mergeHrefParams(group.pricingHref, { returnTo: mergeHrefParams(group.itemHref, rangeLinkParams) })
327
- : mergeHrefParams(group.repairHref, rangeLinkParams)
328
- }
329
- className="inline-flex items-center gap-1 font-medium text-primary underline-offset-4 hover:underline"
330
- >
331
- {group.pricingHref ? "Set model rate" : "Review parser"} <ArrowRight className="h-3.5 w-3.5" />
332
- </Link>
333
- <Link href={mergeHrefParams(group.itemHref, rangeLinkParams)} className="font-medium text-muted-foreground underline-offset-4 hover:underline">
334
- Open repair
335
- </Link>
336
- <Link href={mergeHrefParams(group.parserHref, rangeLinkParams)} className="font-medium text-muted-foreground underline-offset-4 hover:underline">
337
- Review parser
261
+ <div className="space-y-3">
262
+ <RepairBulkActions
263
+ keys={workbench.groups.map((group) => group.key)}
264
+ modelRatesHref={mergeHrefParams("/pricing", rangeLinkParams)}
265
+ scanHealthHref="/diagnostics"
266
+ />
267
+ <Table className="min-w-[84rem]">
268
+ <TableHeader>
269
+ <TableRow>
270
+ <TableHead>State</TableHead>
271
+ <TableHead>Cause</TableHead>
272
+ <TableHead className="min-w-56">Model</TableHead>
273
+ <TableHead className="min-w-72">Suggestion</TableHead>
274
+ <TableHead className="min-w-80">Source</TableHead>
275
+ <TableHead>Interactions</TableHead>
276
+ <TableHead>Tokens</TableHead>
277
+ <TableHead className="min-w-28">Actions</TableHead>
278
+ </TableRow>
279
+ </TableHeader>
280
+ <TableBody>
281
+ {workbench.groups.map((group) => (
282
+ <TableRow key={group.key} className={focusKey === group.key ? "bg-muted/40" : undefined}>
283
+ <TableCell className="align-top">
284
+ <RepairStateControl
285
+ repairKey={group.key}
286
+ initialStatus={group.review.status}
287
+ initialNotes={group.review.notes}
288
+ sourceFile={group.sourceFile}
289
+ model={group.model}
290
+ provider={group.provider}
291
+ cause={group.cause}
292
+ />
293
+ </TableCell>
294
+ <TableCell className="align-top">
295
+ <Badge variant={causeVariant(group.cause)}>{causeLabel(group.cause)}</Badge>
296
+ </TableCell>
297
+ <TableCell className="align-top">
298
+ <div className="font-medium">{group.model}</div>
299
+ <div className="mt-1 text-xs text-muted-foreground">{group.provider} / {group.tool}</div>
300
+ </TableCell>
301
+ <TableCell className="min-w-72 max-w-80 align-top">
302
+ <div className="flex items-center gap-2 text-sm font-medium">
303
+ {group.suggestion.suggestedModel ? (
304
+ <CheckCircle2 className="h-4 w-4 text-primary" />
305
+ ) : (
306
+ <CircleDashed className="h-4 w-4 text-muted-foreground" />
307
+ )}
308
+ {suggestionLabel(group)}
309
+ </div>
310
+ <div className="mt-1 text-xs leading-relaxed text-muted-foreground">
311
+ {group.suggestion.confidence} confidence. {group.suggestion.reason}
312
+ </div>
313
+ </TableCell>
314
+ <TableCell className="max-w-96 align-top">
315
+ <Link href={mergeHrefParams(group.sourceHref, rangeLinkParams)} title={group.sourceFile}>
316
+ <MonoText className="block truncate text-xs text-muted-foreground underline-offset-4 hover:underline">
317
+ {group.sourceFile}
318
+ </MonoText>
338
319
  </Link>
339
- {group.pricingHref ? (
340
- <Link href={mergeHrefParams(group.pricingHref, { returnTo: mergeHrefParams(group.itemHref, rangeLinkParams) })} className="font-medium text-muted-foreground underline-offset-4 hover:underline">
341
- Set model rate
320
+ </TableCell>
321
+ <TableCell className="align-top">{group.interactions.toLocaleString()}</TableCell>
322
+ <TableCell className="min-w-28 align-top">
323
+ <div>{formatTokens(group.totalTokens)}</div>
324
+ <div className="mt-1 text-xs text-muted-foreground">
325
+ {formatTokens(group.inputTokens)} in, {formatTokens(group.outputTokens)} out
326
+ </div>
327
+ </TableCell>
328
+ <TableCell className="align-top">
329
+ <div className="flex flex-wrap gap-2">
330
+ <Link
331
+ href={
332
+ group.pricingHref
333
+ ? mergeHrefParams(group.pricingHref, { returnTo: mergeHrefParams(group.itemHref, rangeLinkParams) })
334
+ : mergeHrefParams(group.repairHref, rangeLinkParams)
335
+ }
336
+ className="inline-flex items-center gap-1 font-medium text-primary underline-offset-4 hover:underline"
337
+ >
338
+ {group.pricingHref ? "Set model rate" : "Review parser"} <ArrowRight className="h-3.5 w-3.5" />
342
339
  </Link>
343
- ) : null}
344
- <Link href={mergeHrefParams(group.sourceHref, rangeLinkParams)} className="font-medium text-muted-foreground underline-offset-4 hover:underline">
345
- View evidence
346
- </Link>
347
- </div>
348
- </TableCell>
349
- </TableRow>
350
- ))}
351
- </TableBody>
352
- </Table>
340
+ <Link href={mergeHrefParams(group.itemHref, rangeLinkParams)} className="font-medium text-muted-foreground underline-offset-4 hover:underline">
341
+ Open repair
342
+ </Link>
343
+ <Link href={mergeHrefParams(group.sourceHref, rangeLinkParams)} className="font-medium text-muted-foreground underline-offset-4 hover:underline">
344
+ View evidence
345
+ </Link>
346
+ </div>
347
+ </TableCell>
348
+ </TableRow>
349
+ ))}
350
+ </TableBody>
351
+ </Table>
352
+ </div>
353
353
  ) : (
354
354
  <EmptyState
355
355
  title="No unknown-cost repair items"
@@ -22,6 +22,13 @@ function eventLabel(kind: SessionTimelineEventKind) {
22
22
  return kind.replace(/-/g, " ");
23
23
  }
24
24
 
25
+ function confidenceVariant(grade: string) {
26
+ if (grade === "high") return "success";
27
+ if (grade === "medium") return "warning";
28
+ if (grade === "low") return "destructive";
29
+ return "secondary";
30
+ }
31
+
25
32
  function SummaryTile({
26
33
  label,
27
34
  value,
@@ -146,6 +153,58 @@ export default async function SessionTimelinePage({
146
153
  />
147
154
  </div>
148
155
 
156
+ <Card>
157
+ <CardHeader className="flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between">
158
+ <div>
159
+ <CardTitle>Session Evidence</CardTitle>
160
+ <CardDescription>
161
+ Why this session costs what it costs, which token counts are exact or estimated, and what can be repaired.
162
+ </CardDescription>
163
+ </div>
164
+ <Badge variant={confidenceVariant(timeline.confidence.grade)}>
165
+ {timeline.confidence.score}/100 {timeline.confidence.grade} confidence
166
+ </Badge>
167
+ </CardHeader>
168
+ <CardContent className="grid gap-4 border-t p-4 lg:grid-cols-[1.2fr_0.8fr]">
169
+ <div className="grid gap-3 sm:grid-cols-3">
170
+ <div>
171
+ <FieldLabel>Token source</FieldLabel>
172
+ <div className="mt-1 text-sm font-medium">
173
+ {timeline.confidence.exactTokenInteractions.toLocaleString()} exact /{" "}
174
+ {timeline.confidence.tokenizerEstimateInteractions.toLocaleString()} tokenizer /{" "}
175
+ {timeline.confidence.simpleEstimateInteractions.toLocaleString()} simple
176
+ </div>
177
+ </div>
178
+ <div>
179
+ <FieldLabel>Cost coverage</FieldLabel>
180
+ <div className="mt-1 text-sm font-medium">
181
+ {timeline.confidence.pricedCostInteractions.toLocaleString()} priced /{" "}
182
+ {timeline.confidence.unknownCostInteractions.toLocaleString()} unknown
183
+ </div>
184
+ </div>
185
+ <div>
186
+ <FieldLabel>Spike clues</FieldLabel>
187
+ <div className="mt-1 text-sm font-medium">
188
+ {timeline.spikes.length ? `${timeline.spikes.length.toLocaleString()} token spikes` : "No token spikes"}
189
+ </div>
190
+ </div>
191
+ </div>
192
+ <div className="rounded-md border bg-muted/20 p-3">
193
+ <FieldLabel>Next repair step</FieldLabel>
194
+ <div className="mt-1 text-sm text-muted-foreground">
195
+ {timeline.repair.repairHref
196
+ ? `Unknown cost cause: ${timeline.repair.unknownCostCause}. Open the repair workbench for this source.`
197
+ : "No session-specific cost repair is required."}
198
+ </div>
199
+ {timeline.repair.repairHref ? (
200
+ <Button asChild size="sm" variant="outline" className="mt-3">
201
+ <Link href={timeline.repair.repairHref}>Open repair</Link>
202
+ </Button>
203
+ ) : null}
204
+ </div>
205
+ </CardContent>
206
+ </Card>
207
+
149
208
  <Card>
150
209
  <CardHeader>
151
210
  <CardTitle>Timeline</CardTitle>
package/bin/tokentrace.js CHANGED
@@ -30,7 +30,7 @@ Usage:
30
30
  tokentrace capabilities --json
31
31
  Alias for agent discovery manifest
32
32
  tokentrace roadmap --json
33
- Print Guided Operator release implementation status
33
+ Print Accuracy & Evidence release implementation status
34
34
  tokentrace scan Scan local AI CLI usage logs
35
35
  tokentrace doctor --json
36
36
  Inspect scan health and repair recommendations