tokentrace 0.1.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 (224) hide show
  1. package/.next/BUILD_ID +1 -0
  2. package/.next/app-build-manifest.json +167 -0
  3. package/.next/app-path-routes-manifest.json +22 -0
  4. package/.next/build-manifest.json +33 -0
  5. package/.next/export-marker.json +6 -0
  6. package/.next/images-manifest.json +58 -0
  7. package/.next/next-minimal-server.js.nft.json +1 -0
  8. package/.next/next-server.js.nft.json +1 -0
  9. package/.next/package.json +1 -0
  10. package/.next/prerender-manifest.json +37 -0
  11. package/.next/react-loadable-manifest.json +1 -0
  12. package/.next/required-server-files.json +323 -0
  13. package/.next/routes-manifest.json +119 -0
  14. package/.next/server/app/_not-found/page.js +2 -0
  15. package/.next/server/app/_not-found/page.js.nft.json +1 -0
  16. package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -0
  17. package/.next/server/app/_not-found.html +1 -0
  18. package/.next/server/app/_not-found.meta +8 -0
  19. package/.next/server/app/_not-found.rsc +37 -0
  20. package/.next/server/app/api/analytics/route.js +1 -0
  21. package/.next/server/app/api/analytics/route.js.nft.json +1 -0
  22. package/.next/server/app/api/analytics/route_client-reference-manifest.js +1 -0
  23. package/.next/server/app/api/data/route.js +151 -0
  24. package/.next/server/app/api/data/route.js.nft.json +1 -0
  25. package/.next/server/app/api/data/route_client-reference-manifest.js +1 -0
  26. package/.next/server/app/api/export/route.js +1 -0
  27. package/.next/server/app/api/export/route.js.nft.json +1 -0
  28. package/.next/server/app/api/export/route_client-reference-manifest.js +1 -0
  29. package/.next/server/app/api/files/route.js +1 -0
  30. package/.next/server/app/api/files/route.js.nft.json +1 -0
  31. package/.next/server/app/api/files/route_client-reference-manifest.js +1 -0
  32. package/.next/server/app/api/prices/route.js +151 -0
  33. package/.next/server/app/api/prices/route.js.nft.json +1 -0
  34. package/.next/server/app/api/prices/route_client-reference-manifest.js +1 -0
  35. package/.next/server/app/api/scan/route.js +144 -0
  36. package/.next/server/app/api/scan/route.js.nft.json +1 -0
  37. package/.next/server/app/api/scan/route_client-reference-manifest.js +1 -0
  38. package/.next/server/app/api/settings/route.js +128 -0
  39. package/.next/server/app/api/settings/route.js.nft.json +1 -0
  40. package/.next/server/app/api/settings/route_client-reference-manifest.js +1 -0
  41. package/.next/server/app/debug/page.js +2 -0
  42. package/.next/server/app/debug/page.js.nft.json +1 -0
  43. package/.next/server/app/debug/page_client-reference-manifest.js +1 -0
  44. package/.next/server/app/diagnostics/page.js +2 -0
  45. package/.next/server/app/diagnostics/page.js.nft.json +1 -0
  46. package/.next/server/app/diagnostics/page_client-reference-manifest.js +1 -0
  47. package/.next/server/app/discovery/page.js +2 -0
  48. package/.next/server/app/discovery/page.js.nft.json +1 -0
  49. package/.next/server/app/discovery/page_client-reference-manifest.js +1 -0
  50. package/.next/server/app/models/page.js +2 -0
  51. package/.next/server/app/models/page.js.nft.json +1 -0
  52. package/.next/server/app/models/page_client-reference-manifest.js +1 -0
  53. package/.next/server/app/optimisation/page.js +2 -0
  54. package/.next/server/app/optimisation/page.js.nft.json +1 -0
  55. package/.next/server/app/optimisation/page_client-reference-manifest.js +1 -0
  56. package/.next/server/app/page.js +2 -0
  57. package/.next/server/app/page.js.nft.json +1 -0
  58. package/.next/server/app/page_client-reference-manifest.js +1 -0
  59. package/.next/server/app/parser-debug/page.js +2 -0
  60. package/.next/server/app/parser-debug/page.js.nft.json +1 -0
  61. package/.next/server/app/parser-debug/page_client-reference-manifest.js +1 -0
  62. package/.next/server/app/pricing/page.js +152 -0
  63. package/.next/server/app/pricing/page.js.nft.json +1 -0
  64. package/.next/server/app/pricing/page_client-reference-manifest.js +1 -0
  65. package/.next/server/app/projects/page.js +2 -0
  66. package/.next/server/app/projects/page.js.nft.json +1 -0
  67. package/.next/server/app/projects/page_client-reference-manifest.js +1 -0
  68. package/.next/server/app/sessions/page.js +2 -0
  69. package/.next/server/app/sessions/page.js.nft.json +1 -0
  70. package/.next/server/app/sessions/page_client-reference-manifest.js +1 -0
  71. package/.next/server/app/settings/page.js +129 -0
  72. package/.next/server/app/settings/page.js.nft.json +1 -0
  73. package/.next/server/app/settings/page_client-reference-manifest.js +1 -0
  74. package/.next/server/app/tools/page.js +2 -0
  75. package/.next/server/app/tools/page.js.nft.json +1 -0
  76. package/.next/server/app/tools/page_client-reference-manifest.js +1 -0
  77. package/.next/server/app-paths-manifest.json +22 -0
  78. package/.next/server/chunks/123.js +9 -0
  79. package/.next/server/chunks/153.js +1 -0
  80. package/.next/server/chunks/237.js +13 -0
  81. package/.next/server/chunks/331.js +22 -0
  82. package/.next/server/chunks/366.js +1 -0
  83. package/.next/server/chunks/444.js +267 -0
  84. package/.next/server/chunks/611.js +6 -0
  85. package/.next/server/chunks/692.js +1 -0
  86. package/.next/server/chunks/779.js +1 -0
  87. package/.next/server/chunks/815.js +1 -0
  88. package/.next/server/chunks/868.js +1 -0
  89. package/.next/server/functions-config-manifest.json +4 -0
  90. package/.next/server/interception-route-rewrite-manifest.js +1 -0
  91. package/.next/server/middleware-build-manifest.js +1 -0
  92. package/.next/server/middleware-manifest.json +6 -0
  93. package/.next/server/middleware-react-loadable-manifest.js +1 -0
  94. package/.next/server/next-font-manifest.js +1 -0
  95. package/.next/server/next-font-manifest.json +1 -0
  96. package/.next/server/pages/404.html +1 -0
  97. package/.next/server/pages/500.html +1 -0
  98. package/.next/server/pages/_app.js +1 -0
  99. package/.next/server/pages/_app.js.nft.json +1 -0
  100. package/.next/server/pages/_document.js +1 -0
  101. package/.next/server/pages/_document.js.nft.json +1 -0
  102. package/.next/server/pages/_error.js +19 -0
  103. package/.next/server/pages/_error.js.nft.json +1 -0
  104. package/.next/server/pages-manifest.json +6 -0
  105. package/.next/server/server-reference-manifest.js +1 -0
  106. package/.next/server/server-reference-manifest.json +1 -0
  107. package/.next/server/webpack-runtime.js +1 -0
  108. package/.next/static/Fh8usqK3dgfncUx9s3VR1/_buildManifest.js +1 -0
  109. package/.next/static/Fh8usqK3dgfncUx9s3VR1/_ssgManifest.js +1 -0
  110. package/.next/static/chunks/125-ab0f8db8f84c1166.js +1 -0
  111. package/.next/static/chunks/255-e881f48ae1d2333a.js +1 -0
  112. package/.next/static/chunks/4bd1b696-409494caf8c83275.js +1 -0
  113. package/.next/static/chunks/619-f072ac750404f9da.js +1 -0
  114. package/.next/static/chunks/850-8bc31e41590b5831.js +1 -0
  115. package/.next/static/chunks/938-23236de1c47554ea.js +1 -0
  116. package/.next/static/chunks/app/_not-found/page-6d75243350d9e0b5.js +1 -0
  117. package/.next/static/chunks/app/api/analytics/route-33d3f29973de91a4.js +1 -0
  118. package/.next/static/chunks/app/api/data/route-33d3f29973de91a4.js +1 -0
  119. package/.next/static/chunks/app/api/export/route-33d3f29973de91a4.js +1 -0
  120. package/.next/static/chunks/app/api/files/route-33d3f29973de91a4.js +1 -0
  121. package/.next/static/chunks/app/api/prices/route-33d3f29973de91a4.js +1 -0
  122. package/.next/static/chunks/app/api/scan/route-33d3f29973de91a4.js +1 -0
  123. package/.next/static/chunks/app/api/settings/route-33d3f29973de91a4.js +1 -0
  124. package/.next/static/chunks/app/debug/page-33d3f29973de91a4.js +1 -0
  125. package/.next/static/chunks/app/diagnostics/page-053a5e810a59e548.js +1 -0
  126. package/.next/static/chunks/app/discovery/page-33d3f29973de91a4.js +1 -0
  127. package/.next/static/chunks/app/layout-8942804176ff26f3.js +1 -0
  128. package/.next/static/chunks/app/models/page-c0acf74dd8197e01.js +1 -0
  129. package/.next/static/chunks/app/optimisation/page-33d3f29973de91a4.js +1 -0
  130. package/.next/static/chunks/app/page-b6886ec802c03cbf.js +1 -0
  131. package/.next/static/chunks/app/parser-debug/page-33d3f29973de91a4.js +1 -0
  132. package/.next/static/chunks/app/pricing/page-5e27b1ae27314539.js +1 -0
  133. package/.next/static/chunks/app/projects/page-b6886ec802c03cbf.js +1 -0
  134. package/.next/static/chunks/app/sessions/page-0abcdc88aac9dcaf.js +1 -0
  135. package/.next/static/chunks/app/settings/page-59fc80673f0750cd.js +1 -0
  136. package/.next/static/chunks/app/tools/page-c0acf74dd8197e01.js +1 -0
  137. package/.next/static/chunks/framework-3457b9c2619cdd96.js +1 -0
  138. package/.next/static/chunks/main-8744520a8a31e6ae.js +1 -0
  139. package/.next/static/chunks/main-app-e9ccddef393e28c3.js +1 -0
  140. package/.next/static/chunks/pages/_app-5addca2b3b969fde.js +1 -0
  141. package/.next/static/chunks/pages/_error-022e4ac7bbb9914f.js +1 -0
  142. package/.next/static/chunks/polyfills-42372ed130431b0a.js +1 -0
  143. package/.next/static/chunks/webpack-3fcacae817f3ffab.js +1 -0
  144. package/.next/static/css/366bb38b386229a5.css +3 -0
  145. package/LICENSE +21 -0
  146. package/README.md +216 -0
  147. package/app/api/analytics/route.ts +8 -0
  148. package/app/api/data/route.ts +9 -0
  149. package/app/api/export/route.ts +26 -0
  150. package/app/api/files/route.ts +8 -0
  151. package/app/api/prices/route.ts +33 -0
  152. package/app/api/scan/route.ts +15 -0
  153. package/app/api/settings/route.ts +25 -0
  154. package/app/debug/page.tsx +101 -0
  155. package/app/diagnostics/page.tsx +113 -0
  156. package/app/discovery/page.tsx +61 -0
  157. package/app/globals.css +51 -0
  158. package/app/layout.tsx +30 -0
  159. package/app/models/page.tsx +97 -0
  160. package/app/optimisation/page.tsx +67 -0
  161. package/app/page.tsx +164 -0
  162. package/app/parser-debug/page.tsx +57 -0
  163. package/app/pricing/page.tsx +18 -0
  164. package/app/projects/page.tsx +111 -0
  165. package/app/sessions/page.tsx +24 -0
  166. package/app/settings/page.tsx +26 -0
  167. package/app/tools/page.tsx +92 -0
  168. package/bin/tokentrace.js +316 -0
  169. package/components/charts/rank-bar-chart.tsx +69 -0
  170. package/components/charts/trend-chart.tsx +123 -0
  171. package/components/empty-state.tsx +14 -0
  172. package/components/pricing-settings.tsx +171 -0
  173. package/components/session-explorer.tsx +210 -0
  174. package/components/settings-panel.tsx +203 -0
  175. package/components/sidebar.tsx +88 -0
  176. package/components/ui/badge.tsx +30 -0
  177. package/components/ui/button.tsx +47 -0
  178. package/components/ui/card.tsx +22 -0
  179. package/components/ui/input.tsx +19 -0
  180. package/components/ui/label.tsx +6 -0
  181. package/components/ui/table.tsx +31 -0
  182. package/components/ui/textarea.tsx +18 -0
  183. package/components.json +16 -0
  184. package/dist/runtime/db-migrate.mjs +410 -0
  185. package/dist/runtime/db-seed.mjs +506 -0
  186. package/dist/runtime/reset.mjs +519 -0
  187. package/dist/runtime/scan.mjs +1817 -0
  188. package/fixtures/generic-jsonl/sample.jsonl +2 -0
  189. package/next.config.mjs +7 -0
  190. package/package.json +96 -0
  191. package/postcss.config.mjs +8 -0
  192. package/scripts/build-cli-runtime.mjs +40 -0
  193. package/scripts/db-migrate.ts +5 -0
  194. package/scripts/db-seed.ts +5 -0
  195. package/scripts/reset.ts +5 -0
  196. package/scripts/scan.ts +30 -0
  197. package/src/db/client.ts +32 -0
  198. package/src/db/migrate-core.ts +147 -0
  199. package/src/db/reset.ts +14 -0
  200. package/src/db/schema.ts +259 -0
  201. package/src/db/seed.ts +110 -0
  202. package/src/db/settings.ts +47 -0
  203. package/src/ingestion/adapters/claude-code.ts +78 -0
  204. package/src/ingestion/adapters/codex-cli.ts +82 -0
  205. package/src/ingestion/adapters/generic-json.ts +93 -0
  206. package/src/ingestion/adapters/generic-jsonl.ts +62 -0
  207. package/src/ingestion/adapters/generic-log.ts +144 -0
  208. package/src/ingestion/adapters/generic-records.ts +178 -0
  209. package/src/ingestion/adapters/helpers.ts +309 -0
  210. package/src/ingestion/adapters/index.ts +15 -0
  211. package/src/ingestion/discovery.ts +130 -0
  212. package/src/ingestion/persist.ts +283 -0
  213. package/src/ingestion/scan.ts +247 -0
  214. package/src/ingestion/types.ts +78 -0
  215. package/src/lib/analytics.ts +592 -0
  216. package/src/lib/cost.ts +62 -0
  217. package/src/lib/csv.ts +15 -0
  218. package/src/lib/format.ts +51 -0
  219. package/src/lib/ids.ts +23 -0
  220. package/src/lib/pricing.ts +86 -0
  221. package/src/lib/token-estimator.ts +24 -0
  222. package/src/lib/utils.ts +6 -0
  223. package/tailwind.config.ts +53 -0
  224. package/tsconfig.json +28 -0
@@ -0,0 +1,26 @@
1
+ import { NextResponse } from "next/server";
2
+ import { getAnalyticsData } from "@/src/lib/analytics";
3
+ import { toCsv } from "@/src/lib/csv";
4
+
5
+ export const dynamic = "force-dynamic";
6
+
7
+ export async function GET(request: Request) {
8
+ const url = new URL(request.url);
9
+ const type = url.searchParams.get("type") ?? "sessions";
10
+ const analytics = getAnalyticsData();
11
+ const rows =
12
+ type === "projects"
13
+ ? analytics.projects
14
+ : type === "models"
15
+ ? analytics.models
16
+ : type === "tools"
17
+ ? analytics.tools
18
+ : analytics.sessions;
19
+
20
+ return new NextResponse(toCsv(rows as Array<Record<string, unknown>>), {
21
+ headers: {
22
+ "content-type": "text/csv; charset=utf-8",
23
+ "content-disposition": `attachment; filename="tokentrace-${type}.csv"`
24
+ }
25
+ });
26
+ }
@@ -0,0 +1,8 @@
1
+ import { NextResponse } from "next/server";
2
+ import { getDebugData } from "@/src/lib/analytics";
3
+
4
+ export const dynamic = "force-dynamic";
5
+
6
+ export async function GET() {
7
+ return NextResponse.json(getDebugData());
8
+ }
@@ -0,0 +1,33 @@
1
+ import { NextResponse } from "next/server";
2
+ import { getPricingRows, upsertPricing } from "@/src/lib/pricing";
3
+
4
+ export const dynamic = "force-dynamic";
5
+
6
+ function nullableNumber(value: unknown) {
7
+ if (value === "" || value == null) return null;
8
+ const number = Number(value);
9
+ return Number.isFinite(number) ? number : null;
10
+ }
11
+
12
+ export async function GET() {
13
+ return NextResponse.json(getPricingRows());
14
+ }
15
+
16
+ export async function POST(request: Request) {
17
+ const body = await request.json();
18
+ if (!body.providerId || !body.model) {
19
+ return NextResponse.json({ error: "providerId and model are required" }, { status: 400 });
20
+ }
21
+
22
+ const id = upsertPricing({
23
+ providerId: String(body.providerId),
24
+ providerName: body.providerName ? String(body.providerName) : undefined,
25
+ model: String(body.model),
26
+ inputTokenPrice: nullableNumber(body.inputTokenPrice),
27
+ outputTokenPrice: nullableNumber(body.outputTokenPrice),
28
+ cachedInputTokenPrice: nullableNumber(body.cachedInputTokenPrice),
29
+ currency: body.currency ? String(body.currency) : "USD"
30
+ });
31
+
32
+ return NextResponse.json({ id });
33
+ }
@@ -0,0 +1,15 @@
1
+ import { NextResponse } from "next/server";
2
+ import { runScan } from "@/src/ingestion/scan";
3
+
4
+ export const dynamic = "force-dynamic";
5
+
6
+ export async function POST(request: Request) {
7
+ const body = await request.json().catch(() => ({}));
8
+ const result = await runScan({
9
+ folders: Array.isArray(body.folders)
10
+ ? body.folders.filter((folder: unknown): folder is string => typeof folder === "string")
11
+ : undefined,
12
+ force: Boolean(body.force)
13
+ });
14
+ return NextResponse.json(result);
15
+ }
@@ -0,0 +1,25 @@
1
+ import { NextResponse } from "next/server";
2
+ import { getDatabasePath } from "@/src/db/client";
3
+ import { getAppSettings, saveAppSettings } from "@/src/db/settings";
4
+
5
+ export const dynamic = "force-dynamic";
6
+
7
+ export async function GET() {
8
+ return NextResponse.json({
9
+ ...getAppSettings(),
10
+ databasePath: getDatabasePath()
11
+ });
12
+ }
13
+
14
+ export async function PUT(request: Request) {
15
+ const body = await request.json();
16
+ const customFolders = Array.isArray(body.customFolders)
17
+ ? body.customFolders.filter((folder: unknown): folder is string => typeof folder === "string")
18
+ : [];
19
+ const saved = saveAppSettings({
20
+ customFolders,
21
+ storeRawMessageContent: Boolean(body.storeRawMessageContent)
22
+ });
23
+
24
+ return NextResponse.json(saved);
25
+ }
@@ -0,0 +1,101 @@
1
+ import { Badge } from "@/components/ui/badge";
2
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
3
+ import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
4
+ import { getDebugData } from "@/src/lib/analytics";
5
+ import { formatDate } from "@/src/lib/format";
6
+
7
+ export const dynamic = "force-dynamic";
8
+
9
+ function statusVariant(status: string) {
10
+ if (status === "imported") return "success";
11
+ if (status.includes("error") || status === "failed") return "destructive";
12
+ if (status.includes("skipped")) return "secondary";
13
+ return "warning";
14
+ }
15
+
16
+ export default function DebugPage() {
17
+ const data = getDebugData();
18
+
19
+ return (
20
+ <div className="space-y-6">
21
+ <div>
22
+ <h1 className="text-2xl font-semibold tracking-normal">Raw Data</h1>
23
+ <p className="text-sm text-muted-foreground">
24
+ Inspect recent scan runs, parser selection, imported records, warnings, and failures.
25
+ </p>
26
+ </div>
27
+
28
+ <Card>
29
+ <CardHeader>
30
+ <CardTitle>Recent Scan Runs</CardTitle>
31
+ <CardDescription>High-level import history.</CardDescription>
32
+ </CardHeader>
33
+ <CardContent className="table-scroll">
34
+ <Table>
35
+ <TableHeader>
36
+ <TableRow>
37
+ <TableHead>Started</TableHead>
38
+ <TableHead>Completed</TableHead>
39
+ <TableHead>Files</TableHead>
40
+ <TableHead>Imported</TableHead>
41
+ <TableHead>Warnings</TableHead>
42
+ <TableHead>Errors</TableHead>
43
+ </TableRow>
44
+ </TableHeader>
45
+ <TableBody>
46
+ {data.scanRuns.map((run) => (
47
+ <TableRow key={run.id}>
48
+ <TableCell>{formatDate(run.startedAt)}</TableCell>
49
+ <TableCell>{formatDate(run.completedAt)}</TableCell>
50
+ <TableCell>{run.filesScanned.toLocaleString()}</TableCell>
51
+ <TableCell>{run.recordsImported.toLocaleString()}</TableCell>
52
+ <TableCell>{run.warnings.length.toLocaleString()}</TableCell>
53
+ <TableCell>{run.errors.length.toLocaleString()}</TableCell>
54
+ </TableRow>
55
+ ))}
56
+ </TableBody>
57
+ </Table>
58
+ </CardContent>
59
+ </Card>
60
+
61
+ <Card>
62
+ <CardHeader>
63
+ <CardTitle>Scanned Files</CardTitle>
64
+ <CardDescription>Parser, status, warning, error, and metadata preview per file.</CardDescription>
65
+ </CardHeader>
66
+ <CardContent className="table-scroll">
67
+ <Table>
68
+ <TableHeader>
69
+ <TableRow>
70
+ <TableHead>File</TableHead>
71
+ <TableHead>Parser</TableHead>
72
+ <TableHead>Status</TableHead>
73
+ <TableHead>Imported</TableHead>
74
+ <TableHead>Warnings</TableHead>
75
+ <TableHead>Errors</TableHead>
76
+ <TableHead>Metadata</TableHead>
77
+ </TableRow>
78
+ </TableHeader>
79
+ <TableBody>
80
+ {data.scanFiles.map((file) => (
81
+ <TableRow key={file.id}>
82
+ <TableCell className="max-w-96 truncate font-mono text-xs">{file.path}</TableCell>
83
+ <TableCell>{file.parser ?? "None"}</TableCell>
84
+ <TableCell>
85
+ <Badge variant={statusVariant(file.status)}>{file.status}</Badge>
86
+ </TableCell>
87
+ <TableCell>{file.recordsImported.toLocaleString()}</TableCell>
88
+ <TableCell className="max-w-64 truncate">{file.warnings.join("; ") || "None"}</TableCell>
89
+ <TableCell className="max-w-64 truncate">{file.errors.join("; ") || "None"}</TableCell>
90
+ <TableCell className="max-w-80 truncate font-mono text-xs">
91
+ {JSON.stringify(file.rawMetadata)}
92
+ </TableCell>
93
+ </TableRow>
94
+ ))}
95
+ </TableBody>
96
+ </Table>
97
+ </CardContent>
98
+ </Card>
99
+ </div>
100
+ );
101
+ }
@@ -0,0 +1,113 @@
1
+ import Link from "next/link";
2
+ import { ArrowRight, CheckCircle2, FileWarning, SearchX } from "lucide-react";
3
+ import { Badge } from "@/components/ui/badge";
4
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
5
+ import { getDebugData } from "@/src/lib/analytics";
6
+ import { formatDate } from "@/src/lib/format";
7
+
8
+ export const dynamic = "force-dynamic";
9
+
10
+ export default function DiagnosticsPage() {
11
+ const data = getDebugData();
12
+ const latest = data.scanRuns[0];
13
+ const imported = data.scanFiles.filter((file) => file.status === "imported").length;
14
+ const unsupported = data.scanFiles.filter((file) => file.status === "skipped_unknown").length;
15
+ const failed = data.scanFiles.filter((file) => file.status === "failed").length;
16
+
17
+ return (
18
+ <div className="space-y-6">
19
+ <div>
20
+ <h1 className="text-2xl font-semibold tracking-normal">Ingestion Diagnostics</h1>
21
+ <p className="text-sm text-muted-foreground">
22
+ Passive filesystem ingestion status, parser coverage, and confidence transparency.
23
+ </p>
24
+ </div>
25
+
26
+ <div className="dashboard-grid">
27
+ <Card>
28
+ <CardHeader>
29
+ <CardTitle>Latest Scan</CardTitle>
30
+ <CardDescription>{latest ? formatDate(latest.startedAt) : "No scans yet"}</CardDescription>
31
+ </CardHeader>
32
+ <CardContent className="text-2xl font-semibold">
33
+ {latest?.filesScanned.toLocaleString() ?? "0"} files
34
+ </CardContent>
35
+ </Card>
36
+ <Card>
37
+ <CardHeader>
38
+ <CardTitle>Imported</CardTitle>
39
+ <CardDescription>Files parsed successfully.</CardDescription>
40
+ </CardHeader>
41
+ <CardContent className="flex items-center gap-2 text-2xl font-semibold">
42
+ <CheckCircle2 className="h-5 w-5 text-primary" />
43
+ {imported.toLocaleString()}
44
+ </CardContent>
45
+ </Card>
46
+ <Card>
47
+ <CardHeader>
48
+ <CardTitle>Unsupported</CardTitle>
49
+ <CardDescription>Discovered but no adapter matched.</CardDescription>
50
+ </CardHeader>
51
+ <CardContent className="flex items-center gap-2 text-2xl font-semibold">
52
+ <SearchX className="h-5 w-5 text-muted-foreground" />
53
+ {unsupported.toLocaleString()}
54
+ </CardContent>
55
+ </Card>
56
+ <Card>
57
+ <CardHeader>
58
+ <CardTitle>Failed</CardTitle>
59
+ <CardDescription>Parser or import errors.</CardDescription>
60
+ </CardHeader>
61
+ <CardContent className="flex items-center gap-2 text-2xl font-semibold">
62
+ <FileWarning className="h-5 w-5 text-destructive" />
63
+ {failed.toLocaleString()}
64
+ </CardContent>
65
+ </Card>
66
+ </div>
67
+
68
+ <div className="grid gap-4 md:grid-cols-3">
69
+ {[
70
+ {
71
+ href: "/discovery",
72
+ title: "File Discovery Explorer",
73
+ description: "Inspect which local files were discovered, skipped, imported, or unsupported."
74
+ },
75
+ {
76
+ href: "/parser-debug",
77
+ title: "Parser Debug",
78
+ description: "Review adapter selection, parser confidence, warnings, errors, and extracted metadata."
79
+ },
80
+ {
81
+ href: "/debug",
82
+ title: "Raw Data",
83
+ description: "See raw scan files and metadata previews for troubleshooting vendor format changes."
84
+ }
85
+ ].map((item) => (
86
+ <Link key={item.href} href={item.href}>
87
+ <Card className="h-full transition-colors hover:bg-muted/40">
88
+ <CardHeader>
89
+ <CardTitle className="flex items-center justify-between">
90
+ {item.title}
91
+ <ArrowRight className="h-4 w-4" />
92
+ </CardTitle>
93
+ <CardDescription>{item.description}</CardDescription>
94
+ </CardHeader>
95
+ </Card>
96
+ </Link>
97
+ ))}
98
+ </div>
99
+
100
+ <Card>
101
+ <CardHeader>
102
+ <CardTitle>Architecture Guardrails</CardTitle>
103
+ <CardDescription>TokenTrace uses direct local filesystem ingestion as the primary architecture.</CardDescription>
104
+ </CardHeader>
105
+ <CardContent className="flex flex-wrap gap-2">
106
+ {["no proxy", "no packet sniffing", "no browser extension", "no cloud telemetry", "adapter based"].map((item) => (
107
+ <Badge key={item} variant="secondary">{item}</Badge>
108
+ ))}
109
+ </CardContent>
110
+ </Card>
111
+ </div>
112
+ );
113
+ }
@@ -0,0 +1,61 @@
1
+ import { Badge } from "@/components/ui/badge";
2
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
3
+ import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
4
+ import { getDebugData } from "@/src/lib/analytics";
5
+ import { formatDate } from "@/src/lib/format";
6
+
7
+ export const dynamic = "force-dynamic";
8
+
9
+ function variant(status: string) {
10
+ if (status === "imported") return "success";
11
+ if (status === "skipped_unknown") return "secondary";
12
+ if (status === "failed") return "destructive";
13
+ return "warning";
14
+ }
15
+
16
+ export default function DiscoveryPage() {
17
+ const { scanFiles } = getDebugData();
18
+
19
+ return (
20
+ <div className="space-y-6">
21
+ <div>
22
+ <h1 className="text-2xl font-semibold tracking-normal">File Discovery Explorer</h1>
23
+ <p className="text-sm text-muted-foreground">
24
+ Every file shown here was discovered by passive local filesystem scanning.
25
+ </p>
26
+ </div>
27
+ <Card>
28
+ <CardHeader>
29
+ <CardTitle>Discovered Files</CardTitle>
30
+ <CardDescription>Unsupported files are retained so parser gaps are visible.</CardDescription>
31
+ </CardHeader>
32
+ <CardContent className="table-scroll">
33
+ <Table>
34
+ <TableHeader>
35
+ <TableRow>
36
+ <TableHead>File</TableHead>
37
+ <TableHead>Status</TableHead>
38
+ <TableHead>Size</TableHead>
39
+ <TableHead>Modified</TableHead>
40
+ <TableHead>Parser</TableHead>
41
+ <TableHead>Reason</TableHead>
42
+ </TableRow>
43
+ </TableHeader>
44
+ <TableBody>
45
+ {scanFiles.map((file) => (
46
+ <TableRow key={file.id}>
47
+ <TableCell className="max-w-xl truncate font-mono text-xs">{file.path}</TableCell>
48
+ <TableCell><Badge variant={variant(file.status)}>{file.status}</Badge></TableCell>
49
+ <TableCell>{file.sizeBytes.toLocaleString()} bytes</TableCell>
50
+ <TableCell>{formatDate(file.modifiedTime)}</TableCell>
51
+ <TableCell>{file.parser ?? "None"}</TableCell>
52
+ <TableCell className="max-w-sm truncate">{String(file.rawMetadata.reason ?? file.errors[0] ?? "None")}</TableCell>
53
+ </TableRow>
54
+ ))}
55
+ </TableBody>
56
+ </Table>
57
+ </CardContent>
58
+ </Card>
59
+ </div>
60
+ );
61
+ }
@@ -0,0 +1,51 @@
1
+ @tailwind base;
2
+ @tailwind components;
3
+ @tailwind utilities;
4
+
5
+ :root {
6
+ --background: 40 33% 98%;
7
+ --foreground: 220 20% 10%;
8
+ --card: 0 0% 100%;
9
+ --card-foreground: 220 20% 10%;
10
+ --popover: 0 0% 100%;
11
+ --popover-foreground: 220 20% 10%;
12
+ --primary: 176 72% 28%;
13
+ --primary-foreground: 0 0% 100%;
14
+ --secondary: 22 88% 56%;
15
+ --secondary-foreground: 0 0% 100%;
16
+ --muted: 35 24% 92%;
17
+ --muted-foreground: 220 9% 43%;
18
+ --accent: 46 96% 62%;
19
+ --accent-foreground: 220 20% 10%;
20
+ --destructive: 0 74% 48%;
21
+ --destructive-foreground: 0 0% 100%;
22
+ --border: 35 18% 84%;
23
+ --input: 35 18% 84%;
24
+ --ring: 176 72% 28%;
25
+ }
26
+
27
+ * {
28
+ border-color: hsl(var(--border));
29
+ }
30
+
31
+ body {
32
+ background: hsl(var(--background));
33
+ color: hsl(var(--foreground));
34
+ font-feature-settings: "rlig" 1, "calt" 1;
35
+ }
36
+
37
+ ::selection {
38
+ background: hsl(var(--accent));
39
+ color: hsl(var(--accent-foreground));
40
+ }
41
+
42
+ .dashboard-grid {
43
+ display: grid;
44
+ gap: 1rem;
45
+ grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
46
+ }
47
+
48
+ .table-scroll {
49
+ overflow-x: auto;
50
+ scrollbar-width: thin;
51
+ }
package/app/layout.tsx ADDED
@@ -0,0 +1,30 @@
1
+ import type { Metadata } from "next";
2
+ import "./globals.css";
3
+ import { MobileNav, Sidebar } from "@/components/sidebar";
4
+
5
+ export const metadata: Metadata = {
6
+ title: "TokenTrace CLI",
7
+ description: "Local-first AI CLI token and cost analytics"
8
+ };
9
+
10
+ export default function RootLayout({ children }: { children: React.ReactNode }) {
11
+ return (
12
+ <html lang="en">
13
+ <body>
14
+ <div className="flex min-h-screen">
15
+ <Sidebar />
16
+ <main className="min-w-0 flex-1">
17
+ <div className="border-b bg-card px-4 py-3 md:hidden">
18
+ <div className="text-sm font-semibold">TokenTrace CLI</div>
19
+ <div className="text-xs text-muted-foreground">Local analytics dashboard</div>
20
+ </div>
21
+ <MobileNav />
22
+ <div className="mx-auto w-full max-w-7xl px-4 py-6 sm:px-6 lg:px-8">
23
+ {children}
24
+ </div>
25
+ </main>
26
+ </div>
27
+ </body>
28
+ </html>
29
+ );
30
+ }
@@ -0,0 +1,97 @@
1
+ import { Badge } from "@/components/ui/badge";
2
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
3
+ import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
4
+ import { RankBarChart } from "@/components/charts/rank-bar-chart";
5
+ import { getAnalyticsData } from "@/src/lib/analytics";
6
+ import { formatCurrency, formatTokens } from "@/src/lib/format";
7
+
8
+ export const dynamic = "force-dynamic";
9
+
10
+ export default function ModelAnalyticsPage() {
11
+ const data = getAnalyticsData();
12
+
13
+ return (
14
+ <div className="space-y-6">
15
+ <div>
16
+ <h1 className="text-2xl font-semibold tracking-normal">Model Analytics</h1>
17
+ <p className="text-sm text-muted-foreground">
18
+ Usage, cost, output length, efficiency, and configured cheaper alternatives.
19
+ </p>
20
+ </div>
21
+
22
+ <div className="grid gap-4 lg:grid-cols-2">
23
+ <Card>
24
+ <CardHeader>
25
+ <CardTitle>Usage By Model</CardTitle>
26
+ <CardDescription>Total tokens by model.</CardDescription>
27
+ </CardHeader>
28
+ <CardContent>
29
+ <RankBarChart
30
+ data={data.models as unknown as Array<Record<string, string | number | null>>}
31
+ nameKey="model"
32
+ valueKey="totalTokens"
33
+ />
34
+ </CardContent>
35
+ </Card>
36
+ <Card>
37
+ <CardHeader>
38
+ <CardTitle>Cost By Model</CardTitle>
39
+ <CardDescription>Aggregated from per-interaction costs.</CardDescription>
40
+ </CardHeader>
41
+ <CardContent>
42
+ <RankBarChart
43
+ data={data.models as unknown as Array<Record<string, string | number | null>>}
44
+ nameKey="model"
45
+ valueKey="cost"
46
+ mode="cost"
47
+ color="#0f766e"
48
+ />
49
+ </CardContent>
50
+ </Card>
51
+ </div>
52
+
53
+ <Card>
54
+ <CardHeader>
55
+ <CardTitle>Model Table</CardTitle>
56
+ <CardDescription>Flags are deterministic and based on your configured price table.</CardDescription>
57
+ </CardHeader>
58
+ <CardContent className="table-scroll">
59
+ <Table>
60
+ <TableHeader>
61
+ <TableRow>
62
+ <TableHead>Model</TableHead>
63
+ <TableHead>Provider</TableHead>
64
+ <TableHead>Tokens</TableHead>
65
+ <TableHead>Cost</TableHead>
66
+ <TableHead>Avg output</TableHead>
67
+ <TableHead>Efficiency</TableHead>
68
+ <TableHead>Alternative</TableHead>
69
+ <TableHead>Flag</TableHead>
70
+ </TableRow>
71
+ </TableHeader>
72
+ <TableBody>
73
+ {data.models.map((model) => (
74
+ <TableRow key={`${model.provider}-${model.model}`}>
75
+ <TableCell className="font-medium">{model.model}</TableCell>
76
+ <TableCell>{model.provider}</TableCell>
77
+ <TableCell>{formatTokens(model.totalTokens)}</TableCell>
78
+ <TableCell>{formatCurrency(model.cost)}</TableCell>
79
+ <TableCell>{formatTokens(model.averageOutputTokens)}</TableCell>
80
+ <TableCell>{model.tokenEfficiency.toFixed(2)}x</TableCell>
81
+ <TableCell>{model.suggestedAlternative ?? "None configured"}</TableCell>
82
+ <TableCell>
83
+ {model.overuseFlag ? (
84
+ <Badge variant="warning">{model.overuseFlag}</Badge>
85
+ ) : (
86
+ <Badge variant="secondary">No flag</Badge>
87
+ )}
88
+ </TableCell>
89
+ </TableRow>
90
+ ))}
91
+ </TableBody>
92
+ </Table>
93
+ </CardContent>
94
+ </Card>
95
+ </div>
96
+ );
97
+ }
@@ -0,0 +1,67 @@
1
+ import { AlertTriangle, CheckCircle2, Info, Lightbulb } from "lucide-react";
2
+ import { Badge } from "@/components/ui/badge";
3
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
4
+ import { getAnalyticsData } from "@/src/lib/analytics";
5
+
6
+ const severityIcon = {
7
+ high: AlertTriangle,
8
+ medium: Lightbulb,
9
+ low: Info
10
+ };
11
+
12
+ const severityVariant = {
13
+ high: "destructive",
14
+ medium: "warning",
15
+ low: "secondary"
16
+ } as const;
17
+
18
+ export const dynamic = "force-dynamic";
19
+
20
+ export default function OptimisationPage() {
21
+ const data = getAnalyticsData();
22
+
23
+ return (
24
+ <div className="space-y-6">
25
+ <div>
26
+ <h1 className="text-2xl font-semibold tracking-normal">Optimisation Insights</h1>
27
+ <p className="text-sm text-muted-foreground">
28
+ Deterministic recommendations based on imported usage patterns.
29
+ </p>
30
+ </div>
31
+
32
+ <div className="grid gap-4">
33
+ {data.insights.map((insight) => {
34
+ const Icon = severityIcon[insight.severity] ?? CheckCircle2;
35
+ return (
36
+ <Card key={insight.id}>
37
+ <CardHeader className="flex flex-row items-start justify-between gap-4">
38
+ <div className="space-y-1">
39
+ <CardTitle className="flex items-center gap-2 text-base">
40
+ <Icon className="h-4 w-4 text-primary" />
41
+ {insight.problem}
42
+ </CardTitle>
43
+ <CardDescription>{insight.evidence}</CardDescription>
44
+ </div>
45
+ <Badge variant={severityVariant[insight.severity]}>{insight.severity}</Badge>
46
+ </CardHeader>
47
+ <CardContent className="grid gap-4 md:grid-cols-2">
48
+ <div className="rounded-md border bg-muted/40 p-3">
49
+ <div className="text-xs font-medium uppercase text-muted-foreground">
50
+ Saving opportunity
51
+ </div>
52
+ <p className="mt-1 text-sm">{insight.savingOpportunity}</p>
53
+ </div>
54
+ <div className="rounded-md border bg-muted/40 p-3">
55
+ <div className="text-xs font-medium uppercase text-muted-foreground">
56
+ Recommendation
57
+ </div>
58
+ <p className="mt-1 text-sm">{insight.recommendation}</p>
59
+ </div>
60
+ </CardContent>
61
+ </Card>
62
+ );
63
+ })}
64
+ </div>
65
+ </div>
66
+ );
67
+ }