@wasabeef/agentnote 0.1.6 → 0.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +446 -65
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -25,12 +25,18 @@ var TRUNCATE_PROMPT = 120;
|
|
|
25
25
|
var TRUNCATE_RESPONSE_SHOW = 200;
|
|
26
26
|
var TRUNCATE_RESPONSE_PR = 500;
|
|
27
27
|
var TRUNCATE_RESPONSE_CHAT = 800;
|
|
28
|
+
var ARCHIVE_ID_RE = /^[0-9a-z]{6,}$/;
|
|
29
|
+
var HEARTBEAT_FILE = "heartbeat";
|
|
30
|
+
var PRE_BLOBS_FILE = "pre_blobs.jsonl";
|
|
31
|
+
var COMMITTED_PAIRS_FILE = "committed_pairs.jsonl";
|
|
32
|
+
var EMPTY_BLOB = "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391";
|
|
28
33
|
var SCHEMA_VERSION = 1;
|
|
29
34
|
var DEBUG = !!process.env.AGENTNOTE_DEBUG;
|
|
30
35
|
|
|
31
36
|
// src/core/record.ts
|
|
32
37
|
import { existsSync as existsSync3 } from "node:fs";
|
|
33
|
-
import { readdir, readFile as readFile3 } from "node:fs/promises";
|
|
38
|
+
import { readdir, readFile as readFile3, unlink, writeFile as writeFile2 } from "node:fs/promises";
|
|
39
|
+
import { tmpdir } from "node:os";
|
|
34
40
|
import { join as join2 } from "node:path";
|
|
35
41
|
|
|
36
42
|
// src/agents/claude-code.ts
|
|
@@ -44,6 +50,10 @@ var HOOKS_CONFIG = {
|
|
|
44
50
|
Stop: [{ hooks: [{ type: "command", command: HOOK_COMMAND, async: true }] }],
|
|
45
51
|
UserPromptSubmit: [{ hooks: [{ type: "command", command: HOOK_COMMAND, async: true }] }],
|
|
46
52
|
PreToolUse: [
|
|
53
|
+
{
|
|
54
|
+
matcher: "Edit|Write|NotebookEdit",
|
|
55
|
+
hooks: [{ type: "command", command: HOOK_COMMAND }]
|
|
56
|
+
},
|
|
47
57
|
{
|
|
48
58
|
matcher: "Bash",
|
|
49
59
|
hooks: [{ type: "command", if: "Bash(*git commit*)", command: HOOK_COMMAND }]
|
|
@@ -147,8 +157,19 @@ var claudeCode = {
|
|
|
147
157
|
case "UserPromptSubmit":
|
|
148
158
|
return e.prompt ? { kind: "prompt", sessionId: sid, timestamp: ts, prompt: e.prompt } : null;
|
|
149
159
|
case "PreToolUse": {
|
|
160
|
+
const tool = e.tool_name;
|
|
150
161
|
const cmd = e.tool_input?.command ?? "";
|
|
151
|
-
if (
|
|
162
|
+
if ((tool === "Edit" || tool === "Write" || tool === "NotebookEdit") && e.tool_input?.file_path) {
|
|
163
|
+
return {
|
|
164
|
+
kind: "pre_edit",
|
|
165
|
+
sessionId: sid,
|
|
166
|
+
timestamp: ts,
|
|
167
|
+
tool,
|
|
168
|
+
file: e.tool_input.file_path,
|
|
169
|
+
toolUseId: e.tool_use_id
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
if (tool === "Bash" && isGitCommit(cmd)) {
|
|
152
173
|
return { kind: "pre_commit", sessionId: sid, timestamp: ts, commitCommand: cmd };
|
|
153
174
|
}
|
|
154
175
|
return null;
|
|
@@ -161,7 +182,8 @@ var claudeCode = {
|
|
|
161
182
|
sessionId: sid,
|
|
162
183
|
timestamp: ts,
|
|
163
184
|
tool,
|
|
164
|
-
file: e.tool_input.file_path
|
|
185
|
+
file: e.tool_input.file_path,
|
|
186
|
+
toolUseId: e.tool_use_id
|
|
165
187
|
};
|
|
166
188
|
}
|
|
167
189
|
if (tool === "Bash" && isGitCommit(e.tool_input?.command ?? "")) {
|
|
@@ -260,15 +282,97 @@ async function repoRoot() {
|
|
|
260
282
|
return git(["rev-parse", "--show-toplevel"]);
|
|
261
283
|
}
|
|
262
284
|
|
|
285
|
+
// src/core/attribution.ts
|
|
286
|
+
function parseUnifiedHunks(diffOutput) {
|
|
287
|
+
const hunks = [];
|
|
288
|
+
for (const line of diffOutput.split("\n")) {
|
|
289
|
+
const m = line.match(/^@@\s+-(\d+)(?:,(\d+))?\s+\+(\d+)(?:,(\d+))?\s+@@/);
|
|
290
|
+
if (m) {
|
|
291
|
+
hunks.push({
|
|
292
|
+
oldStart: Number(m[1]),
|
|
293
|
+
oldCount: m[2] != null ? Number(m[2]) : 1,
|
|
294
|
+
newStart: Number(m[3]),
|
|
295
|
+
newCount: m[4] != null ? Number(m[4]) : 1
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
return hunks;
|
|
300
|
+
}
|
|
301
|
+
function expandNewPositions(hunks) {
|
|
302
|
+
const positions = /* @__PURE__ */ new Set();
|
|
303
|
+
for (const h of hunks) {
|
|
304
|
+
for (let i = 0; i < h.newCount; i++) {
|
|
305
|
+
positions.add(h.newStart + i);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
return positions;
|
|
309
|
+
}
|
|
310
|
+
function countLines(hunks) {
|
|
311
|
+
let added = 0;
|
|
312
|
+
let deleted = 0;
|
|
313
|
+
for (const h of hunks) {
|
|
314
|
+
added += h.newCount;
|
|
315
|
+
deleted += h.oldCount;
|
|
316
|
+
}
|
|
317
|
+
return { added, deleted };
|
|
318
|
+
}
|
|
319
|
+
async function computePositionAttribution(parentBlob, committedBlob, turnPairs) {
|
|
320
|
+
const diff1Output = await gitDiffUnified0(parentBlob, committedBlob);
|
|
321
|
+
const diff1Hunks = parseUnifiedHunks(diff1Output);
|
|
322
|
+
const diff1Added = expandNewPositions(diff1Hunks);
|
|
323
|
+
const { added: totalAddedLines, deleted: deletedLines } = countLines(diff1Hunks);
|
|
324
|
+
if (turnPairs.length === 0 || totalAddedLines === 0) {
|
|
325
|
+
return {
|
|
326
|
+
aiAddedLines: 0,
|
|
327
|
+
humanAddedLines: totalAddedLines,
|
|
328
|
+
totalAddedLines,
|
|
329
|
+
deletedLines
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
const aiPositions = /* @__PURE__ */ new Set();
|
|
333
|
+
for (const { preBlob, postBlob } of turnPairs) {
|
|
334
|
+
const diff2Output = await gitDiffUnified0(preBlob, committedBlob);
|
|
335
|
+
const diff2Positions = expandNewPositions(parseUnifiedHunks(diff2Output));
|
|
336
|
+
const diff3Output = await gitDiffUnified0(postBlob, committedBlob);
|
|
337
|
+
const diff3Positions = expandNewPositions(parseUnifiedHunks(diff3Output));
|
|
338
|
+
for (const pos of diff2Positions) {
|
|
339
|
+
if (!diff3Positions.has(pos)) {
|
|
340
|
+
aiPositions.add(pos);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
let aiAddedLines = 0;
|
|
345
|
+
let humanAddedLines = 0;
|
|
346
|
+
for (const pos of diff1Added) {
|
|
347
|
+
if (aiPositions.has(pos)) {
|
|
348
|
+
aiAddedLines++;
|
|
349
|
+
} else {
|
|
350
|
+
humanAddedLines++;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
return { aiAddedLines, humanAddedLines, totalAddedLines, deletedLines };
|
|
354
|
+
}
|
|
355
|
+
async function gitDiffUnified0(blobA, blobB) {
|
|
356
|
+
if (!blobA || !blobB || blobA === blobB) return "";
|
|
357
|
+
const { stdout, exitCode } = await gitSafe(["diff", "--unified=0", "--no-color", blobA, blobB]);
|
|
358
|
+
if (exitCode !== 0 && exitCode !== 1) {
|
|
359
|
+
throw new Error(`git diff failed with exit code ${exitCode}`);
|
|
360
|
+
}
|
|
361
|
+
return stdout;
|
|
362
|
+
}
|
|
363
|
+
|
|
263
364
|
// src/core/entry.ts
|
|
264
|
-
function calcAiRatio(commitFiles, aiFiles) {
|
|
365
|
+
function calcAiRatio(commitFiles, aiFiles, lineCounts) {
|
|
366
|
+
if (lineCounts && lineCounts.totalAddedLines > 0) {
|
|
367
|
+
return Math.round(lineCounts.aiAddedLines / lineCounts.totalAddedLines * 100);
|
|
368
|
+
}
|
|
265
369
|
if (commitFiles.length === 0) return 0;
|
|
266
370
|
const aiSet = new Set(aiFiles);
|
|
267
371
|
const matched = commitFiles.filter((f) => aiSet.has(f));
|
|
268
372
|
return Math.round(matched.length / commitFiles.length * 100);
|
|
269
373
|
}
|
|
270
374
|
function buildEntry(opts) {
|
|
271
|
-
|
|
375
|
+
const entry = {
|
|
272
376
|
v: SCHEMA_VERSION,
|
|
273
377
|
session_id: opts.sessionId,
|
|
274
378
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -281,8 +385,14 @@ function buildEntry(opts) {
|
|
|
281
385
|
}),
|
|
282
386
|
files_in_commit: opts.commitFiles,
|
|
283
387
|
files_by_ai: opts.aiFiles,
|
|
284
|
-
ai_ratio: calcAiRatio(opts.commitFiles, opts.aiFiles)
|
|
388
|
+
ai_ratio: calcAiRatio(opts.commitFiles, opts.aiFiles, opts.lineCounts)
|
|
285
389
|
};
|
|
390
|
+
if (opts.lineCounts) {
|
|
391
|
+
entry.ai_added_lines = opts.lineCounts.aiAddedLines;
|
|
392
|
+
entry.total_added_lines = opts.lineCounts.totalAddedLines;
|
|
393
|
+
entry.deleted_lines = opts.lineCounts.deletedLines;
|
|
394
|
+
}
|
|
395
|
+
return entry;
|
|
286
396
|
}
|
|
287
397
|
|
|
288
398
|
// src/core/jsonl.ts
|
|
@@ -332,25 +442,53 @@ async function recordCommitEntry(opts) {
|
|
|
332
442
|
} catch {
|
|
333
443
|
}
|
|
334
444
|
const commitFileSet = new Set(commitFiles);
|
|
335
|
-
const
|
|
445
|
+
const allChangeEntries = await readAllSessionJsonl(sessionDir, CHANGES_FILE);
|
|
336
446
|
const promptEntries = await readAllSessionJsonl(sessionDir, PROMPTS_FILE);
|
|
447
|
+
const allPreBlobEntries = await readAllSessionJsonl(sessionDir, PRE_BLOBS_FILE);
|
|
448
|
+
const preBlobTurnById = /* @__PURE__ */ new Map();
|
|
449
|
+
for (const e of allPreBlobEntries) {
|
|
450
|
+
const id = e.tool_use_id;
|
|
451
|
+
if (id && typeof e.turn === "number") preBlobTurnById.set(id, e.turn);
|
|
452
|
+
}
|
|
453
|
+
for (const entry2 of allChangeEntries) {
|
|
454
|
+
const id = entry2.tool_use_id;
|
|
455
|
+
if (id && preBlobTurnById.has(id)) {
|
|
456
|
+
entry2.turn = preBlobTurnById.get(id);
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
const consumedPairs = await readConsumedPairs(sessionDir);
|
|
460
|
+
const changeEntries = allChangeEntries.filter((e) => !consumedPairs.has(consumedKey(e)));
|
|
461
|
+
const preBlobEntriesForTurnFix = allPreBlobEntries.filter(
|
|
462
|
+
(e) => !consumedPairs.has(consumedKey(e))
|
|
463
|
+
);
|
|
337
464
|
const hasTurnData = promptEntries.some((e) => typeof e.turn === "number" && e.turn > 0);
|
|
338
465
|
let aiFiles;
|
|
339
466
|
let prompts;
|
|
340
467
|
let relevantPromptEntries;
|
|
468
|
+
const relevantTurns = /* @__PURE__ */ new Set();
|
|
341
469
|
if (hasTurnData) {
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
)
|
|
346
|
-
|
|
347
|
-
const
|
|
470
|
+
const aiFileSet = /* @__PURE__ */ new Set();
|
|
471
|
+
for (const e of changeEntries) {
|
|
472
|
+
const f = e.file;
|
|
473
|
+
if (f && commitFileSet.has(f)) aiFileSet.add(f);
|
|
474
|
+
}
|
|
475
|
+
for (const e of preBlobEntriesForTurnFix) {
|
|
476
|
+
const f = e.file;
|
|
477
|
+
if (f && commitFileSet.has(f)) aiFileSet.add(f);
|
|
478
|
+
}
|
|
479
|
+
aiFiles = [...aiFileSet];
|
|
348
480
|
for (const entry2 of changeEntries) {
|
|
349
481
|
const file = entry2.file;
|
|
350
482
|
if (file && commitFileSet.has(file)) {
|
|
351
483
|
relevantTurns.add(typeof entry2.turn === "number" ? entry2.turn : 0);
|
|
352
484
|
}
|
|
353
485
|
}
|
|
486
|
+
for (const entry2 of preBlobEntriesForTurnFix) {
|
|
487
|
+
const file = entry2.file;
|
|
488
|
+
if (file && commitFileSet.has(file)) {
|
|
489
|
+
relevantTurns.add(typeof entry2.turn === "number" ? entry2.turn : 0);
|
|
490
|
+
}
|
|
491
|
+
}
|
|
354
492
|
relevantPromptEntries = promptEntries.filter((e) => {
|
|
355
493
|
const turn = typeof e.turn === "number" ? e.turn : 0;
|
|
356
494
|
return relevantTurns.has(turn);
|
|
@@ -362,8 +500,18 @@ async function recordCommitEntry(opts) {
|
|
|
362
500
|
relevantPromptEntries = promptEntries;
|
|
363
501
|
}
|
|
364
502
|
const transcriptPath = opts.transcriptPath ?? await readSavedTranscriptPath(sessionDir);
|
|
503
|
+
let crossTurnCommit = false;
|
|
504
|
+
if (hasTurnData && relevantTurns.size > 0) {
|
|
505
|
+
const turnFilePath = join2(sessionDir, TURN_FILE);
|
|
506
|
+
let currentTurn = 0;
|
|
507
|
+
if (existsSync3(turnFilePath)) {
|
|
508
|
+
currentTurn = Number.parseInt((await readFile3(turnFilePath, "utf-8")).trim(), 10) || 0;
|
|
509
|
+
}
|
|
510
|
+
const minRelevantTurn = Math.min(...relevantTurns);
|
|
511
|
+
crossTurnCommit = minRelevantTurn < currentTurn;
|
|
512
|
+
}
|
|
365
513
|
let interactions;
|
|
366
|
-
if (transcriptPath && prompts.length > 0) {
|
|
514
|
+
if (transcriptPath && prompts.length > 0 && !crossTurnCommit) {
|
|
367
515
|
const allInteractions = await claudeCode.extractInteractions(transcriptPath);
|
|
368
516
|
interactions = allInteractions.length > 0 ? allInteractions.slice(-prompts.length) : prompts.map((p) => ({ prompt: p, response: null }));
|
|
369
517
|
} else {
|
|
@@ -372,13 +520,23 @@ async function recordCommitEntry(opts) {
|
|
|
372
520
|
if (hasTurnData) {
|
|
373
521
|
attachFilesTouched(changeEntries, relevantPromptEntries, interactions, commitFileSet);
|
|
374
522
|
}
|
|
523
|
+
const lineCounts = await computeLineAttribution({
|
|
524
|
+
sessionDir,
|
|
525
|
+
commitFileSet,
|
|
526
|
+
aiFileSet: new Set(aiFiles),
|
|
527
|
+
relevantTurns,
|
|
528
|
+
hasTurnData,
|
|
529
|
+
changeEntries
|
|
530
|
+
});
|
|
375
531
|
const entry = buildEntry({
|
|
376
532
|
sessionId: opts.sessionId,
|
|
377
533
|
interactions,
|
|
378
534
|
commitFiles,
|
|
379
|
-
aiFiles
|
|
535
|
+
aiFiles,
|
|
536
|
+
lineCounts: lineCounts ?? void 0
|
|
380
537
|
});
|
|
381
538
|
await writeNote(commitSha, entry);
|
|
539
|
+
await recordConsumedPairs(sessionDir, changeEntries, commitFileSet);
|
|
382
540
|
return { promptCount: interactions.length, aiRatio: entry.ai_ratio };
|
|
383
541
|
}
|
|
384
542
|
function attachFilesTouched(changeEntries, promptEntries, interactions, commitFileSet) {
|
|
@@ -403,7 +561,17 @@ function attachFilesTouched(changeEntries, promptEntries, interactions, commitFi
|
|
|
403
561
|
async function readAllSessionJsonl(sessionDir, baseFile) {
|
|
404
562
|
const stem = baseFile.slice(0, baseFile.lastIndexOf(".jsonl"));
|
|
405
563
|
const files = await readdir(sessionDir).catch(() => []);
|
|
406
|
-
const matching = files.filter((f) =>
|
|
564
|
+
const matching = files.filter((f) => {
|
|
565
|
+
if (f === baseFile) return true;
|
|
566
|
+
const suffix = f.slice(stem.length + 1, -".jsonl".length);
|
|
567
|
+
return f.startsWith(`${stem}-`) && f.endsWith(".jsonl") && ARCHIVE_ID_RE.test(suffix);
|
|
568
|
+
}).sort((a, b) => {
|
|
569
|
+
const getId = (f) => {
|
|
570
|
+
const s = f.slice(stem.length + 1, -".jsonl".length);
|
|
571
|
+
return s ? parseInt(s, 36) : Infinity;
|
|
572
|
+
};
|
|
573
|
+
return getId(a) - getId(b);
|
|
574
|
+
}).map((f) => join2(sessionDir, f));
|
|
407
575
|
const all = [];
|
|
408
576
|
for (const file of matching) {
|
|
409
577
|
const entries = await readJsonlEntries(file);
|
|
@@ -417,6 +585,168 @@ async function readSavedTranscriptPath(sessionDir) {
|
|
|
417
585
|
const p = (await readFile3(saved, "utf-8")).trim();
|
|
418
586
|
return p || null;
|
|
419
587
|
}
|
|
588
|
+
async function computeLineAttribution(opts) {
|
|
589
|
+
const { sessionDir, commitFileSet, aiFileSet, relevantTurns, hasTurnData, changeEntries } = opts;
|
|
590
|
+
let diffTreeOutput;
|
|
591
|
+
try {
|
|
592
|
+
diffTreeOutput = await git(["diff-tree", "--raw", "--root", "-r", "HEAD"]);
|
|
593
|
+
} catch {
|
|
594
|
+
return null;
|
|
595
|
+
}
|
|
596
|
+
const committedBlobs = parseDiffTreeBlobs(diffTreeOutput);
|
|
597
|
+
if (committedBlobs.size === 0) return null;
|
|
598
|
+
await ensureEmptyBlobInStore();
|
|
599
|
+
const preBlobEntries = await readAllSessionJsonl(sessionDir, PRE_BLOBS_FILE);
|
|
600
|
+
const hasPreBlobData = preBlobEntries.some((e) => e.blob);
|
|
601
|
+
const hasPostBlobData = changeEntries.some((e) => e.blob);
|
|
602
|
+
if (!hasPreBlobData && !hasPostBlobData) return null;
|
|
603
|
+
const preBlobById = /* @__PURE__ */ new Map();
|
|
604
|
+
const preBlobsFallback = /* @__PURE__ */ new Map();
|
|
605
|
+
for (const entry of preBlobEntries) {
|
|
606
|
+
const file = entry.file;
|
|
607
|
+
const turn = typeof entry.turn === "number" ? entry.turn : 0;
|
|
608
|
+
const id = entry.tool_use_id;
|
|
609
|
+
if (!file || !commitFileSet.has(file)) continue;
|
|
610
|
+
if (hasTurnData && !relevantTurns.has(turn)) continue;
|
|
611
|
+
if (id) {
|
|
612
|
+
preBlobById.set(id, { file, blob: entry.blob || "", turn });
|
|
613
|
+
} else {
|
|
614
|
+
if (!preBlobsFallback.has(file)) preBlobsFallback.set(file, []);
|
|
615
|
+
preBlobsFallback.get(file)?.push(entry.blob || "");
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
const turnPairsByFile = /* @__PURE__ */ new Map();
|
|
619
|
+
const hadNewFileEditByFile = /* @__PURE__ */ new Map();
|
|
620
|
+
const postBlobsFallback = /* @__PURE__ */ new Map();
|
|
621
|
+
for (const entry of changeEntries) {
|
|
622
|
+
const file = entry.file;
|
|
623
|
+
const turn = typeof entry.turn === "number" ? entry.turn : 0;
|
|
624
|
+
const id = entry.tool_use_id;
|
|
625
|
+
const postBlob = entry.blob || "";
|
|
626
|
+
if (!file || !commitFileSet.has(file) || !postBlob) continue;
|
|
627
|
+
if (id) {
|
|
628
|
+
const pre = preBlobById.get(id);
|
|
629
|
+
if (!pre) continue;
|
|
630
|
+
if (hasTurnData && !relevantTurns.has(pre.turn)) continue;
|
|
631
|
+
if (!pre.blob) {
|
|
632
|
+
hadNewFileEditByFile.set(file, true);
|
|
633
|
+
} else {
|
|
634
|
+
if (!turnPairsByFile.has(file)) turnPairsByFile.set(file, []);
|
|
635
|
+
turnPairsByFile.get(file)?.push({ preBlob: pre.blob, postBlob });
|
|
636
|
+
}
|
|
637
|
+
} else {
|
|
638
|
+
if (hasTurnData && !relevantTurns.has(turn)) continue;
|
|
639
|
+
if (!postBlobsFallback.has(file)) postBlobsFallback.set(file, []);
|
|
640
|
+
postBlobsFallback.get(file)?.push(postBlob);
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
for (const [file, postBlobs] of postBlobsFallback) {
|
|
644
|
+
const preBlobs = preBlobsFallback.get(file) ?? [];
|
|
645
|
+
const pairCount = Math.min(preBlobs.length, postBlobs.length);
|
|
646
|
+
for (let i = 0; i < pairCount; i++) {
|
|
647
|
+
const pre = preBlobs[i] || "";
|
|
648
|
+
const post = postBlobs[i] || "";
|
|
649
|
+
if (!pre) {
|
|
650
|
+
hadNewFileEditByFile.set(file, true);
|
|
651
|
+
} else if (post) {
|
|
652
|
+
if (!turnPairsByFile.has(file)) turnPairsByFile.set(file, []);
|
|
653
|
+
turnPairsByFile.get(file)?.push({ preBlob: pre, postBlob: post });
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
for (const file of aiFileSet) {
|
|
658
|
+
if (!commitFileSet.has(file)) continue;
|
|
659
|
+
const hasPairs = (turnPairsByFile.get(file) ?? []).length > 0;
|
|
660
|
+
const hasNewFileEdit = hadNewFileEditByFile.get(file) ?? false;
|
|
661
|
+
if (!hasPairs && !hasNewFileEdit) {
|
|
662
|
+
return null;
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
let totalAiAdded = 0;
|
|
666
|
+
let totalAdded = 0;
|
|
667
|
+
let totalDeleted = 0;
|
|
668
|
+
for (const file of commitFileSet) {
|
|
669
|
+
const blobs = committedBlobs.get(file);
|
|
670
|
+
if (!blobs) continue;
|
|
671
|
+
const { parentBlob, committedBlob } = blobs;
|
|
672
|
+
const turnPairs = turnPairsByFile.get(file) ?? [];
|
|
673
|
+
const hadNewFileEdit = hadNewFileEditByFile.get(file) ?? false;
|
|
674
|
+
try {
|
|
675
|
+
const result = await computePositionAttribution(parentBlob, committedBlob, turnPairs);
|
|
676
|
+
if (hadNewFileEdit && aiFileSet.has(file) && turnPairs.length === 0) {
|
|
677
|
+
totalAiAdded += result.totalAddedLines;
|
|
678
|
+
} else {
|
|
679
|
+
totalAiAdded += result.aiAddedLines;
|
|
680
|
+
}
|
|
681
|
+
totalAdded += result.totalAddedLines;
|
|
682
|
+
totalDeleted += result.deletedLines;
|
|
683
|
+
} catch {
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
return { aiAddedLines: totalAiAdded, totalAddedLines: totalAdded, deletedLines: totalDeleted };
|
|
687
|
+
}
|
|
688
|
+
function parseDiffTreeBlobs(output) {
|
|
689
|
+
const map = /* @__PURE__ */ new Map();
|
|
690
|
+
const ZEROS = "0000000000000000000000000000000000000000";
|
|
691
|
+
for (const line of output.split("\n")) {
|
|
692
|
+
const m = line.match(/^:\d+ \d+ ([0-9a-f]+) ([0-9a-f]+) \w+\t(.+)$/);
|
|
693
|
+
if (!m) continue;
|
|
694
|
+
const parentBlob = m[1] === ZEROS ? EMPTY_BLOB : m[1];
|
|
695
|
+
const committedBlob = m[2] === ZEROS ? EMPTY_BLOB : m[2];
|
|
696
|
+
const paths = m[3];
|
|
697
|
+
const parts = paths.split(" ");
|
|
698
|
+
const file = parts[parts.length - 1];
|
|
699
|
+
map.set(file, { parentBlob, committedBlob });
|
|
700
|
+
}
|
|
701
|
+
return map;
|
|
702
|
+
}
|
|
703
|
+
async function readConsumedPairs(sessionDir) {
|
|
704
|
+
const file = join2(sessionDir, COMMITTED_PAIRS_FILE);
|
|
705
|
+
if (!existsSync3(file)) return /* @__PURE__ */ new Set();
|
|
706
|
+
const entries = await readJsonlEntries(file);
|
|
707
|
+
const set = /* @__PURE__ */ new Set();
|
|
708
|
+
for (const e of entries) {
|
|
709
|
+
if (e.tool_use_id) {
|
|
710
|
+
set.add(`id:${e.tool_use_id}`);
|
|
711
|
+
} else if (e.turn !== void 0 && e.file) {
|
|
712
|
+
set.add(`${e.turn}:${e.file}`);
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
return set;
|
|
716
|
+
}
|
|
717
|
+
function consumedKey(entry) {
|
|
718
|
+
if (entry.tool_use_id) return `id:${entry.tool_use_id}`;
|
|
719
|
+
return `${entry.turn}:${entry.file}`;
|
|
720
|
+
}
|
|
721
|
+
async function recordConsumedPairs(sessionDir, changeEntries, commitFileSet) {
|
|
722
|
+
const seen = /* @__PURE__ */ new Set();
|
|
723
|
+
const pairsFile = join2(sessionDir, COMMITTED_PAIRS_FILE);
|
|
724
|
+
for (const entry of changeEntries) {
|
|
725
|
+
const file = entry.file;
|
|
726
|
+
if (!file || !commitFileSet.has(file)) continue;
|
|
727
|
+
const key = consumedKey(entry);
|
|
728
|
+
if (seen.has(key)) continue;
|
|
729
|
+
seen.add(key);
|
|
730
|
+
await appendJsonl(pairsFile, {
|
|
731
|
+
turn: entry.turn,
|
|
732
|
+
file,
|
|
733
|
+
tool_use_id: entry.tool_use_id ?? null
|
|
734
|
+
});
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
async function ensureEmptyBlobInStore() {
|
|
738
|
+
const tmp = join2(tmpdir(), `agentnote-empty-${process.pid}.tmp`);
|
|
739
|
+
try {
|
|
740
|
+
await writeFile2(tmp, "");
|
|
741
|
+
await git(["hash-object", "-w", tmp]);
|
|
742
|
+
} catch {
|
|
743
|
+
} finally {
|
|
744
|
+
try {
|
|
745
|
+
await unlink(tmp);
|
|
746
|
+
} catch {
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
}
|
|
420
750
|
|
|
421
751
|
// src/paths.ts
|
|
422
752
|
import { join as join3 } from "node:path";
|
|
@@ -484,34 +814,48 @@ async function commit(args2) {
|
|
|
484
814
|
|
|
485
815
|
// src/commands/hook.ts
|
|
486
816
|
import { existsSync as existsSync6 } from "node:fs";
|
|
487
|
-
import { mkdir as mkdir2, readFile as readFile5, realpath, writeFile as
|
|
817
|
+
import { mkdir as mkdir2, readFile as readFile5, realpath, writeFile as writeFile3 } from "node:fs/promises";
|
|
488
818
|
import { isAbsolute, join as join5, relative } from "node:path";
|
|
489
819
|
|
|
490
820
|
// src/core/rotate.ts
|
|
491
821
|
import { existsSync as existsSync5 } from "node:fs";
|
|
492
|
-
import {
|
|
822
|
+
import { rename } from "node:fs/promises";
|
|
493
823
|
import { join as join4 } from "node:path";
|
|
494
|
-
async function rotateLogs(sessionDir,
|
|
495
|
-
await purgeRotatedArchives(sessionDir, fileNames);
|
|
824
|
+
async function rotateLogs(sessionDir, rotateId, fileNames = [PROMPTS_FILE, CHANGES_FILE]) {
|
|
496
825
|
for (const name of fileNames) {
|
|
497
826
|
const src = join4(sessionDir, name);
|
|
498
827
|
if (existsSync5(src)) {
|
|
499
828
|
const base = name.replace(".jsonl", "");
|
|
500
|
-
await rename(src, join4(sessionDir, `${base}-${
|
|
829
|
+
await rename(src, join4(sessionDir, `${base}-${rotateId}.jsonl`));
|
|
501
830
|
}
|
|
502
831
|
}
|
|
503
832
|
}
|
|
504
|
-
async function purgeRotatedArchives(sessionDir, fileNames) {
|
|
505
|
-
const files = await readdir2(sessionDir).catch(() => []);
|
|
506
|
-
for (const name of fileNames) {
|
|
507
|
-
const stem = name.replace(".jsonl", "");
|
|
508
|
-
const rotated = files.filter((f) => f.startsWith(`${stem}-`) && f.endsWith(".jsonl"));
|
|
509
|
-
await Promise.all(rotated.map((f) => unlink(join4(sessionDir, f)).catch(() => {
|
|
510
|
-
})));
|
|
511
|
-
}
|
|
512
|
-
}
|
|
513
833
|
|
|
514
834
|
// src/commands/hook.ts
|
|
835
|
+
async function normalizeToRepoRelative(filePath) {
|
|
836
|
+
if (!isAbsolute(filePath)) return filePath;
|
|
837
|
+
try {
|
|
838
|
+
const rawRoot = (await git(["rev-parse", "--show-toplevel"])).trim();
|
|
839
|
+
const repoRoot2 = await realpath(rawRoot);
|
|
840
|
+
let normalized = filePath;
|
|
841
|
+
if (repoRoot2.startsWith("/private") && !normalized.startsWith("/private")) {
|
|
842
|
+
normalized = `/private${normalized}`;
|
|
843
|
+
} else if (!repoRoot2.startsWith("/private") && normalized.startsWith("/private")) {
|
|
844
|
+
normalized = normalized.replace(/^\/private/, "");
|
|
845
|
+
}
|
|
846
|
+
return relative(repoRoot2, normalized);
|
|
847
|
+
} catch {
|
|
848
|
+
return filePath;
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
async function blobHash(absPath) {
|
|
852
|
+
try {
|
|
853
|
+
if (!existsSync6(absPath)) return EMPTY_BLOB;
|
|
854
|
+
return (await git(["hash-object", "-w", absPath])).trim();
|
|
855
|
+
} catch {
|
|
856
|
+
return EMPTY_BLOB;
|
|
857
|
+
}
|
|
858
|
+
}
|
|
515
859
|
async function readStdin() {
|
|
516
860
|
const chunks = [];
|
|
517
861
|
for await (const chunk of process.stdin) {
|
|
@@ -537,9 +881,9 @@ async function hook() {
|
|
|
537
881
|
await mkdir2(sessionDir, { recursive: true });
|
|
538
882
|
switch (event.kind) {
|
|
539
883
|
case "session_start": {
|
|
540
|
-
await
|
|
884
|
+
await writeFile3(join5(agentnoteDirPath, SESSION_FILE), event.sessionId);
|
|
541
885
|
if (event.transcriptPath) {
|
|
542
|
-
await
|
|
886
|
+
await writeFile3(join5(sessionDir, TRANSCRIPT_PATH_FILE), event.transcriptPath);
|
|
543
887
|
}
|
|
544
888
|
await appendJsonl(join5(sessionDir, EVENTS_FILE), {
|
|
545
889
|
event: "session_start",
|
|
@@ -547,12 +891,13 @@ async function hook() {
|
|
|
547
891
|
timestamp: event.timestamp,
|
|
548
892
|
model: event.model ?? null
|
|
549
893
|
});
|
|
894
|
+
await writeFile3(join5(sessionDir, HEARTBEAT_FILE), String(Date.now()));
|
|
550
895
|
break;
|
|
551
896
|
}
|
|
552
897
|
case "stop": {
|
|
553
|
-
await
|
|
898
|
+
await writeFile3(join5(agentnoteDirPath, SESSION_FILE), event.sessionId);
|
|
554
899
|
if (event.transcriptPath) {
|
|
555
|
-
await
|
|
900
|
+
await writeFile3(join5(sessionDir, TRANSCRIPT_PATH_FILE), event.transcriptPath);
|
|
556
901
|
}
|
|
557
902
|
await appendJsonl(join5(sessionDir, EVENTS_FILE), {
|
|
558
903
|
event: "stop",
|
|
@@ -563,7 +908,7 @@ async function hook() {
|
|
|
563
908
|
}
|
|
564
909
|
case "prompt": {
|
|
565
910
|
const rotateId = Date.now().toString(36);
|
|
566
|
-
await rotateLogs(sessionDir, rotateId);
|
|
911
|
+
await rotateLogs(sessionDir, rotateId, [PROMPTS_FILE, CHANGES_FILE, PRE_BLOBS_FILE]);
|
|
567
912
|
const turnPath = join5(sessionDir, TURN_FILE);
|
|
568
913
|
let turn = 0;
|
|
569
914
|
if (existsSync6(turnPath)) {
|
|
@@ -571,44 +916,58 @@ async function hook() {
|
|
|
571
916
|
turn = Number.parseInt(raw2, 10) || 0;
|
|
572
917
|
}
|
|
573
918
|
turn += 1;
|
|
574
|
-
await
|
|
919
|
+
await writeFile3(turnPath, String(turn));
|
|
575
920
|
await appendJsonl(join5(sessionDir, PROMPTS_FILE), {
|
|
576
921
|
event: "prompt",
|
|
577
922
|
timestamp: event.timestamp,
|
|
578
923
|
prompt: event.prompt,
|
|
579
924
|
turn
|
|
580
925
|
});
|
|
926
|
+
await writeFile3(join5(sessionDir, HEARTBEAT_FILE), String(Date.now()));
|
|
581
927
|
break;
|
|
582
928
|
}
|
|
583
|
-
case "
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
normalizedFile = `/private${normalizedFile}`;
|
|
592
|
-
} else if (!repoRoot2.startsWith("/private") && normalizedFile.startsWith("/private")) {
|
|
593
|
-
normalizedFile = normalizedFile.replace(/^\/private/, "");
|
|
594
|
-
}
|
|
595
|
-
filePath = relative(repoRoot2, normalizedFile);
|
|
596
|
-
} catch {
|
|
597
|
-
}
|
|
929
|
+
case "pre_edit": {
|
|
930
|
+
const absPath = event.file ?? "";
|
|
931
|
+
const filePath = await normalizeToRepoRelative(absPath);
|
|
932
|
+
let turn = 0;
|
|
933
|
+
const turnPath = join5(sessionDir, TURN_FILE);
|
|
934
|
+
if (existsSync6(turnPath)) {
|
|
935
|
+
const raw2 = (await readFile5(turnPath, "utf-8")).trim();
|
|
936
|
+
turn = Number.parseInt(raw2, 10) || 0;
|
|
598
937
|
}
|
|
938
|
+
const preBlob = isAbsolute(absPath) ? await blobHash(absPath) : EMPTY_BLOB;
|
|
939
|
+
await appendJsonl(join5(sessionDir, PRE_BLOBS_FILE), {
|
|
940
|
+
event: "pre_blob",
|
|
941
|
+
turn,
|
|
942
|
+
file: filePath,
|
|
943
|
+
blob: preBlob,
|
|
944
|
+
// tool_use_id links this pre-blob to its PostToolUse counterpart,
|
|
945
|
+
// enabling correct pairing even when async hooks fire out of order.
|
|
946
|
+
tool_use_id: event.toolUseId ?? null
|
|
947
|
+
});
|
|
948
|
+
break;
|
|
949
|
+
}
|
|
950
|
+
case "file_change": {
|
|
951
|
+
const absPath = event.file ?? "";
|
|
952
|
+
const filePath = await normalizeToRepoRelative(absPath);
|
|
599
953
|
let turn = 0;
|
|
600
954
|
const turnPath = join5(sessionDir, TURN_FILE);
|
|
601
955
|
if (existsSync6(turnPath)) {
|
|
602
956
|
const raw2 = (await readFile5(turnPath, "utf-8")).trim();
|
|
603
957
|
turn = Number.parseInt(raw2, 10) || 0;
|
|
604
958
|
}
|
|
959
|
+
const postBlob = isAbsolute(absPath) ? await blobHash(absPath) : EMPTY_BLOB;
|
|
605
960
|
await appendJsonl(join5(sessionDir, CHANGES_FILE), {
|
|
606
961
|
event: "file_change",
|
|
607
962
|
timestamp: event.timestamp,
|
|
608
963
|
tool: event.tool,
|
|
609
964
|
file: filePath,
|
|
610
965
|
session_id: event.sessionId,
|
|
611
|
-
turn
|
|
966
|
+
turn,
|
|
967
|
+
blob: postBlob,
|
|
968
|
+
// Same tool_use_id as the matching pre_blob entry — used for reliable pairing
|
|
969
|
+
// even when this async hook fires after the next prompt has advanced the turn counter.
|
|
970
|
+
tool_use_id: event.toolUseId ?? null
|
|
612
971
|
});
|
|
613
972
|
break;
|
|
614
973
|
}
|
|
@@ -644,7 +1003,7 @@ async function hook() {
|
|
|
644
1003
|
|
|
645
1004
|
// src/commands/init.ts
|
|
646
1005
|
import { existsSync as existsSync7 } from "node:fs";
|
|
647
|
-
import { mkdir as mkdir3, writeFile as
|
|
1006
|
+
import { mkdir as mkdir3, writeFile as writeFile4 } from "node:fs/promises";
|
|
648
1007
|
import { join as join6 } from "node:path";
|
|
649
1008
|
var WORKFLOW_TEMPLATE = `name: Agent Note
|
|
650
1009
|
on:
|
|
@@ -691,7 +1050,7 @@ async function init(args2) {
|
|
|
691
1050
|
results.push(" \xB7 workflow already exists at .github/workflows/agentnote.yml");
|
|
692
1051
|
} else {
|
|
693
1052
|
await mkdir3(workflowDir, { recursive: true });
|
|
694
|
-
await
|
|
1053
|
+
await writeFile4(workflowPath, WORKFLOW_TEMPLATE);
|
|
695
1054
|
results.push(" \u2713 workflow created at .github/workflows/agentnote.yml");
|
|
696
1055
|
}
|
|
697
1056
|
}
|
|
@@ -790,7 +1149,9 @@ async function collectReport(base) {
|
|
|
790
1149
|
files_total: 0,
|
|
791
1150
|
files_ai: 0,
|
|
792
1151
|
files: [],
|
|
793
|
-
interactions: []
|
|
1152
|
+
interactions: [],
|
|
1153
|
+
ai_added_lines: null,
|
|
1154
|
+
total_added_lines: null
|
|
794
1155
|
});
|
|
795
1156
|
continue;
|
|
796
1157
|
}
|
|
@@ -811,12 +1172,23 @@ async function collectReport(base) {
|
|
|
811
1172
|
path: f,
|
|
812
1173
|
by_ai: filesByAi.includes(f)
|
|
813
1174
|
})),
|
|
814
|
-
interactions
|
|
1175
|
+
interactions,
|
|
1176
|
+
ai_added_lines: entry.ai_added_lines ?? null,
|
|
1177
|
+
total_added_lines: entry.total_added_lines ?? null
|
|
815
1178
|
});
|
|
816
1179
|
}
|
|
817
1180
|
const tracked = commits.filter((c) => c.session_id !== null);
|
|
818
1181
|
const totalFiles = tracked.reduce((s, c) => s + c.files_total, 0);
|
|
819
1182
|
const totalFilesAi = tracked.reduce((s, c) => s + c.files_ai, 0);
|
|
1183
|
+
const allHaveLineData = tracked.every((c) => c.total_added_lines !== null);
|
|
1184
|
+
let overallAiRatio;
|
|
1185
|
+
if (allHaveLineData) {
|
|
1186
|
+
const totalAiLines = tracked.reduce((s, c) => s + (c.ai_added_lines ?? 0), 0);
|
|
1187
|
+
const totalAllLines = tracked.reduce((s, c) => s + (c.total_added_lines ?? 0), 0);
|
|
1188
|
+
overallAiRatio = totalAllLines > 0 ? Math.round(totalAiLines / totalAllLines * 100) : 0;
|
|
1189
|
+
} else {
|
|
1190
|
+
overallAiRatio = totalFiles > 0 ? Math.round(totalFilesAi / totalFiles * 100) : 0;
|
|
1191
|
+
}
|
|
820
1192
|
return {
|
|
821
1193
|
base,
|
|
822
1194
|
head,
|
|
@@ -825,7 +1197,7 @@ async function collectReport(base) {
|
|
|
825
1197
|
total_prompts: tracked.reduce((s, c) => s + c.prompts_count, 0),
|
|
826
1198
|
total_files: totalFiles,
|
|
827
1199
|
total_files_ai: totalFilesAi,
|
|
828
|
-
overall_ai_ratio:
|
|
1200
|
+
overall_ai_ratio: overallAiRatio,
|
|
829
1201
|
commits
|
|
830
1202
|
};
|
|
831
1203
|
}
|
|
@@ -1082,6 +1454,8 @@ async function session(sessionId) {
|
|
|
1082
1454
|
console.log(`Commits: ${matches.length}`);
|
|
1083
1455
|
console.log();
|
|
1084
1456
|
let totalPrompts = 0;
|
|
1457
|
+
let totalAiLines = 0;
|
|
1458
|
+
let totalAllLines = 0;
|
|
1085
1459
|
let totalRatio = 0;
|
|
1086
1460
|
let ratioCount = 0;
|
|
1087
1461
|
for (const m of matches) {
|
|
@@ -1089,16 +1463,22 @@ async function session(sessionId) {
|
|
|
1089
1463
|
if (m.entry) {
|
|
1090
1464
|
const promptCount = m.entry.interactions?.length ?? m.entry.prompts?.length ?? 0;
|
|
1091
1465
|
totalPrompts += promptCount;
|
|
1092
|
-
|
|
1093
|
-
|
|
1466
|
+
if (m.entry.ai_added_lines !== void 0 && m.entry.total_added_lines !== void 0) {
|
|
1467
|
+
totalAiLines += m.entry.ai_added_lines;
|
|
1468
|
+
totalAllLines += m.entry.total_added_lines;
|
|
1469
|
+
} else {
|
|
1470
|
+
totalRatio += m.entry.ai_ratio;
|
|
1471
|
+
ratioCount++;
|
|
1472
|
+
}
|
|
1094
1473
|
suffix = ` [\u{1F916}${m.entry.ai_ratio}% | ${promptCount}p]`;
|
|
1095
1474
|
}
|
|
1096
1475
|
console.log(`${m.shortInfo}${suffix}`);
|
|
1097
1476
|
}
|
|
1098
1477
|
console.log();
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1478
|
+
const displayRatio = totalAllLines > 0 ? Math.round(totalAiLines / totalAllLines * 100) : ratioCount > 0 ? Math.round(totalRatio / ratioCount) : null;
|
|
1479
|
+
if (displayRatio !== null) {
|
|
1480
|
+
const lineDetail = totalAllLines > 0 ? ` (${totalAiLines}/${totalAllLines} lines)` : "";
|
|
1481
|
+
console.log(`Total: ${totalPrompts} prompts, AI ratio ${displayRatio}%${lineDetail}`);
|
|
1102
1482
|
}
|
|
1103
1483
|
}
|
|
1104
1484
|
|
|
@@ -1120,7 +1500,8 @@ async function show(commitRef) {
|
|
|
1120
1500
|
if (entry) {
|
|
1121
1501
|
console.log();
|
|
1122
1502
|
const ratioBar = renderRatioBar(entry.ai_ratio);
|
|
1123
|
-
|
|
1503
|
+
const lineDetail = entry.ai_added_lines !== void 0 && entry.total_added_lines !== void 0 && entry.total_added_lines > 0 ? ` (${entry.ai_added_lines}/${entry.total_added_lines} lines)` : "";
|
|
1504
|
+
console.log(`ai: ${entry.ai_ratio}%${lineDetail} ${ratioBar}`);
|
|
1124
1505
|
console.log(
|
|
1125
1506
|
`files: ${entry.files_in_commit.length} changed, ${entry.files_by_ai.length} by AI`
|
|
1126
1507
|
);
|
|
@@ -1181,7 +1562,7 @@ function truncateLines(text, maxLen) {
|
|
|
1181
1562
|
// src/commands/status.ts
|
|
1182
1563
|
import { existsSync as existsSync8 } from "node:fs";
|
|
1183
1564
|
import { readFile as readFile6 } from "node:fs/promises";
|
|
1184
|
-
var VERSION = "0.1.
|
|
1565
|
+
var VERSION = "0.1.7";
|
|
1185
1566
|
async function status() {
|
|
1186
1567
|
console.log(`agentnote v${VERSION}`);
|
|
1187
1568
|
console.log();
|
|
@@ -1210,7 +1591,7 @@ async function status() {
|
|
|
1210
1591
|
}
|
|
1211
1592
|
|
|
1212
1593
|
// src/cli.ts
|
|
1213
|
-
var VERSION2 = "0.1.
|
|
1594
|
+
var VERSION2 = "0.1.7";
|
|
1214
1595
|
var HELP = `
|
|
1215
1596
|
agentnote v${VERSION2} \u2014 remember why your code changed
|
|
1216
1597
|
|