tokentrace 0.5.0 → 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 (183) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/README.md +24 -4
  3. package/SECURITY.md +6 -3
  4. package/app/diagnostics/page.tsx +32 -0
  5. package/app/icon.svg +10 -0
  6. package/app/layout.tsx +7 -3
  7. package/app/page.tsx +54 -6
  8. package/bin/tokentrace.js +94 -7
  9. package/components/period-filter.tsx +19 -17
  10. package/components/settings-panel.tsx +2 -2
  11. package/components/sidebar.tsx +2 -4
  12. package/components/token-trace-logo.tsx +46 -0
  13. package/dist/runtime/db-migrate.mjs +1 -0
  14. package/dist/runtime/db-seed.mjs +13 -0
  15. package/dist/runtime/doctor.mjs +154 -1
  16. package/dist/runtime/insights.mjs +16 -0
  17. package/dist/runtime/pricing-refresh.mjs +13 -0
  18. package/dist/runtime/reset.mjs +13 -0
  19. package/dist/runtime/scan.mjs +17 -2
  20. package/dist/runtime/status.mjs +1 -0
  21. package/docs/assets/tokentrace-logo.svg +10 -0
  22. package/next.config.mjs +5 -0
  23. package/package.json +17 -18
  24. package/scripts/doctor.ts +2 -0
  25. package/scripts/package-inspect.mjs +51 -38
  26. package/scripts/smoke-cli.mjs +179 -0
  27. package/scripts/smoke-packed-install.mjs +180 -0
  28. package/src/db/client.ts +1 -0
  29. package/src/ingestion/adapters/claude-code.ts +2 -1
  30. package/src/ingestion/adapters/codex-cli.ts +2 -1
  31. package/src/lib/doctor.ts +17 -0
  32. package/src/lib/first-run-status.ts +128 -0
  33. package/src/lib/model-aliases.ts +14 -0
  34. package/src/lib/scan-health.ts +67 -1
  35. package/src/lib/support-matrix.ts +113 -0
  36. package/.next/BUILD_ID +0 -1
  37. package/.next/app-build-manifest.json +0 -192
  38. package/.next/app-path-routes-manifest.json +0 -23
  39. package/.next/build-manifest.json +0 -33
  40. package/.next/export-marker.json +0 -6
  41. package/.next/images-manifest.json +0 -58
  42. package/.next/next-minimal-server.js.nft.json +0 -1
  43. package/.next/next-server.js.nft.json +0 -1
  44. package/.next/package.json +0 -1
  45. package/.next/prerender-manifest.json +0 -37
  46. package/.next/react-loadable-manifest.json +0 -1
  47. package/.next/required-server-files.json +0 -323
  48. package/.next/routes-manifest.json +0 -119
  49. package/.next/server/app/_not-found/page.js +0 -1105
  50. package/.next/server/app/_not-found/page.js.nft.json +0 -1
  51. package/.next/server/app/_not-found/page_client-reference-manifest.js +0 -1
  52. package/.next/server/app/_not-found.html +0 -1
  53. package/.next/server/app/_not-found.meta +0 -8
  54. package/.next/server/app/_not-found.rsc +0 -40
  55. package/.next/server/app/api/analytics/route.js +0 -669
  56. package/.next/server/app/api/analytics/route.js.nft.json +0 -1
  57. package/.next/server/app/api/analytics/route_client-reference-manifest.js +0 -1
  58. package/.next/server/app/api/data/route.js +0 -1355
  59. package/.next/server/app/api/data/route.js.nft.json +0 -1
  60. package/.next/server/app/api/data/route_client-reference-manifest.js +0 -1
  61. package/.next/server/app/api/export/route.js +0 -545
  62. package/.next/server/app/api/export/route.js.nft.json +0 -1
  63. package/.next/server/app/api/export/route_client-reference-manifest.js +0 -1
  64. package/.next/server/app/api/files/route.js +0 -512
  65. package/.next/server/app/api/files/route.js.nft.json +0 -1
  66. package/.next/server/app/api/files/route_client-reference-manifest.js +0 -1
  67. package/.next/server/app/api/prices/refresh/route.js +0 -1449
  68. package/.next/server/app/api/prices/refresh/route.js.nft.json +0 -1
  69. package/.next/server/app/api/prices/refresh/route_client-reference-manifest.js +0 -1
  70. package/.next/server/app/api/prices/route.js +0 -1382
  71. package/.next/server/app/api/prices/route.js.nft.json +0 -1
  72. package/.next/server/app/api/prices/route_client-reference-manifest.js +0 -1
  73. package/.next/server/app/api/scan/route.js +0 -3066
  74. package/.next/server/app/api/scan/route.js.nft.json +0 -1
  75. package/.next/server/app/api/scan/route_client-reference-manifest.js +0 -1
  76. package/.next/server/app/api/settings/route.js +0 -1076
  77. package/.next/server/app/api/settings/route.js.nft.json +0 -1
  78. package/.next/server/app/api/settings/route_client-reference-manifest.js +0 -1
  79. package/.next/server/app/debug/page.js +0 -1626
  80. package/.next/server/app/debug/page.js.nft.json +0 -1
  81. package/.next/server/app/debug/page_client-reference-manifest.js +0 -1
  82. package/.next/server/app/diagnostics/page.js +0 -3033
  83. package/.next/server/app/diagnostics/page.js.nft.json +0 -1
  84. package/.next/server/app/diagnostics/page_client-reference-manifest.js +0 -1
  85. package/.next/server/app/discovery/page.js +0 -1717
  86. package/.next/server/app/discovery/page.js.nft.json +0 -1
  87. package/.next/server/app/discovery/page_client-reference-manifest.js +0 -1
  88. package/.next/server/app/models/page.js +0 -1777
  89. package/.next/server/app/models/page.js.nft.json +0 -1
  90. package/.next/server/app/models/page_client-reference-manifest.js +0 -1
  91. package/.next/server/app/optimisation/page.js +0 -1590
  92. package/.next/server/app/optimisation/page.js.nft.json +0 -1
  93. package/.next/server/app/optimisation/page_client-reference-manifest.js +0 -1
  94. package/.next/server/app/page.js +0 -3101
  95. package/.next/server/app/page.js.nft.json +0 -1
  96. package/.next/server/app/page_client-reference-manifest.js +0 -1
  97. package/.next/server/app/parser-debug/page.js +0 -1634
  98. package/.next/server/app/parser-debug/page.js.nft.json +0 -1
  99. package/.next/server/app/parser-debug/page_client-reference-manifest.js +0 -1
  100. package/.next/server/app/pricing/page.js +0 -2578
  101. package/.next/server/app/pricing/page.js.nft.json +0 -1
  102. package/.next/server/app/pricing/page_client-reference-manifest.js +0 -1
  103. package/.next/server/app/projects/page.js +0 -1955
  104. package/.next/server/app/projects/page.js.nft.json +0 -1
  105. package/.next/server/app/projects/page_client-reference-manifest.js +0 -1
  106. package/.next/server/app/sessions/page.js +0 -2514
  107. package/.next/server/app/sessions/page.js.nft.json +0 -1
  108. package/.next/server/app/sessions/page_client-reference-manifest.js +0 -1
  109. package/.next/server/app/settings/page.js +0 -2455
  110. package/.next/server/app/settings/page.js.nft.json +0 -1
  111. package/.next/server/app/settings/page_client-reference-manifest.js +0 -1
  112. package/.next/server/app/tools/page.js +0 -1733
  113. package/.next/server/app/tools/page.js.nft.json +0 -1
  114. package/.next/server/app/tools/page_client-reference-manifest.js +0 -1
  115. package/.next/server/app-paths-manifest.json +0 -23
  116. package/.next/server/chunks/168.js +0 -29504
  117. package/.next/server/chunks/287.js +0 -1650
  118. package/.next/server/chunks/331.js +0 -7896
  119. package/.next/server/chunks/366.js +0 -32801
  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/chunks/889.js +0 -707
  125. package/.next/server/functions-config-manifest.json +0 -4
  126. package/.next/server/interception-route-rewrite-manifest.js +0 -1
  127. package/.next/server/middleware-build-manifest.js +0 -1
  128. package/.next/server/middleware-manifest.json +0 -6
  129. package/.next/server/middleware-react-loadable-manifest.js +0 -1
  130. package/.next/server/next-font-manifest.js +0 -1
  131. package/.next/server/next-font-manifest.json +0 -1
  132. package/.next/server/pages/404.html +0 -1
  133. package/.next/server/pages/500.html +0 -1
  134. package/.next/server/pages/_app.js +0 -277
  135. package/.next/server/pages/_app.js.nft.json +0 -1
  136. package/.next/server/pages/_document.js +0 -46
  137. package/.next/server/pages/_document.js.nft.json +0 -1
  138. package/.next/server/pages/_error.js +0 -6315
  139. package/.next/server/pages/_error.js.nft.json +0 -1
  140. package/.next/server/pages-manifest.json +0 -6
  141. package/.next/server/server-reference-manifest.js +0 -1
  142. package/.next/server/server-reference-manifest.json +0 -1
  143. package/.next/server/webpack-runtime.js +0 -207
  144. package/.next/static/NsZcU7ZBwskTrl3ru-2LL/_buildManifest.js +0 -1
  145. package/.next/static/NsZcU7ZBwskTrl3ru-2LL/_ssgManifest.js +0 -1
  146. package/.next/static/chunks/125-ab0f8db8f84c1166.js +0 -1
  147. package/.next/static/chunks/255-e881f48ae1d2333a.js +0 -1
  148. package/.next/static/chunks/4bd1b696-409494caf8c83275.js +0 -1
  149. package/.next/static/chunks/619-f072ac750404f9da.js +0 -1
  150. package/.next/static/chunks/850-8bc31e41590b5831.js +0 -1
  151. package/.next/static/chunks/938-23236de1c47554ea.js +0 -1
  152. package/.next/static/chunks/app/_not-found/page-73368c3ff767c206.js +0 -1
  153. package/.next/static/chunks/app/api/analytics/route-73368c3ff767c206.js +0 -1
  154. package/.next/static/chunks/app/api/data/route-73368c3ff767c206.js +0 -1
  155. package/.next/static/chunks/app/api/export/route-73368c3ff767c206.js +0 -1
  156. package/.next/static/chunks/app/api/files/route-73368c3ff767c206.js +0 -1
  157. package/.next/static/chunks/app/api/prices/refresh/route-73368c3ff767c206.js +0 -1
  158. package/.next/static/chunks/app/api/prices/route-73368c3ff767c206.js +0 -1
  159. package/.next/static/chunks/app/api/scan/route-73368c3ff767c206.js +0 -1
  160. package/.next/static/chunks/app/api/settings/route-73368c3ff767c206.js +0 -1
  161. package/.next/static/chunks/app/debug/page-73368c3ff767c206.js +0 -1
  162. package/.next/static/chunks/app/diagnostics/page-3b65a4b6734f0c62.js +0 -1
  163. package/.next/static/chunks/app/discovery/page-73368c3ff767c206.js +0 -1
  164. package/.next/static/chunks/app/global-error-88b965bb96071bef.js +0 -1
  165. package/.next/static/chunks/app/layout-234c4399a92e808b.js +0 -1
  166. package/.next/static/chunks/app/models/page-b2b7c974754ac991.js +0 -1
  167. package/.next/static/chunks/app/not-found-0644ad2dcc40cfd9.js +0 -1
  168. package/.next/static/chunks/app/optimisation/page-73368c3ff767c206.js +0 -1
  169. package/.next/static/chunks/app/page-509085a4fdf00d49.js +0 -1
  170. package/.next/static/chunks/app/parser-debug/page-73368c3ff767c206.js +0 -1
  171. package/.next/static/chunks/app/pricing/page-d8419f42f65f7429.js +0 -1
  172. package/.next/static/chunks/app/projects/page-a49976ccd65bd81a.js +0 -1
  173. package/.next/static/chunks/app/sessions/page-86f6b8c220f4aa95.js +0 -1
  174. package/.next/static/chunks/app/settings/page-46abb09e882c62db.js +0 -1
  175. package/.next/static/chunks/app/tools/page-b2b7c974754ac991.js +0 -1
  176. package/.next/static/chunks/framework-3457b9c2619cdd96.js +0 -1
  177. package/.next/static/chunks/main-8744520a8a31e6ae.js +0 -1
  178. package/.next/static/chunks/main-app-039335219a472e20.js +0 -1
  179. package/.next/static/chunks/pages/_app-5addca2b3b969fde.js +0 -1
  180. package/.next/static/chunks/pages/_error-022e4ac7bbb9914f.js +0 -1
  181. package/.next/static/chunks/polyfills-42372ed130431b0a.js +0 -1
  182. package/.next/static/chunks/webpack-3fcacae817f3ffab.js +0 -1
  183. package/.next/static/css/46d6a87bebe3b542.css +0 -3
package/CHANGELOG.md CHANGED
@@ -4,6 +4,47 @@ 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
+
38
+ ## [0.5.1] - 2026-05-10
39
+
40
+ ### Added
41
+
42
+ - TokenTrace logo SVG asset and Next.js app icon.
43
+
44
+ ### Changed
45
+
46
+ - App shell and README now use the TokenTrace logo instead of the generic chart mark.
47
+
7
48
  ## [0.5.0] - 2026-05-10
8
49
 
9
50
  ### Added
package/README.md CHANGED
@@ -1,3 +1,7 @@
1
+ <p align="center">
2
+ <img src="docs/assets/tokentrace-logo.svg" alt="TokenTrace logo" width="96" height="96">
3
+ </p>
4
+
1
5
  # TokenTrace CLI
2
6
 
3
7
  Local-first analytics for AI CLI usage. TokenTrace scans local CLI logs, normalizes token usage, estimates missing counts, and shows cost, model, project, and session analytics in a browser dashboard.
@@ -231,11 +235,12 @@ Stop the server with `Ctrl+C` in the terminal where `tokentrace` is running.
231
235
 
232
236
  ## Package Trust
233
237
 
234
- - The npm package has no `preinstall`, `install`, or `postinstall` scripts.
235
- - Published Next.js server bundles are intentionally left readable instead of server-minified so Socket, npm, maintainers, and users can inspect generated runtime code.
236
- - `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.
237
242
  - Public npm publishing is configured through GitHub Actions Trusted Publishing and provenance from version tags.
238
- - 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`.
239
244
  - Release notes are published directly in GitHub Releases from the relevant changelog section, not as a link-only summary.
240
245
 
241
246
  See [SECURITY.md](SECURITY.md) for the full security and privacy model.
@@ -287,6 +292,21 @@ Adapters live under `src/ingestion/adapters/`:
287
292
 
288
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.
289
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
+
290
310
  ## Extending Parsers
291
311
 
292
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/icon.svg ADDED
@@ -0,0 +1,10 @@
1
+ <svg aria-label="TokenTrace logo" role="img" viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg">
2
+ <title>TokenTrace logo</title>
3
+ <rect width="64" height="64" rx="14" fill="#147b74"/>
4
+ <path d="M17 42h11V29h12V18h8" fill="none" stroke="#fcfaf8" stroke-linecap="round" stroke-linejoin="round" stroke-width="5"/>
5
+ <circle cx="17" cy="42" r="5" fill="#fbd041"/>
6
+ <circle cx="40" cy="29" r="5" fill="#fcfaf8"/>
7
+ <circle cx="48" cy="18" r="5" fill="#f2742c"/>
8
+ <path d="M17 19h12" stroke="#fcfaf8" stroke-linecap="round" stroke-width="4" opacity="0.72"/>
9
+ <path d="M17 29h7" stroke="#fcfaf8" stroke-linecap="round" stroke-width="4" opacity="0.48"/>
10
+ </svg>
package/app/layout.tsx CHANGED
@@ -1,6 +1,7 @@
1
1
  import type { Metadata } from "next";
2
2
  import "./globals.css";
3
3
  import { MobileNav, Sidebar } from "@/components/sidebar";
4
+ import { TokenTraceLogo } from "@/components/token-trace-logo";
4
5
  import { formatAppVersion, getAppVersion } from "@/src/lib/app-version";
5
6
 
6
7
  export const metadata: Metadata = {
@@ -19,9 +20,12 @@ export default function RootLayout({ children }: { children: React.ReactNode })
19
20
  <main className="min-w-0 flex-1">
20
21
  <div className="border-b bg-card px-4 py-3 md:hidden">
21
22
  <div className="flex items-center justify-between gap-3">
22
- <div className="min-w-0">
23
- <div className="text-sm font-semibold">TokenTrace CLI</div>
24
- <div className="text-xs text-muted-foreground">Local only · No telemetry</div>
23
+ <div className="flex min-w-0 items-center gap-2">
24
+ <TokenTraceLogo className="h-9 w-9 shrink-0" />
25
+ <div className="min-w-0">
26
+ <div className="text-sm font-semibold">TokenTrace CLI</div>
27
+ <div className="text-xs text-muted-foreground">Local only · No telemetry</div>
28
+ </div>
25
29
  </div>
26
30
  <div className="shrink-0 rounded-md border bg-muted/50 px-2 py-1 text-xs font-medium text-muted-foreground">
27
31
  {formatAppVersion(appVersion)}
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">
@@ -7,13 +7,13 @@ import {
7
7
  ClipboardList,
8
8
  FolderGit2,
9
9
  Gauge,
10
- LineChart,
11
10
  Search,
12
11
  Settings,
13
12
  SlidersHorizontal,
14
13
  Sparkles,
15
14
  Terminal
16
15
  } from "lucide-react";
16
+ import { TokenTraceLogo } from "@/components/token-trace-logo";
17
17
  import { formatAppVersion, getAppVersion } from "@/src/lib/app-version";
18
18
 
19
19
  const navItems = [
@@ -37,9 +37,7 @@ export function Sidebar({ appVersion = getAppVersion() }: { appVersion?: string
37
37
  <div className="flex h-full flex-col">
38
38
  <div className="border-b p-5">
39
39
  <div className="flex items-center gap-2">
40
- <div className="flex h-9 w-9 items-center justify-center rounded-md bg-primary text-primary-foreground">
41
- <LineChart className="h-5 w-5" />
42
- </div>
40
+ <TokenTraceLogo className="h-9 w-9 shrink-0" />
43
41
  <div>
44
42
  <div className="text-sm font-semibold">TokenTrace CLI</div>
45
43
  <div className="text-xs text-muted-foreground">Local only · No telemetry</div>
@@ -0,0 +1,46 @@
1
+ import * as React from "react";
2
+
3
+ export function TokenTraceLogo({
4
+ className,
5
+ label = "TokenTrace logo"
6
+ }: {
7
+ className?: string;
8
+ label?: string;
9
+ }) {
10
+ return (
11
+ <svg
12
+ aria-label={label}
13
+ role="img"
14
+ viewBox="0 0 64 64"
15
+ className={className}
16
+ xmlns="http://www.w3.org/2000/svg"
17
+ >
18
+ <rect width="64" height="64" rx="14" fill="#147b74" />
19
+ <path
20
+ d="M17 42h11V29h12V18h8"
21
+ fill="none"
22
+ stroke="#fcfaf8"
23
+ strokeLinecap="round"
24
+ strokeLinejoin="round"
25
+ strokeWidth="5"
26
+ />
27
+ <circle cx="17" cy="42" r="5" fill="#fbd041" />
28
+ <circle cx="40" cy="29" r="5" fill="#fcfaf8" />
29
+ <circle cx="48" cy="18" r="5" fill="#f2742c" />
30
+ <path
31
+ d="M17 19h12"
32
+ stroke="#fcfaf8"
33
+ strokeLinecap="round"
34
+ strokeWidth="4"
35
+ opacity="0.72"
36
+ />
37
+ <path
38
+ d="M17 29h7"
39
+ stroke="#fcfaf8"
40
+ strokeLinecap="round"
41
+ strokeWidth="4"
42
+ opacity="0.48"
43
+ />
44
+ </svg>
45
+ );
46
+ }
@@ -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