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,316 @@
1
+ #!/usr/bin/env node
2
+ import fs from "node:fs";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import { spawn } from "node:child_process";
6
+ import { randomUUID } from "node:crypto";
7
+ import { createInterface } from "node:readline/promises";
8
+ import { stdin as input, stdout as output } from "node:process";
9
+ import { fileURLToPath } from "node:url";
10
+ import getPort, { portNumbers } from "get-port";
11
+ import open from "open";
12
+
13
+ const binPath = fs.realpathSync(fileURLToPath(import.meta.url));
14
+ const packageRoot = path.resolve(path.dirname(binPath), "..");
15
+ const invocationCwd = process.cwd();
16
+ const packageJson = JSON.parse(
17
+ fs.readFileSync(path.join(packageRoot, "package.json"), "utf8")
18
+ );
19
+
20
+ function help() {
21
+ return `TokenTrace CLI
22
+
23
+ Usage:
24
+ tokentrace Start local dashboard
25
+ tokentrace serve Start local dashboard
26
+ tokentrace scan Scan local AI CLI usage logs
27
+ tokentrace run <cmd> Run a command and record wrapper diagnostics
28
+ tokentrace reset Reset local database
29
+ tokentrace --version Print version`;
30
+ }
31
+
32
+ function appDataDir() {
33
+ if (process.env.TOKENTRACE_HOME) return path.resolve(process.env.TOKENTRACE_HOME);
34
+ const home = os.homedir();
35
+ if (process.platform === "darwin") {
36
+ return path.join(home, "Library", "Application Support", "TokenTrace");
37
+ }
38
+ if (process.platform === "win32") {
39
+ return path.join(process.env.APPDATA ?? path.join(home, "AppData", "Roaming"), "TokenTrace");
40
+ }
41
+ return path.join(process.env.XDG_DATA_HOME ?? path.join(home, ".local", "share"), "tokentrace");
42
+ }
43
+
44
+ function runtimeEnv() {
45
+ const dataDir = appDataDir();
46
+ fs.mkdirSync(dataDir, { recursive: true });
47
+ const dbPath = path.join(dataDir, "tokentrace.db");
48
+ return {
49
+ ...process.env,
50
+ TOKENTRACE_DB: process.env.TOKENTRACE_DB ?? dbPath,
51
+ DATABASE_URL: process.env.DATABASE_URL ?? `file:${dbPath}`,
52
+ TOKENTRACE_APP_DATA_DIR: dataDir,
53
+ TOKENTRACE_WORKDIR: invocationCwd,
54
+ NEXT_TELEMETRY_DISABLED: "1"
55
+ };
56
+ }
57
+
58
+ function nextBin() {
59
+ return path.join(packageRoot, "node_modules", "next", "dist", "bin", "next");
60
+ }
61
+
62
+ function runtimeScriptPath(scriptName) {
63
+ const compiled = path.join(packageRoot, "dist", "runtime", `${scriptName}.mjs`);
64
+ if (fs.existsSync(compiled)) return compiled;
65
+ return path.join(packageRoot, "scripts", `${scriptName}.ts`);
66
+ }
67
+
68
+ function scriptCommand(scriptName, args) {
69
+ const scriptPath = runtimeScriptPath(scriptName);
70
+ if (scriptPath.endsWith(".mjs")) {
71
+ return [process.execPath, [scriptPath, ...args]];
72
+ }
73
+ const tsx = path.join(packageRoot, "node_modules", "tsx", "dist", "cli.mjs");
74
+ return [process.execPath, [tsx, scriptPath, ...args]];
75
+ }
76
+
77
+ function runNodeScript(scriptName, args = [], options = {}) {
78
+ return new Promise((resolve, reject) => {
79
+ const [command, commandArgs] = scriptCommand(scriptName, args);
80
+ const child = spawn(command, commandArgs, {
81
+ cwd: packageRoot,
82
+ env: runtimeEnv(),
83
+ stdio: options.stdio ?? "inherit"
84
+ });
85
+ child.on("error", reject);
86
+ child.on("exit", (code) => {
87
+ if (code === 0) resolve();
88
+ else reject(new Error(`${scriptName} exited with code ${code}`));
89
+ });
90
+ });
91
+ }
92
+
93
+ async function initializeDatabase({ quiet = false } = {}) {
94
+ const env = runtimeEnv();
95
+ if (!quiet) {
96
+ console.log(`TokenTrace data: ${env.TOKENTRACE_APP_DATA_DIR}`);
97
+ }
98
+ await runNodeScript("db-migrate", [], { stdio: quiet ? "ignore" : "inherit" });
99
+ await runNodeScript("db-seed", [], { stdio: quiet ? "ignore" : "inherit" });
100
+ }
101
+
102
+ function sleep(ms) {
103
+ return new Promise((resolve) => setTimeout(resolve, ms));
104
+ }
105
+
106
+ async function waitForServer(url, child) {
107
+ const deadline = Date.now() + 30_000;
108
+ while (Date.now() < deadline) {
109
+ if (child.exitCode != null) {
110
+ throw new Error(`TokenTrace server exited with code ${child.exitCode}`);
111
+ }
112
+ try {
113
+ const response = await fetch(url, { method: "HEAD" });
114
+ if (response.ok) return;
115
+ } catch {
116
+ // Keep polling until the server is ready or the timeout expires.
117
+ }
118
+ await sleep(300);
119
+ }
120
+ throw new Error("Timed out waiting for the TokenTrace server to start.");
121
+ }
122
+
123
+ async function serve() {
124
+ const buildId = path.join(packageRoot, ".next", "BUILD_ID");
125
+ if (!fs.existsSync(buildId)) {
126
+ console.error("TokenTrace is not built yet. Run `npm run build` before using the package CLI from a source checkout.");
127
+ process.exit(1);
128
+ }
129
+
130
+ await initializeDatabase();
131
+ const port = await getPort({ port: portNumbers(3030, 3999) });
132
+ const hostname = "127.0.0.1";
133
+ const url = `http://localhost:${port}`;
134
+
135
+ console.log(`Starting TokenTrace at ${url}`);
136
+ console.log("Press Ctrl+C to stop the server.");
137
+
138
+ const child = spawn(
139
+ process.execPath,
140
+ [nextBin(), "start", "--hostname", hostname, "--port", String(port)],
141
+ {
142
+ cwd: packageRoot,
143
+ env: {
144
+ ...runtimeEnv(),
145
+ PORT: String(port),
146
+ HOSTNAME: hostname
147
+ },
148
+ stdio: "inherit"
149
+ }
150
+ );
151
+
152
+ const stop = () => {
153
+ if (!child.killed) child.kill("SIGINT");
154
+ };
155
+ process.on("SIGINT", stop);
156
+ process.on("SIGTERM", stop);
157
+
158
+ try {
159
+ await waitForServer(url, child);
160
+ await open(url).catch(() => {
161
+ console.log(`Open this URL in your browser: ${url}`);
162
+ });
163
+ } catch (error) {
164
+ console.error(error instanceof Error ? error.message : "Failed to start TokenTrace.");
165
+ }
166
+
167
+ child.on("exit", (code) => process.exit(code ?? 0));
168
+ }
169
+
170
+ async function scan(args) {
171
+ await initializeDatabase({ quiet: true });
172
+ await runNodeScript("scan", args);
173
+ }
174
+
175
+ async function reset(args) {
176
+ await initializeDatabase({ quiet: true });
177
+ if (!args.includes("--yes")) {
178
+ const rl = createInterface({ input, output });
179
+ const answer = await rl.question(
180
+ "Reset TokenTrace imported data and scan history? Settings and pricing will be kept. Continue? [y/N] "
181
+ );
182
+ rl.close();
183
+ if (!/^y(es)?$/i.test(answer.trim())) {
184
+ console.log("Reset cancelled.");
185
+ return;
186
+ }
187
+ }
188
+ await runNodeScript("reset", []);
189
+ }
190
+
191
+ function looksStructured(text) {
192
+ const trimmed = text.trim();
193
+ if (!trimmed || trimmed.length > 100_000) return null;
194
+ try {
195
+ const parsed = JSON.parse(trimmed);
196
+ if (parsed && typeof parsed === "object") return parsed;
197
+ } catch {
198
+ // Not structured JSON.
199
+ }
200
+ return null;
201
+ }
202
+
203
+ async function runWrapped(args) {
204
+ if (!args.length) {
205
+ console.error("Usage: tokentrace run <command> [args...]");
206
+ process.exit(1);
207
+ }
208
+
209
+ const env = runtimeEnv();
210
+ fs.mkdirSync(path.join(env.TOKENTRACE_APP_DATA_DIR, "wrapper-runs"), {
211
+ recursive: true
212
+ });
213
+
214
+ const [command, ...commandArgs] = args;
215
+ const startedAt = new Date();
216
+ let stdoutBytes = 0;
217
+ let stderrBytes = 0;
218
+ const stdoutChunks = [];
219
+ const stderrChunks = [];
220
+
221
+ const child = spawn(command, commandArgs, {
222
+ cwd: invocationCwd,
223
+ env: process.env,
224
+ stdio: ["inherit", "pipe", "pipe"],
225
+ shell: process.platform === "win32"
226
+ });
227
+
228
+ child.stdout.on("data", (chunk) => {
229
+ stdoutBytes += chunk.length;
230
+ if (stdoutBytes <= 100_000) stdoutChunks.push(chunk);
231
+ process.stdout.write(chunk);
232
+ });
233
+ child.stderr.on("data", (chunk) => {
234
+ stderrBytes += chunk.length;
235
+ if (stderrBytes <= 100_000) stderrChunks.push(chunk);
236
+ process.stderr.write(chunk);
237
+ });
238
+
239
+ const exitCode = await new Promise((resolve, reject) => {
240
+ child.on("error", reject);
241
+ child.on("exit", (code) => resolve(code ?? 0));
242
+ });
243
+
244
+ const endedAt = new Date();
245
+ const durationMs = endedAt.getTime() - startedAt.getTime();
246
+ const stdoutSample = Buffer.concat(stdoutChunks).toString("utf8");
247
+ const stderrSample = Buffer.concat(stderrChunks).toString("utf8");
248
+ const structuredOutput = looksStructured(stdoutSample);
249
+ const sessionId = `wrapper-${randomUUID()}`;
250
+ const record = {
251
+ timestamp: endedAt.toISOString(),
252
+ session_id: sessionId,
253
+ role: "tool",
254
+ type: "tokentrace.wrapper_run",
255
+ cwd: invocationCwd,
256
+ content: `Wrapper run for ${command} completed in ${durationMs}ms with ${stdoutBytes} stdout bytes and ${stderrBytes} stderr bytes.`,
257
+ command,
258
+ args: commandArgs,
259
+ duration_ms: durationMs,
260
+ stdout_bytes: stdoutBytes,
261
+ stderr_bytes: stderrBytes,
262
+ exit_code: exitCode,
263
+ structured_output_detected: Boolean(structuredOutput),
264
+ structured_output_preview: structuredOutput ?? undefined
265
+ };
266
+ const logPath = path.join(env.TOKENTRACE_APP_DATA_DIR, "wrapper-runs", "runs.jsonl");
267
+ fs.appendFileSync(logPath, `${JSON.stringify(record)}\n`);
268
+
269
+ console.log("");
270
+ console.log("TokenTrace wrapper summary");
271
+ console.log(`Duration: ${durationMs}ms`);
272
+ console.log(`stdout bytes: ${stdoutBytes}`);
273
+ console.log(`stderr bytes: ${stderrBytes}`);
274
+ console.log(`exit code: ${exitCode}`);
275
+ console.log(`diagnostic log: ${logPath}`);
276
+
277
+ process.exit(exitCode);
278
+ }
279
+
280
+ async function main() {
281
+ const [command = "serve", ...args] = process.argv.slice(2);
282
+
283
+ if (command === "--help" || command === "-h" || command === "help") {
284
+ console.log(help());
285
+ return;
286
+ }
287
+ if (command === "--version" || command === "-v") {
288
+ console.log(packageJson.version);
289
+ return;
290
+ }
291
+ if (command === "serve") {
292
+ await serve();
293
+ return;
294
+ }
295
+ if (command === "scan") {
296
+ await scan(args);
297
+ return;
298
+ }
299
+ if (command === "run") {
300
+ await runWrapped(args);
301
+ return;
302
+ }
303
+ if (command === "reset") {
304
+ await reset(args);
305
+ return;
306
+ }
307
+
308
+ console.error(`Unknown command: ${command}\n`);
309
+ console.error(help());
310
+ process.exit(1);
311
+ }
312
+
313
+ main().catch((error) => {
314
+ console.error(error instanceof Error ? error.message : error);
315
+ process.exit(1);
316
+ });
@@ -0,0 +1,69 @@
1
+ "use client";
2
+
3
+ import {
4
+ Bar,
5
+ BarChart,
6
+ CartesianGrid,
7
+ ResponsiveContainer,
8
+ Tooltip,
9
+ XAxis,
10
+ YAxis
11
+ } from "recharts";
12
+ import { formatCurrency, formatTokens } from "@/src/lib/format";
13
+
14
+ export function RankBarChart({
15
+ data,
16
+ nameKey,
17
+ valueKey,
18
+ mode = "tokens",
19
+ color = "#ea580c"
20
+ }: {
21
+ data: Array<Record<string, string | number | null>>;
22
+ nameKey: string;
23
+ valueKey: string;
24
+ mode?: "tokens" | "cost" | "count";
25
+ color?: string;
26
+ }) {
27
+ const chartData = data
28
+ .slice(0, 8)
29
+ .map((item) => ({
30
+ name: String(item[nameKey] ?? "Unknown"),
31
+ value: Number(item[valueKey] ?? 0)
32
+ }))
33
+ .filter((item) => item.value > 0);
34
+
35
+ return (
36
+ <div className="h-72">
37
+ <ResponsiveContainer width="100%" height="100%">
38
+ <BarChart data={chartData} layout="vertical" margin={{ left: 16, right: 12, top: 8, bottom: 0 }}>
39
+ <CartesianGrid strokeDasharray="3 3" stroke="#e7ded3" />
40
+ <XAxis
41
+ type="number"
42
+ tick={{ fontSize: 12 }}
43
+ stroke="#786f65"
44
+ tickFormatter={(value) =>
45
+ mode === "cost" ? `$${Number(value).toFixed(0)}` : formatTokens(Number(value))
46
+ }
47
+ />
48
+ <YAxis
49
+ dataKey="name"
50
+ type="category"
51
+ width={120}
52
+ tick={{ fontSize: 12 }}
53
+ stroke="#786f65"
54
+ />
55
+ <Tooltip
56
+ formatter={(value) =>
57
+ mode === "cost"
58
+ ? formatCurrency(Number(value))
59
+ : mode === "tokens"
60
+ ? `${formatTokens(Number(value))} tokens`
61
+ : Number(value).toLocaleString()
62
+ }
63
+ />
64
+ <Bar dataKey="value" fill={color} radius={[0, 4, 4, 0]} />
65
+ </BarChart>
66
+ </ResponsiveContainer>
67
+ </div>
68
+ );
69
+ }
@@ -0,0 +1,123 @@
1
+ "use client";
2
+
3
+ import { useMemo, useState } from "react";
4
+ import {
5
+ Area,
6
+ AreaChart,
7
+ CartesianGrid,
8
+ ResponsiveContainer,
9
+ Tooltip,
10
+ XAxis,
11
+ YAxis
12
+ } from "recharts";
13
+ import type { TrendPoint } from "@/src/lib/analytics";
14
+ import { formatCurrency, formatShortDate, formatTokens } from "@/src/lib/format";
15
+ import { Button } from "@/components/ui/button";
16
+
17
+ type Period = "daily" | "weekly" | "monthly";
18
+
19
+ function bucketFor(date: string, period: Period) {
20
+ const parsed = new Date(`${date}T00:00:00`);
21
+ if (period === "monthly") return `${parsed.getFullYear()}-${String(parsed.getMonth() + 1).padStart(2, "0")}-01`;
22
+ if (period === "weekly") {
23
+ const day = parsed.getDay() || 7;
24
+ parsed.setDate(parsed.getDate() - day + 1);
25
+ return parsed.toISOString().slice(0, 10);
26
+ }
27
+ return date;
28
+ }
29
+
30
+ export function TrendChart({
31
+ data,
32
+ metric,
33
+ color = "#0f766e"
34
+ }: {
35
+ data: TrendPoint[];
36
+ metric: "totalTokens" | "cost";
37
+ color?: string;
38
+ }) {
39
+ const [period, setPeriod] = useState<Period>("daily");
40
+ const chartData = useMemo(() => {
41
+ const buckets = new Map<string, TrendPoint>();
42
+ data.forEach((point) => {
43
+ const key = bucketFor(point.date, period);
44
+ const existing =
45
+ buckets.get(key) ??
46
+ ({
47
+ date: key,
48
+ totalTokens: 0,
49
+ inputTokens: 0,
50
+ outputTokens: 0,
51
+ cachedTokens: 0,
52
+ reasoningTokens: 0,
53
+ cost: 0
54
+ } satisfies TrendPoint);
55
+ existing.totalTokens += point.totalTokens;
56
+ existing.inputTokens += point.inputTokens;
57
+ existing.outputTokens += point.outputTokens;
58
+ existing.cachedTokens += point.cachedTokens;
59
+ existing.reasoningTokens += point.reasoningTokens;
60
+ existing.cost += point.cost;
61
+ buckets.set(key, existing);
62
+ });
63
+ return Array.from(buckets.values()).sort((a, b) => a.date.localeCompare(b.date));
64
+ }, [data, period]);
65
+
66
+ return (
67
+ <div className="space-y-3">
68
+ <div className="flex flex-wrap gap-2">
69
+ {(["daily", "weekly", "monthly"] as const).map((item) => (
70
+ <Button
71
+ key={item}
72
+ size="sm"
73
+ variant={period === item ? "default" : "outline"}
74
+ onClick={() => setPeriod(item)}
75
+ >
76
+ {item[0].toUpperCase() + item.slice(1)}
77
+ </Button>
78
+ ))}
79
+ </div>
80
+ <div className="h-72">
81
+ <ResponsiveContainer width="100%" height="100%">
82
+ <AreaChart data={chartData} margin={{ left: 0, right: 8, top: 8, bottom: 0 }}>
83
+ <defs>
84
+ <linearGradient id={`trend-${metric}`} x1="0" y1="0" x2="0" y2="1">
85
+ <stop offset="5%" stopColor={color} stopOpacity={0.35} />
86
+ <stop offset="95%" stopColor={color} stopOpacity={0.02} />
87
+ </linearGradient>
88
+ </defs>
89
+ <CartesianGrid strokeDasharray="3 3" stroke="#e7ded3" />
90
+ <XAxis
91
+ dataKey="date"
92
+ tickFormatter={formatShortDate}
93
+ tick={{ fontSize: 12 }}
94
+ stroke="#786f65"
95
+ />
96
+ <YAxis
97
+ tick={{ fontSize: 12 }}
98
+ stroke="#786f65"
99
+ tickFormatter={(value) =>
100
+ metric === "cost" ? `$${Number(value).toFixed(0)}` : formatTokens(Number(value))
101
+ }
102
+ />
103
+ <Tooltip
104
+ formatter={(value) =>
105
+ metric === "cost"
106
+ ? formatCurrency(Number(value))
107
+ : `${formatTokens(Number(value))} tokens`
108
+ }
109
+ labelFormatter={formatShortDate}
110
+ />
111
+ <Area
112
+ type="monotone"
113
+ dataKey={metric}
114
+ stroke={color}
115
+ fill={`url(#trend-${metric})`}
116
+ strokeWidth={2}
117
+ />
118
+ </AreaChart>
119
+ </ResponsiveContainer>
120
+ </div>
121
+ </div>
122
+ );
123
+ }
@@ -0,0 +1,14 @@
1
+ import { Database } from "lucide-react";
2
+ import { Card, CardContent } from "@/components/ui/card";
3
+
4
+ export function EmptyState({ title, description }: { title: string; description: string }) {
5
+ return (
6
+ <Card>
7
+ <CardContent className="flex min-h-48 flex-col items-center justify-center gap-2 text-center">
8
+ <Database className="h-8 w-8 text-muted-foreground" />
9
+ <div className="font-medium">{title}</div>
10
+ <p className="max-w-md text-sm text-muted-foreground">{description}</p>
11
+ </CardContent>
12
+ </Card>
13
+ );
14
+ }
@@ -0,0 +1,171 @@
1
+ "use client";
2
+
3
+ import { useState, useTransition } from "react";
4
+ import { Plus, Save } from "lucide-react";
5
+ import type { PricingRow } from "@/src/lib/pricing";
6
+ import { Button } from "@/components/ui/button";
7
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
8
+ import { Input } from "@/components/ui/input";
9
+ import { Label } from "@/components/ui/label";
10
+ import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
11
+
12
+ type EditablePricingRow = PricingRow & {
13
+ providerName?: string;
14
+ };
15
+
16
+ function numberInputValue(value: number | null) {
17
+ return value == null ? "" : String(value);
18
+ }
19
+
20
+ export function PricingSettings({ initialRows }: { initialRows: PricingRow[] }) {
21
+ const [rows, setRows] = useState<EditablePricingRow[]>(initialRows);
22
+ const [isPending, startTransition] = useTransition();
23
+ const [message, setMessage] = useState("");
24
+
25
+ function updateRow(index: number, patch: Partial<EditablePricingRow>) {
26
+ setRows((current) =>
27
+ current.map((row, rowIndex) => (rowIndex === index ? { ...row, ...patch } : row))
28
+ );
29
+ }
30
+
31
+ function addRow() {
32
+ setRows((current) => [
33
+ {
34
+ id: `new-${Date.now()}`,
35
+ providerId: "custom",
36
+ provider: "Custom",
37
+ providerName: "Custom",
38
+ model: "",
39
+ inputTokenPrice: null,
40
+ outputTokenPrice: null,
41
+ cachedInputTokenPrice: null,
42
+ currency: "USD",
43
+ effectiveFrom: null
44
+ },
45
+ ...current
46
+ ]);
47
+ }
48
+
49
+ function saveRow(row: EditablePricingRow) {
50
+ startTransition(async () => {
51
+ setMessage("");
52
+ const response = await fetch("/api/prices", {
53
+ method: "POST",
54
+ headers: { "content-type": "application/json" },
55
+ body: JSON.stringify({
56
+ providerId: row.providerId,
57
+ providerName: row.providerName ?? row.provider,
58
+ model: row.model,
59
+ inputTokenPrice: row.inputTokenPrice,
60
+ outputTokenPrice: row.outputTokenPrice,
61
+ cachedInputTokenPrice: row.cachedInputTokenPrice,
62
+ currency: row.currency
63
+ })
64
+ });
65
+ if (!response.ok) {
66
+ setMessage("Price save failed.");
67
+ return;
68
+ }
69
+ const latest = (await fetch("/api/prices").then((res) => res.json())) as PricingRow[];
70
+ setRows(latest);
71
+ setMessage("Price saved.");
72
+ });
73
+ }
74
+
75
+ return (
76
+ <div className="space-y-4">
77
+ <Card>
78
+ <CardHeader className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
79
+ <div>
80
+ <CardTitle>Model Pricing</CardTitle>
81
+ <CardDescription>
82
+ Prices are per 1M tokens. Seed values are editable placeholders.
83
+ </CardDescription>
84
+ </div>
85
+ <Button variant="outline" onClick={addRow}>
86
+ <Plus className="h-4 w-4" />
87
+ Add model
88
+ </Button>
89
+ </CardHeader>
90
+ <CardContent className="table-scroll">
91
+ <Table>
92
+ <TableHeader>
93
+ <TableRow>
94
+ <TableHead>Provider ID</TableHead>
95
+ <TableHead>Provider</TableHead>
96
+ <TableHead>Model</TableHead>
97
+ <TableHead>Input / 1M</TableHead>
98
+ <TableHead>Output / 1M</TableHead>
99
+ <TableHead>Cached input / 1M</TableHead>
100
+ <TableHead>Currency</TableHead>
101
+ <TableHead></TableHead>
102
+ </TableRow>
103
+ </TableHeader>
104
+ <TableBody>
105
+ {rows.map((row, index) => (
106
+ <TableRow key={row.id}>
107
+ <TableCell>
108
+ <Input value={row.providerId} onChange={(event) => updateRow(index, { providerId: event.target.value })} />
109
+ </TableCell>
110
+ <TableCell>
111
+ <Input value={row.providerName ?? row.provider} onChange={(event) => updateRow(index, { providerName: event.target.value })} />
112
+ </TableCell>
113
+ <TableCell>
114
+ <Input value={row.model} onChange={(event) => updateRow(index, { model: event.target.value })} />
115
+ </TableCell>
116
+ <TableCell>
117
+ <Input
118
+ inputMode="decimal"
119
+ value={numberInputValue(row.inputTokenPrice)}
120
+ onChange={(event) => updateRow(index, { inputTokenPrice: event.target.value === "" ? null : Number(event.target.value) })}
121
+ />
122
+ </TableCell>
123
+ <TableCell>
124
+ <Input
125
+ inputMode="decimal"
126
+ value={numberInputValue(row.outputTokenPrice)}
127
+ onChange={(event) => updateRow(index, { outputTokenPrice: event.target.value === "" ? null : Number(event.target.value) })}
128
+ />
129
+ </TableCell>
130
+ <TableCell>
131
+ <Input
132
+ inputMode="decimal"
133
+ value={numberInputValue(row.cachedInputTokenPrice)}
134
+ onChange={(event) => updateRow(index, { cachedInputTokenPrice: event.target.value === "" ? null : Number(event.target.value) })}
135
+ />
136
+ </TableCell>
137
+ <TableCell>
138
+ <Input value={row.currency} onChange={(event) => updateRow(index, { currency: event.target.value })} />
139
+ </TableCell>
140
+ <TableCell>
141
+ <Button size="sm" onClick={() => saveRow(row)} disabled={isPending || !row.model}>
142
+ <Save className="h-4 w-4" />
143
+ Save
144
+ </Button>
145
+ </TableCell>
146
+ </TableRow>
147
+ ))}
148
+ </TableBody>
149
+ </Table>
150
+ {message ? <div className="mt-3 text-sm text-muted-foreground">{message}</div> : null}
151
+ </CardContent>
152
+ </Card>
153
+
154
+ <Card>
155
+ <CardHeader>
156
+ <CardTitle>Cost Formula</CardTitle>
157
+ <CardDescription>Costs are computed per interaction and then aggregated.</CardDescription>
158
+ </CardHeader>
159
+ <CardContent className="space-y-3 text-sm text-muted-foreground">
160
+ <Label>Formula</Label>
161
+ <div className="rounded-md border bg-muted/40 p-3 font-mono text-xs text-foreground">
162
+ input * inputPrice + output * outputPrice + cacheRead * cachedInputPrice + cacheWrite * inputPrice
163
+ </div>
164
+ <p>
165
+ TokenTrace separates exact token counts from estimated counts. Unknown model prices produce unknown costs.
166
+ </p>
167
+ </CardContent>
168
+ </Card>
169
+ </div>
170
+ );
171
+ }