Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "agentblame",
"displayName": "Agent Blame",
"description": "Track AI-generated vs human-written code. Provides git notes storage, CLI, and GitHub PR attribution.",
"version": "0.2.6",
"version": "0.2.10",
"private": true,
"license": "Apache-2.0",
"repository": {
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@mesadev/agentblame",
"version": "0.2.9",
"version": "0.2.10",
"description": "CLI to track AI-generated vs human-written code",
"license": "Apache-2.0",
"repository": {
Expand Down
22 changes: 21 additions & 1 deletion packages/cli/src/capture.ts
Original file line number Diff line number Diff line change
Expand Up @@ -510,6 +510,11 @@ async function processClaudePayload(payload: ClaudePayload): Promise<CapturedEdi
if (toolName === "edit" || toolName === "multiedit") {
if (!toolResponse?.structuredPatch || toolResponse.structuredPatch.length === 0) {
// No structuredPatch - skip this capture to avoid incorrect attribution
// Log for debugging missing captures
if (process.env.AGENTBLAME_DEBUG) {
console.error(`[agentblame] Skipping ${toolName} for ${filePath}: no structuredPatch in tool_response`);
console.error(`[agentblame] tool_response keys: ${toolResponse ? Object.keys(toolResponse).join(", ") : "null"}`);
}
return edits;
}

Expand Down Expand Up @@ -762,17 +767,32 @@ export async function runCapture(): Promise<void> {
}

// Save all edits to SQLite database
if (process.env.AGENTBLAME_DEBUG && edits.length === 0) {
console.error(`[agentblame] No edits extracted from ${provider} payload`);
}

for (const edit of edits) {
// Find the agentblame directory for this file
const agentblameDir = findAgentBlameDir(edit.filePath);
if (!agentblameDir) {
// File is not in an initialized repo, skip silently
if (process.env.AGENTBLAME_DEBUG) {
console.error(`[agentblame] No agentblame dir found for ${edit.filePath}`);
}
continue;
}

// Set the database directory and save
setAgentBlameDir(agentblameDir);
saveEdit(edit);
try {
saveEdit(edit);
if (process.env.AGENTBLAME_DEBUG) {
console.error(`[agentblame] Saved edit for ${edit.filePath}: ${edit.lines.length} lines`);
}
} catch (saveErr) {
// Log database errors even without debug mode since they indicate lost data
console.error(`[agentblame] Failed to save edit for ${edit.filePath}:`, saveErr);
}
}

process.exit(0);
Expand Down
72 changes: 43 additions & 29 deletions packages/cli/src/lib/database.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,9 @@ export function getDatabase(): Database {
// Enable foreign keys and WAL mode for better performance
dbInstance.exec("PRAGMA foreign_keys = ON");
dbInstance.exec("PRAGMA journal_mode = WAL");
// Set busy timeout to handle concurrent writes from async hooks
// Without this, concurrent capture processes get SQLITE_BUSY and fail silently
dbInstance.exec("PRAGMA busy_timeout = 5000");

// Create tables and indexes
dbInstance.exec(SCHEMA);
Expand Down Expand Up @@ -204,7 +207,10 @@ export interface InsertEditParams {
}

/**
* Insert a new AI edit into the database
* Insert a new AI edit into the database.
* Uses an explicit transaction to ensure atomicity - either all data
* is written (edit + lines) or none. This is especially important
* when running async hooks where the process could be interrupted.
*/
export function insertEdit(params: InsertEditParams): number {
const db = getDatabase();
Expand All @@ -217,41 +223,49 @@ export function insertEdit(params: InsertEditParams): number {
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`);

const result = editStmt.run(
params.timestamp,
params.provider,
params.filePath,
params.model,
params.content,
params.contentHash,
params.contentHashNormalized,
params.editType,
params.oldContent || null,
params.sessionId || null,
params.toolUseId || null
);

const editId = Number(result.lastInsertRowid);

// Insert lines with line numbers and context
const lineStmt = db.prepare(`
INSERT INTO lines (edit_id, content, hash, hash_normalized, line_number, context_before, context_after)
VALUES (?, ?, ?, ?, ?, ?, ?)
`);

for (const line of params.lines) {
lineStmt.run(
editId,
line.content,
line.hash,
line.hashNormalized,
line.lineNumber || null,
line.contextBefore || null,
line.contextAfter || null
// Wrap in transaction for atomicity
db.exec("BEGIN TRANSACTION");
try {
const result = editStmt.run(
params.timestamp,
params.provider,
params.filePath,
params.model,
params.content,
params.contentHash,
params.contentHashNormalized,
params.editType,
params.oldContent || null,
params.sessionId || null,
params.toolUseId || null
);
}

return editId;
const editId = Number(result.lastInsertRowid);

// Insert lines with line numbers and context
for (const line of params.lines) {
lineStmt.run(
editId,
line.content,
line.hash,
line.hashNormalized,
line.lineNumber || null,
line.contextBefore || null,
line.contextAfter || null
);
}

db.exec("COMMIT");
return editId;
} catch (err) {
db.exec("ROLLBACK");
throw err;
}
}

// =============================================================================
Expand Down
4 changes: 2 additions & 2 deletions packages/cli/src/lib/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -232,10 +232,10 @@ export async function installClaudeHooks(repoRoot: string): Promise<boolean> {
)
);

// Add the new hook
// Add the new hook with async: true for non-blocking execution
config.hooks.PostToolUse.push({
matcher: "Edit|Write|MultiEdit",
hooks: [{ type: "command", command: hookCommand }],
hooks: [{ type: "command", command: hookCommand, async: true }],
});

await fs.promises.writeFile(
Expand Down