@mesadev/agentblame 0.2.10 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/dist/agentblame.js +3500 -0
  2. package/dist/blame.d.ts +4 -1
  3. package/dist/blame.js +280 -78
  4. package/dist/blame.js.map +1 -1
  5. package/dist/capture.d.ts +4 -7
  6. package/dist/capture.js +465 -467
  7. package/dist/capture.js.map +1 -1
  8. package/dist/index.d.ts +1 -1
  9. package/dist/index.js +334 -72
  10. package/dist/index.js.map +1 -1
  11. package/dist/lib/analytics.d.ts +179 -0
  12. package/dist/lib/analytics.js +833 -0
  13. package/dist/lib/analytics.js.map +1 -0
  14. package/dist/lib/attribution.d.ts +54 -0
  15. package/dist/lib/attribution.js +266 -0
  16. package/dist/lib/attribution.js.map +1 -0
  17. package/dist/lib/checkpoint.d.ts +97 -0
  18. package/dist/lib/checkpoint.js +441 -0
  19. package/dist/lib/checkpoint.js.map +1 -0
  20. package/dist/lib/config.d.ts +46 -0
  21. package/dist/lib/config.js +123 -0
  22. package/dist/lib/config.js.map +1 -0
  23. package/dist/lib/database.d.ts +115 -85
  24. package/dist/lib/database.js +306 -323
  25. package/dist/lib/database.js.map +1 -1
  26. package/dist/lib/delta.d.ts +78 -0
  27. package/dist/lib/delta.js +309 -0
  28. package/dist/lib/delta.js.map +1 -0
  29. package/dist/lib/git/gitBlame.js +9 -4
  30. package/dist/lib/git/gitBlame.js.map +1 -1
  31. package/dist/lib/git/gitConfig.d.ts +5 -3
  32. package/dist/lib/git/gitConfig.js +41 -6
  33. package/dist/lib/git/gitConfig.js.map +1 -1
  34. package/dist/lib/git/gitDiff.d.ts +13 -1
  35. package/dist/lib/git/gitDiff.js +39 -7
  36. package/dist/lib/git/gitDiff.js.map +1 -1
  37. package/dist/lib/git/gitNotes.d.ts +30 -4
  38. package/dist/lib/git/gitNotes.js +140 -24
  39. package/dist/lib/git/gitNotes.js.map +1 -1
  40. package/dist/lib/hooks.d.ts +1 -0
  41. package/dist/lib/hooks.js +148 -27
  42. package/dist/lib/hooks.js.map +1 -1
  43. package/dist/lib/index.d.ts +7 -0
  44. package/dist/lib/index.js +13 -0
  45. package/dist/lib/index.js.map +1 -1
  46. package/dist/lib/storage.d.ts +163 -0
  47. package/dist/lib/storage.js +823 -0
  48. package/dist/lib/storage.js.map +1 -0
  49. package/dist/lib/trace.d.ts +118 -0
  50. package/dist/lib/trace.js +499 -0
  51. package/dist/lib/trace.js.map +1 -0
  52. package/dist/lib/types.d.ts +322 -114
  53. package/dist/lib/types.js +2 -1
  54. package/dist/lib/types.js.map +1 -1
  55. package/dist/lib/util.d.ts +8 -8
  56. package/dist/lib/util.js +18 -22
  57. package/dist/lib/util.js.map +1 -1
  58. package/dist/lib/watcher.d.ts +104 -0
  59. package/dist/lib/watcher.js +398 -0
  60. package/dist/lib/watcher.js.map +1 -0
  61. package/dist/post-merge.js +460 -421
  62. package/dist/post-merge.js.map +1 -1
  63. package/dist/process.d.ts +6 -5
  64. package/dist/process.js +182 -158
  65. package/dist/process.js.map +1 -1
  66. package/dist/sync.js +172 -131
  67. package/dist/sync.js.map +1 -1
  68. package/package.json +3 -2
@@ -1,8 +1,8 @@
1
1
  "use strict";
2
2
  /**
3
- * SQLite Database Module
3
+ * SQLite Database Module v3
4
4
  *
5
- * Handles persistent storage of AI edits for attribution matching.
5
+ * Handles persistent storage of sessions, prompts, and tool calls.
6
6
  * Uses Bun's built-in SQLite for high-performance lookups.
7
7
  */
8
8
  var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
@@ -39,104 +39,102 @@ var __importStar = (this && this.__importStar) || (function () {
39
39
  };
40
40
  })();
41
41
  Object.defineProperty(exports, "__esModule", { value: true });
42
- exports.setAgentBlameDir = setAgentBlameDir;
43
- exports.getAgentBlameDir = getAgentBlameDir;
42
+ exports.setDatabasePath = setDatabasePath;
44
43
  exports.getDbPath = getDbPath;
45
44
  exports.getDatabase = getDatabase;
46
45
  exports.closeDatabase = closeDatabase;
47
46
  exports.initDatabase = initDatabase;
48
- exports.insertEdit = insertEdit;
49
- exports.findByExactHash = findByExactHash;
50
- exports.findByNormalizedHash = findByNormalizedHash;
51
- exports.findEditsByFile = findEditsByFile;
52
- exports.getEditLines = getEditLines;
53
- exports.findLineMatch = findLineMatch;
54
- exports.markEditAsMatched = markEditAsMatched;
55
- exports.markEditsAsMatched = markEditsAsMatched;
47
+ exports.resetDatabase = resetDatabase;
48
+ exports.generateSessionId = generateSessionId;
49
+ exports.upsertSession = upsertSession;
50
+ exports.getSession = getSession;
51
+ exports.updateSessionFirstCommit = updateSessionFirstCommit;
52
+ exports.getRecentSessions = getRecentSessions;
53
+ exports.insertPrompt = insertPrompt;
54
+ exports.hashPromptContent = hashPromptContent;
55
+ exports.getPromptsForSession = getPromptsForSession;
56
+ exports.getLatestPromptForSession = getLatestPromptForSession;
57
+ exports.getConcatenatedPromptsForSession = getConcatenatedPromptsForSession;
58
+ exports.getPromptsWithToolCounts = getPromptsWithToolCounts;
59
+ exports.promptExists = promptExists;
60
+ exports.insertToolCall = insertToolCall;
61
+ exports.getToolCallsForSession = getToolCallsForSession;
62
+ exports.getToolNamesForSession = getToolNamesForSession;
63
+ exports.getToolCountsForSession = getToolCountsForSession;
64
+ exports.getSessionDuration = getSessionDuration;
56
65
  exports.cleanupOldEntries = cleanupOldEntries;
57
- exports.getPendingEditCount = getPendingEditCount;
58
- exports.getRecentPendingEdits = getRecentPendingEdits;
66
+ exports.getStats = getStats;
59
67
  const bun_sqlite_1 = require("bun:sqlite");
60
68
  const fs = __importStar(require("node:fs"));
61
69
  const path = __importStar(require("node:path"));
70
+ const node_crypto_1 = require("node:crypto");
62
71
  // =============================================================================
63
72
  // Database Schema
64
73
  // =============================================================================
65
74
  const SCHEMA = `
66
- -- Main edits table (one row per AI edit operation)
67
- CREATE TABLE IF NOT EXISTS edits (
68
- id INTEGER PRIMARY KEY AUTOINCREMENT,
69
- timestamp TEXT NOT NULL,
70
- provider TEXT NOT NULL,
71
- file_path TEXT NOT NULL,
75
+ -- Sessions: One per AI conversation
76
+ CREATE TABLE IF NOT EXISTS sessions (
77
+ id TEXT PRIMARY KEY,
78
+ agent TEXT NOT NULL,
72
79
  model TEXT,
73
- content TEXT NOT NULL,
80
+ conversation_id TEXT,
81
+ created_at TEXT NOT NULL,
82
+ first_commit_sha TEXT,
83
+ first_commit_at TEXT
84
+ );
85
+
86
+ -- Prompts: User messages that triggered AI actions
87
+ CREATE TABLE IF NOT EXISTS prompts (
88
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
89
+ session_id TEXT NOT NULL,
90
+ content TEXT,
74
91
  content_hash TEXT NOT NULL,
75
- content_hash_normalized TEXT NOT NULL,
76
- edit_type TEXT NOT NULL,
77
- old_content TEXT,
78
- status TEXT DEFAULT 'pending',
79
- matched_commit TEXT,
80
- matched_at TEXT,
81
- session_id TEXT,
82
- tool_use_id TEXT
92
+ timestamp TEXT NOT NULL,
93
+ FOREIGN KEY (session_id) REFERENCES sessions(id)
83
94
  );
84
95
 
85
- -- Lines table (one row per line in an edit)
86
- CREATE TABLE IF NOT EXISTS lines (
96
+ -- Tool Calls: What the AI did (minimal for counting per prompt)
97
+ CREATE TABLE IF NOT EXISTS tool_calls (
87
98
  id INTEGER PRIMARY KEY AUTOINCREMENT,
88
- edit_id INTEGER NOT NULL,
89
- content TEXT NOT NULL,
90
- hash TEXT NOT NULL,
91
- hash_normalized TEXT NOT NULL,
92
- line_number INTEGER,
93
- context_before TEXT,
94
- context_after TEXT,
95
- FOREIGN KEY (edit_id) REFERENCES edits(id) ON DELETE CASCADE
99
+ session_id TEXT NOT NULL,
100
+ tool_name TEXT NOT NULL,
101
+ file_path TEXT,
102
+ timestamp TEXT NOT NULL,
103
+ FOREIGN KEY (session_id) REFERENCES sessions(id)
96
104
  );
97
105
 
98
- -- Indexes for fast lookup
99
- CREATE INDEX IF NOT EXISTS idx_lines_hash ON lines(hash);
100
- CREATE INDEX IF NOT EXISTS idx_lines_hash_normalized ON lines(hash_normalized);
101
- CREATE INDEX IF NOT EXISTS idx_lines_line_number ON lines(line_number);
102
- CREATE INDEX IF NOT EXISTS idx_edits_status ON edits(status);
103
- CREATE INDEX IF NOT EXISTS idx_edits_file_path ON edits(file_path);
104
- CREATE INDEX IF NOT EXISTS idx_edits_content_hash ON edits(content_hash);
105
- CREATE INDEX IF NOT EXISTS idx_edits_session_id ON edits(session_id);
106
+ -- Indexes
107
+ CREATE INDEX IF NOT EXISTS idx_sessions_agent ON sessions(agent, conversation_id);
108
+ CREATE INDEX IF NOT EXISTS idx_prompts_session ON prompts(session_id);
109
+ CREATE INDEX IF NOT EXISTS idx_tool_calls_session ON tool_calls(session_id);
110
+ CREATE INDEX IF NOT EXISTS idx_tool_calls_file ON tool_calls(file_path);
111
+ CREATE INDEX IF NOT EXISTS idx_tool_calls_timestamp ON tool_calls(timestamp);
106
112
  `;
107
113
  // =============================================================================
108
114
  // Database Connection
109
115
  // =============================================================================
110
116
  let dbInstance = null;
111
- let currentAgentBlameDir = null;
117
+ let currentDbPath = null;
112
118
  /**
113
- * Set the agentblame directory for database operations.
114
- * Must be called before using any database functions.
119
+ * Set the database path directly.
115
120
  */
116
- function setAgentBlameDir(dir) {
117
- if (currentAgentBlameDir !== dir) {
118
- // Close existing connection if switching directories
121
+ function setDatabasePath(dbPath) {
122
+ if (currentDbPath !== dbPath) {
119
123
  if (dbInstance) {
120
124
  dbInstance.close();
121
125
  dbInstance = null;
122
126
  }
123
- currentAgentBlameDir = dir;
127
+ currentDbPath = dbPath;
124
128
  }
125
129
  }
126
- /**
127
- * Get the current agentblame directory.
128
- */
129
- function getAgentBlameDir() {
130
- return currentAgentBlameDir;
131
- }
132
130
  /**
133
131
  * Get the database file path
134
132
  */
135
133
  function getDbPath() {
136
- if (!currentAgentBlameDir) {
137
- throw new Error("agentblame directory not set. Call setAgentBlameDir() first.");
134
+ if (!currentDbPath) {
135
+ throw new Error("Database path not set. Call setDatabasePath() first.");
138
136
  }
139
- return path.join(currentAgentBlameDir, "agentblame.db");
137
+ return currentDbPath;
140
138
  }
141
139
  /**
142
140
  * Initialize and return the database connection
@@ -147,16 +145,13 @@ function getDatabase() {
147
145
  }
148
146
  const dbPath = getDbPath();
149
147
  const dbDir = path.dirname(dbPath);
150
- // Ensure directory exists
151
148
  if (!fs.existsSync(dbDir)) {
152
149
  fs.mkdirSync(dbDir, { recursive: true });
153
150
  }
154
- // Create database connection
155
151
  dbInstance = new bun_sqlite_1.Database(dbPath);
156
- // Enable foreign keys and WAL mode for better performance
157
152
  dbInstance.exec("PRAGMA foreign_keys = ON");
158
153
  dbInstance.exec("PRAGMA journal_mode = WAL");
159
- // Create tables and indexes
154
+ dbInstance.exec("PRAGMA busy_timeout = 5000");
160
155
  dbInstance.exec(SCHEMA);
161
156
  return dbInstance;
162
157
  }
@@ -170,332 +165,320 @@ function closeDatabase() {
170
165
  }
171
166
  }
172
167
  /**
173
- * Initialize database (creates file and schema if needed)
174
- * Call this during install to ensure DB is ready
168
+ * Initialize database
175
169
  */
176
170
  function initDatabase() {
177
171
  const db = getDatabase();
178
- // Database is initialized by getDatabase()
179
- // Just verify it's working
180
172
  db.exec("SELECT 1");
181
173
  }
182
174
  /**
183
- * Insert a new AI edit into the database.
184
- * Uses an explicit transaction to ensure atomicity - either all data
185
- * is written (edit + lines) or none. This is especially important
186
- * when running async hooks where the process could be interrupted.
175
+ * Reset database (drop and recreate tables)
187
176
  */
188
- function insertEdit(params) {
177
+ function resetDatabase() {
189
178
  const db = getDatabase();
190
- const editStmt = db.prepare(`
191
- INSERT INTO edits (
192
- timestamp, provider, file_path, model, content,
193
- content_hash, content_hash_normalized, edit_type, old_content,
194
- session_id, tool_use_id
195
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
196
- `);
197
- const lineStmt = db.prepare(`
198
- INSERT INTO lines (edit_id, content, hash, hash_normalized, line_number, context_before, context_after)
199
- VALUES (?, ?, ?, ?, ?, ?, ?)
200
- `);
201
- // Wrap in transaction for atomicity
202
- db.exec("BEGIN TRANSACTION");
203
- try {
204
- const result = editStmt.run(params.timestamp, params.provider, params.filePath, params.model, params.content, params.contentHash, params.contentHashNormalized, params.editType, params.oldContent || null, params.sessionId || null, params.toolUseId || null);
205
- const editId = Number(result.lastInsertRowid);
206
- // Insert lines with line numbers and context
207
- for (const line of params.lines) {
208
- lineStmt.run(editId, line.content, line.hash, line.hashNormalized, line.lineNumber || null, line.contextBefore || null, line.contextAfter || null);
209
- }
210
- db.exec("COMMIT");
211
- return editId;
212
- }
213
- catch (err) {
214
- db.exec("ROLLBACK");
215
- throw err;
216
- }
179
+ db.exec("DROP TABLE IF EXISTS tool_calls");
180
+ db.exec("DROP TABLE IF EXISTS prompts");
181
+ db.exec("DROP TABLE IF EXISTS sessions");
182
+ db.exec(SCHEMA);
217
183
  }
218
184
  // =============================================================================
219
- // Query Operations (used by process.ts for matching)
185
+ // Session ID Generation
220
186
  // =============================================================================
221
187
  /**
222
- * Find a line match by exact hash
223
- * Returns the edit and line if found, with same-file matches preferred
188
+ * Generate a stable session ID from agent and conversation ID
189
+ */
190
+ function generateSessionId(agent, conversationId) {
191
+ const hash = (0, node_crypto_1.createHash)("sha256");
192
+ hash.update(`${agent}:${conversationId}`);
193
+ return hash.digest("hex").substring(0, 16);
194
+ }
195
+ /**
196
+ * Upsert a session
224
197
  */
225
- function findByExactHash(hash, filePath) {
198
+ function upsertSession(params) {
226
199
  const db = getDatabase();
227
- // First try same-file match
228
- const sameFileStmt = db.prepare(`
229
- SELECT
230
- l.id as line_id, l.edit_id, l.content as line_content,
231
- l.hash, l.hash_normalized, l.line_number, l.context_before, l.context_after,
232
- e.*
233
- FROM lines l
234
- JOIN edits e ON l.edit_id = e.id
235
- WHERE l.hash = ? AND (
236
- e.file_path = ? OR
237
- e.file_path LIKE ? OR
238
- ? LIKE '%' || substr(e.file_path, instr(e.file_path, '/') + 1)
239
- )
240
- ORDER BY e.timestamp DESC
241
- LIMIT 1
200
+ const stmt = db.prepare(`
201
+ INSERT INTO sessions (id, agent, model, conversation_id, created_at)
202
+ VALUES (?, ?, ?, ?, ?)
203
+ ON CONFLICT(id) DO UPDATE SET
204
+ model = COALESCE(excluded.model, sessions.model)
242
205
  `);
243
- const fileName = filePath.split("/").pop() || "";
244
- let row = sameFileStmt.get(hash, filePath, `%${fileName}`, filePath);
245
- // If no same-file match, try any match
246
- if (!row) {
247
- const anyStmt = db.prepare(`
248
- SELECT
249
- l.id as line_id, l.edit_id, l.content as line_content,
250
- l.hash, l.hash_normalized, l.line_number, l.context_before, l.context_after,
251
- e.*
252
- FROM lines l
253
- JOIN edits e ON l.edit_id = e.id
254
- WHERE l.hash = ?
255
- ORDER BY e.timestamp DESC
256
- LIMIT 1
257
- `);
258
- row = anyStmt.get(hash);
259
- }
260
- if (!row)
261
- return null;
262
- return {
263
- edit: rowToEdit(row),
264
- line: {
265
- id: row.line_id,
266
- editId: row.edit_id,
267
- content: row.line_content,
268
- hash: row.hash,
269
- hashNormalized: row.hash_normalized,
270
- lineNumber: row.line_number,
271
- contextBefore: row.context_before,
272
- contextAfter: row.context_after,
273
- },
274
- matchType: "exact_hash",
275
- confidence: 1.0,
276
- };
206
+ stmt.run(params.id, params.agent, params.model ?? null, params.conversationId ?? null, new Date().toISOString());
277
207
  }
278
208
  /**
279
- * Find a line match by normalized hash
209
+ * Get a session by ID
280
210
  */
281
- function findByNormalizedHash(hashNormalized, filePath) {
211
+ function getSession(sessionId) {
282
212
  const db = getDatabase();
283
- const fileName = filePath.split("/").pop() || "";
284
- // First try same-file match
285
- const sameFileStmt = db.prepare(`
286
- SELECT
287
- l.id as line_id, l.edit_id, l.content as line_content,
288
- l.hash, l.hash_normalized, l.line_number, l.context_before, l.context_after,
289
- e.*
290
- FROM lines l
291
- JOIN edits e ON l.edit_id = e.id
292
- WHERE l.hash_normalized = ? AND (
293
- e.file_path = ? OR
294
- e.file_path LIKE ? OR
295
- ? LIKE '%' || substr(e.file_path, instr(e.file_path, '/') + 1)
296
- )
297
- ORDER BY e.timestamp DESC
298
- LIMIT 1
299
- `);
300
- let row = sameFileStmt.get(hashNormalized, filePath, `%${fileName}`, filePath);
301
- if (!row) {
302
- const anyStmt = db.prepare(`
303
- SELECT
304
- l.id as line_id, l.edit_id, l.content as line_content,
305
- l.hash, l.hash_normalized, l.line_number, l.context_before, l.context_after,
306
- e.*
307
- FROM lines l
308
- JOIN edits e ON l.edit_id = e.id
309
- WHERE l.hash_normalized = ?
310
- ORDER BY e.timestamp DESC
311
- LIMIT 1
312
- `);
313
- row = anyStmt.get(hashNormalized);
314
- }
213
+ const stmt = db.prepare("SELECT * FROM sessions WHERE id = ?");
214
+ const row = stmt.get(sessionId);
315
215
  if (!row)
316
216
  return null;
317
- return {
318
- edit: rowToEdit(row),
319
- line: {
320
- id: row.line_id,
321
- editId: row.edit_id,
322
- content: row.line_content,
323
- hash: row.hash,
324
- hashNormalized: row.hash_normalized,
325
- lineNumber: row.line_number,
326
- contextBefore: row.context_before,
327
- contextAfter: row.context_after,
328
- },
329
- matchType: "normalized_hash",
330
- confidence: 0.95,
331
- };
217
+ return rowToSession(row);
332
218
  }
333
219
  /**
334
- * Find edits for a specific file (used for substring matching fallback)
220
+ * Update session with first commit info
335
221
  */
336
- function findEditsByFile(filePath) {
222
+ function updateSessionFirstCommit(sessionId, commitSha) {
337
223
  const db = getDatabase();
338
- const fileName = filePath.split("/").pop() || "";
339
224
  const stmt = db.prepare(`
340
- SELECT * FROM edits
341
- WHERE file_path = ? OR
342
- file_path LIKE ? OR
343
- ? LIKE '%' || substr(file_path, instr(file_path, '/') + 1)
344
- ORDER BY timestamp DESC
225
+ UPDATE sessions
226
+ SET first_commit_sha = COALESCE(first_commit_sha, ?),
227
+ first_commit_at = COALESCE(first_commit_at, ?)
228
+ WHERE id = ?
345
229
  `);
346
- const rows = stmt.all(filePath, `%${fileName}`, filePath);
347
- return rows.map(rowToEdit);
230
+ stmt.run(commitSha, new Date().toISOString(), sessionId);
348
231
  }
349
232
  /**
350
- * Get lines for a specific edit
233
+ * Get recent sessions
351
234
  */
352
- function getEditLines(editId) {
235
+ function getRecentSessions(limit = 5) {
353
236
  const db = getDatabase();
354
- const stmt = db.prepare(`SELECT * FROM lines WHERE edit_id = ?`);
355
- const rows = stmt.all(editId);
356
- return rows.map(row => ({
357
- id: row.id,
358
- editId: row.edit_id,
359
- content: row.content,
360
- hash: row.hash,
361
- hashNormalized: row.hash_normalized,
362
- lineNumber: row.line_number,
363
- contextBefore: row.context_before,
364
- contextAfter: row.context_after,
365
- }));
237
+ const stmt = db.prepare(`SELECT * FROM sessions ORDER BY created_at DESC LIMIT ?`);
238
+ const rows = stmt.all(limit);
239
+ return rows.map(rowToSession);
366
240
  }
367
241
  /**
368
- * Find a line match using exact matching only:
369
- * 1. Exact hash match (confidence: 1.0)
370
- * 2. Normalized hash match (confidence: 0.95) - handles formatter whitespace changes
371
- *
372
- * No substring/fuzzy matching - if hash doesn't match, it's human code.
373
- * Philosophy: "If user modified AI code, it's human code"
242
+ * Insert a new prompt
243
+ */
244
+ function insertPrompt(params) {
245
+ const db = getDatabase();
246
+ const stmt = db.prepare(`
247
+ INSERT INTO prompts (session_id, content, content_hash, timestamp)
248
+ VALUES (?, ?, ?, ?)
249
+ `);
250
+ const result = stmt.run(params.sessionId, params.content, params.contentHash, params.timestamp ?? new Date().toISOString());
251
+ return Number(result.lastInsertRowid);
252
+ }
253
+ /**
254
+ * Generate a hash for prompt content (for deduplication)
374
255
  */
375
- function findLineMatch(lineContent, lineHash, lineHashNormalized, filePath) {
376
- // Strategy 1: Exact hash - perfect match
377
- let match = findByExactHash(lineHash, filePath);
378
- if (match)
379
- return match;
380
- // Strategy 2: Normalized hash - handles whitespace changes from formatters
381
- match = findByNormalizedHash(lineHashNormalized, filePath);
382
- if (match)
383
- return match;
384
- // No match = human code (either written by human or modified from AI)
385
- return null;
256
+ function hashPromptContent(content) {
257
+ return (0, node_crypto_1.createHash)('sha256').update(content).digest('hex').substring(0, 16);
386
258
  }
387
- // =============================================================================
388
- // Update Operations
389
- // =============================================================================
390
259
  /**
391
- * Mark an edit as matched to a commit
260
+ * Get prompts for a session
392
261
  */
393
- function markEditAsMatched(editId, commitSha) {
262
+ function getPromptsForSession(sessionId) {
394
263
  const db = getDatabase();
395
- const stmt = db.prepare(`
396
- UPDATE edits
397
- SET status = 'matched', matched_commit = ?, matched_at = ?
398
- WHERE id = ?
399
- `);
400
- stmt.run(commitSha, new Date().toISOString(), editId);
264
+ const stmt = db.prepare("SELECT * FROM prompts WHERE session_id = ? ORDER BY timestamp ASC");
265
+ const rows = stmt.all(sessionId);
266
+ return rows.map(rowToPrompt);
401
267
  }
402
268
  /**
403
- * Mark multiple edits as matched
269
+ * Get the most recent prompt for a session
404
270
  */
405
- function markEditsAsMatched(editIds, commitSha) {
271
+ function getLatestPromptForSession(sessionId) {
406
272
  const db = getDatabase();
407
- const timestamp = new Date().toISOString();
408
273
  const stmt = db.prepare(`
409
- UPDATE edits
410
- SET status = 'matched', matched_commit = ?, matched_at = ?
411
- WHERE id = ?
274
+ SELECT * FROM prompts WHERE session_id = ? ORDER BY timestamp DESC LIMIT 1
412
275
  `);
413
- db.exec("BEGIN TRANSACTION");
414
- try {
415
- for (const editId of editIds) {
416
- stmt.run(commitSha, timestamp, editId);
276
+ const row = stmt.get(sessionId);
277
+ return row ? rowToPrompt(row) : null;
278
+ }
279
+ /**
280
+ * Get all prompts for a session concatenated into one string
281
+ * Useful for displaying the full conversation context in CLI
282
+ */
283
+ function getConcatenatedPromptsForSession(sessionId) {
284
+ const prompts = getPromptsForSession(sessionId);
285
+ if (prompts.length === 0)
286
+ return null;
287
+ if (prompts.length === 1) {
288
+ return prompts[0].content ?? "[content not stored]";
289
+ }
290
+ // Concatenate with separator showing it's multiple prompts
291
+ return prompts
292
+ .map((p, i) => `[${i + 1}] ${p.content ?? '[not stored]'}`)
293
+ .join(" → ");
294
+ }
295
+ /**
296
+ * Get all prompts for a session with their associated tool call summaries
297
+ * Tool calls are grouped by the prompt that triggered them (based on timestamps)
298
+ * Used for git notes and analytics
299
+ */
300
+ function getPromptsWithToolCounts(sessionId) {
301
+ const prompts = getPromptsForSession(sessionId);
302
+ if (prompts.length === 0)
303
+ return null;
304
+ const toolCalls = getToolCallsForSession(sessionId);
305
+ const result = [];
306
+ for (let i = 0; i < prompts.length; i++) {
307
+ const prompt = prompts[i];
308
+ const promptTime = new Date(prompt.timestamp).getTime();
309
+ const nextPromptTime = i < prompts.length - 1
310
+ ? new Date(prompts[i + 1].timestamp).getTime()
311
+ : Infinity;
312
+ // Find tool calls between this prompt and the next
313
+ const toolsForPrompt = toolCalls.filter((tc) => {
314
+ const tcTime = new Date(tc.timestamp).getTime();
315
+ return tcTime >= promptTime && tcTime < nextPromptTime;
316
+ });
317
+ // Count tools
318
+ const tools = {};
319
+ for (const tool of toolsForPrompt) {
320
+ tools[tool.toolName] = (tools[tool.toolName] || 0) + 1;
417
321
  }
418
- db.exec("COMMIT");
322
+ // Calculate duration until next prompt (or last tool call)
323
+ let duration;
324
+ if (i < prompts.length - 1) {
325
+ duration = Math.round((nextPromptTime - promptTime) / 1000);
326
+ }
327
+ else if (toolsForPrompt.length > 0) {
328
+ // Last prompt: duration until last tool call
329
+ const lastToolTime = new Date(toolsForPrompt[toolsForPrompt.length - 1].timestamp).getTime();
330
+ duration = Math.round((lastToolTime - promptTime) / 1000);
331
+ }
332
+ result.push({
333
+ id: prompt.id,
334
+ timestamp: prompt.timestamp,
335
+ content: prompt.content,
336
+ tools: Object.keys(tools).length > 0 ? tools : undefined,
337
+ duration,
338
+ });
419
339
  }
420
- catch (err) {
421
- db.exec("ROLLBACK");
422
- throw err;
340
+ return result;
341
+ }
342
+ /**
343
+ * Check if a prompt already exists (by hash)
344
+ */
345
+ function promptExists(sessionId, contentHash) {
346
+ const db = getDatabase();
347
+ const stmt = db.prepare(`SELECT 1 FROM prompts WHERE session_id = ? AND content_hash = ? LIMIT 1`);
348
+ const result = stmt.get(sessionId, contentHash);
349
+ if (process.env.AGENTBLAME_DEBUG) {
350
+ console.error(`[agentblame] promptExists query result:`, result, `type:`, typeof result);
423
351
  }
352
+ return result !== undefined && result !== null;
424
353
  }
425
- // =============================================================================
426
- // Cleanup Operations
427
- // =============================================================================
428
354
  /**
429
- * Clean up old entries
430
- * - Removes matched entries older than maxAgeDays
431
- * - Removes unmatched entries older than expireDays
355
+ * Insert a new tool call
432
356
  */
433
- function cleanupOldEntries(maxAgeDays = 7, expireDays = 30) {
357
+ function insertToolCall(params) {
434
358
  const db = getDatabase();
435
- // Count before
436
- const beforeCount = db.prepare("SELECT COUNT(*) as count FROM edits").get().count;
437
- // Delete old matched entries
438
- db.prepare(`
439
- DELETE FROM edits
440
- WHERE status = 'matched'
441
- AND datetime(matched_at) < datetime('now', '-' || ? || ' days')
442
- `).run(maxAgeDays);
443
- // Delete old unmatched entries
444
- db.prepare(`
445
- DELETE FROM edits
446
- WHERE (status IS NULL OR status = 'pending')
447
- AND datetime(timestamp) < datetime('now', '-' || ? || ' days')
448
- `).run(expireDays);
449
- // Count after
450
- const afterCount = db.prepare("SELECT COUNT(*) as count FROM edits").get().count;
451
- return {
452
- removed: beforeCount - afterCount,
453
- kept: afterCount,
454
- };
359
+ const stmt = db.prepare(`
360
+ INSERT INTO tool_calls (session_id, tool_name, file_path, timestamp)
361
+ VALUES (?, ?, ?, ?)
362
+ `);
363
+ const result = stmt.run(params.sessionId, params.toolName, params.filePath ?? null, params.timestamp ?? new Date().toISOString());
364
+ return Number(result.lastInsertRowid);
455
365
  }
456
366
  /**
457
- * Get count of pending edits
367
+ * Get tool calls for a session
458
368
  */
459
- function getPendingEditCount() {
369
+ function getToolCallsForSession(sessionId) {
460
370
  const db = getDatabase();
461
- const result = db.prepare(`
462
- SELECT COUNT(*) as count FROM edits
463
- WHERE status IS NULL OR status = 'pending'
464
- `).get();
465
- return result.count;
371
+ const stmt = db.prepare("SELECT * FROM tool_calls WHERE session_id = ? ORDER BY timestamp ASC");
372
+ const rows = stmt.all(sessionId);
373
+ return rows.map(rowToToolCall);
466
374
  }
467
375
  /**
468
- * Get recent pending edits for status display
376
+ * Get unique tool names used in a session
469
377
  */
470
- function getRecentPendingEdits(limit = 5) {
378
+ function getToolNamesForSession(sessionId) {
379
+ const db = getDatabase();
380
+ const stmt = db.prepare(`SELECT DISTINCT tool_name FROM tool_calls WHERE session_id = ? ORDER BY tool_name`);
381
+ const rows = stmt.all(sessionId);
382
+ return rows.map((r) => r.tool_name);
383
+ }
384
+ /**
385
+ * Get tool call counts for a session
386
+ * Returns a map of tool_name -> count
387
+ */
388
+ function getToolCountsForSession(sessionId) {
471
389
  const db = getDatabase();
472
390
  const stmt = db.prepare(`
473
- SELECT * FROM edits
474
- WHERE status IS NULL OR status = 'pending'
475
- ORDER BY timestamp DESC
476
- LIMIT ?
391
+ SELECT tool_name, COUNT(*) as count
392
+ FROM tool_calls
393
+ WHERE session_id = ?
394
+ GROUP BY tool_name
477
395
  `);
478
- const rows = stmt.all(limit);
479
- return rows.map(rowToEdit);
396
+ const rows = stmt.all(sessionId);
397
+ const counts = {};
398
+ for (const row of rows) {
399
+ counts[row.tool_name] = row.count;
400
+ }
401
+ return counts;
402
+ }
403
+ /**
404
+ * Get session duration in seconds (last tool call - session start)
405
+ * Returns null if no tool calls
406
+ */
407
+ function getSessionDuration(sessionId) {
408
+ const db = getDatabase();
409
+ // Get session start time
410
+ const session = db.prepare("SELECT created_at FROM sessions WHERE id = ?").get(sessionId);
411
+ if (!session)
412
+ return null;
413
+ // Get last tool call time
414
+ const lastToolCall = db.prepare(`
415
+ SELECT MAX(timestamp) as last_ts
416
+ FROM tool_calls
417
+ WHERE session_id = ?
418
+ `).get(sessionId);
419
+ if (!lastToolCall?.last_ts)
420
+ return null;
421
+ const startTime = new Date(session.created_at).getTime();
422
+ const endTime = new Date(lastToolCall.last_ts).getTime();
423
+ return Math.round((endTime - startTime) / 1000);
424
+ }
425
+ // =============================================================================
426
+ // Cleanup Operations
427
+ // =============================================================================
428
+ /**
429
+ * Clean up old entries (sessions without commits older than maxAgeDays)
430
+ */
431
+ function cleanupOldEntries(maxAgeDays = 30) {
432
+ const db = getDatabase();
433
+ const beforeCount = db.prepare("SELECT COUNT(*) as count FROM sessions").get().count;
434
+ db.prepare(`
435
+ DELETE FROM sessions
436
+ WHERE first_commit_sha IS NULL
437
+ AND datetime(created_at) < datetime('now', '-' || ? || ' days')
438
+ `).run(maxAgeDays);
439
+ const afterCount = db.prepare("SELECT COUNT(*) as count FROM sessions").get().count;
440
+ return { removed: beforeCount - afterCount, kept: afterCount };
441
+ }
442
+ /**
443
+ * Get stats for status display
444
+ */
445
+ function getStats() {
446
+ const db = getDatabase();
447
+ const sessions = db.prepare("SELECT COUNT(*) as count FROM sessions").get().count;
448
+ const prompts = db.prepare("SELECT COUNT(*) as count FROM prompts").get().count;
449
+ const toolCalls = db.prepare("SELECT COUNT(*) as count FROM tool_calls").get().count;
450
+ return { sessions, prompts, toolCalls };
480
451
  }
481
452
  // =============================================================================
482
453
  // Helpers
483
454
  // =============================================================================
484
- function rowToEdit(row) {
455
+ function rowToSession(row) {
485
456
  return {
486
457
  id: row.id,
487
- timestamp: row.timestamp,
488
- provider: row.provider,
489
- filePath: row.file_path,
458
+ agent: row.agent,
490
459
  model: row.model,
460
+ conversationId: row.conversation_id,
461
+ createdAt: row.created_at,
462
+ firstCommitSha: row.first_commit_sha,
463
+ firstCommitAt: row.first_commit_at,
464
+ };
465
+ }
466
+ function rowToPrompt(row) {
467
+ return {
468
+ id: row.id,
469
+ sessionId: row.session_id,
491
470
  content: row.content,
492
471
  contentHash: row.content_hash,
493
- contentHashNormalized: row.content_hash_normalized,
494
- editType: row.edit_type,
495
- oldContent: row.old_content,
496
- status: row.status,
497
- matchedCommit: row.matched_commit,
498
- matchedAt: row.matched_at,
472
+ timestamp: row.timestamp,
473
+ };
474
+ }
475
+ function rowToToolCall(row) {
476
+ return {
477
+ id: row.id,
478
+ sessionId: row.session_id,
479
+ toolName: row.tool_name,
480
+ filePath: row.file_path,
481
+ timestamp: row.timestamp,
499
482
  };
500
483
  }
501
484
  //# sourceMappingURL=database.js.map