tokentrace 0.5.1 → 0.6.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 (182) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/README.md +20 -4
  3. package/SECURITY.md +6 -3
  4. package/app/diagnostics/page.tsx +32 -0
  5. package/app/page.tsx +54 -6
  6. package/bin/tokentrace.js +94 -7
  7. package/components/period-filter.tsx +19 -17
  8. package/components/settings-panel.tsx +2 -2
  9. package/dist/runtime/db-migrate.mjs +1 -0
  10. package/dist/runtime/db-seed.mjs +13 -0
  11. package/dist/runtime/doctor.mjs +154 -1
  12. package/dist/runtime/insights.mjs +16 -0
  13. package/dist/runtime/pricing-refresh.mjs +13 -0
  14. package/dist/runtime/reset.mjs +13 -0
  15. package/dist/runtime/scan.mjs +17 -2
  16. package/dist/runtime/status.mjs +1 -0
  17. package/next.config.mjs +5 -0
  18. package/package.json +17 -18
  19. package/scripts/doctor.ts +2 -0
  20. package/scripts/package-inspect.mjs +51 -38
  21. package/scripts/smoke-cli.mjs +179 -0
  22. package/scripts/smoke-packed-install.mjs +180 -0
  23. package/src/db/client.ts +1 -0
  24. package/src/ingestion/adapters/claude-code.ts +2 -1
  25. package/src/ingestion/adapters/codex-cli.ts +2 -1
  26. package/src/lib/doctor.ts +17 -0
  27. package/src/lib/first-run-status.ts +128 -0
  28. package/src/lib/model-aliases.ts +14 -0
  29. package/src/lib/scan-health.ts +67 -1
  30. package/src/lib/support-matrix.ts +113 -0
  31. package/.next/BUILD_ID +0 -1
  32. package/.next/app-build-manifest.json +0 -192
  33. package/.next/app-path-routes-manifest.json +0 -24
  34. package/.next/build-manifest.json +0 -33
  35. package/.next/export-marker.json +0 -6
  36. package/.next/images-manifest.json +0 -58
  37. package/.next/next-minimal-server.js.nft.json +0 -1
  38. package/.next/next-server.js.nft.json +0 -1
  39. package/.next/package.json +0 -1
  40. package/.next/prerender-manifest.json +0 -66
  41. package/.next/react-loadable-manifest.json +0 -1
  42. package/.next/required-server-files.json +0 -323
  43. package/.next/routes-manifest.json +0 -125
  44. package/.next/server/app/_not-found/page.js +0 -1111
  45. package/.next/server/app/_not-found/page.js.nft.json +0 -1
  46. package/.next/server/app/_not-found/page_client-reference-manifest.js +0 -1
  47. package/.next/server/app/_not-found.html +0 -1
  48. package/.next/server/app/_not-found.meta +0 -8
  49. package/.next/server/app/_not-found.rsc +0 -40
  50. package/.next/server/app/api/analytics/route.js +0 -669
  51. package/.next/server/app/api/analytics/route.js.nft.json +0 -1
  52. package/.next/server/app/api/analytics/route_client-reference-manifest.js +0 -1
  53. package/.next/server/app/api/data/route.js +0 -1355
  54. package/.next/server/app/api/data/route.js.nft.json +0 -1
  55. package/.next/server/app/api/data/route_client-reference-manifest.js +0 -1
  56. package/.next/server/app/api/export/route.js +0 -545
  57. package/.next/server/app/api/export/route.js.nft.json +0 -1
  58. package/.next/server/app/api/export/route_client-reference-manifest.js +0 -1
  59. package/.next/server/app/api/files/route.js +0 -512
  60. package/.next/server/app/api/files/route.js.nft.json +0 -1
  61. package/.next/server/app/api/files/route_client-reference-manifest.js +0 -1
  62. package/.next/server/app/api/prices/refresh/route.js +0 -1449
  63. package/.next/server/app/api/prices/refresh/route.js.nft.json +0 -1
  64. package/.next/server/app/api/prices/refresh/route_client-reference-manifest.js +0 -1
  65. package/.next/server/app/api/prices/route.js +0 -1382
  66. package/.next/server/app/api/prices/route.js.nft.json +0 -1
  67. package/.next/server/app/api/prices/route_client-reference-manifest.js +0 -1
  68. package/.next/server/app/api/scan/route.js +0 -3066
  69. package/.next/server/app/api/scan/route.js.nft.json +0 -1
  70. package/.next/server/app/api/scan/route_client-reference-manifest.js +0 -1
  71. package/.next/server/app/api/settings/route.js +0 -1076
  72. package/.next/server/app/api/settings/route.js.nft.json +0 -1
  73. package/.next/server/app/api/settings/route_client-reference-manifest.js +0 -1
  74. package/.next/server/app/debug/page.js +0 -1638
  75. package/.next/server/app/debug/page.js.nft.json +0 -1
  76. package/.next/server/app/debug/page_client-reference-manifest.js +0 -1
  77. package/.next/server/app/diagnostics/page.js +0 -3045
  78. package/.next/server/app/diagnostics/page.js.nft.json +0 -1
  79. package/.next/server/app/diagnostics/page_client-reference-manifest.js +0 -1
  80. package/.next/server/app/discovery/page.js +0 -1729
  81. package/.next/server/app/discovery/page.js.nft.json +0 -1
  82. package/.next/server/app/discovery/page_client-reference-manifest.js +0 -1
  83. package/.next/server/app/icon.svg/route.js +0 -469
  84. package/.next/server/app/icon.svg/route.js.nft.json +0 -1
  85. package/.next/server/app/icon.svg.body +0 -10
  86. package/.next/server/app/icon.svg.meta +0 -1
  87. package/.next/server/app/models/page.js +0 -1789
  88. package/.next/server/app/models/page.js.nft.json +0 -1
  89. package/.next/server/app/models/page_client-reference-manifest.js +0 -1
  90. package/.next/server/app/optimisation/page.js +0 -1602
  91. package/.next/server/app/optimisation/page.js.nft.json +0 -1
  92. package/.next/server/app/optimisation/page_client-reference-manifest.js +0 -1
  93. package/.next/server/app/page.js +0 -3113
  94. package/.next/server/app/page.js.nft.json +0 -1
  95. package/.next/server/app/page_client-reference-manifest.js +0 -1
  96. package/.next/server/app/parser-debug/page.js +0 -1646
  97. package/.next/server/app/parser-debug/page.js.nft.json +0 -1
  98. package/.next/server/app/parser-debug/page_client-reference-manifest.js +0 -1
  99. package/.next/server/app/pricing/page.js +0 -2590
  100. package/.next/server/app/pricing/page.js.nft.json +0 -1
  101. package/.next/server/app/pricing/page_client-reference-manifest.js +0 -1
  102. package/.next/server/app/projects/page.js +0 -1967
  103. package/.next/server/app/projects/page.js.nft.json +0 -1
  104. package/.next/server/app/projects/page_client-reference-manifest.js +0 -1
  105. package/.next/server/app/sessions/page.js +0 -2526
  106. package/.next/server/app/sessions/page.js.nft.json +0 -1
  107. package/.next/server/app/sessions/page_client-reference-manifest.js +0 -1
  108. package/.next/server/app/settings/page.js +0 -2467
  109. package/.next/server/app/settings/page.js.nft.json +0 -1
  110. package/.next/server/app/settings/page_client-reference-manifest.js +0 -1
  111. package/.next/server/app/tools/page.js +0 -1745
  112. package/.next/server/app/tools/page.js.nft.json +0 -1
  113. package/.next/server/app/tools/page_client-reference-manifest.js +0 -1
  114. package/.next/server/app-paths-manifest.json +0 -24
  115. package/.next/server/chunks/287.js +0 -1650
  116. package/.next/server/chunks/331.js +0 -7896
  117. package/.next/server/chunks/366.js +0 -32801
  118. package/.next/server/chunks/483.js +0 -31264
  119. package/.next/server/chunks/576.js +0 -797
  120. package/.next/server/chunks/611.js +0 -2979
  121. package/.next/server/chunks/692.js +0 -905
  122. package/.next/server/chunks/722.js +0 -6318
  123. package/.next/server/chunks/868.js +0 -730
  124. package/.next/server/functions-config-manifest.json +0 -4
  125. package/.next/server/interception-route-rewrite-manifest.js +0 -1
  126. package/.next/server/middleware-build-manifest.js +0 -1
  127. package/.next/server/middleware-manifest.json +0 -6
  128. package/.next/server/middleware-react-loadable-manifest.js +0 -1
  129. package/.next/server/next-font-manifest.js +0 -1
  130. package/.next/server/next-font-manifest.json +0 -1
  131. package/.next/server/pages/404.html +0 -1
  132. package/.next/server/pages/500.html +0 -1
  133. package/.next/server/pages/_app.js +0 -277
  134. package/.next/server/pages/_app.js.nft.json +0 -1
  135. package/.next/server/pages/_document.js +0 -46
  136. package/.next/server/pages/_document.js.nft.json +0 -1
  137. package/.next/server/pages/_error.js +0 -6315
  138. package/.next/server/pages/_error.js.nft.json +0 -1
  139. package/.next/server/pages-manifest.json +0 -6
  140. package/.next/server/server-reference-manifest.js +0 -1
  141. package/.next/server/server-reference-manifest.json +0 -1
  142. package/.next/server/webpack-runtime.js +0 -207
  143. package/.next/static/cNv_sOa0AntpwuwvPjuRA/_buildManifest.js +0 -1
  144. package/.next/static/cNv_sOa0AntpwuwvPjuRA/_ssgManifest.js +0 -1
  145. package/.next/static/chunks/125-ab0f8db8f84c1166.js +0 -1
  146. package/.next/static/chunks/255-e881f48ae1d2333a.js +0 -1
  147. package/.next/static/chunks/4bd1b696-409494caf8c83275.js +0 -1
  148. package/.next/static/chunks/619-f072ac750404f9da.js +0 -1
  149. package/.next/static/chunks/850-8bc31e41590b5831.js +0 -1
  150. package/.next/static/chunks/938-23236de1c47554ea.js +0 -1
  151. package/.next/static/chunks/app/_not-found/page-73368c3ff767c206.js +0 -1
  152. package/.next/static/chunks/app/api/analytics/route-73368c3ff767c206.js +0 -1
  153. package/.next/static/chunks/app/api/data/route-73368c3ff767c206.js +0 -1
  154. package/.next/static/chunks/app/api/export/route-73368c3ff767c206.js +0 -1
  155. package/.next/static/chunks/app/api/files/route-73368c3ff767c206.js +0 -1
  156. package/.next/static/chunks/app/api/prices/refresh/route-73368c3ff767c206.js +0 -1
  157. package/.next/static/chunks/app/api/prices/route-73368c3ff767c206.js +0 -1
  158. package/.next/static/chunks/app/api/scan/route-73368c3ff767c206.js +0 -1
  159. package/.next/static/chunks/app/api/settings/route-73368c3ff767c206.js +0 -1
  160. package/.next/static/chunks/app/debug/page-73368c3ff767c206.js +0 -1
  161. package/.next/static/chunks/app/diagnostics/page-3b65a4b6734f0c62.js +0 -1
  162. package/.next/static/chunks/app/discovery/page-73368c3ff767c206.js +0 -1
  163. package/.next/static/chunks/app/global-error-88b965bb96071bef.js +0 -1
  164. package/.next/static/chunks/app/layout-234c4399a92e808b.js +0 -1
  165. package/.next/static/chunks/app/models/page-b2b7c974754ac991.js +0 -1
  166. package/.next/static/chunks/app/not-found-0644ad2dcc40cfd9.js +0 -1
  167. package/.next/static/chunks/app/optimisation/page-73368c3ff767c206.js +0 -1
  168. package/.next/static/chunks/app/page-509085a4fdf00d49.js +0 -1
  169. package/.next/static/chunks/app/parser-debug/page-73368c3ff767c206.js +0 -1
  170. package/.next/static/chunks/app/pricing/page-d8419f42f65f7429.js +0 -1
  171. package/.next/static/chunks/app/projects/page-a49976ccd65bd81a.js +0 -1
  172. package/.next/static/chunks/app/sessions/page-86f6b8c220f4aa95.js +0 -1
  173. package/.next/static/chunks/app/settings/page-b2d78790be6a114d.js +0 -1
  174. package/.next/static/chunks/app/tools/page-b2b7c974754ac991.js +0 -1
  175. package/.next/static/chunks/framework-3457b9c2619cdd96.js +0 -1
  176. package/.next/static/chunks/main-8744520a8a31e6ae.js +0 -1
  177. package/.next/static/chunks/main-app-039335219a472e20.js +0 -1
  178. package/.next/static/chunks/pages/_app-5addca2b3b969fde.js +0 -1
  179. package/.next/static/chunks/pages/_error-022e4ac7bbb9914f.js +0 -1
  180. package/.next/static/chunks/polyfills-42372ed130431b0a.js +0 -1
  181. package/.next/static/chunks/webpack-3fcacae817f3ffab.js +0 -1
  182. package/.next/static/css/46d6a87bebe3b542.css +0 -3
package/CHANGELOG.md CHANGED
@@ -4,6 +4,37 @@ All notable changes to TokenTrace are documented here.
4
4
 
5
5
  ## Unreleased
6
6
 
7
+ ## [0.6.0] - 2026-05-10
8
+
9
+ ### Added
10
+
11
+ - 0.6.0 Stable Daily Tool roadmap with explicit release cards and gates.
12
+ - Support matrix for stable, best-effort, ignored, and unsupported TokenTrace surfaces.
13
+ - Scan Doctor support matrix and scan freshness status.
14
+ - Overview first-run checklist that explains missing roots, no scans, zero imports, and next actions.
15
+ - `npm run smoke:cli` for clean-home CLI checks across scan, serve, doctor, status, Claude status-line, and watch-mode commands.
16
+ - `npm run smoke:packed` for installing the packed tarball into a temp project and verifying the published CLI entrypoint.
17
+
18
+ ### Changed
19
+
20
+ - Claude Code and Codex JSONL parsers now keep valid records when a transcript contains malformed lines.
21
+ - Model alias suggestions now handle OpenAI/Codex provider-prefixed and dated snapshot names.
22
+ - Overview period filter keeps the custom date fields and Apply button visible on desktop while presets absorb overflow.
23
+ - Local development config declares localhost/127.0.0.1 dev origins and disables the standard Next.js dev indicator preference.
24
+ - README documents the support matrix and unsupported product boundaries.
25
+ - `npm run release:check` now includes CLI and packed-install smoke checks before package security inspection.
26
+ - The npm package now excludes generated `.next` output and prepares the dashboard build in the user's TokenTrace app-data directory on first `tokentrace serve`.
27
+ - Package trust inspection now verifies the publish tarball directly and fails if generated Next.js build output is included.
28
+ - Packed-install smoke now starts `tokentrace serve` from the packed tarball and allows dependency install scripts needed by native SQLite bindings while keeping TokenTrace itself free of lifecycle scripts.
29
+ - Dev security posture is cleaner: Vitest is updated to the Node 18-compatible patched line, and the unused `drizzle-kit` dev dependency has been removed.
30
+ - `npm run security:package` now audits the full dependency graph at moderate-or-higher severity instead of checking only production high-severity advisories.
31
+
32
+ ### Fixed
33
+
34
+ - Package inspection no longer depends on the user's global npm cache, which can contain root-owned files on some machines.
35
+ - First-run dashboard preparation from a packed install no longer hits SQLite `database is locked` errors while Next.js imports API routes during build.
36
+ - `tokentrace serve` now exits nonzero when the underlying Next.js server fails before readiness.
37
+
7
38
  ## [0.5.1] - 2026-05-10
8
39
 
9
40
  ### Added
package/README.md CHANGED
@@ -235,11 +235,12 @@ Stop the server with `Ctrl+C` in the terminal where `tokentrace` is running.
235
235
 
236
236
  ## Package Trust
237
237
 
238
- - The npm package has no `preinstall`, `install`, or `postinstall` scripts.
239
- - Published Next.js server bundles are intentionally left readable instead of server-minified so Socket, npm, maintainers, and users can inspect generated runtime code.
240
- - `npm run package:inspect` fails if generated app route bundles look packed or unreadable.
238
+ - The TokenTrace npm package has no `preinstall`, `install`, or `postinstall` scripts.
239
+ - The published package ships readable application source and the compiled CLI runtime, not generated `.next/server` route bundles.
240
+ - `tokentrace serve` prepares the local dashboard build in the user's TokenTrace app-data directory the first time it is needed.
241
+ - `npm run package:inspect` fails if generated Next.js build output appears in the published tarball.
241
242
  - Public npm publishing is configured through GitHub Actions Trusted Publishing and provenance from version tags.
242
- - Socket GitHub checks and ProjScan are used as release guardrails, alongside `npm audit --omit=dev --audit-level=high`.
243
+ - Socket GitHub checks and ProjScan are used as release guardrails, alongside `npm audit --audit-level=moderate`.
243
244
  - Release notes are published directly in GitHub Releases from the relevant changelog section, not as a link-only summary.
244
245
 
245
246
  See [SECURITY.md](SECURITY.md) for the full security and privacy model.
@@ -291,6 +292,21 @@ Adapters live under `src/ingestion/adapters/`:
291
292
 
292
293
  Formats for Claude Code and Codex CLI can vary across versions, so these adapters are defensive and best-effort. Unknown files fail safely and show warnings in the Raw Data page.
293
294
 
295
+ ## Support Matrix
296
+
297
+ TokenTrace keeps a visible support contract so daily scans are easier to trust:
298
+
299
+ | Surface | Support level | Notes |
300
+ | --- | --- | --- |
301
+ | Claude Code project transcripts | Stable | Primary local CLI ingestion source. |
302
+ | Codex CLI session artifacts | Best-effort | Parsed defensively while CLI formats evolve. |
303
+ | Generic JSONL, JSON, and text logs | Best-effort | Conservative usage-shaped records only. |
304
+ | Claude/Codex cache, plugin, todo, config, and support files | Ignored | Tracked as non-usage files, not parser failures. |
305
+ | Editable model pricing | Stable | Local pricing rows drive costs and unknown-cost repair queues. |
306
+ | Claude Code status line | Stable | Uses Claude Code's documented statusLine stdin contract. |
307
+ | Codex sticky status line | Best-effort fallback | Use `tokentrace watch --session --compact` in a split or tmux pane. |
308
+ | Desktop app scraping, browser extensions, proxying, packet capture, telemetry | Unsupported | Outside TokenTrace's product boundary. |
309
+
294
310
  ## Extending Parsers
295
311
 
296
312
  Example generic JSONL fixtures are in `fixtures/generic-jsonl/`.
package/SECURITY.md CHANGED
@@ -8,14 +8,17 @@ downloads.
8
8
 
9
9
  - No telemetry, cloud sync, traffic interception, proxying, packet sniffing, or
10
10
  browser extension is part of the product.
11
- - No `preinstall`, `install`, or `postinstall` npm lifecycle scripts are used.
11
+ - The TokenTrace package has no `preinstall`, `install`, or `postinstall`
12
+ npm lifecycle scripts.
12
13
  - Runtime state is stored locally in the user's TokenTrace app-data directory.
13
14
  - Raw full prompts and responses are off by default.
14
15
  - Pricing refresh downloads a public model-pricing manifest only. It does not
15
16
  send usage logs, prompts, file paths, analytics, or identifiers. Set
16
17
  `TOKENTRACE_DISABLE_PRICE_REFRESH=1` to use bundled prices only.
17
- - Generated Next.js server bundles are intentionally published without server
18
- minification so package scanners and maintainers can inspect them.
18
+ - The published package ships readable application source and the compiled CLI
19
+ runtime, not generated `.next/server` route bundles. `tokentrace serve`
20
+ prepares the local dashboard build in the user's app-data directory when
21
+ needed.
19
22
 
20
23
  ## What TokenTrace Reads
21
24
 
@@ -176,6 +176,13 @@ function DoctorReportPanel({ report }: { report: DoctorReport }) {
176
176
  </div>
177
177
  </div>
178
178
 
179
+ <div className="rounded-md border bg-muted/30 p-3">
180
+ <div className="text-sm font-semibold">Scan freshness</div>
181
+ <div className="mt-1 text-xs leading-relaxed text-muted-foreground">
182
+ {report.scanFreshness.description}
183
+ </div>
184
+ </div>
185
+
179
186
  {report.latestScan.zeroImportExplanation ? (
180
187
  <div className="rounded-md border border-amber-300 bg-amber-50 p-3 text-sm text-amber-950">
181
188
  {report.latestScan.zeroImportExplanation}
@@ -224,6 +231,31 @@ function DoctorReportPanel({ report }: { report: DoctorReport }) {
224
231
  ))}
225
232
  </div>
226
233
  </div>
234
+
235
+ <div className="space-y-3">
236
+ <div>
237
+ <div className="text-sm font-semibold">Support matrix</div>
238
+ <div className="mt-1 text-xs text-muted-foreground">
239
+ {report.supportMatrix.summary.stable.toLocaleString()} stable,{" "}
240
+ {report.supportMatrix.summary.bestEffort.toLocaleString()} best-effort,{" "}
241
+ {report.supportMatrix.summary.ignored.toLocaleString()} ignored,{" "}
242
+ {report.supportMatrix.summary.unsupported.toLocaleString()} unsupported.
243
+ </div>
244
+ </div>
245
+ <div className="grid divide-y border-y lg:grid-cols-2 lg:divide-x lg:divide-y-0">
246
+ {report.supportMatrix.items.map((item) => (
247
+ <div key={item.id} className="p-3">
248
+ <div className="flex flex-wrap items-center gap-2">
249
+ <div className="text-sm font-semibold">{item.label}</div>
250
+ <Badge variant={item.level === "stable" ? "success" : item.level === "unsupported" ? "destructive" : item.level === "best-effort" ? "warning" : "secondary"}>
251
+ {item.level}
252
+ </Badge>
253
+ </div>
254
+ <div className="mt-1 text-xs leading-relaxed text-muted-foreground">{item.description}</div>
255
+ </div>
256
+ ))}
257
+ </div>
258
+ </div>
227
259
  </CardContent>
228
260
  </Card>
229
261
  );
package/app/page.tsx CHANGED
@@ -1,6 +1,5 @@
1
1
  import Link from "next/link";
2
2
  import { ArrowRight, Coins, Database, MessageSquare, Minus, Sparkles, TrendingDown, TrendingUp } from "lucide-react";
3
- import { EmptyState } from "@/components/empty-state";
4
3
  import { RankBarChart } from "@/components/charts/rank-bar-chart";
5
4
  import { TrendChart } from "@/components/charts/trend-chart";
6
5
  import { PeriodFilter } from "@/components/period-filter";
@@ -10,7 +9,10 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/com
10
9
  import { HelpTooltip } from "@/components/ui/help-tooltip";
11
10
  import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
12
11
  import { DataValue, FieldLabel, MonoText, PageHeader } from "@/components/ui/typography";
13
- import { getAnalyticsData } from "@/src/lib/analytics";
12
+ import { getAnalyticsData, getScanTrustData } from "@/src/lib/analytics";
13
+ import { buildDoctorReport } from "@/src/lib/doctor";
14
+ import { getDefaultSearchRoots } from "@/src/ingestion/discovery";
15
+ import { buildFirstRunStatus, type FirstRunStatus } from "@/src/lib/first-run-status";
14
16
  import { resolveDateRange } from "@/src/lib/date-range";
15
17
  import { formatCurrency, formatTokens, percent } from "@/src/lib/format";
16
18
  import { cn } from "@/src/lib/utils";
@@ -125,6 +127,39 @@ function DeltaMetric({
125
127
  );
126
128
  }
127
129
 
130
+ function FirstRunPanel({ status }: { status: FirstRunStatus }) {
131
+ return (
132
+ <Card className={status.tone === "warning" ? "border-amber-300 bg-amber-50/50" : undefined}>
133
+ <CardHeader className="flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between">
134
+ <div>
135
+ <CardTitle>{status.title}</CardTitle>
136
+ <CardDescription>{status.description}</CardDescription>
137
+ </div>
138
+ <Button asChild variant={status.tone === "warning" ? "outline" : "default"}>
139
+ <Link href={status.primaryAction.href}>
140
+ {status.primaryAction.label} <ArrowRight className="h-4 w-4" />
141
+ </Link>
142
+ </Button>
143
+ </CardHeader>
144
+ <CardContent>
145
+ <div className="grid divide-y border-y md:grid-cols-5 md:divide-x md:divide-y-0">
146
+ {status.checks.map((check) => (
147
+ <div key={check.id} className="p-3">
148
+ <div className="flex flex-wrap items-center gap-2">
149
+ <div className="text-sm font-semibold">{check.label}</div>
150
+ <Badge variant={check.state === "pass" ? "success" : check.state === "warn" ? "warning" : "secondary"}>
151
+ {check.state}
152
+ </Badge>
153
+ </div>
154
+ <div className="mt-1 text-xs leading-relaxed text-muted-foreground">{check.detail}</div>
155
+ </div>
156
+ ))}
157
+ </div>
158
+ </CardContent>
159
+ </Card>
160
+ );
161
+ }
162
+
128
163
  type OverviewPageProps = {
129
164
  searchParams?: Promise<Record<string, string | string[] | undefined>>;
130
165
  };
@@ -133,6 +168,22 @@ export default async function OverviewPage({ searchParams }: OverviewPageProps)
133
168
  const params = (await searchParams) ?? {};
134
169
  const range = resolveDateRange(params);
135
170
  const data = getAnalyticsData(range.filters);
171
+ const trust = getScanTrustData();
172
+ const roots = await getDefaultSearchRoots();
173
+ const doctorReport = buildDoctorReport({ ...trust, roots });
174
+ const firstRunStatus = buildFirstRunStatus({
175
+ rootCount: roots.length,
176
+ pricedModelCount: trust.pricedModelCount,
177
+ latestScan: doctorReport.latestScan.id
178
+ ? {
179
+ filesScanned: doctorReport.latestScan.filesScanned,
180
+ recordsImported: doctorReport.latestScan.recordsImported,
181
+ zeroImportExplanation: doctorReport.latestScan.zeroImportExplanation
182
+ }
183
+ : null,
184
+ interactions: trust.confidence.interactions,
185
+ unknownCostInteractions: trust.confidence.unknownCostInteractions
186
+ });
136
187
  const { summary } = data;
137
188
 
138
189
  return (
@@ -152,10 +203,7 @@ export default async function OverviewPage({ searchParams }: OverviewPageProps)
152
203
  <PeriodFilter range={range} />
153
204
 
154
205
  {summary.interactions === 0 ? (
155
- <EmptyState
156
- title="No usage imported yet"
157
- description="Add custom folders if needed, run a scan from Settings, then return here for analytics."
158
- />
206
+ <FirstRunPanel status={firstRunStatus} />
159
207
  ) : null}
160
208
 
161
209
  <Card>
package/bin/tokentrace.js CHANGED
@@ -94,6 +94,96 @@ function nextBin() {
94
94
  }
95
95
  }
96
96
 
97
+ function dashboardBuildId() {
98
+ return path.join(dashboardWorkdir(), ".next", "BUILD_ID");
99
+ }
100
+
101
+ function dashboardWorkdir() {
102
+ return path.join(appDataDir(), "dashboard-runtime");
103
+ }
104
+
105
+ function dashboardBuildMarker() {
106
+ return path.join(dashboardWorkdir(), ".tokentrace-dashboard-version");
107
+ }
108
+
109
+ function dependencyModulesDir() {
110
+ const localNodeModules = path.join(packageRoot, "node_modules");
111
+ if (fs.existsSync(localNodeModules)) return localNodeModules;
112
+
113
+ const parent = path.dirname(packageRoot);
114
+ if (path.basename(parent) === "node_modules") return parent;
115
+
116
+ return localNodeModules;
117
+ }
118
+
119
+ function copyDashboardSource(targetRoot) {
120
+ const directories = ["app", "components", "pricing", "public", "src"];
121
+ const files = [
122
+ "components.json",
123
+ "next.config.mjs",
124
+ "package.json",
125
+ "postcss.config.mjs",
126
+ "tailwind.config.ts",
127
+ "tsconfig.json"
128
+ ];
129
+
130
+ fs.rmSync(targetRoot, { recursive: true, force: true });
131
+ fs.mkdirSync(targetRoot, { recursive: true });
132
+
133
+ for (const directory of directories) {
134
+ const source = path.join(packageRoot, directory);
135
+ if (fs.existsSync(source)) {
136
+ fs.cpSync(source, path.join(targetRoot, directory), { recursive: true });
137
+ }
138
+ }
139
+
140
+ for (const file of files) {
141
+ const source = path.join(packageRoot, file);
142
+ if (fs.existsSync(source)) {
143
+ fs.copyFileSync(source, path.join(targetRoot, file));
144
+ }
145
+ }
146
+
147
+ const nodeModulesTarget = dependencyModulesDir();
148
+ const nodeModulesLink = path.join(targetRoot, "node_modules");
149
+ fs.symlinkSync(
150
+ nodeModulesTarget,
151
+ nodeModulesLink,
152
+ process.platform === "win32" ? "junction" : "dir"
153
+ );
154
+ }
155
+
156
+ function runNextBuild(cwd) {
157
+ return new Promise((resolve, reject) => {
158
+ const child = spawn(process.execPath, [nextBin(), "build"], {
159
+ cwd,
160
+ env: runtimeEnv(),
161
+ stdio: "inherit"
162
+ });
163
+ child.on("error", reject);
164
+ child.on("exit", (code) => {
165
+ if (code === 0) resolve();
166
+ else reject(new Error(`next build exited with code ${code}`));
167
+ });
168
+ });
169
+ }
170
+
171
+ async function ensureDashboardBuild() {
172
+ const workdir = dashboardWorkdir();
173
+ const marker = dashboardBuildMarker();
174
+ const builtVersion = fs.existsSync(marker) ? fs.readFileSync(marker, "utf8").trim() : null;
175
+ if (fs.existsSync(dashboardBuildId()) && builtVersion === packageJson.version) {
176
+ return workdir;
177
+ }
178
+
179
+ console.log("Preparing TokenTrace dashboard for this install...");
180
+ console.log("This runs locally and may take a moment the first time.");
181
+ copyDashboardSource(workdir);
182
+ await runNextBuild(workdir);
183
+ fs.writeFileSync(marker, `${packageJson.version}\n`);
184
+ return workdir;
185
+ }
186
+
97
187
  function runtimeScriptPath(scriptName) {
98
188
  const compiled = path.join(packageRoot, "dist", "runtime", `${scriptName}.mjs`);
99
189
  if (fs.existsSync(compiled)) return compiled;
@@ -220,13 +310,8 @@ async function serve(args = []) {
220
310
  return;
221
311
  }
222
312
 
223
- const buildId = path.join(packageRoot, ".next", "BUILD_ID");
224
- if (!fs.existsSync(buildId)) {
225
- console.error("TokenTrace is not built yet. Run `npm run build` before using the package CLI from a source checkout.");
226
- process.exit(1);
227
- }
228
-
229
313
  await initializeDatabase();
314
+ const dashboardRoot = await ensureDashboardBuild();
230
315
  const hostname = options.hostname;
231
316
  const port = options.port ?? (await getPort({ port: portNumbers(3030, 3999), host: hostname }));
232
317
  const url = `http://${hostname}:${port}`;
@@ -238,7 +323,7 @@ async function serve(args = []) {
238
323
  process.execPath,
239
324
  [nextBin(), "start", "--hostname", hostname, "--port", String(port)],
240
325
  {
241
- cwd: packageRoot,
326
+ cwd: dashboardRoot,
242
327
  env: {
243
328
  ...runtimeEnv(),
244
329
  PORT: String(port),
@@ -263,6 +348,8 @@ async function serve(args = []) {
263
348
  }
264
349
  } catch (error) {
265
350
  console.error(error instanceof Error ? error.message : "Failed to start TokenTrace.");
351
+ stop();
352
+ process.exit(1);
266
353
  }
267
354
 
268
355
  child.on("exit", (code) => process.exit(code ?? 0));
@@ -16,36 +16,38 @@ export function PeriodFilter({ range }: { range: ResolvedDateRange }) {
16
16
  <div className="rounded-lg bg-card p-3 outline outline-1 outline-border sm:p-4">
17
17
  <form className="overflow-x-auto" action="/">
18
18
  <input type="hidden" name="range" value="custom" />
19
- <div className="flex min-w-max items-center gap-2">
20
- <div className="flex items-center gap-2 pr-1 text-sm font-semibold">
19
+ <div className="flex min-w-[720px] items-center gap-2">
20
+ <div className="flex shrink-0 items-center gap-2 pr-1 text-sm font-semibold">
21
21
  <CalendarDays className="h-4 w-4 shrink-0 text-muted-foreground" />
22
22
  <span>Period</span>
23
- <span className="whitespace-nowrap rounded-md bg-muted px-2 py-0.5 text-xs font-medium text-muted-foreground">
23
+ <span className="hidden whitespace-nowrap rounded-md bg-muted px-2 py-0.5 text-xs font-medium text-muted-foreground 2xl:inline-flex">
24
24
  {statusLabel}
25
25
  </span>
26
26
  </div>
27
27
  <div className="h-6 w-px shrink-0 bg-border" />
28
- <div className="flex items-center gap-1.5">
29
- {dateRangeOptions.map((option) => (
30
- <Button
31
- key={option.key}
32
- asChild
33
- size="sm"
34
- variant={range.key === option.key ? "default" : "outline"}
35
- >
36
- <Link href={rangeHref(option.key)}>{option.label}</Link>
37
- </Button>
38
- ))}
28
+ <div className="min-w-0 flex-1 overflow-x-auto">
29
+ <div className="flex w-max items-center gap-1.5 pr-1">
30
+ {dateRangeOptions.map((option) => (
31
+ <Button
32
+ key={option.key}
33
+ asChild
34
+ size="sm"
35
+ variant={range.key === option.key ? "default" : "outline"}
36
+ >
37
+ <Link href={rangeHref(option.key)}>{option.label}</Link>
38
+ </Button>
39
+ ))}
40
+ </div>
39
41
  </div>
40
42
  <div className="h-6 w-px shrink-0 bg-border" />
41
- <div className="flex items-center gap-1.5">
43
+ <div className="flex shrink-0 items-center gap-1.5">
42
44
  <label className="flex items-center gap-1.5 text-xs text-muted-foreground">
43
45
  <span>From</span>
44
- <Input type="date" name="from" defaultValue={range.fromInput} className="period-date-input h-8 w-36" />
46
+ <Input type="date" name="from" defaultValue={range.fromInput} className="period-date-input h-8 w-32" />
45
47
  </label>
46
48
  <label className="flex items-center gap-1.5 text-xs text-muted-foreground">
47
49
  <span>To</span>
48
- <Input type="date" name="to" defaultValue={range.toInput} className="period-date-input h-8 w-36" />
50
+ <Input type="date" name="to" defaultValue={range.toInput} className="period-date-input h-8 w-32" />
49
51
  </label>
50
52
  <Button size="sm" type="submit" variant={range.key === "custom" ? "default" : "outline"}>
51
53
  Apply
@@ -164,7 +164,7 @@ export function SettingsPanel({
164
164
  {
165
165
  label: "Install scripts",
166
166
  value: "None",
167
- detail: "No preinstall, install, or postinstall lifecycle scripts."
167
+ detail: "The TokenTrace package has no preinstall, install, or postinstall lifecycle scripts."
168
168
  },
169
169
  {
170
170
  label: "Network behavior",
@@ -174,7 +174,7 @@ export function SettingsPanel({
174
174
  {
175
175
  label: "Release proof",
176
176
  value: "Tag based",
177
- detail: "Future npm releases publish through GitHub Trusted Publishing."
177
+ detail: "npm releases publish through GitHub Trusted Publishing."
178
178
  }
179
179
  ].map((item) => (
180
180
  <div key={item.label} className="p-3">
@@ -410,6 +410,7 @@ function databaseUrlPath(value) {
410
410
  var dbPath = process.env.TOKENTRACE_DB ?? databaseUrlPath(process.env.DATABASE_URL) ?? defaultDbPath;
411
411
  fs.mkdirSync(path.dirname(dbPath), { recursive: true });
412
412
  var sqlite = new Database(dbPath);
413
+ sqlite.pragma("busy_timeout = 10000");
413
414
  sqlite.pragma("foreign_keys = ON");
414
415
  applyMigrations(sqlite);
415
416
  var db = drizzle(sqlite, { schema: schema_exports });
@@ -410,6 +410,7 @@ function databaseUrlPath(value) {
410
410
  var dbPath = process.env.TOKENTRACE_DB ?? databaseUrlPath(process.env.DATABASE_URL) ?? defaultDbPath;
411
411
  fs.mkdirSync(path.dirname(dbPath), { recursive: true });
412
412
  var sqlite = new Database(dbPath);
413
+ sqlite.pragma("busy_timeout = 10000");
413
414
  sqlite.pragma("foreign_keys = ON");
414
415
  applyMigrations(sqlite);
415
416
  var db = drizzle(sqlite, { schema: schema_exports });
@@ -1230,11 +1231,23 @@ function addClaudeCandidates(name, candidates) {
1230
1231
  candidates.push(`${prefix}-${family}-${major}.${minor}`);
1231
1232
  }
1232
1233
  }
1234
+ function addProviderPrefixCandidates(name, candidates) {
1235
+ const withoutProvider = name.replace(/^[a-z0-9_.-]+\//i, "");
1236
+ if (withoutProvider !== name) candidates.push(withoutProvider);
1237
+ }
1238
+ function addSnapshotDateCandidates(name, candidates) {
1239
+ const withoutProvider = name.replace(/^[a-z0-9_.-]+\//i, "");
1240
+ const lower = withoutProvider.toLowerCase();
1241
+ const stripped = lower.replace(/[-_.]\d{4}[-_.]?\d{2}[-_.]?\d{2}$/, "");
1242
+ if (stripped !== lower) candidates.push(stripped);
1243
+ }
1233
1244
  function modelNameCandidates(modelName) {
1234
1245
  const trimmed = modelName?.trim();
1235
1246
  if (!trimmed) return ["unknown"];
1236
1247
  const candidates = [trimmed];
1248
+ addProviderPrefixCandidates(trimmed, candidates);
1237
1249
  addClaudeCandidates(trimmed, candidates);
1250
+ addSnapshotDateCandidates(trimmed, candidates);
1238
1251
  return unique(candidates);
1239
1252
  }
1240
1253