@wasabeef/agentnote 0.1.5 → 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 +1018 -514
- package/package.json +9 -4
package/dist/cli.js
CHANGED
|
@@ -1,83 +1,59 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
// src/commands/
|
|
4
|
-
import {
|
|
5
|
-
import { existsSync as
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
// src/paths.ts
|
|
9
|
-
import { join } from "node:path";
|
|
3
|
+
// src/commands/commit.ts
|
|
4
|
+
import { spawn } from "node:child_process";
|
|
5
|
+
import { existsSync as existsSync4 } from "node:fs";
|
|
6
|
+
import { readFile as readFile4 } from "node:fs/promises";
|
|
10
7
|
|
|
11
|
-
// src/
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
var
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
8
|
+
// src/core/constants.ts
|
|
9
|
+
var TRAILER_KEY = "Agentnote-Session";
|
|
10
|
+
var NOTES_REF = "agentnote";
|
|
11
|
+
var NOTES_REF_FULL = `refs/notes/${NOTES_REF}`;
|
|
12
|
+
var NOTES_FETCH_REFSPEC = `+${NOTES_REF_FULL}:${NOTES_REF_FULL}`;
|
|
13
|
+
var AGENTNOTE_DIR = "agentnote";
|
|
14
|
+
var SESSIONS_DIR = "sessions";
|
|
15
|
+
var PROMPTS_FILE = "prompts.jsonl";
|
|
16
|
+
var CHANGES_FILE = "changes.jsonl";
|
|
17
|
+
var EVENTS_FILE = "events.jsonl";
|
|
18
|
+
var TRANSCRIPT_PATH_FILE = "transcript_path";
|
|
19
|
+
var TURN_FILE = "turn";
|
|
20
|
+
var SESSION_FILE = "session";
|
|
21
|
+
var MAX_COMMITS = 500;
|
|
22
|
+
var BAR_WIDTH_COMPACT = 5;
|
|
23
|
+
var BAR_WIDTH_FULL = 20;
|
|
24
|
+
var TRUNCATE_PROMPT = 120;
|
|
25
|
+
var TRUNCATE_RESPONSE_SHOW = 200;
|
|
26
|
+
var TRUNCATE_RESPONSE_PR = 500;
|
|
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";
|
|
33
|
+
var SCHEMA_VERSION = 1;
|
|
34
|
+
var DEBUG = !!process.env.AGENTNOTE_DEBUG;
|
|
33
35
|
|
|
34
|
-
// src/
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
try {
|
|
40
|
-
_root = await repoRoot();
|
|
41
|
-
} catch {
|
|
42
|
-
console.error("error: git repository not found");
|
|
43
|
-
process.exit(1);
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
return _root;
|
|
47
|
-
}
|
|
48
|
-
async function gitDir() {
|
|
49
|
-
if (!_gitDir) {
|
|
50
|
-
_gitDir = await git(["rev-parse", "--git-dir"]);
|
|
51
|
-
if (!_gitDir.startsWith("/")) {
|
|
52
|
-
_gitDir = join(await root(), _gitDir);
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
return _gitDir;
|
|
56
|
-
}
|
|
57
|
-
async function agentnoteDir() {
|
|
58
|
-
return join(await gitDir(), "agentnote");
|
|
59
|
-
}
|
|
60
|
-
async function sessionFile() {
|
|
61
|
-
return join(await agentnoteDir(), "session");
|
|
62
|
-
}
|
|
36
|
+
// src/core/record.ts
|
|
37
|
+
import { existsSync as existsSync3 } from "node:fs";
|
|
38
|
+
import { readdir, readFile as readFile3, unlink, writeFile as writeFile2 } from "node:fs/promises";
|
|
39
|
+
import { tmpdir } from "node:os";
|
|
40
|
+
import { join as join2 } from "node:path";
|
|
63
41
|
|
|
64
42
|
// src/agents/claude-code.ts
|
|
65
|
-
import { readFile, writeFile, mkdir } from "node:fs/promises";
|
|
66
43
|
import { existsSync, readdirSync } from "node:fs";
|
|
67
|
-
import {
|
|
44
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
68
45
|
import { homedir } from "node:os";
|
|
46
|
+
import { join } from "node:path";
|
|
69
47
|
var HOOK_COMMAND = "npx --yes @wasabeef/agentnote hook";
|
|
70
48
|
var HOOKS_CONFIG = {
|
|
71
|
-
SessionStart: [
|
|
72
|
-
|
|
73
|
-
],
|
|
74
|
-
Stop: [
|
|
75
|
-
{ hooks: [{ type: "command", command: HOOK_COMMAND, async: true }] }
|
|
76
|
-
],
|
|
77
|
-
UserPromptSubmit: [
|
|
78
|
-
{ hooks: [{ type: "command", command: HOOK_COMMAND, async: true }] }
|
|
79
|
-
],
|
|
49
|
+
SessionStart: [{ hooks: [{ type: "command", command: HOOK_COMMAND, async: true }] }],
|
|
50
|
+
Stop: [{ hooks: [{ type: "command", command: HOOK_COMMAND, async: true }] }],
|
|
51
|
+
UserPromptSubmit: [{ hooks: [{ type: "command", command: HOOK_COMMAND, async: true }] }],
|
|
80
52
|
PreToolUse: [
|
|
53
|
+
{
|
|
54
|
+
matcher: "Edit|Write|NotebookEdit",
|
|
55
|
+
hooks: [{ type: "command", command: HOOK_COMMAND }]
|
|
56
|
+
},
|
|
81
57
|
{
|
|
82
58
|
matcher: "Bash",
|
|
83
59
|
hooks: [{ type: "command", if: "Bash(*git commit*)", command: HOOK_COMMAND }]
|
|
@@ -95,18 +71,17 @@ function isValidSessionId(id) {
|
|
|
95
71
|
return UUID_PATTERN.test(id);
|
|
96
72
|
}
|
|
97
73
|
function isValidTranscriptPath(p) {
|
|
98
|
-
const claudeBase =
|
|
74
|
+
const claudeBase = join(homedir(), ".claude");
|
|
99
75
|
return p.startsWith(claudeBase);
|
|
100
76
|
}
|
|
101
77
|
function isGitCommit(cmd) {
|
|
102
|
-
|
|
103
|
-
return (trimmed.startsWith("git commit") || trimmed.startsWith("git -c ")) && trimmed.includes("commit") && !trimmed.includes("--amend");
|
|
78
|
+
return cmd.includes("git commit") && !cmd.includes("--amend");
|
|
104
79
|
}
|
|
105
80
|
var claudeCode = {
|
|
106
81
|
name: "claude-code",
|
|
107
82
|
settingsRelPath: ".claude/settings.json",
|
|
108
83
|
async installHooks(repoRoot2) {
|
|
109
|
-
const settingsPath =
|
|
84
|
+
const settingsPath = join(repoRoot2, this.settingsRelPath);
|
|
110
85
|
const { dirname } = await import("node:path");
|
|
111
86
|
await mkdir(dirname(settingsPath), { recursive: true });
|
|
112
87
|
let settings = {};
|
|
@@ -119,15 +94,17 @@ var claudeCode = {
|
|
|
119
94
|
}
|
|
120
95
|
const hooks = settings.hooks ?? {};
|
|
121
96
|
const raw = JSON.stringify(hooks);
|
|
122
|
-
if (raw.includes("@wasabeef/agentnote"))
|
|
97
|
+
if (raw.includes("@wasabeef/agentnote") || raw.includes("agentnote hook") || raw.includes("cli.js hook"))
|
|
98
|
+
return;
|
|
123
99
|
for (const [event, entries] of Object.entries(HOOKS_CONFIG)) {
|
|
124
100
|
hooks[event] = [...hooks[event] ?? [], ...entries];
|
|
125
101
|
}
|
|
126
102
|
settings.hooks = hooks;
|
|
127
|
-
await writeFile(settingsPath, JSON.stringify(settings, null, 2)
|
|
103
|
+
await writeFile(settingsPath, `${JSON.stringify(settings, null, 2)}
|
|
104
|
+
`);
|
|
128
105
|
},
|
|
129
106
|
async removeHooks(repoRoot2) {
|
|
130
|
-
const settingsPath =
|
|
107
|
+
const settingsPath = join(repoRoot2, this.settingsRelPath);
|
|
131
108
|
if (!existsSync(settingsPath)) return;
|
|
132
109
|
try {
|
|
133
110
|
const settings = JSON.parse(await readFile(settingsPath, "utf-8"));
|
|
@@ -135,21 +112,22 @@ var claudeCode = {
|
|
|
135
112
|
for (const [event, entries] of Object.entries(settings.hooks)) {
|
|
136
113
|
settings.hooks[event] = entries.filter((e) => {
|
|
137
114
|
const text = JSON.stringify(e);
|
|
138
|
-
return !text.includes("@wasabeef/agentnote");
|
|
115
|
+
return !text.includes("@wasabeef/agentnote") && !text.includes("agentnote hook") && !text.includes("cli.js hook");
|
|
139
116
|
});
|
|
140
117
|
if (settings.hooks[event].length === 0) delete settings.hooks[event];
|
|
141
118
|
}
|
|
142
119
|
if (Object.keys(settings.hooks).length === 0) delete settings.hooks;
|
|
143
|
-
await writeFile(settingsPath, JSON.stringify(settings, null, 2)
|
|
120
|
+
await writeFile(settingsPath, `${JSON.stringify(settings, null, 2)}
|
|
121
|
+
`);
|
|
144
122
|
} catch {
|
|
145
123
|
}
|
|
146
124
|
},
|
|
147
125
|
async isEnabled(repoRoot2) {
|
|
148
|
-
const settingsPath =
|
|
126
|
+
const settingsPath = join(repoRoot2, this.settingsRelPath);
|
|
149
127
|
if (!existsSync(settingsPath)) return false;
|
|
150
128
|
try {
|
|
151
129
|
const content = await readFile(settingsPath, "utf-8");
|
|
152
|
-
return content.includes("@wasabeef/agentnote");
|
|
130
|
+
return content.includes("@wasabeef/agentnote") || content.includes("agentnote hook") || content.includes("cli.js hook");
|
|
153
131
|
} catch {
|
|
154
132
|
return false;
|
|
155
133
|
}
|
|
@@ -167,14 +145,31 @@ var claudeCode = {
|
|
|
167
145
|
const tp = e.transcript_path && isValidTranscriptPath(e.transcript_path) ? e.transcript_path : void 0;
|
|
168
146
|
switch (e.hook_event_name) {
|
|
169
147
|
case "SessionStart":
|
|
170
|
-
return {
|
|
148
|
+
return {
|
|
149
|
+
kind: "session_start",
|
|
150
|
+
sessionId: sid,
|
|
151
|
+
timestamp: ts,
|
|
152
|
+
model: e.model,
|
|
153
|
+
transcriptPath: tp
|
|
154
|
+
};
|
|
171
155
|
case "Stop":
|
|
172
156
|
return { kind: "stop", sessionId: sid, timestamp: ts, transcriptPath: tp };
|
|
173
157
|
case "UserPromptSubmit":
|
|
174
158
|
return e.prompt ? { kind: "prompt", sessionId: sid, timestamp: ts, prompt: e.prompt } : null;
|
|
175
159
|
case "PreToolUse": {
|
|
160
|
+
const tool = e.tool_name;
|
|
176
161
|
const cmd = e.tool_input?.command ?? "";
|
|
177
|
-
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)) {
|
|
178
173
|
return { kind: "pre_commit", sessionId: sid, timestamp: ts, commitCommand: cmd };
|
|
179
174
|
}
|
|
180
175
|
return null;
|
|
@@ -182,7 +177,14 @@ var claudeCode = {
|
|
|
182
177
|
case "PostToolUse": {
|
|
183
178
|
const tool = e.tool_name;
|
|
184
179
|
if ((tool === "Edit" || tool === "Write" || tool === "NotebookEdit") && e.tool_input?.file_path) {
|
|
185
|
-
return {
|
|
180
|
+
return {
|
|
181
|
+
kind: "file_change",
|
|
182
|
+
sessionId: sid,
|
|
183
|
+
timestamp: ts,
|
|
184
|
+
tool,
|
|
185
|
+
file: e.tool_input.file_path,
|
|
186
|
+
toolUseId: e.tool_use_id
|
|
187
|
+
};
|
|
186
188
|
}
|
|
187
189
|
if (tool === "Bash" && isGitCommit(e.tool_input?.command ?? "")) {
|
|
188
190
|
return { kind: "post_commit", sessionId: sid, timestamp: ts, transcriptPath: tp };
|
|
@@ -195,13 +197,13 @@ var claudeCode = {
|
|
|
195
197
|
},
|
|
196
198
|
findTranscript(sessionId) {
|
|
197
199
|
if (!isValidSessionId(sessionId)) return null;
|
|
198
|
-
const claudeDir =
|
|
200
|
+
const claudeDir = join(homedir(), ".claude", "projects");
|
|
199
201
|
if (!existsSync(claudeDir)) return null;
|
|
200
202
|
try {
|
|
201
203
|
for (const project of readdirSync(claudeDir)) {
|
|
202
|
-
const sessionsDir =
|
|
204
|
+
const sessionsDir = join(claudeDir, project, "sessions");
|
|
203
205
|
if (!existsSync(sessionsDir)) continue;
|
|
204
|
-
const candidate =
|
|
206
|
+
const candidate = join(sessionsDir, `${sessionId}.jsonl`);
|
|
205
207
|
if (existsSync(candidate) && isValidTranscriptPath(candidate)) {
|
|
206
208
|
return candidate;
|
|
207
209
|
}
|
|
@@ -253,141 +255,174 @@ var claudeCode = {
|
|
|
253
255
|
}
|
|
254
256
|
};
|
|
255
257
|
|
|
256
|
-
// src/
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
const
|
|
288
|
-
if (
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
258
|
+
// src/git.ts
|
|
259
|
+
import { execFile } from "node:child_process";
|
|
260
|
+
import { promisify } from "node:util";
|
|
261
|
+
var execFileAsync = promisify(execFile);
|
|
262
|
+
async function git(args2, options) {
|
|
263
|
+
const { stdout } = await execFileAsync("git", args2, {
|
|
264
|
+
cwd: options?.cwd,
|
|
265
|
+
encoding: "utf-8"
|
|
266
|
+
});
|
|
267
|
+
return stdout.trim();
|
|
268
|
+
}
|
|
269
|
+
async function gitSafe(args2, options) {
|
|
270
|
+
try {
|
|
271
|
+
const stdout = await git(args2, options);
|
|
272
|
+
return { stdout, exitCode: 0 };
|
|
273
|
+
} catch (err) {
|
|
274
|
+
const e = err;
|
|
275
|
+
return {
|
|
276
|
+
stdout: typeof e.stdout === "string" ? e.stdout.trim() : "",
|
|
277
|
+
exitCode: typeof e.code === "number" ? e.code : 1
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
async function repoRoot() {
|
|
282
|
+
return git(["rev-parse", "--show-toplevel"]);
|
|
283
|
+
}
|
|
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
|
+
});
|
|
293
297
|
}
|
|
294
298
|
}
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
await writeFile2(workflowPath, WORKFLOW_TEMPLATE);
|
|
303
|
-
results.push(" \u2713 workflow created at .github/workflows/agentnote.yml");
|
|
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);
|
|
304
306
|
}
|
|
305
307
|
}
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
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++;
|
|
314
349
|
} else {
|
|
315
|
-
|
|
316
|
-
"config",
|
|
317
|
-
"--add",
|
|
318
|
-
"remote.origin.fetch",
|
|
319
|
-
"+refs/notes/agentnote:refs/notes/agentnote"
|
|
320
|
-
]);
|
|
321
|
-
results.push(" \u2713 git configured to auto-fetch notes on pull");
|
|
350
|
+
humanAddedLines++;
|
|
322
351
|
}
|
|
323
352
|
}
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
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}`);
|
|
329
360
|
}
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
361
|
+
return stdout;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// src/core/entry.ts
|
|
365
|
+
function calcAiRatio(commitFiles, aiFiles, lineCounts) {
|
|
366
|
+
if (lineCounts && lineCounts.totalAddedLines > 0) {
|
|
367
|
+
return Math.round(lineCounts.aiAddedLines / lineCounts.totalAddedLines * 100);
|
|
335
368
|
}
|
|
336
|
-
if (
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
369
|
+
if (commitFiles.length === 0) return 0;
|
|
370
|
+
const aiSet = new Set(aiFiles);
|
|
371
|
+
const matched = commitFiles.filter((f) => aiSet.has(f));
|
|
372
|
+
return Math.round(matched.length / commitFiles.length * 100);
|
|
373
|
+
}
|
|
374
|
+
function buildEntry(opts) {
|
|
375
|
+
const entry = {
|
|
376
|
+
v: SCHEMA_VERSION,
|
|
377
|
+
session_id: opts.sessionId,
|
|
378
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
379
|
+
interactions: opts.interactions.map((i) => {
|
|
380
|
+
const base = { prompt: i.prompt, response: i.response };
|
|
381
|
+
if (i.files_touched && i.files_touched.length > 0) {
|
|
382
|
+
base.files_touched = i.files_touched;
|
|
383
|
+
}
|
|
384
|
+
return base;
|
|
385
|
+
}),
|
|
386
|
+
files_in_commit: opts.commitFiles,
|
|
387
|
+
files_by_ai: opts.aiFiles,
|
|
388
|
+
ai_ratio: calcAiRatio(opts.commitFiles, opts.aiFiles, opts.lineCounts)
|
|
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;
|
|
342
394
|
}
|
|
343
|
-
|
|
395
|
+
return entry;
|
|
344
396
|
}
|
|
345
397
|
|
|
346
|
-
// src/commands/commit.ts
|
|
347
|
-
import { readFile as readFile3 } from "node:fs/promises";
|
|
348
|
-
import { existsSync as existsSync5 } from "node:fs";
|
|
349
|
-
import { spawn } from "node:child_process";
|
|
350
|
-
import { join as join5 } from "node:path";
|
|
351
|
-
|
|
352
398
|
// src/core/jsonl.ts
|
|
353
|
-
import {
|
|
354
|
-
import {
|
|
355
|
-
async function
|
|
356
|
-
if (!
|
|
399
|
+
import { existsSync as existsSync2 } from "node:fs";
|
|
400
|
+
import { appendFile, readFile as readFile2 } from "node:fs/promises";
|
|
401
|
+
async function readJsonlEntries(filePath) {
|
|
402
|
+
if (!existsSync2(filePath)) return [];
|
|
357
403
|
const content = await readFile2(filePath, "utf-8");
|
|
358
|
-
const
|
|
359
|
-
const values = [];
|
|
404
|
+
const entries = [];
|
|
360
405
|
for (const line of content.trim().split("\n")) {
|
|
361
406
|
if (!line) continue;
|
|
362
407
|
try {
|
|
363
|
-
|
|
364
|
-
const val = entry[field];
|
|
365
|
-
if (val && !seen.has(val)) {
|
|
366
|
-
seen.add(val);
|
|
367
|
-
values.push(val);
|
|
368
|
-
}
|
|
408
|
+
entries.push(JSON.parse(line));
|
|
369
409
|
} catch {
|
|
370
410
|
}
|
|
371
411
|
}
|
|
372
|
-
return
|
|
412
|
+
return entries;
|
|
373
413
|
}
|
|
374
414
|
async function appendJsonl(filePath, data) {
|
|
375
|
-
await appendFile(filePath, JSON.stringify(data)
|
|
415
|
+
await appendFile(filePath, `${JSON.stringify(data)}
|
|
416
|
+
`);
|
|
376
417
|
}
|
|
377
418
|
|
|
378
419
|
// src/core/storage.ts
|
|
379
|
-
var NOTES_REF = "agentnote";
|
|
380
420
|
async function writeNote(commitSha, data) {
|
|
381
421
|
const body = JSON.stringify(data, null, 2);
|
|
382
422
|
await gitSafe(["notes", `--ref=${NOTES_REF}`, "add", "-f", "-m", body, commitSha]);
|
|
383
423
|
}
|
|
384
424
|
async function readNote(commitSha) {
|
|
385
|
-
const { stdout, exitCode } = await gitSafe([
|
|
386
|
-
"notes",
|
|
387
|
-
`--ref=${NOTES_REF}`,
|
|
388
|
-
"show",
|
|
389
|
-
commitSha
|
|
390
|
-
]);
|
|
425
|
+
const { stdout, exitCode } = await gitSafe(["notes", `--ref=${NOTES_REF}`, "show", commitSha]);
|
|
391
426
|
if (exitCode !== 0 || !stdout.trim()) return null;
|
|
392
427
|
try {
|
|
393
428
|
return JSON.parse(stdout);
|
|
@@ -396,53 +431,364 @@ async function readNote(commitSha) {
|
|
|
396
431
|
}
|
|
397
432
|
}
|
|
398
433
|
|
|
399
|
-
// src/core/
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
434
|
+
// src/core/record.ts
|
|
435
|
+
async function recordCommitEntry(opts) {
|
|
436
|
+
const sessionDir = join2(opts.agentnoteDirPath, "sessions", opts.sessionId);
|
|
437
|
+
const commitSha = await git(["rev-parse", "HEAD"]);
|
|
438
|
+
let commitFiles = [];
|
|
439
|
+
try {
|
|
440
|
+
const raw = await git(["diff-tree", "--no-commit-id", "--name-only", "-r", "HEAD"]);
|
|
441
|
+
commitFiles = raw.split("\n").filter(Boolean);
|
|
442
|
+
} catch {
|
|
443
|
+
}
|
|
444
|
+
const commitFileSet = new Set(commitFiles);
|
|
445
|
+
const allChangeEntries = await readAllSessionJsonl(sessionDir, CHANGES_FILE);
|
|
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
|
+
);
|
|
464
|
+
const hasTurnData = promptEntries.some((e) => typeof e.turn === "number" && e.turn > 0);
|
|
465
|
+
let aiFiles;
|
|
466
|
+
let prompts;
|
|
467
|
+
let relevantPromptEntries;
|
|
468
|
+
const relevantTurns = /* @__PURE__ */ new Set();
|
|
469
|
+
if (hasTurnData) {
|
|
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];
|
|
480
|
+
for (const entry2 of changeEntries) {
|
|
481
|
+
const file = entry2.file;
|
|
482
|
+
if (file && commitFileSet.has(file)) {
|
|
483
|
+
relevantTurns.add(typeof entry2.turn === "number" ? entry2.turn : 0);
|
|
484
|
+
}
|
|
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
|
+
}
|
|
492
|
+
relevantPromptEntries = promptEntries.filter((e) => {
|
|
493
|
+
const turn = typeof e.turn === "number" ? e.turn : 0;
|
|
494
|
+
return relevantTurns.has(turn);
|
|
495
|
+
});
|
|
496
|
+
prompts = relevantPromptEntries.map((e) => e.prompt);
|
|
497
|
+
} else {
|
|
498
|
+
aiFiles = changeEntries.map((e) => e.file).filter(Boolean);
|
|
499
|
+
prompts = promptEntries.map((e) => e.prompt);
|
|
500
|
+
relevantPromptEntries = promptEntries;
|
|
501
|
+
}
|
|
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
|
+
}
|
|
513
|
+
let interactions;
|
|
514
|
+
if (transcriptPath && prompts.length > 0 && !crossTurnCommit) {
|
|
515
|
+
const allInteractions = await claudeCode.extractInteractions(transcriptPath);
|
|
516
|
+
interactions = allInteractions.length > 0 ? allInteractions.slice(-prompts.length) : prompts.map((p) => ({ prompt: p, response: null }));
|
|
517
|
+
} else {
|
|
518
|
+
interactions = prompts.map((p) => ({ prompt: p, response: null }));
|
|
519
|
+
}
|
|
520
|
+
if (hasTurnData) {
|
|
521
|
+
attachFilesTouched(changeEntries, relevantPromptEntries, interactions, commitFileSet);
|
|
522
|
+
}
|
|
523
|
+
const lineCounts = await computeLineAttribution({
|
|
524
|
+
sessionDir,
|
|
525
|
+
commitFileSet,
|
|
526
|
+
aiFileSet: new Set(aiFiles),
|
|
527
|
+
relevantTurns,
|
|
528
|
+
hasTurnData,
|
|
529
|
+
changeEntries
|
|
530
|
+
});
|
|
531
|
+
const entry = buildEntry({
|
|
532
|
+
sessionId: opts.sessionId,
|
|
533
|
+
interactions,
|
|
534
|
+
commitFiles,
|
|
535
|
+
aiFiles,
|
|
536
|
+
lineCounts: lineCounts ?? void 0
|
|
537
|
+
});
|
|
538
|
+
await writeNote(commitSha, entry);
|
|
539
|
+
await recordConsumedPairs(sessionDir, changeEntries, commitFileSet);
|
|
540
|
+
return { promptCount: interactions.length, aiRatio: entry.ai_ratio };
|
|
406
541
|
}
|
|
407
|
-
function
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
542
|
+
function attachFilesTouched(changeEntries, promptEntries, interactions, commitFileSet) {
|
|
543
|
+
const filesByTurn = /* @__PURE__ */ new Map();
|
|
544
|
+
for (const entry of changeEntries) {
|
|
545
|
+
const turn = typeof entry.turn === "number" ? entry.turn : 0;
|
|
546
|
+
const file = entry.file;
|
|
547
|
+
if (!file || !commitFileSet.has(file)) continue;
|
|
548
|
+
if (!filesByTurn.has(turn)) filesByTurn.set(turn, /* @__PURE__ */ new Set());
|
|
549
|
+
filesByTurn.get(turn)?.add(file);
|
|
550
|
+
}
|
|
551
|
+
for (let i = 0; i < interactions.length; i++) {
|
|
552
|
+
const promptEntry = promptEntries[i];
|
|
553
|
+
if (!promptEntry) continue;
|
|
554
|
+
const turn = typeof promptEntry.turn === "number" ? promptEntry.turn : 0;
|
|
555
|
+
const files = filesByTurn.get(turn);
|
|
556
|
+
if (files && files.size > 0) {
|
|
557
|
+
interactions[i].files_touched = [...files];
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
async function readAllSessionJsonl(sessionDir, baseFile) {
|
|
562
|
+
const stem = baseFile.slice(0, baseFile.lastIndexOf(".jsonl"));
|
|
563
|
+
const files = await readdir(sessionDir).catch(() => []);
|
|
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));
|
|
575
|
+
const all = [];
|
|
576
|
+
for (const file of matching) {
|
|
577
|
+
const entries = await readJsonlEntries(file);
|
|
578
|
+
all.push(...entries);
|
|
579
|
+
}
|
|
580
|
+
return all;
|
|
581
|
+
}
|
|
582
|
+
async function readSavedTranscriptPath(sessionDir) {
|
|
583
|
+
const saved = join2(sessionDir, TRANSCRIPT_PATH_FILE);
|
|
584
|
+
if (!existsSync3(saved)) return null;
|
|
585
|
+
const p = (await readFile3(saved, "utf-8")).trim();
|
|
586
|
+
return p || null;
|
|
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
|
+
}
|
|
417
749
|
}
|
|
418
750
|
|
|
419
|
-
// src/
|
|
420
|
-
import {
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
async function
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
join4(sessionDir, `${base}-${commitSha.slice(0, 8)}.jsonl`)
|
|
431
|
-
);
|
|
751
|
+
// src/paths.ts
|
|
752
|
+
import { join as join3 } from "node:path";
|
|
753
|
+
var _root = null;
|
|
754
|
+
var _gitDir = null;
|
|
755
|
+
async function root() {
|
|
756
|
+
if (!_root) {
|
|
757
|
+
try {
|
|
758
|
+
_root = await repoRoot();
|
|
759
|
+
} catch {
|
|
760
|
+
console.error("error: git repository not found");
|
|
761
|
+
process.exit(1);
|
|
432
762
|
}
|
|
433
763
|
}
|
|
764
|
+
return _root;
|
|
765
|
+
}
|
|
766
|
+
async function gitDir() {
|
|
767
|
+
if (!_gitDir) {
|
|
768
|
+
_gitDir = await git(["rev-parse", "--git-dir"]);
|
|
769
|
+
if (!_gitDir.startsWith("/")) {
|
|
770
|
+
_gitDir = join3(await root(), _gitDir);
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
return _gitDir;
|
|
774
|
+
}
|
|
775
|
+
async function agentnoteDir() {
|
|
776
|
+
return join3(await gitDir(), AGENTNOTE_DIR);
|
|
777
|
+
}
|
|
778
|
+
async function sessionFile() {
|
|
779
|
+
return join3(await agentnoteDir(), SESSION_FILE);
|
|
434
780
|
}
|
|
435
781
|
|
|
436
782
|
// src/commands/commit.ts
|
|
437
783
|
async function commit(args2) {
|
|
438
784
|
const sf = await sessionFile();
|
|
439
785
|
let sessionId = "";
|
|
440
|
-
if (
|
|
441
|
-
sessionId = (await
|
|
786
|
+
if (existsSync4(sf)) {
|
|
787
|
+
sessionId = (await readFile4(sf, "utf-8")).trim();
|
|
442
788
|
}
|
|
443
789
|
const gitArgs = ["commit"];
|
|
444
790
|
if (sessionId) {
|
|
445
|
-
gitArgs.push("--trailer",
|
|
791
|
+
gitArgs.push("--trailer", `${TRAILER_KEY}: ${sessionId}`);
|
|
446
792
|
}
|
|
447
793
|
gitArgs.push(...args2);
|
|
448
794
|
const child = spawn("git", gitArgs, {
|
|
@@ -458,210 +804,58 @@ async function commit(args2) {
|
|
|
458
804
|
if (sessionId) {
|
|
459
805
|
try {
|
|
460
806
|
const agentnoteDirPath = await agentnoteDir();
|
|
461
|
-
const
|
|
462
|
-
|
|
463
|
-
let commitFiles = [];
|
|
464
|
-
try {
|
|
465
|
-
const raw = await git([
|
|
466
|
-
"diff-tree",
|
|
467
|
-
"--no-commit-id",
|
|
468
|
-
"--name-only",
|
|
469
|
-
"-r",
|
|
470
|
-
"HEAD"
|
|
471
|
-
]);
|
|
472
|
-
commitFiles = raw.split("\n").filter(Boolean);
|
|
473
|
-
} catch {
|
|
474
|
-
}
|
|
475
|
-
const aiFiles = await readJsonlField(
|
|
476
|
-
join5(sessionDir, "changes.jsonl"),
|
|
477
|
-
"file"
|
|
478
|
-
);
|
|
479
|
-
const prompts = await readJsonlField(
|
|
480
|
-
join5(sessionDir, "prompts.jsonl"),
|
|
481
|
-
"prompt"
|
|
482
|
-
);
|
|
483
|
-
let interactions;
|
|
484
|
-
const transcriptPathFile = join5(sessionDir, "transcript_path");
|
|
485
|
-
if (existsSync5(transcriptPathFile)) {
|
|
486
|
-
const transcriptPath = (await readFile3(transcriptPathFile, "utf-8")).trim();
|
|
487
|
-
if (transcriptPath) {
|
|
488
|
-
const allInteractions = await claudeCode.extractInteractions(transcriptPath);
|
|
489
|
-
interactions = prompts.length > 0 && allInteractions.length > 0 ? allInteractions.slice(-prompts.length) : prompts.map((p) => ({ prompt: p, response: null }));
|
|
490
|
-
} else {
|
|
491
|
-
interactions = prompts.map((p) => ({ prompt: p, response: null }));
|
|
492
|
-
}
|
|
493
|
-
} else {
|
|
494
|
-
interactions = prompts.map((p) => ({ prompt: p, response: null }));
|
|
495
|
-
}
|
|
496
|
-
const entry = buildEntry({
|
|
497
|
-
sessionId,
|
|
498
|
-
interactions,
|
|
499
|
-
commitFiles,
|
|
500
|
-
aiFiles
|
|
501
|
-
});
|
|
502
|
-
await writeNote(commitSha, entry);
|
|
503
|
-
await rotateLogs(sessionDir, commitSha);
|
|
504
|
-
console.log(
|
|
505
|
-
`agentnote: ${interactions.length} prompts, AI ratio ${entry.ai_ratio}%`
|
|
506
|
-
);
|
|
807
|
+
const result = await recordCommitEntry({ agentnoteDirPath, sessionId });
|
|
808
|
+
console.log(`agentnote: ${result.promptCount} prompts, AI ratio ${result.aiRatio}%`);
|
|
507
809
|
} catch (err) {
|
|
508
810
|
console.error(`agentnote: warning: ${err.message}`);
|
|
509
811
|
}
|
|
510
812
|
}
|
|
511
813
|
}
|
|
512
814
|
|
|
513
|
-
// src/commands/
|
|
514
|
-
import {
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
return;
|
|
529
|
-
}
|
|
530
|
-
console.log(`session: ${sessionId}`);
|
|
531
|
-
const raw = await readNote(commitSha);
|
|
532
|
-
const entry = raw;
|
|
533
|
-
if (entry) {
|
|
534
|
-
console.log();
|
|
535
|
-
const ratioBar = renderRatioBar(entry.ai_ratio);
|
|
536
|
-
console.log(`ai: ${entry.ai_ratio}% ${ratioBar}`);
|
|
537
|
-
console.log(
|
|
538
|
-
`files: ${entry.files_in_commit.length} changed, ${entry.files_by_ai.length} by AI`
|
|
539
|
-
);
|
|
540
|
-
if (entry.files_in_commit.length > 0) {
|
|
541
|
-
console.log();
|
|
542
|
-
for (const file of entry.files_in_commit) {
|
|
543
|
-
const isAi = entry.files_by_ai.includes(file);
|
|
544
|
-
const marker = isAi ? " \u{1F916}" : " \u{1F464}";
|
|
545
|
-
console.log(` ${file}${marker}`);
|
|
546
|
-
}
|
|
547
|
-
}
|
|
548
|
-
const interactions = entry.interactions ?? (entry.prompts ?? []).map((p) => ({
|
|
549
|
-
prompt: p,
|
|
550
|
-
response: null
|
|
551
|
-
}));
|
|
552
|
-
if (interactions.length > 0) {
|
|
553
|
-
console.log();
|
|
554
|
-
console.log(`prompts: ${interactions.length}`);
|
|
555
|
-
for (let i = 0; i < interactions.length; i++) {
|
|
556
|
-
const { prompt, response } = interactions[i];
|
|
557
|
-
console.log();
|
|
558
|
-
console.log(` ${i + 1}. ${truncateLines(prompt, 120)}`);
|
|
559
|
-
if (response) {
|
|
560
|
-
console.log(` \u2192 ${truncateLines(response, 200)}`);
|
|
561
|
-
}
|
|
562
|
-
}
|
|
815
|
+
// src/commands/hook.ts
|
|
816
|
+
import { existsSync as existsSync6 } from "node:fs";
|
|
817
|
+
import { mkdir as mkdir2, readFile as readFile5, realpath, writeFile as writeFile3 } from "node:fs/promises";
|
|
818
|
+
import { isAbsolute, join as join5, relative } from "node:path";
|
|
819
|
+
|
|
820
|
+
// src/core/rotate.ts
|
|
821
|
+
import { existsSync as existsSync5 } from "node:fs";
|
|
822
|
+
import { rename } from "node:fs/promises";
|
|
823
|
+
import { join as join4 } from "node:path";
|
|
824
|
+
async function rotateLogs(sessionDir, rotateId, fileNames = [PROMPTS_FILE, CHANGES_FILE]) {
|
|
825
|
+
for (const name of fileNames) {
|
|
826
|
+
const src = join4(sessionDir, name);
|
|
827
|
+
if (existsSync5(src)) {
|
|
828
|
+
const base = name.replace(".jsonl", "");
|
|
829
|
+
await rename(src, join4(sessionDir, `${base}-${rotateId}.jsonl`));
|
|
563
830
|
}
|
|
564
|
-
} else {
|
|
565
|
-
console.log("entry: no agentnote note found for this commit");
|
|
566
|
-
}
|
|
567
|
-
console.log();
|
|
568
|
-
const adapter = claudeCode;
|
|
569
|
-
const transcriptPath = adapter.findTranscript(sessionId);
|
|
570
|
-
if (transcriptPath) {
|
|
571
|
-
const stats = await stat(transcriptPath);
|
|
572
|
-
const sizeKb = (stats.size / 1024).toFixed(1);
|
|
573
|
-
console.log(`transcript: ${transcriptPath} (${sizeKb} KB)`);
|
|
574
|
-
} else {
|
|
575
|
-
console.log("transcript: not found locally");
|
|
576
831
|
}
|
|
577
832
|
}
|
|
578
|
-
function renderRatioBar(ratio) {
|
|
579
|
-
const width = 20;
|
|
580
|
-
const filled = Math.round(ratio / 100 * width);
|
|
581
|
-
const empty = width - filled;
|
|
582
|
-
return `[${"\u2588".repeat(filled)}${"\u2591".repeat(empty)}]`;
|
|
583
|
-
}
|
|
584
|
-
function truncateLines(text, maxLen) {
|
|
585
|
-
const firstLine = text.split("\n")[0];
|
|
586
|
-
if (firstLine.length <= maxLen) return firstLine;
|
|
587
|
-
return firstLine.slice(0, maxLen) + "\u2026";
|
|
588
|
-
}
|
|
589
833
|
|
|
590
|
-
// src/commands/
|
|
591
|
-
async function
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
for (const line of raw.split("\n")) {
|
|
602
|
-
if (!line.trim()) continue;
|
|
603
|
-
const parts = line.split(" ");
|
|
604
|
-
const fullSha = parts[0];
|
|
605
|
-
const commitPart = parts[1];
|
|
606
|
-
const sid = parts[2]?.trim();
|
|
607
|
-
if (!fullSha || !commitPart) continue;
|
|
608
|
-
if (!sid) {
|
|
609
|
-
console.log(commitPart);
|
|
610
|
-
continue;
|
|
611
|
-
}
|
|
612
|
-
let ratioStr = "";
|
|
613
|
-
let promptCount = "";
|
|
614
|
-
const note = await readNote(fullSha);
|
|
615
|
-
if (note) {
|
|
616
|
-
const entry = note;
|
|
617
|
-
ratioStr = `${entry.ai_ratio}%`;
|
|
618
|
-
promptCount = `${entry.interactions?.length ?? entry.prompts?.length ?? 0}p`;
|
|
619
|
-
}
|
|
620
|
-
if (ratioStr) {
|
|
621
|
-
console.log(
|
|
622
|
-
`${commitPart} [${sid.slice(0, 8)}\u2026 | \u{1F916}${ratioStr} | ${promptCount}]`
|
|
623
|
-
);
|
|
624
|
-
} else {
|
|
625
|
-
console.log(`${commitPart} [${sid.slice(0, 8)}\u2026]`);
|
|
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/, "");
|
|
626
845
|
}
|
|
846
|
+
return relative(repoRoot2, normalized);
|
|
847
|
+
} catch {
|
|
848
|
+
return filePath;
|
|
627
849
|
}
|
|
628
850
|
}
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
console.log(`agentnote v${VERSION}`);
|
|
636
|
-
console.log();
|
|
637
|
-
const repoRoot2 = await root();
|
|
638
|
-
const adapter = claudeCode;
|
|
639
|
-
const hooksActive = await adapter.isEnabled(repoRoot2);
|
|
640
|
-
if (hooksActive) {
|
|
641
|
-
console.log("hooks: active");
|
|
642
|
-
} else {
|
|
643
|
-
console.log("hooks: not configured (run 'agentnote init')");
|
|
644
|
-
}
|
|
645
|
-
const sessionPath = await sessionFile();
|
|
646
|
-
if (existsSync6(sessionPath)) {
|
|
647
|
-
const sid = (await readFile4(sessionPath, "utf-8")).trim();
|
|
648
|
-
console.log(`session: ${sid.slice(0, 8)}\u2026`);
|
|
649
|
-
} else {
|
|
650
|
-
console.log("session: none");
|
|
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;
|
|
651
857
|
}
|
|
652
|
-
const { stdout } = await gitSafe([
|
|
653
|
-
"log",
|
|
654
|
-
"-20",
|
|
655
|
-
"--format=%(trailers:key=Agentnote-Session,valueonly)"
|
|
656
|
-
]);
|
|
657
|
-
const linked = stdout.split("\n").filter((line) => line.trim().length > 0).length;
|
|
658
|
-
console.log(`linked: ${linked}/20 recent commits`);
|
|
659
858
|
}
|
|
660
|
-
|
|
661
|
-
// src/commands/hook.ts
|
|
662
|
-
import { mkdir as mkdir3, readFile as readFile5, writeFile as writeFile3, realpath } from "node:fs/promises";
|
|
663
|
-
import { existsSync as existsSync7 } from "node:fs";
|
|
664
|
-
import { join as join6, relative, isAbsolute } from "node:path";
|
|
665
859
|
async function readStdin() {
|
|
666
860
|
const chunks = [];
|
|
667
861
|
for await (const chunk of process.stdin) {
|
|
@@ -683,28 +877,29 @@ async function hook() {
|
|
|
683
877
|
const event = adapter.parseEvent(input);
|
|
684
878
|
if (!event) return;
|
|
685
879
|
const agentnoteDirPath = await agentnoteDir();
|
|
686
|
-
const sessionDir =
|
|
687
|
-
await
|
|
880
|
+
const sessionDir = join5(agentnoteDirPath, SESSIONS_DIR, event.sessionId);
|
|
881
|
+
await mkdir2(sessionDir, { recursive: true });
|
|
688
882
|
switch (event.kind) {
|
|
689
883
|
case "session_start": {
|
|
690
|
-
await writeFile3(
|
|
884
|
+
await writeFile3(join5(agentnoteDirPath, SESSION_FILE), event.sessionId);
|
|
691
885
|
if (event.transcriptPath) {
|
|
692
|
-
await writeFile3(
|
|
886
|
+
await writeFile3(join5(sessionDir, TRANSCRIPT_PATH_FILE), event.transcriptPath);
|
|
693
887
|
}
|
|
694
|
-
await appendJsonl(
|
|
888
|
+
await appendJsonl(join5(sessionDir, EVENTS_FILE), {
|
|
695
889
|
event: "session_start",
|
|
696
890
|
session_id: event.sessionId,
|
|
697
891
|
timestamp: event.timestamp,
|
|
698
892
|
model: event.model ?? null
|
|
699
893
|
});
|
|
894
|
+
await writeFile3(join5(sessionDir, HEARTBEAT_FILE), String(Date.now()));
|
|
700
895
|
break;
|
|
701
896
|
}
|
|
702
897
|
case "stop": {
|
|
703
|
-
await writeFile3(
|
|
898
|
+
await writeFile3(join5(agentnoteDirPath, SESSION_FILE), event.sessionId);
|
|
704
899
|
if (event.transcriptPath) {
|
|
705
|
-
await writeFile3(
|
|
900
|
+
await writeFile3(join5(sessionDir, TRANSCRIPT_PATH_FILE), event.transcriptPath);
|
|
706
901
|
}
|
|
707
|
-
await appendJsonl(
|
|
902
|
+
await appendJsonl(join5(sessionDir, EVENTS_FILE), {
|
|
708
903
|
event: "stop",
|
|
709
904
|
session_id: event.sessionId,
|
|
710
905
|
timestamp: event.timestamp
|
|
@@ -712,47 +907,79 @@ async function hook() {
|
|
|
712
907
|
break;
|
|
713
908
|
}
|
|
714
909
|
case "prompt": {
|
|
715
|
-
|
|
910
|
+
const rotateId = Date.now().toString(36);
|
|
911
|
+
await rotateLogs(sessionDir, rotateId, [PROMPTS_FILE, CHANGES_FILE, PRE_BLOBS_FILE]);
|
|
912
|
+
const turnPath = join5(sessionDir, TURN_FILE);
|
|
913
|
+
let turn = 0;
|
|
914
|
+
if (existsSync6(turnPath)) {
|
|
915
|
+
const raw2 = (await readFile5(turnPath, "utf-8")).trim();
|
|
916
|
+
turn = Number.parseInt(raw2, 10) || 0;
|
|
917
|
+
}
|
|
918
|
+
turn += 1;
|
|
919
|
+
await writeFile3(turnPath, String(turn));
|
|
920
|
+
await appendJsonl(join5(sessionDir, PROMPTS_FILE), {
|
|
716
921
|
event: "prompt",
|
|
717
922
|
timestamp: event.timestamp,
|
|
718
|
-
prompt: event.prompt
|
|
923
|
+
prompt: event.prompt,
|
|
924
|
+
turn
|
|
719
925
|
});
|
|
926
|
+
await writeFile3(join5(sessionDir, HEARTBEAT_FILE), String(Date.now()));
|
|
720
927
|
break;
|
|
721
928
|
}
|
|
722
|
-
case "
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
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;
|
|
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);
|
|
953
|
+
let turn = 0;
|
|
954
|
+
const turnPath = join5(sessionDir, TURN_FILE);
|
|
955
|
+
if (existsSync6(turnPath)) {
|
|
956
|
+
const raw2 = (await readFile5(turnPath, "utf-8")).trim();
|
|
957
|
+
turn = Number.parseInt(raw2, 10) || 0;
|
|
737
958
|
}
|
|
738
|
-
await
|
|
959
|
+
const postBlob = isAbsolute(absPath) ? await blobHash(absPath) : EMPTY_BLOB;
|
|
960
|
+
await appendJsonl(join5(sessionDir, CHANGES_FILE), {
|
|
739
961
|
event: "file_change",
|
|
740
962
|
timestamp: event.timestamp,
|
|
741
963
|
tool: event.tool,
|
|
742
964
|
file: filePath,
|
|
743
|
-
session_id: event.sessionId
|
|
965
|
+
session_id: event.sessionId,
|
|
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
|
|
744
971
|
});
|
|
745
972
|
break;
|
|
746
973
|
}
|
|
747
974
|
case "pre_commit": {
|
|
748
975
|
const cmd = event.commitCommand ?? "";
|
|
749
|
-
if (!cmd.includes(
|
|
976
|
+
if (!cmd.includes(TRAILER_KEY) && event.sessionId) {
|
|
750
977
|
process.stdout.write(
|
|
751
978
|
JSON.stringify({
|
|
752
979
|
hookSpecificOutput: {
|
|
753
980
|
hookEventName: "PreToolUse",
|
|
754
981
|
updatedInput: {
|
|
755
|
-
command: `${cmd} --trailer '
|
|
982
|
+
command: `${cmd} --trailer '${TRAILER_KEY}: ${event.sessionId}'`
|
|
756
983
|
}
|
|
757
984
|
}
|
|
758
985
|
})
|
|
@@ -762,47 +989,138 @@ async function hook() {
|
|
|
762
989
|
}
|
|
763
990
|
case "post_commit": {
|
|
764
991
|
try {
|
|
765
|
-
await
|
|
992
|
+
await recordCommitEntry({
|
|
993
|
+
agentnoteDirPath,
|
|
994
|
+
sessionId: event.sessionId,
|
|
995
|
+
transcriptPath: event.transcriptPath
|
|
996
|
+
});
|
|
766
997
|
} catch {
|
|
767
998
|
}
|
|
768
999
|
break;
|
|
769
1000
|
}
|
|
770
1001
|
}
|
|
771
1002
|
}
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
1003
|
+
|
|
1004
|
+
// src/commands/init.ts
|
|
1005
|
+
import { existsSync as existsSync7 } from "node:fs";
|
|
1006
|
+
import { mkdir as mkdir3, writeFile as writeFile4 } from "node:fs/promises";
|
|
1007
|
+
import { join as join6 } from "node:path";
|
|
1008
|
+
var WORKFLOW_TEMPLATE = `name: Agent Note
|
|
1009
|
+
on:
|
|
1010
|
+
pull_request:
|
|
1011
|
+
types: [opened, synchronize]
|
|
1012
|
+
concurrency:
|
|
1013
|
+
group: agentnote-\${{ github.event.pull_request.number }}
|
|
1014
|
+
cancel-in-progress: true
|
|
1015
|
+
permissions:
|
|
1016
|
+
contents: read
|
|
1017
|
+
pull-requests: write
|
|
1018
|
+
jobs:
|
|
1019
|
+
report:
|
|
1020
|
+
runs-on: ubuntu-latest
|
|
1021
|
+
timeout-minutes: 5
|
|
1022
|
+
steps:
|
|
1023
|
+
- uses: actions/checkout@v4
|
|
1024
|
+
with:
|
|
1025
|
+
fetch-depth: 0
|
|
1026
|
+
- uses: wasabeef/agentnote@v0
|
|
1027
|
+
`;
|
|
1028
|
+
async function init(args2) {
|
|
1029
|
+
const skipHooks = args2.includes("--no-hooks");
|
|
1030
|
+
const skipAction = args2.includes("--no-action");
|
|
1031
|
+
const skipNotes = args2.includes("--no-notes");
|
|
1032
|
+
const hooksOnly = args2.includes("--hooks");
|
|
1033
|
+
const actionOnly = args2.includes("--action");
|
|
1034
|
+
const repoRoot2 = await root();
|
|
1035
|
+
const results = [];
|
|
1036
|
+
await mkdir3(await agentnoteDir(), { recursive: true });
|
|
1037
|
+
if (!skipHooks && !actionOnly) {
|
|
1038
|
+
const adapter = claudeCode;
|
|
1039
|
+
if (await adapter.isEnabled(repoRoot2)) {
|
|
1040
|
+
results.push(" \xB7 hooks already configured");
|
|
1041
|
+
} else {
|
|
1042
|
+
await adapter.installHooks(repoRoot2);
|
|
1043
|
+
results.push(" \u2713 hooks added to .claude/settings.json");
|
|
1044
|
+
}
|
|
780
1045
|
}
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
1046
|
+
if (!skipAction && !hooksOnly) {
|
|
1047
|
+
const workflowDir = join6(repoRoot2, ".github", "workflows");
|
|
1048
|
+
const workflowPath = join6(workflowDir, "agentnote.yml");
|
|
1049
|
+
if (existsSync7(workflowPath)) {
|
|
1050
|
+
results.push(" \xB7 workflow already exists at .github/workflows/agentnote.yml");
|
|
1051
|
+
} else {
|
|
1052
|
+
await mkdir3(workflowDir, { recursive: true });
|
|
1053
|
+
await writeFile4(workflowPath, WORKFLOW_TEMPLATE);
|
|
1054
|
+
results.push(" \u2713 workflow created at .github/workflows/agentnote.yml");
|
|
1055
|
+
}
|
|
791
1056
|
}
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
1057
|
+
if (!skipNotes && !hooksOnly && !actionOnly) {
|
|
1058
|
+
const { stdout } = await gitSafe(["config", "--get-all", "remote.origin.fetch"]);
|
|
1059
|
+
if (stdout.includes(NOTES_REF_FULL)) {
|
|
1060
|
+
results.push(" \xB7 git already configured to fetch notes");
|
|
1061
|
+
} else {
|
|
1062
|
+
await gitSafe(["config", "--add", "remote.origin.fetch", NOTES_FETCH_REFSPEC]);
|
|
1063
|
+
results.push(" \u2713 git configured to auto-fetch notes on pull");
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
console.log("");
|
|
1067
|
+
console.log("agentnote init");
|
|
1068
|
+
console.log("");
|
|
1069
|
+
for (const line of results) {
|
|
1070
|
+
console.log(line);
|
|
1071
|
+
}
|
|
1072
|
+
const toCommit = [];
|
|
1073
|
+
if (!skipHooks && !actionOnly) toCommit.push(".claude/settings.json");
|
|
1074
|
+
if (!skipAction && !hooksOnly) {
|
|
1075
|
+
const workflowPath = join6(repoRoot2, ".github", "workflows", "agentnote.yml");
|
|
1076
|
+
if (existsSync7(workflowPath)) toCommit.push(".github/workflows/agentnote.yml");
|
|
1077
|
+
}
|
|
1078
|
+
if (toCommit.length > 0) {
|
|
1079
|
+
console.log("");
|
|
1080
|
+
console.log(" Next: commit and push these files");
|
|
1081
|
+
console.log(` git add ${toCommit.join(" ")}`);
|
|
1082
|
+
console.log(' git commit -m "chore: enable agentnote session tracking"');
|
|
1083
|
+
console.log(" git push");
|
|
1084
|
+
}
|
|
1085
|
+
console.log("");
|
|
800
1086
|
}
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
const
|
|
805
|
-
|
|
1087
|
+
|
|
1088
|
+
// src/commands/log.ts
|
|
1089
|
+
async function log(count = 10) {
|
|
1090
|
+
const raw = await git([
|
|
1091
|
+
"log",
|
|
1092
|
+
`-${count}`,
|
|
1093
|
+
`--format=%H %h %s %(trailers:key=${TRAILER_KEY},valueonly)`
|
|
1094
|
+
]);
|
|
1095
|
+
if (!raw) {
|
|
1096
|
+
console.log("no commits found");
|
|
1097
|
+
return;
|
|
1098
|
+
}
|
|
1099
|
+
for (const line of raw.split("\n")) {
|
|
1100
|
+
if (!line.trim()) continue;
|
|
1101
|
+
const parts = line.split(" ");
|
|
1102
|
+
const fullSha = parts[0];
|
|
1103
|
+
const commitPart = parts[1];
|
|
1104
|
+
const sid = parts[2]?.trim();
|
|
1105
|
+
if (!fullSha || !commitPart) continue;
|
|
1106
|
+
if (!sid) {
|
|
1107
|
+
console.log(commitPart);
|
|
1108
|
+
continue;
|
|
1109
|
+
}
|
|
1110
|
+
let ratioStr = "";
|
|
1111
|
+
let promptCount = "";
|
|
1112
|
+
const note = await readNote(fullSha);
|
|
1113
|
+
if (note) {
|
|
1114
|
+
const entry = note;
|
|
1115
|
+
ratioStr = `${entry.ai_ratio}%`;
|
|
1116
|
+
promptCount = `${entry.interactions?.length ?? entry.prompts?.length ?? 0}p`;
|
|
1117
|
+
}
|
|
1118
|
+
if (ratioStr) {
|
|
1119
|
+
console.log(`${commitPart} [${sid.slice(0, 8)}\u2026 | \u{1F916}${ratioStr} | ${promptCount}]`);
|
|
1120
|
+
} else {
|
|
1121
|
+
console.log(`${commitPart} [${sid.slice(0, 8)}\u2026]`);
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
806
1124
|
}
|
|
807
1125
|
|
|
808
1126
|
// src/commands/pr.ts
|
|
@@ -813,12 +1131,7 @@ var MARKER_BEGIN = "<!-- agentnote-begin -->";
|
|
|
813
1131
|
var MARKER_END = "<!-- agentnote-end -->";
|
|
814
1132
|
async function collectReport(base) {
|
|
815
1133
|
const head = await git(["rev-parse", "--short", "HEAD"]);
|
|
816
|
-
const raw = await git([
|
|
817
|
-
"log",
|
|
818
|
-
"--reverse",
|
|
819
|
-
"--format=%H %h %s",
|
|
820
|
-
`${base}..HEAD`
|
|
821
|
-
]);
|
|
1134
|
+
const raw = await git(["log", "--reverse", "--format=%H %h %s", `${base}..HEAD`]);
|
|
822
1135
|
if (!raw.trim()) return null;
|
|
823
1136
|
const commits = [];
|
|
824
1137
|
for (const line of raw.trim().split("\n")) {
|
|
@@ -836,7 +1149,9 @@ async function collectReport(base) {
|
|
|
836
1149
|
files_total: 0,
|
|
837
1150
|
files_ai: 0,
|
|
838
1151
|
files: [],
|
|
839
|
-
interactions: []
|
|
1152
|
+
interactions: [],
|
|
1153
|
+
ai_added_lines: null,
|
|
1154
|
+
total_added_lines: null
|
|
840
1155
|
});
|
|
841
1156
|
continue;
|
|
842
1157
|
}
|
|
@@ -857,12 +1172,23 @@ async function collectReport(base) {
|
|
|
857
1172
|
path: f,
|
|
858
1173
|
by_ai: filesByAi.includes(f)
|
|
859
1174
|
})),
|
|
860
|
-
interactions
|
|
1175
|
+
interactions,
|
|
1176
|
+
ai_added_lines: entry.ai_added_lines ?? null,
|
|
1177
|
+
total_added_lines: entry.total_added_lines ?? null
|
|
861
1178
|
});
|
|
862
1179
|
}
|
|
863
1180
|
const tracked = commits.filter((c) => c.session_id !== null);
|
|
864
1181
|
const totalFiles = tracked.reduce((s, c) => s + c.files_total, 0);
|
|
865
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
|
+
}
|
|
866
1192
|
return {
|
|
867
1193
|
base,
|
|
868
1194
|
head,
|
|
@@ -871,7 +1197,7 @@ async function collectReport(base) {
|
|
|
871
1197
|
total_prompts: tracked.reduce((s, c) => s + c.prompts_count, 0),
|
|
872
1198
|
total_files: totalFiles,
|
|
873
1199
|
total_files_ai: totalFilesAi,
|
|
874
|
-
overall_ai_ratio:
|
|
1200
|
+
overall_ai_ratio: overallAiRatio,
|
|
875
1201
|
commits
|
|
876
1202
|
};
|
|
877
1203
|
}
|
|
@@ -908,7 +1234,7 @@ function renderMarkdown(report) {
|
|
|
908
1234
|
for (const { prompt, response } of c.interactions) {
|
|
909
1235
|
lines.push(`> **Prompt:** ${prompt}`);
|
|
910
1236
|
if (response) {
|
|
911
|
-
const truncated = response.length >
|
|
1237
|
+
const truncated = response.length > TRUNCATE_RESPONSE_PR ? `${response.slice(0, TRUNCATE_RESPONSE_PR)}\u2026` : response;
|
|
912
1238
|
lines.push(">");
|
|
913
1239
|
lines.push(`> **Response:** ${truncated.split("\n").join("\n> ")}`);
|
|
914
1240
|
}
|
|
@@ -944,7 +1270,9 @@ function renderChat(report) {
|
|
|
944
1270
|
continue;
|
|
945
1271
|
}
|
|
946
1272
|
lines.push(`<details>`);
|
|
947
|
-
lines.push(
|
|
1273
|
+
lines.push(
|
|
1274
|
+
`<summary><code>${c.short}</code> ${c.message}${summaryExtra}${summaryFiles}</summary>`
|
|
1275
|
+
);
|
|
948
1276
|
lines.push("");
|
|
949
1277
|
for (const { prompt, response } of c.interactions) {
|
|
950
1278
|
lines.push(`> **\u{1F9D1} Prompt**`);
|
|
@@ -953,7 +1281,7 @@ function renderChat(report) {
|
|
|
953
1281
|
if (response) {
|
|
954
1282
|
lines.push(`**\u{1F916} Response**`);
|
|
955
1283
|
lines.push("");
|
|
956
|
-
const truncated = response.length >
|
|
1284
|
+
const truncated = response.length > TRUNCATE_RESPONSE_CHAT ? `${response.slice(0, TRUNCATE_RESPONSE_CHAT)}\u2026` : response;
|
|
957
1285
|
lines.push(truncated);
|
|
958
1286
|
lines.push("");
|
|
959
1287
|
}
|
|
@@ -1007,27 +1335,23 @@ function upsertInDescription(existingBody, section) {
|
|
|
1007
1335
|
if (existingBody.includes(MARKER_BEGIN)) {
|
|
1008
1336
|
const before = existingBody.slice(0, existingBody.indexOf(MARKER_BEGIN));
|
|
1009
1337
|
const after = existingBody.includes(MARKER_END) ? existingBody.slice(existingBody.indexOf(MARKER_END) + MARKER_END.length) : "";
|
|
1010
|
-
return before.trimEnd()
|
|
1338
|
+
return `${before.trimEnd()}
|
|
1339
|
+
|
|
1340
|
+
${marked}${after}`;
|
|
1011
1341
|
}
|
|
1012
|
-
return existingBody.trimEnd()
|
|
1342
|
+
return `${existingBody.trimEnd()}
|
|
1343
|
+
|
|
1344
|
+
${marked}`;
|
|
1013
1345
|
}
|
|
1014
1346
|
async function updatePrDescription(prNumber, section) {
|
|
1015
|
-
const { stdout: bodyJson } = await execFileAsync2(
|
|
1016
|
-
"
|
|
1017
|
-
"view",
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
"body"
|
|
1021
|
-
], { encoding: "utf-8" });
|
|
1347
|
+
const { stdout: bodyJson } = await execFileAsync2(
|
|
1348
|
+
"gh",
|
|
1349
|
+
["pr", "view", prNumber, "--json", "body"],
|
|
1350
|
+
{ encoding: "utf-8" }
|
|
1351
|
+
);
|
|
1022
1352
|
const currentBody = JSON.parse(bodyJson).body ?? "";
|
|
1023
1353
|
const newBody = upsertInDescription(currentBody, section);
|
|
1024
|
-
await execFileAsync2("gh", [
|
|
1025
|
-
"pr",
|
|
1026
|
-
"edit",
|
|
1027
|
-
prNumber,
|
|
1028
|
-
"--body",
|
|
1029
|
-
newBody
|
|
1030
|
-
], { encoding: "utf-8" });
|
|
1354
|
+
await execFileAsync2("gh", ["pr", "edit", prNumber, "--body", newBody], { encoding: "utf-8" });
|
|
1031
1355
|
}
|
|
1032
1356
|
async function pr(args2) {
|
|
1033
1357
|
const isJson = args2.includes("--json");
|
|
@@ -1040,9 +1364,7 @@ async function pr(args2) {
|
|
|
1040
1364
|
);
|
|
1041
1365
|
const base = positional[0] ?? await detectBaseBranch();
|
|
1042
1366
|
if (!base) {
|
|
1043
|
-
console.error(
|
|
1044
|
-
"error: could not detect base branch. pass it as argument: agentnote pr <base>"
|
|
1045
|
-
);
|
|
1367
|
+
console.error("error: could not detect base branch. pass it as argument: agentnote pr <base>");
|
|
1046
1368
|
process.exit(1);
|
|
1047
1369
|
}
|
|
1048
1370
|
const report = await collectReport(base);
|
|
@@ -1050,7 +1372,7 @@ async function pr(args2) {
|
|
|
1050
1372
|
if (isJson) {
|
|
1051
1373
|
console.log(JSON.stringify({ error: "no commits found" }));
|
|
1052
1374
|
} else {
|
|
1053
|
-
console.log(
|
|
1375
|
+
console.log(`no commits found between HEAD and ${base}`);
|
|
1054
1376
|
}
|
|
1055
1377
|
return;
|
|
1056
1378
|
}
|
|
@@ -1070,7 +1392,7 @@ async function pr(args2) {
|
|
|
1070
1392
|
}
|
|
1071
1393
|
}
|
|
1072
1394
|
function renderBar(ratio) {
|
|
1073
|
-
const width =
|
|
1395
|
+
const width = BAR_WIDTH_COMPACT;
|
|
1074
1396
|
const filled = Math.round(ratio / 100 * width);
|
|
1075
1397
|
return "\u2588".repeat(filled) + "\u2591".repeat(width - filled);
|
|
1076
1398
|
}
|
|
@@ -1079,20 +1401,199 @@ function basename(path) {
|
|
|
1079
1401
|
}
|
|
1080
1402
|
async function detectBaseBranch() {
|
|
1081
1403
|
for (const name of ["main", "master", "develop"]) {
|
|
1082
|
-
const { exitCode } = await gitSafe([
|
|
1083
|
-
"rev-parse",
|
|
1084
|
-
"--verify",
|
|
1085
|
-
`origin/${name}`
|
|
1086
|
-
]);
|
|
1404
|
+
const { exitCode } = await gitSafe(["rev-parse", "--verify", `origin/${name}`]);
|
|
1087
1405
|
if (exitCode === 0) return `origin/${name}`;
|
|
1088
1406
|
}
|
|
1089
1407
|
return null;
|
|
1090
1408
|
}
|
|
1091
1409
|
|
|
1410
|
+
// src/commands/session.ts
|
|
1411
|
+
async function session(sessionId) {
|
|
1412
|
+
if (!sessionId) {
|
|
1413
|
+
console.error("usage: agentnote session <session-id>");
|
|
1414
|
+
process.exit(1);
|
|
1415
|
+
}
|
|
1416
|
+
const raw = await git([
|
|
1417
|
+
"log",
|
|
1418
|
+
"--all",
|
|
1419
|
+
`--max-count=${MAX_COMMITS}`,
|
|
1420
|
+
`--format=%H %h %s %(trailers:key=${TRAILER_KEY},valueonly)`
|
|
1421
|
+
]);
|
|
1422
|
+
if (!raw) {
|
|
1423
|
+
console.log("no commits found");
|
|
1424
|
+
return;
|
|
1425
|
+
}
|
|
1426
|
+
const matches = [];
|
|
1427
|
+
for (const line of raw.split("\n")) {
|
|
1428
|
+
if (!line.trim()) continue;
|
|
1429
|
+
const parts = line.split(" ");
|
|
1430
|
+
const fullSha = parts[0];
|
|
1431
|
+
const shortInfo = parts[1];
|
|
1432
|
+
const trailer = parts[2]?.trim();
|
|
1433
|
+
if (!fullSha || !shortInfo) continue;
|
|
1434
|
+
if (trailer === sessionId) {
|
|
1435
|
+
const note = await readNote(fullSha);
|
|
1436
|
+
const entry = note;
|
|
1437
|
+
matches.push({ sha: fullSha, shortInfo, entry });
|
|
1438
|
+
continue;
|
|
1439
|
+
}
|
|
1440
|
+
if (!trailer) {
|
|
1441
|
+
const note = await readNote(fullSha);
|
|
1442
|
+
if (note && note.session_id === sessionId) {
|
|
1443
|
+
const entry = note;
|
|
1444
|
+
matches.push({ sha: fullSha, shortInfo, entry });
|
|
1445
|
+
}
|
|
1446
|
+
}
|
|
1447
|
+
}
|
|
1448
|
+
if (matches.length === 0) {
|
|
1449
|
+
console.log(`no commits found for session ${sessionId}`);
|
|
1450
|
+
return;
|
|
1451
|
+
}
|
|
1452
|
+
matches.reverse();
|
|
1453
|
+
console.log(`Session: ${sessionId}`);
|
|
1454
|
+
console.log(`Commits: ${matches.length}`);
|
|
1455
|
+
console.log();
|
|
1456
|
+
let totalPrompts = 0;
|
|
1457
|
+
let totalAiLines = 0;
|
|
1458
|
+
let totalAllLines = 0;
|
|
1459
|
+
let totalRatio = 0;
|
|
1460
|
+
let ratioCount = 0;
|
|
1461
|
+
for (const m of matches) {
|
|
1462
|
+
let suffix = "";
|
|
1463
|
+
if (m.entry) {
|
|
1464
|
+
const promptCount = m.entry.interactions?.length ?? m.entry.prompts?.length ?? 0;
|
|
1465
|
+
totalPrompts += promptCount;
|
|
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
|
+
}
|
|
1473
|
+
suffix = ` [\u{1F916}${m.entry.ai_ratio}% | ${promptCount}p]`;
|
|
1474
|
+
}
|
|
1475
|
+
console.log(`${m.shortInfo}${suffix}`);
|
|
1476
|
+
}
|
|
1477
|
+
console.log();
|
|
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}`);
|
|
1482
|
+
}
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1485
|
+
// src/commands/show.ts
|
|
1486
|
+
import { stat } from "node:fs/promises";
|
|
1487
|
+
async function show(commitRef) {
|
|
1488
|
+
const ref = commitRef ?? "HEAD";
|
|
1489
|
+
const commitInfo = await git(["log", "-1", "--format=%h %s", ref]);
|
|
1490
|
+
const commitSha = await git(["log", "-1", "--format=%H", ref]);
|
|
1491
|
+
const sessionId = (await git(["log", "-1", `--format=%(trailers:key=${TRAILER_KEY},valueonly)`, ref])).trim();
|
|
1492
|
+
console.log(`commit: ${commitInfo}`);
|
|
1493
|
+
if (!sessionId) {
|
|
1494
|
+
console.log("session: none (no agentnote data)");
|
|
1495
|
+
return;
|
|
1496
|
+
}
|
|
1497
|
+
console.log(`session: ${sessionId}`);
|
|
1498
|
+
const raw = await readNote(commitSha);
|
|
1499
|
+
const entry = raw;
|
|
1500
|
+
if (entry) {
|
|
1501
|
+
console.log();
|
|
1502
|
+
const ratioBar = renderRatioBar(entry.ai_ratio);
|
|
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}`);
|
|
1505
|
+
console.log(
|
|
1506
|
+
`files: ${entry.files_in_commit.length} changed, ${entry.files_by_ai.length} by AI`
|
|
1507
|
+
);
|
|
1508
|
+
if (entry.files_in_commit.length > 0) {
|
|
1509
|
+
console.log();
|
|
1510
|
+
for (const file of entry.files_in_commit) {
|
|
1511
|
+
const isAi = entry.files_by_ai.includes(file);
|
|
1512
|
+
const marker = isAi ? " \u{1F916}" : " \u{1F464}";
|
|
1513
|
+
console.log(` ${file}${marker}`);
|
|
1514
|
+
}
|
|
1515
|
+
}
|
|
1516
|
+
const legacy = entry;
|
|
1517
|
+
const interactions = entry.interactions ?? (legacy.prompts ?? []).map((p) => ({
|
|
1518
|
+
prompt: p,
|
|
1519
|
+
response: null
|
|
1520
|
+
}));
|
|
1521
|
+
if (interactions.length > 0) {
|
|
1522
|
+
console.log();
|
|
1523
|
+
console.log(`prompts: ${interactions.length}`);
|
|
1524
|
+
for (let i = 0; i < interactions.length; i++) {
|
|
1525
|
+
const interaction = interactions[i];
|
|
1526
|
+
console.log();
|
|
1527
|
+
console.log(` ${i + 1}. ${truncateLines(interaction.prompt, TRUNCATE_PROMPT)}`);
|
|
1528
|
+
if (interaction.response) {
|
|
1529
|
+
console.log(` \u2192 ${truncateLines(interaction.response, TRUNCATE_RESPONSE_SHOW)}`);
|
|
1530
|
+
}
|
|
1531
|
+
if (interaction.files_touched && interaction.files_touched.length > 0) {
|
|
1532
|
+
for (const file of interaction.files_touched) {
|
|
1533
|
+
console.log(` \u{1F4C4} ${file}`);
|
|
1534
|
+
}
|
|
1535
|
+
}
|
|
1536
|
+
}
|
|
1537
|
+
}
|
|
1538
|
+
} else {
|
|
1539
|
+
console.log("entry: no agentnote note found for this commit");
|
|
1540
|
+
}
|
|
1541
|
+
const adapter = claudeCode;
|
|
1542
|
+
const transcriptPath = adapter.findTranscript(sessionId);
|
|
1543
|
+
if (transcriptPath) {
|
|
1544
|
+
console.log();
|
|
1545
|
+
const stats = await stat(transcriptPath);
|
|
1546
|
+
const sizeKb = (stats.size / 1024).toFixed(1);
|
|
1547
|
+
console.log(`transcript: ${transcriptPath} (${sizeKb} KB)`);
|
|
1548
|
+
}
|
|
1549
|
+
}
|
|
1550
|
+
function renderRatioBar(ratio) {
|
|
1551
|
+
const width = BAR_WIDTH_FULL;
|
|
1552
|
+
const filled = Math.round(ratio / 100 * width);
|
|
1553
|
+
const empty = width - filled;
|
|
1554
|
+
return `[${"\u2588".repeat(filled)}${"\u2591".repeat(empty)}]`;
|
|
1555
|
+
}
|
|
1556
|
+
function truncateLines(text, maxLen) {
|
|
1557
|
+
const firstLine = text.split("\n")[0];
|
|
1558
|
+
if (firstLine.length <= maxLen) return firstLine;
|
|
1559
|
+
return `${firstLine.slice(0, maxLen)}\u2026`;
|
|
1560
|
+
}
|
|
1561
|
+
|
|
1562
|
+
// src/commands/status.ts
|
|
1563
|
+
import { existsSync as existsSync8 } from "node:fs";
|
|
1564
|
+
import { readFile as readFile6 } from "node:fs/promises";
|
|
1565
|
+
var VERSION = "0.1.7";
|
|
1566
|
+
async function status() {
|
|
1567
|
+
console.log(`agentnote v${VERSION}`);
|
|
1568
|
+
console.log();
|
|
1569
|
+
const repoRoot2 = await root();
|
|
1570
|
+
const adapter = claudeCode;
|
|
1571
|
+
const hooksActive = await adapter.isEnabled(repoRoot2);
|
|
1572
|
+
if (hooksActive) {
|
|
1573
|
+
console.log("hooks: active");
|
|
1574
|
+
} else {
|
|
1575
|
+
console.log("hooks: not configured (run 'agentnote init')");
|
|
1576
|
+
}
|
|
1577
|
+
const sessionPath = await sessionFile();
|
|
1578
|
+
if (existsSync8(sessionPath)) {
|
|
1579
|
+
const sid = (await readFile6(sessionPath, "utf-8")).trim();
|
|
1580
|
+
console.log(`session: ${sid.slice(0, 8)}\u2026`);
|
|
1581
|
+
} else {
|
|
1582
|
+
console.log("session: none");
|
|
1583
|
+
}
|
|
1584
|
+
const { stdout } = await gitSafe([
|
|
1585
|
+
"log",
|
|
1586
|
+
"-20",
|
|
1587
|
+
`--format=%(trailers:key=${TRAILER_KEY},valueonly)`
|
|
1588
|
+
]);
|
|
1589
|
+
const linked = stdout.split("\n").filter((line) => line.trim().length > 0).length;
|
|
1590
|
+
console.log(`linked: ${linked}/20 recent commits`);
|
|
1591
|
+
}
|
|
1592
|
+
|
|
1092
1593
|
// src/cli.ts
|
|
1093
|
-
var VERSION2 = "0.1.
|
|
1594
|
+
var VERSION2 = "0.1.7";
|
|
1094
1595
|
var HELP = `
|
|
1095
|
-
agentnote \u2014 remember why your code changed
|
|
1596
|
+
agentnote v${VERSION2} \u2014 remember why your code changed
|
|
1096
1597
|
|
|
1097
1598
|
usage:
|
|
1098
1599
|
agentnote init set up hooks, workflow, and notes auto-fetch
|
|
@@ -1123,6 +1624,9 @@ switch (command) {
|
|
|
1123
1624
|
case "status":
|
|
1124
1625
|
await status();
|
|
1125
1626
|
break;
|
|
1627
|
+
case "session":
|
|
1628
|
+
await session(args[0]);
|
|
1629
|
+
break;
|
|
1126
1630
|
case "hook":
|
|
1127
1631
|
await hook();
|
|
1128
1632
|
break;
|