@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.
- package/dist/agentblame.js +3500 -0
- package/dist/blame.d.ts +4 -1
- package/dist/blame.js +280 -78
- package/dist/blame.js.map +1 -1
- package/dist/capture.d.ts +4 -7
- package/dist/capture.js +465 -467
- package/dist/capture.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +334 -72
- 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 +306 -323
- 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 +182 -158
- 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,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
|
-
|
|
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
|
|
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
|
-
*
|
|
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
|
|
177
|
+
function resetDatabase() {
|
|
189
178
|
const db = getDatabase();
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
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
|
-
//
|
|
185
|
+
// Session ID Generation
|
|
220
186
|
// =============================================================================
|
|
221
187
|
/**
|
|
222
|
-
*
|
|
223
|
-
|
|
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
|
|
198
|
+
function upsertSession(params) {
|
|
226
199
|
const db = getDatabase();
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
209
|
+
* Get a session by ID
|
|
280
210
|
*/
|
|
281
|
-
function
|
|
211
|
+
function getSession(sessionId) {
|
|
282
212
|
const db = getDatabase();
|
|
283
|
-
const
|
|
284
|
-
|
|
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
|
-
*
|
|
220
|
+
* Update session with first commit info
|
|
335
221
|
*/
|
|
336
|
-
function
|
|
222
|
+
function updateSessionFirstCommit(sessionId, commitSha) {
|
|
337
223
|
const db = getDatabase();
|
|
338
|
-
const fileName = filePath.split("/").pop() || "";
|
|
339
224
|
const stmt = db.prepare(`
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
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
|
-
|
|
347
|
-
return rows.map(rowToEdit);
|
|
230
|
+
stmt.run(commitSha, new Date().toISOString(), sessionId);
|
|
348
231
|
}
|
|
349
232
|
/**
|
|
350
|
-
* Get
|
|
233
|
+
* Get recent sessions
|
|
351
234
|
*/
|
|
352
|
-
function
|
|
235
|
+
function getRecentSessions(limit = 5) {
|
|
353
236
|
const db = getDatabase();
|
|
354
|
-
const stmt = db.prepare(`SELECT * FROM
|
|
355
|
-
const rows = stmt.all(
|
|
356
|
-
return rows.map(
|
|
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
|
-
*
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
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
|
|
376
|
-
|
|
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
|
-
*
|
|
260
|
+
* Get prompts for a session
|
|
392
261
|
*/
|
|
393
|
-
function
|
|
262
|
+
function getPromptsForSession(sessionId) {
|
|
394
263
|
const db = getDatabase();
|
|
395
|
-
const stmt = db.prepare(
|
|
396
|
-
|
|
397
|
-
|
|
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
|
-
*
|
|
269
|
+
* Get the most recent prompt for a session
|
|
404
270
|
*/
|
|
405
|
-
function
|
|
271
|
+
function getLatestPromptForSession(sessionId) {
|
|
406
272
|
const db = getDatabase();
|
|
407
|
-
const timestamp = new Date().toISOString();
|
|
408
273
|
const stmt = db.prepare(`
|
|
409
|
-
|
|
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
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
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
|
-
|
|
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
|
-
|
|
421
|
-
|
|
422
|
-
|
|
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
|
-
*
|
|
430
|
-
* - Removes matched entries older than maxAgeDays
|
|
431
|
-
* - Removes unmatched entries older than expireDays
|
|
355
|
+
* Insert a new tool call
|
|
432
356
|
*/
|
|
433
|
-
function
|
|
357
|
+
function insertToolCall(params) {
|
|
434
358
|
const db = getDatabase();
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
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
|
|
367
|
+
* Get tool calls for a session
|
|
458
368
|
*/
|
|
459
|
-
function
|
|
369
|
+
function getToolCallsForSession(sessionId) {
|
|
460
370
|
const db = getDatabase();
|
|
461
|
-
const
|
|
462
|
-
|
|
463
|
-
|
|
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
|
|
376
|
+
* Get unique tool names used in a session
|
|
469
377
|
*/
|
|
470
|
-
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) {
|
|
471
389
|
const db = getDatabase();
|
|
472
390
|
const stmt = db.prepare(`
|
|
473
|
-
SELECT *
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
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(
|
|
479
|
-
|
|
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
|
|
455
|
+
function rowToSession(row) {
|
|
485
456
|
return {
|
|
486
457
|
id: row.id,
|
|
487
|
-
|
|
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
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
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
|