@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
@@ -0,0 +1,3500 @@
1
+ #!/usr/bin/env bun
2
+ // @bun
3
+ var __require = import.meta.require;
4
+ // src/index.ts
5
+ import * as path7 from "path";
6
+ import * as fs7 from "fs";
7
+ import * as os from "os";
8
+ import { execSync as execSync2 } from "child_process";
9
+
10
+ // src/blame.ts
11
+ import * as path2 from "path";
12
+ import * as fs2 from "fs";
13
+
14
+ // src/lib/git/gitCli.ts
15
+ import { spawn } from "child_process";
16
+ async function runGit(cwd, args, timeoutMs = 30000) {
17
+ return new Promise((resolve) => {
18
+ const proc = spawn("git", args, {
19
+ cwd,
20
+ timeout: timeoutMs,
21
+ stdio: ["ignore", "pipe", "pipe"]
22
+ });
23
+ let stdout = "";
24
+ let stderr = "";
25
+ proc.stdout.on("data", (data) => {
26
+ stdout += data.toString();
27
+ });
28
+ proc.stderr.on("data", (data) => {
29
+ stderr += data.toString();
30
+ });
31
+ proc.on("close", (code) => {
32
+ resolve({
33
+ exitCode: code ?? 1,
34
+ stdout,
35
+ stderr
36
+ });
37
+ });
38
+ proc.on("error", (err) => {
39
+ resolve({
40
+ exitCode: 1,
41
+ stdout,
42
+ stderr: err.message
43
+ });
44
+ });
45
+ });
46
+ }
47
+ async function getRepoRoot(dir) {
48
+ const result = await runGit(dir, ["rev-parse", "--show-toplevel"], 5000);
49
+ if (result.exitCode !== 0)
50
+ return null;
51
+ return result.stdout.trim() || null;
52
+ }
53
+
54
+ // src/lib/git/gitBlame.ts
55
+ async function getBlame(repoRoot, filePath) {
56
+ const result = await runGit(repoRoot, ["blame", "--porcelain", filePath], 30000);
57
+ if (result.exitCode !== 0) {
58
+ throw new Error(`git blame failed: ${result.stderr}`);
59
+ }
60
+ return parseBlameOutput(filePath, result.stdout);
61
+ }
62
+ function parseBlameOutput(filePath, output) {
63
+ const lines = [];
64
+ const commits = new Map;
65
+ const rawLines = output.split(`
66
+ `);
67
+ let i = 0;
68
+ let lineNumber = 0;
69
+ while (i < rawLines.length) {
70
+ const headerLine = rawLines[i];
71
+ if (!headerLine || !headerLine.match(/^[0-9a-f]{40}/)) {
72
+ i++;
73
+ continue;
74
+ }
75
+ const parts = headerLine.split(" ");
76
+ const sha = parts[0];
77
+ const origLine = parseInt(parts[1], 10);
78
+ lineNumber++;
79
+ let author = "";
80
+ let authorTime = new Date;
81
+ i++;
82
+ while (i < rawLines.length && !rawLines[i].startsWith("\t")) {
83
+ const line = rawLines[i];
84
+ if (line.startsWith("author ")) {
85
+ author = line.slice(7);
86
+ } else if (line.startsWith("author-time ")) {
87
+ authorTime = new Date(parseInt(line.slice(12), 10) * 1000);
88
+ }
89
+ i++;
90
+ }
91
+ let content = "";
92
+ if (i < rawLines.length && rawLines[i].startsWith("\t")) {
93
+ content = rawLines[i].slice(1);
94
+ i++;
95
+ }
96
+ if (!commits.has(sha)) {
97
+ commits.set(sha, { author, time: authorTime });
98
+ }
99
+ lines.push({
100
+ lineNumber,
101
+ origLine,
102
+ sha,
103
+ author,
104
+ authorTime,
105
+ content
106
+ });
107
+ }
108
+ return { file: filePath, lines, commits };
109
+ }
110
+
111
+ // src/lib/storage.ts
112
+ import * as fs from "fs";
113
+ import * as path from "path";
114
+ import { spawn as spawn2 } from "child_process";
115
+ async function storeSnapshot(repoRoot, content) {
116
+ return new Promise((resolve, reject) => {
117
+ const proc = spawn2("git", ["hash-object", "-w", "--stdin"], {
118
+ cwd: repoRoot,
119
+ stdio: ["pipe", "pipe", "pipe"]
120
+ });
121
+ let stdout = "";
122
+ let stderr = "";
123
+ proc.stdout.on("data", (data) => {
124
+ stdout += data.toString();
125
+ });
126
+ proc.stderr.on("data", (data) => {
127
+ stderr += data.toString();
128
+ });
129
+ proc.on("close", (code) => {
130
+ if (code === 0) {
131
+ resolve(stdout.trim());
132
+ } else {
133
+ reject(new Error(`git hash-object failed: ${stderr}`));
134
+ }
135
+ });
136
+ proc.on("error", (err) => {
137
+ reject(err);
138
+ });
139
+ proc.stdin.write(content);
140
+ proc.stdin.end();
141
+ });
142
+ }
143
+ async function loadSnapshot(repoRoot, blobSha) {
144
+ return new Promise((resolve, reject) => {
145
+ const proc = spawn2("git", ["cat-file", "blob", blobSha], {
146
+ cwd: repoRoot,
147
+ stdio: ["ignore", "pipe", "pipe"]
148
+ });
149
+ let stdout = "";
150
+ let stderr = "";
151
+ proc.stdout.on("data", (data) => {
152
+ stdout += data.toString();
153
+ });
154
+ proc.stderr.on("data", (data) => {
155
+ stderr += data.toString();
156
+ });
157
+ proc.on("close", (code) => {
158
+ if (code === 0) {
159
+ resolve(stdout);
160
+ } else {
161
+ reject(new Error(`git cat-file failed: ${stderr}`));
162
+ }
163
+ });
164
+ proc.on("error", (err) => {
165
+ reject(err);
166
+ });
167
+ });
168
+ }
169
+ function getAgentBlameGitDir(repoRoot) {
170
+ return path.join(repoRoot, ".git", "agentblame");
171
+ }
172
+ function getWorkingDir(repoRoot, baseSha) {
173
+ return path.join(getAgentBlameGitDir(repoRoot), "working", baseSha);
174
+ }
175
+ function getSnapshotsPath(repoRoot, baseSha) {
176
+ return path.join(getWorkingDir(repoRoot, baseSha), "snapshots.jsonl");
177
+ }
178
+ function getDatabasePath(repoRoot) {
179
+ return path.join(getAgentBlameGitDir(repoRoot), "agentblame.db");
180
+ }
181
+ function ensureAgentBlameDirs(repoRoot) {
182
+ const agentBlameDir = getAgentBlameGitDir(repoRoot);
183
+ const workingDir = path.join(agentBlameDir, "working");
184
+ if (!fs.existsSync(agentBlameDir)) {
185
+ fs.mkdirSync(agentBlameDir, { recursive: true });
186
+ }
187
+ if (!fs.existsSync(workingDir)) {
188
+ fs.mkdirSync(workingDir, { recursive: true });
189
+ }
190
+ }
191
+ function ensureWorkingDir(repoRoot, baseSha) {
192
+ const workingDir = getWorkingDir(repoRoot, baseSha);
193
+ if (!fs.existsSync(workingDir)) {
194
+ fs.mkdirSync(workingDir, { recursive: true });
195
+ }
196
+ }
197
+ function appendToWorkingLog(repoRoot, baseSha, entry) {
198
+ ensureWorkingDir(repoRoot, baseSha);
199
+ const logPath = getSnapshotsPath(repoRoot, baseSha);
200
+ const line = JSON.stringify(entry) + `
201
+ `;
202
+ fs.appendFileSync(logPath, line, "utf8");
203
+ }
204
+ function readWorkingLog(repoRoot, baseSha) {
205
+ const logPath = getSnapshotsPath(repoRoot, baseSha);
206
+ if (!fs.existsSync(logPath)) {
207
+ return [];
208
+ }
209
+ const content = fs.readFileSync(logPath, "utf8");
210
+ const lines = content.trim().split(`
211
+ `).filter(Boolean);
212
+ return lines.map((line) => JSON.parse(line));
213
+ }
214
+ function getSnapshotChainForFile(repoRoot, baseSha, filePath) {
215
+ const entries = readWorkingLog(repoRoot, baseSha);
216
+ return entries.filter((entry) => entry.file === filePath).map((entry) => ({
217
+ timestamp: entry.ts,
218
+ filePath: entry.file,
219
+ blobSha: entry.blob,
220
+ sessionId: entry.session,
221
+ type: entry.type
222
+ }));
223
+ }
224
+ function getModifiedFiles(repoRoot, baseSha) {
225
+ const entries = readWorkingLog(repoRoot, baseSha);
226
+ const files = new Set;
227
+ for (const entry of entries) {
228
+ files.add(entry.file);
229
+ }
230
+ return Array.from(files);
231
+ }
232
+ function cleanupWorkingDir(repoRoot, baseSha) {
233
+ const workingDir = getWorkingDir(repoRoot, baseSha);
234
+ if (fs.existsSync(workingDir)) {
235
+ fs.rmSync(workingDir, { recursive: true });
236
+ }
237
+ }
238
+ function getActiveBaseShas(repoRoot) {
239
+ const workingBaseDir = path.join(getAgentBlameGitDir(repoRoot), "working");
240
+ if (!fs.existsSync(workingBaseDir)) {
241
+ return [];
242
+ }
243
+ try {
244
+ return fs.readdirSync(workingBaseDir).filter((name) => {
245
+ const stat = fs.statSync(path.join(workingBaseDir, name));
246
+ return stat.isDirectory();
247
+ });
248
+ } catch {
249
+ return [];
250
+ }
251
+ }
252
+ async function cleanupStaleWorkingDirs(repoRoot) {
253
+ const baseShas = getActiveBaseShas(repoRoot);
254
+ const cleaned = [];
255
+ const kept = [];
256
+ for (const baseSha of baseShas) {
257
+ const exists = await commitExists(repoRoot, baseSha);
258
+ if (!exists) {
259
+ cleanupWorkingDir(repoRoot, baseSha);
260
+ cleaned.push(baseSha);
261
+ } else {
262
+ kept.push(baseSha);
263
+ }
264
+ }
265
+ return { cleaned, kept };
266
+ }
267
+ async function commitExists(repoRoot, commitSha) {
268
+ return new Promise((resolve) => {
269
+ const proc = spawn2("git", ["cat-file", "-e", commitSha], {
270
+ cwd: repoRoot,
271
+ stdio: ["ignore", "ignore", "ignore"]
272
+ });
273
+ proc.on("close", (code) => {
274
+ resolve(code === 0);
275
+ });
276
+ proc.on("error", () => {
277
+ resolve(false);
278
+ });
279
+ });
280
+ }
281
+ async function getGitHead(repoRoot) {
282
+ return new Promise((resolve) => {
283
+ const proc = spawn2("git", ["rev-parse", "HEAD"], {
284
+ cwd: repoRoot,
285
+ stdio: ["ignore", "pipe", "pipe"]
286
+ });
287
+ let stdout = "";
288
+ proc.stdout.on("data", (data) => {
289
+ stdout += data.toString();
290
+ });
291
+ proc.on("close", (code) => {
292
+ if (code === 0) {
293
+ resolve(stdout.trim());
294
+ } else {
295
+ resolve(null);
296
+ }
297
+ });
298
+ proc.on("error", () => {
299
+ resolve(null);
300
+ });
301
+ });
302
+ }
303
+ async function getParentCommit(repoRoot, commitSha) {
304
+ return new Promise((resolve) => {
305
+ const proc = spawn2("git", ["rev-parse", `${commitSha}^`], {
306
+ cwd: repoRoot,
307
+ stdio: ["ignore", "pipe", "pipe"]
308
+ });
309
+ let stdout = "";
310
+ proc.stdout.on("data", (data) => {
311
+ stdout += data.toString();
312
+ });
313
+ proc.on("close", (code) => {
314
+ if (code === 0) {
315
+ resolve(stdout.trim());
316
+ } else {
317
+ resolve(null);
318
+ }
319
+ });
320
+ proc.on("error", () => {
321
+ resolve(null);
322
+ });
323
+ });
324
+ }
325
+ async function getFileAtCommit(repoRoot, commitSha, filePath) {
326
+ return new Promise((resolve) => {
327
+ const proc = spawn2("git", ["show", `${commitSha}:${filePath}`], {
328
+ cwd: repoRoot,
329
+ stdio: ["ignore", "pipe", "pipe"]
330
+ });
331
+ let stdout = "";
332
+ proc.stdout.on("data", (data) => {
333
+ stdout += data.toString();
334
+ });
335
+ proc.on("close", (code) => {
336
+ if (code === 0) {
337
+ resolve(stdout);
338
+ } else {
339
+ resolve(null);
340
+ }
341
+ });
342
+ proc.on("error", () => {
343
+ resolve(null);
344
+ });
345
+ });
346
+ }
347
+ async function getRootCommit(repoRoot) {
348
+ return new Promise((resolve) => {
349
+ const proc = spawn2("git", ["rev-list", "--max-parents=0", "HEAD"], {
350
+ cwd: repoRoot,
351
+ stdio: ["ignore", "pipe", "pipe"]
352
+ });
353
+ let stdout = "";
354
+ proc.stdout.on("data", (data) => {
355
+ stdout += data.toString();
356
+ });
357
+ proc.on("close", (code) => {
358
+ if (code === 0) {
359
+ const commits = stdout.trim().split(`
360
+ `);
361
+ resolve(commits[0] || null);
362
+ } else {
363
+ resolve(null);
364
+ }
365
+ });
366
+ proc.on("error", () => {
367
+ resolve(null);
368
+ });
369
+ });
370
+ }
371
+ function readFileContent(filePath) {
372
+ try {
373
+ return fs.readFileSync(filePath, "utf8");
374
+ } catch {
375
+ return null;
376
+ }
377
+ }
378
+ async function captureFileSnapshot(repoRoot, baseSha, filePath, content, sessionId, type) {
379
+ const blobSha = await storeSnapshot(repoRoot, content);
380
+ const entry = {
381
+ ts: new Date().toISOString(),
382
+ file: filePath,
383
+ blob: blobSha,
384
+ session: sessionId,
385
+ type
386
+ };
387
+ appendToWorkingLog(repoRoot, baseSha, entry);
388
+ return blobSha;
389
+ }
390
+
391
+ // src/lib/trace.ts
392
+ async function traceFileLines(repoRoot, lines, chain) {
393
+ if (chain.length === 0) {
394
+ return lines.map(() => ({ sessionId: null, confidence: 1 }));
395
+ }
396
+ const snapshotContents = [];
397
+ for (const snapshot of chain) {
398
+ const content = await loadSnapshot(repoRoot, snapshot.blobSha);
399
+ snapshotContents.push(content.split(`
400
+ `));
401
+ }
402
+ const results = [];
403
+ for (let lineIdx = 0;lineIdx < lines.length; lineIdx++) {
404
+ const lineContent = lines[lineIdx];
405
+ let context = {
406
+ prev: lines[lineIdx - 1],
407
+ next: lines[lineIdx + 1]
408
+ };
409
+ let origin = { sessionId: null, confidence: 1 };
410
+ for (let i = chain.length - 1;i >= 0; i--) {
411
+ const snapshotLines = snapshotContents[i];
412
+ const match = findLineWithContext(lineContent, context, snapshotLines);
413
+ if (match === null) {
414
+ const introducer = chain[i + 1];
415
+ origin = {
416
+ sessionId: introducer?.sessionId || null,
417
+ confidence: 1
418
+ };
419
+ break;
420
+ }
421
+ context = {
422
+ prev: snapshotLines[match.index - 1],
423
+ next: snapshotLines[match.index + 1]
424
+ };
425
+ }
426
+ results.push(origin);
427
+ }
428
+ return results;
429
+ }
430
+ function findLineWithContext(target, context, lines) {
431
+ const matches = findExactMatches(target, lines);
432
+ if (matches.length === 0) {
433
+ const normMatches = findNormalizedMatches(target, lines);
434
+ if (normMatches.length === 1) {
435
+ return { index: normMatches[0], confidence: 0.95 };
436
+ }
437
+ if (normMatches.length > 1) {
438
+ const contextMatch2 = findBestContextMatch(normMatches, context, lines);
439
+ if (contextMatch2 !== null) {
440
+ return { index: contextMatch2, confidence: 0.9 };
441
+ }
442
+ }
443
+ return null;
444
+ }
445
+ if (matches.length === 1) {
446
+ return { index: matches[0], confidence: 1 };
447
+ }
448
+ const contextMatch = findBestContextMatch(matches, context, lines);
449
+ if (contextMatch !== null) {
450
+ return { index: contextMatch, confidence: 1 };
451
+ }
452
+ const fuzzyMatch = findFuzzyContextMatch(matches, context, lines);
453
+ if (fuzzyMatch !== null) {
454
+ return { index: fuzzyMatch.index, confidence: fuzzyMatch.confidence };
455
+ }
456
+ return { index: matches[0], confidence: 0.5 };
457
+ }
458
+ function findExactMatches(target, lines) {
459
+ const matches = [];
460
+ for (let i = 0;i < lines.length; i++) {
461
+ if (lines[i] === target) {
462
+ matches.push(i);
463
+ }
464
+ }
465
+ return matches;
466
+ }
467
+ function findNormalizedMatches(target, lines) {
468
+ const normalizedTarget = normalizeWhitespace(target);
469
+ const matches = [];
470
+ for (let i = 0;i < lines.length; i++) {
471
+ if (normalizeWhitespace(lines[i]) === normalizedTarget) {
472
+ matches.push(i);
473
+ }
474
+ }
475
+ return matches;
476
+ }
477
+ function findBestContextMatch(candidates, context, lines) {
478
+ for (const idx of candidates) {
479
+ const prevMatch = !context.prev || lines[idx - 1] === context.prev;
480
+ const nextMatch = !context.next || lines[idx + 1] === context.next;
481
+ if (prevMatch && nextMatch) {
482
+ return idx;
483
+ }
484
+ }
485
+ for (const idx of candidates) {
486
+ const prevMatch = !context.prev || lines[idx - 1] === context.prev;
487
+ const nextMatch = !context.next || lines[idx + 1] === context.next;
488
+ if (prevMatch || nextMatch) {
489
+ return idx;
490
+ }
491
+ }
492
+ return null;
493
+ }
494
+ function findFuzzyContextMatch(candidates, context, lines) {
495
+ let best = null;
496
+ for (const idx of candidates) {
497
+ let score = 0;
498
+ let maxScore = 0;
499
+ if (context.prev !== undefined) {
500
+ score += similarity(lines[idx - 1] || "", context.prev);
501
+ maxScore += 1;
502
+ }
503
+ if (context.next !== undefined) {
504
+ score += similarity(lines[idx + 1] || "", context.next);
505
+ maxScore += 1;
506
+ }
507
+ const normalizedScore = maxScore > 0 ? score / maxScore : 0;
508
+ if (!best || normalizedScore > best.score) {
509
+ best = { index: idx, score: normalizedScore };
510
+ }
511
+ }
512
+ if (best && best.score > 0.5) {
513
+ const confidence = 0.7 + best.score * 0.2;
514
+ return { index: best.index, confidence };
515
+ }
516
+ return null;
517
+ }
518
+ function similarity(a, b) {
519
+ if (a === b)
520
+ return 1;
521
+ if (!a || !b)
522
+ return 0;
523
+ const normA = normalizeWhitespace(a);
524
+ const normB = normalizeWhitespace(b);
525
+ if (normA === normB)
526
+ return 0.95;
527
+ const tokensA = tokenize(normA);
528
+ const tokensB = tokenize(normB);
529
+ if (tokensA.length === 0 && tokensB.length === 0)
530
+ return 1;
531
+ if (tokensA.length === 0 || tokensB.length === 0)
532
+ return 0;
533
+ const setA = new Set(tokensA);
534
+ const setB = new Set(tokensB);
535
+ let intersection = 0;
536
+ for (const token of setA) {
537
+ if (setB.has(token))
538
+ intersection++;
539
+ }
540
+ const union = setA.size + setB.size - intersection;
541
+ return union > 0 ? intersection / union : 0;
542
+ }
543
+ function normalizeWhitespace(s) {
544
+ return s.replace(/\s+/g, " ").trim();
545
+ }
546
+ function tokenize(s) {
547
+ return s.split(/[^a-zA-Z0-9_]+/).filter((t) => t.length > 0).map((t) => t.toLowerCase());
548
+ }
549
+ function aggregateToRanges(attributions) {
550
+ if (attributions.length === 0)
551
+ return [];
552
+ const sorted = [...attributions].sort((a, b) => a.line - b.line);
553
+ const ranges = [];
554
+ let current = {
555
+ startLine: sorted[0].line,
556
+ endLine: sorted[0].line,
557
+ sessionId: sorted[0].sessionId
558
+ };
559
+ for (let i = 1;i < sorted.length; i++) {
560
+ const attr = sorted[i];
561
+ if (attr.line === current.endLine + 1 && attr.sessionId === current.sessionId) {
562
+ current.endLine = attr.line;
563
+ } else {
564
+ ranges.push(current);
565
+ current = {
566
+ startLine: attr.line,
567
+ endLine: attr.line,
568
+ sessionId: attr.sessionId
569
+ };
570
+ }
571
+ }
572
+ ranges.push(current);
573
+ return ranges;
574
+ }
575
+ function separateRanges(ranges) {
576
+ const aiRanges = [];
577
+ const humanRanges = [];
578
+ for (const range of ranges) {
579
+ if (range.sessionId) {
580
+ aiRanges.push({
581
+ startLine: range.startLine,
582
+ endLine: range.endLine,
583
+ sessionId: range.sessionId
584
+ });
585
+ } else {
586
+ humanRanges.push({
587
+ startLine: range.startLine,
588
+ endLine: range.endLine
589
+ });
590
+ }
591
+ }
592
+ return { aiRanges, humanRanges };
593
+ }
594
+ function formatRanges(ranges) {
595
+ return ranges.map((r) => r.startLine === r.endLine ? `${r.startLine}` : `${r.startLine}-${r.endLine}`).join(",");
596
+ }
597
+ function parseRanges(str) {
598
+ if (!str)
599
+ return [];
600
+ return str.split(",").map((part) => {
601
+ const [start, end] = part.split("-").map((n) => parseInt(n, 10));
602
+ return {
603
+ startLine: start,
604
+ endLine: end ?? start
605
+ };
606
+ });
607
+ }
608
+ function buildSessionMap(fileAttributions) {
609
+ const sessionMap = new Map;
610
+ for (const [filePath, attributions] of fileAttributions) {
611
+ for (const { line, sessionId } of attributions) {
612
+ if (!sessionId)
613
+ continue;
614
+ if (!sessionMap.has(sessionId)) {
615
+ sessionMap.set(sessionId, new Map);
616
+ }
617
+ const fileMap = sessionMap.get(sessionId);
618
+ if (!fileMap.has(filePath)) {
619
+ fileMap.set(filePath, []);
620
+ }
621
+ fileMap.get(filePath).push(line);
622
+ }
623
+ }
624
+ return sessionMap;
625
+ }
626
+ function buildHumanMap(fileAttributions) {
627
+ const humanMap = new Map;
628
+ for (const [filePath, attributions] of fileAttributions) {
629
+ const humanLines = [];
630
+ for (const { line, sessionId } of attributions) {
631
+ if (!sessionId) {
632
+ humanLines.push(line);
633
+ }
634
+ }
635
+ if (humanLines.length > 0) {
636
+ humanMap.set(filePath, humanLines);
637
+ }
638
+ }
639
+ return humanMap;
640
+ }
641
+
642
+ // src/lib/git/gitNotes.ts
643
+ var NOTES_REF = "refs/notes/agentblame";
644
+ function buildV3Note(attribution, sessions) {
645
+ const lines = [];
646
+ for (const [filePath, fileAttr] of Object.entries(attribution.files)) {
647
+ lines.push(filePath);
648
+ const sessionRanges = new Map;
649
+ for (const range of fileAttr.aiRanges) {
650
+ if (!sessionRanges.has(range.sessionId)) {
651
+ sessionRanges.set(range.sessionId, []);
652
+ }
653
+ sessionRanges.get(range.sessionId).push([range.startLine, range.endLine]);
654
+ }
655
+ for (const [sessionId, ranges] of sessionRanges) {
656
+ const rangeStr = formatRanges(ranges.map(([start, end]) => ({ startLine: start, endLine: end })));
657
+ lines.push(` ${sessionId} ${rangeStr}`);
658
+ }
659
+ }
660
+ lines.push("---");
661
+ const metadata = {
662
+ v: 3,
663
+ ts: attribution.timestamp,
664
+ sessions,
665
+ h: {}
666
+ };
667
+ for (const [filePath, fileAttr] of Object.entries(attribution.files)) {
668
+ if (fileAttr.humanRanges.length > 0) {
669
+ metadata.h[filePath] = formatRanges(fileAttr.humanRanges);
670
+ }
671
+ }
672
+ lines.push(JSON.stringify(metadata));
673
+ return lines.join(`
674
+ `);
675
+ }
676
+ function parseNote(content) {
677
+ if (!content.trim())
678
+ return null;
679
+ const dividerIndex = content.indexOf(`
680
+ ---
681
+ `);
682
+ if (dividerIndex === -1) {
683
+ try {
684
+ return JSON.parse(content);
685
+ } catch {
686
+ return null;
687
+ }
688
+ }
689
+ const attestation = content.substring(0, dividerIndex);
690
+ const metadataStr = content.substring(dividerIndex + 5).trim();
691
+ try {
692
+ const metadata = JSON.parse(metadataStr);
693
+ return parseV3Note(attestation, metadata);
694
+ } catch {
695
+ return null;
696
+ }
697
+ }
698
+ function parseV3Note(attestation, metadata) {
699
+ const attribution = {
700
+ version: 3,
701
+ timestamp: metadata.ts,
702
+ sessions: metadata.sessions,
703
+ files: {}
704
+ };
705
+ let currentFile = null;
706
+ for (const line of attestation.split(`
707
+ `)) {
708
+ if (!line.trim())
709
+ continue;
710
+ if (!line.startsWith(" ")) {
711
+ currentFile = line.trim();
712
+ attribution.files[currentFile] = {
713
+ aiRanges: [],
714
+ humanRanges: []
715
+ };
716
+ } else if (currentFile) {
717
+ const trimmed = line.trim();
718
+ const spaceIdx = trimmed.indexOf(" ");
719
+ if (spaceIdx > 0) {
720
+ const sessionId = trimmed.substring(0, spaceIdx);
721
+ const rangeStr = trimmed.substring(spaceIdx + 1);
722
+ const ranges = parseRanges(rangeStr);
723
+ for (const range of ranges) {
724
+ attribution.files[currentFile].aiRanges.push({
725
+ sessionId,
726
+ startLine: range.startLine,
727
+ endLine: range.endLine
728
+ });
729
+ }
730
+ }
731
+ }
732
+ }
733
+ for (const [filePath, rangeStr] of Object.entries(metadata.h)) {
734
+ if (!attribution.files[filePath]) {
735
+ attribution.files[filePath] = {
736
+ aiRanges: [],
737
+ humanRanges: []
738
+ };
739
+ }
740
+ attribution.files[filePath].humanRanges = parseRanges(rangeStr);
741
+ }
742
+ return attribution;
743
+ }
744
+ function isV3Note(note) {
745
+ return "version" in note && note.version === 3;
746
+ }
747
+ function convertV2ToV3(note) {
748
+ const v3 = {
749
+ version: 3,
750
+ timestamp: note.timestamp,
751
+ sessions: {},
752
+ files: {}
753
+ };
754
+ const sessionMap = new Map;
755
+ for (const attr of note.attributions) {
756
+ const key = `${attr.provider}:${attr.model || "unknown"}`;
757
+ if (!sessionMap.has(key)) {
758
+ const sessionId2 = `legacy-${sessionMap.size}`;
759
+ sessionMap.set(key, sessionId2);
760
+ v3.sessions[sessionId2] = {
761
+ a: providerToAgent(attr.provider),
762
+ m: attr.model,
763
+ p: null,
764
+ t: note.timestamp,
765
+ tools: []
766
+ };
767
+ }
768
+ const sessionId = sessionMap.get(key);
769
+ if (!v3.files[attr.path]) {
770
+ v3.files[attr.path] = {
771
+ aiRanges: [],
772
+ humanRanges: []
773
+ };
774
+ }
775
+ v3.files[attr.path].aiRanges.push({
776
+ sessionId,
777
+ startLine: attr.startLine,
778
+ endLine: attr.endLine
779
+ });
780
+ }
781
+ return v3;
782
+ }
783
+ function providerToAgent(provider) {
784
+ if (provider === "claudeCode")
785
+ return "claude";
786
+ if (provider === "cursor" || provider === "opencode")
787
+ return provider;
788
+ return "cursor";
789
+ }
790
+ async function attachNoteV3(repoRoot, sha, attribution, sessions) {
791
+ const noteContent = buildV3Note(attribution, sessions);
792
+ const result = await runGit(repoRoot, ["notes", `--ref=${NOTES_REF}`, "add", "-f", "-m", noteContent, sha], 1e4);
793
+ return result.exitCode === 0;
794
+ }
795
+ async function readNoteV3(repoRoot, sha) {
796
+ const result = await runGit(repoRoot, ["notes", `--ref=${NOTES_REF}`, "show", sha], 5000);
797
+ if (result.exitCode !== 0)
798
+ return null;
799
+ const parsed = parseNote(result.stdout.trim());
800
+ if (!parsed)
801
+ return null;
802
+ if (!isV3Note(parsed)) {
803
+ return convertV2ToV3(parsed);
804
+ }
805
+ return parsed;
806
+ }
807
+ async function fetchNotesQuiet(repoRoot, remote = "origin", verbose = false) {
808
+ if (verbose) {
809
+ console.log("Fetching attribution notes from remote...");
810
+ }
811
+ const result = await runGit(repoRoot, ["fetch", remote, `${NOTES_REF}:${NOTES_REF}`], 30000);
812
+ if (result.exitCode === 0) {
813
+ if (verbose) {
814
+ console.log(`Notes fetched successfully.
815
+ `);
816
+ }
817
+ return true;
818
+ }
819
+ if (verbose) {
820
+ console.log(`No remote notes found (this is normal for new repos).
821
+ `);
822
+ }
823
+ return false;
824
+ }
825
+
826
+ // src/blame.ts
827
+ var c = {
828
+ reset: "\x1B[0m",
829
+ bold: "\x1B[1m",
830
+ dim: "\x1B[2m",
831
+ cyan: "\x1B[36m",
832
+ yellow: "\x1B[33m",
833
+ green: "\x1B[32m",
834
+ orange: "\x1B[38;2;184;101;64m",
835
+ blue: "\x1B[34m",
836
+ gray: "\x1B[90m",
837
+ magenta: "\x1B[35m"
838
+ };
839
+ async function blame(filePath, options = {}) {
840
+ const absolutePath = path2.resolve(filePath);
841
+ if (!fs2.existsSync(absolutePath)) {
842
+ console.error(`Error: File not found: ${absolutePath}`);
843
+ process.exit(1);
844
+ }
845
+ const stat = fs2.statSync(absolutePath);
846
+ if (!stat.isFile()) {
847
+ console.error(`Error: Not a file: ${absolutePath}`);
848
+ process.exit(1);
849
+ }
850
+ const repoRoot = await getRepoRoot(path2.dirname(absolutePath));
851
+ if (!repoRoot) {
852
+ console.error("Error: Not in a git repository");
853
+ process.exit(1);
854
+ }
855
+ const relativePath = path2.relative(repoRoot, absolutePath);
856
+ if (relativePath.startsWith("..")) {
857
+ console.error("Error: File is outside the repository");
858
+ process.exit(1);
859
+ }
860
+ await fetchNotesQuiet(repoRoot);
861
+ let blameResult;
862
+ try {
863
+ blameResult = await getBlame(repoRoot, relativePath);
864
+ } catch (err) {
865
+ console.error(`Error: ${err}`);
866
+ process.exit(1);
867
+ }
868
+ const uniqueShas = [...new Set(blameResult.lines.map((l) => l.sha))];
869
+ const notesMap = new Map;
870
+ for (const sha of uniqueShas) {
871
+ const note = await readNoteV3(repoRoot, sha);
872
+ if (note) {
873
+ notesMap.set(sha, note);
874
+ }
875
+ }
876
+ const lineAttributions = blameResult.lines.map((line) => {
877
+ const note = notesMap.get(line.sha);
878
+ if (!note) {
879
+ return { line, sessionId: null, session: null };
880
+ }
881
+ const fileAttr = note.files[relativePath];
882
+ if (!fileAttr) {
883
+ const matchingPath = Object.keys(note.files).find((p) => p === relativePath || p.endsWith(relativePath) || relativePath.endsWith(p));
884
+ if (!matchingPath) {
885
+ return { line, sessionId: null, session: null };
886
+ }
887
+ const altFileAttr = note.files[matchingPath];
888
+ const aiRange2 = altFileAttr.aiRanges.find((r) => line.origLine >= r.startLine && line.origLine <= r.endLine);
889
+ if (aiRange2) {
890
+ return {
891
+ line,
892
+ sessionId: aiRange2.sessionId,
893
+ session: note.sessions[aiRange2.sessionId] || null
894
+ };
895
+ }
896
+ return { line, sessionId: null, session: null };
897
+ }
898
+ const aiRange = fileAttr.aiRanges.find((r) => line.origLine >= r.startLine && line.origLine <= r.endLine);
899
+ if (aiRange) {
900
+ return {
901
+ line,
902
+ sessionId: aiRange.sessionId,
903
+ session: note.sessions[aiRange.sessionId] || null
904
+ };
905
+ }
906
+ return { line, sessionId: null, session: null };
907
+ });
908
+ const uniqueSessions = new Map;
909
+ for (const { sessionId, session } of lineAttributions) {
910
+ if (sessionId && session && !uniqueSessions.has(sessionId)) {
911
+ uniqueSessions.set(sessionId, session);
912
+ }
913
+ }
914
+ if (options.json) {
915
+ outputJson(lineAttributions, relativePath, uniqueSessions);
916
+ } else if (options.summary) {
917
+ outputSummary(lineAttributions, relativePath, uniqueSessions);
918
+ } else {
919
+ outputFormatted(lineAttributions, relativePath, uniqueSessions, options.showPrompts);
920
+ }
921
+ }
922
+ function outputFormatted(lines, filePath, sessions, showPrompts = true) {
923
+ console.log("");
924
+ console.log(` ${c.bold}${c.cyan}${filePath}${c.reset}`);
925
+ console.log(` ${c.dim}${"\u2500".repeat(70)}${c.reset}`);
926
+ if (showPrompts && sessions.size > 0) {
927
+ console.log(` ${c.bold}Sessions:${c.reset}`);
928
+ for (const [sessionId, session] of sessions) {
929
+ const agent = session.a === "cursor" ? "Cursor" : session.a === "opencode" ? "OpenCode" : "Claude";
930
+ const model = session.m || "";
931
+ const modelStr = model ? ` - ${model}` : "";
932
+ console.log(` ${c.orange}${sessionId.slice(0, 8)}${c.reset} ${c.dim}[${agent}${modelStr}]${c.reset}`);
933
+ if (session.p) {
934
+ const prompt = session.p.length > 80 ? session.p.substring(0, 80) + "..." : session.p;
935
+ console.log(` ${c.dim}"${prompt}"${c.reset}`);
936
+ }
937
+ if (session.tools.length > 0) {
938
+ console.log(` ${c.dim}Tools: ${session.tools.join(", ")}${c.reset}`);
939
+ }
940
+ }
941
+ console.log(` ${c.dim}${"\u2500".repeat(70)}${c.reset}`);
942
+ }
943
+ const maxLineNum = lines.length.toString().length;
944
+ for (const { line, sessionId, session } of lines) {
945
+ const sha = `${c.yellow}${line.sha.slice(0, 7)}${c.reset}`;
946
+ const author = `${c.blue}${line.author.slice(0, 12).padEnd(12)}${c.reset}`;
947
+ const date = `${c.dim}${formatDate(line.authorTime)}${c.reset}`;
948
+ const lineNum = `${c.dim}${line.lineNumber.toString().padStart(maxLineNum)}${c.reset}`;
949
+ const ATTR_WIDTH = 24;
950
+ let attrInfo = "";
951
+ let visibleLen = 0;
952
+ if (sessionId && session) {
953
+ const agent = session.a === "cursor" ? "Cursor" : session.a === "opencode" ? "OpenCode" : "Claude";
954
+ const label = `${sessionId.slice(0, 8)} ${agent}`;
955
+ visibleLen = label.length + 3;
956
+ attrInfo = `${c.orange}\u2728 ${sessionId.slice(0, 8)} ${c.dim}${agent}${c.reset}`;
957
+ }
958
+ const attrPadded = sessionId ? attrInfo + " ".repeat(Math.max(0, ATTR_WIDTH - visibleLen)) : " ".repeat(ATTR_WIDTH);
959
+ console.log(` ${sha} ${author} ${date} ${attrPadded} ${c.dim}\u2502${c.reset} ${lineNum} ${c.dim}\u2502${c.reset} ${line.content}`);
960
+ }
961
+ const nonEmptyLines = filterNonEmptyLines(lines);
962
+ const aiGenerated = nonEmptyLines.filter((l) => l.sessionId !== null).length;
963
+ const human = nonEmptyLines.length - aiGenerated;
964
+ const aiPct = nonEmptyLines.length > 0 ? Math.round(aiGenerated / nonEmptyLines.length * 100) : 0;
965
+ const humanPct = 100 - aiPct;
966
+ const barWidth = 40;
967
+ const aiBarWidth = Math.round(aiPct / 100 * barWidth);
968
+ const humanBarWidth = barWidth - aiBarWidth;
969
+ const aiBar = `${c.orange}${"\u2588".repeat(aiBarWidth)}${c.reset}`;
970
+ const humanBar = `${c.dim}${"\u2591".repeat(humanBarWidth)}${c.reset}`;
971
+ console.log(` ${c.dim}${"\u2500".repeat(70)}${c.reset}`);
972
+ console.log(` ${aiBar}${humanBar}`);
973
+ console.log(` ${c.orange}\u2728 AI: ${aiGenerated} (${aiPct}%)${c.reset} ${c.dim}\u2502${c.reset} ${c.green}\uD83D\uDC64 Human: ${human} (${humanPct}%)${c.reset}`);
974
+ console.log("");
975
+ }
976
+ function outputSummary(lines, filePath, sessions) {
977
+ const nonEmptyLines = filterNonEmptyLines(lines);
978
+ const aiGenerated = nonEmptyLines.filter((l) => l.sessionId !== null).length;
979
+ const human = nonEmptyLines.length - aiGenerated;
980
+ console.log(`
981
+ ${filePath}:`);
982
+ console.log(` Total lines: ${nonEmptyLines.length}`);
983
+ console.log(` AI-generated: ${aiGenerated} (${pct(aiGenerated, nonEmptyLines.length)})`);
984
+ console.log(` Human: ${human} (${pct(human, nonEmptyLines.length)})`);
985
+ console.log("");
986
+ const sessionCounts = new Map;
987
+ for (const { sessionId } of nonEmptyLines) {
988
+ if (sessionId) {
989
+ sessionCounts.set(sessionId, (sessionCounts.get(sessionId) || 0) + 1);
990
+ }
991
+ }
992
+ if (sessionCounts.size > 0) {
993
+ console.log(" By session:");
994
+ for (const [sessionId, count] of sessionCounts) {
995
+ const session = sessions.get(sessionId);
996
+ const agent = session?.a || "unknown";
997
+ const model = session?.m || "";
998
+ console.log(` ${sessionId.slice(0, 8)} [${agent}${model ? ` - ${model}` : ""}]: ${count} lines`);
999
+ }
1000
+ console.log("");
1001
+ }
1002
+ const agentCounts = new Map;
1003
+ for (const { session } of nonEmptyLines) {
1004
+ if (session) {
1005
+ const agent = session.a;
1006
+ agentCounts.set(agent, (agentCounts.get(agent) || 0) + 1);
1007
+ }
1008
+ }
1009
+ if (agentCounts.size > 0) {
1010
+ console.log(" By agent:");
1011
+ for (const [agent, count] of agentCounts) {
1012
+ console.log(` ${agent}: ${count} lines`);
1013
+ }
1014
+ console.log("");
1015
+ }
1016
+ if (sessions.size > 0) {
1017
+ console.log(" Prompts:");
1018
+ for (const [sessionId, session] of sessions) {
1019
+ if (session.p) {
1020
+ const truncated = session.p.length > 60 ? session.p.substring(0, 60) + "..." : session.p;
1021
+ console.log(` ${sessionId.slice(0, 8)}: "${truncated}"`);
1022
+ }
1023
+ }
1024
+ console.log("");
1025
+ }
1026
+ }
1027
+ function outputJson(lines, filePath, sessions) {
1028
+ const nonEmptyLines = filterNonEmptyLines(lines);
1029
+ const output = {
1030
+ file: filePath,
1031
+ sessions: Object.fromEntries(Array.from(sessions.entries()).map(([id, session]) => [
1032
+ id,
1033
+ {
1034
+ agent: session.a,
1035
+ model: session.m,
1036
+ prompt: session.p,
1037
+ timestamp: session.t,
1038
+ tools: session.tools
1039
+ }
1040
+ ])),
1041
+ lines: lines.map(({ line, sessionId, session }) => ({
1042
+ lineNumber: line.lineNumber,
1043
+ sha: line.sha,
1044
+ author: line.author,
1045
+ date: line.authorTime.toISOString(),
1046
+ content: line.content,
1047
+ attribution: sessionId ? {
1048
+ sessionId,
1049
+ agent: session?.a || null,
1050
+ model: session?.m || null,
1051
+ prompt: session?.p || null
1052
+ } : null
1053
+ })),
1054
+ summary: {
1055
+ total: nonEmptyLines.length,
1056
+ aiGenerated: nonEmptyLines.filter((l) => l.sessionId !== null).length,
1057
+ human: nonEmptyLines.filter((l) => l.sessionId === null).length
1058
+ }
1059
+ };
1060
+ console.log(JSON.stringify(output, null, 2));
1061
+ }
1062
+ function formatDate(date) {
1063
+ return date.toISOString().slice(0, 10);
1064
+ }
1065
+ function pct(n, total) {
1066
+ if (total === 0)
1067
+ return "0%";
1068
+ return `${Math.round(n / total * 100)}%`;
1069
+ }
1070
+ function isEmptyLine(line) {
1071
+ return line.line.content.trim() === "";
1072
+ }
1073
+ function filterNonEmptyLines(lines) {
1074
+ return lines.filter((l) => !isEmptyLine(l));
1075
+ }
1076
+
1077
+ // src/sync.ts
1078
+ import { execSync, spawnSync } from "child_process";
1079
+ // src/lib/database.ts
1080
+ import { Database } from "bun:sqlite";
1081
+ import * as fs3 from "fs";
1082
+ import * as path3 from "path";
1083
+ import { createHash } from "crypto";
1084
+ var SCHEMA_V3 = `
1085
+ -- Sessions: One per AI conversation
1086
+ CREATE TABLE IF NOT EXISTS sessions (
1087
+ id TEXT PRIMARY KEY, -- SHA256(agent:conversation_id)[0:16]
1088
+ agent TEXT NOT NULL, -- 'cursor' | 'claude' | 'opencode'
1089
+ model TEXT,
1090
+ conversation_id TEXT, -- Original ID from agent
1091
+ created_at TEXT NOT NULL,
1092
+ first_commit_sha TEXT, -- First commit using this session
1093
+ first_commit_at TEXT
1094
+ );
1095
+
1096
+ -- Prompts: User messages that triggered AI actions
1097
+ CREATE TABLE IF NOT EXISTS prompts (
1098
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
1099
+ session_id TEXT NOT NULL,
1100
+ content TEXT NOT NULL, -- The actual prompt text
1101
+ timestamp TEXT NOT NULL,
1102
+ FOREIGN KEY (session_id) REFERENCES sessions(id)
1103
+ );
1104
+
1105
+ -- Tool Calls: What the AI did
1106
+ CREATE TABLE IF NOT EXISTS tool_calls (
1107
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
1108
+ session_id TEXT NOT NULL,
1109
+ tool_name TEXT NOT NULL, -- 'Edit', 'Write', 'MultiEdit'
1110
+ tool_input TEXT, -- JSON of tool arguments
1111
+ file_path TEXT,
1112
+ timestamp TEXT NOT NULL,
1113
+ before_blob TEXT, -- Git blob SHA of file before
1114
+ after_blob TEXT, -- Git blob SHA of file after
1115
+ FOREIGN KEY (session_id) REFERENCES sessions(id)
1116
+ );
1117
+
1118
+ -- Indexes
1119
+ CREATE INDEX IF NOT EXISTS idx_sessions_agent ON sessions(agent, conversation_id);
1120
+ CREATE INDEX IF NOT EXISTS idx_prompts_session ON prompts(session_id);
1121
+ CREATE INDEX IF NOT EXISTS idx_tool_calls_session ON tool_calls(session_id);
1122
+ CREATE INDEX IF NOT EXISTS idx_tool_calls_file ON tool_calls(file_path);
1123
+ CREATE INDEX IF NOT EXISTS idx_tool_calls_timestamp ON tool_calls(timestamp);
1124
+ `;
1125
+ var dbInstance = null;
1126
+ var currentDbPath = null;
1127
+ function setDatabasePath(dbPath) {
1128
+ if (currentDbPath !== dbPath) {
1129
+ if (dbInstance) {
1130
+ dbInstance.close();
1131
+ dbInstance = null;
1132
+ }
1133
+ currentDbPath = dbPath;
1134
+ }
1135
+ }
1136
+ function getDbPath() {
1137
+ if (!currentDbPath) {
1138
+ throw new Error("Database path not set. Call setDatabasePath() or setAgentBlameDir() first.");
1139
+ }
1140
+ return currentDbPath;
1141
+ }
1142
+ function getDatabase() {
1143
+ if (dbInstance) {
1144
+ return dbInstance;
1145
+ }
1146
+ const dbPath = getDbPath();
1147
+ const dbDir = path3.dirname(dbPath);
1148
+ if (!fs3.existsSync(dbDir)) {
1149
+ fs3.mkdirSync(dbDir, { recursive: true });
1150
+ }
1151
+ dbInstance = new Database(dbPath);
1152
+ dbInstance.exec("PRAGMA foreign_keys = ON");
1153
+ dbInstance.exec("PRAGMA journal_mode = WAL");
1154
+ dbInstance.exec("PRAGMA busy_timeout = 5000");
1155
+ dbInstance.exec(SCHEMA_V3);
1156
+ return dbInstance;
1157
+ }
1158
+ function initDatabase() {
1159
+ const db = getDatabase();
1160
+ db.exec("SELECT 1");
1161
+ }
1162
+ function resetDatabase() {
1163
+ const db = getDatabase();
1164
+ db.exec("DROP TABLE IF EXISTS lines");
1165
+ db.exec("DROP TABLE IF EXISTS edits");
1166
+ db.exec("DROP TABLE IF EXISTS tool_calls");
1167
+ db.exec("DROP TABLE IF EXISTS prompts");
1168
+ db.exec("DROP TABLE IF EXISTS sessions");
1169
+ db.exec(SCHEMA_V3);
1170
+ }
1171
+ function upsertSession(params) {
1172
+ const db = getDatabase();
1173
+ const stmt = db.prepare(`
1174
+ INSERT INTO sessions (id, agent, model, conversation_id, created_at)
1175
+ VALUES (?, ?, ?, ?, ?)
1176
+ ON CONFLICT(id) DO UPDATE SET
1177
+ model = COALESCE(excluded.model, sessions.model)
1178
+ `);
1179
+ stmt.run(params.id, params.agent, params.model ?? null, params.conversationId ?? null, new Date().toISOString());
1180
+ }
1181
+ function getSession(sessionId) {
1182
+ const db = getDatabase();
1183
+ const stmt = db.prepare("SELECT * FROM sessions WHERE id = ?");
1184
+ const row = stmt.get(sessionId);
1185
+ if (!row)
1186
+ return null;
1187
+ return {
1188
+ id: row.id,
1189
+ agent: row.agent,
1190
+ model: row.model,
1191
+ conversationId: row.conversation_id,
1192
+ createdAt: row.created_at,
1193
+ firstCommitSha: row.first_commit_sha,
1194
+ firstCommitAt: row.first_commit_at
1195
+ };
1196
+ }
1197
+ function updateSessionFirstCommit(sessionId, commitSha) {
1198
+ const db = getDatabase();
1199
+ const stmt = db.prepare(`
1200
+ UPDATE sessions
1201
+ SET first_commit_sha = COALESCE(first_commit_sha, ?),
1202
+ first_commit_at = COALESCE(first_commit_at, ?)
1203
+ WHERE id = ?
1204
+ `);
1205
+ stmt.run(commitSha, new Date().toISOString(), sessionId);
1206
+ }
1207
+ function insertPrompt(params) {
1208
+ const db = getDatabase();
1209
+ const stmt = db.prepare(`
1210
+ INSERT INTO prompts (session_id, content, timestamp)
1211
+ VALUES (?, ?, ?)
1212
+ `);
1213
+ const result = stmt.run(params.sessionId, params.content, params.timestamp ?? new Date().toISOString());
1214
+ return Number(result.lastInsertRowid);
1215
+ }
1216
+ function getLatestPromptForSession(sessionId) {
1217
+ const db = getDatabase();
1218
+ const stmt = db.prepare(`
1219
+ SELECT * FROM prompts
1220
+ WHERE session_id = ?
1221
+ ORDER BY timestamp DESC
1222
+ LIMIT 1
1223
+ `);
1224
+ const row = stmt.get(sessionId);
1225
+ return row ? rowToPrompt(row) : null;
1226
+ }
1227
+ function promptExists(sessionId, content) {
1228
+ const db = getDatabase();
1229
+ const stmt = db.prepare(`
1230
+ SELECT 1 FROM prompts
1231
+ WHERE session_id = ? AND content = ?
1232
+ LIMIT 1
1233
+ `);
1234
+ return stmt.get(sessionId, content) !== undefined;
1235
+ }
1236
+ function insertToolCall(params) {
1237
+ const db = getDatabase();
1238
+ const stmt = db.prepare(`
1239
+ INSERT INTO tool_calls (
1240
+ session_id, tool_name, tool_input, file_path, timestamp, before_blob, after_blob
1241
+ ) VALUES (?, ?, ?, ?, ?, ?, ?)
1242
+ `);
1243
+ const result = stmt.run(params.sessionId, params.toolName, params.toolInput ?? null, params.filePath ?? null, params.timestamp ?? new Date().toISOString(), params.beforeBlob ?? null, params.afterBlob ?? null);
1244
+ return Number(result.lastInsertRowid);
1245
+ }
1246
+ function getToolNamesForSession(sessionId) {
1247
+ const db = getDatabase();
1248
+ const stmt = db.prepare(`
1249
+ SELECT DISTINCT tool_name FROM tool_calls
1250
+ WHERE session_id = ?
1251
+ ORDER BY tool_name
1252
+ `);
1253
+ const rows = stmt.all(sessionId);
1254
+ return rows.map((r) => r.tool_name);
1255
+ }
1256
+ function cleanupOldEntries(maxAgeDays = 30) {
1257
+ const db = getDatabase();
1258
+ const beforeCount = db.prepare("SELECT COUNT(*) as count FROM sessions").get().count;
1259
+ db.prepare(`
1260
+ DELETE FROM sessions
1261
+ WHERE first_commit_sha IS NULL
1262
+ AND datetime(created_at) < datetime('now', '-' || ? || ' days')
1263
+ `).run(maxAgeDays);
1264
+ const afterCount = db.prepare("SELECT COUNT(*) as count FROM sessions").get().count;
1265
+ return {
1266
+ removed: beforeCount - afterCount,
1267
+ kept: afterCount
1268
+ };
1269
+ }
1270
+ function getRecentSessions(limit = 5) {
1271
+ const db = getDatabase();
1272
+ const stmt = db.prepare(`
1273
+ SELECT * FROM sessions
1274
+ ORDER BY created_at DESC
1275
+ LIMIT ?
1276
+ `);
1277
+ const rows = stmt.all(limit);
1278
+ return rows.map(rowToSession);
1279
+ }
1280
+ function getStats() {
1281
+ const db = getDatabase();
1282
+ const sessions = db.prepare("SELECT COUNT(*) as count FROM sessions").get().count;
1283
+ const prompts = db.prepare("SELECT COUNT(*) as count FROM prompts").get().count;
1284
+ const toolCalls = db.prepare("SELECT COUNT(*) as count FROM tool_calls").get().count;
1285
+ return { sessions, prompts, toolCalls };
1286
+ }
1287
+ function rowToSession(row) {
1288
+ return {
1289
+ id: row.id,
1290
+ agent: row.agent,
1291
+ model: row.model,
1292
+ conversationId: row.conversation_id,
1293
+ createdAt: row.created_at,
1294
+ firstCommitSha: row.first_commit_sha,
1295
+ firstCommitAt: row.first_commit_at
1296
+ };
1297
+ }
1298
+ function rowToPrompt(row) {
1299
+ return {
1300
+ id: row.id,
1301
+ sessionId: row.session_id,
1302
+ content: row.content,
1303
+ timestamp: row.timestamp
1304
+ };
1305
+ }
1306
+ function generateSessionId(agent, conversationId) {
1307
+ const hash = createHash("sha256");
1308
+ hash.update(`${agent}:${conversationId}`);
1309
+ return hash.digest("hex").substring(0, 16);
1310
+ }
1311
+ // src/lib/watcher.ts
1312
+ import { watch } from "fs";
1313
+ import * as fs4 from "fs";
1314
+ import * as path4 from "path";
1315
+ class FileWatcher {
1316
+ watcher = null;
1317
+ repoRoot;
1318
+ aiEditInProgress = new Set;
1319
+ debounceTimers = new Map;
1320
+ debounceMs;
1321
+ lastCapturedBlobs = new Map;
1322
+ constructor(repoRoot, options) {
1323
+ this.repoRoot = repoRoot;
1324
+ this.debounceMs = options?.debounceMs ?? 500;
1325
+ }
1326
+ start() {
1327
+ if (this.watcher) {
1328
+ return;
1329
+ }
1330
+ try {
1331
+ this.watcher = watch(this.repoRoot, { recursive: true }, (event, filename) => {
1332
+ if (!filename)
1333
+ return;
1334
+ if (this.shouldIgnore(filename))
1335
+ return;
1336
+ this.debounce(filename, async () => {
1337
+ await this.handleFileChange(filename);
1338
+ });
1339
+ });
1340
+ this.watcher.on("error", (err) => {
1341
+ console.error("[agentblame] Watcher error:", err.message);
1342
+ });
1343
+ } catch (err) {
1344
+ console.error("[agentblame] Failed to start watcher:", err);
1345
+ }
1346
+ }
1347
+ stop() {
1348
+ if (this.watcher) {
1349
+ this.watcher.close();
1350
+ this.watcher = null;
1351
+ }
1352
+ for (const timer of this.debounceTimers.values()) {
1353
+ clearTimeout(timer);
1354
+ }
1355
+ this.debounceTimers.clear();
1356
+ }
1357
+ markAIEditStart(filePath) {
1358
+ const normalized = this.normalizePath(filePath);
1359
+ this.aiEditInProgress.add(normalized);
1360
+ }
1361
+ markAIEditEnd(filePath) {
1362
+ const normalized = this.normalizePath(filePath);
1363
+ this.aiEditInProgress.delete(normalized);
1364
+ }
1365
+ isAIEditInProgress(filePath) {
1366
+ const normalized = this.normalizePath(filePath);
1367
+ return this.aiEditInProgress.has(normalized);
1368
+ }
1369
+ async handleFileChange(relativePath) {
1370
+ const absolutePath = path4.join(this.repoRoot, relativePath);
1371
+ const normalized = this.normalizePath(relativePath);
1372
+ if (this.aiEditInProgress.has(normalized)) {
1373
+ return;
1374
+ }
1375
+ if (!fs4.existsSync(absolutePath)) {
1376
+ return;
1377
+ }
1378
+ try {
1379
+ const stat = fs4.statSync(absolutePath);
1380
+ if (stat.isDirectory())
1381
+ return;
1382
+ } catch {
1383
+ return;
1384
+ }
1385
+ const content = readFileContent(absolutePath);
1386
+ if (content === null)
1387
+ return;
1388
+ const baseSha = await getGitHead(this.repoRoot);
1389
+ if (!baseSha)
1390
+ return;
1391
+ try {
1392
+ const blobSha = await captureFileSnapshot(this.repoRoot, baseSha, relativePath, content, null, "human_edit");
1393
+ this.lastCapturedBlobs.set(normalized, blobSha);
1394
+ } catch (err) {
1395
+ if (process.env.AGENTBLAME_DEBUG) {
1396
+ console.error("[agentblame] Failed to capture human edit:", err);
1397
+ }
1398
+ }
1399
+ }
1400
+ shouldIgnore(relativePath) {
1401
+ if (relativePath.startsWith(".git"))
1402
+ return true;
1403
+ if (relativePath.includes("/.git/"))
1404
+ return true;
1405
+ if (relativePath.includes("node_modules"))
1406
+ return true;
1407
+ if (relativePath.includes("/dist/"))
1408
+ return true;
1409
+ if (relativePath.includes("/build/"))
1410
+ return true;
1411
+ if (relativePath.includes("/.next/"))
1412
+ return true;
1413
+ if (relativePath.includes(".agentblame"))
1414
+ return true;
1415
+ if (relativePath.endsWith(".lock"))
1416
+ return true;
1417
+ if (relativePath.endsWith("-lock.json"))
1418
+ return true;
1419
+ if (relativePath.endsWith(".log"))
1420
+ return true;
1421
+ const binaryExtensions = [
1422
+ ".png",
1423
+ ".jpg",
1424
+ ".jpeg",
1425
+ ".gif",
1426
+ ".ico",
1427
+ ".webp",
1428
+ ".mp3",
1429
+ ".mp4",
1430
+ ".wav",
1431
+ ".avi",
1432
+ ".mov",
1433
+ ".pdf",
1434
+ ".zip",
1435
+ ".tar",
1436
+ ".gz",
1437
+ ".rar",
1438
+ ".7z",
1439
+ ".exe",
1440
+ ".dll",
1441
+ ".so",
1442
+ ".dylib",
1443
+ ".wasm",
1444
+ ".ttf",
1445
+ ".woff",
1446
+ ".woff2",
1447
+ ".eot"
1448
+ ];
1449
+ const ext = path4.extname(relativePath).toLowerCase();
1450
+ if (binaryExtensions.includes(ext))
1451
+ return true;
1452
+ return false;
1453
+ }
1454
+ debounce(key, fn) {
1455
+ const existing = this.debounceTimers.get(key);
1456
+ if (existing) {
1457
+ clearTimeout(existing);
1458
+ }
1459
+ const timer = setTimeout(async () => {
1460
+ this.debounceTimers.delete(key);
1461
+ try {
1462
+ await fn();
1463
+ } catch (err) {
1464
+ if (process.env.AGENTBLAME_DEBUG) {
1465
+ console.error("[agentblame] Debounced function error:", err);
1466
+ }
1467
+ }
1468
+ }, this.debounceMs);
1469
+ this.debounceTimers.set(key, timer);
1470
+ }
1471
+ normalizePath(filePath) {
1472
+ return filePath.replace(/^\.?\//, "").replace(/\\/g, "/");
1473
+ }
1474
+ }
1475
+ var globalWatcher = null;
1476
+ function markAIEditStart(filePath) {
1477
+ if (globalWatcher) {
1478
+ globalWatcher.markAIEditStart(filePath);
1479
+ }
1480
+ }
1481
+ function markAIEditEnd(filePath) {
1482
+ if (globalWatcher) {
1483
+ globalWatcher.markAIEditEnd(filePath);
1484
+ }
1485
+ }
1486
+ // src/lib/analytics.ts
1487
+ import { spawn as spawn3 } from "child_process";
1488
+ var ANALYTICS_REF = "refs/notes/agentblame-analytics";
1489
+ async function getAnalyticsAnchor(repoRoot) {
1490
+ return getRootCommit(repoRoot);
1491
+ }
1492
+ async function readAnalyticsNote(repoRoot) {
1493
+ const anchor = await getAnalyticsAnchor(repoRoot);
1494
+ if (!anchor)
1495
+ return null;
1496
+ return new Promise((resolve2) => {
1497
+ const proc = spawn3("git", ["notes", "--ref", ANALYTICS_REF, "show", anchor], {
1498
+ cwd: repoRoot,
1499
+ stdio: ["ignore", "pipe", "pipe"]
1500
+ });
1501
+ let stdout = "";
1502
+ proc.stdout.on("data", (data) => {
1503
+ stdout += data.toString();
1504
+ });
1505
+ proc.on("close", (code) => {
1506
+ if (code === 0 && stdout.trim()) {
1507
+ try {
1508
+ const note = JSON.parse(stdout);
1509
+ resolve2(note);
1510
+ } catch {
1511
+ resolve2(null);
1512
+ }
1513
+ } else {
1514
+ resolve2(null);
1515
+ }
1516
+ });
1517
+ proc.on("error", () => {
1518
+ resolve2(null);
1519
+ });
1520
+ });
1521
+ }
1522
+ async function writeAnalyticsNote(repoRoot, analytics) {
1523
+ const anchor = await getAnalyticsAnchor(repoRoot);
1524
+ if (!anchor)
1525
+ return false;
1526
+ const content = JSON.stringify(analytics, null, 2);
1527
+ return new Promise((resolve2) => {
1528
+ const proc = spawn3("git", ["notes", "--ref", ANALYTICS_REF, "add", "-f", "-m", content, anchor], {
1529
+ cwd: repoRoot,
1530
+ stdio: ["ignore", "ignore", "pipe"]
1531
+ });
1532
+ proc.on("close", (code) => {
1533
+ resolve2(code === 0);
1534
+ });
1535
+ proc.on("error", () => {
1536
+ resolve2(false);
1537
+ });
1538
+ });
1539
+ }
1540
+ function computeCommitStats(sessions, fileAttributions) {
1541
+ const stats = {
1542
+ aiLines: 0,
1543
+ humanLines: 0,
1544
+ byAgent: {},
1545
+ byModel: {},
1546
+ sessions: []
1547
+ };
1548
+ const sessionIds = new Set;
1549
+ for (const file of Object.values(fileAttributions)) {
1550
+ for (const range of file.aiRanges) {
1551
+ const lineCount = range.endLine - range.startLine + 1;
1552
+ stats.aiLines += lineCount;
1553
+ const session = sessions[range.sessionId];
1554
+ if (session) {
1555
+ sessionIds.add(range.sessionId);
1556
+ const agent = session.a;
1557
+ stats.byAgent[agent] = (stats.byAgent[agent] || 0) + lineCount;
1558
+ if (session.m) {
1559
+ stats.byModel[session.m] = (stats.byModel[session.m] || 0) + lineCount;
1560
+ }
1561
+ }
1562
+ }
1563
+ for (const range of file.humanRanges) {
1564
+ stats.humanLines += range.endLine - range.startLine + 1;
1565
+ }
1566
+ }
1567
+ stats.sessions = Array.from(sessionIds);
1568
+ return stats;
1569
+ }
1570
+ function mergeAnalytics(existing, commitStats, commitAuthor) {
1571
+ const base = existing || {
1572
+ v: 1,
1573
+ updated: new Date().toISOString(),
1574
+ summary: {
1575
+ totalLines: 0,
1576
+ aiLines: 0,
1577
+ humanLines: 0,
1578
+ byAgent: {},
1579
+ byModel: {}
1580
+ },
1581
+ contributors: {},
1582
+ recentPRs: []
1583
+ };
1584
+ base.summary.aiLines += commitStats.aiLines;
1585
+ base.summary.humanLines += commitStats.humanLines;
1586
+ base.summary.totalLines = base.summary.aiLines + base.summary.humanLines;
1587
+ for (const [agent, count] of Object.entries(commitStats.byAgent)) {
1588
+ const key = agent;
1589
+ base.summary.byAgent[key] = (base.summary.byAgent[key] || 0) + count;
1590
+ }
1591
+ for (const [model, count] of Object.entries(commitStats.byModel)) {
1592
+ base.summary.byModel[model] = (base.summary.byModel[model] || 0) + count;
1593
+ }
1594
+ if (commitAuthor) {
1595
+ if (!base.contributors[commitAuthor]) {
1596
+ base.contributors[commitAuthor] = {
1597
+ commits: 0,
1598
+ aiLines: 0,
1599
+ humanLines: 0,
1600
+ topModels: []
1601
+ };
1602
+ }
1603
+ const contributor = base.contributors[commitAuthor];
1604
+ contributor.commits += 1;
1605
+ contributor.aiLines += commitStats.aiLines;
1606
+ contributor.humanLines += commitStats.humanLines;
1607
+ const modelCounts = new Map;
1608
+ for (const model of contributor.topModels) {
1609
+ modelCounts.set(model, (modelCounts.get(model) || 0) + 1);
1610
+ }
1611
+ for (const [model, count] of Object.entries(commitStats.byModel)) {
1612
+ modelCounts.set(model, (modelCounts.get(model) || 0) + count);
1613
+ }
1614
+ contributor.topModels = Array.from(modelCounts.entries()).sort((a, b) => b[1] - a[1]).slice(0, 3).map(([model]) => model);
1615
+ }
1616
+ base.updated = new Date().toISOString();
1617
+ return base;
1618
+ }
1619
+ async function updateAnalytics(repoRoot, commitStats, commitAuthor) {
1620
+ try {
1621
+ const existing = await readAnalyticsNote(repoRoot);
1622
+ const updated = mergeAnalytics(existing, commitStats, commitAuthor);
1623
+ return await writeAnalyticsNote(repoRoot, updated);
1624
+ } catch (err) {
1625
+ if (process.env.AGENTBLAME_DEBUG) {
1626
+ console.error("[agentblame] Failed to update analytics:", err);
1627
+ }
1628
+ return false;
1629
+ }
1630
+ }
1631
+ async function initAnalytics(repoRoot) {
1632
+ const existing = await readAnalyticsNote(repoRoot);
1633
+ if (!existing) {
1634
+ const initial = {
1635
+ v: 1,
1636
+ updated: new Date().toISOString(),
1637
+ summary: {
1638
+ totalLines: 0,
1639
+ aiLines: 0,
1640
+ humanLines: 0,
1641
+ byAgent: {},
1642
+ byModel: {}
1643
+ },
1644
+ contributors: {},
1645
+ recentPRs: []
1646
+ };
1647
+ return await writeAnalyticsNote(repoRoot, initial);
1648
+ }
1649
+ return true;
1650
+ }
1651
+ // src/lib/hooks.ts
1652
+ import * as fs5 from "fs";
1653
+ import * as path5 from "path";
1654
+ function getCursorHooksPath(repoRoot) {
1655
+ return path5.join(repoRoot, ".cursor", "hooks.json");
1656
+ }
1657
+ function getClaudeSettingsPath(repoRoot) {
1658
+ return path5.join(repoRoot, ".claude", "settings.json");
1659
+ }
1660
+ function getOpenCodePluginDir(repoRoot) {
1661
+ return path5.join(repoRoot, ".opencode", "plugin");
1662
+ }
1663
+ function getOpenCodePluginPath(repoRoot) {
1664
+ return path5.join(getOpenCodePluginDir(repoRoot), "agentblame.ts");
1665
+ }
1666
+ function getHookCommand(provider, event) {
1667
+ const eventArg = event ? ` --event ${event}` : "";
1668
+ return `agentblame capture --provider ${provider}${eventArg}`;
1669
+ }
1670
+ var OPENCODE_PLUGIN_TEMPLATE = `import type { Plugin } from "@opencode-ai/plugin"
1671
+ import { execSync } from "child_process"
1672
+
1673
+ export default (async (ctx: any) => {
1674
+ return {
1675
+ "tool.execute.after": async (input: any, output: any) => {
1676
+ // Only capture edit and write tools
1677
+ if (input?.tool !== "edit" && input?.tool !== "write") {
1678
+ return
1679
+ }
1680
+
1681
+ try {
1682
+ // Get model info from config
1683
+ let model: string | null = null
1684
+ if (ctx?.client?.config?.providers) {
1685
+ try {
1686
+ const configResult = await ctx.client.config.providers()
1687
+ const config = configResult?.data || configResult
1688
+ const activeProvider = config?.connected?.[0]
1689
+ if (activeProvider && config?.default?.[activeProvider]) {
1690
+ const modelId = config.default[activeProvider]
1691
+ // Try to get display name from provider models
1692
+ const provider = config?.providers?.find((p: any) => p.id === activeProvider)
1693
+ const modelInfo = provider?.models?.[modelId]
1694
+ model = modelInfo?.name || modelId
1695
+ }
1696
+ } catch {
1697
+ // Ignore config errors
1698
+ }
1699
+ }
1700
+
1701
+ // Build payload based on tool type
1702
+ const payload: any = {
1703
+ tool: input.tool,
1704
+ sessionID: input.sessionID,
1705
+ callID: input.callID,
1706
+ }
1707
+
1708
+ if (input.tool === "edit") {
1709
+ // Edit tool: has before/after content in metadata
1710
+ payload.filePath = output?.metadata?.filediff?.file || output?.args?.filePath
1711
+ payload.oldString = output?.args?.oldString
1712
+ payload.newString = output?.args?.newString
1713
+ payload.before = output?.metadata?.filediff?.before
1714
+ payload.after = output?.metadata?.filediff?.after
1715
+ payload.diff = output?.metadata?.diff
1716
+ } else if (input.tool === "write") {
1717
+ // Write tool: has content in args
1718
+ payload.filePath = output?.args?.filePath || output?.metadata?.filepath
1719
+ payload.content = output?.args?.content
1720
+ }
1721
+
1722
+ if (model) {
1723
+ payload.model = model
1724
+ }
1725
+
1726
+ // Call agentblame capture with the payload
1727
+ execSync("agentblame capture --provider opencode", {
1728
+ input: JSON.stringify(payload),
1729
+ cwd: ctx?.directory || process.cwd(),
1730
+ stdio: ["pipe", "inherit", "inherit"],
1731
+ timeout: 5000,
1732
+ })
1733
+ } catch {
1734
+ // Silent failure - don't interrupt OpenCode
1735
+ }
1736
+ },
1737
+ }
1738
+ }) satisfies Plugin
1739
+ `;
1740
+ async function installCursorHooks(repoRoot) {
1741
+ if (process.platform === "win32") {
1742
+ console.error("Windows is not supported yet");
1743
+ return false;
1744
+ }
1745
+ const hooksPath = getCursorHooksPath(repoRoot);
1746
+ try {
1747
+ await fs5.promises.mkdir(path5.dirname(hooksPath), {
1748
+ recursive: true
1749
+ });
1750
+ let config = {};
1751
+ try {
1752
+ const existing = await fs5.promises.readFile(hooksPath, "utf8");
1753
+ config = JSON.parse(existing || "{}");
1754
+ } catch {}
1755
+ config.version = config.version ?? 1;
1756
+ config.hooks = config.hooks ?? {};
1757
+ const fileEditCommand = getHookCommand("cursor", "afterFileEdit");
1758
+ config.hooks.afterFileEdit = config.hooks.afterFileEdit ?? [];
1759
+ if (!Array.isArray(config.hooks.afterFileEdit)) {
1760
+ config.hooks.afterFileEdit = [];
1761
+ }
1762
+ config.hooks.afterFileEdit = config.hooks.afterFileEdit.filter((h) => !h?.command?.includes("agentblame") && !h?.command?.includes("capture.ts"));
1763
+ config.hooks.afterFileEdit.push({ command: fileEditCommand });
1764
+ if (config.hooks.afterTabFileEdit) {
1765
+ config.hooks.afterTabFileEdit = config.hooks.afterTabFileEdit.filter((h) => !h?.command?.includes("agentblame") && !h?.command?.includes("capture.ts"));
1766
+ if (config.hooks.afterTabFileEdit.length === 0) {
1767
+ delete config.hooks.afterTabFileEdit;
1768
+ }
1769
+ }
1770
+ await fs5.promises.writeFile(hooksPath, JSON.stringify(config, null, 2), "utf8");
1771
+ return true;
1772
+ } catch (err) {
1773
+ console.error("Failed to install Cursor hooks:", err);
1774
+ return false;
1775
+ }
1776
+ }
1777
+ async function installClaudeHooks(repoRoot) {
1778
+ if (process.platform === "win32") {
1779
+ console.error("Windows is not supported yet");
1780
+ return false;
1781
+ }
1782
+ const settingsPath = getClaudeSettingsPath(repoRoot);
1783
+ try {
1784
+ await fs5.promises.mkdir(path5.dirname(settingsPath), { recursive: true });
1785
+ let config = {};
1786
+ try {
1787
+ const existing = await fs5.promises.readFile(settingsPath, "utf8");
1788
+ config = JSON.parse(existing || "{}");
1789
+ } catch {}
1790
+ config.hooks = config.hooks ?? {};
1791
+ const hookCommand = getHookCommand("claude");
1792
+ config.hooks.PostToolUse = config.hooks.PostToolUse ?? [];
1793
+ if (!Array.isArray(config.hooks.PostToolUse)) {
1794
+ config.hooks.PostToolUse = [];
1795
+ }
1796
+ config.hooks.PostToolUse = config.hooks.PostToolUse.filter((h) => !h?.hooks?.some((hh) => hh?.command?.includes("agentblame") || hh?.command?.includes("capture.ts")));
1797
+ config.hooks.PostToolUse.push({
1798
+ matcher: "Edit|Write|MultiEdit",
1799
+ hooks: [{ type: "command", command: hookCommand, async: true }]
1800
+ });
1801
+ await fs5.promises.writeFile(settingsPath, JSON.stringify(config, null, 2), "utf8");
1802
+ return true;
1803
+ } catch (err) {
1804
+ console.error("Failed to install Claude hooks:", err);
1805
+ return false;
1806
+ }
1807
+ }
1808
+ async function installOpenCodeHooks(repoRoot) {
1809
+ if (process.platform === "win32") {
1810
+ console.error("Windows is not supported yet");
1811
+ return false;
1812
+ }
1813
+ const pluginDir = getOpenCodePluginDir(repoRoot);
1814
+ const pluginPath = getOpenCodePluginPath(repoRoot);
1815
+ try {
1816
+ await fs5.promises.mkdir(pluginDir, { recursive: true });
1817
+ await fs5.promises.writeFile(pluginPath, OPENCODE_PLUGIN_TEMPLATE, "utf8");
1818
+ return true;
1819
+ } catch (err) {
1820
+ console.error("Failed to install OpenCode hooks:", err);
1821
+ return false;
1822
+ }
1823
+ }
1824
+ async function uninstallOpenCodeHooks(repoRoot) {
1825
+ try {
1826
+ const pluginPath = getOpenCodePluginPath(repoRoot);
1827
+ if (fs5.existsSync(pluginPath)) {
1828
+ await fs5.promises.unlink(pluginPath);
1829
+ }
1830
+ return true;
1831
+ } catch (err) {
1832
+ console.error("Failed to uninstall OpenCode hooks:", err);
1833
+ return false;
1834
+ }
1835
+ }
1836
+ async function installGitHook(repoRoot) {
1837
+ const hooksDir = path5.join(repoRoot, ".git", "hooks");
1838
+ const hookPath = path5.join(hooksDir, "post-commit");
1839
+ const hookContent = `#!/bin/sh
1840
+ # Agent Blame - Auto-process commits for AI attribution
1841
+ agentblame process HEAD 2>/dev/null || true
1842
+
1843
+ # Push notes to remote (silently fails if no notes or no remote)
1844
+ git push origin refs/notes/agentblame:refs/notes/agentblame 2>/dev/null || true
1845
+ `;
1846
+ try {
1847
+ await fs5.promises.mkdir(hooksDir, { recursive: true });
1848
+ let existingContent = "";
1849
+ try {
1850
+ existingContent = await fs5.promises.readFile(hookPath, "utf8");
1851
+ } catch {}
1852
+ if (existingContent.includes("agentblame") || existingContent.includes("Agent Blame")) {
1853
+ existingContent = removeAgentBlameSection(existingContent);
1854
+ }
1855
+ if (existingContent.trim()) {
1856
+ const newContent = existingContent.trimEnd() + `
1857
+
1858
+ ` + hookContent.split(`
1859
+ `).slice(1).join(`
1860
+ `);
1861
+ await fs5.promises.writeFile(hookPath, newContent, { mode: 493 });
1862
+ } else {
1863
+ await fs5.promises.writeFile(hookPath, hookContent, { mode: 493 });
1864
+ }
1865
+ return true;
1866
+ } catch (err) {
1867
+ console.error("Failed to install git hook:", err);
1868
+ return false;
1869
+ }
1870
+ }
1871
+ function removeAgentBlameSection(content) {
1872
+ const lines = content.split(`
1873
+ `);
1874
+ const result = [];
1875
+ for (let i = 0;i < lines.length; i++) {
1876
+ const line = lines[i];
1877
+ const nextLine = lines[i + 1] || "";
1878
+ if (line.includes("agentblame") || line.includes("Agent Blame")) {
1879
+ continue;
1880
+ }
1881
+ if (line.includes("Push notes to remote") && nextLine.includes("refs/notes/agentblame")) {
1882
+ continue;
1883
+ }
1884
+ if (line.trim() === "" && result.length > 0 && result[result.length - 1].trim() === "") {
1885
+ continue;
1886
+ }
1887
+ result.push(line);
1888
+ }
1889
+ return result.join(`
1890
+ `);
1891
+ }
1892
+ async function uninstallGitHook(repoRoot) {
1893
+ const hookPath = path5.join(repoRoot, ".git", "hooks", "post-commit");
1894
+ try {
1895
+ if (!fs5.existsSync(hookPath)) {
1896
+ return true;
1897
+ }
1898
+ const content = await fs5.promises.readFile(hookPath, "utf8");
1899
+ if (!content.includes("agentblame") && !content.includes("Agent Blame")) {
1900
+ return true;
1901
+ }
1902
+ const newContent = removeAgentBlameSection(content);
1903
+ const meaningfulLines = newContent.split(`
1904
+ `).filter((l) => l.trim() && !l.startsWith("#!"));
1905
+ if (meaningfulLines.length === 0) {
1906
+ await fs5.promises.unlink(hookPath);
1907
+ } else {
1908
+ await fs5.promises.writeFile(hookPath, newContent, { mode: 493 });
1909
+ }
1910
+ return true;
1911
+ } catch (err) {
1912
+ console.error("Failed to uninstall git hook:", err);
1913
+ return false;
1914
+ }
1915
+ }
1916
+ async function uninstallCursorHooks(repoRoot) {
1917
+ try {
1918
+ const hooksPath = getCursorHooksPath(repoRoot);
1919
+ if (fs5.existsSync(hooksPath)) {
1920
+ const config = JSON.parse(await fs5.promises.readFile(hooksPath, "utf8"));
1921
+ if (config.hooks?.afterFileEdit) {
1922
+ config.hooks.afterFileEdit = config.hooks.afterFileEdit.filter((h) => !h?.command?.includes("agentblame") && !h?.command?.includes("capture.ts"));
1923
+ }
1924
+ await fs5.promises.writeFile(hooksPath, JSON.stringify(config, null, 2), "utf8");
1925
+ }
1926
+ return true;
1927
+ } catch (err) {
1928
+ console.error("Failed to uninstall Cursor hooks:", err);
1929
+ return false;
1930
+ }
1931
+ }
1932
+ async function uninstallClaudeHooks(repoRoot) {
1933
+ try {
1934
+ const settingsPath = getClaudeSettingsPath(repoRoot);
1935
+ if (fs5.existsSync(settingsPath)) {
1936
+ const config = JSON.parse(await fs5.promises.readFile(settingsPath, "utf8"));
1937
+ if (config.hooks?.PostToolUse) {
1938
+ config.hooks.PostToolUse = config.hooks.PostToolUse.filter((h) => !h?.hooks?.some((hh) => hh?.command?.includes("agentblame") || hh?.command?.includes("capture.ts")));
1939
+ }
1940
+ await fs5.promises.writeFile(settingsPath, JSON.stringify(config, null, 2), "utf8");
1941
+ }
1942
+ return true;
1943
+ } catch (err) {
1944
+ console.error("Failed to uninstall Claude hooks:", err);
1945
+ return false;
1946
+ }
1947
+ }
1948
+ var GITHUB_WORKFLOW_CONTENT = `name: Agent Blame
1949
+
1950
+ on:
1951
+ pull_request:
1952
+ types: [closed]
1953
+
1954
+ jobs:
1955
+ post-merge:
1956
+ # Only run if the PR was merged (not just closed)
1957
+ if: github.event.pull_request.merged == true
1958
+ runs-on: ubuntu-latest
1959
+
1960
+ permissions:
1961
+ contents: write # Needed to push notes
1962
+
1963
+ steps:
1964
+ - name: Checkout repository
1965
+ uses: actions/checkout@v4
1966
+ with:
1967
+ fetch-depth: 0 # Full history needed for notes and blame
1968
+ ref: \${{ github.event.pull_request.base.ref }} # Checkout target branch (e.g., main)
1969
+
1970
+ - name: Setup Bun
1971
+ uses: oven-sh/setup-bun@v1
1972
+
1973
+ - name: Configure git identity
1974
+ run: |
1975
+ git config user.name "github-actions[bot]"
1976
+ git config user.email "github-actions[bot]@users.noreply.github.com"
1977
+
1978
+ - name: Install agentblame
1979
+ run: npm install -g @mesadev/agentblame
1980
+
1981
+ - name: Fetch notes, tags, and PR head
1982
+ run: |
1983
+ git fetch origin refs/notes/agentblame:refs/notes/agentblame 2>/dev/null || echo "No existing attribution notes"
1984
+ git fetch origin refs/notes/agentblame-analytics:refs/notes/agentblame-analytics 2>/dev/null || echo "No existing analytics notes"
1985
+ git fetch origin --tags 2>/dev/null || echo "No tags to fetch"
1986
+ git fetch origin refs/pull/\${{ github.event.pull_request.number }}/head:refs/pull/\${{ github.event.pull_request.number }}/head 2>/dev/null || echo "Could not fetch PR head"
1987
+
1988
+ - name: Process merge (transfer notes + update analytics)
1989
+ run: bun $(npm root -g)/@mesadev/agentblame/dist/post-merge.js
1990
+ env:
1991
+ PR_NUMBER: \${{ github.event.pull_request.number }}
1992
+ PR_TITLE: \${{ github.event.pull_request.title }}
1993
+ PR_AUTHOR: \${{ github.event.pull_request.user.login }}
1994
+ BASE_REF: \${{ github.event.pull_request.base.ref }}
1995
+ BASE_SHA: \${{ github.event.pull_request.base.sha }}
1996
+ HEAD_SHA: \${{ github.event.pull_request.head.sha }}
1997
+ MERGE_SHA: \${{ github.event.pull_request.merge_commit_sha }}
1998
+
1999
+ - name: Push notes and tags
2000
+ run: |
2001
+ # Push attribution notes
2002
+ git push origin refs/notes/agentblame 2>/dev/null || echo "No attribution notes to push"
2003
+ # Push analytics notes
2004
+ git push origin refs/notes/agentblame-analytics 2>/dev/null || echo "No analytics notes to push"
2005
+ # Push analytics anchor tag
2006
+ git push origin agentblame-analytics-anchor 2>/dev/null || echo "No analytics tag to push"
2007
+ `;
2008
+ async function installGitHubAction(repoRoot) {
2009
+ const workflowDir = path5.join(repoRoot, ".github", "workflows");
2010
+ const workflowPath = path5.join(workflowDir, "agentblame.yml");
2011
+ try {
2012
+ await fs5.promises.mkdir(workflowDir, { recursive: true });
2013
+ await fs5.promises.writeFile(workflowPath, GITHUB_WORKFLOW_CONTENT, "utf8");
2014
+ return true;
2015
+ } catch (err) {
2016
+ console.error("Failed to install GitHub Action:", err);
2017
+ return false;
2018
+ }
2019
+ }
2020
+ async function uninstallGitHubAction(repoRoot) {
2021
+ const workflowPath = path5.join(repoRoot, ".github", "workflows", "agentblame.yml");
2022
+ try {
2023
+ if (fs5.existsSync(workflowPath)) {
2024
+ await fs5.promises.unlink(workflowPath);
2025
+ }
2026
+ return true;
2027
+ } catch (err) {
2028
+ console.error("Failed to uninstall GitHub Action:", err);
2029
+ return false;
2030
+ }
2031
+ }
2032
+ // src/lib/git/gitDiff.ts
2033
+ async function getCommitDiff(repoRoot, sha) {
2034
+ const nameStatus = await runGit(repoRoot, [
2035
+ "diff-tree",
2036
+ "--no-commit-id",
2037
+ "--name-status",
2038
+ "-r",
2039
+ sha
2040
+ ]);
2041
+ const files = [];
2042
+ if (nameStatus.exitCode === 0) {
2043
+ for (const line of nameStatus.stdout.split(`
2044
+ `)) {
2045
+ if (!line.trim())
2046
+ continue;
2047
+ const [status, ...pathParts] = line.split("\t");
2048
+ const path6 = pathParts.join("\t");
2049
+ if (!path6)
2050
+ continue;
2051
+ let fileStatus = "modified";
2052
+ if (status === "A")
2053
+ fileStatus = "added";
2054
+ else if (status === "D")
2055
+ fileStatus = "deleted";
2056
+ files.push({ path: path6, status: fileStatus });
2057
+ }
2058
+ }
2059
+ const result = await runGit(repoRoot, [
2060
+ "diff",
2061
+ `${sha}^`,
2062
+ sha,
2063
+ "--unified=0"
2064
+ ]);
2065
+ let raw = result.stdout;
2066
+ if (result.exitCode !== 0) {
2067
+ const emptyTree = await runGit(repoRoot, [
2068
+ "diff",
2069
+ "4b825dc642cb6eb9a060e54bf8d69288fbee4904",
2070
+ sha,
2071
+ "--unified=0"
2072
+ ]);
2073
+ raw = emptyTree.stdout;
2074
+ }
2075
+ return { files, raw };
2076
+ }
2077
+ // src/lib/git/gitConfig.ts
2078
+ async function configureNotesSync(_repoRoot) {
2079
+ return true;
2080
+ }
2081
+ async function removeNotesSync(repoRoot) {
2082
+ try {
2083
+ await runGit(repoRoot, [
2084
+ "config",
2085
+ "--local",
2086
+ "--unset-all",
2087
+ "remote.origin.push",
2088
+ "refs/notes/agentblame"
2089
+ ]).catch(() => {});
2090
+ await runGit(repoRoot, [
2091
+ "config",
2092
+ "--local",
2093
+ "--unset-all",
2094
+ "remote.origin.fetch",
2095
+ "refs/notes/agentblame"
2096
+ ]).catch(() => {});
2097
+ return true;
2098
+ } catch {
2099
+ return false;
2100
+ }
2101
+ }
2102
+ // src/sync.ts
2103
+ function run(cmd, cwd) {
2104
+ try {
2105
+ return execSync(cmd, { encoding: "utf8", cwd }).trim();
2106
+ } catch {
2107
+ return "";
2108
+ }
2109
+ }
2110
+ function vlog(msg, options) {
2111
+ if (options.verbose) {
2112
+ console.log(` ${msg}`);
2113
+ }
2114
+ }
2115
+ function findMergeCandidates(repoRoot, options) {
2116
+ const candidates = [];
2117
+ const logOutput = run(`git log --oneline -20 --format="%H %s"`, repoRoot);
2118
+ if (!logOutput)
2119
+ return candidates;
2120
+ for (const line of logOutput.split(`
2121
+ `)) {
2122
+ const match = line.match(/^([a-f0-9]+)\s+(.+)$/);
2123
+ if (!match)
2124
+ continue;
2125
+ const [, sha, message] = match;
2126
+ const prMatch = message.match(/\(#?(\d+)\)\s*$/);
2127
+ if (!prMatch)
2128
+ continue;
2129
+ const prNumber = parseInt(prMatch[1], 10);
2130
+ const hasNote = run(`git notes --ref=refs/notes/agentblame show ${sha} 2>/dev/null`, repoRoot);
2131
+ if (hasNote) {
2132
+ vlog(`Skipping ${sha.slice(0, 7)} - already has note`, options);
2133
+ continue;
2134
+ }
2135
+ const parents = run(`git rev-list --parents -n 1 ${sha}`, repoRoot).split(" ");
2136
+ if (parents.length > 2) {
2137
+ vlog(`Skipping ${sha.slice(0, 7)} - merge commit (has multiple parents)`, options);
2138
+ continue;
2139
+ }
2140
+ candidates.push({ sha, prNumber, message });
2141
+ }
2142
+ return candidates;
2143
+ }
2144
+ function fetchPRRef(repoRoot, prNumber, options) {
2145
+ vlog(`Fetching refs/pull/${prNumber}/head...`, options);
2146
+ run(`git fetch origin refs/pull/${prNumber}/head:refs/remotes/origin/pr/${prNumber} 2>&1`, repoRoot);
2147
+ const refExists = run(`git rev-parse --verify refs/remotes/origin/pr/${prNumber} 2>/dev/null`, repoRoot);
2148
+ return !!refExists;
2149
+ }
2150
+ function getPRCommits(repoRoot, prNumber, baseSha) {
2151
+ const prRef = `refs/remotes/origin/pr/${prNumber}`;
2152
+ const output = run(`git rev-list ${baseSha}..${prRef} 2>/dev/null`, repoRoot);
2153
+ if (!output)
2154
+ return [];
2155
+ return output.split(`
2156
+ `).filter(Boolean);
2157
+ }
2158
+ function readNote2(repoRoot, sha) {
2159
+ const note = run(`git notes --ref=refs/notes/agentblame show ${sha} 2>/dev/null`, repoRoot);
2160
+ if (!note)
2161
+ return null;
2162
+ try {
2163
+ return JSON.parse(note);
2164
+ } catch {
2165
+ return null;
2166
+ }
2167
+ }
2168
+ function writeNote(repoRoot, sha, attribution) {
2169
+ const noteJson = JSON.stringify(attribution);
2170
+ try {
2171
+ const result = spawnSync("git", ["notes", "--ref=refs/notes/agentblame", "add", "-f", "-m", noteJson, sha], { encoding: "utf8", cwd: repoRoot });
2172
+ if (result.status !== 0) {
2173
+ console.error(`Failed to write note to ${sha}: ${result.stderr}`);
2174
+ return false;
2175
+ }
2176
+ return true;
2177
+ } catch (err) {
2178
+ console.error(`Failed to write note to ${sha}: ${err}`);
2179
+ return false;
2180
+ }
2181
+ }
2182
+ function getCommitHunks(repoRoot, sha) {
2183
+ const diff2 = run(`git diff-tree -p ${sha}`, repoRoot);
2184
+ if (!diff2)
2185
+ return [];
2186
+ const hunks = [];
2187
+ let currentFile = "";
2188
+ let lineNumber = 0;
2189
+ let addedLines = [];
2190
+ let startLine = 0;
2191
+ const computeHash = (content) => {
2192
+ const crypto = __require("crypto");
2193
+ return `sha256:${crypto.createHash("sha256").update(content).digest("hex")}`;
2194
+ };
2195
+ for (const line of diff2.split(`
2196
+ `)) {
2197
+ if (line.startsWith("+++ b/")) {
2198
+ if (addedLines.length > 0 && currentFile) {
2199
+ const content = addedLines.join(`
2200
+ `);
2201
+ hunks.push({
2202
+ path: currentFile,
2203
+ startLine,
2204
+ content,
2205
+ contentHash: computeHash(content)
2206
+ });
2207
+ addedLines = [];
2208
+ }
2209
+ currentFile = line.slice(6);
2210
+ continue;
2211
+ }
2212
+ if (line.startsWith("@@")) {
2213
+ if (addedLines.length > 0 && currentFile) {
2214
+ const content = addedLines.join(`
2215
+ `);
2216
+ hunks.push({
2217
+ path: currentFile,
2218
+ startLine,
2219
+ content,
2220
+ contentHash: computeHash(content)
2221
+ });
2222
+ addedLines = [];
2223
+ }
2224
+ const match = line.match(/@@ -\d+(?:,\d+)? \+(\d+)/);
2225
+ if (match) {
2226
+ lineNumber = parseInt(match[1], 10);
2227
+ startLine = lineNumber;
2228
+ }
2229
+ continue;
2230
+ }
2231
+ if (line.startsWith("+") && !line.startsWith("+++")) {
2232
+ if (addedLines.length === 0) {
2233
+ startLine = lineNumber;
2234
+ }
2235
+ addedLines.push(line.slice(1));
2236
+ lineNumber++;
2237
+ continue;
2238
+ }
2239
+ if (!line.startsWith("-")) {
2240
+ if (addedLines.length > 0 && currentFile) {
2241
+ const content = addedLines.join(`
2242
+ `);
2243
+ hunks.push({
2244
+ path: currentFile,
2245
+ startLine,
2246
+ content,
2247
+ contentHash: computeHash(content)
2248
+ });
2249
+ addedLines = [];
2250
+ }
2251
+ lineNumber++;
2252
+ }
2253
+ }
2254
+ if (addedLines.length > 0 && currentFile) {
2255
+ const content = addedLines.join(`
2256
+ `);
2257
+ hunks.push({
2258
+ path: currentFile,
2259
+ startLine,
2260
+ content,
2261
+ contentHash: computeHash(content)
2262
+ });
2263
+ }
2264
+ return hunks;
2265
+ }
2266
+ function collectPRAttributions(repoRoot, prCommits) {
2267
+ const byHash = new Map;
2268
+ const withContent = [];
2269
+ for (const sha of prCommits) {
2270
+ const note = readNote2(repoRoot, sha);
2271
+ if (!note?.attributions)
2272
+ continue;
2273
+ const hunks = getCommitHunks(repoRoot, sha);
2274
+ const hunksByHash = new Map;
2275
+ for (const hunk of hunks) {
2276
+ hunksByHash.set(hunk.contentHash, hunk.content);
2277
+ }
2278
+ for (const attr of note.attributions) {
2279
+ const hash = attr.contentHash;
2280
+ if (!byHash.has(hash)) {
2281
+ byHash.set(hash, []);
2282
+ }
2283
+ byHash.get(hash)?.push(attr);
2284
+ const content = hunksByHash.get(hash) || "";
2285
+ if (content) {
2286
+ withContent.push({ ...attr, originalContent: content });
2287
+ }
2288
+ }
2289
+ }
2290
+ return { byHash, withContent };
2291
+ }
2292
+ function findContainedAttributions(hunk, attributions) {
2293
+ const results = [];
2294
+ for (const attr of attributions) {
2295
+ const attrFileName = attr.path.split("/").pop();
2296
+ const hunkFileName = hunk.path.split("/").pop();
2297
+ const sameFile = attrFileName === hunkFileName || attr.path.endsWith(hunk.path) || hunk.path.endsWith(attrFileName || "");
2298
+ if (!sameFile)
2299
+ continue;
2300
+ const aiContent = attr.originalContent.trim();
2301
+ const hunkContent = hunk.content;
2302
+ if (!hunkContent.includes(aiContent))
2303
+ continue;
2304
+ const offset = hunkContent.indexOf(aiContent);
2305
+ let startLine = hunk.startLine;
2306
+ if (offset > 0) {
2307
+ const contentBeforeAI = hunkContent.slice(0, offset);
2308
+ const linesBeforeAI = contentBeforeAI.split(`
2309
+ `).length - 1;
2310
+ startLine = hunk.startLine + linesBeforeAI;
2311
+ }
2312
+ const aiLineCount = aiContent.split(`
2313
+ `).length;
2314
+ const endLine = startLine + aiLineCount - 1;
2315
+ const { originalContent: _, ...cleanAttr } = attr;
2316
+ results.push({
2317
+ ...cleanAttr,
2318
+ path: hunk.path,
2319
+ startLine,
2320
+ endLine
2321
+ });
2322
+ }
2323
+ return results;
2324
+ }
2325
+ function transferNotes(repoRoot, candidate, prCommits, options) {
2326
+ const { byHash, withContent } = collectPRAttributions(repoRoot, prCommits);
2327
+ if (byHash.size === 0) {
2328
+ vlog(`No attributions found in PR commits`, options);
2329
+ return 0;
2330
+ }
2331
+ vlog(`Found ${byHash.size} unique content hashes`, options);
2332
+ const hunks = getCommitHunks(repoRoot, candidate.sha);
2333
+ vlog(`Merge commit has ${hunks.length} hunks`, options);
2334
+ const newAttributions = [];
2335
+ const matchedHashes = new Set;
2336
+ for (const hunk of hunks) {
2337
+ const attrs = byHash.get(hunk.contentHash);
2338
+ if (attrs && attrs.length > 0) {
2339
+ const attr = attrs[0];
2340
+ newAttributions.push({
2341
+ ...attr,
2342
+ path: hunk.path,
2343
+ startLine: hunk.startLine,
2344
+ endLine: hunk.startLine + hunk.content.split(`
2345
+ `).length - 1
2346
+ });
2347
+ matchedHashes.add(attr.contentHash);
2348
+ vlog(` Exact match: ${hunk.path}:${hunk.startLine}`, options);
2349
+ continue;
2350
+ }
2351
+ const unmatchedAttrs = withContent.filter((a) => !matchedHashes.has(a.contentHash));
2352
+ const containedMatches = findContainedAttributions(hunk, unmatchedAttrs);
2353
+ for (const match of containedMatches) {
2354
+ newAttributions.push(match);
2355
+ matchedHashes.add(match.contentHash);
2356
+ vlog(` Contained match: ${match.path}:${match.startLine}-${match.endLine}`, options);
2357
+ }
2358
+ }
2359
+ if (newAttributions.length === 0) {
2360
+ return 0;
2361
+ }
2362
+ if (options.dryRun) {
2363
+ console.log(` Would attach ${newAttributions.length} attribution(s)`);
2364
+ return newAttributions.length;
2365
+ }
2366
+ const note = {
2367
+ version: 2,
2368
+ timestamp: new Date().toISOString(),
2369
+ attributions: newAttributions
2370
+ };
2371
+ if (writeNote(repoRoot, candidate.sha, note)) {
2372
+ return newAttributions.length;
2373
+ }
2374
+ return 0;
2375
+ }
2376
+ function findBaseSha(repoRoot, mergeSha) {
2377
+ const parent = run(`git rev-parse ${mergeSha}^`, repoRoot);
2378
+ return parent || "";
2379
+ }
2380
+ function pushNotes(repoRoot, options) {
2381
+ if (options.dryRun) {
2382
+ console.log(`
2383
+ Would push notes to origin`);
2384
+ return true;
2385
+ }
2386
+ console.log(`
2387
+ Pushing notes to origin...`);
2388
+ try {
2389
+ execSync("git push origin refs/notes/agentblame", {
2390
+ encoding: "utf8",
2391
+ cwd: repoRoot,
2392
+ stdio: "inherit"
2393
+ });
2394
+ return true;
2395
+ } catch {
2396
+ console.error("Failed to push notes");
2397
+ return false;
2398
+ }
2399
+ }
2400
+ async function sync(options = {}) {
2401
+ const repoRoot = await getRepoRoot(process.cwd());
2402
+ if (!repoRoot) {
2403
+ console.error("Error: Not in a git repository");
2404
+ process.exit(1);
2405
+ }
2406
+ console.log(`Agent Blame Sync - Transferring attribution notes
2407
+ `);
2408
+ if (options.dryRun) {
2409
+ console.log(`[DRY RUN - no changes will be made]
2410
+ `);
2411
+ }
2412
+ await fetchNotesQuiet(repoRoot, "origin", options.verbose);
2413
+ const candidates = findMergeCandidates(repoRoot, options);
2414
+ if (candidates.length === 0) {
2415
+ console.log("No squash/rebase merges found that need notes transferred.");
2416
+ console.log("(Looking for commits with PR numbers like '#123' that don't have notes)");
2417
+ return;
2418
+ }
2419
+ console.log(`Found ${candidates.length} merge(s) that may need notes:
2420
+ `);
2421
+ let totalTransferred = 0;
2422
+ let successCount = 0;
2423
+ for (const candidate of candidates) {
2424
+ console.log(`PR #${candidate.prNumber}: ${candidate.message.slice(0, 50)}...`);
2425
+ console.log(` Commit: ${candidate.sha.slice(0, 7)}`);
2426
+ if (!fetchPRRef(repoRoot, candidate.prNumber, options)) {
2427
+ console.log(` Skipped: Could not fetch PR #${candidate.prNumber} refs`);
2428
+ continue;
2429
+ }
2430
+ const baseSha = findBaseSha(repoRoot, candidate.sha);
2431
+ if (!baseSha) {
2432
+ console.log(` Skipped: Could not find base commit`);
2433
+ continue;
2434
+ }
2435
+ const prCommits = getPRCommits(repoRoot, candidate.prNumber, baseSha);
2436
+ if (prCommits.length === 0) {
2437
+ console.log(` Skipped: No PR commits found`);
2438
+ continue;
2439
+ }
2440
+ vlog(`Found ${prCommits.length} PR commits`, options);
2441
+ const transferred = transferNotes(repoRoot, candidate, prCommits, options);
2442
+ if (transferred > 0) {
2443
+ console.log(` Transferred ${transferred} attribution(s)`);
2444
+ totalTransferred += transferred;
2445
+ successCount++;
2446
+ } else {
2447
+ console.log(` No attributions to transfer`);
2448
+ }
2449
+ console.log("");
2450
+ }
2451
+ if (totalTransferred > 0) {
2452
+ console.log(`
2453
+ Summary: Transferred ${totalTransferred} attribution(s) for ${successCount} merge(s)`);
2454
+ pushNotes(repoRoot, options);
2455
+ } else {
2456
+ console.log(`
2457
+ No attributions were transferred.`);
2458
+ }
2459
+ }
2460
+
2461
+ // src/process.ts
2462
+ var c2 = {
2463
+ reset: "\x1B[0m",
2464
+ bold: "\x1B[1m",
2465
+ dim: "\x1B[2m",
2466
+ cyan: "\x1B[36m",
2467
+ yellow: "\x1B[33m",
2468
+ green: "\x1B[32m",
2469
+ orange: "\x1B[38;2;184;101;64m",
2470
+ blue: "\x1B[34m"
2471
+ };
2472
+ async function processCommit(repoRoot, commitSha) {
2473
+ const parentSha = await getParentCommit(repoRoot, commitSha);
2474
+ if (!parentSha) {
2475
+ return {
2476
+ sha: commitSha,
2477
+ filesProcessed: 0,
2478
+ aiLines: 0,
2479
+ humanLines: 0,
2480
+ sessions: []
2481
+ };
2482
+ }
2483
+ const diff2 = await getCommitDiff(repoRoot, commitSha);
2484
+ const trackedFiles = getModifiedFiles(repoRoot, parentSha);
2485
+ const fileAttributions = new Map;
2486
+ let totalAiLines = 0;
2487
+ let totalHumanLines = 0;
2488
+ const allSessionIds = new Set;
2489
+ for (const file of diff2.files || []) {
2490
+ if (file.status === "deleted")
2491
+ continue;
2492
+ const committedContent = await getFileAtCommit(repoRoot, commitSha, file.path);
2493
+ if (!committedContent)
2494
+ continue;
2495
+ const committedLines = committedContent.split(`
2496
+ `);
2497
+ if (!trackedFiles.includes(file.path)) {
2498
+ const humanAttrs = committedLines.map((_, i) => ({
2499
+ line: i + 1,
2500
+ sessionId: null
2501
+ }));
2502
+ fileAttributions.set(file.path, humanAttrs);
2503
+ totalHumanLines += committedLines.length;
2504
+ continue;
2505
+ }
2506
+ const chain = getSnapshotChainForFile(repoRoot, parentSha, file.path);
2507
+ if (chain.length === 0) {
2508
+ const humanAttrs = committedLines.map((_, i) => ({
2509
+ line: i + 1,
2510
+ sessionId: null
2511
+ }));
2512
+ fileAttributions.set(file.path, humanAttrs);
2513
+ totalHumanLines += committedLines.length;
2514
+ continue;
2515
+ }
2516
+ const origins = await traceFileLines(repoRoot, committedLines, chain);
2517
+ const attrs = [];
2518
+ for (let i = 0;i < origins.length; i++) {
2519
+ const origin = origins[i];
2520
+ attrs.push({
2521
+ line: i + 1,
2522
+ sessionId: origin.sessionId
2523
+ });
2524
+ if (origin.sessionId) {
2525
+ totalAiLines++;
2526
+ allSessionIds.add(origin.sessionId);
2527
+ } else {
2528
+ totalHumanLines++;
2529
+ }
2530
+ }
2531
+ fileAttributions.set(file.path, attrs);
2532
+ }
2533
+ const sessionMap = buildSessionMap(fileAttributions);
2534
+ const humanMap = buildHumanMap(fileAttributions);
2535
+ const attribution = {
2536
+ version: 3,
2537
+ timestamp: new Date().toISOString(),
2538
+ sessions: {},
2539
+ files: {}
2540
+ };
2541
+ const sessions = {};
2542
+ for (const sessionId of allSessionIds) {
2543
+ const session = getSession(sessionId);
2544
+ const prompt = getLatestPromptForSession(sessionId);
2545
+ const tools = getToolNamesForSession(sessionId);
2546
+ sessions[sessionId] = {
2547
+ a: session?.agent || "cursor",
2548
+ m: session?.model || null,
2549
+ p: prompt?.content || null,
2550
+ t: session?.createdAt || new Date().toISOString(),
2551
+ tools
2552
+ };
2553
+ updateSessionFirstCommit(sessionId, commitSha);
2554
+ }
2555
+ attribution.sessions = sessions;
2556
+ for (const [filePath, attrs] of fileAttributions) {
2557
+ const ranges = aggregateToRanges(attrs);
2558
+ const { aiRanges, humanRanges } = separateRanges(ranges);
2559
+ attribution.files[filePath] = {
2560
+ aiRanges: aiRanges.map((r) => ({
2561
+ sessionId: r.sessionId,
2562
+ startLine: r.startLine,
2563
+ endLine: r.endLine
2564
+ })),
2565
+ humanRanges
2566
+ };
2567
+ }
2568
+ if (allSessionIds.size > 0 || Object.keys(attribution.files).length > 0) {
2569
+ await attachNoteV3(repoRoot, commitSha, attribution, sessions);
2570
+ }
2571
+ const commitStats = computeCommitStats(sessions, attribution.files);
2572
+ const authorResult = await runGit(repoRoot, [
2573
+ "log",
2574
+ "-1",
2575
+ "--format=%ae",
2576
+ commitSha
2577
+ ]);
2578
+ const commitAuthor = authorResult.exitCode === 0 ? authorResult.stdout.trim() : undefined;
2579
+ await updateAnalytics(repoRoot, commitStats, commitAuthor);
2580
+ cleanupWorkingDir(repoRoot, parentSha);
2581
+ return {
2582
+ sha: commitSha,
2583
+ filesProcessed: fileAttributions.size,
2584
+ aiLines: totalAiLines,
2585
+ humanLines: totalHumanLines,
2586
+ sessions: Array.from(allSessionIds)
2587
+ };
2588
+ }
2589
+ async function runProcess(sha) {
2590
+ const repoRoot = await getRepoRoot(process.cwd());
2591
+ if (!repoRoot) {
2592
+ console.error("Error: Not in a git repository");
2593
+ process.exit(1);
2594
+ }
2595
+ const dbPath = getDatabasePath(repoRoot);
2596
+ setDatabasePath(dbPath);
2597
+ await fetchNotesQuiet(repoRoot);
2598
+ let commitSha = sha || "HEAD";
2599
+ const resolveResult = await runGit(repoRoot, ["rev-parse", commitSha]);
2600
+ if (resolveResult.exitCode !== 0) {
2601
+ console.error("Error: Could not resolve commit");
2602
+ process.exit(1);
2603
+ }
2604
+ commitSha = resolveResult.stdout.trim();
2605
+ const result = await processCommit(repoRoot, commitSha);
2606
+ const totalLines = result.aiLines + result.humanLines;
2607
+ const aiPercent = totalLines > 0 ? Math.round(result.aiLines / totalLines * 100) : 0;
2608
+ const humanPercent = 100 - aiPercent;
2609
+ const WIDTH = 72;
2610
+ const INNER = WIDTH - 2;
2611
+ const border = `${c2.dim}\u2502${c2.reset}`;
2612
+ const padRight = (content, visibleLen) => content + " ".repeat(Math.max(0, INNER - visibleLen));
2613
+ console.log("");
2614
+ console.log(`${c2.dim}\u250C${"\u2500".repeat(WIDTH - 2)}\u2510${c2.reset}`);
2615
+ const title = "Agent Blame v3";
2616
+ const titlePadLeft = Math.floor((INNER - title.length) / 2);
2617
+ const titlePadRight = INNER - title.length - titlePadLeft;
2618
+ console.log(`${border}${" ".repeat(titlePadLeft)}${c2.bold}${c2.cyan}${title}${c2.reset}${" ".repeat(titlePadRight)}${border}`);
2619
+ console.log(`${c2.dim}\u251C${"\u2500".repeat(WIDTH - 2)}\u2524${c2.reset}`);
2620
+ const commitVisible = ` Commit: ${commitSha.slice(0, 8)}`;
2621
+ const commitColored = ` ${c2.yellow}Commit: ${commitSha.slice(0, 8)}${c2.reset}`;
2622
+ console.log(`${border}${padRight(commitColored, commitVisible.length)}${border}`);
2623
+ const filesVisible = ` Files: ${result.filesProcessed}`;
2624
+ console.log(`${border}${padRight(filesVisible, filesVisible.length)}${border}`);
2625
+ console.log(`${c2.dim}\u251C${"\u2500".repeat(WIDTH - 2)}\u2524${c2.reset}`);
2626
+ if (result.sessions.length > 0) {
2627
+ const sessHeader = " Sessions:";
2628
+ console.log(`${border}${padRight(sessHeader, sessHeader.length)}${border}`);
2629
+ for (const sessionId of result.sessions) {
2630
+ const session = getSession(sessionId);
2631
+ const agent = session?.agent || "unknown";
2632
+ const model = session?.model || "";
2633
+ const modelStr = model ? ` - ${model}` : "";
2634
+ const visibleText = ` ${sessionId.slice(0, 8)} [${agent}${modelStr}]`;
2635
+ const coloredText = ` ${c2.blue}${sessionId.slice(0, 8)}${c2.reset} ${c2.orange}[${agent}${modelStr}]${c2.reset}`;
2636
+ console.log(`${border}${padRight(coloredText, visibleText.length)}${border}`);
2637
+ const prompt = getLatestPromptForSession(sessionId);
2638
+ if (prompt) {
2639
+ const truncatedPrompt = prompt.content.length > 50 ? prompt.content.substring(0, 50) + "..." : prompt.content;
2640
+ const promptVisible = ` "${truncatedPrompt}"`;
2641
+ const promptColored = ` ${c2.dim}"${truncatedPrompt}"${c2.reset}`;
2642
+ console.log(`${border}${padRight(promptColored, promptVisible.length)}${border}`);
2643
+ }
2644
+ }
2645
+ console.log(`${c2.dim}\u251C${"\u2500".repeat(WIDTH - 2)}\u2524${c2.reset}`);
2646
+ }
2647
+ const barWidth = 50;
2648
+ const aiBarWidth = Math.round(aiPercent / 100 * barWidth);
2649
+ const humanBarWidth = barWidth - aiBarWidth;
2650
+ const summaryHeader = " Summary:";
2651
+ console.log(`${border}${padRight(summaryHeader, summaryHeader.length)}${border}`);
2652
+ const barVisible = ` ${"\u2588".repeat(aiBarWidth)}${"\u2591".repeat(humanBarWidth)}`;
2653
+ const barColored = ` ${c2.orange}${"\u2588".repeat(aiBarWidth)}${c2.reset}${c2.dim}${"\u2591".repeat(humanBarWidth)}${c2.reset}`;
2654
+ console.log(`${border}${padRight(barColored, barVisible.length)}${border}`);
2655
+ const statsVisible = ` AI: ${String(result.aiLines).padStart(3)} lines (${String(aiPercent).padStart(3)}%) Human: ${String(result.humanLines).padStart(3)} lines (${String(humanPercent).padStart(3)}%)`;
2656
+ const statsColored = ` ${c2.orange}AI: ${String(result.aiLines).padStart(3)} lines (${String(aiPercent).padStart(3)}%)${c2.reset} ${c2.green}Human: ${String(result.humanLines).padStart(3)} lines (${String(humanPercent).padStart(3)}%)${c2.reset}`;
2657
+ console.log(`${border}${padRight(statsColored, statsVisible.length)}${border}`);
2658
+ console.log(`${c2.dim}\u2514${"\u2500".repeat(WIDTH - 2)}\u2518${c2.reset}`);
2659
+ console.log("");
2660
+ }
2661
+
2662
+ // src/capture.ts
2663
+ import * as fs6 from "fs";
2664
+ import * as path6 from "path";
2665
+ async function findRepoRoot(filePath) {
2666
+ let dir = path6.dirname(filePath);
2667
+ while (dir !== "/" && dir !== ".") {
2668
+ const gitDir = path6.join(dir, ".git");
2669
+ if (fs6.existsSync(gitDir)) {
2670
+ return dir;
2671
+ }
2672
+ dir = path6.dirname(dir);
2673
+ }
2674
+ return null;
2675
+ }
2676
+ function makeRelative(repoRoot, filePath) {
2677
+ if (filePath.startsWith(repoRoot)) {
2678
+ return filePath.slice(repoRoot.length + 1);
2679
+ }
2680
+ return filePath;
2681
+ }
2682
+ async function extractModelFromTranscript(transcriptPath) {
2683
+ try {
2684
+ const content = fs6.readFileSync(transcriptPath, "utf8");
2685
+ const lines = content.split(`
2686
+ `);
2687
+ for (let i = lines.length - 1;i >= 0; i--) {
2688
+ const line = lines[i].trim();
2689
+ if (!line)
2690
+ continue;
2691
+ try {
2692
+ const entry = JSON.parse(line);
2693
+ if (entry.message?.model) {
2694
+ return entry.message.model;
2695
+ }
2696
+ } catch {
2697
+ continue;
2698
+ }
2699
+ }
2700
+ return null;
2701
+ } catch {
2702
+ return null;
2703
+ }
2704
+ }
2705
+ async function extractPromptFromTranscript(transcriptPath) {
2706
+ try {
2707
+ const content = fs6.readFileSync(transcriptPath, "utf8");
2708
+ const lines = content.split(`
2709
+ `).reverse();
2710
+ for (const line of lines) {
2711
+ if (!line.trim())
2712
+ continue;
2713
+ try {
2714
+ const entry = JSON.parse(line);
2715
+ if (entry.type === "human" || entry.message?.role === "user") {
2716
+ const msg = entry.message?.content;
2717
+ if (Array.isArray(msg)) {
2718
+ const textPart = msg.find((m) => m.type === "text" || typeof m === "string");
2719
+ if (textPart) {
2720
+ return typeof textPart === "string" ? textPart : textPart.text;
2721
+ }
2722
+ } else if (typeof msg === "string") {
2723
+ return msg;
2724
+ } else if (entry.content) {
2725
+ return entry.content;
2726
+ }
2727
+ }
2728
+ } catch {
2729
+ continue;
2730
+ }
2731
+ }
2732
+ return null;
2733
+ } catch {
2734
+ return null;
2735
+ }
2736
+ }
2737
+ async function setupCaptureContext(filePath, agent, conversationId, model) {
2738
+ const repoRoot = await findRepoRoot(filePath);
2739
+ if (!repoRoot) {
2740
+ if (process.env.AGENTBLAME_DEBUG) {
2741
+ console.error(`[agentblame] No git repo found for ${filePath}`);
2742
+ }
2743
+ return null;
2744
+ }
2745
+ ensureAgentBlameDirs(repoRoot);
2746
+ const dbPath = getDatabasePath(repoRoot);
2747
+ setDatabasePath(dbPath);
2748
+ const baseSha = await getGitHead(repoRoot);
2749
+ if (!baseSha) {
2750
+ if (process.env.AGENTBLAME_DEBUG) {
2751
+ console.error(`[agentblame] No HEAD commit found in ${repoRoot}`);
2752
+ }
2753
+ return null;
2754
+ }
2755
+ const sessionId = generateSessionId(agent, conversationId);
2756
+ return {
2757
+ repoRoot,
2758
+ baseSha,
2759
+ agent,
2760
+ sessionId,
2761
+ model
2762
+ };
2763
+ }
2764
+ async function captureBeforeSnapshot(ctx, filePath) {
2765
+ const absolutePath = path6.isAbsolute(filePath) ? filePath : path6.join(ctx.repoRoot, filePath);
2766
+ const content = readFileContent(absolutePath);
2767
+ if (content === null) {
2768
+ return null;
2769
+ }
2770
+ try {
2771
+ return await storeSnapshot(ctx.repoRoot, content);
2772
+ } catch (err) {
2773
+ if (process.env.AGENTBLAME_DEBUG) {
2774
+ console.error(`[agentblame] Failed to store before snapshot:`, err);
2775
+ }
2776
+ return null;
2777
+ }
2778
+ }
2779
+ async function captureAfterSnapshot(ctx, filePath) {
2780
+ const absolutePath = path6.isAbsolute(filePath) ? filePath : path6.join(ctx.repoRoot, filePath);
2781
+ const content = readFileContent(absolutePath);
2782
+ if (content === null) {
2783
+ if (process.env.AGENTBLAME_DEBUG) {
2784
+ console.error(`[agentblame] File not found after edit: ${absolutePath}`);
2785
+ }
2786
+ return null;
2787
+ }
2788
+ try {
2789
+ const blobSha = await storeSnapshot(ctx.repoRoot, content);
2790
+ const relativePath = makeRelative(ctx.repoRoot, absolutePath);
2791
+ const entry = {
2792
+ ts: new Date().toISOString(),
2793
+ file: relativePath,
2794
+ blob: blobSha,
2795
+ session: ctx.sessionId,
2796
+ type: "ai_edit"
2797
+ };
2798
+ appendToWorkingLog(ctx.repoRoot, ctx.baseSha, entry);
2799
+ return blobSha;
2800
+ } catch (err) {
2801
+ if (process.env.AGENTBLAME_DEBUG) {
2802
+ console.error(`[agentblame] Failed to store after snapshot:`, err);
2803
+ }
2804
+ return null;
2805
+ }
2806
+ }
2807
+ function parseArgs() {
2808
+ const args = process.argv.slice(2);
2809
+ let provider = "cursor";
2810
+ let event;
2811
+ for (let i = 0;i < args.length; i++) {
2812
+ if (args[i] === "--provider" && args[i + 1]) {
2813
+ provider = args[i + 1];
2814
+ i++;
2815
+ } else if (args[i] === "--event" && args[i + 1]) {
2816
+ event = args[i + 1];
2817
+ i++;
2818
+ }
2819
+ }
2820
+ return { provider, event };
2821
+ }
2822
+ async function processCursorPayload(payload, event) {
2823
+ if (event === "afterTabFileEdit") {
2824
+ return;
2825
+ }
2826
+ if (!payload.edits || payload.edits.length === 0) {
2827
+ return;
2828
+ }
2829
+ const filePath = payload.file_path;
2830
+ if (!filePath)
2831
+ return;
2832
+ const conversationId = payload.conversation_id || `cursor-${Date.now()}`;
2833
+ const ctx = await setupCaptureContext(filePath, "cursor", conversationId, payload.model || null);
2834
+ if (!ctx)
2835
+ return;
2836
+ markAIEditStart(filePath);
2837
+ try {
2838
+ upsertSession({
2839
+ id: ctx.sessionId,
2840
+ agent: "cursor",
2841
+ model: ctx.model,
2842
+ conversationId
2843
+ });
2844
+ for (const edit of payload.edits) {
2845
+ const oldString = edit.old_string || "";
2846
+ const newString = edit.new_string || "";
2847
+ if (!newString)
2848
+ continue;
2849
+ const beforeBlob = await captureBeforeSnapshot(ctx, filePath);
2850
+ const afterBlob = await captureAfterSnapshot(ctx, filePath);
2851
+ insertToolCall({
2852
+ sessionId: ctx.sessionId,
2853
+ toolName: "edit",
2854
+ toolInput: JSON.stringify({
2855
+ file_path: makeRelative(ctx.repoRoot, filePath),
2856
+ old_string: oldString.substring(0, 500),
2857
+ new_string: newString.substring(0, 500)
2858
+ }),
2859
+ filePath: makeRelative(ctx.repoRoot, filePath),
2860
+ beforeBlob,
2861
+ afterBlob
2862
+ });
2863
+ if (process.env.AGENTBLAME_DEBUG) {
2864
+ console.error(`[agentblame] Captured Cursor edit: ${filePath} (before: ${beforeBlob}, after: ${afterBlob})`);
2865
+ }
2866
+ }
2867
+ } finally {
2868
+ markAIEditEnd(filePath);
2869
+ }
2870
+ }
2871
+ async function processClaudePayload(payload) {
2872
+ if (payload.cursor_version) {
2873
+ return;
2874
+ }
2875
+ const toolName = payload.tool_name?.toLowerCase() || "";
2876
+ if (toolName !== "edit" && toolName !== "write" && toolName !== "multiedit") {
2877
+ return;
2878
+ }
2879
+ const toolInput = payload.tool_input;
2880
+ const toolResponse = payload.tool_response;
2881
+ const filePath = toolResponse?.filePath || toolInput?.file_path || payload.file_path;
2882
+ if (!filePath)
2883
+ return;
2884
+ let model = null;
2885
+ if (payload.transcript_path) {
2886
+ model = await extractModelFromTranscript(payload.transcript_path);
2887
+ }
2888
+ if (!model) {
2889
+ model = "claude";
2890
+ }
2891
+ const conversationId = payload.session_id || `claude-${Date.now()}`;
2892
+ const ctx = await setupCaptureContext(filePath, "claude", conversationId, model);
2893
+ if (!ctx)
2894
+ return;
2895
+ markAIEditStart(filePath);
2896
+ try {
2897
+ upsertSession({
2898
+ id: ctx.sessionId,
2899
+ agent: "claude",
2900
+ model: ctx.model,
2901
+ conversationId
2902
+ });
2903
+ if (payload.transcript_path) {
2904
+ const prompt = await extractPromptFromTranscript(payload.transcript_path);
2905
+ if (prompt && !promptExists(ctx.sessionId, prompt)) {
2906
+ insertPrompt({
2907
+ sessionId: ctx.sessionId,
2908
+ content: prompt
2909
+ });
2910
+ if (process.env.AGENTBLAME_DEBUG) {
2911
+ console.error(`[agentblame] Captured prompt: ${prompt.substring(0, 100)}...`);
2912
+ }
2913
+ }
2914
+ }
2915
+ const beforeBlob = await captureBeforeSnapshot(ctx, filePath);
2916
+ const afterBlob = await captureAfterSnapshot(ctx, filePath);
2917
+ let toolInputJson;
2918
+ if (toolName === "write") {
2919
+ toolInputJson = JSON.stringify({
2920
+ file_path: makeRelative(ctx.repoRoot, filePath),
2921
+ content: (toolInput?.content || "").substring(0, 500)
2922
+ });
2923
+ } else {
2924
+ toolInputJson = JSON.stringify({
2925
+ file_path: makeRelative(ctx.repoRoot, filePath),
2926
+ old_string: (toolInput?.old_string || "").substring(0, 500),
2927
+ new_string: (toolInput?.new_string || "").substring(0, 500)
2928
+ });
2929
+ }
2930
+ insertToolCall({
2931
+ sessionId: ctx.sessionId,
2932
+ toolName: toolName === "multiedit" ? "MultiEdit" : toolName === "write" ? "Write" : "Edit",
2933
+ toolInput: toolInputJson,
2934
+ filePath: makeRelative(ctx.repoRoot, filePath),
2935
+ beforeBlob,
2936
+ afterBlob
2937
+ });
2938
+ if (process.env.AGENTBLAME_DEBUG) {
2939
+ console.error(`[agentblame] Captured Claude ${toolName}: ${filePath} (before: ${beforeBlob}, after: ${afterBlob})`);
2940
+ }
2941
+ } finally {
2942
+ markAIEditEnd(filePath);
2943
+ }
2944
+ }
2945
+ async function processOpenCodePayload(payload) {
2946
+ const filePath = payload.filePath;
2947
+ if (!filePath)
2948
+ return;
2949
+ const conversationId = payload.sessionID || `opencode-${Date.now()}`;
2950
+ const ctx = await setupCaptureContext(filePath, "opencode", conversationId, payload.model || null);
2951
+ if (!ctx)
2952
+ return;
2953
+ markAIEditStart(filePath);
2954
+ try {
2955
+ upsertSession({
2956
+ id: ctx.sessionId,
2957
+ agent: "opencode",
2958
+ model: ctx.model,
2959
+ conversationId
2960
+ });
2961
+ if (payload.prompt && !promptExists(ctx.sessionId, payload.prompt)) {
2962
+ insertPrompt({
2963
+ sessionId: ctx.sessionId,
2964
+ content: payload.prompt
2965
+ });
2966
+ }
2967
+ const beforeBlob = await captureBeforeSnapshot(ctx, filePath);
2968
+ const afterBlob = await captureAfterSnapshot(ctx, filePath);
2969
+ let toolInputJson;
2970
+ if (payload.tool === "write") {
2971
+ toolInputJson = JSON.stringify({
2972
+ file_path: makeRelative(ctx.repoRoot, filePath),
2973
+ content: (payload.content || "").substring(0, 500)
2974
+ });
2975
+ } else {
2976
+ toolInputJson = JSON.stringify({
2977
+ file_path: makeRelative(ctx.repoRoot, filePath),
2978
+ old_string: (payload.oldString || "").substring(0, 500),
2979
+ new_string: (payload.newString || "").substring(0, 500)
2980
+ });
2981
+ }
2982
+ insertToolCall({
2983
+ sessionId: ctx.sessionId,
2984
+ toolName: payload.tool === "write" ? "Write" : "Edit",
2985
+ toolInput: toolInputJson,
2986
+ filePath: makeRelative(ctx.repoRoot, filePath),
2987
+ beforeBlob,
2988
+ afterBlob
2989
+ });
2990
+ if (process.env.AGENTBLAME_DEBUG) {
2991
+ console.error(`[agentblame] Captured OpenCode ${payload.tool}: ${filePath} (before: ${beforeBlob}, after: ${afterBlob})`);
2992
+ }
2993
+ } finally {
2994
+ markAIEditEnd(filePath);
2995
+ }
2996
+ }
2997
+ async function readStdin() {
2998
+ const chunks = [];
2999
+ for await (const chunk of process.stdin) {
3000
+ chunks.push(chunk);
3001
+ }
3002
+ return Buffer.concat(chunks).toString("utf8");
3003
+ }
3004
+ async function runCapture() {
3005
+ try {
3006
+ const { provider, event } = parseArgs();
3007
+ const input = await readStdin();
3008
+ if (!input.trim()) {
3009
+ process.exit(0);
3010
+ }
3011
+ const data = JSON.parse(input);
3012
+ const payload = data.payload || data;
3013
+ if (provider === "cursor") {
3014
+ const eventName = event || data.hook_event_name || "afterFileEdit";
3015
+ await processCursorPayload(payload, eventName);
3016
+ } else if (provider === "claude") {
3017
+ await processClaudePayload(payload);
3018
+ } else if (provider === "opencode") {
3019
+ await processOpenCodePayload(payload);
3020
+ }
3021
+ process.exit(0);
3022
+ } catch (err) {
3023
+ if (process.env.AGENTBLAME_DEBUG) {
3024
+ console.error("Agent Blame capture error:", err);
3025
+ }
3026
+ process.exit(0);
3027
+ }
3028
+ }
3029
+
3030
+ // src/index.ts
3031
+ var __dirname = "/Users/murali/Code/agentblame/packages/cli/src";
3032
+ var ANALYTICS_TAG = "agentblame-analytics-anchor";
3033
+ function isBunInstalled() {
3034
+ try {
3035
+ execSync2("bun --version", { stdio: "pipe" });
3036
+ return true;
3037
+ } catch {
3038
+ return false;
3039
+ }
3040
+ }
3041
+ var args = process.argv.slice(2);
3042
+ var command = args[0];
3043
+ async function main() {
3044
+ switch (command) {
3045
+ case "init":
3046
+ await runInit(args.slice(1));
3047
+ break;
3048
+ case "clean":
3049
+ await runClean(args.slice(1));
3050
+ break;
3051
+ case "capture":
3052
+ await runCapture();
3053
+ break;
3054
+ case "blame":
3055
+ await runBlame(args.slice(1));
3056
+ break;
3057
+ case "process":
3058
+ await runProcess(args[1]);
3059
+ break;
3060
+ case "sync":
3061
+ await runSync(args.slice(1));
3062
+ break;
3063
+ case "status":
3064
+ await runStatus();
3065
+ break;
3066
+ case "prune":
3067
+ await runPrune();
3068
+ break;
3069
+ case "migrate":
3070
+ await runMigrate();
3071
+ break;
3072
+ case "--version":
3073
+ case "-v":
3074
+ printVersion();
3075
+ break;
3076
+ case "--help":
3077
+ case "-h":
3078
+ case undefined:
3079
+ printHelp();
3080
+ break;
3081
+ default:
3082
+ console.error(`Unknown command: ${command}`);
3083
+ printHelp();
3084
+ process.exit(1);
3085
+ }
3086
+ }
3087
+ function printHelp() {
3088
+ console.log(`
3089
+ Agent Blame v3 - Track AI-generated code in your commits
3090
+
3091
+ Usage:
3092
+ agentblame init Set up hooks for current repo
3093
+ agentblame init --force Set up hooks and clean up old global install
3094
+ agentblame clean Remove hooks from current repo
3095
+ agentblame clean --force Also clean up old global install
3096
+ agentblame blame <file> Show AI attribution for a file
3097
+ agentblame blame --summary Show summary only
3098
+ agentblame blame --json Output as JSON
3099
+ agentblame status Show session and capture stats
3100
+ agentblame sync Transfer notes after squash/rebase
3101
+ agentblame prune Remove old entries from database
3102
+ agentblame migrate Migrate from v2 to v3 schema
3103
+
3104
+ Examples:
3105
+ agentblame init
3106
+ agentblame blame src/index.ts
3107
+ `);
3108
+ }
3109
+ function printVersion() {
3110
+ const packageJsonPath = path7.join(__dirname, "..", "package.json");
3111
+ try {
3112
+ const packageJson = JSON.parse(fs7.readFileSync(packageJsonPath, "utf8"));
3113
+ console.log(`agentblame v${packageJson.version}`);
3114
+ } catch {
3115
+ console.log("agentblame (version unknown)");
3116
+ }
3117
+ }
3118
+ async function createAnalyticsTag(repoRoot) {
3119
+ try {
3120
+ const existingTag = await runGit(repoRoot, ["tag", "-l", ANALYTICS_TAG], 5000);
3121
+ if (existingTag.stdout.trim()) {
3122
+ return true;
3123
+ }
3124
+ const rootResult = await runGit(repoRoot, ["rev-list", "--max-parents=0", "HEAD"], 1e4);
3125
+ if (rootResult.exitCode !== 0 || !rootResult.stdout.trim()) {
3126
+ return false;
3127
+ }
3128
+ const rootLines = rootResult.stdout.trim().split(`
3129
+ `).filter(Boolean);
3130
+ if (rootLines.length === 0) {
3131
+ return false;
3132
+ }
3133
+ const rootSha = rootLines[0];
3134
+ const tagResult = await runGit(repoRoot, ["tag", ANALYTICS_TAG, rootSha], 5000);
3135
+ return tagResult.exitCode === 0;
3136
+ } catch {
3137
+ return false;
3138
+ }
3139
+ }
3140
+ async function cleanupGlobalInstall() {
3141
+ const results = { cursor: false, claude: false, db: false };
3142
+ const home = os.homedir();
3143
+ const globalCursorHooks = path7.join(home, ".cursor", "hooks.json");
3144
+ try {
3145
+ if (fs7.existsSync(globalCursorHooks)) {
3146
+ const config = JSON.parse(await fs7.promises.readFile(globalCursorHooks, "utf8"));
3147
+ if (config.hooks?.afterFileEdit) {
3148
+ config.hooks.afterFileEdit = config.hooks.afterFileEdit.filter((h) => !h?.command?.includes("agentblame") && !h?.command?.includes("capture"));
3149
+ }
3150
+ await fs7.promises.writeFile(globalCursorHooks, JSON.stringify(config, null, 2), "utf8");
3151
+ results.cursor = true;
3152
+ }
3153
+ } catch {}
3154
+ const globalClaudeSettings = path7.join(home, ".claude", "settings.json");
3155
+ try {
3156
+ if (fs7.existsSync(globalClaudeSettings)) {
3157
+ const config = JSON.parse(await fs7.promises.readFile(globalClaudeSettings, "utf8"));
3158
+ if (config.hooks?.PostToolUse) {
3159
+ config.hooks.PostToolUse = config.hooks.PostToolUse.filter((h) => !h?.hooks?.some((hh) => hh?.command?.includes("agentblame") || hh?.command?.includes("capture")));
3160
+ }
3161
+ await fs7.promises.writeFile(globalClaudeSettings, JSON.stringify(config, null, 2), "utf8");
3162
+ results.claude = true;
3163
+ }
3164
+ } catch {}
3165
+ const globalDb = path7.join(home, ".agentblame");
3166
+ try {
3167
+ if (fs7.existsSync(globalDb)) {
3168
+ await fs7.promises.rm(globalDb, { recursive: true });
3169
+ results.db = true;
3170
+ }
3171
+ } catch {}
3172
+ return results;
3173
+ }
3174
+ async function runInit(initArgs = []) {
3175
+ const forceCleanup = initArgs.includes("--force") || initArgs.includes("-f");
3176
+ if (!isBunInstalled()) {
3177
+ const installCmd = process.platform === "win32" ? 'powershell -c "irm bun.sh/install.ps1 | iex"' : "curl -fsSL https://bun.sh/install | bash";
3178
+ console.log("");
3179
+ console.log(" \x1B[31m\u2717\x1B[0m Bun is required but not installed");
3180
+ console.log("");
3181
+ console.log(" Agent Blame uses Bun to run hooks. Install it first:");
3182
+ console.log("");
3183
+ console.log(` \x1B[36m${installCmd}\x1B[0m`);
3184
+ console.log("");
3185
+ console.log(" Then restart your terminal and run this command again.");
3186
+ console.log(" Learn more: \x1B[36mhttps://bun.sh\x1B[0m");
3187
+ console.log("");
3188
+ process.exit(1);
3189
+ }
3190
+ const repoRoot = await getRepoRoot(process.cwd());
3191
+ if (!repoRoot) {
3192
+ console.log("");
3193
+ console.log(" \x1B[31m\u2717\x1B[0m Not in a git repository");
3194
+ console.log("");
3195
+ console.log(" Run this command from inside a git repository.");
3196
+ console.log("");
3197
+ process.exit(1);
3198
+ }
3199
+ console.log("");
3200
+ console.log(" \x1B[1m\x1B[35m\u25C6\x1B[0m \x1B[1mAgent Blame v3\x1B[0m");
3201
+ console.log(" \x1B[2mTrack AI-generated code in your commits\x1B[0m");
3202
+ console.log("");
3203
+ const repoName = path7.basename(repoRoot);
3204
+ console.log(` \x1B[2mRepository:\x1B[0m ${repoName}`);
3205
+ console.log("");
3206
+ if (forceCleanup) {
3207
+ console.log(" \x1B[2mCleaning up global install...\x1B[0m");
3208
+ const cleanup = await cleanupGlobalInstall();
3209
+ if (cleanup.cursor)
3210
+ console.log(" \x1B[32m\u2713\x1B[0m Removed global Cursor hooks");
3211
+ if (cleanup.claude)
3212
+ console.log(" \x1B[32m\u2713\x1B[0m Removed global Claude hooks");
3213
+ if (cleanup.db)
3214
+ console.log(" \x1B[32m\u2713\x1B[0m Removed global database");
3215
+ if (!cleanup.cursor && !cleanup.claude && !cleanup.db) {
3216
+ console.log(" \x1B[2m No global install found\x1B[0m");
3217
+ }
3218
+ console.log("");
3219
+ }
3220
+ const results = [];
3221
+ try {
3222
+ ensureAgentBlameDirs(repoRoot);
3223
+ const dbPath = getDatabasePath(repoRoot);
3224
+ setDatabasePath(dbPath);
3225
+ initDatabase();
3226
+ results.push({ name: "Database (.git/agentblame/)", success: true });
3227
+ } catch (err) {
3228
+ results.push({ name: "Database", success: false });
3229
+ }
3230
+ try {
3231
+ await initAnalytics(repoRoot);
3232
+ results.push({ name: "Analytics", success: true });
3233
+ } catch {
3234
+ results.push({ name: "Analytics", success: false });
3235
+ }
3236
+ try {
3237
+ const gitignorePath = path7.join(repoRoot, ".gitignore");
3238
+ let gitignoreContent = "";
3239
+ if (fs7.existsSync(gitignorePath)) {
3240
+ gitignoreContent = await fs7.promises.readFile(gitignorePath, "utf8");
3241
+ }
3242
+ let entriesToAdd = "";
3243
+ if (!gitignoreContent.includes(".agentblame")) {
3244
+ entriesToAdd += `
3245
+ # Agent Blame local database (legacy)
3246
+ .agentblame/
3247
+ `;
3248
+ }
3249
+ if (!gitignoreContent.includes(".opencode")) {
3250
+ entriesToAdd += `
3251
+ # OpenCode local plugin (installed by agentblame init)
3252
+ .opencode/
3253
+ `;
3254
+ }
3255
+ if (entriesToAdd) {
3256
+ await fs7.promises.appendFile(gitignorePath, entriesToAdd);
3257
+ }
3258
+ results.push({ name: "Updated .gitignore", success: true });
3259
+ } catch {
3260
+ results.push({ name: "Updated .gitignore", success: false });
3261
+ }
3262
+ const cursorSuccess = await installCursorHooks(repoRoot);
3263
+ results.push({ name: "Cursor hooks", success: cursorSuccess });
3264
+ const claudeSuccess = await installClaudeHooks(repoRoot);
3265
+ results.push({ name: "Claude Code hooks", success: claudeSuccess });
3266
+ const opencodeSuccess = await installOpenCodeHooks(repoRoot);
3267
+ results.push({ name: "OpenCode hooks", success: opencodeSuccess });
3268
+ const gitHookSuccess = await installGitHook(repoRoot);
3269
+ results.push({ name: "Git post-commit hook", success: gitHookSuccess });
3270
+ const notesPushSuccess = await configureNotesSync(repoRoot);
3271
+ results.push({ name: "Notes auto-push", success: notesPushSuccess });
3272
+ const githubActionSuccess = await installGitHubAction(repoRoot);
3273
+ results.push({ name: "GitHub Actions workflow", success: githubActionSuccess });
3274
+ const analyticsTagSuccess = await createAnalyticsTag(repoRoot);
3275
+ results.push({ name: "Analytics anchor tag", success: analyticsTagSuccess });
3276
+ console.log(" \x1B[2m\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\x1B[0m");
3277
+ console.log("");
3278
+ for (const result of results) {
3279
+ const icon = result.success ? "\x1B[32m\u2713\x1B[0m" : "\x1B[31m\u2717\x1B[0m";
3280
+ console.log(` ${icon} ${result.name}`);
3281
+ }
3282
+ const allSuccess = results.every((r) => r.success);
3283
+ const anySuccess = results.some((r) => r.success);
3284
+ console.log("");
3285
+ console.log(" \x1B[2m\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\x1B[0m");
3286
+ console.log("");
3287
+ if (allSuccess) {
3288
+ console.log(" \x1B[32m\u2713\x1B[0m \x1B[1mSetup complete\x1B[0m");
3289
+ } else if (anySuccess) {
3290
+ console.log(" \x1B[33m!\x1B[0m \x1B[1mSetup completed with warnings\x1B[0m");
3291
+ } else {
3292
+ console.log(" \x1B[31m\u2717\x1B[0m \x1B[1mSetup failed\x1B[0m");
3293
+ }
3294
+ console.log("");
3295
+ console.log(" \x1B[1mNext steps:\x1B[0m");
3296
+ console.log(" \x1B[33m1.\x1B[0m Commit \x1B[36m.github/workflows/agentblame.yml\x1B[0m to enable PR analytics");
3297
+ console.log(" \x1B[33m2.\x1B[0m Restart Cursor or Claude Code to pick up hooks");
3298
+ console.log(" \x1B[33m3.\x1B[0m Make AI edits and commit your changes");
3299
+ console.log(" \x1B[33m4.\x1B[0m Run \x1B[36magentblame blame <file>\x1B[0m to see attribution");
3300
+ console.log("");
3301
+ }
3302
+ async function runClean(uninstallArgs = []) {
3303
+ const forceCleanup = uninstallArgs.includes("--force") || uninstallArgs.includes("-f");
3304
+ const repoRoot = await getRepoRoot(process.cwd());
3305
+ if (!repoRoot) {
3306
+ console.log("");
3307
+ console.log(" \x1B[31m\u2717\x1B[0m Not in a git repository");
3308
+ console.log("");
3309
+ console.log(" Run this command from inside a git repository.");
3310
+ console.log("");
3311
+ process.exit(1);
3312
+ }
3313
+ console.log("");
3314
+ console.log(" \x1B[1m\x1B[35m\u25C6\x1B[0m \x1B[1mAgent Blame\x1B[0m");
3315
+ console.log(" \x1B[2mRemoving hooks and configuration\x1B[0m");
3316
+ console.log("");
3317
+ const repoName = path7.basename(repoRoot);
3318
+ console.log(` \x1B[2mRepository:\x1B[0m ${repoName}`);
3319
+ console.log("");
3320
+ if (forceCleanup) {
3321
+ console.log(" \x1B[2mCleaning up global install...\x1B[0m");
3322
+ const cleanup = await cleanupGlobalInstall();
3323
+ if (cleanup.cursor)
3324
+ console.log(" \x1B[32m\u2713\x1B[0m Removed global Cursor hooks");
3325
+ if (cleanup.claude)
3326
+ console.log(" \x1B[32m\u2713\x1B[0m Removed global Claude hooks");
3327
+ if (cleanup.db)
3328
+ console.log(" \x1B[32m\u2713\x1B[0m Removed global database");
3329
+ if (!cleanup.cursor && !cleanup.claude && !cleanup.db) {
3330
+ console.log(" \x1B[2m No global install found\x1B[0m");
3331
+ }
3332
+ console.log("");
3333
+ }
3334
+ const results = [];
3335
+ const cursorSuccess = await uninstallCursorHooks(repoRoot);
3336
+ results.push({ name: "Cursor hooks", success: cursorSuccess });
3337
+ const claudeSuccess = await uninstallClaudeHooks(repoRoot);
3338
+ results.push({ name: "Claude Code hooks", success: claudeSuccess });
3339
+ const opencodeSuccess = await uninstallOpenCodeHooks(repoRoot);
3340
+ results.push({ name: "OpenCode hooks", success: opencodeSuccess });
3341
+ const gitHookSuccess = await uninstallGitHook(repoRoot);
3342
+ results.push({ name: "Git post-commit hook", success: gitHookSuccess });
3343
+ const notesPushSuccess = await removeNotesSync(repoRoot);
3344
+ results.push({ name: "Notes auto-push", success: notesPushSuccess });
3345
+ const githubActionSuccess = await uninstallGitHubAction(repoRoot);
3346
+ results.push({ name: "GitHub Actions workflow", success: githubActionSuccess });
3347
+ const agentBlameGitDir = getAgentBlameGitDir(repoRoot);
3348
+ let dbSuccess = true;
3349
+ try {
3350
+ if (fs7.existsSync(agentBlameGitDir)) {
3351
+ await fs7.promises.rm(agentBlameGitDir, { recursive: true });
3352
+ }
3353
+ } catch {
3354
+ dbSuccess = false;
3355
+ }
3356
+ results.push({ name: "Database (.git/agentblame/)", success: dbSuccess });
3357
+ const legacyDir = path7.join(repoRoot, ".agentblame");
3358
+ try {
3359
+ if (fs7.existsSync(legacyDir)) {
3360
+ await fs7.promises.rm(legacyDir, { recursive: true });
3361
+ results.push({ name: "Legacy database (.agentblame/)", success: true });
3362
+ }
3363
+ } catch {}
3364
+ console.log(" \x1B[2m\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\x1B[0m");
3365
+ console.log("");
3366
+ for (const result of results) {
3367
+ const icon = result.success ? "\x1B[32m\u2713\x1B[0m" : "\x1B[31m\u2717\x1B[0m";
3368
+ console.log(` ${icon} ${result.name}`);
3369
+ }
3370
+ const allSuccess = results.every((r) => r.success);
3371
+ console.log("");
3372
+ console.log(" \x1B[2m\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\x1B[0m");
3373
+ console.log("");
3374
+ if (allSuccess) {
3375
+ console.log(" \x1B[32m\u2713\x1B[0m \x1B[1mUninstall complete\x1B[0m");
3376
+ } else {
3377
+ console.log(" \x1B[33m!\x1B[0m \x1B[1mUninstall completed with warnings\x1B[0m");
3378
+ }
3379
+ console.log("");
3380
+ }
3381
+ async function runBlame(args2) {
3382
+ const options = {};
3383
+ let filePath;
3384
+ for (const arg of args2) {
3385
+ if (arg === "--json") {
3386
+ options.json = true;
3387
+ } else if (arg === "--summary") {
3388
+ options.summary = true;
3389
+ } else if (arg === "--prompts" || arg === "-p") {
3390
+ options.showPrompts = true;
3391
+ } else if (!arg.startsWith("-")) {
3392
+ filePath = arg;
3393
+ }
3394
+ }
3395
+ if (!filePath) {
3396
+ console.error("Usage: agentblame blame [--json|--summary|--prompts] <file>");
3397
+ process.exit(1);
3398
+ }
3399
+ await blame(filePath, options);
3400
+ }
3401
+ async function runSync(args2) {
3402
+ const options = {};
3403
+ for (const arg of args2) {
3404
+ if (arg === "--dry-run") {
3405
+ options.dryRun = true;
3406
+ } else if (arg === "--verbose" || arg === "-v") {
3407
+ options.verbose = true;
3408
+ }
3409
+ }
3410
+ await sync(options);
3411
+ }
3412
+ async function runStatus() {
3413
+ const repoRoot = await getRepoRoot(process.cwd());
3414
+ if (!repoRoot) {
3415
+ console.error("Not in a git repository");
3416
+ process.exit(1);
3417
+ }
3418
+ const dbPath = getDatabasePath(repoRoot);
3419
+ setDatabasePath(dbPath);
3420
+ console.log(`
3421
+ Agent Blame v3 Status
3422
+ `);
3423
+ try {
3424
+ const stats = getStats();
3425
+ console.log(`Sessions: ${stats.sessions}`);
3426
+ console.log(`Prompts: ${stats.prompts}`);
3427
+ console.log(`Tool Calls: ${stats.toolCalls}`);
3428
+ if (stats.sessions > 0) {
3429
+ console.log(`
3430
+ Recent sessions:`);
3431
+ const recent = getRecentSessions(5);
3432
+ for (const session of recent) {
3433
+ const time = new Date(session.createdAt).toLocaleTimeString();
3434
+ const agent = session.agent;
3435
+ const model = session.model || "";
3436
+ const committed = session.firstCommitSha ? "committed" : "pending";
3437
+ console.log(` [${agent}] ${session.id.slice(0, 8)} - ${model || "unknown"} (${committed}) at ${time}`);
3438
+ }
3439
+ if (stats.sessions > 5) {
3440
+ console.log(` ... and ${stats.sessions - 5} more`);
3441
+ }
3442
+ }
3443
+ } catch (err) {
3444
+ console.log(" No database found. Run 'agentblame init' first.");
3445
+ }
3446
+ console.log("");
3447
+ }
3448
+ async function runPrune() {
3449
+ const repoRoot = await getRepoRoot(process.cwd());
3450
+ if (!repoRoot) {
3451
+ console.error("Not in a git repository");
3452
+ process.exit(1);
3453
+ }
3454
+ const dbPath = getDatabasePath(repoRoot);
3455
+ setDatabasePath(dbPath);
3456
+ console.log(`
3457
+ Agent Blame Prune
3458
+ `);
3459
+ const dbResult = cleanupOldEntries();
3460
+ console.log(` Database: Removed ${dbResult.removed} sessions, kept ${dbResult.kept}`);
3461
+ const workingResult = await cleanupStaleWorkingDirs(repoRoot);
3462
+ console.log(` Working dirs: Cleaned ${workingResult.cleaned.length}, kept ${workingResult.kept.length}`);
3463
+ console.log(`
3464
+ Prune complete!`);
3465
+ }
3466
+ async function runMigrate() {
3467
+ const repoRoot = await getRepoRoot(process.cwd());
3468
+ if (!repoRoot) {
3469
+ console.error("Not in a git repository");
3470
+ process.exit(1);
3471
+ }
3472
+ console.log(`
3473
+ Agent Blame v3 Migration
3474
+ `);
3475
+ ensureAgentBlameDirs(repoRoot);
3476
+ const dbPath = getDatabasePath(repoRoot);
3477
+ setDatabasePath(dbPath);
3478
+ console.log(" Resetting database to v3 schema...");
3479
+ resetDatabase();
3480
+ console.log(" \x1B[32m\u2713\x1B[0m Database migrated");
3481
+ console.log(" Initializing analytics...");
3482
+ await initAnalytics(repoRoot);
3483
+ console.log(" \x1B[32m\u2713\x1B[0m Analytics initialized");
3484
+ const legacyDir = path7.join(repoRoot, ".agentblame");
3485
+ if (fs7.existsSync(legacyDir)) {
3486
+ console.log(" Removing legacy database...");
3487
+ await fs7.promises.rm(legacyDir, { recursive: true });
3488
+ console.log(" \x1B[32m\u2713\x1B[0m Legacy database removed");
3489
+ }
3490
+ console.log(`
3491
+ \x1B[32m\u2713\x1B[0m Migration complete!`);
3492
+ console.log(`
3493
+ Note: Existing v2 git notes remain readable.`);
3494
+ console.log(`New commits will use the v3 format.
3495
+ `);
3496
+ }
3497
+ main().catch((err) => {
3498
+ console.error(err);
3499
+ process.exit(1);
3500
+ });