@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
package/dist/capture.js CHANGED
@@ -1,18 +1,15 @@
1
1
  #!/usr/bin/env bun
2
2
  "use strict";
3
3
  /**
4
- * Agent Blame Hook Capture
4
+ * Agent Blame Hook Capture v3
5
5
  *
6
- * Captures AI-generated code from Cursor and Claude Code hooks.
7
- * Performs line-level hashing for precise attribution matching.
6
+ * Captures AI-generated code from Cursor, Claude Code, and OpenCode hooks.
7
+ * Uses delta-based tracking for accurate line attribution.
8
8
  *
9
9
  * Usage:
10
10
  * echo '{"payload": ...}' | bun run capture.ts --provider cursor --event afterFileEdit
11
11
  * echo '{"payload": ...}' | bun run capture.ts --provider claude
12
- *
13
- * Note: We only track afterFileEdit (Composer/Agent mode).
14
- * Tab completions (afterTabFileEdit) are NOT tracked because they fire
15
- * as fragments that cannot be reliably matched to commits.
12
+ * echo '{"payload": ...}' | bun run capture.ts --provider opencode
16
13
  */
17
14
  var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
18
15
  if (k2 === undefined) k2 = k;
@@ -49,164 +46,59 @@ var __importStar = (this && this.__importStar) || (function () {
49
46
  })();
50
47
  Object.defineProperty(exports, "__esModule", { value: true });
51
48
  exports.runCapture = runCapture;
52
- const crypto = __importStar(require("node:crypto"));
53
- const diff_1 = require("diff");
49
+ const fs = __importStar(require("node:fs"));
50
+ const path = __importStar(require("node:path"));
54
51
  const database_1 = require("./lib/database");
52
+ const config_1 = require("./lib/config");
53
+ const storage_1 = require("./lib/storage");
54
+ const checkpoint_1 = require("./lib/checkpoint");
55
55
  const util_1 = require("./lib/util");
56
+ const delta_1 = require("./lib/delta");
56
57
  // =============================================================================
57
58
  // Utilities
58
59
  // =============================================================================
59
- function computeHash(content) {
60
- return `sha256:${crypto.createHash("sha256").update(content).digest("hex")}`;
61
- }
62
- function computeNormalizedHash(content) {
63
- const normalized = content.replace(/\s+/g, "");
64
- return `sha256:${crypto.createHash("sha256").update(normalized).digest("hex")}`;
65
- }
66
- /**
67
- * Extract only the added lines from a diff between old and new text.
68
- */
69
- function extractAddedContent(oldText, newText) {
70
- // Normalize trailing newlines to avoid false positives.
71
- // diffLines treats "line" and "line\n" as different, so when a line
72
- // goes from being last (no \n) to having content after it (has \n),
73
- // it gets marked as "added" even though the content is identical.
74
- const normalize = (text) => {
75
- if (!text)
76
- return "";
77
- return text.endsWith("\n") ? text : text + "\n";
78
- };
79
- const parts = (0, diff_1.diffLines)(normalize(oldText), normalize(newText));
80
- const addedParts = [];
81
- for (const part of parts) {
82
- if (part.added) {
83
- addedParts.push(part.value ?? "");
84
- }
85
- }
86
- return addedParts.join("");
87
- }
88
- /**
89
- * Determine the edit type based on old and new content
90
- */
91
- function determineEditType(oldContent, newContent) {
92
- if (!oldContent || oldContent.trim() === "") {
93
- return "addition";
94
- }
95
- if (newContent.includes(oldContent)) {
96
- return "modification"; // New content contains old content (added to it)
97
- }
98
- return "replacement"; // Old content was replaced
99
- }
100
- /**
101
- * Hash each line individually for precise matching
102
- */
103
- function hashLines(content) {
104
- const lines = content.split("\n");
105
- const result = [];
106
- for (const line of lines) {
107
- // Skip empty lines for hashing purposes but keep them for content
108
- if (!line.trim())
109
- continue;
110
- result.push({
111
- content: line,
112
- hash: computeHash(line),
113
- hashNormalized: computeNormalizedHash(line),
114
- });
60
+ async function findRepoRoot(filePath) {
61
+ const absolutePath = path.isAbsolute(filePath)
62
+ ? filePath
63
+ : path.resolve(process.cwd(), filePath);
64
+ let dir;
65
+ try {
66
+ const stat = fs.statSync(absolutePath);
67
+ dir = stat.isDirectory() ? absolutePath : path.dirname(absolutePath);
115
68
  }
116
- return result;
117
- }
118
- /**
119
- * Hash lines with line numbers and context (for Claude structuredPatch)
120
- */
121
- function hashLinesWithNumbers(lines, allFileLines) {
122
- const result = [];
123
- for (const { content, lineNumber } of lines) {
124
- // Skip empty lines
125
- if (!content.trim())
126
- continue;
127
- // Get context (3 lines before and after)
128
- const contextBefore = allFileLines
129
- .slice(Math.max(0, lineNumber - 4), lineNumber - 1)
130
- .join("\n");
131
- const contextAfter = allFileLines
132
- .slice(lineNumber, Math.min(allFileLines.length, lineNumber + 3))
133
- .join("\n");
134
- result.push({
135
- content,
136
- hash: computeHash(content),
137
- hashNormalized: computeNormalizedHash(content),
138
- lineNumber,
139
- contextBefore: contextBefore || undefined,
140
- contextAfter: contextAfter || undefined,
141
- });
69
+ catch {
70
+ dir = path.dirname(absolutePath);
142
71
  }
143
- return result;
144
- }
145
- /**
146
- * Parse Claude Code's structuredPatch to extract added lines with line numbers
147
- */
148
- function parseStructuredPatch(hunks, originalFileLines) {
149
- const addedLines = [];
150
- for (const hunk of hunks) {
151
- let newLineNumber = hunk.newStart;
152
- for (const line of hunk.lines) {
153
- if (line.startsWith("+")) {
154
- // Added line - strip the + prefix
155
- addedLines.push({
156
- content: line.slice(1),
157
- lineNumber: newLineNumber,
158
- });
159
- newLineNumber++;
160
- }
161
- else if (line.startsWith("-")) {
162
- // Deleted line - don't increment new line number
163
- continue;
164
- }
165
- else {
166
- // Context line (starts with space) - increment line number
167
- newLineNumber++;
168
- }
72
+ while (dir !== "/" && dir !== ".") {
73
+ const gitDir = path.join(dir, ".git");
74
+ if (fs.existsSync(gitDir)) {
75
+ return dir;
169
76
  }
77
+ dir = path.dirname(dir);
170
78
  }
171
- return addedLines;
79
+ return null;
172
80
  }
173
- /**
174
- * Read a file and return its lines (for Cursor line number derivation)
175
- */
176
- async function readFileLines(filePath) {
177
- try {
178
- const fs = await Promise.resolve().then(() => __importStar(require("node:fs/promises")));
179
- const content = await fs.readFile(filePath, "utf8");
180
- return content.split("\n");
181
- }
182
- catch {
183
- return null;
81
+ function makeRelative(repoRoot, filePath) {
82
+ if (filePath.startsWith(repoRoot)) {
83
+ return filePath.slice(repoRoot.length + 1);
184
84
  }
85
+ return filePath;
185
86
  }
186
- /**
187
- * Extract model name from Claude Code transcript file.
188
- * The transcript is a JSONL file where assistant messages contain the model field.
189
- * We read from the end to find the most recent model used.
190
- */
191
87
  async function extractModelFromTranscript(transcriptPath) {
192
88
  try {
193
- const fs = await Promise.resolve().then(() => __importStar(require("node:fs/promises")));
194
- const content = await fs.readFile(transcriptPath, "utf8");
89
+ const content = fs.readFileSync(transcriptPath, "utf8");
195
90
  const lines = content.split("\n");
196
- // Read from the end to find the most recent assistant message with model info
197
91
  for (let i = lines.length - 1; i >= 0; i--) {
198
92
  const line = lines[i].trim();
199
93
  if (!line)
200
94
  continue;
201
95
  try {
202
96
  const entry = JSON.parse(line);
203
- // Assistant messages have message.model field
204
97
  if (entry.message?.model) {
205
98
  return entry.message.model;
206
99
  }
207
100
  }
208
101
  catch {
209
- // Skip malformed lines
210
102
  continue;
211
103
  }
212
104
  }
@@ -216,50 +108,210 @@ async function extractModelFromTranscript(transcriptPath) {
216
108
  return null;
217
109
  }
218
110
  }
219
- /**
220
- * Find where old_string exists in file and return line numbers for new_string
221
- * Returns null if old_string not found (new file or complex edit)
222
- */
223
- function findEditLocation(fileLines, oldString, newString) {
224
- if (!oldString) {
225
- // New content with no old string - can't determine line numbers without more context
111
+ async function extractPromptFromTranscript(transcriptPath) {
112
+ try {
113
+ const content = fs.readFileSync(transcriptPath, "utf8");
114
+ const lines = content.split("\n").reverse();
115
+ for (const line of lines) {
116
+ if (!line.trim())
117
+ continue;
118
+ try {
119
+ const entry = JSON.parse(line);
120
+ if (entry.type === "human" || entry.message?.role === "user") {
121
+ const msg = entry.message?.content;
122
+ if (Array.isArray(msg)) {
123
+ const textPart = msg.find((m) => m.type === "text" || typeof m === "string");
124
+ if (textPart) {
125
+ return typeof textPart === "string" ? textPart : textPart.text;
126
+ }
127
+ }
128
+ else if (typeof msg === "string") {
129
+ return msg;
130
+ }
131
+ else if (entry.content) {
132
+ return entry.content;
133
+ }
134
+ }
135
+ }
136
+ catch {
137
+ continue;
138
+ }
139
+ }
226
140
  return null;
227
141
  }
228
- const fileContent = fileLines.join("\n");
229
- const oldIndex = fileContent.indexOf(oldString);
230
- if (oldIndex === -1) {
142
+ catch {
231
143
  return null;
232
144
  }
233
- // Count lines before the match to get line number
234
- const linesBefore = fileContent.slice(0, oldIndex).split("\n").length;
235
- const startLine = linesBefore;
236
- // Calculate what the new file will look like after the edit
237
- const newFileContent = fileContent.replace(oldString, newString);
238
- const newFileLines = newFileContent.split("\n");
239
- // Find the added lines by comparing old and new
240
- const addedContent = extractAddedContent(oldString, newString);
241
- if (!addedContent.trim()) {
145
+ }
146
+ async function setupCaptureContext(filePath, agent, conversationId, model) {
147
+ const repoRoot = await findRepoRoot(filePath);
148
+ if (!repoRoot) {
149
+ if (process.env.AGENTBLAME_DEBUG) {
150
+ console.error(`[agentblame] No git repo found for ${filePath}`);
151
+ }
242
152
  return null;
243
153
  }
244
- const addedLines = addedContent.split("\n").filter(l => l.trim());
245
- const result = [];
246
- // Find each added line in the new content
247
- let searchStart = startLine - 1;
248
- for (const addedLine of addedLines) {
249
- if (!addedLine.trim())
250
- continue;
251
- for (let i = searchStart; i < newFileLines.length; i++) {
252
- if (newFileLines[i] === addedLine || newFileLines[i].trim() === addedLine.trim()) {
253
- result.push({
254
- content: addedLine,
255
- lineNumber: i + 1, // 1-indexed
256
- });
257
- searchStart = i + 1;
258
- break;
154
+ (0, storage_1.ensureAgentBlameDirs)(repoRoot);
155
+ const dbPath = (0, storage_1.getDatabasePath)(repoRoot);
156
+ (0, database_1.setDatabasePath)(dbPath);
157
+ const baseSha = await (0, storage_1.getGitHead)(repoRoot);
158
+ if (!baseSha) {
159
+ if (process.env.AGENTBLAME_DEBUG) {
160
+ console.error(`[agentblame] No HEAD commit found in ${repoRoot}`);
161
+ }
162
+ return null;
163
+ }
164
+ // Smart cleanup: only clean up working directories for commits that:
165
+ // 1. Are ancestors of current HEAD (we've moved past them)
166
+ // 2. Have git notes (deltas were processed)
167
+ // This preserves deltas for reset/recommit and stash scenarios
168
+ await (0, storage_1.cleanupProcessedWorkingDirs)(repoRoot, baseSha);
169
+ const sessionId = (0, database_1.generateSessionId)(agent, conversationId);
170
+ const normalizedModel = (0, util_1.normalizeModelName)(model);
171
+ // Load config setting for prompt content storage
172
+ const storePromptContent = await (0, config_1.getConfig)(repoRoot, 'storePromptContent');
173
+ return {
174
+ repoRoot,
175
+ baseSha,
176
+ agent,
177
+ sessionId,
178
+ model: normalizedModel,
179
+ storePromptContent,
180
+ };
181
+ }
182
+ // =============================================================================
183
+ // Delta Capture (v3 - The Only Way)
184
+ // =============================================================================
185
+ /**
186
+ * Detect and record human edits since last checkpoint.
187
+ *
188
+ * IMPORTANT: We need to be careful not to mark another AI tool's edits as human.
189
+ * The most reliable baseline is the most recent delta's afterBlob (from any session).
190
+ * This handles:
191
+ * 1. Cross-session detection (another AI edited the file)
192
+ * 2. Same-session async race (PostToolUse is async, so checkpoint might be stale)
193
+ *
194
+ * Priority:
195
+ * 1. Last delta's afterBlob (most reliable - represents actual file state)
196
+ * 2. Checkpoint content (fallback if no delta exists)
197
+ */
198
+ async function detectAndRecordHumanEdits(ctx, conversationId, filePath) {
199
+ const absolutePath = path.isAbsolute(filePath)
200
+ ? filePath
201
+ : path.join(ctx.repoRoot, filePath);
202
+ const relativePath = makeRelative(ctx.repoRoot, absolutePath);
203
+ // Read current file content first
204
+ const currentContent = (0, storage_1.readFileContent)(absolutePath);
205
+ if (currentContent === null) {
206
+ return;
207
+ }
208
+ if (process.env.AGENTBLAME_DEBUG) {
209
+ console.error(`[agentblame] detectHumanEdits: ${relativePath}`);
210
+ }
211
+ // Priority 1: Use the last delta's afterBlob as baseline (most reliable)
212
+ // This handles both cross-session AND same-session scenarios
213
+ // (e.g., when async PostToolUse writes delta but checkpoint is stale)
214
+ let beforeContent = null;
215
+ const lastDelta = (0, delta_1.getLastDeltaForFile)(ctx.repoRoot, ctx.baseSha, relativePath);
216
+ if (process.env.AGENTBLAME_DEBUG) {
217
+ console.error(`[agentblame] lastDelta: ${lastDelta ? `session=${lastDelta.sessionId?.slice(0, 8) || 'human'}, afterBlob=${lastDelta.afterBlob?.slice(0, 8)}` : 'null'}`);
218
+ }
219
+ if (lastDelta?.afterBlob) {
220
+ try {
221
+ beforeContent = await (0, storage_1.loadSnapshot)(ctx.repoRoot, lastDelta.afterBlob);
222
+ if (process.env.AGENTBLAME_DEBUG) {
223
+ console.error(`[agentblame] Loaded afterBlob: ${beforeContent?.length} chars`);
259
224
  }
260
225
  }
226
+ catch (err) {
227
+ if (process.env.AGENTBLAME_DEBUG) {
228
+ console.error(`[agentblame] Failed to load afterBlob: ${err}`);
229
+ }
230
+ // Fall through to checkpoint
231
+ }
232
+ }
233
+ // Priority 2: Fall back to checkpoint if no delta afterBlob available
234
+ if (beforeContent === null) {
235
+ beforeContent = await (0, checkpoint_1.getCheckpointContent)(ctx.repoRoot, conversationId, relativePath);
236
+ if (process.env.AGENTBLAME_DEBUG) {
237
+ console.error(`[agentblame] checkpoint content: ${beforeContent ? beforeContent.length + ' chars' : 'null'}`);
238
+ }
239
+ }
240
+ // No baseline available - can't detect changes
241
+ if (beforeContent === null) {
242
+ if (process.env.AGENTBLAME_DEBUG) {
243
+ console.error(`[agentblame] No baseline - skipping`);
244
+ }
245
+ return;
246
+ }
247
+ const hunks = (0, delta_1.computeDiff)(beforeContent, currentContent);
248
+ if (process.env.AGENTBLAME_DEBUG) {
249
+ console.error(`[agentblame] beforeContent: ${beforeContent.length} chars, currentContent: ${currentContent.length} chars`);
250
+ console.error(`[agentblame] diff hunks: ${hunks.length} (${hunks.map(h => `L${h.newStart}+${h.newCount}`).join(',')})`);
251
+ }
252
+ if (hunks.length === 0) {
253
+ // Update checkpoint even if no changes (keeps it current)
254
+ await (0, checkpoint_1.captureFileCheckpoint)(ctx.repoRoot, conversationId, relativePath);
255
+ return;
256
+ }
257
+ // Store afterBlob for cross-session detection
258
+ const afterBlob = await (0, storage_1.storeSnapshot)(ctx.repoRoot, currentContent);
259
+ if (process.env.AGENTBLAME_DEBUG) {
260
+ console.error(`[agentblame] RECORDING HUMAN DELTA: ${relativePath} with ${hunks.length} hunks`);
261
+ }
262
+ const humanDelta = (0, delta_1.createHumanDelta)(relativePath, hunks, afterBlob);
263
+ (0, delta_1.appendDelta)(ctx.repoRoot, ctx.baseSha, humanDelta);
264
+ // Update checkpoint to current state
265
+ await (0, checkpoint_1.captureFileCheckpoint)(ctx.repoRoot, conversationId, relativePath);
266
+ if (process.env.AGENTBLAME_DEBUG) {
267
+ console.error(`[agentblame] Detected human edit: ${relativePath} (${hunks.length} hunks)`);
268
+ }
269
+ }
270
+ /**
271
+ * Record an AI edit delta.
272
+ */
273
+ async function recordAIDelta(ctx, filePath, beforeContent, afterContent) {
274
+ const absolutePath = path.isAbsolute(filePath)
275
+ ? filePath
276
+ : path.join(ctx.repoRoot, filePath);
277
+ const relativePath = makeRelative(ctx.repoRoot, absolutePath);
278
+ const before = beforeContent ?? "";
279
+ const hunks = (0, delta_1.computeDiff)(before, afterContent);
280
+ if (hunks.length === 0) {
281
+ if (process.env.AGENTBLAME_DEBUG) {
282
+ console.error(`[agentblame] recordAIDelta: no diff for ${relativePath} (before=${before.length} chars, after=${afterContent.length} chars)`);
283
+ }
284
+ return;
285
+ }
286
+ // Store afterBlob for cross-session human edit detection
287
+ const afterBlob = await (0, storage_1.storeSnapshot)(ctx.repoRoot, afterContent);
288
+ const latestPrompt = (0, database_1.getLatestPromptForSession)(ctx.sessionId);
289
+ const promptId = latestPrompt?.id ?? null;
290
+ const aiDelta = (0, delta_1.createAIDelta)(relativePath, ctx.sessionId, promptId, hunks, afterBlob);
291
+ (0, delta_1.appendDelta)(ctx.repoRoot, ctx.baseSha, aiDelta);
292
+ if (process.env.AGENTBLAME_DEBUG) {
293
+ console.error(`[agentblame] Recorded AI delta: ${relativePath} (${hunks.length} hunks, prompt: ${promptId}, lines: ${hunks.map(h => `${h.newStart}+${h.newCount}`).join(',')})`);
294
+ console.error(`[agentblame] stored path: "${relativePath}"`);
295
+ }
296
+ }
297
+ /**
298
+ * Get before content from checkpoint or git HEAD.
299
+ */
300
+ async function getBeforeContent(ctx, conversationId, filePath) {
301
+ const absolutePath = path.isAbsolute(filePath)
302
+ ? filePath
303
+ : path.join(ctx.repoRoot, filePath);
304
+ const relativePath = makeRelative(ctx.repoRoot, absolutePath);
305
+ const beforeBlob = await (0, checkpoint_1.getBeforeBlob)(ctx.repoRoot, conversationId, relativePath);
306
+ if (!beforeBlob) {
307
+ return null;
308
+ }
309
+ try {
310
+ return await (0, storage_1.loadSnapshot)(ctx.repoRoot, beforeBlob);
311
+ }
312
+ catch {
313
+ return null;
261
314
  }
262
- return result.length > 0 ? result : null;
263
315
  }
264
316
  // =============================================================================
265
317
  // Payload Processing
@@ -280,316 +332,267 @@ function parseArgs() {
280
332
  }
281
333
  return { provider, event };
282
334
  }
283
- /**
284
- * Save an edit to the SQLite database
285
- */
286
- function saveEdit(edit) {
287
- (0, database_1.insertEdit)({
288
- timestamp: edit.timestamp,
289
- provider: edit.provider,
290
- filePath: edit.filePath,
291
- model: edit.model,
292
- content: edit.content,
293
- contentHash: edit.contentHash,
294
- contentHashNormalized: edit.contentHashNormalized,
295
- editType: edit.editType,
296
- oldContent: edit.oldContent,
297
- lines: edit.lines,
298
- sessionId: edit.sessionId,
299
- toolUseId: edit.toolUseId,
335
+ async function processCursorBeforeSubmitPrompt(payload) {
336
+ const workspaceRoot = payload.workspace_roots?.[0];
337
+ if (!workspaceRoot)
338
+ return;
339
+ const conversationId = payload.conversation_id || `cursor-${Date.now()}`;
340
+ const ctx = await setupCaptureContext(workspaceRoot, "cursor", conversationId, payload.model || null);
341
+ if (!ctx)
342
+ return;
343
+ (0, database_1.upsertSession)({
344
+ id: ctx.sessionId,
345
+ agent: "cursor",
346
+ model: ctx.model,
347
+ conversationId,
300
348
  });
349
+ // Detect human edits on existing checkpoints, then capture new checkpoint
350
+ try {
351
+ const existingCheckpoint = (0, checkpoint_1.loadCheckpoint)(ctx.repoRoot, conversationId);
352
+ if (existingCheckpoint) {
353
+ for (const filePath of Object.keys(existingCheckpoint.files)) {
354
+ await detectAndRecordHumanEdits(ctx, conversationId, filePath);
355
+ }
356
+ }
357
+ await (0, checkpoint_1.captureBeforePromptCheckpoint)(ctx.repoRoot, conversationId);
358
+ }
359
+ catch (err) {
360
+ if (process.env.AGENTBLAME_DEBUG) {
361
+ console.error(`[agentblame] Failed to capture checkpoint:`, err);
362
+ }
363
+ }
364
+ // Store prompt if provided
365
+ const prompt = payload.prompt;
366
+ if (prompt) {
367
+ const contentHash = (0, database_1.hashPromptContent)(prompt);
368
+ if (!(0, database_1.promptExists)(ctx.sessionId, contentHash)) {
369
+ (0, database_1.insertPrompt)({
370
+ sessionId: ctx.sessionId,
371
+ content: ctx.storePromptContent ? prompt : null,
372
+ contentHash,
373
+ });
374
+ if (process.env.AGENTBLAME_DEBUG) {
375
+ console.error(`[agentblame] Captured prompt: ${prompt.substring(0, 100)}...`);
376
+ }
377
+ }
378
+ }
301
379
  }
302
380
  async function processCursorPayload(payload, event) {
303
- const edits = [];
304
- const timestamp = new Date().toISOString();
305
- // Only process afterFileEdit (Composer/Agent mode)
306
- // Skip afterTabFileEdit - tab completions fire as fragments that can't be matched
381
+ if (event === "beforeSubmitPrompt") {
382
+ await processCursorBeforeSubmitPrompt(payload);
383
+ return;
384
+ }
307
385
  if (event === "afterTabFileEdit") {
308
- return edits;
386
+ return;
309
387
  }
310
388
  if (!payload.edits || payload.edits.length === 0) {
311
- return edits;
312
- }
313
- // Read the file to derive line numbers (Cursor doesn't provide them)
314
- const fileLines = await readFileLines(payload.file_path);
315
- for (const edit of payload.edits) {
316
- const oldString = edit.old_string || "";
317
- const newString = edit.new_string || "";
318
- if (!newString)
319
- continue;
320
- // Extract only the added content
321
- const addedContent = extractAddedContent(oldString, newString);
322
- if (!addedContent.trim())
323
- continue;
324
- let lines;
325
- // Try to derive line numbers if we have the file
326
- if (fileLines && oldString) {
327
- const linesWithNumbers = findEditLocation(fileLines, oldString, newString);
328
- if (linesWithNumbers && linesWithNumbers.length > 0) {
329
- lines = hashLinesWithNumbers(linesWithNumbers, fileLines);
330
- }
331
- else {
332
- // Fallback to basic hashing without line numbers
333
- lines = hashLines(addedContent);
334
- }
335
- }
336
- else {
337
- // No file or no old_string - hash without line numbers
338
- lines = hashLines(addedContent);
339
- }
340
- if (lines.length === 0)
341
- continue;
342
- edits.push({
343
- timestamp,
344
- provider: "cursor",
345
- filePath: payload.file_path,
346
- model: payload.model || null,
347
- lines,
348
- content: addedContent,
349
- contentHash: computeHash(addedContent),
350
- contentHashNormalized: computeNormalizedHash(addedContent),
351
- editType: determineEditType(oldString, newString),
352
- oldContent: oldString || undefined,
353
- sessionId: payload.conversation_id,
354
- toolUseId: payload.generation_id,
355
- });
389
+ return;
390
+ }
391
+ const filePath = payload.file_path;
392
+ if (!filePath)
393
+ return;
394
+ const conversationId = payload.conversation_id || `cursor-${Date.now()}`;
395
+ const ctx = await setupCaptureContext(filePath, "cursor", conversationId, payload.model || null);
396
+ if (!ctx)
397
+ return;
398
+ (0, database_1.upsertSession)({
399
+ id: ctx.sessionId,
400
+ agent: "cursor",
401
+ model: ctx.model,
402
+ conversationId,
403
+ });
404
+ const absolutePath = path.isAbsolute(filePath)
405
+ ? filePath
406
+ : path.join(ctx.repoRoot, filePath);
407
+ const relativePath = makeRelative(ctx.repoRoot, absolutePath);
408
+ // Get before/after content and record delta
409
+ const beforeContent = await getBeforeContent(ctx, conversationId, filePath);
410
+ const afterContent = (0, storage_1.readFileContent)(absolutePath);
411
+ if (afterContent) {
412
+ await recordAIDelta(ctx, filePath, beforeContent, afterContent);
413
+ // Update checkpoint to current state so next beforeSubmitPrompt doesn't
414
+ // incorrectly detect this AI edit as a "human edit"
415
+ await (0, checkpoint_1.captureFileCheckpoint)(ctx.repoRoot, conversationId, filePath);
416
+ }
417
+ (0, database_1.insertToolCall)({
418
+ sessionId: ctx.sessionId,
419
+ toolName: "edit",
420
+ filePath: relativePath,
421
+ });
422
+ if (process.env.AGENTBLAME_DEBUG) {
423
+ console.error(`[agentblame] Captured Cursor edit: ${filePath}`);
356
424
  }
357
- return edits;
358
425
  }
359
426
  async function processClaudePayload(payload) {
360
- const edits = [];
361
- // CRITICAL: Skip payloads that are actually from Cursor.
362
- // Both Cursor and Claude Code can trigger hooks from .claude/settings.json,
363
- // so we need to detect Cursor payloads and skip them here.
364
- // Cursor payloads have cursor_version field, Claude payloads don't.
365
427
  if (payload.cursor_version) {
366
- return edits;
367
- }
368
- // CRITICAL: Only process if this is an actual Edit or Write tool usage from Claude.
369
- // Claude Code's hooks fire for various reasons, but we only want to capture
370
- // when Claude actually performed an edit/write operation.
371
- // Without a valid tool_name, this is likely a spurious trigger (e.g., from file
372
- // watcher detecting external changes).
373
- const toolName = payload.tool_name?.toLowerCase() || "";
374
- if (toolName !== "edit" && toolName !== "write" && toolName !== "multiedit") {
375
- return edits;
376
- }
377
- const timestamp = new Date().toISOString();
378
- // Claude Code has tool_input with the actual content
428
+ return;
429
+ }
430
+ const toolName = payload.tool_name || "";
431
+ if (!toolName)
432
+ return;
433
+ const hookEvent = payload.hook_event_name || "PostToolUse";
379
434
  const toolInput = payload.tool_input;
380
435
  const toolResponse = payload.tool_response;
381
436
  const filePath = toolResponse?.filePath || toolInput?.file_path || payload.file_path;
382
- if (!filePath)
383
- return edits;
384
- // Extract session info for correlation
385
- const sessionId = payload.session_id;
386
- const toolUseId = payload.tool_use_id;
387
- // Extract model from transcript file (Claude Code provides transcript_path in hook payload)
437
+ const toolNameLower = toolName.toLowerCase();
438
+ const isFileModifying = toolNameLower === "edit" ||
439
+ toolNameLower === "write" ||
440
+ toolNameLower === "multiedit";
441
+ const pathForRepo = filePath || payload.transcript_path;
442
+ if (!pathForRepo)
443
+ return;
444
+ if (process.env.AGENTBLAME_DEBUG) {
445
+ console.error(`[agentblame] Claude ${hookEvent} ${toolName}:`);
446
+ console.error(`[agentblame] toolResponse.filePath: ${toolResponse?.filePath}`);
447
+ console.error(`[agentblame] toolInput.file_path: ${toolInput?.file_path}`);
448
+ console.error(`[agentblame] payload.file_path: ${payload.file_path}`);
449
+ console.error(`[agentblame] resolved filePath: ${filePath}`);
450
+ }
388
451
  let model = null;
389
452
  if (payload.transcript_path) {
390
453
  model = await extractModelFromTranscript(payload.transcript_path);
391
454
  }
392
- // Fallback to generic "claude" if transcript parsing fails
393
455
  if (!model) {
394
456
  model = "claude";
395
457
  }
396
- // For Edit/MultiEdit tools, REQUIRE structuredPatch.
397
- // Without structuredPatch, we cannot accurately determine what Claude added.
398
- // Spurious triggers (e.g., file watcher detecting external changes) won't have
399
- // structuredPatch and would incorrectly capture the entire file.
400
- if (toolName === "edit" || toolName === "multiedit") {
401
- if (!toolResponse?.structuredPatch || toolResponse.structuredPatch.length === 0) {
402
- // No structuredPatch - skip this capture to avoid incorrect attribution
403
- // Log for debugging missing captures
404
- if (process.env.AGENTBLAME_DEBUG) {
405
- console.error(`[agentblame] Skipping ${toolName} for ${filePath}: no structuredPatch in tool_response`);
406
- console.error(`[agentblame] tool_response keys: ${toolResponse ? Object.keys(toolResponse).join(", ") : "null"}`);
458
+ const conversationId = payload.session_id || `claude-${Date.now()}`;
459
+ const ctx = await setupCaptureContext(pathForRepo, "claude", conversationId, model);
460
+ if (!ctx)
461
+ return;
462
+ (0, database_1.upsertSession)({
463
+ id: ctx.sessionId,
464
+ agent: "claude",
465
+ model: ctx.model,
466
+ conversationId,
467
+ });
468
+ // Extract and store prompt
469
+ if (payload.transcript_path) {
470
+ const prompt = await extractPromptFromTranscript(payload.transcript_path);
471
+ if (prompt) {
472
+ const contentHash = (0, database_1.hashPromptContent)(prompt);
473
+ if (!(0, database_1.promptExists)(ctx.sessionId, contentHash)) {
474
+ (0, database_1.insertPrompt)({
475
+ sessionId: ctx.sessionId,
476
+ content: ctx.storePromptContent ? prompt : null,
477
+ contentHash,
478
+ });
407
479
  }
408
- return edits;
409
480
  }
410
- // Get original file lines for context
411
- const originalFileLines = (toolResponse.originalFile || "").split("\n");
412
- // Parse the structured patch to get added lines with line numbers
413
- const addedLinesWithNumbers = parseStructuredPatch(toolResponse.structuredPatch, originalFileLines);
414
- if (addedLinesWithNumbers.length === 0)
415
- return edits;
416
- // Hash lines with their line numbers and context
417
- const lines = hashLinesWithNumbers(addedLinesWithNumbers, originalFileLines);
418
- if (lines.length === 0)
419
- return edits;
420
- // Aggregate content
421
- const addedContent = addedLinesWithNumbers.map(l => l.content).join("\n");
422
- edits.push({
423
- timestamp,
424
- provider: "claudeCode",
425
- filePath,
426
- model,
427
- lines,
428
- content: addedContent,
429
- contentHash: computeHash(addedContent),
430
- contentHashNormalized: computeNormalizedHash(addedContent),
431
- editType: "modification",
432
- sessionId,
433
- toolUseId,
481
+ }
482
+ // PreToolUse: detect human edits and capture checkpoint
483
+ if (hookEvent === "PreToolUse" && isFileModifying && filePath) {
484
+ if (process.env.AGENTBLAME_DEBUG) {
485
+ console.error(`[agentblame] PreToolUse: ${filePath}`);
486
+ }
487
+ await detectAndRecordHumanEdits(ctx, conversationId, filePath);
488
+ await (0, checkpoint_1.captureFileCheckpoint)(ctx.repoRoot, conversationId, filePath);
489
+ if (process.env.AGENTBLAME_DEBUG) {
490
+ console.error(`[agentblame] PreToolUse complete: ${filePath}`);
491
+ }
492
+ return;
493
+ }
494
+ // PostToolUse: record AI delta
495
+ if (hookEvent === "PostToolUse" && isFileModifying && filePath) {
496
+ const absolutePath = path.isAbsolute(filePath)
497
+ ? filePath
498
+ : path.join(ctx.repoRoot, filePath);
499
+ const relativePath = makeRelative(ctx.repoRoot, absolutePath);
500
+ const beforeContent = await getBeforeContent(ctx, conversationId, filePath);
501
+ const afterContent = (0, storage_1.readFileContent)(absolutePath);
502
+ if (process.env.AGENTBLAME_DEBUG) {
503
+ console.error(`[agentblame] PostToolUse ${toolName}: ${relativePath}`);
504
+ console.error(`[agentblame] beforeContent: ${beforeContent ? beforeContent.length + ' chars' : 'null'}`);
505
+ console.error(`[agentblame] afterContent: ${afterContent ? afterContent.length + ' chars' : 'null'}`);
506
+ }
507
+ if (afterContent) {
508
+ await recordAIDelta(ctx, filePath, beforeContent, afterContent);
509
+ // Update checkpoint to current state so next PreToolUse doesn't
510
+ // incorrectly detect this AI edit as a "human edit"
511
+ await (0, checkpoint_1.captureFileCheckpoint)(ctx.repoRoot, conversationId, filePath);
512
+ }
513
+ else if (process.env.AGENTBLAME_DEBUG) {
514
+ console.error(`[agentblame] SKIPPED: no afterContent for ${relativePath}`);
515
+ }
516
+ (0, database_1.insertToolCall)({
517
+ sessionId: ctx.sessionId,
518
+ toolName: toolName.toLowerCase(),
519
+ filePath: relativePath,
434
520
  });
435
- return edits;
436
- }
437
- // Handle Write tool (new file creation)
438
- // For Write, we need content
439
- if (toolName === "write") {
440
- const content = toolInput?.content || payload.content;
441
- if (!content || !content.trim()) {
442
- return edits;
521
+ if (process.env.AGENTBLAME_DEBUG) {
522
+ console.error(`[agentblame] Captured Claude ${toolName}: ${filePath}`);
443
523
  }
444
- const lines = hashLines(content);
445
- if (lines.length === 0)
446
- return edits;
447
- edits.push({
448
- timestamp,
449
- provider: "claudeCode",
450
- filePath,
451
- model,
452
- lines,
453
- content,
454
- contentHash: computeHash(content),
455
- contentHashNormalized: computeNormalizedHash(content),
456
- editType: "addition",
457
- sessionId,
458
- toolUseId,
524
+ }
525
+ else if (!isFileModifying) {
526
+ // Read-only tools
527
+ (0, database_1.insertToolCall)({
528
+ sessionId: ctx.sessionId,
529
+ toolName: toolName.toLowerCase(),
530
+ filePath: filePath ? makeRelative(ctx.repoRoot, filePath) : null,
459
531
  });
460
- return edits;
461
532
  }
462
- // Unknown tool type that passed the initial check - skip
463
- return edits;
464
533
  }
465
- /**
466
- * Process OpenCode payload.
467
- * OpenCode provides before/after file content which allows precise line number extraction.
468
- */
469
- function processOpenCodePayload(payload) {
470
- const edits = [];
471
- const timestamp = new Date().toISOString();
534
+ async function processOpenCodePayload(payload) {
472
535
  const filePath = payload.filePath;
473
536
  if (!filePath)
474
- return edits;
475
- const sessionId = payload.sessionID;
476
- const toolUseId = payload.callID;
477
- const model = payload.model || null;
478
- // Handle write tool (new file creation)
479
- if (payload.tool === "write" && payload.content) {
480
- const content = payload.content;
481
- if (!content.trim())
482
- return edits;
483
- // For new files, all lines are added
484
- const fileLines = content.split("\n");
485
- const linesWithNumbers = fileLines
486
- .map((line, i) => ({ content: line, lineNumber: i + 1 }))
487
- .filter(l => l.content.trim());
488
- const lines = hashLinesWithNumbers(linesWithNumbers, fileLines);
489
- if (lines.length === 0)
490
- return edits;
491
- edits.push({
492
- timestamp,
493
- provider: "opencode",
494
- filePath,
495
- model,
496
- lines,
497
- content,
498
- contentHash: computeHash(content),
499
- contentHashNormalized: computeNormalizedHash(content),
500
- editType: "addition",
501
- sessionId,
502
- toolUseId,
503
- });
504
- return edits;
505
- }
506
- // Handle edit tool
507
- if (payload.tool === "edit") {
508
- // OpenCode provides full before/after content - use it for precise line detection
509
- if (payload.before !== undefined && payload.after !== undefined) {
510
- const beforeLines = payload.before.split("\n");
511
- const afterLines = payload.after.split("\n");
512
- // Use diffLines to find added lines with their positions
513
- const parts = (0, diff_1.diffLines)(payload.before, payload.after);
514
- const addedLinesWithNumbers = [];
515
- let afterLineIndex = 0;
516
- for (const part of parts) {
517
- const partLines = part.value.split("\n");
518
- // Remove empty string from split if value ends with \n
519
- if (partLines[partLines.length - 1] === "") {
520
- partLines.pop();
521
- }
522
- if (part.added) {
523
- // These lines were added
524
- for (const line of partLines) {
525
- addedLinesWithNumbers.push({
526
- content: line,
527
- lineNumber: afterLineIndex + 1, // 1-indexed
528
- });
529
- afterLineIndex++;
530
- }
531
- }
532
- else if (part.removed) {
533
- // Removed lines don't affect after line index
534
- }
535
- else {
536
- // Context lines - advance the after line index
537
- afterLineIndex += partLines.length;
538
- }
539
- }
540
- if (addedLinesWithNumbers.length === 0)
541
- return edits;
542
- // Filter empty lines and hash with context
543
- const nonEmptyLines = addedLinesWithNumbers.filter(l => l.content.trim());
544
- if (nonEmptyLines.length === 0)
545
- return edits;
546
- const lines = hashLinesWithNumbers(nonEmptyLines, afterLines);
547
- if (lines.length === 0)
548
- return edits;
549
- const addedContent = nonEmptyLines.map(l => l.content).join("\n");
550
- edits.push({
551
- timestamp,
552
- provider: "opencode",
553
- filePath,
554
- model,
555
- lines,
556
- content: addedContent,
557
- contentHash: computeHash(addedContent),
558
- contentHashNormalized: computeNormalizedHash(addedContent),
559
- editType: "modification",
560
- oldContent: payload.oldString,
561
- sessionId,
562
- toolUseId,
537
+ return;
538
+ const conversationId = payload.sessionID || `opencode-${Date.now()}`;
539
+ const ctx = await setupCaptureContext(filePath, "opencode", conversationId, payload.model || null);
540
+ if (!ctx)
541
+ return;
542
+ (0, database_1.upsertSession)({
543
+ id: ctx.sessionId,
544
+ agent: "opencode",
545
+ model: ctx.model,
546
+ conversationId,
547
+ });
548
+ if (payload.prompt) {
549
+ const contentHash = (0, database_1.hashPromptContent)(payload.prompt);
550
+ if (!(0, database_1.promptExists)(ctx.sessionId, contentHash)) {
551
+ (0, database_1.insertPrompt)({
552
+ sessionId: ctx.sessionId,
553
+ content: ctx.storePromptContent ? payload.prompt : null,
554
+ contentHash,
563
555
  });
564
- return edits;
565
556
  }
566
- // Fallback: use oldString/newString if before/after not available
567
- const oldString = payload.oldString || "";
568
- const newString = payload.newString || "";
569
- if (!newString)
570
- return edits;
571
- const addedContent = extractAddedContent(oldString, newString);
572
- if (!addedContent.trim())
573
- return edits;
574
- const lines = hashLines(addedContent);
575
- if (lines.length === 0)
576
- return edits;
577
- edits.push({
578
- timestamp,
579
- provider: "opencode",
580
- filePath,
581
- model,
582
- lines,
583
- content: addedContent,
584
- contentHash: computeHash(addedContent),
585
- contentHashNormalized: computeNormalizedHash(addedContent),
586
- editType: determineEditType(oldString, newString),
587
- oldContent: oldString || undefined,
588
- sessionId,
589
- toolUseId,
590
- });
591
557
  }
592
- return edits;
558
+ // Before hook: detect human edits and capture checkpoint
559
+ if (payload.hook_event === "before") {
560
+ await detectAndRecordHumanEdits(ctx, conversationId, filePath);
561
+ await (0, checkpoint_1.captureFileCheckpoint)(ctx.repoRoot, conversationId, filePath);
562
+ if (process.env.AGENTBLAME_DEBUG) {
563
+ console.error(`[agentblame] OpenCode before checkpoint: ${filePath}`);
564
+ }
565
+ return;
566
+ }
567
+ // After hook: record AI delta
568
+ const absolutePath = path.isAbsolute(filePath)
569
+ ? filePath
570
+ : path.join(ctx.repoRoot, filePath);
571
+ const relativePath = makeRelative(ctx.repoRoot, absolutePath);
572
+ // Get before content (OpenCode may provide it directly)
573
+ let beforeContent = null;
574
+ if (payload.before) {
575
+ beforeContent = payload.before;
576
+ }
577
+ else {
578
+ beforeContent = await getBeforeContent(ctx, conversationId, filePath);
579
+ }
580
+ // Get after content
581
+ const afterContent = payload.after ?? (0, storage_1.readFileContent)(absolutePath);
582
+ if (afterContent) {
583
+ await recordAIDelta(ctx, filePath, beforeContent, afterContent);
584
+ // Update checkpoint to current state so next before hook doesn't
585
+ // incorrectly detect this AI edit as a "human edit"
586
+ await (0, checkpoint_1.captureFileCheckpoint)(ctx.repoRoot, conversationId, filePath);
587
+ }
588
+ (0, database_1.insertToolCall)({
589
+ sessionId: ctx.sessionId,
590
+ toolName: payload.tool,
591
+ filePath: relativePath,
592
+ });
593
+ if (process.env.AGENTBLAME_DEBUG) {
594
+ console.error(`[agentblame] Captured OpenCode ${payload.tool}: ${filePath}`);
595
+ }
593
596
  }
594
597
  // =============================================================================
595
598
  // Main
@@ -605,55 +608,30 @@ async function runCapture() {
605
608
  try {
606
609
  const { provider, event } = parseArgs();
607
610
  const input = await readStdin();
611
+ if (process.env.AGENTBLAME_DEBUG) {
612
+ console.error(`[agentblame] Capture: provider=${provider}, event=${event}`);
613
+ }
608
614
  if (!input.trim()) {
609
615
  process.exit(0);
610
616
  }
611
617
  const data = JSON.parse(input);
612
- // The hook receives the payload directly or wrapped
613
618
  const payload = data.payload || data;
614
- let edits = [];
615
619
  if (provider === "cursor") {
616
620
  const eventName = event || data.hook_event_name || "afterFileEdit";
617
- edits = await processCursorPayload(payload, eventName);
621
+ await processCursorPayload(payload, eventName);
618
622
  }
619
623
  else if (provider === "claude") {
620
- edits = await processClaudePayload(payload);
624
+ await processClaudePayload(payload);
621
625
  }
622
626
  else if (provider === "opencode") {
623
- edits = processOpenCodePayload(payload);
624
- }
625
- // Save all edits to SQLite database
626
- if (process.env.AGENTBLAME_DEBUG && edits.length === 0) {
627
- console.error(`[agentblame] No edits extracted from ${provider} payload`);
628
- }
629
- for (const edit of edits) {
630
- // Find the agentblame directory for this file
631
- const agentblameDir = (0, util_1.findAgentBlameDir)(edit.filePath);
632
- if (!agentblameDir) {
633
- // File is not in an initialized repo, skip silently
634
- if (process.env.AGENTBLAME_DEBUG) {
635
- console.error(`[agentblame] No agentblame dir found for ${edit.filePath}`);
636
- }
637
- continue;
638
- }
639
- // Set the database directory and save
640
- (0, database_1.setAgentBlameDir)(agentblameDir);
641
- try {
642
- saveEdit(edit);
643
- if (process.env.AGENTBLAME_DEBUG) {
644
- console.error(`[agentblame] Saved edit for ${edit.filePath}: ${edit.lines.length} lines`);
645
- }
646
- }
647
- catch (saveErr) {
648
- // Log database errors even without debug mode since they indicate lost data
649
- console.error(`[agentblame] Failed to save edit for ${edit.filePath}:`, saveErr);
650
- }
627
+ await processOpenCodePayload(payload);
651
628
  }
652
629
  process.exit(0);
653
630
  }
654
631
  catch (err) {
655
- // Silent failure - don't interrupt the editor
656
- console.error("Agent Blame capture error:", err);
632
+ if (process.env.AGENTBLAME_DEBUG) {
633
+ console.error("Agent Blame capture error:", err);
634
+ }
657
635
  process.exit(0);
658
636
  }
659
637
  }