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,1817 @@
1
+ import { createRequire as __tokentraceCreateRequire } from 'node:module'; const require = __tokentraceCreateRequire(import.meta.url);
2
+ var __defProp = Object.defineProperty;
3
+ var __export = (target, all) => {
4
+ for (var name in all)
5
+ __defProp(target, name, { get: all[name], enumerable: true });
6
+ };
7
+
8
+ // src/ingestion/scan.ts
9
+ import fs5 from "node:fs/promises";
10
+ import path10 from "node:path";
11
+
12
+ // src/db/client.ts
13
+ import fs from "node:fs";
14
+ import path from "node:path";
15
+ import { fileURLToPath } from "node:url";
16
+ import Database from "better-sqlite3";
17
+ import { drizzle } from "drizzle-orm/better-sqlite3";
18
+
19
+ // src/db/migrate-core.ts
20
+ var ddl = `
21
+ PRAGMA journal_mode = WAL;
22
+ PRAGMA foreign_keys = ON;
23
+
24
+ CREATE TABLE IF NOT EXISTS providers (
25
+ id TEXT PRIMARY KEY,
26
+ name TEXT NOT NULL,
27
+ type TEXT NOT NULL,
28
+ created_at INTEGER NOT NULL DEFAULT (unixepoch() * 1000)
29
+ );
30
+
31
+ CREATE TABLE IF NOT EXISTS tools (
32
+ id TEXT PRIMARY KEY,
33
+ provider_id TEXT NOT NULL REFERENCES providers(id) ON DELETE CASCADE,
34
+ name TEXT NOT NULL,
35
+ created_at INTEGER NOT NULL DEFAULT (unixepoch() * 1000)
36
+ );
37
+ CREATE UNIQUE INDEX IF NOT EXISTS tools_provider_name_idx ON tools(provider_id, name);
38
+
39
+ CREATE TABLE IF NOT EXISTS models (
40
+ id TEXT PRIMARY KEY,
41
+ provider_id TEXT NOT NULL REFERENCES providers(id) ON DELETE CASCADE,
42
+ name TEXT NOT NULL,
43
+ input_token_price REAL,
44
+ output_token_price REAL,
45
+ cached_input_token_price REAL,
46
+ currency TEXT NOT NULL DEFAULT 'USD',
47
+ effective_from INTEGER,
48
+ raw_metadata TEXT
49
+ );
50
+ CREATE UNIQUE INDEX IF NOT EXISTS models_provider_name_idx ON models(provider_id, name);
51
+
52
+ CREATE TABLE IF NOT EXISTS projects (
53
+ id TEXT PRIMARY KEY,
54
+ name TEXT NOT NULL,
55
+ path TEXT NOT NULL,
56
+ created_at INTEGER NOT NULL DEFAULT (unixepoch() * 1000)
57
+ );
58
+ CREATE UNIQUE INDEX IF NOT EXISTS projects_path_idx ON projects(path);
59
+
60
+ CREATE TABLE IF NOT EXISTS sessions (
61
+ id TEXT PRIMARY KEY,
62
+ source_id TEXT NOT NULL,
63
+ tool_id TEXT NOT NULL REFERENCES tools(id) ON DELETE CASCADE,
64
+ project_id TEXT REFERENCES projects(id) ON DELETE SET NULL,
65
+ started_at INTEGER,
66
+ ended_at INTEGER,
67
+ title TEXT,
68
+ source_file TEXT NOT NULL,
69
+ raw_metadata TEXT
70
+ );
71
+ CREATE UNIQUE INDEX IF NOT EXISTS sessions_source_id_idx ON sessions(source_id);
72
+ CREATE INDEX IF NOT EXISTS sessions_tool_idx ON sessions(tool_id);
73
+ CREATE INDEX IF NOT EXISTS sessions_project_idx ON sessions(project_id);
74
+ CREATE INDEX IF NOT EXISTS sessions_started_idx ON sessions(started_at);
75
+
76
+ CREATE TABLE IF NOT EXISTS interactions (
77
+ id TEXT PRIMARY KEY,
78
+ source_id TEXT NOT NULL,
79
+ session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
80
+ timestamp INTEGER,
81
+ role TEXT NOT NULL,
82
+ model_id TEXT REFERENCES models(id) ON DELETE SET NULL,
83
+ input_tokens INTEGER NOT NULL DEFAULT 0,
84
+ output_tokens INTEGER NOT NULL DEFAULT 0,
85
+ cache_read_tokens INTEGER NOT NULL DEFAULT 0,
86
+ cache_write_tokens INTEGER NOT NULL DEFAULT 0,
87
+ reasoning_tokens INTEGER NOT NULL DEFAULT 0,
88
+ total_tokens INTEGER NOT NULL DEFAULT 0,
89
+ estimated_tokens INTEGER NOT NULL DEFAULT 0,
90
+ token_confidence TEXT NOT NULL DEFAULT 'unknown',
91
+ cost REAL,
92
+ cost_estimated INTEGER NOT NULL DEFAULT 0,
93
+ latency_ms INTEGER,
94
+ raw_text_preview TEXT,
95
+ raw_text TEXT,
96
+ raw_metadata TEXT
97
+ );
98
+ CREATE UNIQUE INDEX IF NOT EXISTS interactions_source_id_idx ON interactions(source_id);
99
+ CREATE INDEX IF NOT EXISTS interactions_session_idx ON interactions(session_id);
100
+ CREATE INDEX IF NOT EXISTS interactions_model_idx ON interactions(model_id);
101
+ CREATE INDEX IF NOT EXISTS interactions_timestamp_idx ON interactions(timestamp);
102
+
103
+ PRAGMA user_version;
104
+
105
+ CREATE TABLE IF NOT EXISTS tool_calls (
106
+ id TEXT PRIMARY KEY,
107
+ interaction_id TEXT NOT NULL REFERENCES interactions(id) ON DELETE CASCADE,
108
+ name TEXT NOT NULL,
109
+ status TEXT,
110
+ duration_ms INTEGER,
111
+ raw_metadata TEXT
112
+ );
113
+ CREATE INDEX IF NOT EXISTS tool_calls_interaction_idx ON tool_calls(interaction_id);
114
+
115
+ CREATE TABLE IF NOT EXISTS scan_runs (
116
+ id TEXT PRIMARY KEY,
117
+ started_at INTEGER NOT NULL,
118
+ completed_at INTEGER,
119
+ files_scanned INTEGER NOT NULL DEFAULT 0,
120
+ records_imported INTEGER NOT NULL DEFAULT 0,
121
+ warnings TEXT NOT NULL DEFAULT '[]',
122
+ errors TEXT NOT NULL DEFAULT '[]'
123
+ );
124
+
125
+ CREATE TABLE IF NOT EXISTS scan_files (
126
+ id TEXT PRIMARY KEY,
127
+ scan_run_id TEXT NOT NULL REFERENCES scan_runs(id) ON DELETE CASCADE,
128
+ path TEXT NOT NULL,
129
+ modified_time INTEGER,
130
+ size_bytes INTEGER NOT NULL DEFAULT 0,
131
+ file_hash TEXT,
132
+ parser TEXT,
133
+ status TEXT NOT NULL,
134
+ records_imported INTEGER NOT NULL DEFAULT 0,
135
+ warnings TEXT NOT NULL DEFAULT '[]',
136
+ errors TEXT NOT NULL DEFAULT '[]',
137
+ raw_metadata TEXT
138
+ );
139
+ CREATE INDEX IF NOT EXISTS scan_files_path_hash_idx ON scan_files(path, file_hash);
140
+ CREATE INDEX IF NOT EXISTS scan_files_run_idx ON scan_files(scan_run_id);
141
+
142
+ CREATE TABLE IF NOT EXISTS settings (
143
+ key TEXT PRIMARY KEY,
144
+ value TEXT NOT NULL,
145
+ updated_at INTEGER NOT NULL DEFAULT (unixepoch() * 1000)
146
+ );
147
+ `;
148
+ function applyMigrations(sqlite2) {
149
+ sqlite2.exec(ddl);
150
+ const columns = sqlite2.prepare("PRAGMA table_info(interactions)").all();
151
+ if (!columns.some((column) => column.name === "token_confidence")) {
152
+ try {
153
+ sqlite2.exec("ALTER TABLE interactions ADD COLUMN token_confidence TEXT NOT NULL DEFAULT 'unknown'");
154
+ } catch (error) {
155
+ if (!(error instanceof Error) || !error.message.toLowerCase().includes("duplicate column")) {
156
+ throw error;
157
+ }
158
+ }
159
+ }
160
+ }
161
+
162
+ // src/db/schema.ts
163
+ var schema_exports = {};
164
+ __export(schema_exports, {
165
+ interactionRelations: () => interactionRelations,
166
+ interactions: () => interactions,
167
+ modelRelations: () => modelRelations,
168
+ models: () => models,
169
+ projectRelations: () => projectRelations,
170
+ projects: () => projects,
171
+ providerRelations: () => providerRelations,
172
+ providers: () => providers,
173
+ scanFiles: () => scanFiles,
174
+ scanRuns: () => scanRuns,
175
+ sessionRelations: () => sessionRelations,
176
+ sessions: () => sessions,
177
+ settings: () => settings,
178
+ toolCalls: () => toolCalls,
179
+ toolRelations: () => toolRelations,
180
+ tools: () => tools
181
+ });
182
+ import { relations, sql } from "drizzle-orm";
183
+ import {
184
+ index,
185
+ integer,
186
+ real,
187
+ sqliteTable,
188
+ text,
189
+ uniqueIndex
190
+ } from "drizzle-orm/sqlite-core";
191
+ var providers = sqliteTable("providers", {
192
+ id: text("id").primaryKey(),
193
+ name: text("name").notNull(),
194
+ type: text("type").notNull(),
195
+ createdAt: integer("created_at", { mode: "timestamp_ms" }).notNull().$defaultFn(() => /* @__PURE__ */ new Date())
196
+ });
197
+ var tools = sqliteTable(
198
+ "tools",
199
+ {
200
+ id: text("id").primaryKey(),
201
+ providerId: text("provider_id").notNull().references(() => providers.id, { onDelete: "cascade" }),
202
+ name: text("name").notNull(),
203
+ createdAt: integer("created_at", { mode: "timestamp_ms" }).notNull().$defaultFn(() => /* @__PURE__ */ new Date())
204
+ },
205
+ (table) => ({
206
+ providerNameIdx: uniqueIndex("tools_provider_name_idx").on(
207
+ table.providerId,
208
+ table.name
209
+ )
210
+ })
211
+ );
212
+ var models = sqliteTable(
213
+ "models",
214
+ {
215
+ id: text("id").primaryKey(),
216
+ providerId: text("provider_id").notNull().references(() => providers.id, { onDelete: "cascade" }),
217
+ name: text("name").notNull(),
218
+ inputTokenPrice: real("input_token_price"),
219
+ outputTokenPrice: real("output_token_price"),
220
+ cachedInputTokenPrice: real("cached_input_token_price"),
221
+ currency: text("currency").notNull().default("USD"),
222
+ effectiveFrom: integer("effective_from", { mode: "timestamp_ms" }),
223
+ rawMetadata: text("raw_metadata", { mode: "json" }).$type()
224
+ },
225
+ (table) => ({
226
+ providerModelIdx: uniqueIndex("models_provider_name_idx").on(
227
+ table.providerId,
228
+ table.name
229
+ )
230
+ })
231
+ );
232
+ var projects = sqliteTable(
233
+ "projects",
234
+ {
235
+ id: text("id").primaryKey(),
236
+ name: text("name").notNull(),
237
+ path: text("path").notNull(),
238
+ createdAt: integer("created_at", { mode: "timestamp_ms" }).notNull().$defaultFn(() => /* @__PURE__ */ new Date())
239
+ },
240
+ (table) => ({
241
+ pathIdx: uniqueIndex("projects_path_idx").on(table.path)
242
+ })
243
+ );
244
+ var sessions = sqliteTable(
245
+ "sessions",
246
+ {
247
+ id: text("id").primaryKey(),
248
+ sourceId: text("source_id").notNull(),
249
+ toolId: text("tool_id").notNull().references(() => tools.id, { onDelete: "cascade" }),
250
+ projectId: text("project_id").references(() => projects.id, {
251
+ onDelete: "set null"
252
+ }),
253
+ startedAt: integer("started_at", { mode: "timestamp_ms" }),
254
+ endedAt: integer("ended_at", { mode: "timestamp_ms" }),
255
+ title: text("title"),
256
+ sourceFile: text("source_file").notNull(),
257
+ rawMetadata: text("raw_metadata", { mode: "json" }).$type()
258
+ },
259
+ (table) => ({
260
+ sourceIdx: uniqueIndex("sessions_source_id_idx").on(table.sourceId),
261
+ toolIdx: index("sessions_tool_idx").on(table.toolId),
262
+ projectIdx: index("sessions_project_idx").on(table.projectId),
263
+ startedIdx: index("sessions_started_idx").on(table.startedAt)
264
+ })
265
+ );
266
+ var interactions = sqliteTable(
267
+ "interactions",
268
+ {
269
+ id: text("id").primaryKey(),
270
+ sourceId: text("source_id").notNull(),
271
+ sessionId: text("session_id").notNull().references(() => sessions.id, { onDelete: "cascade" }),
272
+ timestamp: integer("timestamp", { mode: "timestamp_ms" }),
273
+ role: text("role").notNull(),
274
+ modelId: text("model_id").references(() => models.id, {
275
+ onDelete: "set null"
276
+ }),
277
+ inputTokens: integer("input_tokens").notNull().default(0),
278
+ outputTokens: integer("output_tokens").notNull().default(0),
279
+ cacheReadTokens: integer("cache_read_tokens").notNull().default(0),
280
+ cacheWriteTokens: integer("cache_write_tokens").notNull().default(0),
281
+ reasoningTokens: integer("reasoning_tokens").notNull().default(0),
282
+ totalTokens: integer("total_tokens").notNull().default(0),
283
+ estimatedTokens: integer("estimated_tokens", { mode: "boolean" }).notNull().default(false),
284
+ tokenConfidence: text("token_confidence").notNull().default("unknown"),
285
+ cost: real("cost"),
286
+ costEstimated: integer("cost_estimated", { mode: "boolean" }).notNull().default(false),
287
+ latencyMs: integer("latency_ms"),
288
+ rawTextPreview: text("raw_text_preview"),
289
+ rawText: text("raw_text"),
290
+ rawMetadata: text("raw_metadata", { mode: "json" }).$type()
291
+ },
292
+ (table) => ({
293
+ sourceIdx: uniqueIndex("interactions_source_id_idx").on(table.sourceId),
294
+ sessionIdx: index("interactions_session_idx").on(table.sessionId),
295
+ modelIdx: index("interactions_model_idx").on(table.modelId),
296
+ timestampIdx: index("interactions_timestamp_idx").on(table.timestamp)
297
+ })
298
+ );
299
+ var toolCalls = sqliteTable(
300
+ "tool_calls",
301
+ {
302
+ id: text("id").primaryKey(),
303
+ interactionId: text("interaction_id").notNull().references(() => interactions.id, { onDelete: "cascade" }),
304
+ name: text("name").notNull(),
305
+ status: text("status"),
306
+ durationMs: integer("duration_ms"),
307
+ rawMetadata: text("raw_metadata", { mode: "json" }).$type()
308
+ },
309
+ (table) => ({
310
+ interactionIdx: index("tool_calls_interaction_idx").on(table.interactionId)
311
+ })
312
+ );
313
+ var scanRuns = sqliteTable("scan_runs", {
314
+ id: text("id").primaryKey(),
315
+ startedAt: integer("started_at", { mode: "timestamp_ms" }).notNull(),
316
+ completedAt: integer("completed_at", { mode: "timestamp_ms" }),
317
+ filesScanned: integer("files_scanned").notNull().default(0),
318
+ recordsImported: integer("records_imported").notNull().default(0),
319
+ warnings: text("warnings", { mode: "json" }).$type().notNull().default(sql`'[]'`),
320
+ errors: text("errors", { mode: "json" }).$type().notNull().default(sql`'[]'`)
321
+ });
322
+ var scanFiles = sqliteTable(
323
+ "scan_files",
324
+ {
325
+ id: text("id").primaryKey(),
326
+ scanRunId: text("scan_run_id").notNull().references(() => scanRuns.id, { onDelete: "cascade" }),
327
+ path: text("path").notNull(),
328
+ modifiedTime: integer("modified_time", { mode: "timestamp_ms" }),
329
+ sizeBytes: integer("size_bytes").notNull().default(0),
330
+ fileHash: text("file_hash"),
331
+ parser: text("parser"),
332
+ status: text("status").notNull(),
333
+ recordsImported: integer("records_imported").notNull().default(0),
334
+ warnings: text("warnings", { mode: "json" }).$type().notNull().default(sql`'[]'`),
335
+ errors: text("errors", { mode: "json" }).$type().notNull().default(sql`'[]'`),
336
+ rawMetadata: text("raw_metadata", { mode: "json" }).$type()
337
+ },
338
+ (table) => ({
339
+ pathHashIdx: index("scan_files_path_hash_idx").on(table.path, table.fileHash),
340
+ scanRunIdx: index("scan_files_run_idx").on(table.scanRunId)
341
+ })
342
+ );
343
+ var settings = sqliteTable("settings", {
344
+ key: text("key").primaryKey(),
345
+ value: text("value", { mode: "json" }).$type().notNull(),
346
+ updatedAt: integer("updated_at", { mode: "timestamp_ms" }).notNull().$defaultFn(() => /* @__PURE__ */ new Date())
347
+ });
348
+ var providerRelations = relations(providers, ({ many }) => ({
349
+ tools: many(tools),
350
+ models: many(models)
351
+ }));
352
+ var toolRelations = relations(tools, ({ one, many }) => ({
353
+ provider: one(providers, {
354
+ fields: [tools.providerId],
355
+ references: [providers.id]
356
+ }),
357
+ sessions: many(sessions)
358
+ }));
359
+ var modelRelations = relations(models, ({ one, many }) => ({
360
+ provider: one(providers, {
361
+ fields: [models.providerId],
362
+ references: [providers.id]
363
+ }),
364
+ interactions: many(interactions)
365
+ }));
366
+ var projectRelations = relations(projects, ({ many }) => ({
367
+ sessions: many(sessions)
368
+ }));
369
+ var sessionRelations = relations(sessions, ({ one, many }) => ({
370
+ tool: one(tools, {
371
+ fields: [sessions.toolId],
372
+ references: [tools.id]
373
+ }),
374
+ project: one(projects, {
375
+ fields: [sessions.projectId],
376
+ references: [projects.id]
377
+ }),
378
+ interactions: many(interactions)
379
+ }));
380
+ var interactionRelations = relations(interactions, ({ one, many }) => ({
381
+ session: one(sessions, {
382
+ fields: [interactions.sessionId],
383
+ references: [sessions.id]
384
+ }),
385
+ model: one(models, {
386
+ fields: [interactions.modelId],
387
+ references: [models.id]
388
+ }),
389
+ toolCalls: many(toolCalls)
390
+ }));
391
+
392
+ // src/db/client.ts
393
+ var defaultDbPath = path.join(process.cwd(), ".tokentrace", "tokentrace.db");
394
+ function databaseUrlPath(value) {
395
+ if (!value?.startsWith("file:")) return null;
396
+ try {
397
+ return fileURLToPath(value);
398
+ } catch {
399
+ return value.slice("file:".length);
400
+ }
401
+ }
402
+ var dbPath = process.env.TOKENTRACE_DB ?? databaseUrlPath(process.env.DATABASE_URL) ?? defaultDbPath;
403
+ fs.mkdirSync(path.dirname(dbPath), { recursive: true });
404
+ var sqlite = new Database(dbPath);
405
+ sqlite.pragma("foreign_keys = ON");
406
+ applyMigrations(sqlite);
407
+ var db = drizzle(sqlite, { schema: schema_exports });
408
+
409
+ // src/db/settings.ts
410
+ import { eq } from "drizzle-orm";
411
+ var defaultSettings = {
412
+ customFolders: [],
413
+ storeRawMessageContent: false
414
+ };
415
+ function normalizeSettings(value) {
416
+ if (!value || typeof value !== "object") return defaultSettings;
417
+ const candidate = value;
418
+ return {
419
+ customFolders: Array.isArray(candidate.customFolders) ? candidate.customFolders.filter((item) => typeof item === "string") : [],
420
+ storeRawMessageContent: Boolean(candidate.storeRawMessageContent)
421
+ };
422
+ }
423
+ function getAppSettings() {
424
+ const row = db.select().from(settings).where(eq(settings.key, "app")).get();
425
+ return normalizeSettings(row?.value);
426
+ }
427
+
428
+ // src/lib/ids.ts
429
+ import crypto from "node:crypto";
430
+ function stableId(prefix, parts) {
431
+ const hash = crypto.createHash("sha1").update(parts.map((part) => String(part ?? "")).join("")).digest("hex").slice(0, 24);
432
+ return `${prefix}_${hash}`;
433
+ }
434
+ function hashContent(content) {
435
+ return crypto.createHash("sha256").update(content).digest("hex");
436
+ }
437
+
438
+ // src/ingestion/adapters/claude-code.ts
439
+ import path3 from "node:path";
440
+
441
+ // src/ingestion/adapters/helpers.ts
442
+ import fs2 from "node:fs/promises";
443
+ import path2 from "node:path";
444
+
445
+ // src/lib/token-estimator.ts
446
+ function estimateTokensFromText(text2) {
447
+ const normalized = (text2 ?? "").trim();
448
+ if (!normalized) {
449
+ return { tokens: 0, method: "chars-div-4" };
450
+ }
451
+ return {
452
+ tokens: Math.max(1, Math.ceil(normalized.length / 4)),
453
+ method: "chars-div-4"
454
+ };
455
+ }
456
+ function previewText(text2, maxLength = 240) {
457
+ const normalized = (text2 ?? "").replace(/\s+/g, " ").trim();
458
+ if (normalized.length <= maxLength) return normalized;
459
+ return `${normalized.slice(0, maxLength - 3)}...`;
460
+ }
461
+
462
+ // src/ingestion/adapters/helpers.ts
463
+ async function readTextSample(filePath, bytes = 64e3) {
464
+ const handle = await fs2.open(filePath, "r");
465
+ try {
466
+ const buffer = Buffer.alloc(bytes);
467
+ const { bytesRead } = await handle.read(buffer, 0, bytes, 0);
468
+ return buffer.subarray(0, bytesRead).toString("utf8");
469
+ } finally {
470
+ await handle.close();
471
+ }
472
+ }
473
+ async function readFileText(filePath, maxBytes = 25 * 1024 * 1024) {
474
+ const stat = await fs2.stat(filePath);
475
+ if (stat.size > maxBytes) {
476
+ throw new Error(`File is larger than ${Math.round(maxBytes / 1024 / 1024)} MB.`);
477
+ }
478
+ return fs2.readFile(filePath, "utf8");
479
+ }
480
+ function safeJsonParse(value) {
481
+ try {
482
+ return JSON.parse(value);
483
+ } catch {
484
+ return null;
485
+ }
486
+ }
487
+ function asObject(value) {
488
+ return value && typeof value === "object" && !Array.isArray(value) ? value : null;
489
+ }
490
+ function asArray(value) {
491
+ return Array.isArray(value) ? value : [];
492
+ }
493
+ function firstString(...values) {
494
+ for (const value of values) {
495
+ if (typeof value === "string" && value.trim()) return value.trim();
496
+ }
497
+ return null;
498
+ }
499
+ function firstNumber(...values) {
500
+ for (const value of values) {
501
+ if (typeof value === "number" && Number.isFinite(value)) return Math.max(0, Math.round(value));
502
+ if (typeof value === "string" && value.trim() && Number.isFinite(Number(value))) {
503
+ return Math.max(0, Math.round(Number(value)));
504
+ }
505
+ }
506
+ return null;
507
+ }
508
+ function parseTimestamp(...values) {
509
+ for (const value of values) {
510
+ if (value instanceof Date && !Number.isNaN(value.getTime())) return value;
511
+ if (typeof value === "number" && Number.isFinite(value)) {
512
+ const ms = value > 1e10 ? value : value * 1e3;
513
+ const date = new Date(ms);
514
+ if (!Number.isNaN(date.getTime())) return date;
515
+ }
516
+ if (typeof value === "string" && value.trim()) {
517
+ const date = new Date(value);
518
+ if (!Number.isNaN(date.getTime())) return date;
519
+ }
520
+ }
521
+ return null;
522
+ }
523
+ function textFromContent(value) {
524
+ if (typeof value === "string") return value;
525
+ if (Array.isArray(value)) {
526
+ return value.map((item) => {
527
+ if (typeof item === "string") return item;
528
+ const object2 = asObject(item);
529
+ return firstString(object2?.text, object2?.content, object2?.input, object2?.output);
530
+ }).filter(Boolean).join("\n");
531
+ }
532
+ const object = asObject(value);
533
+ if (!object) return null;
534
+ return firstString(object.text, object.content, object.value, object.input, object.output);
535
+ }
536
+ function extractText(record) {
537
+ const message = asObject(record.message);
538
+ const payload = asObject(record.payload);
539
+ const response = asObject(record.response);
540
+ return textFromContent(record.text) ?? textFromContent(record.content) ?? textFromContent(record.prompt) ?? textFromContent(record.completion) ?? textFromContent(record.output) ?? textFromContent(message?.content) ?? textFromContent(payload?.content) ?? textFromContent(payload?.message) ?? textFromContent(response?.content) ?? null;
541
+ }
542
+ function extractUsage(record) {
543
+ const message = asObject(record.message);
544
+ const payload = asObject(record.payload);
545
+ const response = asObject(record.response) ?? asObject(payload?.response);
546
+ const usage = asObject(record.usage) ?? asObject(message?.usage) ?? asObject(payload?.usage) ?? asObject(response?.usage) ?? asObject(record.token_usage) ?? asObject(record.tokens);
547
+ const inputDetails = asObject(usage?.input_tokens_details) ?? asObject(usage?.prompt_tokens_details) ?? asObject(usage?.cache);
548
+ const outputDetails = asObject(usage?.output_tokens_details) ?? asObject(usage?.completion_tokens_details);
549
+ return {
550
+ inputTokens: firstNumber(
551
+ usage?.input_tokens,
552
+ usage?.prompt_tokens,
553
+ usage?.inputTokens,
554
+ usage?.promptTokens,
555
+ record.input_tokens,
556
+ record.prompt_tokens
557
+ ),
558
+ outputTokens: firstNumber(
559
+ usage?.output_tokens,
560
+ usage?.completion_tokens,
561
+ usage?.outputTokens,
562
+ usage?.completionTokens,
563
+ record.output_tokens,
564
+ record.completion_tokens
565
+ ),
566
+ cacheReadTokens: firstNumber(
567
+ usage?.cache_read_input_tokens,
568
+ usage?.cached_input_tokens,
569
+ inputDetails?.cached_tokens,
570
+ inputDetails?.cache_read_tokens,
571
+ record.cache_read_tokens
572
+ ),
573
+ cacheWriteTokens: firstNumber(
574
+ usage?.cache_creation_input_tokens,
575
+ usage?.cache_write_input_tokens,
576
+ inputDetails?.cache_creation_tokens,
577
+ inputDetails?.cache_write_tokens,
578
+ record.cache_write_tokens
579
+ ),
580
+ reasoningTokens: firstNumber(
581
+ usage?.reasoning_tokens,
582
+ outputDetails?.reasoning_tokens,
583
+ record.reasoning_tokens
584
+ ),
585
+ totalTokens: firstNumber(
586
+ usage?.total_tokens,
587
+ usage?.totalTokens,
588
+ usage?.tokens,
589
+ record.total_tokens,
590
+ record.totalTokens
591
+ )
592
+ };
593
+ }
594
+ function extractModel(record) {
595
+ const message = asObject(record.message);
596
+ const payload = asObject(record.payload);
597
+ const response = asObject(record.response) ?? asObject(payload?.response);
598
+ return firstString(
599
+ record.model,
600
+ record.model_name,
601
+ record.modelName,
602
+ message?.model,
603
+ payload?.model,
604
+ response?.model
605
+ );
606
+ }
607
+ function extractRole(record) {
608
+ const message = asObject(record.message);
609
+ const raw = firstString(record.role, record.type, message?.role);
610
+ if (!raw) return "unknown";
611
+ const normalized = raw.toLowerCase();
612
+ if (normalized.includes("assistant") || normalized.includes("completion")) return "assistant";
613
+ if (normalized.includes("user") || normalized.includes("prompt")) return "user";
614
+ if (normalized.includes("system")) return "system";
615
+ if (normalized.includes("tool")) return "tool";
616
+ return "unknown";
617
+ }
618
+ function extractToolCalls(record) {
619
+ const message = asObject(record.message);
620
+ const payload = asObject(record.payload);
621
+ const possible = [
622
+ ...asArray(record.tool_calls),
623
+ ...asArray(record.toolCalls),
624
+ ...asArray(message?.tool_calls),
625
+ ...asArray(payload?.tool_calls),
626
+ ...asArray(payload?.toolCalls)
627
+ ];
628
+ const calls = [];
629
+ possible.forEach((item, index2) => {
630
+ const object = asObject(item);
631
+ if (!object) return;
632
+ const name = firstString(object.name, object.tool, object.function_name, asObject(object.function)?.name);
633
+ if (!name) return;
634
+ calls.push({
635
+ externalId: firstString(object.id, object.call_id) ?? `${index2}`,
636
+ name,
637
+ status: firstString(object.status, object.state),
638
+ durationMs: firstNumber(object.duration_ms, object.durationMs),
639
+ rawMetadata: object
640
+ });
641
+ });
642
+ return calls;
643
+ }
644
+ var sensitiveMetadataKeys = /* @__PURE__ */ new Set([
645
+ "content",
646
+ "text",
647
+ "prompt",
648
+ "completion",
649
+ "input",
650
+ "output",
651
+ "arguments",
652
+ "message"
653
+ ]);
654
+ function sanitizeMetadata(value, depth = 0) {
655
+ if (value == null) return value;
656
+ if (typeof value === "string") return previewText(value, 160);
657
+ if (typeof value !== "object") return value;
658
+ if (depth > 3) return "[nested metadata]";
659
+ if (Array.isArray(value)) {
660
+ return value.slice(0, 5).map((item) => sanitizeMetadata(item, depth + 1));
661
+ }
662
+ const object = value;
663
+ return Object.fromEntries(
664
+ Object.entries(object).map(([key, item]) => {
665
+ if (sensitiveMetadataKeys.has(key.toLowerCase())) {
666
+ return [key, "[redacted: raw storage disabled]"];
667
+ }
668
+ return [key, sanitizeMetadata(item, depth + 1)];
669
+ })
670
+ );
671
+ }
672
+ function normalizeInteraction(record, externalId, storeRawMessageContent) {
673
+ const text2 = extractText(record);
674
+ const usage = extractUsage(record);
675
+ const hasUsage2 = Object.values(usage).some((value) => value != null && value > 0);
676
+ const message = asObject(record.message);
677
+ const payload = asObject(record.payload);
678
+ return {
679
+ externalId,
680
+ timestamp: parseTimestamp(record.timestamp, record.created_at, record.createdAt, record.time, record.ts),
681
+ role: extractRole(record),
682
+ modelName: extractModel(record),
683
+ inputTokens: usage.inputTokens,
684
+ outputTokens: usage.outputTokens,
685
+ cacheReadTokens: usage.cacheReadTokens,
686
+ cacheWriteTokens: usage.cacheWriteTokens,
687
+ reasoningTokens: usage.reasoningTokens,
688
+ totalTokens: usage.totalTokens,
689
+ estimatedTokens: false,
690
+ tokenConfidence: hasUsage2 ? "exact" : text2 ? "high-confidence estimate" : "unknown",
691
+ latencyMs: firstNumber(record.latency_ms, record.latencyMs, payload?.latency_ms, message?.latency_ms),
692
+ rawText: storeRawMessageContent ? text2 : null,
693
+ rawTextPreview: previewText(text2),
694
+ rawMetadata: storeRawMessageContent ? record : sanitizeMetadata(record),
695
+ toolCalls: extractToolCalls(record).map((toolCall) => ({
696
+ ...toolCall,
697
+ rawMetadata: storeRawMessageContent ? toolCall.rawMetadata : sanitizeMetadata(toolCall.rawMetadata)
698
+ }))
699
+ };
700
+ }
701
+ function sessionNameFromFile(filePath) {
702
+ return path2.basename(filePath).replace(/\.(jsonl|json|log|txt|md)$/i, "");
703
+ }
704
+ function fileLooksLikeJsonl(sample) {
705
+ const lines = sample.split(/\r?\n/).map((line) => line.trim()).filter(Boolean).slice(0, 5);
706
+ return lines.length > 0 && lines.every((line) => safeJsonParse(line) !== null);
707
+ }
708
+
709
+ // src/ingestion/adapters/generic-records.ts
710
+ function interactionExternalId(record, index2) {
711
+ const message = asObject(record.message);
712
+ const payload = asObject(record.payload);
713
+ const response = asObject(record.response) ?? asObject(payload?.response);
714
+ return firstString(
715
+ record.uuid,
716
+ record.id,
717
+ record.message_id,
718
+ record.messageId,
719
+ message?.id,
720
+ payload?.id,
721
+ response?.id
722
+ ) ?? `${index2}`;
723
+ }
724
+ function sessionExternalId(record, fallback) {
725
+ const message = asObject(record.message);
726
+ const payload = asObject(record.payload);
727
+ return firstString(
728
+ record.session_id,
729
+ record.sessionId,
730
+ record.conversation_id,
731
+ record.conversationId,
732
+ record.thread_id,
733
+ record.threadId,
734
+ message?.session_id,
735
+ message?.sessionId,
736
+ payload?.session_id,
737
+ payload?.sessionId,
738
+ payload?.conversation_id
739
+ ) ?? fallback;
740
+ }
741
+ function projectPathFromRecord(record) {
742
+ const payload = asObject(record.payload);
743
+ return firstString(
744
+ record.cwd,
745
+ record.project_path,
746
+ record.projectPath,
747
+ record.repository,
748
+ payload?.cwd,
749
+ payload?.project_path,
750
+ payload?.projectPath
751
+ );
752
+ }
753
+ function titleFromRecord(record) {
754
+ const payload = asObject(record.payload);
755
+ return firstString(record.title, record.summary, payload?.title, payload?.summary);
756
+ }
757
+ function hasUsage(usage) {
758
+ return Object.values(usage).some((value) => typeof value === "number" && value > 0);
759
+ }
760
+ function shouldKeepInteraction(record) {
761
+ const usage = extractUsage(record);
762
+ return Boolean(
763
+ extractText(record) || hasUsage(usage) || extractModel(record) || extractToolCalls(record).length || extractRole(record) !== "unknown"
764
+ );
765
+ }
766
+ function tuneRole(interaction) {
767
+ if (interaction.role !== "unknown") return interaction;
768
+ if ((interaction.outputTokens ?? 0) > 0 || (interaction.reasoningTokens ?? 0) > 0) {
769
+ return { ...interaction, role: "assistant" };
770
+ }
771
+ if ((interaction.inputTokens ?? 0) > 0) {
772
+ return { ...interaction, role: "user" };
773
+ }
774
+ return interaction;
775
+ }
776
+ function buildSessionsFromRecords(options) {
777
+ const fallbackSessionId = options.defaultSessionId ?? sessionNameFromFile(options.file.path);
778
+ const grouped = /* @__PURE__ */ new Map();
779
+ options.records.forEach((record, index2) => {
780
+ const sessionId = sessionExternalId(record, fallbackSessionId);
781
+ const group = grouped.get(sessionId) ?? {
782
+ interactions: [],
783
+ projectPath: options.defaultProjectPath ?? null,
784
+ title: null,
785
+ timestamps: [],
786
+ metadata: []
787
+ };
788
+ group.projectPath = group.projectPath ?? projectPathFromRecord(record);
789
+ group.title = group.title ?? titleFromRecord(record);
790
+ group.metadata.push(record);
791
+ const timestamp = parseTimestamp(
792
+ record.timestamp,
793
+ record.created_at,
794
+ record.createdAt,
795
+ record.time,
796
+ record.ts
797
+ );
798
+ if (timestamp) group.timestamps.push(timestamp);
799
+ if (shouldKeepInteraction(record)) {
800
+ const interaction = tuneRole(
801
+ normalizeInteraction(
802
+ record,
803
+ interactionExternalId(record, index2),
804
+ options.storeRawMessageContent
805
+ )
806
+ );
807
+ group.interactions.push(interaction);
808
+ }
809
+ grouped.set(sessionId, group);
810
+ });
811
+ return Array.from(grouped.entries()).filter(([, group]) => group.interactions.length > 0).map(([externalId, group]) => {
812
+ const sorted = [...group.timestamps].sort((a, b) => a.getTime() - b.getTime());
813
+ return {
814
+ externalId,
815
+ provider: options.provider,
816
+ tool: options.tool,
817
+ projectPath: group.projectPath,
818
+ projectName: group.projectPath ? void 0 : "Unknown project",
819
+ startedAt: sorted[0] ?? options.file.modifiedTime,
820
+ endedAt: sorted[sorted.length - 1] ?? options.file.modifiedTime,
821
+ title: group.title ?? sessionNameFromFile(options.file.path),
822
+ sourceFile: options.file.path,
823
+ rawMetadata: {
824
+ parserInputRecords: group.metadata.length
825
+ },
826
+ interactions: group.interactions
827
+ };
828
+ });
829
+ }
830
+
831
+ // src/ingestion/adapters/claude-code.ts
832
+ function projectPathFromClaudeProjectFile(filePath) {
833
+ const parts = filePath.split(path3.sep);
834
+ const projectsIndex = parts.lastIndexOf("projects");
835
+ if (projectsIndex === -1 || !parts[projectsIndex + 1]) return null;
836
+ const encoded = parts[projectsIndex + 1];
837
+ if (!encoded.startsWith("-")) return null;
838
+ return encoded.replace(/-/g, path3.sep);
839
+ }
840
+ var claudeCodeAdapter = {
841
+ id: "claude-code",
842
+ displayName: "Claude Code",
843
+ async detect(file) {
844
+ const normalized = file.path.toLowerCase();
845
+ const extension = path3.extname(file.path).toLowerCase();
846
+ if (normalized.includes(`${path3.sep}.claude${path3.sep}`) && [".jsonl", ".json"].includes(extension)) {
847
+ return { detected: true, confidence: 0.95, reason: "Path is inside a .claude directory" };
848
+ }
849
+ if (![".jsonl", ".json", ".log"].includes(extension)) {
850
+ return { detected: false, confidence: 0 };
851
+ }
852
+ const sample = await readTextSample(file.path);
853
+ if (/claude|anthropic|cache_creation_input_tokens|cache_read_input_tokens/i.test(sample)) {
854
+ return { detected: true, confidence: 0.72, reason: "Claude/Anthropic fields found" };
855
+ }
856
+ return { detected: false, confidence: 0 };
857
+ },
858
+ async parse(file, context) {
859
+ const warnings = [];
860
+ const records = [];
861
+ const text2 = await readFileText(file.path);
862
+ if (fileLooksLikeJsonl(text2)) {
863
+ text2.split(/\r?\n/).forEach((line, index2) => {
864
+ const trimmed = line.trim();
865
+ if (!trimmed) return;
866
+ const object = asObject(safeJsonParse(trimmed));
867
+ if (!object) {
868
+ warnings.push(`Line ${index2 + 1} is not a JSON object.`);
869
+ return;
870
+ }
871
+ const message = asObject(object.message);
872
+ records.push({
873
+ ...object,
874
+ model: firstString(object.model, message?.model),
875
+ role: object.type ?? message?.role ?? object.role
876
+ });
877
+ });
878
+ } else {
879
+ const object = asObject(safeJsonParse(text2));
880
+ if (object) records.push(object);
881
+ }
882
+ const projectPath = projectPathFromClaudeProjectFile(file.path);
883
+ return {
884
+ sessions: buildSessionsFromRecords({
885
+ file,
886
+ records,
887
+ provider: { id: "anthropic", name: "Anthropic", type: "llm-provider" },
888
+ tool: { id: "claude-code", name: "Claude Code" },
889
+ storeRawMessageContent: context.storeRawMessageContent,
890
+ defaultProjectPath: projectPath
891
+ }),
892
+ warnings,
893
+ errors: records.length ? [] : ["No Claude Code JSON records were parsed."]
894
+ };
895
+ }
896
+ };
897
+
898
+ // src/ingestion/adapters/codex-cli.ts
899
+ import path4 from "node:path";
900
+ function flattenCodexRecord(record) {
901
+ const payload = asObject(record.payload);
902
+ const response = asObject(payload?.response) ?? asObject(record.response);
903
+ const usage = asObject(response?.usage) ?? asObject(payload?.usage) ?? asObject(record.usage);
904
+ const message = asObject(payload?.message) ?? asObject(record.message);
905
+ const type = firstString(record.type, payload?.type, message?.type);
906
+ return {
907
+ ...record,
908
+ ...payload,
909
+ response,
910
+ usage,
911
+ model: firstString(record.model, payload?.model, response?.model),
912
+ role: firstString(record.role, payload?.role, message?.role) ?? (type?.includes("response") ? "assistant" : void 0),
913
+ content: record.content ?? payload?.content ?? message?.content,
914
+ cwd: record.cwd ?? payload?.cwd,
915
+ id: firstString(record.id, payload?.id, response?.id, message?.id),
916
+ timestamp: record.timestamp ?? payload?.timestamp
917
+ };
918
+ }
919
+ var codexCliAdapter = {
920
+ id: "codex-cli",
921
+ displayName: "Codex CLI",
922
+ async detect(file) {
923
+ const normalized = file.path.toLowerCase();
924
+ const extension = path4.extname(file.path).toLowerCase();
925
+ if (normalized.includes(`${path4.sep}.codex${path4.sep}`) && [".jsonl", ".json", ".log"].includes(extension)) {
926
+ return { detected: true, confidence: 0.95, reason: "Path is inside a .codex directory" };
927
+ }
928
+ if (![".jsonl", ".json", ".log"].includes(extension)) {
929
+ return { detected: false, confidence: 0 };
930
+ }
931
+ const sample = await readTextSample(file.path);
932
+ if (/codex|openai|response\.completed|response_completed|turn_context/i.test(sample)) {
933
+ return { detected: true, confidence: 0.72, reason: "Codex/OpenAI event fields found" };
934
+ }
935
+ return { detected: false, confidence: 0 };
936
+ },
937
+ async parse(file, context) {
938
+ const warnings = [];
939
+ const records = [];
940
+ const text2 = await readFileText(file.path);
941
+ if (fileLooksLikeJsonl(text2)) {
942
+ text2.split(/\r?\n/).forEach((line, index2) => {
943
+ const trimmed = line.trim();
944
+ if (!trimmed) return;
945
+ const object = asObject(safeJsonParse(trimmed));
946
+ if (!object) {
947
+ warnings.push(`Line ${index2 + 1} is not a JSON object.`);
948
+ return;
949
+ }
950
+ records.push(flattenCodexRecord(object));
951
+ });
952
+ } else {
953
+ const object = asObject(safeJsonParse(text2));
954
+ if (object) records.push(flattenCodexRecord(object));
955
+ }
956
+ return {
957
+ sessions: buildSessionsFromRecords({
958
+ file,
959
+ records,
960
+ provider: { id: "openai", name: "OpenAI", type: "llm-provider" },
961
+ tool: { id: "codex-cli", name: "Codex CLI" },
962
+ storeRawMessageContent: context.storeRawMessageContent
963
+ }),
964
+ warnings,
965
+ errors: records.length ? [] : ["No Codex CLI JSON records were parsed."]
966
+ };
967
+ }
968
+ };
969
+
970
+ // src/ingestion/adapters/generic-json.ts
971
+ import path5 from "node:path";
972
+ function collectRecords(value) {
973
+ const object = asObject(value);
974
+ if (Array.isArray(value)) {
975
+ return value.map(asObject).filter((item) => Boolean(item));
976
+ }
977
+ if (!object) return [];
978
+ const sessions2 = asArray(object.sessions);
979
+ if (sessions2.length) {
980
+ return sessions2.flatMap((session, sessionIndex) => {
981
+ const sessionObject = asObject(session);
982
+ if (!sessionObject) return [];
983
+ const messages = [
984
+ ...asArray(sessionObject.messages),
985
+ ...asArray(sessionObject.interactions),
986
+ ...asArray(sessionObject.events)
987
+ ];
988
+ if (!messages.length) return [sessionObject];
989
+ return messages.map(asObject).filter((item) => Boolean(item)).map((message, messageIndex) => ({
990
+ ...message,
991
+ session_id: sessionObject.session_id ?? sessionObject.sessionId ?? sessionObject.id ?? `session-${sessionIndex}`,
992
+ cwd: message.cwd ?? sessionObject.cwd ?? sessionObject.project_path,
993
+ title: message.title ?? sessionObject.title,
994
+ id: message.id ?? `${sessionIndex}-${messageIndex}`
995
+ }));
996
+ });
997
+ }
998
+ const records = [
999
+ ...asArray(object.messages),
1000
+ ...asArray(object.interactions),
1001
+ ...asArray(object.events),
1002
+ ...asArray(object.records)
1003
+ ];
1004
+ if (records.length) {
1005
+ return records.map(asObject).filter((item) => Boolean(item));
1006
+ }
1007
+ return [object];
1008
+ }
1009
+ var genericJsonAdapter = {
1010
+ id: "generic-json",
1011
+ displayName: "Generic JSON",
1012
+ async detect(file) {
1013
+ if (path5.extname(file.path).toLowerCase() !== ".json") {
1014
+ return { detected: false, confidence: 0 };
1015
+ }
1016
+ const sample = await readTextSample(file.path);
1017
+ const parsed = safeJsonParse(sample);
1018
+ return parsed ? { detected: true, confidence: 0.65, reason: "JSON extension and valid JSON sample" } : { detected: true, confidence: 0.35, reason: "JSON extension" };
1019
+ },
1020
+ async parse(file, context) {
1021
+ const warnings = [];
1022
+ const errors = [];
1023
+ const parsed = safeJsonParse(await readFileText(file.path));
1024
+ const records = collectRecords(parsed);
1025
+ if (!records.length) {
1026
+ errors.push("No usable JSON records were found.");
1027
+ }
1028
+ return {
1029
+ sessions: buildSessionsFromRecords({
1030
+ file,
1031
+ records,
1032
+ provider: { id: "generic", name: "Generic", type: "local-log" },
1033
+ tool: { id: "generic-json", name: "Generic JSON" },
1034
+ storeRawMessageContent: context.storeRawMessageContent
1035
+ }),
1036
+ warnings,
1037
+ errors
1038
+ };
1039
+ }
1040
+ };
1041
+
1042
+ // src/ingestion/adapters/generic-jsonl.ts
1043
+ import path6 from "node:path";
1044
+ var genericJsonlAdapter = {
1045
+ id: "generic-jsonl",
1046
+ displayName: "Generic JSONL",
1047
+ async detect(file) {
1048
+ const extension = path6.extname(file.path).toLowerCase();
1049
+ if (extension === ".jsonl" || file.path.endsWith(".jsonl.gz")) {
1050
+ return { detected: true, confidence: 0.75, reason: "JSONL extension" };
1051
+ }
1052
+ if (![".log", ".txt", ""].includes(extension)) {
1053
+ return { detected: false, confidence: 0 };
1054
+ }
1055
+ const sample = await readTextSample(file.path);
1056
+ if (fileLooksLikeJsonl(sample)) {
1057
+ return { detected: true, confidence: 0.55, reason: "First lines are JSON objects" };
1058
+ }
1059
+ return { detected: false, confidence: 0 };
1060
+ },
1061
+ async parse(file, context) {
1062
+ const warnings = [];
1063
+ const errors = [];
1064
+ const text2 = await readFileText(file.path);
1065
+ const records = [];
1066
+ text2.split(/\r?\n/).forEach((line, index2) => {
1067
+ const trimmed = line.trim();
1068
+ if (!trimmed) return;
1069
+ const parsed = safeJsonParse(trimmed);
1070
+ const object = asObject(parsed);
1071
+ if (!object) {
1072
+ warnings.push(`Line ${index2 + 1} is not a JSON object.`);
1073
+ return;
1074
+ }
1075
+ records.push(object);
1076
+ });
1077
+ if (!records.length) {
1078
+ errors.push("No JSON objects were parsed.");
1079
+ }
1080
+ return {
1081
+ sessions: buildSessionsFromRecords({
1082
+ file,
1083
+ records,
1084
+ provider: { id: "generic", name: "Generic", type: "local-log" },
1085
+ tool: { id: "generic-jsonl", name: "Generic JSONL" },
1086
+ storeRawMessageContent: context.storeRawMessageContent
1087
+ }),
1088
+ warnings,
1089
+ errors
1090
+ };
1091
+ }
1092
+ };
1093
+
1094
+ // src/ingestion/adapters/generic-log.ts
1095
+ import path7 from "node:path";
1096
+ function numberAfter(line, patterns) {
1097
+ for (const pattern of patterns) {
1098
+ const match = line.match(pattern);
1099
+ if (match?.[1]) return firstNumber(match[1]);
1100
+ }
1101
+ return null;
1102
+ }
1103
+ function textAfter(line, patterns) {
1104
+ for (const pattern of patterns) {
1105
+ const match = line.match(pattern);
1106
+ if (match?.[1]) return match[1].trim();
1107
+ }
1108
+ return null;
1109
+ }
1110
+ var genericLogAdapter = {
1111
+ id: "generic-log",
1112
+ displayName: "Generic Text Log",
1113
+ async detect(file) {
1114
+ const extension = path7.extname(file.path).toLowerCase();
1115
+ if (![".log", ".txt", ".md", ""].includes(extension)) {
1116
+ return { detected: false, confidence: 0 };
1117
+ }
1118
+ const sample = await readTextSample(file.path);
1119
+ if (/(tokens?|prompt_tokens|completion_tokens|model|session|cost|\$[0-9.]+)/i.test(sample)) {
1120
+ return { detected: true, confidence: 0.45, reason: "Token/model/cost-like text found" };
1121
+ }
1122
+ return { detected: false, confidence: 0 };
1123
+ },
1124
+ async parse(file, context) {
1125
+ const text2 = await readFileText(file.path);
1126
+ const lines = text2.split(/\r?\n/);
1127
+ const interactions2 = [];
1128
+ const warnings = [];
1129
+ let currentSession = sessionNameFromFile(file.path);
1130
+ let projectPath = null;
1131
+ lines.forEach((line, index2) => {
1132
+ const session = textAfter(line, [/session(?:_id)?\s*[:=]\s*([^\s,]+)/i]);
1133
+ if (session) currentSession = session;
1134
+ projectPath = projectPath ?? textAfter(line, [/(?:cwd|project|path)\s*[:=]\s*(.+)$/i]);
1135
+ const model = textAfter(line, [/model\s*[:=]\s*([A-Za-z0-9_.:/-]+)/i]);
1136
+ const inputTokens = numberAfter(line, [
1137
+ /(?:input_tokens|prompt_tokens|input tokens|prompt tokens)\s*[:=]\s*([0-9,]+)/i
1138
+ ]);
1139
+ const outputTokens = numberAfter(line, [
1140
+ /(?:output_tokens|completion_tokens|output tokens|completion tokens)\s*[:=]\s*([0-9,]+)/i
1141
+ ]);
1142
+ const totalTokens = numberAfter(line, [/(?:total_tokens|total tokens|tokens)\s*[:=]\s*([0-9,]+)/i]);
1143
+ const reasoningTokens = numberAfter(line, [/(?:reasoning_tokens|reasoning tokens)\s*[:=]\s*([0-9,]+)/i]);
1144
+ const hasStructuredTokens = inputTokens != null || outputTokens != null || totalTokens != null || reasoningTokens != null;
1145
+ if (!model && !hasStructuredTokens) return;
1146
+ const timestamp = parseTimestamp(textAfter(line, [/^(\d{4}-\d{2}-\d{2}T[^\s]+)/, /^(\d{4}-\d{2}-\d{2} [^\]]+)/])) ?? file.modifiedTime;
1147
+ const estimated = !hasStructuredTokens;
1148
+ const estimate = estimated ? estimateTokensFromText(line) : { tokens: 0 };
1149
+ const structuredTotal = totalTokens ?? (inputTokens ?? 0) + (outputTokens ?? 0) + (reasoningTokens ?? 0);
1150
+ interactions2.push({
1151
+ externalId: `${currentSession}-${index2}`,
1152
+ timestamp,
1153
+ role: outputTokens || reasoningTokens ? "assistant" : "unknown",
1154
+ modelName: model,
1155
+ inputTokens: inputTokens ?? (estimated ? estimate.tokens : 0),
1156
+ outputTokens: outputTokens ?? 0,
1157
+ reasoningTokens: reasoningTokens ?? 0,
1158
+ cacheReadTokens: 0,
1159
+ cacheWriteTokens: 0,
1160
+ totalTokens: structuredTotal || estimate.tokens,
1161
+ estimatedTokens: estimated,
1162
+ tokenConfidence: estimated ? "low-confidence estimate" : "high-confidence estimate",
1163
+ rawText: context.storeRawMessageContent ? line : null,
1164
+ rawTextPreview: previewText(line),
1165
+ rawMetadata: {
1166
+ source: "generic-log-line",
1167
+ line: index2 + 1
1168
+ },
1169
+ toolCalls: []
1170
+ });
1171
+ });
1172
+ if (!interactions2.length && text2.trim()) {
1173
+ const estimate = estimateTokensFromText(text2);
1174
+ warnings.push("No structured token lines found; created one estimated file-level interaction.");
1175
+ interactions2.push({
1176
+ externalId: `${currentSession}-estimated-file`,
1177
+ timestamp: file.modifiedTime,
1178
+ role: "unknown",
1179
+ modelName: "unknown",
1180
+ inputTokens: estimate.tokens,
1181
+ outputTokens: 0,
1182
+ reasoningTokens: 0,
1183
+ cacheReadTokens: 0,
1184
+ cacheWriteTokens: 0,
1185
+ totalTokens: estimate.tokens,
1186
+ estimatedTokens: true,
1187
+ tokenConfidence: "low-confidence estimate",
1188
+ rawText: context.storeRawMessageContent ? text2 : null,
1189
+ rawTextPreview: previewText(text2),
1190
+ rawMetadata: { source: "generic-log-file-estimate" },
1191
+ toolCalls: []
1192
+ });
1193
+ }
1194
+ return {
1195
+ sessions: interactions2.length ? [
1196
+ {
1197
+ externalId: currentSession,
1198
+ provider: { id: "generic", name: "Generic", type: "local-log" },
1199
+ tool: { id: "generic-log", name: "Generic Log" },
1200
+ projectPath,
1201
+ projectName: projectPath ? void 0 : "Unknown project",
1202
+ startedAt: interactions2[0]?.timestamp ?? file.modifiedTime,
1203
+ endedAt: interactions2[interactions2.length - 1]?.timestamp ?? file.modifiedTime,
1204
+ title: sessionNameFromFile(file.path),
1205
+ sourceFile: file.path,
1206
+ rawMetadata: { parser: "generic-log" },
1207
+ interactions: interactions2
1208
+ }
1209
+ ] : [],
1210
+ warnings,
1211
+ errors: interactions2.length ? [] : ["No text log records were inferred."]
1212
+ };
1213
+ }
1214
+ };
1215
+
1216
+ // src/ingestion/adapters/index.ts
1217
+ var adapters = [
1218
+ claudeCodeAdapter,
1219
+ codexCliAdapter,
1220
+ genericJsonlAdapter,
1221
+ genericJsonAdapter,
1222
+ genericLogAdapter
1223
+ ];
1224
+
1225
+ // src/ingestion/discovery.ts
1226
+ import fs3 from "node:fs/promises";
1227
+ import os from "node:os";
1228
+ import path8 from "node:path";
1229
+ var supportedExtensions = /* @__PURE__ */ new Set([
1230
+ ".jsonl",
1231
+ ".json",
1232
+ ".log",
1233
+ ".txt",
1234
+ ".md",
1235
+ ".db",
1236
+ ".sqlite",
1237
+ ".sqlite3"
1238
+ ]);
1239
+ var skippedDirectories = /* @__PURE__ */ new Set([
1240
+ "node_modules",
1241
+ ".git",
1242
+ ".next",
1243
+ "dist",
1244
+ "out",
1245
+ "build",
1246
+ "coverage",
1247
+ "Library",
1248
+ "Applications"
1249
+ ]);
1250
+ function expandHome(input) {
1251
+ if (input === "~") return os.homedir();
1252
+ if (input.startsWith("~/")) return path8.join(os.homedir(), input.slice(2));
1253
+ return input;
1254
+ }
1255
+ async function exists(target) {
1256
+ try {
1257
+ await fs3.access(target);
1258
+ return true;
1259
+ } catch {
1260
+ return false;
1261
+ }
1262
+ }
1263
+ async function getDefaultSearchRoots(customFolders = []) {
1264
+ const home = os.homedir();
1265
+ const workingDirectory = process.env.TOKENTRACE_WORKDIR ?? process.cwd();
1266
+ const appDataDir = process.env.TOKENTRACE_APP_DATA_DIR;
1267
+ const candidates = [
1268
+ path8.join(home, ".claude"),
1269
+ path8.join(home, ".config", "claude"),
1270
+ path8.join(home, ".codex"),
1271
+ path8.join(home, ".config", "codex"),
1272
+ path8.join(home, ".openai"),
1273
+ path8.join(workingDirectory, ".claude"),
1274
+ path8.join(workingDirectory, ".codex"),
1275
+ path8.join(workingDirectory, ".openai"),
1276
+ path8.join(workingDirectory, ".ai"),
1277
+ ...appDataDir ? [path8.join(appDataDir, "wrapper-runs")] : [],
1278
+ ...customFolders.map(expandHome)
1279
+ ];
1280
+ const unique = Array.from(new Set(candidates.map((candidate) => path8.resolve(candidate))));
1281
+ const present = [];
1282
+ for (const candidate of unique) {
1283
+ if (await exists(candidate)) present.push(candidate);
1284
+ }
1285
+ return present;
1286
+ }
1287
+ async function walkDirectory(root, depth, maxDepth, results) {
1288
+ if (depth > maxDepth) return;
1289
+ let entries;
1290
+ try {
1291
+ entries = await fs3.readdir(root, { withFileTypes: true });
1292
+ } catch {
1293
+ return;
1294
+ }
1295
+ for (const entry of entries) {
1296
+ const fullPath = path8.join(root, entry.name);
1297
+ if (entry.isDirectory()) {
1298
+ if (skippedDirectories.has(entry.name)) continue;
1299
+ await walkDirectory(fullPath, depth + 1, maxDepth, results);
1300
+ continue;
1301
+ }
1302
+ if (!entry.isFile()) continue;
1303
+ const extension = path8.extname(entry.name).toLowerCase();
1304
+ if (!supportedExtensions.has(extension)) continue;
1305
+ try {
1306
+ const stat = await fs3.stat(fullPath);
1307
+ if (stat.size <= 0 || stat.size > 25 * 1024 * 1024) continue;
1308
+ results.push({
1309
+ path: fullPath,
1310
+ modifiedTime: stat.mtime,
1311
+ sizeBytes: stat.size
1312
+ });
1313
+ } catch {
1314
+ continue;
1315
+ }
1316
+ }
1317
+ }
1318
+ async function discoverFiles(roots) {
1319
+ const results = [];
1320
+ for (const root of roots) {
1321
+ const stat = await fs3.stat(root).catch(() => null);
1322
+ if (!stat) continue;
1323
+ if (stat.isFile()) {
1324
+ const extension = path8.extname(root).toLowerCase();
1325
+ if (supportedExtensions.has(extension)) {
1326
+ results.push({
1327
+ path: root,
1328
+ modifiedTime: stat.mtime,
1329
+ sizeBytes: stat.size
1330
+ });
1331
+ }
1332
+ continue;
1333
+ }
1334
+ if (stat.isDirectory()) {
1335
+ await walkDirectory(root, 0, 12, results);
1336
+ }
1337
+ }
1338
+ const deduped = /* @__PURE__ */ new Map();
1339
+ for (const result2 of results) deduped.set(result2.path, result2);
1340
+ return Array.from(deduped.values()).sort((a, b) => a.path.localeCompare(b.path));
1341
+ }
1342
+
1343
+ // src/ingestion/persist.ts
1344
+ import fs4 from "node:fs";
1345
+ import path9 from "node:path";
1346
+
1347
+ // src/lib/cost.ts
1348
+ function pricePart(tokens, pricePerMillion) {
1349
+ if (!tokens || pricePerMillion == null) return 0;
1350
+ return tokens * pricePerMillion / 1e6;
1351
+ }
1352
+ function calculateInteractionCost(usage, price) {
1353
+ if (!price || price.inputTokenPrice == null || price.outputTokenPrice == null) {
1354
+ return {
1355
+ amount: null,
1356
+ currency: price?.currency ?? "USD",
1357
+ estimated: usage.estimatedTokens,
1358
+ status: "unknown",
1359
+ explanation: "No complete model pricing is configured."
1360
+ };
1361
+ }
1362
+ const input = pricePart(usage.inputTokens, price.inputTokenPrice);
1363
+ const output = pricePart(usage.outputTokens + usage.reasoningTokens, price.outputTokenPrice);
1364
+ const cacheRead = pricePart(
1365
+ usage.cacheReadTokens,
1366
+ price.cachedInputTokenPrice ?? price.inputTokenPrice
1367
+ );
1368
+ const cacheWrite = pricePart(usage.cacheWriteTokens, price.inputTokenPrice);
1369
+ const amount = input + output + cacheRead + cacheWrite;
1370
+ return {
1371
+ amount,
1372
+ currency: price.currency,
1373
+ estimated: usage.estimatedTokens,
1374
+ status: usage.estimatedTokens ? "estimated" : "exact",
1375
+ explanation: usage.estimatedTokens ? "Token counts were estimated before applying configured prices." : "Exact token counts were multiplied by configured prices."
1376
+ };
1377
+ }
1378
+
1379
+ // src/ingestion/persist.ts
1380
+ function json(value) {
1381
+ return JSON.stringify(value ?? null);
1382
+ }
1383
+ function insertIgnore(sql2, values) {
1384
+ return sqlite.prepare(sql2).run(...values).changes;
1385
+ }
1386
+ function upsertProvider(session) {
1387
+ insertIgnore(
1388
+ "INSERT OR IGNORE INTO providers (id, name, type) VALUES (?, ?, ?)",
1389
+ [session.provider.id, session.provider.name, session.provider.type]
1390
+ );
1391
+ }
1392
+ function upsertTool(session) {
1393
+ insertIgnore(
1394
+ "INSERT OR IGNORE INTO tools (id, provider_id, name) VALUES (?, ?, ?)",
1395
+ [session.tool.id, session.provider.id, session.tool.name]
1396
+ );
1397
+ }
1398
+ function providerForTool(toolId) {
1399
+ return sqlite.prepare("SELECT provider_id FROM tools WHERE id = ?").get(toolId);
1400
+ }
1401
+ function getModel(providerId, modelName) {
1402
+ return sqlite.prepare(
1403
+ `SELECT id, input_token_price, output_token_price, cached_input_token_price, currency
1404
+ FROM models WHERE provider_id = ? AND lower(name) = lower(?)`
1405
+ ).get(providerId, modelName);
1406
+ }
1407
+ function ensureModel(providerId, modelName) {
1408
+ const name = modelName?.trim() || "unknown";
1409
+ const existing = getModel(providerId, name);
1410
+ if (existing) return existing;
1411
+ const id = stableId("model", [providerId, name]);
1412
+ insertIgnore(
1413
+ `INSERT OR IGNORE INTO models
1414
+ (id, provider_id, name, input_token_price, output_token_price, cached_input_token_price, currency, raw_metadata)
1415
+ VALUES (?, ?, ?, NULL, NULL, NULL, 'USD', ?)`,
1416
+ [
1417
+ id,
1418
+ providerId,
1419
+ name,
1420
+ json({
1421
+ note: "Observed during import. Add prices on the Pricing page to enable cost calculation."
1422
+ })
1423
+ ]
1424
+ );
1425
+ return getModel(providerId, name) ?? {
1426
+ id,
1427
+ input_token_price: null,
1428
+ output_token_price: null,
1429
+ cached_input_token_price: null,
1430
+ currency: "USD"
1431
+ };
1432
+ }
1433
+ function findProjectRoot(startFile) {
1434
+ let current = fs4.statSync(startFile).isDirectory() ? startFile : path9.dirname(startFile);
1435
+ const home = process.env.HOME ?? path9.parse(current).root;
1436
+ while (current !== path9.dirname(current)) {
1437
+ if (fs4.existsSync(path9.join(current, ".git")) || fs4.existsSync(path9.join(current, "package.json")) || fs4.existsSync(path9.join(current, "pyproject.toml")) || fs4.existsSync(path9.join(current, "Cargo.toml"))) {
1438
+ return current;
1439
+ }
1440
+ if (current === home) break;
1441
+ current = path9.dirname(current);
1442
+ }
1443
+ return path9.dirname(startFile);
1444
+ }
1445
+ function ensureProject(session) {
1446
+ const projectPath = session.projectPath || findProjectRoot(session.sourceFile);
1447
+ const resolved = path9.resolve(projectPath);
1448
+ const name = session.projectName || path9.basename(resolved) || "Unknown project";
1449
+ const id = stableId("project", [resolved]);
1450
+ insertIgnore("INSERT OR IGNORE INTO projects (id, name, path) VALUES (?, ?, ?)", [
1451
+ id,
1452
+ name,
1453
+ resolved
1454
+ ]);
1455
+ return id;
1456
+ }
1457
+ function normalizeTokens(interaction) {
1458
+ const providedAnyToken = [
1459
+ interaction.inputTokens,
1460
+ interaction.outputTokens,
1461
+ interaction.cacheReadTokens,
1462
+ interaction.cacheWriteTokens,
1463
+ interaction.reasoningTokens,
1464
+ interaction.totalTokens
1465
+ ].some((value) => value != null && value > 0);
1466
+ const baseText = interaction.rawText || interaction.rawTextPreview || "";
1467
+ const estimate = estimateTokensFromText(baseText).tokens;
1468
+ const estimatedTokens = Boolean(interaction.estimatedTokens || !providedAnyToken);
1469
+ let tokenConfidence = interaction.tokenConfidence ?? "unknown";
1470
+ let inputTokens = interaction.inputTokens ?? 0;
1471
+ let outputTokens = interaction.outputTokens ?? 0;
1472
+ const cacheReadTokens = interaction.cacheReadTokens ?? 0;
1473
+ const cacheWriteTokens = interaction.cacheWriteTokens ?? 0;
1474
+ const reasoningTokens = interaction.reasoningTokens ?? 0;
1475
+ if (!providedAnyToken && estimate > 0) {
1476
+ if (interaction.role === "assistant") outputTokens = estimate;
1477
+ else inputTokens = estimate;
1478
+ if (tokenConfidence === "unknown") tokenConfidence = "low-confidence estimate";
1479
+ }
1480
+ const summed = inputTokens + outputTokens + cacheReadTokens + cacheWriteTokens + reasoningTokens;
1481
+ const totalTokens = Math.max(interaction.totalTokens ?? 0, summed);
1482
+ return {
1483
+ inputTokens,
1484
+ outputTokens,
1485
+ cacheReadTokens,
1486
+ cacheWriteTokens,
1487
+ reasoningTokens,
1488
+ totalTokens,
1489
+ estimatedTokens,
1490
+ tokenConfidence
1491
+ };
1492
+ }
1493
+ function importSessions(sessions2) {
1494
+ const warnings = [];
1495
+ let sessionsImported = 0;
1496
+ let interactionsImported = 0;
1497
+ let toolCallsImported = 0;
1498
+ const transaction = sqlite.transaction((records) => {
1499
+ for (const session of records) {
1500
+ upsertProvider(session);
1501
+ upsertTool(session);
1502
+ const providerId = providerForTool(session.tool.id)?.provider_id ?? session.provider.id;
1503
+ const projectId = ensureProject(session);
1504
+ const sessionSourceId = stableId("session-source", [
1505
+ session.tool.id,
1506
+ session.sourceFile,
1507
+ session.externalId ?? session.title
1508
+ ]);
1509
+ const sessionId = stableId("session", [sessionSourceId]);
1510
+ const insertedSession = insertIgnore(
1511
+ `INSERT OR IGNORE INTO sessions
1512
+ (id, source_id, tool_id, project_id, started_at, ended_at, title, source_file, raw_metadata)
1513
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
1514
+ [
1515
+ sessionId,
1516
+ sessionSourceId,
1517
+ session.tool.id,
1518
+ projectId,
1519
+ session.startedAt?.getTime() ?? null,
1520
+ session.endedAt?.getTime() ?? null,
1521
+ session.title ?? null,
1522
+ session.sourceFile,
1523
+ json(session.rawMetadata)
1524
+ ]
1525
+ );
1526
+ sessionsImported += insertedSession;
1527
+ for (const interaction of session.interactions) {
1528
+ const model = ensureModel(providerId, interaction.modelName);
1529
+ const tokens = normalizeTokens(interaction);
1530
+ const cost = calculateInteractionCost(tokens, {
1531
+ inputTokenPrice: model.input_token_price,
1532
+ outputTokenPrice: model.output_token_price,
1533
+ cachedInputTokenPrice: model.cached_input_token_price,
1534
+ currency: model.currency
1535
+ });
1536
+ const interactionSourceId = stableId("interaction-source", [
1537
+ sessionSourceId,
1538
+ interaction.externalId,
1539
+ interaction.timestamp?.getTime(),
1540
+ interaction.role,
1541
+ interaction.rawTextPreview
1542
+ ]);
1543
+ const interactionId = stableId("interaction", [interactionSourceId]);
1544
+ const insertedInteraction = insertIgnore(
1545
+ `INSERT OR IGNORE INTO interactions
1546
+ (id, source_id, session_id, timestamp, role, model_id, input_tokens, output_tokens,
1547
+ cache_read_tokens, cache_write_tokens, reasoning_tokens, total_tokens, estimated_tokens,
1548
+ token_confidence, cost, cost_estimated, latency_ms, raw_text_preview, raw_text, raw_metadata)
1549
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
1550
+ [
1551
+ interactionId,
1552
+ interactionSourceId,
1553
+ sessionId,
1554
+ interaction.timestamp?.getTime() ?? session.startedAt?.getTime() ?? null,
1555
+ interaction.role,
1556
+ model.id,
1557
+ tokens.inputTokens,
1558
+ tokens.outputTokens,
1559
+ tokens.cacheReadTokens,
1560
+ tokens.cacheWriteTokens,
1561
+ tokens.reasoningTokens,
1562
+ tokens.totalTokens,
1563
+ tokens.estimatedTokens ? 1 : 0,
1564
+ tokens.tokenConfidence,
1565
+ cost.amount,
1566
+ cost.status === "estimated" ? 1 : 0,
1567
+ interaction.latencyMs ?? null,
1568
+ previewText(interaction.rawTextPreview || interaction.rawText),
1569
+ interaction.rawText ?? null,
1570
+ json({
1571
+ ...interaction.rawMetadata ?? {},
1572
+ costStatus: cost.status,
1573
+ costExplanation: cost.explanation
1574
+ })
1575
+ ]
1576
+ );
1577
+ interactionsImported += insertedInteraction;
1578
+ for (const [index2, toolCall] of (interaction.toolCalls ?? []).entries()) {
1579
+ const toolCallId = stableId("toolcall", [
1580
+ interactionId,
1581
+ toolCall.externalId ?? index2,
1582
+ toolCall.name
1583
+ ]);
1584
+ toolCallsImported += insertIgnore(
1585
+ `INSERT OR IGNORE INTO tool_calls
1586
+ (id, interaction_id, name, status, duration_ms, raw_metadata)
1587
+ VALUES (?, ?, ?, ?, ?, ?)`,
1588
+ [
1589
+ toolCallId,
1590
+ interactionId,
1591
+ toolCall.name,
1592
+ toolCall.status ?? null,
1593
+ toolCall.durationMs ?? null,
1594
+ json(toolCall.rawMetadata)
1595
+ ]
1596
+ );
1597
+ }
1598
+ }
1599
+ }
1600
+ });
1601
+ transaction(sessions2);
1602
+ return {
1603
+ sessionsImported,
1604
+ interactionsImported,
1605
+ toolCallsImported,
1606
+ warnings
1607
+ };
1608
+ }
1609
+
1610
+ // src/ingestion/scan.ts
1611
+ function json2(value) {
1612
+ return JSON.stringify(value ?? null);
1613
+ }
1614
+ function hasImportedFile(file) {
1615
+ if (!file.hash) return false;
1616
+ const row = sqlite.prepare(
1617
+ "SELECT id FROM scan_files WHERE path = ? AND file_hash = ? AND status = 'imported' LIMIT 1"
1618
+ ).get(file.path, file.hash);
1619
+ return Boolean(row);
1620
+ }
1621
+ async function hashFile(file) {
1622
+ const content = await fs5.readFile(file.path);
1623
+ return {
1624
+ ...file,
1625
+ hash: hashContent(content)
1626
+ };
1627
+ }
1628
+ async function selectAdapter(file) {
1629
+ const matches = [];
1630
+ const warnings = [];
1631
+ for (const adapter of adapters) {
1632
+ try {
1633
+ const result2 = await adapter.detect(file);
1634
+ if (result2.detected) {
1635
+ matches.push({
1636
+ adapter,
1637
+ confidence: result2.confidence,
1638
+ reason: result2.reason
1639
+ });
1640
+ }
1641
+ } catch (error) {
1642
+ warnings.push(
1643
+ `${adapter.displayName} detection failed: ${error instanceof Error ? error.message : "Unknown error"}`
1644
+ );
1645
+ }
1646
+ }
1647
+ matches.sort((a, b) => b.confidence - a.confidence);
1648
+ return { selected: matches[0] ?? null, warnings };
1649
+ }
1650
+ function insertScanFile(args2) {
1651
+ const id = stableId("scanfile", [
1652
+ args2.scanRunId,
1653
+ args2.file.path,
1654
+ args2.file.hash,
1655
+ args2.status,
1656
+ args2.parser
1657
+ ]);
1658
+ sqlite.prepare(
1659
+ `INSERT INTO scan_files
1660
+ (id, scan_run_id, path, modified_time, size_bytes, file_hash, parser, status,
1661
+ records_imported, warnings, errors, raw_metadata)
1662
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
1663
+ ).run(
1664
+ id,
1665
+ args2.scanRunId,
1666
+ args2.file.path,
1667
+ args2.file.modifiedTime?.getTime() ?? null,
1668
+ args2.file.sizeBytes,
1669
+ args2.file.hash ?? null,
1670
+ args2.parser,
1671
+ args2.status,
1672
+ args2.recordsImported,
1673
+ json2(args2.warnings),
1674
+ json2(args2.errors),
1675
+ json2(args2.rawMetadata ?? {})
1676
+ );
1677
+ }
1678
+ async function runScan(options = {}) {
1679
+ const settings2 = getAppSettings();
1680
+ const explicitFolders = options.folders ?? [];
1681
+ const roots = options.includeDefaults === false ? explicitFolders.map((folder) => path10.resolve(expandHome(folder))) : await getDefaultSearchRoots([...settings2.customFolders, ...explicitFolders]);
1682
+ const candidates = await discoverFiles(roots);
1683
+ const startedAt = /* @__PURE__ */ new Date();
1684
+ const scanRunId = stableId("scan", [startedAt.getTime(), roots.join("|")]);
1685
+ const allWarnings = [];
1686
+ const allErrors = [];
1687
+ let recordsImported = 0;
1688
+ let filesScanned = 0;
1689
+ sqlite.prepare(
1690
+ "INSERT INTO scan_runs (id, started_at, warnings, errors) VALUES (?, ?, '[]', '[]')"
1691
+ ).run(scanRunId, startedAt.getTime());
1692
+ for (const candidate of candidates) {
1693
+ filesScanned += 1;
1694
+ let file = candidate;
1695
+ const warnings = [];
1696
+ const errors = [];
1697
+ try {
1698
+ file = await hashFile(candidate);
1699
+ if (!options.force && hasImportedFile(file)) {
1700
+ insertScanFile({
1701
+ scanRunId,
1702
+ file,
1703
+ parser: null,
1704
+ status: "skipped_duplicate",
1705
+ recordsImported: 0,
1706
+ warnings: ["File hash already imported. Use force rescan to parse again."],
1707
+ errors: []
1708
+ });
1709
+ continue;
1710
+ }
1711
+ const adapterChoice = await selectAdapter(file);
1712
+ warnings.push(...adapterChoice.warnings);
1713
+ if (!adapterChoice.selected) {
1714
+ insertScanFile({
1715
+ scanRunId,
1716
+ file,
1717
+ parser: null,
1718
+ status: "skipped_unknown",
1719
+ recordsImported: 0,
1720
+ warnings,
1721
+ errors: ["No parser detected a compatible format."]
1722
+ });
1723
+ continue;
1724
+ }
1725
+ const parseResult = await adapterChoice.selected.adapter.parse(file, {
1726
+ storeRawMessageContent: options.storeRawMessageContent ?? settings2.storeRawMessageContent
1727
+ });
1728
+ warnings.push(...parseResult.warnings);
1729
+ errors.push(...parseResult.errors);
1730
+ const importResult = importSessions(parseResult.sessions);
1731
+ const tokenConfidence = parseResult.sessions.flatMap((session) => session.interactions).reduce((summary2, interaction) => {
1732
+ const key = interaction.tokenConfidence ?? "unknown";
1733
+ summary2[key] = (summary2[key] ?? 0) + 1;
1734
+ return summary2;
1735
+ }, {});
1736
+ warnings.push(...importResult.warnings);
1737
+ recordsImported += importResult.interactionsImported;
1738
+ insertScanFile({
1739
+ scanRunId,
1740
+ file,
1741
+ parser: adapterChoice.selected.adapter.id,
1742
+ status: errors.length ? "imported_with_errors" : "imported",
1743
+ recordsImported: importResult.interactionsImported,
1744
+ warnings,
1745
+ errors,
1746
+ rawMetadata: {
1747
+ confidence: adapterChoice.selected.confidence,
1748
+ reason: adapterChoice.selected.reason,
1749
+ tokenConfidence,
1750
+ sessionsParsed: parseResult.sessions.length,
1751
+ sessionsImported: importResult.sessionsImported,
1752
+ toolCallsImported: importResult.toolCallsImported
1753
+ }
1754
+ });
1755
+ } catch (error) {
1756
+ const message = error instanceof Error ? error.message : "Unknown scan error";
1757
+ errors.push(message);
1758
+ insertScanFile({
1759
+ scanRunId,
1760
+ file,
1761
+ parser: null,
1762
+ status: "failed",
1763
+ recordsImported: 0,
1764
+ warnings,
1765
+ errors
1766
+ });
1767
+ }
1768
+ allWarnings.push(...warnings.map((warning) => `${candidate.path}: ${warning}`));
1769
+ allErrors.push(...errors.map((error) => `${candidate.path}: ${error}`));
1770
+ }
1771
+ sqlite.prepare(
1772
+ `UPDATE scan_runs
1773
+ SET completed_at = ?, files_scanned = ?, records_imported = ?, warnings = ?, errors = ?
1774
+ WHERE id = ?`
1775
+ ).run(
1776
+ Date.now(),
1777
+ filesScanned,
1778
+ recordsImported,
1779
+ json2(allWarnings),
1780
+ json2(allErrors),
1781
+ scanRunId
1782
+ );
1783
+ return {
1784
+ scanRunId,
1785
+ filesScanned,
1786
+ recordsImported,
1787
+ warnings: allWarnings,
1788
+ errors: allErrors
1789
+ };
1790
+ }
1791
+
1792
+ // scripts/scan.ts
1793
+ var args = process.argv.slice(2);
1794
+ var json3 = args.includes("--json");
1795
+ var force = args.includes("--force");
1796
+ var folders = args.filter((arg) => arg !== "--force" && arg !== "--json");
1797
+ var result = await runScan({
1798
+ force,
1799
+ folders,
1800
+ includeDefaults: folders.length === 0
1801
+ });
1802
+ var summary = {
1803
+ scanRunId: result.scanRunId,
1804
+ filesScanned: result.filesScanned,
1805
+ recordsImported: result.recordsImported,
1806
+ warnings: result.warnings.length,
1807
+ errors: result.errors.length
1808
+ };
1809
+ if (json3) {
1810
+ console.log(JSON.stringify(summary, null, 2));
1811
+ } else {
1812
+ console.log("TokenTrace scan complete");
1813
+ console.log(`Files scanned: ${summary.filesScanned}`);
1814
+ console.log(`Records imported: ${summary.recordsImported}`);
1815
+ console.log(`Warnings: ${summary.warnings}`);
1816
+ console.log(`Errors: ${summary.errors}`);
1817
+ }