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
package/src/db/seed.ts ADDED
@@ -0,0 +1,110 @@
1
+ import { db } from "./client";
2
+ import { models, providers, settings, tools } from "./schema";
3
+
4
+ const now = new Date("2026-01-01T00:00:00.000Z");
5
+
6
+ const seedProviders = [
7
+ { id: "openai", name: "OpenAI", type: "llm-provider" },
8
+ { id: "anthropic", name: "Anthropic", type: "llm-provider" },
9
+ { id: "generic", name: "Generic", type: "local-log" }
10
+ ];
11
+
12
+ const seedTools = [
13
+ { id: "codex-cli", providerId: "openai", name: "Codex CLI" },
14
+ { id: "claude-code", providerId: "anthropic", name: "Claude Code" },
15
+ { id: "generic-jsonl", providerId: "generic", name: "Generic JSONL" },
16
+ { id: "generic-json", providerId: "generic", name: "Generic JSON" },
17
+ { id: "generic-log", providerId: "generic", name: "Generic Log" }
18
+ ];
19
+
20
+ const seedModels = [
21
+ {
22
+ id: "openai-gpt-4-1",
23
+ providerId: "openai",
24
+ name: "gpt-4.1",
25
+ inputTokenPrice: 2,
26
+ outputTokenPrice: 8,
27
+ cachedInputTokenPrice: 0.5,
28
+ currency: "USD"
29
+ },
30
+ {
31
+ id: "openai-gpt-4-1-mini",
32
+ providerId: "openai",
33
+ name: "gpt-4.1-mini",
34
+ inputTokenPrice: 0.4,
35
+ outputTokenPrice: 1.6,
36
+ cachedInputTokenPrice: 0.1,
37
+ currency: "USD"
38
+ },
39
+ {
40
+ id: "openai-gpt-4o",
41
+ providerId: "openai",
42
+ name: "gpt-4o",
43
+ inputTokenPrice: 2.5,
44
+ outputTokenPrice: 10,
45
+ cachedInputTokenPrice: 1.25,
46
+ currency: "USD"
47
+ },
48
+ {
49
+ id: "anthropic-claude-sonnet-4",
50
+ providerId: "anthropic",
51
+ name: "claude-sonnet-4",
52
+ inputTokenPrice: 3,
53
+ outputTokenPrice: 15,
54
+ cachedInputTokenPrice: 0.3,
55
+ currency: "USD"
56
+ },
57
+ {
58
+ id: "anthropic-claude-haiku",
59
+ providerId: "anthropic",
60
+ name: "claude-haiku",
61
+ inputTokenPrice: 0.8,
62
+ outputTokenPrice: 4,
63
+ cachedInputTokenPrice: 0.08,
64
+ currency: "USD"
65
+ },
66
+ {
67
+ id: "generic-unknown",
68
+ providerId: "generic",
69
+ name: "unknown",
70
+ inputTokenPrice: null,
71
+ outputTokenPrice: null,
72
+ cachedInputTokenPrice: null,
73
+ currency: "USD"
74
+ }
75
+ ];
76
+
77
+ export function seedDatabase() {
78
+ for (const provider of seedProviders) {
79
+ db.insert(providers).values(provider).onConflictDoNothing().run();
80
+ }
81
+
82
+ for (const tool of seedTools) {
83
+ db.insert(tools).values(tool).onConflictDoNothing().run();
84
+ }
85
+
86
+ for (const model of seedModels) {
87
+ db.insert(models)
88
+ .values({
89
+ ...model,
90
+ effectiveFrom: now,
91
+ rawMetadata: {
92
+ note: "Editable placeholder price. Verify current provider pricing before financial use."
93
+ }
94
+ })
95
+ .onConflictDoNothing()
96
+ .run();
97
+ }
98
+
99
+ db.insert(settings)
100
+ .values({
101
+ key: "app",
102
+ value: {
103
+ customFolders: [],
104
+ storeRawMessageContent: false
105
+ },
106
+ updatedAt: new Date()
107
+ })
108
+ .onConflictDoNothing()
109
+ .run();
110
+ }
@@ -0,0 +1,47 @@
1
+ import { eq } from "drizzle-orm";
2
+ import { db } from "./client";
3
+ import { settings } from "./schema";
4
+
5
+ export type AppSettings = {
6
+ customFolders: string[];
7
+ storeRawMessageContent: boolean;
8
+ };
9
+
10
+ const defaultSettings: AppSettings = {
11
+ customFolders: [],
12
+ storeRawMessageContent: false
13
+ };
14
+
15
+ function normalizeSettings(value: unknown): AppSettings {
16
+ if (!value || typeof value !== "object") return defaultSettings;
17
+ const candidate = value as Partial<AppSettings>;
18
+ return {
19
+ customFolders: Array.isArray(candidate.customFolders)
20
+ ? candidate.customFolders.filter((item): item is string => typeof item === "string")
21
+ : [],
22
+ storeRawMessageContent: Boolean(candidate.storeRawMessageContent)
23
+ };
24
+ }
25
+
26
+ export function getAppSettings(): AppSettings {
27
+ const row = db.select().from(settings).where(eq(settings.key, "app")).get();
28
+ return normalizeSettings(row?.value);
29
+ }
30
+
31
+ export function saveAppSettings(nextSettings: AppSettings) {
32
+ db.insert(settings)
33
+ .values({
34
+ key: "app",
35
+ value: nextSettings,
36
+ updatedAt: new Date()
37
+ })
38
+ .onConflictDoUpdate({
39
+ target: settings.key,
40
+ set: {
41
+ value: nextSettings,
42
+ updatedAt: new Date()
43
+ }
44
+ })
45
+ .run();
46
+ return nextSettings;
47
+ }
@@ -0,0 +1,78 @@
1
+ import path from "node:path";
2
+ import { IngestionAdapter } from "../types";
3
+ import { buildSessionsFromRecords } from "./generic-records";
4
+ import { asObject, fileLooksLikeJsonl, firstString, readFileText, readTextSample, safeJsonParse } from "./helpers";
5
+
6
+ function projectPathFromClaudeProjectFile(filePath: string) {
7
+ const parts = filePath.split(path.sep);
8
+ const projectsIndex = parts.lastIndexOf("projects");
9
+ if (projectsIndex === -1 || !parts[projectsIndex + 1]) return null;
10
+ const encoded = parts[projectsIndex + 1];
11
+ if (!encoded.startsWith("-")) return null;
12
+ return encoded.replace(/-/g, path.sep);
13
+ }
14
+
15
+ export const claudeCodeAdapter: IngestionAdapter = {
16
+ id: "claude-code",
17
+ displayName: "Claude Code",
18
+
19
+ async detect(file) {
20
+ const normalized = file.path.toLowerCase();
21
+ const extension = path.extname(file.path).toLowerCase();
22
+ if (normalized.includes(`${path.sep}.claude${path.sep}`) && [".jsonl", ".json"].includes(extension)) {
23
+ return { detected: true, confidence: 0.95, reason: "Path is inside a .claude directory" };
24
+ }
25
+
26
+ if (![".jsonl", ".json", ".log"].includes(extension)) {
27
+ return { detected: false, confidence: 0 };
28
+ }
29
+
30
+ const sample = await readTextSample(file.path);
31
+ if (/claude|anthropic|cache_creation_input_tokens|cache_read_input_tokens/i.test(sample)) {
32
+ return { detected: true, confidence: 0.72, reason: "Claude/Anthropic fields found" };
33
+ }
34
+ return { detected: false, confidence: 0 };
35
+ },
36
+
37
+ async parse(file, context) {
38
+ const warnings: string[] = [];
39
+ const records: Record<string, unknown>[] = [];
40
+ const text = await readFileText(file.path);
41
+
42
+ if (fileLooksLikeJsonl(text)) {
43
+ text.split(/\r?\n/).forEach((line, index) => {
44
+ const trimmed = line.trim();
45
+ if (!trimmed) return;
46
+ const object = asObject(safeJsonParse(trimmed));
47
+ if (!object) {
48
+ warnings.push(`Line ${index + 1} is not a JSON object.`);
49
+ return;
50
+ }
51
+ const message = asObject(object.message);
52
+ records.push({
53
+ ...object,
54
+ model: firstString(object.model, message?.model),
55
+ role: object.type ?? message?.role ?? object.role
56
+ });
57
+ });
58
+ } else {
59
+ const object = asObject(safeJsonParse(text));
60
+ if (object) records.push(object);
61
+ }
62
+
63
+ const projectPath = projectPathFromClaudeProjectFile(file.path);
64
+
65
+ return {
66
+ sessions: buildSessionsFromRecords({
67
+ file,
68
+ records,
69
+ provider: { id: "anthropic", name: "Anthropic", type: "llm-provider" },
70
+ tool: { id: "claude-code", name: "Claude Code" },
71
+ storeRawMessageContent: context.storeRawMessageContent,
72
+ defaultProjectPath: projectPath
73
+ }),
74
+ warnings,
75
+ errors: records.length ? [] : ["No Claude Code JSON records were parsed."]
76
+ };
77
+ }
78
+ };
@@ -0,0 +1,82 @@
1
+ import path from "node:path";
2
+ import { IngestionAdapter } from "../types";
3
+ import { buildSessionsFromRecords } from "./generic-records";
4
+ import { asObject, fileLooksLikeJsonl, firstString, readFileText, readTextSample, safeJsonParse } from "./helpers";
5
+
6
+ function flattenCodexRecord(record: Record<string, unknown>) {
7
+ const payload = asObject(record.payload);
8
+ const response = asObject(payload?.response) ?? asObject(record.response);
9
+ const usage = asObject(response?.usage) ?? asObject(payload?.usage) ?? asObject(record.usage);
10
+ const message = asObject(payload?.message) ?? asObject(record.message);
11
+ const type = firstString(record.type, payload?.type, message?.type);
12
+
13
+ return {
14
+ ...record,
15
+ ...payload,
16
+ response,
17
+ usage,
18
+ model: firstString(record.model, payload?.model, response?.model),
19
+ role: firstString(record.role, payload?.role, message?.role) ?? (type?.includes("response") ? "assistant" : undefined),
20
+ content: record.content ?? payload?.content ?? message?.content,
21
+ cwd: record.cwd ?? payload?.cwd,
22
+ id: firstString(record.id, payload?.id, response?.id, message?.id),
23
+ timestamp: record.timestamp ?? payload?.timestamp
24
+ };
25
+ }
26
+
27
+ export const codexCliAdapter: IngestionAdapter = {
28
+ id: "codex-cli",
29
+ displayName: "Codex CLI",
30
+
31
+ async detect(file) {
32
+ const normalized = file.path.toLowerCase();
33
+ const extension = path.extname(file.path).toLowerCase();
34
+ if (normalized.includes(`${path.sep}.codex${path.sep}`) && [".jsonl", ".json", ".log"].includes(extension)) {
35
+ return { detected: true, confidence: 0.95, reason: "Path is inside a .codex directory" };
36
+ }
37
+
38
+ if (![".jsonl", ".json", ".log"].includes(extension)) {
39
+ return { detected: false, confidence: 0 };
40
+ }
41
+
42
+ const sample = await readTextSample(file.path);
43
+ if (/codex|openai|response\.completed|response_completed|turn_context/i.test(sample)) {
44
+ return { detected: true, confidence: 0.72, reason: "Codex/OpenAI event fields found" };
45
+ }
46
+ return { detected: false, confidence: 0 };
47
+ },
48
+
49
+ async parse(file, context) {
50
+ const warnings: string[] = [];
51
+ const records: Record<string, unknown>[] = [];
52
+ const text = await readFileText(file.path);
53
+
54
+ if (fileLooksLikeJsonl(text)) {
55
+ text.split(/\r?\n/).forEach((line, index) => {
56
+ const trimmed = line.trim();
57
+ if (!trimmed) return;
58
+ const object = asObject(safeJsonParse(trimmed));
59
+ if (!object) {
60
+ warnings.push(`Line ${index + 1} is not a JSON object.`);
61
+ return;
62
+ }
63
+ records.push(flattenCodexRecord(object));
64
+ });
65
+ } else {
66
+ const object = asObject(safeJsonParse(text));
67
+ if (object) records.push(flattenCodexRecord(object));
68
+ }
69
+
70
+ return {
71
+ sessions: buildSessionsFromRecords({
72
+ file,
73
+ records,
74
+ provider: { id: "openai", name: "OpenAI", type: "llm-provider" },
75
+ tool: { id: "codex-cli", name: "Codex CLI" },
76
+ storeRawMessageContent: context.storeRawMessageContent
77
+ }),
78
+ warnings,
79
+ errors: records.length ? [] : ["No Codex CLI JSON records were parsed."]
80
+ };
81
+ }
82
+ };
@@ -0,0 +1,93 @@
1
+ import path from "node:path";
2
+ import { IngestionAdapter } from "../types";
3
+ import { buildSessionsFromRecords } from "./generic-records";
4
+ import { asArray, asObject, readFileText, readTextSample, safeJsonParse } from "./helpers";
5
+
6
+ function collectRecords(value: unknown): Record<string, unknown>[] {
7
+ const object = asObject(value);
8
+ if (Array.isArray(value)) {
9
+ return value.map(asObject).filter((item): item is Record<string, unknown> => Boolean(item));
10
+ }
11
+ if (!object) return [];
12
+
13
+ const sessions = asArray(object.sessions);
14
+ if (sessions.length) {
15
+ return sessions.flatMap((session, sessionIndex) => {
16
+ const sessionObject = asObject(session);
17
+ if (!sessionObject) return [];
18
+ const messages = [
19
+ ...asArray(sessionObject.messages),
20
+ ...asArray(sessionObject.interactions),
21
+ ...asArray(sessionObject.events)
22
+ ];
23
+ if (!messages.length) return [sessionObject];
24
+ return messages
25
+ .map(asObject)
26
+ .filter((item): item is Record<string, unknown> => Boolean(item))
27
+ .map((message, messageIndex) => ({
28
+ ...message,
29
+ session_id:
30
+ sessionObject.session_id ??
31
+ sessionObject.sessionId ??
32
+ sessionObject.id ??
33
+ `session-${sessionIndex}`,
34
+ cwd: message.cwd ?? sessionObject.cwd ?? sessionObject.project_path,
35
+ title: message.title ?? sessionObject.title,
36
+ id: message.id ?? `${sessionIndex}-${messageIndex}`
37
+ }));
38
+ });
39
+ }
40
+
41
+ const records = [
42
+ ...asArray(object.messages),
43
+ ...asArray(object.interactions),
44
+ ...asArray(object.events),
45
+ ...asArray(object.records)
46
+ ];
47
+
48
+ if (records.length) {
49
+ return records.map(asObject).filter((item): item is Record<string, unknown> => Boolean(item));
50
+ }
51
+
52
+ return [object];
53
+ }
54
+
55
+ export const genericJsonAdapter: IngestionAdapter = {
56
+ id: "generic-json",
57
+ displayName: "Generic JSON",
58
+
59
+ async detect(file) {
60
+ if (path.extname(file.path).toLowerCase() !== ".json") {
61
+ return { detected: false, confidence: 0 };
62
+ }
63
+
64
+ const sample = await readTextSample(file.path);
65
+ const parsed = safeJsonParse(sample);
66
+ return parsed
67
+ ? { detected: true, confidence: 0.65, reason: "JSON extension and valid JSON sample" }
68
+ : { detected: true, confidence: 0.35, reason: "JSON extension" };
69
+ },
70
+
71
+ async parse(file, context) {
72
+ const warnings: string[] = [];
73
+ const errors: string[] = [];
74
+ const parsed = safeJsonParse(await readFileText(file.path));
75
+ const records = collectRecords(parsed);
76
+
77
+ if (!records.length) {
78
+ errors.push("No usable JSON records were found.");
79
+ }
80
+
81
+ return {
82
+ sessions: buildSessionsFromRecords({
83
+ file,
84
+ records,
85
+ provider: { id: "generic", name: "Generic", type: "local-log" },
86
+ tool: { id: "generic-json", name: "Generic JSON" },
87
+ storeRawMessageContent: context.storeRawMessageContent
88
+ }),
89
+ warnings,
90
+ errors
91
+ };
92
+ }
93
+ };
@@ -0,0 +1,62 @@
1
+ import path from "node:path";
2
+ import { IngestionAdapter } from "../types";
3
+ import { buildSessionsFromRecords } from "./generic-records";
4
+ import { asObject, fileLooksLikeJsonl, readFileText, readTextSample, safeJsonParse } from "./helpers";
5
+
6
+ export const genericJsonlAdapter: IngestionAdapter = {
7
+ id: "generic-jsonl",
8
+ displayName: "Generic JSONL",
9
+
10
+ async detect(file) {
11
+ const extension = path.extname(file.path).toLowerCase();
12
+ if (extension === ".jsonl" || file.path.endsWith(".jsonl.gz")) {
13
+ return { detected: true, confidence: 0.75, reason: "JSONL extension" };
14
+ }
15
+
16
+ if (![".log", ".txt", ""].includes(extension)) {
17
+ return { detected: false, confidence: 0 };
18
+ }
19
+
20
+ const sample = await readTextSample(file.path);
21
+ if (fileLooksLikeJsonl(sample)) {
22
+ return { detected: true, confidence: 0.55, reason: "First lines are JSON objects" };
23
+ }
24
+
25
+ return { detected: false, confidence: 0 };
26
+ },
27
+
28
+ async parse(file, context) {
29
+ const warnings: string[] = [];
30
+ const errors: string[] = [];
31
+ const text = await readFileText(file.path);
32
+ const records: Record<string, unknown>[] = [];
33
+
34
+ text.split(/\r?\n/).forEach((line, index) => {
35
+ const trimmed = line.trim();
36
+ if (!trimmed) return;
37
+ const parsed = safeJsonParse(trimmed);
38
+ const object = asObject(parsed);
39
+ if (!object) {
40
+ warnings.push(`Line ${index + 1} is not a JSON object.`);
41
+ return;
42
+ }
43
+ records.push(object);
44
+ });
45
+
46
+ if (!records.length) {
47
+ errors.push("No JSON objects were parsed.");
48
+ }
49
+
50
+ return {
51
+ sessions: buildSessionsFromRecords({
52
+ file,
53
+ records,
54
+ provider: { id: "generic", name: "Generic", type: "local-log" },
55
+ tool: { id: "generic-jsonl", name: "Generic JSONL" },
56
+ storeRawMessageContent: context.storeRawMessageContent
57
+ }),
58
+ warnings,
59
+ errors
60
+ };
61
+ }
62
+ };
@@ -0,0 +1,144 @@
1
+ import path from "node:path";
2
+ import { estimateTokensFromText, previewText } from "@/src/lib/token-estimator";
3
+ import { IngestionAdapter, NormalizedInteraction } from "../types";
4
+ import { firstNumber, parseTimestamp, readFileText, readTextSample, sessionNameFromFile } from "./helpers";
5
+
6
+ function numberAfter(line: string, patterns: RegExp[]) {
7
+ for (const pattern of patterns) {
8
+ const match = line.match(pattern);
9
+ if (match?.[1]) return firstNumber(match[1]);
10
+ }
11
+ return null;
12
+ }
13
+
14
+ function textAfter(line: string, patterns: RegExp[]) {
15
+ for (const pattern of patterns) {
16
+ const match = line.match(pattern);
17
+ if (match?.[1]) return match[1].trim();
18
+ }
19
+ return null;
20
+ }
21
+
22
+ export const genericLogAdapter: IngestionAdapter = {
23
+ id: "generic-log",
24
+ displayName: "Generic Text Log",
25
+
26
+ async detect(file) {
27
+ const extension = path.extname(file.path).toLowerCase();
28
+ if (![".log", ".txt", ".md", ""].includes(extension)) {
29
+ return { detected: false, confidence: 0 };
30
+ }
31
+
32
+ const sample = await readTextSample(file.path);
33
+ if (/(tokens?|prompt_tokens|completion_tokens|model|session|cost|\$[0-9.]+)/i.test(sample)) {
34
+ return { detected: true, confidence: 0.45, reason: "Token/model/cost-like text found" };
35
+ }
36
+
37
+ return { detected: false, confidence: 0 };
38
+ },
39
+
40
+ async parse(file, context) {
41
+ const text = await readFileText(file.path);
42
+ const lines = text.split(/\r?\n/);
43
+ const interactions: NormalizedInteraction[] = [];
44
+ const warnings: string[] = [];
45
+ let currentSession = sessionNameFromFile(file.path);
46
+ let projectPath: string | null = null;
47
+
48
+ lines.forEach((line, index) => {
49
+ const session = textAfter(line, [/session(?:_id)?\s*[:=]\s*([^\s,]+)/i]);
50
+ if (session) currentSession = session;
51
+ projectPath =
52
+ projectPath ?? textAfter(line, [/(?:cwd|project|path)\s*[:=]\s*(.+)$/i]);
53
+
54
+ const model = textAfter(line, [/model\s*[:=]\s*([A-Za-z0-9_.:/-]+)/i]);
55
+ const inputTokens = numberAfter(line, [
56
+ /(?:input_tokens|prompt_tokens|input tokens|prompt tokens)\s*[:=]\s*([0-9,]+)/i
57
+ ]);
58
+ const outputTokens = numberAfter(line, [
59
+ /(?:output_tokens|completion_tokens|output tokens|completion tokens)\s*[:=]\s*([0-9,]+)/i
60
+ ]);
61
+ const totalTokens = numberAfter(line, [/(?:total_tokens|total tokens|tokens)\s*[:=]\s*([0-9,]+)/i]);
62
+ const reasoningTokens = numberAfter(line, [/(?:reasoning_tokens|reasoning tokens)\s*[:=]\s*([0-9,]+)/i]);
63
+ const hasStructuredTokens =
64
+ inputTokens != null || outputTokens != null || totalTokens != null || reasoningTokens != null;
65
+
66
+ if (!model && !hasStructuredTokens) return;
67
+
68
+ const timestamp =
69
+ parseTimestamp(textAfter(line, [/^(\d{4}-\d{2}-\d{2}T[^\s]+)/, /^(\d{4}-\d{2}-\d{2} [^\]]+)/])) ??
70
+ file.modifiedTime;
71
+
72
+ const estimated = !hasStructuredTokens;
73
+ const estimate = estimated ? estimateTokensFromText(line) : { tokens: 0 };
74
+ const structuredTotal =
75
+ totalTokens ?? (inputTokens ?? 0) + (outputTokens ?? 0) + (reasoningTokens ?? 0);
76
+ interactions.push({
77
+ externalId: `${currentSession}-${index}`,
78
+ timestamp,
79
+ role: outputTokens || reasoningTokens ? "assistant" : "unknown",
80
+ modelName: model,
81
+ inputTokens: inputTokens ?? (estimated ? estimate.tokens : 0),
82
+ outputTokens: outputTokens ?? 0,
83
+ reasoningTokens: reasoningTokens ?? 0,
84
+ cacheReadTokens: 0,
85
+ cacheWriteTokens: 0,
86
+ totalTokens: structuredTotal || estimate.tokens,
87
+ estimatedTokens: estimated,
88
+ tokenConfidence: estimated ? "low-confidence estimate" : "high-confidence estimate",
89
+ rawText: context.storeRawMessageContent ? line : null,
90
+ rawTextPreview: previewText(line),
91
+ rawMetadata: {
92
+ source: "generic-log-line",
93
+ line: index + 1
94
+ },
95
+ toolCalls: []
96
+ });
97
+ });
98
+
99
+ if (!interactions.length && text.trim()) {
100
+ const estimate = estimateTokensFromText(text);
101
+ warnings.push("No structured token lines found; created one estimated file-level interaction.");
102
+ interactions.push({
103
+ externalId: `${currentSession}-estimated-file`,
104
+ timestamp: file.modifiedTime,
105
+ role: "unknown",
106
+ modelName: "unknown",
107
+ inputTokens: estimate.tokens,
108
+ outputTokens: 0,
109
+ reasoningTokens: 0,
110
+ cacheReadTokens: 0,
111
+ cacheWriteTokens: 0,
112
+ totalTokens: estimate.tokens,
113
+ estimatedTokens: true,
114
+ tokenConfidence: "low-confidence estimate",
115
+ rawText: context.storeRawMessageContent ? text : null,
116
+ rawTextPreview: previewText(text),
117
+ rawMetadata: { source: "generic-log-file-estimate" },
118
+ toolCalls: []
119
+ });
120
+ }
121
+
122
+ return {
123
+ sessions: interactions.length
124
+ ? [
125
+ {
126
+ externalId: currentSession,
127
+ provider: { id: "generic", name: "Generic", type: "local-log" },
128
+ tool: { id: "generic-log", name: "Generic Log" },
129
+ projectPath,
130
+ projectName: projectPath ? undefined : "Unknown project",
131
+ startedAt: interactions[0]?.timestamp ?? file.modifiedTime,
132
+ endedAt: interactions[interactions.length - 1]?.timestamp ?? file.modifiedTime,
133
+ title: sessionNameFromFile(file.path),
134
+ sourceFile: file.path,
135
+ rawMetadata: { parser: "generic-log" },
136
+ interactions
137
+ }
138
+ ]
139
+ : [],
140
+ warnings,
141
+ errors: interactions.length ? [] : ["No text log records were inferred."]
142
+ };
143
+ }
144
+ };