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.
- package/.next/BUILD_ID +1 -0
- package/.next/app-build-manifest.json +167 -0
- package/.next/app-path-routes-manifest.json +22 -0
- package/.next/build-manifest.json +33 -0
- package/.next/export-marker.json +6 -0
- package/.next/images-manifest.json +58 -0
- package/.next/next-minimal-server.js.nft.json +1 -0
- package/.next/next-server.js.nft.json +1 -0
- package/.next/package.json +1 -0
- package/.next/prerender-manifest.json +37 -0
- package/.next/react-loadable-manifest.json +1 -0
- package/.next/required-server-files.json +323 -0
- package/.next/routes-manifest.json +119 -0
- package/.next/server/app/_not-found/page.js +2 -0
- package/.next/server/app/_not-found/page.js.nft.json +1 -0
- package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -0
- package/.next/server/app/_not-found.html +1 -0
- package/.next/server/app/_not-found.meta +8 -0
- package/.next/server/app/_not-found.rsc +37 -0
- package/.next/server/app/api/analytics/route.js +1 -0
- package/.next/server/app/api/analytics/route.js.nft.json +1 -0
- package/.next/server/app/api/analytics/route_client-reference-manifest.js +1 -0
- package/.next/server/app/api/data/route.js +151 -0
- package/.next/server/app/api/data/route.js.nft.json +1 -0
- package/.next/server/app/api/data/route_client-reference-manifest.js +1 -0
- package/.next/server/app/api/export/route.js +1 -0
- package/.next/server/app/api/export/route.js.nft.json +1 -0
- package/.next/server/app/api/export/route_client-reference-manifest.js +1 -0
- package/.next/server/app/api/files/route.js +1 -0
- package/.next/server/app/api/files/route.js.nft.json +1 -0
- package/.next/server/app/api/files/route_client-reference-manifest.js +1 -0
- package/.next/server/app/api/prices/route.js +151 -0
- package/.next/server/app/api/prices/route.js.nft.json +1 -0
- package/.next/server/app/api/prices/route_client-reference-manifest.js +1 -0
- package/.next/server/app/api/scan/route.js +144 -0
- package/.next/server/app/api/scan/route.js.nft.json +1 -0
- package/.next/server/app/api/scan/route_client-reference-manifest.js +1 -0
- package/.next/server/app/api/settings/route.js +128 -0
- package/.next/server/app/api/settings/route.js.nft.json +1 -0
- package/.next/server/app/api/settings/route_client-reference-manifest.js +1 -0
- package/.next/server/app/debug/page.js +2 -0
- package/.next/server/app/debug/page.js.nft.json +1 -0
- package/.next/server/app/debug/page_client-reference-manifest.js +1 -0
- package/.next/server/app/diagnostics/page.js +2 -0
- package/.next/server/app/diagnostics/page.js.nft.json +1 -0
- package/.next/server/app/diagnostics/page_client-reference-manifest.js +1 -0
- package/.next/server/app/discovery/page.js +2 -0
- package/.next/server/app/discovery/page.js.nft.json +1 -0
- package/.next/server/app/discovery/page_client-reference-manifest.js +1 -0
- package/.next/server/app/models/page.js +2 -0
- package/.next/server/app/models/page.js.nft.json +1 -0
- package/.next/server/app/models/page_client-reference-manifest.js +1 -0
- package/.next/server/app/optimisation/page.js +2 -0
- package/.next/server/app/optimisation/page.js.nft.json +1 -0
- package/.next/server/app/optimisation/page_client-reference-manifest.js +1 -0
- package/.next/server/app/page.js +2 -0
- package/.next/server/app/page.js.nft.json +1 -0
- package/.next/server/app/page_client-reference-manifest.js +1 -0
- package/.next/server/app/parser-debug/page.js +2 -0
- package/.next/server/app/parser-debug/page.js.nft.json +1 -0
- package/.next/server/app/parser-debug/page_client-reference-manifest.js +1 -0
- package/.next/server/app/pricing/page.js +152 -0
- package/.next/server/app/pricing/page.js.nft.json +1 -0
- package/.next/server/app/pricing/page_client-reference-manifest.js +1 -0
- package/.next/server/app/projects/page.js +2 -0
- package/.next/server/app/projects/page.js.nft.json +1 -0
- package/.next/server/app/projects/page_client-reference-manifest.js +1 -0
- package/.next/server/app/sessions/page.js +2 -0
- package/.next/server/app/sessions/page.js.nft.json +1 -0
- package/.next/server/app/sessions/page_client-reference-manifest.js +1 -0
- package/.next/server/app/settings/page.js +129 -0
- package/.next/server/app/settings/page.js.nft.json +1 -0
- package/.next/server/app/settings/page_client-reference-manifest.js +1 -0
- package/.next/server/app/tools/page.js +2 -0
- package/.next/server/app/tools/page.js.nft.json +1 -0
- package/.next/server/app/tools/page_client-reference-manifest.js +1 -0
- package/.next/server/app-paths-manifest.json +22 -0
- package/.next/server/chunks/123.js +9 -0
- package/.next/server/chunks/153.js +1 -0
- package/.next/server/chunks/237.js +13 -0
- package/.next/server/chunks/331.js +22 -0
- package/.next/server/chunks/366.js +1 -0
- package/.next/server/chunks/444.js +267 -0
- package/.next/server/chunks/611.js +6 -0
- package/.next/server/chunks/692.js +1 -0
- package/.next/server/chunks/779.js +1 -0
- package/.next/server/chunks/815.js +1 -0
- package/.next/server/chunks/868.js +1 -0
- package/.next/server/functions-config-manifest.json +4 -0
- package/.next/server/interception-route-rewrite-manifest.js +1 -0
- package/.next/server/middleware-build-manifest.js +1 -0
- package/.next/server/middleware-manifest.json +6 -0
- package/.next/server/middleware-react-loadable-manifest.js +1 -0
- package/.next/server/next-font-manifest.js +1 -0
- package/.next/server/next-font-manifest.json +1 -0
- package/.next/server/pages/404.html +1 -0
- package/.next/server/pages/500.html +1 -0
- package/.next/server/pages/_app.js +1 -0
- package/.next/server/pages/_app.js.nft.json +1 -0
- package/.next/server/pages/_document.js +1 -0
- package/.next/server/pages/_document.js.nft.json +1 -0
- package/.next/server/pages/_error.js +19 -0
- package/.next/server/pages/_error.js.nft.json +1 -0
- package/.next/server/pages-manifest.json +6 -0
- package/.next/server/server-reference-manifest.js +1 -0
- package/.next/server/server-reference-manifest.json +1 -0
- package/.next/server/webpack-runtime.js +1 -0
- package/.next/static/Fh8usqK3dgfncUx9s3VR1/_buildManifest.js +1 -0
- package/.next/static/Fh8usqK3dgfncUx9s3VR1/_ssgManifest.js +1 -0
- package/.next/static/chunks/125-ab0f8db8f84c1166.js +1 -0
- package/.next/static/chunks/255-e881f48ae1d2333a.js +1 -0
- package/.next/static/chunks/4bd1b696-409494caf8c83275.js +1 -0
- package/.next/static/chunks/619-f072ac750404f9da.js +1 -0
- package/.next/static/chunks/850-8bc31e41590b5831.js +1 -0
- package/.next/static/chunks/938-23236de1c47554ea.js +1 -0
- package/.next/static/chunks/app/_not-found/page-6d75243350d9e0b5.js +1 -0
- package/.next/static/chunks/app/api/analytics/route-33d3f29973de91a4.js +1 -0
- package/.next/static/chunks/app/api/data/route-33d3f29973de91a4.js +1 -0
- package/.next/static/chunks/app/api/export/route-33d3f29973de91a4.js +1 -0
- package/.next/static/chunks/app/api/files/route-33d3f29973de91a4.js +1 -0
- package/.next/static/chunks/app/api/prices/route-33d3f29973de91a4.js +1 -0
- package/.next/static/chunks/app/api/scan/route-33d3f29973de91a4.js +1 -0
- package/.next/static/chunks/app/api/settings/route-33d3f29973de91a4.js +1 -0
- package/.next/static/chunks/app/debug/page-33d3f29973de91a4.js +1 -0
- package/.next/static/chunks/app/diagnostics/page-053a5e810a59e548.js +1 -0
- package/.next/static/chunks/app/discovery/page-33d3f29973de91a4.js +1 -0
- package/.next/static/chunks/app/layout-8942804176ff26f3.js +1 -0
- package/.next/static/chunks/app/models/page-c0acf74dd8197e01.js +1 -0
- package/.next/static/chunks/app/optimisation/page-33d3f29973de91a4.js +1 -0
- package/.next/static/chunks/app/page-b6886ec802c03cbf.js +1 -0
- package/.next/static/chunks/app/parser-debug/page-33d3f29973de91a4.js +1 -0
- package/.next/static/chunks/app/pricing/page-5e27b1ae27314539.js +1 -0
- package/.next/static/chunks/app/projects/page-b6886ec802c03cbf.js +1 -0
- package/.next/static/chunks/app/sessions/page-0abcdc88aac9dcaf.js +1 -0
- package/.next/static/chunks/app/settings/page-59fc80673f0750cd.js +1 -0
- package/.next/static/chunks/app/tools/page-c0acf74dd8197e01.js +1 -0
- package/.next/static/chunks/framework-3457b9c2619cdd96.js +1 -0
- package/.next/static/chunks/main-8744520a8a31e6ae.js +1 -0
- package/.next/static/chunks/main-app-e9ccddef393e28c3.js +1 -0
- package/.next/static/chunks/pages/_app-5addca2b3b969fde.js +1 -0
- package/.next/static/chunks/pages/_error-022e4ac7bbb9914f.js +1 -0
- package/.next/static/chunks/polyfills-42372ed130431b0a.js +1 -0
- package/.next/static/chunks/webpack-3fcacae817f3ffab.js +1 -0
- package/.next/static/css/366bb38b386229a5.css +3 -0
- package/LICENSE +21 -0
- package/README.md +216 -0
- package/app/api/analytics/route.ts +8 -0
- package/app/api/data/route.ts +9 -0
- package/app/api/export/route.ts +26 -0
- package/app/api/files/route.ts +8 -0
- package/app/api/prices/route.ts +33 -0
- package/app/api/scan/route.ts +15 -0
- package/app/api/settings/route.ts +25 -0
- package/app/debug/page.tsx +101 -0
- package/app/diagnostics/page.tsx +113 -0
- package/app/discovery/page.tsx +61 -0
- package/app/globals.css +51 -0
- package/app/layout.tsx +30 -0
- package/app/models/page.tsx +97 -0
- package/app/optimisation/page.tsx +67 -0
- package/app/page.tsx +164 -0
- package/app/parser-debug/page.tsx +57 -0
- package/app/pricing/page.tsx +18 -0
- package/app/projects/page.tsx +111 -0
- package/app/sessions/page.tsx +24 -0
- package/app/settings/page.tsx +26 -0
- package/app/tools/page.tsx +92 -0
- package/bin/tokentrace.js +316 -0
- package/components/charts/rank-bar-chart.tsx +69 -0
- package/components/charts/trend-chart.tsx +123 -0
- package/components/empty-state.tsx +14 -0
- package/components/pricing-settings.tsx +171 -0
- package/components/session-explorer.tsx +210 -0
- package/components/settings-panel.tsx +203 -0
- package/components/sidebar.tsx +88 -0
- package/components/ui/badge.tsx +30 -0
- package/components/ui/button.tsx +47 -0
- package/components/ui/card.tsx +22 -0
- package/components/ui/input.tsx +19 -0
- package/components/ui/label.tsx +6 -0
- package/components/ui/table.tsx +31 -0
- package/components/ui/textarea.tsx +18 -0
- package/components.json +16 -0
- package/dist/runtime/db-migrate.mjs +410 -0
- package/dist/runtime/db-seed.mjs +506 -0
- package/dist/runtime/reset.mjs +519 -0
- package/dist/runtime/scan.mjs +1817 -0
- package/fixtures/generic-jsonl/sample.jsonl +2 -0
- package/next.config.mjs +7 -0
- package/package.json +96 -0
- package/postcss.config.mjs +8 -0
- package/scripts/build-cli-runtime.mjs +40 -0
- package/scripts/db-migrate.ts +5 -0
- package/scripts/db-seed.ts +5 -0
- package/scripts/reset.ts +5 -0
- package/scripts/scan.ts +30 -0
- package/src/db/client.ts +32 -0
- package/src/db/migrate-core.ts +147 -0
- package/src/db/reset.ts +14 -0
- package/src/db/schema.ts +259 -0
- package/src/db/seed.ts +110 -0
- package/src/db/settings.ts +47 -0
- package/src/ingestion/adapters/claude-code.ts +78 -0
- package/src/ingestion/adapters/codex-cli.ts +82 -0
- package/src/ingestion/adapters/generic-json.ts +93 -0
- package/src/ingestion/adapters/generic-jsonl.ts +62 -0
- package/src/ingestion/adapters/generic-log.ts +144 -0
- package/src/ingestion/adapters/generic-records.ts +178 -0
- package/src/ingestion/adapters/helpers.ts +309 -0
- package/src/ingestion/adapters/index.ts +15 -0
- package/src/ingestion/discovery.ts +130 -0
- package/src/ingestion/persist.ts +283 -0
- package/src/ingestion/scan.ts +247 -0
- package/src/ingestion/types.ts +78 -0
- package/src/lib/analytics.ts +592 -0
- package/src/lib/cost.ts +62 -0
- package/src/lib/csv.ts +15 -0
- package/src/lib/format.ts +51 -0
- package/src/lib/ids.ts +23 -0
- package/src/lib/pricing.ts +86 -0
- package/src/lib/token-estimator.ts +24 -0
- package/src/lib/utils.ts +6 -0
- package/tailwind.config.ts +53 -0
- 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
|
+
}
|