@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.
- package/dist/agentblame.js +3500 -0
- package/dist/blame.d.ts +4 -1
- package/dist/blame.js +293 -78
- package/dist/blame.js.map +1 -1
- package/dist/capture.d.ts +4 -7
- package/dist/capture.js +464 -486
- package/dist/capture.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +248 -85
- package/dist/index.js.map +1 -1
- package/dist/lib/analytics.d.ts +179 -0
- package/dist/lib/analytics.js +833 -0
- package/dist/lib/analytics.js.map +1 -0
- package/dist/lib/attribution.d.ts +54 -0
- package/dist/lib/attribution.js +266 -0
- package/dist/lib/attribution.js.map +1 -0
- package/dist/lib/checkpoint.d.ts +97 -0
- package/dist/lib/checkpoint.js +441 -0
- package/dist/lib/checkpoint.js.map +1 -0
- package/dist/lib/config.d.ts +46 -0
- package/dist/lib/config.js +123 -0
- package/dist/lib/config.js.map +1 -0
- package/dist/lib/database.d.ts +115 -85
- package/dist/lib/database.js +305 -325
- package/dist/lib/database.js.map +1 -1
- package/dist/lib/delta.d.ts +78 -0
- package/dist/lib/delta.js +309 -0
- package/dist/lib/delta.js.map +1 -0
- package/dist/lib/git/gitBlame.js +9 -4
- package/dist/lib/git/gitBlame.js.map +1 -1
- package/dist/lib/git/gitConfig.d.ts +5 -3
- package/dist/lib/git/gitConfig.js +41 -6
- package/dist/lib/git/gitConfig.js.map +1 -1
- package/dist/lib/git/gitDiff.d.ts +13 -1
- package/dist/lib/git/gitDiff.js +39 -7
- package/dist/lib/git/gitDiff.js.map +1 -1
- package/dist/lib/git/gitNotes.d.ts +30 -4
- package/dist/lib/git/gitNotes.js +140 -24
- package/dist/lib/git/gitNotes.js.map +1 -1
- package/dist/lib/hooks.d.ts +1 -0
- package/dist/lib/hooks.js +148 -27
- package/dist/lib/hooks.js.map +1 -1
- package/dist/lib/index.d.ts +7 -0
- package/dist/lib/index.js +13 -0
- package/dist/lib/index.js.map +1 -1
- package/dist/lib/storage.d.ts +163 -0
- package/dist/lib/storage.js +823 -0
- package/dist/lib/storage.js.map +1 -0
- package/dist/lib/trace.d.ts +118 -0
- package/dist/lib/trace.js +499 -0
- package/dist/lib/trace.js.map +1 -0
- package/dist/lib/types.d.ts +322 -114
- package/dist/lib/types.js +2 -1
- package/dist/lib/types.js.map +1 -1
- package/dist/lib/util.d.ts +8 -8
- package/dist/lib/util.js +18 -22
- package/dist/lib/util.js.map +1 -1
- package/dist/lib/watcher.d.ts +104 -0
- package/dist/lib/watcher.js +398 -0
- package/dist/lib/watcher.js.map +1 -0
- package/dist/post-merge.js +460 -421
- package/dist/post-merge.js.map +1 -1
- package/dist/process.d.ts +6 -5
- package/dist/process.js +233 -152
- package/dist/process.js.map +1 -1
- package/dist/sync.js +172 -131
- package/dist/sync.js.map +1 -1
- package/package.json +3 -2
package/dist/lib/database.js
CHANGED
|
@@ -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
|
|
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.
|
|
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.
|
|
49
|
-
exports.
|
|
50
|
-
exports.
|
|
51
|
-
exports.
|
|
52
|
-
exports.
|
|
53
|
-
exports.
|
|
54
|
-
exports.
|
|
55
|
-
exports.
|
|
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.
|
|
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
|
-
--
|
|
67
|
-
CREATE TABLE IF NOT EXISTS
|
|
68
|
-
id
|
|
69
|
-
|
|
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
|
-
|
|
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
|
-
|
|
76
|
-
|
|
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
|
-
--
|
|
86
|
-
CREATE TABLE IF NOT EXISTS
|
|
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
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
|
99
|
-
CREATE INDEX IF NOT EXISTS
|
|
100
|
-
CREATE INDEX IF NOT EXISTS
|
|
101
|
-
CREATE INDEX IF NOT EXISTS
|
|
102
|
-
CREATE INDEX IF NOT EXISTS
|
|
103
|
-
CREATE INDEX IF NOT EXISTS
|
|
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
|
|
117
|
+
let currentDbPath = null;
|
|
112
118
|
/**
|
|
113
|
-
* Set the
|
|
114
|
-
* Must be called before using any database functions.
|
|
119
|
+
* Set the database path directly.
|
|
115
120
|
*/
|
|
116
|
-
function
|
|
117
|
-
if (
|
|
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
|
-
|
|
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 (!
|
|
137
|
-
throw new Error("
|
|
134
|
+
if (!currentDbPath) {
|
|
135
|
+
throw new Error("Database path not set. Call setDatabasePath() first.");
|
|
138
136
|
}
|
|
139
|
-
return
|
|
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
|
|
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
|
-
*
|
|
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
|
|
177
|
+
function resetDatabase() {
|
|
192
178
|
const db = getDatabase();
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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
|
-
//
|
|
185
|
+
// Session ID Generation
|
|
223
186
|
// =============================================================================
|
|
224
187
|
/**
|
|
225
|
-
*
|
|
226
|
-
|
|
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
|
|
198
|
+
function upsertSession(params) {
|
|
229
199
|
const db = getDatabase();
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
209
|
+
* Get a session by ID
|
|
283
210
|
*/
|
|
284
|
-
function
|
|
211
|
+
function getSession(sessionId) {
|
|
285
212
|
const db = getDatabase();
|
|
286
|
-
const
|
|
287
|
-
|
|
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
|
-
*
|
|
220
|
+
* Update session with first commit info
|
|
338
221
|
*/
|
|
339
|
-
function
|
|
222
|
+
function updateSessionFirstCommit(sessionId, commitSha) {
|
|
340
223
|
const db = getDatabase();
|
|
341
|
-
const fileName = filePath.split("/").pop() || "";
|
|
342
224
|
const stmt = db.prepare(`
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
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
|
-
|
|
350
|
-
return rows.map(rowToEdit);
|
|
230
|
+
stmt.run(commitSha, new Date().toISOString(), sessionId);
|
|
351
231
|
}
|
|
352
232
|
/**
|
|
353
|
-
* Get
|
|
233
|
+
* Get recent sessions
|
|
354
234
|
*/
|
|
355
|
-
function
|
|
235
|
+
function getRecentSessions(limit = 5) {
|
|
356
236
|
const db = getDatabase();
|
|
357
|
-
const stmt = db.prepare(`SELECT * FROM
|
|
358
|
-
const rows = stmt.all(
|
|
359
|
-
return rows.map(
|
|
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
|
-
*
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
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
|
|
379
|
-
|
|
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
|
-
*
|
|
260
|
+
* Get prompts for a session
|
|
395
261
|
*/
|
|
396
|
-
function
|
|
262
|
+
function getPromptsForSession(sessionId) {
|
|
397
263
|
const db = getDatabase();
|
|
398
|
-
const stmt = db.prepare(
|
|
399
|
-
|
|
400
|
-
|
|
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
|
-
*
|
|
269
|
+
* Get the most recent prompt for a session
|
|
407
270
|
*/
|
|
408
|
-
function
|
|
271
|
+
function getLatestPromptForSession(sessionId) {
|
|
409
272
|
const db = getDatabase();
|
|
410
|
-
const timestamp = new Date().toISOString();
|
|
411
273
|
const stmt = db.prepare(`
|
|
412
|
-
|
|
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
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
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
|
-
|
|
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
|
-
|
|
424
|
-
|
|
425
|
-
|
|
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
|
-
*
|
|
433
|
-
* - Removes matched entries older than maxAgeDays
|
|
434
|
-
* - Removes unmatched entries older than expireDays
|
|
355
|
+
* Insert a new tool call
|
|
435
356
|
*/
|
|
436
|
-
function
|
|
357
|
+
function insertToolCall(params) {
|
|
437
358
|
const db = getDatabase();
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
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
|
|
367
|
+
* Get tool calls for a session
|
|
461
368
|
*/
|
|
462
|
-
function
|
|
369
|
+
function getToolCallsForSession(sessionId) {
|
|
463
370
|
const db = getDatabase();
|
|
464
|
-
const
|
|
465
|
-
|
|
466
|
-
|
|
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
|
|
376
|
+
* Get unique tool names used in a session
|
|
472
377
|
*/
|
|
473
|
-
function
|
|
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 *
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
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(
|
|
482
|
-
|
|
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
|
|
455
|
+
function rowToSession(row) {
|
|
488
456
|
return {
|
|
489
457
|
id: row.id,
|
|
490
|
-
|
|
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
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
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
|