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.
- package/CHANGELOG.md +31 -0
- package/README.md +20 -4
- package/SECURITY.md +6 -3
- package/app/diagnostics/page.tsx +32 -0
- package/app/page.tsx +54 -6
- package/bin/tokentrace.js +94 -7
- package/components/period-filter.tsx +19 -17
- package/components/settings-panel.tsx +2 -2
- package/dist/runtime/db-migrate.mjs +1 -0
- package/dist/runtime/db-seed.mjs +13 -0
- package/dist/runtime/doctor.mjs +154 -1
- package/dist/runtime/insights.mjs +16 -0
- package/dist/runtime/pricing-refresh.mjs +13 -0
- package/dist/runtime/reset.mjs +13 -0
- package/dist/runtime/scan.mjs +17 -2
- package/dist/runtime/status.mjs +1 -0
- package/next.config.mjs +5 -0
- package/package.json +17 -18
- package/scripts/doctor.ts +2 -0
- package/scripts/package-inspect.mjs +51 -38
- package/scripts/smoke-cli.mjs +179 -0
- package/scripts/smoke-packed-install.mjs +180 -0
- package/src/db/client.ts +1 -0
- package/src/ingestion/adapters/claude-code.ts +2 -1
- package/src/ingestion/adapters/codex-cli.ts +2 -1
- package/src/lib/doctor.ts +17 -0
- package/src/lib/first-run-status.ts +128 -0
- package/src/lib/model-aliases.ts +14 -0
- package/src/lib/scan-health.ts +67 -1
- package/src/lib/support-matrix.ts +113 -0
- package/.next/BUILD_ID +0 -1
- package/.next/app-build-manifest.json +0 -192
- package/.next/app-path-routes-manifest.json +0 -24
- package/.next/build-manifest.json +0 -33
- package/.next/export-marker.json +0 -6
- package/.next/images-manifest.json +0 -58
- package/.next/next-minimal-server.js.nft.json +0 -1
- package/.next/next-server.js.nft.json +0 -1
- package/.next/package.json +0 -1
- package/.next/prerender-manifest.json +0 -66
- package/.next/react-loadable-manifest.json +0 -1
- package/.next/required-server-files.json +0 -323
- package/.next/routes-manifest.json +0 -125
- package/.next/server/app/_not-found/page.js +0 -1111
- package/.next/server/app/_not-found/page.js.nft.json +0 -1
- package/.next/server/app/_not-found/page_client-reference-manifest.js +0 -1
- package/.next/server/app/_not-found.html +0 -1
- package/.next/server/app/_not-found.meta +0 -8
- package/.next/server/app/_not-found.rsc +0 -40
- package/.next/server/app/api/analytics/route.js +0 -669
- package/.next/server/app/api/analytics/route.js.nft.json +0 -1
- package/.next/server/app/api/analytics/route_client-reference-manifest.js +0 -1
- package/.next/server/app/api/data/route.js +0 -1355
- package/.next/server/app/api/data/route.js.nft.json +0 -1
- package/.next/server/app/api/data/route_client-reference-manifest.js +0 -1
- package/.next/server/app/api/export/route.js +0 -545
- package/.next/server/app/api/export/route.js.nft.json +0 -1
- package/.next/server/app/api/export/route_client-reference-manifest.js +0 -1
- package/.next/server/app/api/files/route.js +0 -512
- package/.next/server/app/api/files/route.js.nft.json +0 -1
- package/.next/server/app/api/files/route_client-reference-manifest.js +0 -1
- package/.next/server/app/api/prices/refresh/route.js +0 -1449
- package/.next/server/app/api/prices/refresh/route.js.nft.json +0 -1
- package/.next/server/app/api/prices/refresh/route_client-reference-manifest.js +0 -1
- package/.next/server/app/api/prices/route.js +0 -1382
- package/.next/server/app/api/prices/route.js.nft.json +0 -1
- package/.next/server/app/api/prices/route_client-reference-manifest.js +0 -1
- package/.next/server/app/api/scan/route.js +0 -3066
- package/.next/server/app/api/scan/route.js.nft.json +0 -1
- package/.next/server/app/api/scan/route_client-reference-manifest.js +0 -1
- package/.next/server/app/api/settings/route.js +0 -1076
- package/.next/server/app/api/settings/route.js.nft.json +0 -1
- package/.next/server/app/api/settings/route_client-reference-manifest.js +0 -1
- package/.next/server/app/debug/page.js +0 -1638
- package/.next/server/app/debug/page.js.nft.json +0 -1
- package/.next/server/app/debug/page_client-reference-manifest.js +0 -1
- package/.next/server/app/diagnostics/page.js +0 -3045
- package/.next/server/app/diagnostics/page.js.nft.json +0 -1
- package/.next/server/app/diagnostics/page_client-reference-manifest.js +0 -1
- package/.next/server/app/discovery/page.js +0 -1729
- package/.next/server/app/discovery/page.js.nft.json +0 -1
- package/.next/server/app/discovery/page_client-reference-manifest.js +0 -1
- package/.next/server/app/icon.svg/route.js +0 -469
- package/.next/server/app/icon.svg/route.js.nft.json +0 -1
- package/.next/server/app/icon.svg.body +0 -10
- package/.next/server/app/icon.svg.meta +0 -1
- package/.next/server/app/models/page.js +0 -1789
- package/.next/server/app/models/page.js.nft.json +0 -1
- package/.next/server/app/models/page_client-reference-manifest.js +0 -1
- package/.next/server/app/optimisation/page.js +0 -1602
- package/.next/server/app/optimisation/page.js.nft.json +0 -1
- package/.next/server/app/optimisation/page_client-reference-manifest.js +0 -1
- package/.next/server/app/page.js +0 -3113
- package/.next/server/app/page.js.nft.json +0 -1
- package/.next/server/app/page_client-reference-manifest.js +0 -1
- package/.next/server/app/parser-debug/page.js +0 -1646
- package/.next/server/app/parser-debug/page.js.nft.json +0 -1
- package/.next/server/app/parser-debug/page_client-reference-manifest.js +0 -1
- package/.next/server/app/pricing/page.js +0 -2590
- package/.next/server/app/pricing/page.js.nft.json +0 -1
- package/.next/server/app/pricing/page_client-reference-manifest.js +0 -1
- package/.next/server/app/projects/page.js +0 -1967
- package/.next/server/app/projects/page.js.nft.json +0 -1
- package/.next/server/app/projects/page_client-reference-manifest.js +0 -1
- package/.next/server/app/sessions/page.js +0 -2526
- package/.next/server/app/sessions/page.js.nft.json +0 -1
- package/.next/server/app/sessions/page_client-reference-manifest.js +0 -1
- package/.next/server/app/settings/page.js +0 -2467
- package/.next/server/app/settings/page.js.nft.json +0 -1
- package/.next/server/app/settings/page_client-reference-manifest.js +0 -1
- package/.next/server/app/tools/page.js +0 -1745
- package/.next/server/app/tools/page.js.nft.json +0 -1
- package/.next/server/app/tools/page_client-reference-manifest.js +0 -1
- package/.next/server/app-paths-manifest.json +0 -24
- package/.next/server/chunks/287.js +0 -1650
- package/.next/server/chunks/331.js +0 -7896
- package/.next/server/chunks/366.js +0 -32801
- package/.next/server/chunks/483.js +0 -31264
- package/.next/server/chunks/576.js +0 -797
- package/.next/server/chunks/611.js +0 -2979
- package/.next/server/chunks/692.js +0 -905
- package/.next/server/chunks/722.js +0 -6318
- package/.next/server/chunks/868.js +0 -730
- package/.next/server/functions-config-manifest.json +0 -4
- package/.next/server/interception-route-rewrite-manifest.js +0 -1
- package/.next/server/middleware-build-manifest.js +0 -1
- package/.next/server/middleware-manifest.json +0 -6
- package/.next/server/middleware-react-loadable-manifest.js +0 -1
- package/.next/server/next-font-manifest.js +0 -1
- package/.next/server/next-font-manifest.json +0 -1
- package/.next/server/pages/404.html +0 -1
- package/.next/server/pages/500.html +0 -1
- package/.next/server/pages/_app.js +0 -277
- package/.next/server/pages/_app.js.nft.json +0 -1
- package/.next/server/pages/_document.js +0 -46
- package/.next/server/pages/_document.js.nft.json +0 -1
- package/.next/server/pages/_error.js +0 -6315
- package/.next/server/pages/_error.js.nft.json +0 -1
- package/.next/server/pages-manifest.json +0 -6
- package/.next/server/server-reference-manifest.js +0 -1
- package/.next/server/server-reference-manifest.json +0 -1
- package/.next/server/webpack-runtime.js +0 -207
- package/.next/static/cNv_sOa0AntpwuwvPjuRA/_buildManifest.js +0 -1
- package/.next/static/cNv_sOa0AntpwuwvPjuRA/_ssgManifest.js +0 -1
- package/.next/static/chunks/125-ab0f8db8f84c1166.js +0 -1
- package/.next/static/chunks/255-e881f48ae1d2333a.js +0 -1
- package/.next/static/chunks/4bd1b696-409494caf8c83275.js +0 -1
- package/.next/static/chunks/619-f072ac750404f9da.js +0 -1
- package/.next/static/chunks/850-8bc31e41590b5831.js +0 -1
- package/.next/static/chunks/938-23236de1c47554ea.js +0 -1
- package/.next/static/chunks/app/_not-found/page-73368c3ff767c206.js +0 -1
- package/.next/static/chunks/app/api/analytics/route-73368c3ff767c206.js +0 -1
- package/.next/static/chunks/app/api/data/route-73368c3ff767c206.js +0 -1
- package/.next/static/chunks/app/api/export/route-73368c3ff767c206.js +0 -1
- package/.next/static/chunks/app/api/files/route-73368c3ff767c206.js +0 -1
- package/.next/static/chunks/app/api/prices/refresh/route-73368c3ff767c206.js +0 -1
- package/.next/static/chunks/app/api/prices/route-73368c3ff767c206.js +0 -1
- package/.next/static/chunks/app/api/scan/route-73368c3ff767c206.js +0 -1
- package/.next/static/chunks/app/api/settings/route-73368c3ff767c206.js +0 -1
- package/.next/static/chunks/app/debug/page-73368c3ff767c206.js +0 -1
- package/.next/static/chunks/app/diagnostics/page-3b65a4b6734f0c62.js +0 -1
- package/.next/static/chunks/app/discovery/page-73368c3ff767c206.js +0 -1
- package/.next/static/chunks/app/global-error-88b965bb96071bef.js +0 -1
- package/.next/static/chunks/app/layout-234c4399a92e808b.js +0 -1
- package/.next/static/chunks/app/models/page-b2b7c974754ac991.js +0 -1
- package/.next/static/chunks/app/not-found-0644ad2dcc40cfd9.js +0 -1
- package/.next/static/chunks/app/optimisation/page-73368c3ff767c206.js +0 -1
- package/.next/static/chunks/app/page-509085a4fdf00d49.js +0 -1
- package/.next/static/chunks/app/parser-debug/page-73368c3ff767c206.js +0 -1
- package/.next/static/chunks/app/pricing/page-d8419f42f65f7429.js +0 -1
- package/.next/static/chunks/app/projects/page-a49976ccd65bd81a.js +0 -1
- package/.next/static/chunks/app/sessions/page-86f6b8c220f4aa95.js +0 -1
- package/.next/static/chunks/app/settings/page-b2d78790be6a114d.js +0 -1
- package/.next/static/chunks/app/tools/page-b2b7c974754ac991.js +0 -1
- package/.next/static/chunks/framework-3457b9c2619cdd96.js +0 -1
- package/.next/static/chunks/main-8744520a8a31e6ae.js +0 -1
- package/.next/static/chunks/main-app-039335219a472e20.js +0 -1
- package/.next/static/chunks/pages/_app-5addca2b3b969fde.js +0 -1
- package/.next/static/chunks/pages/_error-022e4ac7bbb9914f.js +0 -1
- package/.next/static/chunks/polyfills-42372ed130431b0a.js +0 -1
- package/.next/static/chunks/webpack-3fcacae817f3ffab.js +0 -1
- 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
|
-
-
|
|
240
|
-
- `
|
|
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 --
|
|
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
|
-
-
|
|
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
|
-
-
|
|
18
|
-
|
|
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
|
|
package/app/diagnostics/page.tsx
CHANGED
|
@@ -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
|
-
<
|
|
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:
|
|
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-
|
|
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
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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-
|
|
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-
|
|
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: "
|
|
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: "
|
|
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 });
|
package/dist/runtime/db-seed.mjs
CHANGED
|
@@ -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
|
|