@wasabeef/agentnote 0.1.4 → 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.
Files changed (2) hide show
  1. package/dist/cli.js +628 -513
  2. 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/init.ts
4
- import { mkdir as mkdir2, writeFile as writeFile2 } from "node:fs/promises";
5
- import { existsSync as existsSync2 } from "node:fs";
6
- import { join as join3 } from "node:path";
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/git.ts
12
- import { execFile } from "node:child_process";
13
- import { promisify } from "node:util";
14
- var execFileAsync = promisify(execFile);
15
- async function git(args2, options) {
16
- const { stdout } = await execFileAsync("git", args2, {
17
- cwd: options?.cwd,
18
- encoding: "utf-8"
19
- });
20
- return stdout.trim();
21
- }
22
- async function gitSafe(args2, options) {
23
- try {
24
- const stdout = await git(args2, options);
25
- return { stdout, exitCode: 0 };
26
- } catch (err) {
27
- return { stdout: err.stdout?.trim() ?? "", exitCode: err.code ?? 1 };
28
- }
29
- }
30
- async function repoRoot() {
31
- return git(["rev-parse", "--show-toplevel"]);
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/paths.ts
35
- var _root = null;
36
- var _gitDir = null;
37
- async function root() {
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 { join as join2 } from "node:path";
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
- { hooks: [{ type: "command", command: HOOK_COMMAND, async: true }] }
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 = join2(homedir(), ".claude");
64
+ const claudeBase = join(homedir(), ".claude");
99
65
  return p.startsWith(claudeBase);
100
66
  }
101
67
  function isGitCommit(cmd) {
102
- const trimmed = cmd.trim();
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 = join2(repoRoot2, this.settingsRelPath);
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")) return;
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) + "\n");
93
+ await writeFile(settingsPath, `${JSON.stringify(settings, null, 2)}
94
+ `);
128
95
  },
129
96
  async removeHooks(repoRoot2) {
130
- const settingsPath = join2(repoRoot2, this.settingsRelPath);
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) + "\n");
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 = join2(repoRoot2, this.settingsRelPath);
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 { kind: "session_start", sessionId: sid, timestamp: ts, model: e.model, transcriptPath: tp };
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 { kind: "file_change", sessionId: sid, timestamp: ts, tool, file: e.tool_input.file_path };
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 = join2(homedir(), ".claude", "projects");
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 = join2(claudeDir, project, "sessions");
182
+ const sessionsDir = join(claudeDir, project, "sessions");
203
183
  if (!existsSync(sessionsDir)) continue;
204
- const candidate = join2(sessionsDir, `${sessionId}.jsonl`);
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/commands/init.ts
257
- var WORKFLOW_TEMPLATE = `name: Agent Note
258
- on:
259
- pull_request:
260
- types: [opened, synchronize]
261
- concurrency:
262
- group: agentnote-\${{ github.event.pull_request.number }}
263
- cancel-in-progress: true
264
- permissions:
265
- contents: read
266
- pull-requests: write
267
- jobs:
268
- report:
269
- runs-on: ubuntu-latest
270
- timeout-minutes: 5
271
- steps:
272
- - uses: actions/checkout@v4
273
- with:
274
- fetch-depth: 0
275
- - uses: wasabeef/agentnote@v0
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
- console.log("");
258
+ }
259
+ async function repoRoot() {
260
+ return git(["rev-parse", "--show-toplevel"]);
344
261
  }
345
262
 
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";
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 { readFile as readFile2, appendFile } from "node:fs/promises";
354
- import { existsSync as existsSync3 } from "node:fs";
355
- async function readJsonlField(filePath, field) {
356
- if (!existsSync3(filePath)) return [];
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 seen = /* @__PURE__ */ new Set();
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
- const entry = JSON.parse(line);
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 values;
302
+ return entries;
373
303
  }
374
304
  async function appendJsonl(filePath, data) {
375
- await appendFile(filePath, JSON.stringify(data) + "\n");
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,61 +321,144 @@ async function readNote(commitSha) {
396
321
  }
397
322
  }
398
323
 
399
- // src/core/entry.ts
400
- var SCHEMA_VERSION = 1;
401
- var RESPONSE_MAX_LENGTH = 2e3;
402
- function calcAiRatio(commitFiles, aiFiles) {
403
- if (commitFiles.length === 0) return 0;
404
- const aiSet = new Set(aiFiles);
405
- const matched = commitFiles.filter((f) => aiSet.has(f));
406
- return Math.round(matched.length / commitFiles.length * 100);
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 };
407
383
  }
408
- function truncate(text, maxLen) {
409
- if (text.length <= maxLen) return text;
410
- return text.slice(0, maxLen) + "\u2026";
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
+ }
411
402
  }
412
- function buildEntry(opts) {
413
- return {
414
- v: SCHEMA_VERSION,
415
- session_id: opts.sessionId,
416
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
417
- interactions: opts.interactions.map((i) => ({
418
- prompt: i.prompt,
419
- response: i.response ? truncate(i.response, RESPONSE_MAX_LENGTH) : null
420
- })),
421
- files_in_commit: opts.commitFiles,
422
- files_by_ai: opts.aiFiles,
423
- ai_ratio: calcAiRatio(opts.commitFiles, opts.aiFiles)
424
- };
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;
425
419
  }
426
420
 
427
- // src/core/rotate.ts
428
- import { rename } from "node:fs/promises";
429
- import { existsSync as existsSync4 } from "node:fs";
430
- import { join as join4 } from "node:path";
431
- async function rotateLogs(sessionDir, commitSha, fileNames = ["prompts.jsonl", "changes.jsonl"]) {
432
- for (const name of fileNames) {
433
- const src = join4(sessionDir, name);
434
- if (existsSync4(src)) {
435
- const base = name.replace(".jsonl", "");
436
- await rename(
437
- src,
438
- join4(sessionDir, `${base}-${commitSha.slice(0, 8)}.jsonl`)
439
- );
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);
440
441
  }
441
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);
442
450
  }
443
451
 
444
452
  // src/commands/commit.ts
445
453
  async function commit(args2) {
446
454
  const sf = await sessionFile();
447
455
  let sessionId = "";
448
- if (existsSync5(sf)) {
449
- sessionId = (await readFile3(sf, "utf-8")).trim();
456
+ if (existsSync4(sf)) {
457
+ sessionId = (await readFile4(sf, "utf-8")).trim();
450
458
  }
451
459
  const gitArgs = ["commit"];
452
460
  if (sessionId) {
453
- gitArgs.push("--trailer", `Agentnote-Session: ${sessionId}`);
461
+ gitArgs.push("--trailer", `${TRAILER_KEY}: ${sessionId}`);
454
462
  }
455
463
  gitArgs.push(...args2);
456
464
  const child = spawn("git", gitArgs, {
@@ -466,210 +474,44 @@ async function commit(args2) {
466
474
  if (sessionId) {
467
475
  try {
468
476
  const agentnoteDirPath = await agentnoteDir();
469
- const sessionDir = join5(agentnoteDirPath, "sessions", sessionId);
470
- const commitSha = await git(["rev-parse", "HEAD"]);
471
- let commitFiles = [];
472
- try {
473
- const raw = await git([
474
- "diff-tree",
475
- "--no-commit-id",
476
- "--name-only",
477
- "-r",
478
- "HEAD"
479
- ]);
480
- commitFiles = raw.split("\n").filter(Boolean);
481
- } catch {
482
- }
483
- const aiFiles = await readJsonlField(
484
- join5(sessionDir, "changes.jsonl"),
485
- "file"
486
- );
487
- const prompts = await readJsonlField(
488
- join5(sessionDir, "prompts.jsonl"),
489
- "prompt"
490
- );
491
- let interactions;
492
- const transcriptPathFile = join5(sessionDir, "transcript_path");
493
- if (existsSync5(transcriptPathFile)) {
494
- const transcriptPath = (await readFile3(transcriptPathFile, "utf-8")).trim();
495
- if (transcriptPath) {
496
- const allInteractions = await claudeCode.extractInteractions(transcriptPath);
497
- interactions = prompts.length > 0 && allInteractions.length > 0 ? allInteractions.slice(-prompts.length) : prompts.map((p) => ({ prompt: p, response: null }));
498
- } else {
499
- interactions = prompts.map((p) => ({ prompt: p, response: null }));
500
- }
501
- } else {
502
- interactions = prompts.map((p) => ({ prompt: p, response: null }));
503
- }
504
- const entry = buildEntry({
505
- sessionId,
506
- interactions,
507
- commitFiles,
508
- aiFiles
509
- });
510
- await writeNote(commitSha, entry);
511
- await rotateLogs(sessionDir, commitSha);
512
- console.log(
513
- `agentnote: ${interactions.length} prompts, AI ratio ${entry.ai_ratio}%`
514
- );
477
+ const result = await recordCommitEntry({ agentnoteDirPath, sessionId });
478
+ console.log(`agentnote: ${result.promptCount} prompts, AI ratio ${result.aiRatio}%`);
515
479
  } catch (err) {
516
480
  console.error(`agentnote: warning: ${err.message}`);
517
481
  }
518
482
  }
519
483
  }
520
484
 
521
- // src/commands/show.ts
522
- import { stat } from "node:fs/promises";
523
- async function show(commitRef) {
524
- const ref = commitRef ?? "HEAD";
525
- const commitInfo = await git(["log", "-1", "--format=%h %s", ref]);
526
- const commitSha = await git(["log", "-1", "--format=%H", ref]);
527
- const sessionId = (await git([
528
- "log",
529
- "-1",
530
- "--format=%(trailers:key=Agentnote-Session,valueonly)",
531
- ref
532
- ])).trim();
533
- console.log(`commit: ${commitInfo}`);
534
- if (!sessionId) {
535
- console.log("session: none (no agentnote data)");
536
- return;
537
- }
538
- console.log(`session: ${sessionId}`);
539
- const raw = await readNote(commitSha);
540
- const entry = raw;
541
- if (entry) {
542
- console.log();
543
- const ratioBar = renderRatioBar(entry.ai_ratio);
544
- console.log(`ai: ${entry.ai_ratio}% ${ratioBar}`);
545
- console.log(
546
- `files: ${entry.files_in_commit.length} changed, ${entry.files_by_ai.length} by AI`
547
- );
548
- if (entry.files_in_commit.length > 0) {
549
- console.log();
550
- for (const file of entry.files_in_commit) {
551
- const isAi = entry.files_by_ai.includes(file);
552
- const marker = isAi ? " \u{1F916}" : " \u{1F464}";
553
- console.log(` ${file}${marker}`);
554
- }
555
- }
556
- const interactions = entry.interactions ?? (entry.prompts ?? []).map((p) => ({
557
- prompt: p,
558
- response: null
559
- }));
560
- if (interactions.length > 0) {
561
- console.log();
562
- console.log(`prompts: ${interactions.length}`);
563
- for (let i = 0; i < interactions.length; i++) {
564
- const { prompt, response } = interactions[i];
565
- console.log();
566
- console.log(` ${i + 1}. ${truncateLines(prompt, 120)}`);
567
- if (response) {
568
- console.log(` \u2192 ${truncateLines(response, 200)}`);
569
- }
570
- }
571
- }
572
- } else {
573
- console.log("entry: no agentnote note found for this commit");
574
- }
575
- console.log();
576
- const adapter = claudeCode;
577
- const transcriptPath = adapter.findTranscript(sessionId);
578
- if (transcriptPath) {
579
- const stats = await stat(transcriptPath);
580
- const sizeKb = (stats.size / 1024).toFixed(1);
581
- console.log(`transcript: ${transcriptPath} (${sizeKb} KB)`);
582
- } else {
583
- console.log("transcript: not found locally");
584
- }
585
- }
586
- function renderRatioBar(ratio) {
587
- const width = 20;
588
- const filled = Math.round(ratio / 100 * width);
589
- const empty = width - filled;
590
- return `[${"\u2588".repeat(filled)}${"\u2591".repeat(empty)}]`;
591
- }
592
- function truncateLines(text, maxLen) {
593
- const firstLine = text.split("\n")[0];
594
- if (firstLine.length <= maxLen) return firstLine;
595
- return firstLine.slice(0, maxLen) + "\u2026";
596
- }
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";
597
489
 
598
- // src/commands/log.ts
599
- async function log(count = 10) {
600
- const raw = await git([
601
- "log",
602
- `-${count}`,
603
- "--format=%H %h %s %(trailers:key=Agentnote-Session,valueonly)"
604
- ]);
605
- if (!raw) {
606
- console.log("no commits found");
607
- return;
608
- }
609
- for (const line of raw.split("\n")) {
610
- if (!line.trim()) continue;
611
- const parts = line.split(" ");
612
- const fullSha = parts[0];
613
- const commitPart = parts[1];
614
- const sid = parts[2]?.trim();
615
- if (!fullSha || !commitPart) continue;
616
- if (!sid) {
617
- console.log(commitPart);
618
- continue;
619
- }
620
- let ratioStr = "";
621
- let promptCount = "";
622
- const note = await readNote(fullSha);
623
- if (note) {
624
- const entry = note;
625
- ratioStr = `${entry.ai_ratio}%`;
626
- promptCount = `${entry.interactions?.length ?? entry.prompts?.length ?? 0}p`;
627
- }
628
- if (ratioStr) {
629
- console.log(
630
- `${commitPart} [${sid.slice(0, 8)}\u2026 | \u{1F916}${ratioStr} | ${promptCount}]`
631
- );
632
- } else {
633
- 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`));
634
501
  }
635
502
  }
636
503
  }
637
-
638
- // src/commands/status.ts
639
- import { readFile as readFile4 } from "node:fs/promises";
640
- import { existsSync as existsSync6 } from "node:fs";
641
- var VERSION = "0.1.0";
642
- async function status() {
643
- console.log(`agentnote v${VERSION}`);
644
- console.log();
645
- const repoRoot2 = await root();
646
- const adapter = claudeCode;
647
- const hooksActive = await adapter.isEnabled(repoRoot2);
648
- if (hooksActive) {
649
- console.log("hooks: active");
650
- } else {
651
- console.log("hooks: not configured (run 'agentnote init')");
652
- }
653
- const sessionPath = await sessionFile();
654
- if (existsSync6(sessionPath)) {
655
- const sid = (await readFile4(sessionPath, "utf-8")).trim();
656
- console.log(`session: ${sid.slice(0, 8)}\u2026`);
657
- } else {
658
- console.log("session: none");
659
- }
660
- const { stdout } = await gitSafe([
661
- "log",
662
- "-20",
663
- "--format=%(trailers:key=Agentnote-Session,valueonly)"
664
- ]);
665
- const linked = stdout.split("\n").filter((line) => line.trim().length > 0).length;
666
- 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
+ }
667
512
  }
668
513
 
669
514
  // src/commands/hook.ts
670
- import { mkdir as mkdir3, readFile as readFile5, writeFile as writeFile3, realpath } from "node:fs/promises";
671
- import { existsSync as existsSync7 } from "node:fs";
672
- import { join as join6, relative, isAbsolute } from "node:path";
673
515
  async function readStdin() {
674
516
  const chunks = [];
675
517
  for await (const chunk of process.stdin) {
@@ -691,15 +533,15 @@ async function hook() {
691
533
  const event = adapter.parseEvent(input);
692
534
  if (!event) return;
693
535
  const agentnoteDirPath = await agentnoteDir();
694
- const sessionDir = join6(agentnoteDirPath, "sessions", event.sessionId);
695
- await mkdir3(sessionDir, { recursive: true });
536
+ const sessionDir = join5(agentnoteDirPath, SESSIONS_DIR, event.sessionId);
537
+ await mkdir2(sessionDir, { recursive: true });
696
538
  switch (event.kind) {
697
539
  case "session_start": {
698
- await writeFile3(join6(agentnoteDirPath, "session"), event.sessionId);
540
+ await writeFile2(join5(agentnoteDirPath, SESSION_FILE), event.sessionId);
699
541
  if (event.transcriptPath) {
700
- await writeFile3(join6(sessionDir, "transcript_path"), event.transcriptPath);
542
+ await writeFile2(join5(sessionDir, TRANSCRIPT_PATH_FILE), event.transcriptPath);
701
543
  }
702
- await appendJsonl(join6(sessionDir, "events.jsonl"), {
544
+ await appendJsonl(join5(sessionDir, EVENTS_FILE), {
703
545
  event: "session_start",
704
546
  session_id: event.sessionId,
705
547
  timestamp: event.timestamp,
@@ -708,11 +550,11 @@ async function hook() {
708
550
  break;
709
551
  }
710
552
  case "stop": {
711
- await writeFile3(join6(agentnoteDirPath, "session"), event.sessionId);
553
+ await writeFile2(join5(agentnoteDirPath, SESSION_FILE), event.sessionId);
712
554
  if (event.transcriptPath) {
713
- await writeFile3(join6(sessionDir, "transcript_path"), event.transcriptPath);
555
+ await writeFile2(join5(sessionDir, TRANSCRIPT_PATH_FILE), event.transcriptPath);
714
556
  }
715
- await appendJsonl(join6(sessionDir, "events.jsonl"), {
557
+ await appendJsonl(join5(sessionDir, EVENTS_FILE), {
716
558
  event: "stop",
717
559
  session_id: event.sessionId,
718
560
  timestamp: event.timestamp
@@ -720,10 +562,21 @@ async function hook() {
720
562
  break;
721
563
  }
722
564
  case "prompt": {
723
- await appendJsonl(join6(sessionDir, "prompts.jsonl"), {
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), {
724
576
  event: "prompt",
725
577
  timestamp: event.timestamp,
726
- prompt: event.prompt
578
+ prompt: event.prompt,
579
+ turn
727
580
  });
728
581
  break;
729
582
  }
@@ -735,7 +588,7 @@ async function hook() {
735
588
  const repoRoot2 = await realpath(rawRoot);
736
589
  let normalizedFile = filePath;
737
590
  if (repoRoot2.startsWith("/private") && !normalizedFile.startsWith("/private")) {
738
- normalizedFile = "/private" + normalizedFile;
591
+ normalizedFile = `/private${normalizedFile}`;
739
592
  } else if (!repoRoot2.startsWith("/private") && normalizedFile.startsWith("/private")) {
740
593
  normalizedFile = normalizedFile.replace(/^\/private/, "");
741
594
  }
@@ -743,24 +596,31 @@ async function hook() {
743
596
  } catch {
744
597
  }
745
598
  }
746
- await appendJsonl(join6(sessionDir, "changes.jsonl"), {
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), {
747
606
  event: "file_change",
748
607
  timestamp: event.timestamp,
749
608
  tool: event.tool,
750
609
  file: filePath,
751
- session_id: event.sessionId
610
+ session_id: event.sessionId,
611
+ turn
752
612
  });
753
613
  break;
754
614
  }
755
615
  case "pre_commit": {
756
616
  const cmd = event.commitCommand ?? "";
757
- if (!cmd.includes("Agentnote-Session") && event.sessionId) {
617
+ if (!cmd.includes(TRAILER_KEY) && event.sessionId) {
758
618
  process.stdout.write(
759
619
  JSON.stringify({
760
620
  hookSpecificOutput: {
761
621
  hookEventName: "PreToolUse",
762
622
  updatedInput: {
763
- command: `${cmd} --trailer 'Agentnote-Session: ${event.sessionId}'`
623
+ command: `${cmd} --trailer '${TRAILER_KEY}: ${event.sessionId}'`
764
624
  }
765
625
  }
766
626
  })
@@ -770,47 +630,138 @@ async function hook() {
770
630
  }
771
631
  case "post_commit": {
772
632
  try {
773
- await recordEntry(agentnoteDirPath, event.sessionId, event.transcriptPath);
633
+ await recordCommitEntry({
634
+ agentnoteDirPath,
635
+ sessionId: event.sessionId,
636
+ transcriptPath: event.transcriptPath
637
+ });
774
638
  } catch {
775
639
  }
776
640
  break;
777
641
  }
778
642
  }
779
643
  }
780
- async function recordEntry(agentnoteDirPath, sessionId, eventTranscriptPath) {
781
- const sessionDir = join6(agentnoteDirPath, "sessions", sessionId);
782
- const commitSha = await git(["rev-parse", "HEAD"]);
783
- let commitFiles = [];
784
- try {
785
- const raw = await git(["diff-tree", "--no-commit-id", "--name-only", "-r", "HEAD"]);
786
- commitFiles = raw.split("\n").filter(Boolean);
787
- } catch {
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
+ }
788
686
  }
789
- const aiFiles = await readJsonlField(join6(sessionDir, "changes.jsonl"), "file");
790
- const prompts = await readJsonlField(join6(sessionDir, "prompts.jsonl"), "prompt");
791
- const transcriptPath = eventTranscriptPath ?? await readSavedTranscriptPath(sessionDir);
792
- const adapter = claudeCode;
793
- let interactions;
794
- if (transcriptPath) {
795
- const allInteractions = await adapter.extractInteractions(transcriptPath);
796
- interactions = prompts.length > 0 && allInteractions.length > 0 ? allInteractions.slice(-prompts.length) : prompts.map((p) => ({ prompt: p, response: null }));
797
- } else {
798
- interactions = prompts.map((p) => ({ prompt: p, response: null }));
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
+ }
799
697
  }
800
- const entry = buildEntry({
801
- sessionId,
802
- interactions,
803
- commitFiles,
804
- aiFiles
805
- });
806
- await writeNote(commitSha, entry);
807
- await rotateLogs(sessionDir, commitSha);
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("");
808
727
  }
809
- async function readSavedTranscriptPath(sessionDir) {
810
- const saved = join6(sessionDir, "transcript_path");
811
- if (!existsSync7(saved)) return null;
812
- const p = (await readFile5(saved, "utf-8")).trim();
813
- return p || null;
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
+ }
814
765
  }
815
766
 
816
767
  // src/commands/pr.ts
@@ -821,12 +772,7 @@ var MARKER_BEGIN = "<!-- agentnote-begin -->";
821
772
  var MARKER_END = "<!-- agentnote-end -->";
822
773
  async function collectReport(base) {
823
774
  const head = await git(["rev-parse", "--short", "HEAD"]);
824
- const raw = await git([
825
- "log",
826
- "--reverse",
827
- "--format=%H %h %s",
828
- `${base}..HEAD`
829
- ]);
775
+ const raw = await git(["log", "--reverse", "--format=%H %h %s", `${base}..HEAD`]);
830
776
  if (!raw.trim()) return null;
831
777
  const commits = [];
832
778
  for (const line of raw.trim().split("\n")) {
@@ -916,7 +862,7 @@ function renderMarkdown(report) {
916
862
  for (const { prompt, response } of c.interactions) {
917
863
  lines.push(`> **Prompt:** ${prompt}`);
918
864
  if (response) {
919
- const truncated = response.length > 500 ? response.slice(0, 500) + "\u2026" : response;
865
+ const truncated = response.length > TRUNCATE_RESPONSE_PR ? `${response.slice(0, TRUNCATE_RESPONSE_PR)}\u2026` : response;
920
866
  lines.push(">");
921
867
  lines.push(`> **Response:** ${truncated.split("\n").join("\n> ")}`);
922
868
  }
@@ -952,7 +898,9 @@ function renderChat(report) {
952
898
  continue;
953
899
  }
954
900
  lines.push(`<details>`);
955
- lines.push(`<summary><code>${c.short}</code> ${c.message}${summaryExtra}${summaryFiles}</summary>`);
901
+ lines.push(
902
+ `<summary><code>${c.short}</code> ${c.message}${summaryExtra}${summaryFiles}</summary>`
903
+ );
956
904
  lines.push("");
957
905
  for (const { prompt, response } of c.interactions) {
958
906
  lines.push(`> **\u{1F9D1} Prompt**`);
@@ -961,7 +909,7 @@ function renderChat(report) {
961
909
  if (response) {
962
910
  lines.push(`**\u{1F916} Response**`);
963
911
  lines.push("");
964
- const truncated = response.length > 800 ? response.slice(0, 800) + "\u2026" : response;
912
+ const truncated = response.length > TRUNCATE_RESPONSE_CHAT ? `${response.slice(0, TRUNCATE_RESPONSE_CHAT)}\u2026` : response;
965
913
  lines.push(truncated);
966
914
  lines.push("");
967
915
  }
@@ -1015,27 +963,23 @@ function upsertInDescription(existingBody, section) {
1015
963
  if (existingBody.includes(MARKER_BEGIN)) {
1016
964
  const before = existingBody.slice(0, existingBody.indexOf(MARKER_BEGIN));
1017
965
  const after = existingBody.includes(MARKER_END) ? existingBody.slice(existingBody.indexOf(MARKER_END) + MARKER_END.length) : "";
1018
- return before.trimEnd() + "\n\n" + marked + after;
966
+ return `${before.trimEnd()}
967
+
968
+ ${marked}${after}`;
1019
969
  }
1020
- return existingBody.trimEnd() + "\n\n" + marked;
970
+ return `${existingBody.trimEnd()}
971
+
972
+ ${marked}`;
1021
973
  }
1022
974
  async function updatePrDescription(prNumber, section) {
1023
- const { stdout: bodyJson } = await execFileAsync2("gh", [
1024
- "pr",
1025
- "view",
1026
- prNumber,
1027
- "--json",
1028
- "body"
1029
- ], { encoding: "utf-8" });
975
+ const { stdout: bodyJson } = await execFileAsync2(
976
+ "gh",
977
+ ["pr", "view", prNumber, "--json", "body"],
978
+ { encoding: "utf-8" }
979
+ );
1030
980
  const currentBody = JSON.parse(bodyJson).body ?? "";
1031
981
  const newBody = upsertInDescription(currentBody, section);
1032
- await execFileAsync2("gh", [
1033
- "pr",
1034
- "edit",
1035
- prNumber,
1036
- "--body",
1037
- newBody
1038
- ], { encoding: "utf-8" });
982
+ await execFileAsync2("gh", ["pr", "edit", prNumber, "--body", newBody], { encoding: "utf-8" });
1039
983
  }
1040
984
  async function pr(args2) {
1041
985
  const isJson = args2.includes("--json");
@@ -1048,9 +992,7 @@ async function pr(args2) {
1048
992
  );
1049
993
  const base = positional[0] ?? await detectBaseBranch();
1050
994
  if (!base) {
1051
- console.error(
1052
- "error: could not detect base branch. pass it as argument: agentnote pr <base>"
1053
- );
995
+ console.error("error: could not detect base branch. pass it as argument: agentnote pr <base>");
1054
996
  process.exit(1);
1055
997
  }
1056
998
  const report = await collectReport(base);
@@ -1058,7 +1000,7 @@ async function pr(args2) {
1058
1000
  if (isJson) {
1059
1001
  console.log(JSON.stringify({ error: "no commits found" }));
1060
1002
  } else {
1061
- console.log("no commits found between HEAD and " + base);
1003
+ console.log(`no commits found between HEAD and ${base}`);
1062
1004
  }
1063
1005
  return;
1064
1006
  }
@@ -1078,7 +1020,7 @@ async function pr(args2) {
1078
1020
  }
1079
1021
  }
1080
1022
  function renderBar(ratio) {
1081
- const width = 5;
1023
+ const width = BAR_WIDTH_COMPACT;
1082
1024
  const filled = Math.round(ratio / 100 * width);
1083
1025
  return "\u2588".repeat(filled) + "\u2591".repeat(width - filled);
1084
1026
  }
@@ -1087,20 +1029,190 @@ function basename(path) {
1087
1029
  }
1088
1030
  async function detectBaseBranch() {
1089
1031
  for (const name of ["main", "master", "develop"]) {
1090
- const { exitCode } = await gitSafe([
1091
- "rev-parse",
1092
- "--verify",
1093
- `origin/${name}`
1094
- ]);
1032
+ const { exitCode } = await gitSafe(["rev-parse", "--verify", `origin/${name}`]);
1095
1033
  if (exitCode === 0) return `origin/${name}`;
1096
1034
  }
1097
1035
  return null;
1098
1036
  }
1099
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
+
1100
1212
  // src/cli.ts
1101
- var VERSION2 = "0.1.0";
1213
+ var VERSION2 = "0.1.6";
1102
1214
  var HELP = `
1103
- agentnote \u2014 remember why your code changed
1215
+ agentnote v${VERSION2} \u2014 remember why your code changed
1104
1216
 
1105
1217
  usage:
1106
1218
  agentnote init set up hooks, workflow, and notes auto-fetch
@@ -1131,6 +1243,9 @@ switch (command) {
1131
1243
  case "status":
1132
1244
  await status();
1133
1245
  break;
1246
+ case "session":
1247
+ await session(args[0]);
1248
+ break;
1134
1249
  case "hook":
1135
1250
  await hook();
1136
1251
  break;