@mesadev/agentblame 0.2.11 → 3.0.1

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 +293 -78
  4. package/dist/blame.js.map +1 -1
  5. package/dist/capture.d.ts +4 -7
  6. package/dist/capture.js +464 -486
  7. package/dist/capture.js.map +1 -1
  8. package/dist/index.d.ts +1 -1
  9. package/dist/index.js +248 -85
  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 +305 -325
  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 +233 -152
  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,19 +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
- // Set busy timeout to handle concurrent writes from async hooks
160
- // Without this, concurrent capture processes get SQLITE_BUSY and fail silently
161
154
  dbInstance.exec("PRAGMA busy_timeout = 5000");
162
- // Create tables and indexes
163
155
  dbInstance.exec(SCHEMA);
164
156
  return dbInstance;
165
157
  }
@@ -173,332 +165,320 @@ function closeDatabase() {
173
165
  }
174
166
  }
175
167
  /**
176
- * Initialize database (creates file and schema if needed)
177
- * Call this during install to ensure DB is ready
168
+ * Initialize database
178
169
  */
179
170
  function initDatabase() {
180
171
  const db = getDatabase();
181
- // Database is initialized by getDatabase()
182
- // Just verify it's working
183
172
  db.exec("SELECT 1");
184
173
  }
185
174
  /**
186
- * Insert a new AI edit into the database.
187
- * Uses an explicit transaction to ensure atomicity - either all data
188
- * is written (edit + lines) or none. This is especially important
189
- * when running async hooks where the process could be interrupted.
175
+ * Reset database (drop and recreate tables)
190
176
  */
191
- function insertEdit(params) {
177
+ function resetDatabase() {
192
178
  const db = getDatabase();
193
- const editStmt = db.prepare(`
194
- INSERT INTO edits (
195
- timestamp, provider, file_path, model, content,
196
- content_hash, content_hash_normalized, edit_type, old_content,
197
- session_id, tool_use_id
198
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
199
- `);
200
- const lineStmt = db.prepare(`
201
- INSERT INTO lines (edit_id, content, hash, hash_normalized, line_number, context_before, context_after)
202
- VALUES (?, ?, ?, ?, ?, ?, ?)
203
- `);
204
- // Wrap in transaction for atomicity
205
- db.exec("BEGIN TRANSACTION");
206
- try {
207
- 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);
208
- const editId = Number(result.lastInsertRowid);
209
- // Insert lines with line numbers and context
210
- for (const line of params.lines) {
211
- lineStmt.run(editId, line.content, line.hash, line.hashNormalized, line.lineNumber || null, line.contextBefore || null, line.contextAfter || null);
212
- }
213
- db.exec("COMMIT");
214
- return editId;
215
- }
216
- catch (err) {
217
- db.exec("ROLLBACK");
218
- throw err;
219
- }
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);
220
183
  }
221
184
  // =============================================================================
222
- // Query Operations (used by process.ts for matching)
185
+ // Session ID Generation
223
186
  // =============================================================================
224
187
  /**
225
- * Find a line match by exact hash
226
- * 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
227
197
  */
228
- function findByExactHash(hash, filePath) {
198
+ function upsertSession(params) {
229
199
  const db = getDatabase();
230
- // First try same-file match
231
- const sameFileStmt = db.prepare(`
232
- SELECT
233
- l.id as line_id, l.edit_id, l.content as line_content,
234
- l.hash, l.hash_normalized, l.line_number, l.context_before, l.context_after,
235
- e.*
236
- FROM lines l
237
- JOIN edits e ON l.edit_id = e.id
238
- WHERE l.hash = ? AND (
239
- e.file_path = ? OR
240
- e.file_path LIKE ? OR
241
- ? LIKE '%' || substr(e.file_path, instr(e.file_path, '/') + 1)
242
- )
243
- ORDER BY e.timestamp DESC
244
- 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)
245
205
  `);
246
- const fileName = filePath.split("/").pop() || "";
247
- let row = sameFileStmt.get(hash, filePath, `%${fileName}`, filePath);
248
- // If no same-file match, try any match
249
- if (!row) {
250
- const anyStmt = db.prepare(`
251
- SELECT
252
- l.id as line_id, l.edit_id, l.content as line_content,
253
- l.hash, l.hash_normalized, l.line_number, l.context_before, l.context_after,
254
- e.*
255
- FROM lines l
256
- JOIN edits e ON l.edit_id = e.id
257
- WHERE l.hash = ?
258
- ORDER BY e.timestamp DESC
259
- LIMIT 1
260
- `);
261
- row = anyStmt.get(hash);
262
- }
263
- if (!row)
264
- return null;
265
- return {
266
- edit: rowToEdit(row),
267
- line: {
268
- id: row.line_id,
269
- editId: row.edit_id,
270
- content: row.line_content,
271
- hash: row.hash,
272
- hashNormalized: row.hash_normalized,
273
- lineNumber: row.line_number,
274
- contextBefore: row.context_before,
275
- contextAfter: row.context_after,
276
- },
277
- matchType: "exact_hash",
278
- confidence: 1.0,
279
- };
206
+ stmt.run(params.id, params.agent, params.model ?? null, params.conversationId ?? null, new Date().toISOString());
280
207
  }
281
208
  /**
282
- * Find a line match by normalized hash
209
+ * Get a session by ID
283
210
  */
284
- function findByNormalizedHash(hashNormalized, filePath) {
211
+ function getSession(sessionId) {
285
212
  const db = getDatabase();
286
- const fileName = filePath.split("/").pop() || "";
287
- // First try same-file match
288
- const sameFileStmt = db.prepare(`
289
- SELECT
290
- l.id as line_id, l.edit_id, l.content as line_content,
291
- l.hash, l.hash_normalized, l.line_number, l.context_before, l.context_after,
292
- e.*
293
- FROM lines l
294
- JOIN edits e ON l.edit_id = e.id
295
- WHERE l.hash_normalized = ? AND (
296
- e.file_path = ? OR
297
- e.file_path LIKE ? OR
298
- ? LIKE '%' || substr(e.file_path, instr(e.file_path, '/') + 1)
299
- )
300
- ORDER BY e.timestamp DESC
301
- LIMIT 1
302
- `);
303
- let row = sameFileStmt.get(hashNormalized, filePath, `%${fileName}`, filePath);
304
- if (!row) {
305
- const anyStmt = db.prepare(`
306
- SELECT
307
- l.id as line_id, l.edit_id, l.content as line_content,
308
- l.hash, l.hash_normalized, l.line_number, l.context_before, l.context_after,
309
- e.*
310
- FROM lines l
311
- JOIN edits e ON l.edit_id = e.id
312
- WHERE l.hash_normalized = ?
313
- ORDER BY e.timestamp DESC
314
- LIMIT 1
315
- `);
316
- row = anyStmt.get(hashNormalized);
317
- }
213
+ const stmt = db.prepare("SELECT * FROM sessions WHERE id = ?");
214
+ const row = stmt.get(sessionId);
318
215
  if (!row)
319
216
  return null;
320
- return {
321
- edit: rowToEdit(row),
322
- line: {
323
- id: row.line_id,
324
- editId: row.edit_id,
325
- content: row.line_content,
326
- hash: row.hash,
327
- hashNormalized: row.hash_normalized,
328
- lineNumber: row.line_number,
329
- contextBefore: row.context_before,
330
- contextAfter: row.context_after,
331
- },
332
- matchType: "normalized_hash",
333
- confidence: 0.95,
334
- };
217
+ return rowToSession(row);
335
218
  }
336
219
  /**
337
- * Find edits for a specific file (used for substring matching fallback)
220
+ * Update session with first commit info
338
221
  */
339
- function findEditsByFile(filePath) {
222
+ function updateSessionFirstCommit(sessionId, commitSha) {
340
223
  const db = getDatabase();
341
- const fileName = filePath.split("/").pop() || "";
342
224
  const stmt = db.prepare(`
343
- SELECT * FROM edits
344
- WHERE file_path = ? OR
345
- file_path LIKE ? OR
346
- ? LIKE '%' || substr(file_path, instr(file_path, '/') + 1)
347
- 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 = ?
348
229
  `);
349
- const rows = stmt.all(filePath, `%${fileName}`, filePath);
350
- return rows.map(rowToEdit);
230
+ stmt.run(commitSha, new Date().toISOString(), sessionId);
351
231
  }
352
232
  /**
353
- * Get lines for a specific edit
233
+ * Get recent sessions
354
234
  */
355
- function getEditLines(editId) {
235
+ function getRecentSessions(limit = 5) {
356
236
  const db = getDatabase();
357
- const stmt = db.prepare(`SELECT * FROM lines WHERE edit_id = ?`);
358
- const rows = stmt.all(editId);
359
- return rows.map(row => ({
360
- id: row.id,
361
- editId: row.edit_id,
362
- content: row.content,
363
- hash: row.hash,
364
- hashNormalized: row.hash_normalized,
365
- lineNumber: row.line_number,
366
- contextBefore: row.context_before,
367
- contextAfter: row.context_after,
368
- }));
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);
369
240
  }
370
241
  /**
371
- * Find a line match using exact matching only:
372
- * 1. Exact hash match (confidence: 1.0)
373
- * 2. Normalized hash match (confidence: 0.95) - handles formatter whitespace changes
374
- *
375
- * No substring/fuzzy matching - if hash doesn't match, it's human code.
376
- * 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)
377
255
  */
378
- function findLineMatch(lineContent, lineHash, lineHashNormalized, filePath) {
379
- // Strategy 1: Exact hash - perfect match
380
- let match = findByExactHash(lineHash, filePath);
381
- if (match)
382
- return match;
383
- // Strategy 2: Normalized hash - handles whitespace changes from formatters
384
- match = findByNormalizedHash(lineHashNormalized, filePath);
385
- if (match)
386
- return match;
387
- // No match = human code (either written by human or modified from AI)
388
- return null;
256
+ function hashPromptContent(content) {
257
+ return (0, node_crypto_1.createHash)('sha256').update(content).digest('hex').substring(0, 16);
389
258
  }
390
- // =============================================================================
391
- // Update Operations
392
- // =============================================================================
393
259
  /**
394
- * Mark an edit as matched to a commit
260
+ * Get prompts for a session
395
261
  */
396
- function markEditAsMatched(editId, commitSha) {
262
+ function getPromptsForSession(sessionId) {
397
263
  const db = getDatabase();
398
- const stmt = db.prepare(`
399
- UPDATE edits
400
- SET status = 'matched', matched_commit = ?, matched_at = ?
401
- WHERE id = ?
402
- `);
403
- 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);
404
267
  }
405
268
  /**
406
- * Mark multiple edits as matched
269
+ * Get the most recent prompt for a session
407
270
  */
408
- function markEditsAsMatched(editIds, commitSha) {
271
+ function getLatestPromptForSession(sessionId) {
409
272
  const db = getDatabase();
410
- const timestamp = new Date().toISOString();
411
273
  const stmt = db.prepare(`
412
- UPDATE edits
413
- SET status = 'matched', matched_commit = ?, matched_at = ?
414
- WHERE id = ?
274
+ SELECT * FROM prompts WHERE session_id = ? ORDER BY timestamp DESC LIMIT 1
415
275
  `);
416
- db.exec("BEGIN TRANSACTION");
417
- try {
418
- for (const editId of editIds) {
419
- 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;
420
321
  }
421
- 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
+ });
422
339
  }
423
- catch (err) {
424
- db.exec("ROLLBACK");
425
- 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);
426
351
  }
352
+ return result !== undefined && result !== null;
427
353
  }
428
- // =============================================================================
429
- // Cleanup Operations
430
- // =============================================================================
431
354
  /**
432
- * Clean up old entries
433
- * - Removes matched entries older than maxAgeDays
434
- * - Removes unmatched entries older than expireDays
355
+ * Insert a new tool call
435
356
  */
436
- function cleanupOldEntries(maxAgeDays = 7, expireDays = 30) {
357
+ function insertToolCall(params) {
437
358
  const db = getDatabase();
438
- // Count before
439
- const beforeCount = db.prepare("SELECT COUNT(*) as count FROM edits").get().count;
440
- // Delete old matched entries
441
- db.prepare(`
442
- DELETE FROM edits
443
- WHERE status = 'matched'
444
- AND datetime(matched_at) < datetime('now', '-' || ? || ' days')
445
- `).run(maxAgeDays);
446
- // Delete old unmatched entries
447
- db.prepare(`
448
- DELETE FROM edits
449
- WHERE (status IS NULL OR status = 'pending')
450
- AND datetime(timestamp) < datetime('now', '-' || ? || ' days')
451
- `).run(expireDays);
452
- // Count after
453
- const afterCount = db.prepare("SELECT COUNT(*) as count FROM edits").get().count;
454
- return {
455
- removed: beforeCount - afterCount,
456
- kept: afterCount,
457
- };
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);
458
365
  }
459
366
  /**
460
- * Get count of pending edits
367
+ * Get tool calls for a session
461
368
  */
462
- function getPendingEditCount() {
369
+ function getToolCallsForSession(sessionId) {
463
370
  const db = getDatabase();
464
- const result = db.prepare(`
465
- SELECT COUNT(*) as count FROM edits
466
- WHERE status IS NULL OR status = 'pending'
467
- `).get();
468
- 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);
469
374
  }
470
375
  /**
471
- * Get recent pending edits for status display
376
+ * Get unique tool names used in a session
472
377
  */
473
- 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) {
474
389
  const db = getDatabase();
475
390
  const stmt = db.prepare(`
476
- SELECT * FROM edits
477
- WHERE status IS NULL OR status = 'pending'
478
- ORDER BY timestamp DESC
479
- LIMIT ?
391
+ SELECT tool_name, COUNT(*) as count
392
+ FROM tool_calls
393
+ WHERE session_id = ?
394
+ GROUP BY tool_name
480
395
  `);
481
- const rows = stmt.all(limit);
482
- 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 };
483
451
  }
484
452
  // =============================================================================
485
453
  // Helpers
486
454
  // =============================================================================
487
- function rowToEdit(row) {
455
+ function rowToSession(row) {
488
456
  return {
489
457
  id: row.id,
490
- timestamp: row.timestamp,
491
- provider: row.provider,
492
- filePath: row.file_path,
458
+ agent: row.agent,
493
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,
494
470
  content: row.content,
495
471
  contentHash: row.content_hash,
496
- contentHashNormalized: row.content_hash_normalized,
497
- editType: row.edit_type,
498
- oldContent: row.old_content,
499
- status: row.status,
500
- matchedCommit: row.matched_commit,
501
- 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,
502
482
  };
503
483
  }
504
484
  //# sourceMappingURL=database.js.map