agent-profiler 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.
@@ -0,0 +1,253 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { getDefaultDbPath, getLastEventSummary, openDb, resolveUsableDbPath, } from "../core/db.js";
4
+ import { getPreferredConfigPath } from "../core/profile.js";
5
+ function hookEntryCommand(value) {
6
+ if (typeof value === "string")
7
+ return value;
8
+ if (!Array.isArray(value) || value.length === 0)
9
+ return "";
10
+ const first = value[0];
11
+ if (first && typeof first === "object" && typeof first.command === "string") {
12
+ return first.command;
13
+ }
14
+ return "";
15
+ }
16
+ function hookConfigured(hooks, eventName) {
17
+ return hookEntryCommand(hooks[eventName]).trim().length > 0;
18
+ }
19
+ const REQUIRED_CURSOR_EVENTS = [
20
+ "beforeSubmitPrompt",
21
+ "afterAgentResponse",
22
+ "afterShellExecution",
23
+ "afterFileEdit",
24
+ "stop",
25
+ "preToolUse",
26
+ "postToolUse",
27
+ "postToolUseFailure",
28
+ "beforeMCPExecution",
29
+ "afterMCPExecution",
30
+ ];
31
+ const REQUIRED_CODEX_EVENTS = [
32
+ "SessionStart",
33
+ "UserPromptSubmit",
34
+ "PreToolUse",
35
+ "PostToolUse",
36
+ "Stop",
37
+ ];
38
+ function codexEventConfigured(hooks, eventName, marker) {
39
+ const groups = hooks[eventName] ?? [];
40
+ for (const g of groups) {
41
+ for (const h of g.hooks ?? []) {
42
+ if (typeof h.command === "string" && h.command.includes(marker))
43
+ return true;
44
+ }
45
+ }
46
+ return false;
47
+ }
48
+ function readJsonFile(filePath) {
49
+ try {
50
+ return JSON.parse(fs.readFileSync(filePath, "utf8"));
51
+ }
52
+ catch {
53
+ return null;
54
+ }
55
+ }
56
+ function getProfilerConfigPath(resolvedDbPath) {
57
+ return path.join(path.dirname(resolvedDbPath), "config.json");
58
+ }
59
+ function resolveStatusPaths(mode) {
60
+ const preferredConfigPath = mode === "prod"
61
+ ? path.join(process.env.HOME ?? "", ".agent-profiler", "config.json")
62
+ : getPreferredConfigPath(process.cwd());
63
+ const config = readJsonFile(preferredConfigPath);
64
+ if (config?.databasePath) {
65
+ const resolvedFromConfig = resolveUsableDbPath(config.databasePath);
66
+ return {
67
+ configuredDbPath: config.databasePath,
68
+ resolvedDbPath: resolvedFromConfig,
69
+ configPath: preferredConfigPath,
70
+ };
71
+ }
72
+ const defaultDbPath = resolveUsableDbPath(getDefaultDbPath());
73
+ const defaultConfigPath = getProfilerConfigPath(defaultDbPath);
74
+ return {
75
+ configuredDbPath: getDefaultDbPath(),
76
+ resolvedDbPath: defaultDbPath,
77
+ configPath: defaultConfigPath,
78
+ };
79
+ }
80
+ function commandExistsInPath(command) {
81
+ const pathValue = process.env.PATH ?? "";
82
+ for (const dir of pathValue.split(path.delimiter)) {
83
+ if (!dir)
84
+ continue;
85
+ const fullPath = path.join(dir, command);
86
+ if (fs.existsSync(fullPath))
87
+ return true;
88
+ }
89
+ return false;
90
+ }
91
+ function getCursorSetupStatus(configPath, mode) {
92
+ const config = readJsonFile(configPath);
93
+ const cursor = config?.adapters?.cursor;
94
+ if (!cursor?.enabled || !cursor.hookFile) {
95
+ return { state: "not yet", note: "run `agent-profiler init cursor`" };
96
+ }
97
+ const hooks = readJsonFile(cursor.hookFile)?.hooks ?? {};
98
+ const missing = REQUIRED_CURSOR_EVENTS.filter((eventName) => !hookConfigured(hooks, eventName));
99
+ if (missing.length > 0) {
100
+ return {
101
+ state: "partial",
102
+ note: `missing hooks: ${missing.join(", ")}`,
103
+ };
104
+ }
105
+ const sampleCommand = hookEntryCommand(hooks.beforeSubmitPrompt);
106
+ if (mode === "dev") {
107
+ if (!sampleCommand.startsWith("node ")) {
108
+ return { state: "partial", note: "dev mode expects hooks to use `node <abs>/dist/cli.js ...`" };
109
+ }
110
+ const cliPath = sampleCommand.split(" ")[1];
111
+ if (!cliPath || !fs.existsSync(cliPath)) {
112
+ return { state: "partial", note: "dev hook CLI path is missing or invalid" };
113
+ }
114
+ }
115
+ else {
116
+ if (!sampleCommand.startsWith("agent-profiler ")) {
117
+ return { state: "partial", note: "prod mode expects hooks to use `agent-profiler ...`" };
118
+ }
119
+ if (!commandExistsInPath("agent-profiler")) {
120
+ return { state: "partial", note: "`agent-profiler` is not on PATH" };
121
+ }
122
+ }
123
+ return { state: "yes", note: "configured via init" };
124
+ }
125
+ function getCodexSetupStatus(configPath, mode) {
126
+ const config = readJsonFile(configPath);
127
+ const codex = config?.adapters?.codex;
128
+ if (!codex?.enabled || !codex.hookFile) {
129
+ return { state: "not yet", note: "run `agent-profiler init codex`" };
130
+ }
131
+ const data = readJsonFile(codex.hookFile);
132
+ const hooks = data?.hooks ?? {};
133
+ const missing = REQUIRED_CODEX_EVENTS.filter((eventName) => !codexEventConfigured(hooks, eventName, `hook codex ${eventName}`));
134
+ if (missing.length > 0) {
135
+ return {
136
+ state: "partial",
137
+ note: `missing hooks: ${missing.join(", ")}`,
138
+ };
139
+ }
140
+ const userPromptGroups = hooks.UserPromptSubmit ?? [];
141
+ let sampleCommand = "";
142
+ outer: for (const g of userPromptGroups) {
143
+ for (const h of g.hooks ?? []) {
144
+ if (typeof h.command === "string" && h.command.includes("hook codex")) {
145
+ sampleCommand = h.command;
146
+ break outer;
147
+ }
148
+ }
149
+ }
150
+ if (!sampleCommand) {
151
+ return { state: "partial", note: "could not find agent-profiler command in Codex hooks" };
152
+ }
153
+ if (mode === "dev") {
154
+ if (!sampleCommand.startsWith("node ")) {
155
+ return { state: "partial", note: "dev mode expects hooks to use `node <abs>/dist/cli.js ...`" };
156
+ }
157
+ const cliPath = sampleCommand.split(" ")[1];
158
+ if (!cliPath || !fs.existsSync(cliPath)) {
159
+ return { state: "partial", note: "dev hook CLI path is missing or invalid" };
160
+ }
161
+ }
162
+ else {
163
+ if (!sampleCommand.startsWith("agent-profiler ")) {
164
+ return { state: "partial", note: "prod mode expects hooks to use `agent-profiler ...`" };
165
+ }
166
+ if (!commandExistsInPath("agent-profiler")) {
167
+ return { state: "partial", note: "`agent-profiler` is not on PATH" };
168
+ }
169
+ }
170
+ return { state: "yes", note: "configured via init" };
171
+ }
172
+ function formatTimestamp(iso) {
173
+ const date = new Date(iso);
174
+ if (Number.isNaN(date.getTime()))
175
+ return iso;
176
+ return date.toLocaleString();
177
+ }
178
+ function formatCount(value) {
179
+ return new Intl.NumberFormat("en-US").format(value);
180
+ }
181
+ export function getStatusReport(mode = "dev") {
182
+ const { configuredDbPath, resolvedDbPath, configPath } = resolveStatusPaths(mode);
183
+ const cursorSetup = getCursorSetupStatus(configPath, mode);
184
+ const codexSetup = getCodexSetupStatus(configPath, mode);
185
+ let lastEvent = null;
186
+ try {
187
+ const db = openDb(configuredDbPath);
188
+ try {
189
+ lastEvent = getLastEventSummary(db);
190
+ }
191
+ finally {
192
+ db.close();
193
+ }
194
+ }
195
+ catch {
196
+ lastEvent = null;
197
+ }
198
+ return {
199
+ mode,
200
+ databasePath: resolvedDbPath,
201
+ adapters: {
202
+ cursor: {
203
+ state: cursorSetup.state,
204
+ setup: cursorSetup.note,
205
+ },
206
+ codex: {
207
+ state: codexSetup.state,
208
+ setup: codexSetup.note,
209
+ },
210
+ },
211
+ lastEvent: lastEvent
212
+ ? {
213
+ createdAt: lastEvent.createdAt,
214
+ source: lastEvent.source,
215
+ event: lastEvent.sourceEvent,
216
+ estimatedTokens: lastEvent.estimatedTotalTokens,
217
+ }
218
+ : null,
219
+ dashboard: "not running",
220
+ };
221
+ }
222
+ export function runStatus(mode = "dev") {
223
+ const report = getStatusReport(mode);
224
+ const lines = [];
225
+ lines.push("Agent Profiler Status");
226
+ lines.push("");
227
+ lines.push("Mode:");
228
+ lines.push(` ${report.mode}`);
229
+ lines.push("");
230
+ lines.push("Database:");
231
+ lines.push(` ${report.databasePath}`);
232
+ lines.push("");
233
+ lines.push("Configured adapters:");
234
+ lines.push(` Cursor: ${report.adapters.cursor.state}`);
235
+ lines.push(` setup: ${report.adapters.cursor.setup}`);
236
+ lines.push(` Codex: ${report.adapters.codex.state}`);
237
+ lines.push(` setup: ${report.adapters.codex.setup}`);
238
+ lines.push("");
239
+ lines.push("Last event:");
240
+ if (report.lastEvent) {
241
+ lines.push(` ${formatTimestamp(report.lastEvent.createdAt)}`);
242
+ lines.push(` source: ${report.lastEvent.source}`);
243
+ lines.push(` event: ${report.lastEvent.event}`);
244
+ lines.push(` estimated tokens: ${formatCount(report.lastEvent.estimatedTokens)}`);
245
+ }
246
+ else {
247
+ lines.push(" none yet");
248
+ }
249
+ lines.push("");
250
+ lines.push("Dashboard:");
251
+ lines.push(" not running");
252
+ console.log(lines.join("\n"));
253
+ }
@@ -0,0 +1,83 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { estimateTokens } from "./tokens.js";
4
+ const EXACT_FILES = [
5
+ "AGENTS.md",
6
+ "CLAUDE.md",
7
+ ".cursorrules",
8
+ ".codex/config.toml",
9
+ ".codex/hooks.json",
10
+ ".claude/settings.json",
11
+ ];
12
+ const DIRS = [
13
+ ".cursor/rules",
14
+ ".cursor/skills",
15
+ ".claude/commands",
16
+ ".claude/agents",
17
+ ".claude/skills",
18
+ ];
19
+ function readTextFile(filePath) {
20
+ try {
21
+ return fs.readFileSync(filePath, "utf8");
22
+ }
23
+ catch {
24
+ return "";
25
+ }
26
+ }
27
+ function walkFiles(dirPath) {
28
+ const out = [];
29
+ if (!fs.existsSync(dirPath))
30
+ return out;
31
+ const stack = [dirPath];
32
+ while (stack.length > 0) {
33
+ const current = stack.pop();
34
+ if (!current)
35
+ continue;
36
+ let entries = [];
37
+ try {
38
+ entries = fs.readdirSync(current, { withFileTypes: true });
39
+ }
40
+ catch {
41
+ continue;
42
+ }
43
+ for (const entry of entries) {
44
+ const fullPath = path.join(current, entry.name);
45
+ if (entry.isDirectory()) {
46
+ stack.push(fullPath);
47
+ }
48
+ else if (entry.isFile()) {
49
+ out.push(fullPath);
50
+ }
51
+ }
52
+ }
53
+ return out;
54
+ }
55
+ export function runContextAudit(rootDir = process.cwd()) {
56
+ const candidates = new Set();
57
+ for (const relativePath of EXACT_FILES) {
58
+ const fullPath = path.join(rootDir, relativePath);
59
+ if (fs.existsSync(fullPath) && fs.statSync(fullPath).isFile()) {
60
+ candidates.add(fullPath);
61
+ }
62
+ }
63
+ for (const relativeDir of DIRS) {
64
+ const fullDir = path.join(rootDir, relativeDir);
65
+ for (const filePath of walkFiles(fullDir)) {
66
+ candidates.add(filePath);
67
+ }
68
+ }
69
+ const files = [];
70
+ for (const fullPath of candidates) {
71
+ const text = readTextFile(fullPath);
72
+ const estimatedTokens = estimateTokens(text);
73
+ if (estimatedTokens > 0) {
74
+ files.push({
75
+ path: path.relative(rootDir, fullPath) || fullPath,
76
+ estimatedTokens,
77
+ });
78
+ }
79
+ }
80
+ files.sort((a, b) => b.estimatedTokens - a.estimatedTokens);
81
+ const totalEstimatedTokens = files.reduce((sum, file) => sum + file.estimatedTokens, 0);
82
+ return { totalEstimatedTokens, files };
83
+ }
@@ -0,0 +1,276 @@
1
+ import fs from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import Database from "better-sqlite3";
6
+ import { getConfiguredDatabasePath } from "./profile.js";
7
+ const HOME_DIR = path.join(os.homedir(), ".agent-profiler");
8
+ const WORKSPACE_DIR = path.join(process.cwd(), ".agent-profiler");
9
+ /** Same directory as this module: `src/core` when using tsx, `dist/core` when using build + copied schema. */
10
+ const SCHEMA_PATH = path.join(path.dirname(fileURLToPath(import.meta.url)), "schema.sql");
11
+ export function getDefaultDbPath() {
12
+ const fromEnv = process.env.AGENT_PROFILER_DB_PATH;
13
+ if (fromEnv && fromEnv.trim().length > 0)
14
+ return fromEnv;
15
+ const configured = getConfiguredDatabasePath(process.cwd());
16
+ if (configured && configured.trim().length > 0)
17
+ return configured;
18
+ return path.join(HOME_DIR, "events.sqlite");
19
+ }
20
+ export function resolveWritableDbPath(dbPath) {
21
+ try {
22
+ fs.mkdirSync(path.dirname(dbPath), { recursive: true });
23
+ return dbPath;
24
+ }
25
+ catch {
26
+ const fallbackPath = path.join(WORKSPACE_DIR, "events.sqlite");
27
+ fs.mkdirSync(path.dirname(fallbackPath), { recursive: true });
28
+ return fallbackPath;
29
+ }
30
+ }
31
+ export function openDb(dbPath = getDefaultDbPath()) {
32
+ const writableDbPath = resolveWritableDbPath(dbPath);
33
+ const db = new Database(writableDbPath);
34
+ db.pragma("journal_mode = WAL");
35
+ applySchema(db);
36
+ return db;
37
+ }
38
+ export function resolveUsableDbPath(preferredDbPath) {
39
+ const primary = resolveWritableDbPath(preferredDbPath);
40
+ try {
41
+ const db = new Database(primary);
42
+ db.pragma("journal_mode = WAL");
43
+ db.close();
44
+ return primary;
45
+ }
46
+ catch {
47
+ const fallbackPath = path.join(WORKSPACE_DIR, "events.sqlite");
48
+ fs.mkdirSync(path.dirname(fallbackPath), { recursive: true });
49
+ const db = new Database(fallbackPath);
50
+ db.pragma("journal_mode = WAL");
51
+ db.close();
52
+ return fallbackPath;
53
+ }
54
+ }
55
+ export function applySchema(db) {
56
+ const schemaSql = fs.readFileSync(SCHEMA_PATH, "utf8");
57
+ db.exec(schemaSql);
58
+ migrateEventsSchema(db);
59
+ }
60
+ function migrateEventsSchema(db) {
61
+ const columns = db.prepare(`PRAGMA table_info(events)`).all();
62
+ const names = new Set(columns.map((c) => c.name));
63
+ const add = (sql, colName) => {
64
+ if (!names.has(colName)) {
65
+ db.exec(sql);
66
+ names.add(colName);
67
+ }
68
+ };
69
+ add(`ALTER TABLE events ADD COLUMN workspace_path TEXT`, "workspace_path");
70
+ add(`ALTER TABLE events ADD COLUMN git_repo_root TEXT`, "git_repo_root");
71
+ add(`ALTER TABLE events ADD COLUMN git_repo_name TEXT`, "git_repo_name");
72
+ add(`ALTER TABLE events ADD COLUMN git_branch TEXT`, "git_branch");
73
+ add(`ALTER TABLE events ADD COLUMN interaction_kind TEXT`, "interaction_kind");
74
+ add(`ALTER TABLE events ADD COLUMN correlation_id TEXT`, "correlation_id");
75
+ add(`ALTER TABLE events ADD COLUMN tool_canonical_name TEXT`, "tool_canonical_name");
76
+ add(`ALTER TABLE events ADD COLUMN mcp_server TEXT`, "mcp_server");
77
+ add(`ALTER TABLE events ADD COLUMN mcp_tool TEXT`, "mcp_tool");
78
+ add(`ALTER TABLE events ADD COLUMN payload_byte_length INTEGER`, "payload_byte_length");
79
+ add(`ALTER TABLE events ADD COLUMN prompt_fingerprint TEXT`, "prompt_fingerprint");
80
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_events_workspace ON events(workspace_path, created_at)`);
81
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_events_interaction_kind ON events(interaction_kind, created_at)`);
82
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_events_correlation ON events(correlation_id, session_id)`);
83
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_events_prompt_fingerprint ON events(prompt_fingerprint, created_at)`);
84
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_spans_mcp ON interaction_spans(mcp_server, mcp_tool, started_at)`);
85
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_spans_session ON interaction_spans(session_key, started_at)`);
86
+ }
87
+ export function insertEvent(db, event, payloadHash, workspaceGit, derived) {
88
+ const stmt = db.prepare(`
89
+ INSERT INTO events (
90
+ created_at,
91
+ source,
92
+ source_event,
93
+ repo_path,
94
+ session_id,
95
+ turn_id,
96
+ model,
97
+ role,
98
+ estimated_input_tokens,
99
+ estimated_output_tokens,
100
+ estimated_total_tokens,
101
+ payload_hash,
102
+ raw_payload,
103
+ workspace_path,
104
+ git_repo_root,
105
+ git_repo_name,
106
+ git_branch,
107
+ interaction_kind,
108
+ correlation_id,
109
+ tool_canonical_name,
110
+ mcp_server,
111
+ mcp_tool,
112
+ payload_byte_length,
113
+ prompt_fingerprint
114
+ )
115
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
116
+ `);
117
+ const info = stmt.run(new Date().toISOString(), event.source, event.sourceEvent, event.repoPath ?? null, event.sessionId ?? null, event.turnId ?? null, event.model ?? null, event.role, event.estimatedInputTokens, event.estimatedOutputTokens, event.estimatedTotalTokens, payloadHash, JSON.stringify(event.rawPayload), workspaceGit.workspacePath, workspaceGit.gitRepoRoot, workspaceGit.gitRepoName, workspaceGit.gitBranch, derived.interactionKind, derived.correlationId, derived.toolCanonicalName, derived.mcpServer, derived.mcpTool, derived.payloadByteLength, derived.promptFingerprint);
118
+ return Number(info.lastInsertRowid);
119
+ }
120
+ /**
121
+ * Links pre/post/failure hook rows for the same logical tool or MCP call via correlation_id.
122
+ */
123
+ export function mergeInteractionSpan(db, eventId, normalized, workspaceGit, derived) {
124
+ if (!derived.correlationId || !derived.toolPhase)
125
+ return;
126
+ const sessionKey = normalized.sessionId ?? "";
127
+ const source = normalized.source;
128
+ const now = new Date().toISOString();
129
+ const existing = db
130
+ .prepare(`SELECT id, arg_token_estimate, result_token_estimate, pre_event_id, post_event_id, failure_event_id
131
+ FROM interaction_spans
132
+ WHERE session_key = ? AND source = ? AND correlation_id = ?`)
133
+ .get(sessionKey, source, derived.correlationId);
134
+ const argTok = Math.max(normalized.estimatedInputTokens, 0);
135
+ const resTok = Math.max(normalized.estimatedOutputTokens, 0);
136
+ const toolName = derived.toolCanonicalName;
137
+ const mcpS = derived.mcpServer;
138
+ const mcpT = derived.mcpTool;
139
+ if (!existing) {
140
+ const ins = db.prepare(`
141
+ INSERT INTO interaction_spans (
142
+ session_key, source, correlation_id, turn_id,
143
+ tool_canonical_name, mcp_server, mcp_tool,
144
+ pre_event_id, post_event_id, failure_event_id,
145
+ arg_token_estimate, result_token_estimate,
146
+ workspace_path, git_repo_root, git_repo_name, git_branch,
147
+ started_at, completed_at
148
+ )
149
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
150
+ `);
151
+ if (derived.toolPhase === "pre") {
152
+ ins.run(sessionKey, source, derived.correlationId, normalized.turnId ?? null, toolName, mcpS, mcpT, eventId, null, null, argTok, 0, workspaceGit.workspacePath, workspaceGit.gitRepoRoot, workspaceGit.gitRepoName, workspaceGit.gitBranch, now, null);
153
+ }
154
+ else if (derived.toolPhase === "post") {
155
+ ins.run(sessionKey, source, derived.correlationId, normalized.turnId ?? null, toolName, mcpS, mcpT, null, eventId, null, 0, resTok, workspaceGit.workspacePath, workspaceGit.gitRepoRoot, workspaceGit.gitRepoName, workspaceGit.gitBranch, null, now);
156
+ }
157
+ else {
158
+ ins.run(sessionKey, source, derived.correlationId, normalized.turnId ?? null, toolName, mcpS, mcpT, null, null, eventId, 0, resTok, workspaceGit.workspacePath, workspaceGit.gitRepoRoot, workspaceGit.gitRepoName, workspaceGit.gitBranch, null, now);
159
+ }
160
+ return;
161
+ }
162
+ if (derived.toolPhase === "pre") {
163
+ db.prepare(`UPDATE interaction_spans SET
164
+ pre_event_id = COALESCE(pre_event_id, ?),
165
+ started_at = COALESCE(started_at, ?),
166
+ arg_token_estimate = ?,
167
+ tool_canonical_name = COALESCE(tool_canonical_name, ?),
168
+ mcp_server = COALESCE(mcp_server, ?),
169
+ mcp_tool = COALESCE(mcp_tool, ?),
170
+ turn_id = COALESCE(turn_id, ?)
171
+ WHERE id = ?`).run(eventId, now, Math.max(existing.arg_token_estimate, argTok), toolName, mcpS, mcpT, normalized.turnId ?? null, existing.id);
172
+ }
173
+ else if (derived.toolPhase === "post") {
174
+ db.prepare(`UPDATE interaction_spans SET
175
+ post_event_id = COALESCE(post_event_id, ?),
176
+ completed_at = COALESCE(completed_at, ?),
177
+ result_token_estimate = ?,
178
+ tool_canonical_name = COALESCE(tool_canonical_name, ?),
179
+ mcp_server = COALESCE(mcp_server, ?),
180
+ mcp_tool = COALESCE(mcp_tool, ?),
181
+ turn_id = COALESCE(turn_id, ?)
182
+ WHERE id = ?`).run(eventId, now, Math.max(existing.result_token_estimate, resTok), toolName, mcpS, mcpT, normalized.turnId ?? null, existing.id);
183
+ }
184
+ else {
185
+ db.prepare(`UPDATE interaction_spans SET
186
+ failure_event_id = COALESCE(failure_event_id, ?),
187
+ completed_at = COALESCE(completed_at, ?),
188
+ result_token_estimate = ?,
189
+ tool_canonical_name = COALESCE(tool_canonical_name, ?),
190
+ mcp_server = COALESCE(mcp_server, ?),
191
+ mcp_tool = COALESCE(mcp_tool, ?),
192
+ turn_id = COALESCE(turn_id, ?)
193
+ WHERE id = ?`).run(eventId, now, Math.max(existing.result_token_estimate, resTok), toolName, mcpS, mcpT, normalized.turnId ?? null, existing.id);
194
+ }
195
+ }
196
+ export function getLastEventSummary(db) {
197
+ const row = db
198
+ .prepare(`
199
+ SELECT
200
+ created_at AS createdAt,
201
+ source,
202
+ source_event AS sourceEvent,
203
+ estimated_total_tokens AS estimatedTotalTokens
204
+ FROM events
205
+ ORDER BY created_at DESC
206
+ LIMIT 1
207
+ `)
208
+ .get();
209
+ return row ?? null;
210
+ }
211
+ export function getEventsForLatestSession(db) {
212
+ const latest = db
213
+ .prepare(`
214
+ SELECT source, session_id AS sessionId, repo_path AS repoPath
215
+ FROM events
216
+ ORDER BY created_at DESC
217
+ LIMIT 1
218
+ `)
219
+ .get();
220
+ if (!latest)
221
+ return [];
222
+ const rows = latest.sessionId
223
+ ? db
224
+ .prepare(`
225
+ SELECT
226
+ id,
227
+ created_at AS createdAt,
228
+ source,
229
+ source_event AS sourceEvent,
230
+ repo_path AS repoPath,
231
+ session_id AS sessionId,
232
+ turn_id AS turnId,
233
+ model,
234
+ role,
235
+ estimated_input_tokens AS estimatedInputTokens,
236
+ estimated_output_tokens AS estimatedOutputTokens,
237
+ estimated_total_tokens AS estimatedTotalTokens,
238
+ raw_payload AS rawPayload,
239
+ workspace_path AS workspacePath,
240
+ git_repo_root AS gitRepoRoot,
241
+ git_repo_name AS gitRepoName,
242
+ git_branch AS gitBranch
243
+ FROM events
244
+ WHERE source = ? AND session_id = ?
245
+ ORDER BY created_at ASC, id ASC
246
+ `)
247
+ .all(latest.source, latest.sessionId)
248
+ : db
249
+ .prepare(`
250
+ SELECT
251
+ id,
252
+ created_at AS createdAt,
253
+ source,
254
+ source_event AS sourceEvent,
255
+ repo_path AS repoPath,
256
+ session_id AS sessionId,
257
+ turn_id AS turnId,
258
+ model,
259
+ role,
260
+ estimated_input_tokens AS estimatedInputTokens,
261
+ estimated_output_tokens AS estimatedOutputTokens,
262
+ estimated_total_tokens AS estimatedTotalTokens,
263
+ raw_payload AS rawPayload,
264
+ workspace_path AS workspacePath,
265
+ git_repo_root AS gitRepoRoot,
266
+ git_repo_name AS gitRepoName,
267
+ git_branch AS gitBranch
268
+ FROM events
269
+ WHERE source = ? AND repo_path IS ?
270
+ ORDER BY created_at DESC, id DESC
271
+ LIMIT 200
272
+ `)
273
+ .all(latest.source, latest.repoPath ?? null)
274
+ .reverse();
275
+ return rows;
276
+ }