@wasabeef/agentnote 0.1.5 → 0.1.6
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 +629 -506
- package/package.json +9 -4
package/dist/cli.js
CHANGED
|
@@ -1,82 +1,48 @@
|
|
|
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 SCHEMA_VERSION = 1;
|
|
29
|
+
var DEBUG = !!process.env.AGENTNOTE_DEBUG;
|
|
33
30
|
|
|
34
|
-
// src/
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
if (!_root) {
|
|
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
|
-
}
|
|
31
|
+
// src/core/record.ts
|
|
32
|
+
import { existsSync as existsSync3 } from "node:fs";
|
|
33
|
+
import { readdir, readFile as readFile3 } from "node:fs/promises";
|
|
34
|
+
import { join as join2 } from "node:path";
|
|
63
35
|
|
|
64
36
|
// src/agents/claude-code.ts
|
|
65
|
-
import { readFile, writeFile, mkdir } from "node:fs/promises";
|
|
66
37
|
import { existsSync, readdirSync } from "node:fs";
|
|
67
|
-
import {
|
|
38
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
68
39
|
import { homedir } from "node:os";
|
|
40
|
+
import { join } from "node:path";
|
|
69
41
|
var HOOK_COMMAND = "npx --yes @wasabeef/agentnote hook";
|
|
70
42
|
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
|
-
],
|
|
43
|
+
SessionStart: [{ hooks: [{ type: "command", command: HOOK_COMMAND, async: true }] }],
|
|
44
|
+
Stop: [{ hooks: [{ type: "command", command: HOOK_COMMAND, async: true }] }],
|
|
45
|
+
UserPromptSubmit: [{ hooks: [{ type: "command", command: HOOK_COMMAND, async: true }] }],
|
|
80
46
|
PreToolUse: [
|
|
81
47
|
{
|
|
82
48
|
matcher: "Bash",
|
|
@@ -95,18 +61,17 @@ function isValidSessionId(id) {
|
|
|
95
61
|
return UUID_PATTERN.test(id);
|
|
96
62
|
}
|
|
97
63
|
function isValidTranscriptPath(p) {
|
|
98
|
-
const claudeBase =
|
|
64
|
+
const claudeBase = join(homedir(), ".claude");
|
|
99
65
|
return p.startsWith(claudeBase);
|
|
100
66
|
}
|
|
101
67
|
function isGitCommit(cmd) {
|
|
102
|
-
|
|
103
|
-
return (trimmed.startsWith("git commit") || trimmed.startsWith("git -c ")) && trimmed.includes("commit") && !trimmed.includes("--amend");
|
|
68
|
+
return cmd.includes("git commit") && !cmd.includes("--amend");
|
|
104
69
|
}
|
|
105
70
|
var claudeCode = {
|
|
106
71
|
name: "claude-code",
|
|
107
72
|
settingsRelPath: ".claude/settings.json",
|
|
108
73
|
async installHooks(repoRoot2) {
|
|
109
|
-
const settingsPath =
|
|
74
|
+
const settingsPath = join(repoRoot2, this.settingsRelPath);
|
|
110
75
|
const { dirname } = await import("node:path");
|
|
111
76
|
await mkdir(dirname(settingsPath), { recursive: true });
|
|
112
77
|
let settings = {};
|
|
@@ -119,15 +84,17 @@ var claudeCode = {
|
|
|
119
84
|
}
|
|
120
85
|
const hooks = settings.hooks ?? {};
|
|
121
86
|
const raw = JSON.stringify(hooks);
|
|
122
|
-
if (raw.includes("@wasabeef/agentnote"))
|
|
87
|
+
if (raw.includes("@wasabeef/agentnote") || raw.includes("agentnote hook") || raw.includes("cli.js hook"))
|
|
88
|
+
return;
|
|
123
89
|
for (const [event, entries] of Object.entries(HOOKS_CONFIG)) {
|
|
124
90
|
hooks[event] = [...hooks[event] ?? [], ...entries];
|
|
125
91
|
}
|
|
126
92
|
settings.hooks = hooks;
|
|
127
|
-
await writeFile(settingsPath, JSON.stringify(settings, null, 2)
|
|
93
|
+
await writeFile(settingsPath, `${JSON.stringify(settings, null, 2)}
|
|
94
|
+
`);
|
|
128
95
|
},
|
|
129
96
|
async removeHooks(repoRoot2) {
|
|
130
|
-
const settingsPath =
|
|
97
|
+
const settingsPath = join(repoRoot2, this.settingsRelPath);
|
|
131
98
|
if (!existsSync(settingsPath)) return;
|
|
132
99
|
try {
|
|
133
100
|
const settings = JSON.parse(await readFile(settingsPath, "utf-8"));
|
|
@@ -135,21 +102,22 @@ var claudeCode = {
|
|
|
135
102
|
for (const [event, entries] of Object.entries(settings.hooks)) {
|
|
136
103
|
settings.hooks[event] = entries.filter((e) => {
|
|
137
104
|
const text = JSON.stringify(e);
|
|
138
|
-
return !text.includes("@wasabeef/agentnote");
|
|
105
|
+
return !text.includes("@wasabeef/agentnote") && !text.includes("agentnote hook") && !text.includes("cli.js hook");
|
|
139
106
|
});
|
|
140
107
|
if (settings.hooks[event].length === 0) delete settings.hooks[event];
|
|
141
108
|
}
|
|
142
109
|
if (Object.keys(settings.hooks).length === 0) delete settings.hooks;
|
|
143
|
-
await writeFile(settingsPath, JSON.stringify(settings, null, 2)
|
|
110
|
+
await writeFile(settingsPath, `${JSON.stringify(settings, null, 2)}
|
|
111
|
+
`);
|
|
144
112
|
} catch {
|
|
145
113
|
}
|
|
146
114
|
},
|
|
147
115
|
async isEnabled(repoRoot2) {
|
|
148
|
-
const settingsPath =
|
|
116
|
+
const settingsPath = join(repoRoot2, this.settingsRelPath);
|
|
149
117
|
if (!existsSync(settingsPath)) return false;
|
|
150
118
|
try {
|
|
151
119
|
const content = await readFile(settingsPath, "utf-8");
|
|
152
|
-
return content.includes("@wasabeef/agentnote");
|
|
120
|
+
return content.includes("@wasabeef/agentnote") || content.includes("agentnote hook") || content.includes("cli.js hook");
|
|
153
121
|
} catch {
|
|
154
122
|
return false;
|
|
155
123
|
}
|
|
@@ -167,7 +135,13 @@ var claudeCode = {
|
|
|
167
135
|
const tp = e.transcript_path && isValidTranscriptPath(e.transcript_path) ? e.transcript_path : void 0;
|
|
168
136
|
switch (e.hook_event_name) {
|
|
169
137
|
case "SessionStart":
|
|
170
|
-
return {
|
|
138
|
+
return {
|
|
139
|
+
kind: "session_start",
|
|
140
|
+
sessionId: sid,
|
|
141
|
+
timestamp: ts,
|
|
142
|
+
model: e.model,
|
|
143
|
+
transcriptPath: tp
|
|
144
|
+
};
|
|
171
145
|
case "Stop":
|
|
172
146
|
return { kind: "stop", sessionId: sid, timestamp: ts, transcriptPath: tp };
|
|
173
147
|
case "UserPromptSubmit":
|
|
@@ -182,7 +156,13 @@ var claudeCode = {
|
|
|
182
156
|
case "PostToolUse": {
|
|
183
157
|
const tool = e.tool_name;
|
|
184
158
|
if ((tool === "Edit" || tool === "Write" || tool === "NotebookEdit") && e.tool_input?.file_path) {
|
|
185
|
-
return {
|
|
159
|
+
return {
|
|
160
|
+
kind: "file_change",
|
|
161
|
+
sessionId: sid,
|
|
162
|
+
timestamp: ts,
|
|
163
|
+
tool,
|
|
164
|
+
file: e.tool_input.file_path
|
|
165
|
+
};
|
|
186
166
|
}
|
|
187
167
|
if (tool === "Bash" && isGitCommit(e.tool_input?.command ?? "")) {
|
|
188
168
|
return { kind: "post_commit", sessionId: sid, timestamp: ts, transcriptPath: tp };
|
|
@@ -195,13 +175,13 @@ var claudeCode = {
|
|
|
195
175
|
},
|
|
196
176
|
findTranscript(sessionId) {
|
|
197
177
|
if (!isValidSessionId(sessionId)) return null;
|
|
198
|
-
const claudeDir =
|
|
178
|
+
const claudeDir = join(homedir(), ".claude", "projects");
|
|
199
179
|
if (!existsSync(claudeDir)) return null;
|
|
200
180
|
try {
|
|
201
181
|
for (const project of readdirSync(claudeDir)) {
|
|
202
|
-
const sessionsDir =
|
|
182
|
+
const sessionsDir = join(claudeDir, project, "sessions");
|
|
203
183
|
if (!existsSync(sessionsDir)) continue;
|
|
204
|
-
const candidate =
|
|
184
|
+
const candidate = join(sessionsDir, `${sessionId}.jsonl`);
|
|
205
185
|
if (existsSync(candidate) && isValidTranscriptPath(candidate)) {
|
|
206
186
|
return candidate;
|
|
207
187
|
}
|
|
@@ -253,141 +233,86 @@ var claudeCode = {
|
|
|
253
233
|
}
|
|
254
234
|
};
|
|
255
235
|
|
|
256
|
-
// src/
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
async function init(args2) {
|
|
278
|
-
const skipHooks = args2.includes("--no-hooks");
|
|
279
|
-
const skipAction = args2.includes("--no-action");
|
|
280
|
-
const skipNotes = args2.includes("--no-notes");
|
|
281
|
-
const hooksOnly = args2.includes("--hooks");
|
|
282
|
-
const actionOnly = args2.includes("--action");
|
|
283
|
-
const repoRoot2 = await root();
|
|
284
|
-
const results = [];
|
|
285
|
-
await mkdir2(await agentnoteDir(), { recursive: true });
|
|
286
|
-
if (!skipHooks && !actionOnly) {
|
|
287
|
-
const adapter = claudeCode;
|
|
288
|
-
if (await adapter.isEnabled(repoRoot2)) {
|
|
289
|
-
results.push(" \xB7 hooks already configured");
|
|
290
|
-
} else {
|
|
291
|
-
await adapter.installHooks(repoRoot2);
|
|
292
|
-
results.push(" \u2713 hooks added to .claude/settings.json");
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
if (!skipAction && !hooksOnly) {
|
|
296
|
-
const workflowDir = join3(repoRoot2, ".github", "workflows");
|
|
297
|
-
const workflowPath = join3(workflowDir, "agentnote.yml");
|
|
298
|
-
if (existsSync2(workflowPath)) {
|
|
299
|
-
results.push(" \xB7 workflow already exists at .github/workflows/agentnote.yml");
|
|
300
|
-
} else {
|
|
301
|
-
await mkdir2(workflowDir, { recursive: true });
|
|
302
|
-
await writeFile2(workflowPath, WORKFLOW_TEMPLATE);
|
|
303
|
-
results.push(" \u2713 workflow created at .github/workflows/agentnote.yml");
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
if (!skipNotes && !hooksOnly && !actionOnly) {
|
|
307
|
-
const { stdout } = await gitSafe([
|
|
308
|
-
"config",
|
|
309
|
-
"--get-all",
|
|
310
|
-
"remote.origin.fetch"
|
|
311
|
-
]);
|
|
312
|
-
if (stdout.includes("refs/notes/agentnote")) {
|
|
313
|
-
results.push(" \xB7 git already configured to fetch notes");
|
|
314
|
-
} else {
|
|
315
|
-
await gitSafe([
|
|
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");
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
console.log("");
|
|
325
|
-
console.log("agentnote init");
|
|
326
|
-
console.log("");
|
|
327
|
-
for (const line of results) {
|
|
328
|
-
console.log(line);
|
|
329
|
-
}
|
|
330
|
-
const toCommit = [];
|
|
331
|
-
if (!skipHooks && !actionOnly) toCommit.push(".claude/settings.json");
|
|
332
|
-
if (!skipAction && !hooksOnly) {
|
|
333
|
-
const workflowPath = join3(repoRoot2, ".github", "workflows", "agentnote.yml");
|
|
334
|
-
if (existsSync2(workflowPath)) toCommit.push(".github/workflows/agentnote.yml");
|
|
335
|
-
}
|
|
336
|
-
if (toCommit.length > 0) {
|
|
337
|
-
console.log("");
|
|
338
|
-
console.log(" Next: commit and push these files");
|
|
339
|
-
console.log(` git add ${toCommit.join(" ")}`);
|
|
340
|
-
console.log(' git commit -m "chore: enable agentnote session tracking"');
|
|
341
|
-
console.log(" git push");
|
|
236
|
+
// src/git.ts
|
|
237
|
+
import { execFile } from "node:child_process";
|
|
238
|
+
import { promisify } from "node:util";
|
|
239
|
+
var execFileAsync = promisify(execFile);
|
|
240
|
+
async function git(args2, options) {
|
|
241
|
+
const { stdout } = await execFileAsync("git", args2, {
|
|
242
|
+
cwd: options?.cwd,
|
|
243
|
+
encoding: "utf-8"
|
|
244
|
+
});
|
|
245
|
+
return stdout.trim();
|
|
246
|
+
}
|
|
247
|
+
async function gitSafe(args2, options) {
|
|
248
|
+
try {
|
|
249
|
+
const stdout = await git(args2, options);
|
|
250
|
+
return { stdout, exitCode: 0 };
|
|
251
|
+
} catch (err) {
|
|
252
|
+
const e = err;
|
|
253
|
+
return {
|
|
254
|
+
stdout: typeof e.stdout === "string" ? e.stdout.trim() : "",
|
|
255
|
+
exitCode: typeof e.code === "number" ? e.code : 1
|
|
256
|
+
};
|
|
342
257
|
}
|
|
343
|
-
|
|
258
|
+
}
|
|
259
|
+
async function repoRoot() {
|
|
260
|
+
return git(["rev-parse", "--show-toplevel"]);
|
|
344
261
|
}
|
|
345
262
|
|
|
346
|
-
// src/
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
263
|
+
// src/core/entry.ts
|
|
264
|
+
function calcAiRatio(commitFiles, aiFiles) {
|
|
265
|
+
if (commitFiles.length === 0) return 0;
|
|
266
|
+
const aiSet = new Set(aiFiles);
|
|
267
|
+
const matched = commitFiles.filter((f) => aiSet.has(f));
|
|
268
|
+
return Math.round(matched.length / commitFiles.length * 100);
|
|
269
|
+
}
|
|
270
|
+
function buildEntry(opts) {
|
|
271
|
+
return {
|
|
272
|
+
v: SCHEMA_VERSION,
|
|
273
|
+
session_id: opts.sessionId,
|
|
274
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
275
|
+
interactions: opts.interactions.map((i) => {
|
|
276
|
+
const base = { prompt: i.prompt, response: i.response };
|
|
277
|
+
if (i.files_touched && i.files_touched.length > 0) {
|
|
278
|
+
base.files_touched = i.files_touched;
|
|
279
|
+
}
|
|
280
|
+
return base;
|
|
281
|
+
}),
|
|
282
|
+
files_in_commit: opts.commitFiles,
|
|
283
|
+
files_by_ai: opts.aiFiles,
|
|
284
|
+
ai_ratio: calcAiRatio(opts.commitFiles, opts.aiFiles)
|
|
285
|
+
};
|
|
286
|
+
}
|
|
351
287
|
|
|
352
288
|
// src/core/jsonl.ts
|
|
353
|
-
import {
|
|
354
|
-
import {
|
|
355
|
-
async function
|
|
356
|
-
if (!
|
|
289
|
+
import { existsSync as existsSync2 } from "node:fs";
|
|
290
|
+
import { appendFile, readFile as readFile2 } from "node:fs/promises";
|
|
291
|
+
async function readJsonlEntries(filePath) {
|
|
292
|
+
if (!existsSync2(filePath)) return [];
|
|
357
293
|
const content = await readFile2(filePath, "utf-8");
|
|
358
|
-
const
|
|
359
|
-
const values = [];
|
|
294
|
+
const entries = [];
|
|
360
295
|
for (const line of content.trim().split("\n")) {
|
|
361
296
|
if (!line) continue;
|
|
362
297
|
try {
|
|
363
|
-
|
|
364
|
-
const val = entry[field];
|
|
365
|
-
if (val && !seen.has(val)) {
|
|
366
|
-
seen.add(val);
|
|
367
|
-
values.push(val);
|
|
368
|
-
}
|
|
298
|
+
entries.push(JSON.parse(line));
|
|
369
299
|
} catch {
|
|
370
300
|
}
|
|
371
301
|
}
|
|
372
|
-
return
|
|
302
|
+
return entries;
|
|
373
303
|
}
|
|
374
304
|
async function appendJsonl(filePath, data) {
|
|
375
|
-
await appendFile(filePath, JSON.stringify(data)
|
|
305
|
+
await appendFile(filePath, `${JSON.stringify(data)}
|
|
306
|
+
`);
|
|
376
307
|
}
|
|
377
308
|
|
|
378
309
|
// src/core/storage.ts
|
|
379
|
-
var NOTES_REF = "agentnote";
|
|
380
310
|
async function writeNote(commitSha, data) {
|
|
381
311
|
const body = JSON.stringify(data, null, 2);
|
|
382
312
|
await gitSafe(["notes", `--ref=${NOTES_REF}`, "add", "-f", "-m", body, commitSha]);
|
|
383
313
|
}
|
|
384
314
|
async function readNote(commitSha) {
|
|
385
|
-
const { stdout, exitCode } = await gitSafe([
|
|
386
|
-
"notes",
|
|
387
|
-
`--ref=${NOTES_REF}`,
|
|
388
|
-
"show",
|
|
389
|
-
commitSha
|
|
390
|
-
]);
|
|
315
|
+
const { stdout, exitCode } = await gitSafe(["notes", `--ref=${NOTES_REF}`, "show", commitSha]);
|
|
391
316
|
if (exitCode !== 0 || !stdout.trim()) return null;
|
|
392
317
|
try {
|
|
393
318
|
return JSON.parse(stdout);
|
|
@@ -396,53 +321,144 @@ async function readNote(commitSha) {
|
|
|
396
321
|
}
|
|
397
322
|
}
|
|
398
323
|
|
|
399
|
-
// src/core/
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
324
|
+
// src/core/record.ts
|
|
325
|
+
async function recordCommitEntry(opts) {
|
|
326
|
+
const sessionDir = join2(opts.agentnoteDirPath, "sessions", opts.sessionId);
|
|
327
|
+
const commitSha = await git(["rev-parse", "HEAD"]);
|
|
328
|
+
let commitFiles = [];
|
|
329
|
+
try {
|
|
330
|
+
const raw = await git(["diff-tree", "--no-commit-id", "--name-only", "-r", "HEAD"]);
|
|
331
|
+
commitFiles = raw.split("\n").filter(Boolean);
|
|
332
|
+
} catch {
|
|
333
|
+
}
|
|
334
|
+
const commitFileSet = new Set(commitFiles);
|
|
335
|
+
const changeEntries = await readAllSessionJsonl(sessionDir, CHANGES_FILE);
|
|
336
|
+
const promptEntries = await readAllSessionJsonl(sessionDir, PROMPTS_FILE);
|
|
337
|
+
const hasTurnData = promptEntries.some((e) => typeof e.turn === "number" && e.turn > 0);
|
|
338
|
+
let aiFiles;
|
|
339
|
+
let prompts;
|
|
340
|
+
let relevantPromptEntries;
|
|
341
|
+
if (hasTurnData) {
|
|
342
|
+
aiFiles = [
|
|
343
|
+
...new Set(
|
|
344
|
+
changeEntries.map((e) => e.file).filter((f) => f && commitFileSet.has(f))
|
|
345
|
+
)
|
|
346
|
+
];
|
|
347
|
+
const relevantTurns = /* @__PURE__ */ new Set();
|
|
348
|
+
for (const entry2 of changeEntries) {
|
|
349
|
+
const file = entry2.file;
|
|
350
|
+
if (file && commitFileSet.has(file)) {
|
|
351
|
+
relevantTurns.add(typeof entry2.turn === "number" ? entry2.turn : 0);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
relevantPromptEntries = promptEntries.filter((e) => {
|
|
355
|
+
const turn = typeof e.turn === "number" ? e.turn : 0;
|
|
356
|
+
return relevantTurns.has(turn);
|
|
357
|
+
});
|
|
358
|
+
prompts = relevantPromptEntries.map((e) => e.prompt);
|
|
359
|
+
} else {
|
|
360
|
+
aiFiles = changeEntries.map((e) => e.file).filter(Boolean);
|
|
361
|
+
prompts = promptEntries.map((e) => e.prompt);
|
|
362
|
+
relevantPromptEntries = promptEntries;
|
|
363
|
+
}
|
|
364
|
+
const transcriptPath = opts.transcriptPath ?? await readSavedTranscriptPath(sessionDir);
|
|
365
|
+
let interactions;
|
|
366
|
+
if (transcriptPath && prompts.length > 0) {
|
|
367
|
+
const allInteractions = await claudeCode.extractInteractions(transcriptPath);
|
|
368
|
+
interactions = allInteractions.length > 0 ? allInteractions.slice(-prompts.length) : prompts.map((p) => ({ prompt: p, response: null }));
|
|
369
|
+
} else {
|
|
370
|
+
interactions = prompts.map((p) => ({ prompt: p, response: null }));
|
|
371
|
+
}
|
|
372
|
+
if (hasTurnData) {
|
|
373
|
+
attachFilesTouched(changeEntries, relevantPromptEntries, interactions, commitFileSet);
|
|
374
|
+
}
|
|
375
|
+
const entry = buildEntry({
|
|
376
|
+
sessionId: opts.sessionId,
|
|
377
|
+
interactions,
|
|
378
|
+
commitFiles,
|
|
379
|
+
aiFiles
|
|
380
|
+
});
|
|
381
|
+
await writeNote(commitSha, entry);
|
|
382
|
+
return { promptCount: interactions.length, aiRatio: entry.ai_ratio };
|
|
406
383
|
}
|
|
407
|
-
function
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
384
|
+
function attachFilesTouched(changeEntries, promptEntries, interactions, commitFileSet) {
|
|
385
|
+
const filesByTurn = /* @__PURE__ */ new Map();
|
|
386
|
+
for (const entry of changeEntries) {
|
|
387
|
+
const turn = typeof entry.turn === "number" ? entry.turn : 0;
|
|
388
|
+
const file = entry.file;
|
|
389
|
+
if (!file || !commitFileSet.has(file)) continue;
|
|
390
|
+
if (!filesByTurn.has(turn)) filesByTurn.set(turn, /* @__PURE__ */ new Set());
|
|
391
|
+
filesByTurn.get(turn)?.add(file);
|
|
392
|
+
}
|
|
393
|
+
for (let i = 0; i < interactions.length; i++) {
|
|
394
|
+
const promptEntry = promptEntries[i];
|
|
395
|
+
if (!promptEntry) continue;
|
|
396
|
+
const turn = typeof promptEntry.turn === "number" ? promptEntry.turn : 0;
|
|
397
|
+
const files = filesByTurn.get(turn);
|
|
398
|
+
if (files && files.size > 0) {
|
|
399
|
+
interactions[i].files_touched = [...files];
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
async function readAllSessionJsonl(sessionDir, baseFile) {
|
|
404
|
+
const stem = baseFile.slice(0, baseFile.lastIndexOf(".jsonl"));
|
|
405
|
+
const files = await readdir(sessionDir).catch(() => []);
|
|
406
|
+
const matching = files.filter((f) => f === baseFile || f.startsWith(`${stem}-`) && f.endsWith(".jsonl")).sort().map((f) => join2(sessionDir, f));
|
|
407
|
+
const all = [];
|
|
408
|
+
for (const file of matching) {
|
|
409
|
+
const entries = await readJsonlEntries(file);
|
|
410
|
+
all.push(...entries);
|
|
411
|
+
}
|
|
412
|
+
return all;
|
|
413
|
+
}
|
|
414
|
+
async function readSavedTranscriptPath(sessionDir) {
|
|
415
|
+
const saved = join2(sessionDir, TRANSCRIPT_PATH_FILE);
|
|
416
|
+
if (!existsSync3(saved)) return null;
|
|
417
|
+
const p = (await readFile3(saved, "utf-8")).trim();
|
|
418
|
+
return p || null;
|
|
417
419
|
}
|
|
418
420
|
|
|
419
|
-
// src/
|
|
420
|
-
import {
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
async function
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
421
|
+
// src/paths.ts
|
|
422
|
+
import { join as join3 } from "node:path";
|
|
423
|
+
var _root = null;
|
|
424
|
+
var _gitDir = null;
|
|
425
|
+
async function root() {
|
|
426
|
+
if (!_root) {
|
|
427
|
+
try {
|
|
428
|
+
_root = await repoRoot();
|
|
429
|
+
} catch {
|
|
430
|
+
console.error("error: git repository not found");
|
|
431
|
+
process.exit(1);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
return _root;
|
|
435
|
+
}
|
|
436
|
+
async function gitDir() {
|
|
437
|
+
if (!_gitDir) {
|
|
438
|
+
_gitDir = await git(["rev-parse", "--git-dir"]);
|
|
439
|
+
if (!_gitDir.startsWith("/")) {
|
|
440
|
+
_gitDir = join3(await root(), _gitDir);
|
|
432
441
|
}
|
|
433
442
|
}
|
|
443
|
+
return _gitDir;
|
|
444
|
+
}
|
|
445
|
+
async function agentnoteDir() {
|
|
446
|
+
return join3(await gitDir(), AGENTNOTE_DIR);
|
|
447
|
+
}
|
|
448
|
+
async function sessionFile() {
|
|
449
|
+
return join3(await agentnoteDir(), SESSION_FILE);
|
|
434
450
|
}
|
|
435
451
|
|
|
436
452
|
// src/commands/commit.ts
|
|
437
453
|
async function commit(args2) {
|
|
438
454
|
const sf = await sessionFile();
|
|
439
455
|
let sessionId = "";
|
|
440
|
-
if (
|
|
441
|
-
sessionId = (await
|
|
456
|
+
if (existsSync4(sf)) {
|
|
457
|
+
sessionId = (await readFile4(sf, "utf-8")).trim();
|
|
442
458
|
}
|
|
443
459
|
const gitArgs = ["commit"];
|
|
444
460
|
if (sessionId) {
|
|
445
|
-
gitArgs.push("--trailer",
|
|
461
|
+
gitArgs.push("--trailer", `${TRAILER_KEY}: ${sessionId}`);
|
|
446
462
|
}
|
|
447
463
|
gitArgs.push(...args2);
|
|
448
464
|
const child = spawn("git", gitArgs, {
|
|
@@ -458,210 +474,44 @@ async function commit(args2) {
|
|
|
458
474
|
if (sessionId) {
|
|
459
475
|
try {
|
|
460
476
|
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
|
-
);
|
|
477
|
+
const result = await recordCommitEntry({ agentnoteDirPath, sessionId });
|
|
478
|
+
console.log(`agentnote: ${result.promptCount} prompts, AI ratio ${result.aiRatio}%`);
|
|
507
479
|
} catch (err) {
|
|
508
480
|
console.error(`agentnote: warning: ${err.message}`);
|
|
509
481
|
}
|
|
510
482
|
}
|
|
511
483
|
}
|
|
512
484
|
|
|
513
|
-
// src/commands/
|
|
514
|
-
import {
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
const commitInfo = await git(["log", "-1", "--format=%h %s", ref]);
|
|
518
|
-
const commitSha = await git(["log", "-1", "--format=%H", ref]);
|
|
519
|
-
const sessionId = (await git([
|
|
520
|
-
"log",
|
|
521
|
-
"-1",
|
|
522
|
-
"--format=%(trailers:key=Agentnote-Session,valueonly)",
|
|
523
|
-
ref
|
|
524
|
-
])).trim();
|
|
525
|
-
console.log(`commit: ${commitInfo}`);
|
|
526
|
-
if (!sessionId) {
|
|
527
|
-
console.log("session: none (no agentnote data)");
|
|
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
|
-
}
|
|
563
|
-
}
|
|
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
|
-
}
|
|
577
|
-
}
|
|
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
|
-
}
|
|
485
|
+
// src/commands/hook.ts
|
|
486
|
+
import { existsSync as existsSync6 } from "node:fs";
|
|
487
|
+
import { mkdir as mkdir2, readFile as readFile5, realpath, writeFile as writeFile2 } from "node:fs/promises";
|
|
488
|
+
import { isAbsolute, join as join5, relative } from "node:path";
|
|
589
489
|
|
|
590
|
-
// src/
|
|
591
|
-
|
|
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]`);
|
|
490
|
+
// src/core/rotate.ts
|
|
491
|
+
import { existsSync as existsSync5 } from "node:fs";
|
|
492
|
+
import { readdir as readdir2, rename, unlink } from "node:fs/promises";
|
|
493
|
+
import { join as join4 } from "node:path";
|
|
494
|
+
async function rotateLogs(sessionDir, commitSha, fileNames = [PROMPTS_FILE, CHANGES_FILE]) {
|
|
495
|
+
await purgeRotatedArchives(sessionDir, fileNames);
|
|
496
|
+
for (const name of fileNames) {
|
|
497
|
+
const src = join4(sessionDir, name);
|
|
498
|
+
if (existsSync5(src)) {
|
|
499
|
+
const base = name.replace(".jsonl", "");
|
|
500
|
+
await rename(src, join4(sessionDir, `${base}-${commitSha.slice(0, 8)}.jsonl`));
|
|
626
501
|
}
|
|
627
502
|
}
|
|
628
503
|
}
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
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");
|
|
651
|
-
}
|
|
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`);
|
|
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
|
+
}
|
|
659
512
|
}
|
|
660
513
|
|
|
661
514
|
// 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
515
|
async function readStdin() {
|
|
666
516
|
const chunks = [];
|
|
667
517
|
for await (const chunk of process.stdin) {
|
|
@@ -683,15 +533,15 @@ async function hook() {
|
|
|
683
533
|
const event = adapter.parseEvent(input);
|
|
684
534
|
if (!event) return;
|
|
685
535
|
const agentnoteDirPath = await agentnoteDir();
|
|
686
|
-
const sessionDir =
|
|
687
|
-
await
|
|
536
|
+
const sessionDir = join5(agentnoteDirPath, SESSIONS_DIR, event.sessionId);
|
|
537
|
+
await mkdir2(sessionDir, { recursive: true });
|
|
688
538
|
switch (event.kind) {
|
|
689
539
|
case "session_start": {
|
|
690
|
-
await
|
|
540
|
+
await writeFile2(join5(agentnoteDirPath, SESSION_FILE), event.sessionId);
|
|
691
541
|
if (event.transcriptPath) {
|
|
692
|
-
await
|
|
542
|
+
await writeFile2(join5(sessionDir, TRANSCRIPT_PATH_FILE), event.transcriptPath);
|
|
693
543
|
}
|
|
694
|
-
await appendJsonl(
|
|
544
|
+
await appendJsonl(join5(sessionDir, EVENTS_FILE), {
|
|
695
545
|
event: "session_start",
|
|
696
546
|
session_id: event.sessionId,
|
|
697
547
|
timestamp: event.timestamp,
|
|
@@ -700,11 +550,11 @@ async function hook() {
|
|
|
700
550
|
break;
|
|
701
551
|
}
|
|
702
552
|
case "stop": {
|
|
703
|
-
await
|
|
553
|
+
await writeFile2(join5(agentnoteDirPath, SESSION_FILE), event.sessionId);
|
|
704
554
|
if (event.transcriptPath) {
|
|
705
|
-
await
|
|
555
|
+
await writeFile2(join5(sessionDir, TRANSCRIPT_PATH_FILE), event.transcriptPath);
|
|
706
556
|
}
|
|
707
|
-
await appendJsonl(
|
|
557
|
+
await appendJsonl(join5(sessionDir, EVENTS_FILE), {
|
|
708
558
|
event: "stop",
|
|
709
559
|
session_id: event.sessionId,
|
|
710
560
|
timestamp: event.timestamp
|
|
@@ -712,10 +562,21 @@ async function hook() {
|
|
|
712
562
|
break;
|
|
713
563
|
}
|
|
714
564
|
case "prompt": {
|
|
715
|
-
|
|
565
|
+
const rotateId = Date.now().toString(36);
|
|
566
|
+
await rotateLogs(sessionDir, rotateId);
|
|
567
|
+
const turnPath = join5(sessionDir, TURN_FILE);
|
|
568
|
+
let turn = 0;
|
|
569
|
+
if (existsSync6(turnPath)) {
|
|
570
|
+
const raw2 = (await readFile5(turnPath, "utf-8")).trim();
|
|
571
|
+
turn = Number.parseInt(raw2, 10) || 0;
|
|
572
|
+
}
|
|
573
|
+
turn += 1;
|
|
574
|
+
await writeFile2(turnPath, String(turn));
|
|
575
|
+
await appendJsonl(join5(sessionDir, PROMPTS_FILE), {
|
|
716
576
|
event: "prompt",
|
|
717
577
|
timestamp: event.timestamp,
|
|
718
|
-
prompt: event.prompt
|
|
578
|
+
prompt: event.prompt,
|
|
579
|
+
turn
|
|
719
580
|
});
|
|
720
581
|
break;
|
|
721
582
|
}
|
|
@@ -727,7 +588,7 @@ async function hook() {
|
|
|
727
588
|
const repoRoot2 = await realpath(rawRoot);
|
|
728
589
|
let normalizedFile = filePath;
|
|
729
590
|
if (repoRoot2.startsWith("/private") && !normalizedFile.startsWith("/private")) {
|
|
730
|
-
normalizedFile =
|
|
591
|
+
normalizedFile = `/private${normalizedFile}`;
|
|
731
592
|
} else if (!repoRoot2.startsWith("/private") && normalizedFile.startsWith("/private")) {
|
|
732
593
|
normalizedFile = normalizedFile.replace(/^\/private/, "");
|
|
733
594
|
}
|
|
@@ -735,24 +596,31 @@ async function hook() {
|
|
|
735
596
|
} catch {
|
|
736
597
|
}
|
|
737
598
|
}
|
|
738
|
-
|
|
599
|
+
let turn = 0;
|
|
600
|
+
const turnPath = join5(sessionDir, TURN_FILE);
|
|
601
|
+
if (existsSync6(turnPath)) {
|
|
602
|
+
const raw2 = (await readFile5(turnPath, "utf-8")).trim();
|
|
603
|
+
turn = Number.parseInt(raw2, 10) || 0;
|
|
604
|
+
}
|
|
605
|
+
await appendJsonl(join5(sessionDir, CHANGES_FILE), {
|
|
739
606
|
event: "file_change",
|
|
740
607
|
timestamp: event.timestamp,
|
|
741
608
|
tool: event.tool,
|
|
742
609
|
file: filePath,
|
|
743
|
-
session_id: event.sessionId
|
|
610
|
+
session_id: event.sessionId,
|
|
611
|
+
turn
|
|
744
612
|
});
|
|
745
613
|
break;
|
|
746
614
|
}
|
|
747
615
|
case "pre_commit": {
|
|
748
616
|
const cmd = event.commitCommand ?? "";
|
|
749
|
-
if (!cmd.includes(
|
|
617
|
+
if (!cmd.includes(TRAILER_KEY) && event.sessionId) {
|
|
750
618
|
process.stdout.write(
|
|
751
619
|
JSON.stringify({
|
|
752
620
|
hookSpecificOutput: {
|
|
753
621
|
hookEventName: "PreToolUse",
|
|
754
622
|
updatedInput: {
|
|
755
|
-
command: `${cmd} --trailer '
|
|
623
|
+
command: `${cmd} --trailer '${TRAILER_KEY}: ${event.sessionId}'`
|
|
756
624
|
}
|
|
757
625
|
}
|
|
758
626
|
})
|
|
@@ -762,47 +630,138 @@ async function hook() {
|
|
|
762
630
|
}
|
|
763
631
|
case "post_commit": {
|
|
764
632
|
try {
|
|
765
|
-
await
|
|
633
|
+
await recordCommitEntry({
|
|
634
|
+
agentnoteDirPath,
|
|
635
|
+
sessionId: event.sessionId,
|
|
636
|
+
transcriptPath: event.transcriptPath
|
|
637
|
+
});
|
|
766
638
|
} catch {
|
|
767
639
|
}
|
|
768
640
|
break;
|
|
769
641
|
}
|
|
770
642
|
}
|
|
771
643
|
}
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
644
|
+
|
|
645
|
+
// src/commands/init.ts
|
|
646
|
+
import { existsSync as existsSync7 } from "node:fs";
|
|
647
|
+
import { mkdir as mkdir3, writeFile as writeFile3 } from "node:fs/promises";
|
|
648
|
+
import { join as join6 } from "node:path";
|
|
649
|
+
var WORKFLOW_TEMPLATE = `name: Agent Note
|
|
650
|
+
on:
|
|
651
|
+
pull_request:
|
|
652
|
+
types: [opened, synchronize]
|
|
653
|
+
concurrency:
|
|
654
|
+
group: agentnote-\${{ github.event.pull_request.number }}
|
|
655
|
+
cancel-in-progress: true
|
|
656
|
+
permissions:
|
|
657
|
+
contents: read
|
|
658
|
+
pull-requests: write
|
|
659
|
+
jobs:
|
|
660
|
+
report:
|
|
661
|
+
runs-on: ubuntu-latest
|
|
662
|
+
timeout-minutes: 5
|
|
663
|
+
steps:
|
|
664
|
+
- uses: actions/checkout@v4
|
|
665
|
+
with:
|
|
666
|
+
fetch-depth: 0
|
|
667
|
+
- uses: wasabeef/agentnote@v0
|
|
668
|
+
`;
|
|
669
|
+
async function init(args2) {
|
|
670
|
+
const skipHooks = args2.includes("--no-hooks");
|
|
671
|
+
const skipAction = args2.includes("--no-action");
|
|
672
|
+
const skipNotes = args2.includes("--no-notes");
|
|
673
|
+
const hooksOnly = args2.includes("--hooks");
|
|
674
|
+
const actionOnly = args2.includes("--action");
|
|
675
|
+
const repoRoot2 = await root();
|
|
676
|
+
const results = [];
|
|
677
|
+
await mkdir3(await agentnoteDir(), { recursive: true });
|
|
678
|
+
if (!skipHooks && !actionOnly) {
|
|
679
|
+
const adapter = claudeCode;
|
|
680
|
+
if (await adapter.isEnabled(repoRoot2)) {
|
|
681
|
+
results.push(" \xB7 hooks already configured");
|
|
682
|
+
} else {
|
|
683
|
+
await adapter.installHooks(repoRoot2);
|
|
684
|
+
results.push(" \u2713 hooks added to .claude/settings.json");
|
|
685
|
+
}
|
|
780
686
|
}
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
687
|
+
if (!skipAction && !hooksOnly) {
|
|
688
|
+
const workflowDir = join6(repoRoot2, ".github", "workflows");
|
|
689
|
+
const workflowPath = join6(workflowDir, "agentnote.yml");
|
|
690
|
+
if (existsSync7(workflowPath)) {
|
|
691
|
+
results.push(" \xB7 workflow already exists at .github/workflows/agentnote.yml");
|
|
692
|
+
} else {
|
|
693
|
+
await mkdir3(workflowDir, { recursive: true });
|
|
694
|
+
await writeFile3(workflowPath, WORKFLOW_TEMPLATE);
|
|
695
|
+
results.push(" \u2713 workflow created at .github/workflows/agentnote.yml");
|
|
696
|
+
}
|
|
791
697
|
}
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
698
|
+
if (!skipNotes && !hooksOnly && !actionOnly) {
|
|
699
|
+
const { stdout } = await gitSafe(["config", "--get-all", "remote.origin.fetch"]);
|
|
700
|
+
if (stdout.includes(NOTES_REF_FULL)) {
|
|
701
|
+
results.push(" \xB7 git already configured to fetch notes");
|
|
702
|
+
} else {
|
|
703
|
+
await gitSafe(["config", "--add", "remote.origin.fetch", NOTES_FETCH_REFSPEC]);
|
|
704
|
+
results.push(" \u2713 git configured to auto-fetch notes on pull");
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
console.log("");
|
|
708
|
+
console.log("agentnote init");
|
|
709
|
+
console.log("");
|
|
710
|
+
for (const line of results) {
|
|
711
|
+
console.log(line);
|
|
712
|
+
}
|
|
713
|
+
const toCommit = [];
|
|
714
|
+
if (!skipHooks && !actionOnly) toCommit.push(".claude/settings.json");
|
|
715
|
+
if (!skipAction && !hooksOnly) {
|
|
716
|
+
const workflowPath = join6(repoRoot2, ".github", "workflows", "agentnote.yml");
|
|
717
|
+
if (existsSync7(workflowPath)) toCommit.push(".github/workflows/agentnote.yml");
|
|
718
|
+
}
|
|
719
|
+
if (toCommit.length > 0) {
|
|
720
|
+
console.log("");
|
|
721
|
+
console.log(" Next: commit and push these files");
|
|
722
|
+
console.log(` git add ${toCommit.join(" ")}`);
|
|
723
|
+
console.log(' git commit -m "chore: enable agentnote session tracking"');
|
|
724
|
+
console.log(" git push");
|
|
725
|
+
}
|
|
726
|
+
console.log("");
|
|
800
727
|
}
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
const
|
|
805
|
-
|
|
728
|
+
|
|
729
|
+
// src/commands/log.ts
|
|
730
|
+
async function log(count = 10) {
|
|
731
|
+
const raw = await git([
|
|
732
|
+
"log",
|
|
733
|
+
`-${count}`,
|
|
734
|
+
`--format=%H %h %s %(trailers:key=${TRAILER_KEY},valueonly)`
|
|
735
|
+
]);
|
|
736
|
+
if (!raw) {
|
|
737
|
+
console.log("no commits found");
|
|
738
|
+
return;
|
|
739
|
+
}
|
|
740
|
+
for (const line of raw.split("\n")) {
|
|
741
|
+
if (!line.trim()) continue;
|
|
742
|
+
const parts = line.split(" ");
|
|
743
|
+
const fullSha = parts[0];
|
|
744
|
+
const commitPart = parts[1];
|
|
745
|
+
const sid = parts[2]?.trim();
|
|
746
|
+
if (!fullSha || !commitPart) continue;
|
|
747
|
+
if (!sid) {
|
|
748
|
+
console.log(commitPart);
|
|
749
|
+
continue;
|
|
750
|
+
}
|
|
751
|
+
let ratioStr = "";
|
|
752
|
+
let promptCount = "";
|
|
753
|
+
const note = await readNote(fullSha);
|
|
754
|
+
if (note) {
|
|
755
|
+
const entry = note;
|
|
756
|
+
ratioStr = `${entry.ai_ratio}%`;
|
|
757
|
+
promptCount = `${entry.interactions?.length ?? entry.prompts?.length ?? 0}p`;
|
|
758
|
+
}
|
|
759
|
+
if (ratioStr) {
|
|
760
|
+
console.log(`${commitPart} [${sid.slice(0, 8)}\u2026 | \u{1F916}${ratioStr} | ${promptCount}]`);
|
|
761
|
+
} else {
|
|
762
|
+
console.log(`${commitPart} [${sid.slice(0, 8)}\u2026]`);
|
|
763
|
+
}
|
|
764
|
+
}
|
|
806
765
|
}
|
|
807
766
|
|
|
808
767
|
// src/commands/pr.ts
|
|
@@ -813,12 +772,7 @@ var MARKER_BEGIN = "<!-- agentnote-begin -->";
|
|
|
813
772
|
var MARKER_END = "<!-- agentnote-end -->";
|
|
814
773
|
async function collectReport(base) {
|
|
815
774
|
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
|
-
]);
|
|
775
|
+
const raw = await git(["log", "--reverse", "--format=%H %h %s", `${base}..HEAD`]);
|
|
822
776
|
if (!raw.trim()) return null;
|
|
823
777
|
const commits = [];
|
|
824
778
|
for (const line of raw.trim().split("\n")) {
|
|
@@ -908,7 +862,7 @@ function renderMarkdown(report) {
|
|
|
908
862
|
for (const { prompt, response } of c.interactions) {
|
|
909
863
|
lines.push(`> **Prompt:** ${prompt}`);
|
|
910
864
|
if (response) {
|
|
911
|
-
const truncated = response.length >
|
|
865
|
+
const truncated = response.length > TRUNCATE_RESPONSE_PR ? `${response.slice(0, TRUNCATE_RESPONSE_PR)}\u2026` : response;
|
|
912
866
|
lines.push(">");
|
|
913
867
|
lines.push(`> **Response:** ${truncated.split("\n").join("\n> ")}`);
|
|
914
868
|
}
|
|
@@ -944,7 +898,9 @@ function renderChat(report) {
|
|
|
944
898
|
continue;
|
|
945
899
|
}
|
|
946
900
|
lines.push(`<details>`);
|
|
947
|
-
lines.push(
|
|
901
|
+
lines.push(
|
|
902
|
+
`<summary><code>${c.short}</code> ${c.message}${summaryExtra}${summaryFiles}</summary>`
|
|
903
|
+
);
|
|
948
904
|
lines.push("");
|
|
949
905
|
for (const { prompt, response } of c.interactions) {
|
|
950
906
|
lines.push(`> **\u{1F9D1} Prompt**`);
|
|
@@ -953,7 +909,7 @@ function renderChat(report) {
|
|
|
953
909
|
if (response) {
|
|
954
910
|
lines.push(`**\u{1F916} Response**`);
|
|
955
911
|
lines.push("");
|
|
956
|
-
const truncated = response.length >
|
|
912
|
+
const truncated = response.length > TRUNCATE_RESPONSE_CHAT ? `${response.slice(0, TRUNCATE_RESPONSE_CHAT)}\u2026` : response;
|
|
957
913
|
lines.push(truncated);
|
|
958
914
|
lines.push("");
|
|
959
915
|
}
|
|
@@ -1007,27 +963,23 @@ function upsertInDescription(existingBody, section) {
|
|
|
1007
963
|
if (existingBody.includes(MARKER_BEGIN)) {
|
|
1008
964
|
const before = existingBody.slice(0, existingBody.indexOf(MARKER_BEGIN));
|
|
1009
965
|
const after = existingBody.includes(MARKER_END) ? existingBody.slice(existingBody.indexOf(MARKER_END) + MARKER_END.length) : "";
|
|
1010
|
-
return before.trimEnd()
|
|
966
|
+
return `${before.trimEnd()}
|
|
967
|
+
|
|
968
|
+
${marked}${after}`;
|
|
1011
969
|
}
|
|
1012
|
-
return existingBody.trimEnd()
|
|
970
|
+
return `${existingBody.trimEnd()}
|
|
971
|
+
|
|
972
|
+
${marked}`;
|
|
1013
973
|
}
|
|
1014
974
|
async function updatePrDescription(prNumber, section) {
|
|
1015
|
-
const { stdout: bodyJson } = await execFileAsync2(
|
|
1016
|
-
"
|
|
1017
|
-
"view",
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
"body"
|
|
1021
|
-
], { encoding: "utf-8" });
|
|
975
|
+
const { stdout: bodyJson } = await execFileAsync2(
|
|
976
|
+
"gh",
|
|
977
|
+
["pr", "view", prNumber, "--json", "body"],
|
|
978
|
+
{ encoding: "utf-8" }
|
|
979
|
+
);
|
|
1022
980
|
const currentBody = JSON.parse(bodyJson).body ?? "";
|
|
1023
981
|
const newBody = upsertInDescription(currentBody, section);
|
|
1024
|
-
await execFileAsync2("gh", [
|
|
1025
|
-
"pr",
|
|
1026
|
-
"edit",
|
|
1027
|
-
prNumber,
|
|
1028
|
-
"--body",
|
|
1029
|
-
newBody
|
|
1030
|
-
], { encoding: "utf-8" });
|
|
982
|
+
await execFileAsync2("gh", ["pr", "edit", prNumber, "--body", newBody], { encoding: "utf-8" });
|
|
1031
983
|
}
|
|
1032
984
|
async function pr(args2) {
|
|
1033
985
|
const isJson = args2.includes("--json");
|
|
@@ -1040,9 +992,7 @@ async function pr(args2) {
|
|
|
1040
992
|
);
|
|
1041
993
|
const base = positional[0] ?? await detectBaseBranch();
|
|
1042
994
|
if (!base) {
|
|
1043
|
-
console.error(
|
|
1044
|
-
"error: could not detect base branch. pass it as argument: agentnote pr <base>"
|
|
1045
|
-
);
|
|
995
|
+
console.error("error: could not detect base branch. pass it as argument: agentnote pr <base>");
|
|
1046
996
|
process.exit(1);
|
|
1047
997
|
}
|
|
1048
998
|
const report = await collectReport(base);
|
|
@@ -1050,7 +1000,7 @@ async function pr(args2) {
|
|
|
1050
1000
|
if (isJson) {
|
|
1051
1001
|
console.log(JSON.stringify({ error: "no commits found" }));
|
|
1052
1002
|
} else {
|
|
1053
|
-
console.log(
|
|
1003
|
+
console.log(`no commits found between HEAD and ${base}`);
|
|
1054
1004
|
}
|
|
1055
1005
|
return;
|
|
1056
1006
|
}
|
|
@@ -1070,7 +1020,7 @@ async function pr(args2) {
|
|
|
1070
1020
|
}
|
|
1071
1021
|
}
|
|
1072
1022
|
function renderBar(ratio) {
|
|
1073
|
-
const width =
|
|
1023
|
+
const width = BAR_WIDTH_COMPACT;
|
|
1074
1024
|
const filled = Math.round(ratio / 100 * width);
|
|
1075
1025
|
return "\u2588".repeat(filled) + "\u2591".repeat(width - filled);
|
|
1076
1026
|
}
|
|
@@ -1079,20 +1029,190 @@ function basename(path) {
|
|
|
1079
1029
|
}
|
|
1080
1030
|
async function detectBaseBranch() {
|
|
1081
1031
|
for (const name of ["main", "master", "develop"]) {
|
|
1082
|
-
const { exitCode } = await gitSafe([
|
|
1083
|
-
"rev-parse",
|
|
1084
|
-
"--verify",
|
|
1085
|
-
`origin/${name}`
|
|
1086
|
-
]);
|
|
1032
|
+
const { exitCode } = await gitSafe(["rev-parse", "--verify", `origin/${name}`]);
|
|
1087
1033
|
if (exitCode === 0) return `origin/${name}`;
|
|
1088
1034
|
}
|
|
1089
1035
|
return null;
|
|
1090
1036
|
}
|
|
1091
1037
|
|
|
1038
|
+
// src/commands/session.ts
|
|
1039
|
+
async function session(sessionId) {
|
|
1040
|
+
if (!sessionId) {
|
|
1041
|
+
console.error("usage: agentnote session <session-id>");
|
|
1042
|
+
process.exit(1);
|
|
1043
|
+
}
|
|
1044
|
+
const raw = await git([
|
|
1045
|
+
"log",
|
|
1046
|
+
"--all",
|
|
1047
|
+
`--max-count=${MAX_COMMITS}`,
|
|
1048
|
+
`--format=%H %h %s %(trailers:key=${TRAILER_KEY},valueonly)`
|
|
1049
|
+
]);
|
|
1050
|
+
if (!raw) {
|
|
1051
|
+
console.log("no commits found");
|
|
1052
|
+
return;
|
|
1053
|
+
}
|
|
1054
|
+
const matches = [];
|
|
1055
|
+
for (const line of raw.split("\n")) {
|
|
1056
|
+
if (!line.trim()) continue;
|
|
1057
|
+
const parts = line.split(" ");
|
|
1058
|
+
const fullSha = parts[0];
|
|
1059
|
+
const shortInfo = parts[1];
|
|
1060
|
+
const trailer = parts[2]?.trim();
|
|
1061
|
+
if (!fullSha || !shortInfo) continue;
|
|
1062
|
+
if (trailer === sessionId) {
|
|
1063
|
+
const note = await readNote(fullSha);
|
|
1064
|
+
const entry = note;
|
|
1065
|
+
matches.push({ sha: fullSha, shortInfo, entry });
|
|
1066
|
+
continue;
|
|
1067
|
+
}
|
|
1068
|
+
if (!trailer) {
|
|
1069
|
+
const note = await readNote(fullSha);
|
|
1070
|
+
if (note && note.session_id === sessionId) {
|
|
1071
|
+
const entry = note;
|
|
1072
|
+
matches.push({ sha: fullSha, shortInfo, entry });
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
if (matches.length === 0) {
|
|
1077
|
+
console.log(`no commits found for session ${sessionId}`);
|
|
1078
|
+
return;
|
|
1079
|
+
}
|
|
1080
|
+
matches.reverse();
|
|
1081
|
+
console.log(`Session: ${sessionId}`);
|
|
1082
|
+
console.log(`Commits: ${matches.length}`);
|
|
1083
|
+
console.log();
|
|
1084
|
+
let totalPrompts = 0;
|
|
1085
|
+
let totalRatio = 0;
|
|
1086
|
+
let ratioCount = 0;
|
|
1087
|
+
for (const m of matches) {
|
|
1088
|
+
let suffix = "";
|
|
1089
|
+
if (m.entry) {
|
|
1090
|
+
const promptCount = m.entry.interactions?.length ?? m.entry.prompts?.length ?? 0;
|
|
1091
|
+
totalPrompts += promptCount;
|
|
1092
|
+
totalRatio += m.entry.ai_ratio;
|
|
1093
|
+
ratioCount++;
|
|
1094
|
+
suffix = ` [\u{1F916}${m.entry.ai_ratio}% | ${promptCount}p]`;
|
|
1095
|
+
}
|
|
1096
|
+
console.log(`${m.shortInfo}${suffix}`);
|
|
1097
|
+
}
|
|
1098
|
+
console.log();
|
|
1099
|
+
if (ratioCount > 0) {
|
|
1100
|
+
const avgRatio = Math.round(totalRatio / ratioCount);
|
|
1101
|
+
console.log(`Total: ${totalPrompts} prompts, avg AI ratio ${avgRatio}%`);
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
// src/commands/show.ts
|
|
1106
|
+
import { stat } from "node:fs/promises";
|
|
1107
|
+
async function show(commitRef) {
|
|
1108
|
+
const ref = commitRef ?? "HEAD";
|
|
1109
|
+
const commitInfo = await git(["log", "-1", "--format=%h %s", ref]);
|
|
1110
|
+
const commitSha = await git(["log", "-1", "--format=%H", ref]);
|
|
1111
|
+
const sessionId = (await git(["log", "-1", `--format=%(trailers:key=${TRAILER_KEY},valueonly)`, ref])).trim();
|
|
1112
|
+
console.log(`commit: ${commitInfo}`);
|
|
1113
|
+
if (!sessionId) {
|
|
1114
|
+
console.log("session: none (no agentnote data)");
|
|
1115
|
+
return;
|
|
1116
|
+
}
|
|
1117
|
+
console.log(`session: ${sessionId}`);
|
|
1118
|
+
const raw = await readNote(commitSha);
|
|
1119
|
+
const entry = raw;
|
|
1120
|
+
if (entry) {
|
|
1121
|
+
console.log();
|
|
1122
|
+
const ratioBar = renderRatioBar(entry.ai_ratio);
|
|
1123
|
+
console.log(`ai: ${entry.ai_ratio}% ${ratioBar}`);
|
|
1124
|
+
console.log(
|
|
1125
|
+
`files: ${entry.files_in_commit.length} changed, ${entry.files_by_ai.length} by AI`
|
|
1126
|
+
);
|
|
1127
|
+
if (entry.files_in_commit.length > 0) {
|
|
1128
|
+
console.log();
|
|
1129
|
+
for (const file of entry.files_in_commit) {
|
|
1130
|
+
const isAi = entry.files_by_ai.includes(file);
|
|
1131
|
+
const marker = isAi ? " \u{1F916}" : " \u{1F464}";
|
|
1132
|
+
console.log(` ${file}${marker}`);
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
const legacy = entry;
|
|
1136
|
+
const interactions = entry.interactions ?? (legacy.prompts ?? []).map((p) => ({
|
|
1137
|
+
prompt: p,
|
|
1138
|
+
response: null
|
|
1139
|
+
}));
|
|
1140
|
+
if (interactions.length > 0) {
|
|
1141
|
+
console.log();
|
|
1142
|
+
console.log(`prompts: ${interactions.length}`);
|
|
1143
|
+
for (let i = 0; i < interactions.length; i++) {
|
|
1144
|
+
const interaction = interactions[i];
|
|
1145
|
+
console.log();
|
|
1146
|
+
console.log(` ${i + 1}. ${truncateLines(interaction.prompt, TRUNCATE_PROMPT)}`);
|
|
1147
|
+
if (interaction.response) {
|
|
1148
|
+
console.log(` \u2192 ${truncateLines(interaction.response, TRUNCATE_RESPONSE_SHOW)}`);
|
|
1149
|
+
}
|
|
1150
|
+
if (interaction.files_touched && interaction.files_touched.length > 0) {
|
|
1151
|
+
for (const file of interaction.files_touched) {
|
|
1152
|
+
console.log(` \u{1F4C4} ${file}`);
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
} else {
|
|
1158
|
+
console.log("entry: no agentnote note found for this commit");
|
|
1159
|
+
}
|
|
1160
|
+
const adapter = claudeCode;
|
|
1161
|
+
const transcriptPath = adapter.findTranscript(sessionId);
|
|
1162
|
+
if (transcriptPath) {
|
|
1163
|
+
console.log();
|
|
1164
|
+
const stats = await stat(transcriptPath);
|
|
1165
|
+
const sizeKb = (stats.size / 1024).toFixed(1);
|
|
1166
|
+
console.log(`transcript: ${transcriptPath} (${sizeKb} KB)`);
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
function renderRatioBar(ratio) {
|
|
1170
|
+
const width = BAR_WIDTH_FULL;
|
|
1171
|
+
const filled = Math.round(ratio / 100 * width);
|
|
1172
|
+
const empty = width - filled;
|
|
1173
|
+
return `[${"\u2588".repeat(filled)}${"\u2591".repeat(empty)}]`;
|
|
1174
|
+
}
|
|
1175
|
+
function truncateLines(text, maxLen) {
|
|
1176
|
+
const firstLine = text.split("\n")[0];
|
|
1177
|
+
if (firstLine.length <= maxLen) return firstLine;
|
|
1178
|
+
return `${firstLine.slice(0, maxLen)}\u2026`;
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
// src/commands/status.ts
|
|
1182
|
+
import { existsSync as existsSync8 } from "node:fs";
|
|
1183
|
+
import { readFile as readFile6 } from "node:fs/promises";
|
|
1184
|
+
var VERSION = "0.1.6";
|
|
1185
|
+
async function status() {
|
|
1186
|
+
console.log(`agentnote v${VERSION}`);
|
|
1187
|
+
console.log();
|
|
1188
|
+
const repoRoot2 = await root();
|
|
1189
|
+
const adapter = claudeCode;
|
|
1190
|
+
const hooksActive = await adapter.isEnabled(repoRoot2);
|
|
1191
|
+
if (hooksActive) {
|
|
1192
|
+
console.log("hooks: active");
|
|
1193
|
+
} else {
|
|
1194
|
+
console.log("hooks: not configured (run 'agentnote init')");
|
|
1195
|
+
}
|
|
1196
|
+
const sessionPath = await sessionFile();
|
|
1197
|
+
if (existsSync8(sessionPath)) {
|
|
1198
|
+
const sid = (await readFile6(sessionPath, "utf-8")).trim();
|
|
1199
|
+
console.log(`session: ${sid.slice(0, 8)}\u2026`);
|
|
1200
|
+
} else {
|
|
1201
|
+
console.log("session: none");
|
|
1202
|
+
}
|
|
1203
|
+
const { stdout } = await gitSafe([
|
|
1204
|
+
"log",
|
|
1205
|
+
"-20",
|
|
1206
|
+
`--format=%(trailers:key=${TRAILER_KEY},valueonly)`
|
|
1207
|
+
]);
|
|
1208
|
+
const linked = stdout.split("\n").filter((line) => line.trim().length > 0).length;
|
|
1209
|
+
console.log(`linked: ${linked}/20 recent commits`);
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1092
1212
|
// src/cli.ts
|
|
1093
|
-
var VERSION2 = "0.1.
|
|
1213
|
+
var VERSION2 = "0.1.6";
|
|
1094
1214
|
var HELP = `
|
|
1095
|
-
agentnote \u2014 remember why your code changed
|
|
1215
|
+
agentnote v${VERSION2} \u2014 remember why your code changed
|
|
1096
1216
|
|
|
1097
1217
|
usage:
|
|
1098
1218
|
agentnote init set up hooks, workflow, and notes auto-fetch
|
|
@@ -1123,6 +1243,9 @@ switch (command) {
|
|
|
1123
1243
|
case "status":
|
|
1124
1244
|
await status();
|
|
1125
1245
|
break;
|
|
1246
|
+
case "session":
|
|
1247
|
+
await session(args[0]);
|
|
1248
|
+
break;
|
|
1126
1249
|
case "hook":
|
|
1127
1250
|
await hook();
|
|
1128
1251
|
break;
|