@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.
Files changed (2) hide show
  1. package/dist/cli.js +629 -506
  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,53 +321,144 @@ async function readNote(commitSha) {
396
321
  }
397
322
  }
398
323
 
399
- // src/core/entry.ts
400
- var SCHEMA_VERSION = 1;
401
- function calcAiRatio(commitFiles, aiFiles) {
402
- if (commitFiles.length === 0) return 0;
403
- const aiSet = new Set(aiFiles);
404
- const matched = commitFiles.filter((f) => aiSet.has(f));
405
- 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 };
406
383
  }
407
- function buildEntry(opts) {
408
- return {
409
- v: SCHEMA_VERSION,
410
- session_id: opts.sessionId,
411
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
412
- interactions: opts.interactions,
413
- files_in_commit: opts.commitFiles,
414
- files_by_ai: opts.aiFiles,
415
- ai_ratio: calcAiRatio(opts.commitFiles, opts.aiFiles)
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/core/rotate.ts
420
- import { rename } from "node:fs/promises";
421
- import { existsSync as existsSync4 } from "node:fs";
422
- import { join as join4 } from "node:path";
423
- async function rotateLogs(sessionDir, commitSha, fileNames = ["prompts.jsonl", "changes.jsonl"]) {
424
- for (const name of fileNames) {
425
- const src = join4(sessionDir, name);
426
- if (existsSync4(src)) {
427
- const base = name.replace(".jsonl", "");
428
- await rename(
429
- src,
430
- join4(sessionDir, `${base}-${commitSha.slice(0, 8)}.jsonl`)
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 (existsSync5(sf)) {
441
- sessionId = (await readFile3(sf, "utf-8")).trim();
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", `Agentnote-Session: ${sessionId}`);
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 sessionDir = join5(agentnoteDirPath, "sessions", sessionId);
462
- const commitSha = await git(["rev-parse", "HEAD"]);
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/show.ts
514
- import { stat } from "node:fs/promises";
515
- async function show(commitRef) {
516
- const ref = commitRef ?? "HEAD";
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/commands/log.ts
591
- async function log(count = 10) {
592
- const raw = await git([
593
- "log",
594
- `-${count}`,
595
- "--format=%H %h %s %(trailers:key=Agentnote-Session,valueonly)"
596
- ]);
597
- if (!raw) {
598
- console.log("no commits found");
599
- return;
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
- // src/commands/status.ts
631
- import { readFile as readFile4 } from "node:fs/promises";
632
- import { existsSync as existsSync6 } from "node:fs";
633
- var VERSION = "0.1.0";
634
- async function status() {
635
- console.log(`agentnote v${VERSION}`);
636
- console.log();
637
- const repoRoot2 = await root();
638
- const adapter = claudeCode;
639
- const hooksActive = await adapter.isEnabled(repoRoot2);
640
- if (hooksActive) {
641
- console.log("hooks: active");
642
- } else {
643
- console.log("hooks: not configured (run 'agentnote init')");
644
- }
645
- const sessionPath = await sessionFile();
646
- if (existsSync6(sessionPath)) {
647
- const sid = (await readFile4(sessionPath, "utf-8")).trim();
648
- console.log(`session: ${sid.slice(0, 8)}\u2026`);
649
- } else {
650
- console.log("session: none");
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 = join6(agentnoteDirPath, "sessions", event.sessionId);
687
- await mkdir3(sessionDir, { recursive: true });
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 writeFile3(join6(agentnoteDirPath, "session"), event.sessionId);
540
+ await writeFile2(join5(agentnoteDirPath, SESSION_FILE), event.sessionId);
691
541
  if (event.transcriptPath) {
692
- await writeFile3(join6(sessionDir, "transcript_path"), event.transcriptPath);
542
+ await writeFile2(join5(sessionDir, TRANSCRIPT_PATH_FILE), event.transcriptPath);
693
543
  }
694
- await appendJsonl(join6(sessionDir, "events.jsonl"), {
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 writeFile3(join6(agentnoteDirPath, "session"), event.sessionId);
553
+ await writeFile2(join5(agentnoteDirPath, SESSION_FILE), event.sessionId);
704
554
  if (event.transcriptPath) {
705
- await writeFile3(join6(sessionDir, "transcript_path"), event.transcriptPath);
555
+ await writeFile2(join5(sessionDir, TRANSCRIPT_PATH_FILE), event.transcriptPath);
706
556
  }
707
- await appendJsonl(join6(sessionDir, "events.jsonl"), {
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
- 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), {
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 = "/private" + 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
- 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), {
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("Agentnote-Session") && event.sessionId) {
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 'Agentnote-Session: ${event.sessionId}'`
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 recordEntry(agentnoteDirPath, event.sessionId, event.transcriptPath);
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
- async function recordEntry(agentnoteDirPath, sessionId, eventTranscriptPath) {
773
- const sessionDir = join6(agentnoteDirPath, "sessions", sessionId);
774
- const commitSha = await git(["rev-parse", "HEAD"]);
775
- let commitFiles = [];
776
- try {
777
- const raw = await git(["diff-tree", "--no-commit-id", "--name-only", "-r", "HEAD"]);
778
- commitFiles = raw.split("\n").filter(Boolean);
779
- } 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
+ }
780
686
  }
781
- const aiFiles = await readJsonlField(join6(sessionDir, "changes.jsonl"), "file");
782
- const prompts = await readJsonlField(join6(sessionDir, "prompts.jsonl"), "prompt");
783
- const transcriptPath = eventTranscriptPath ?? await readSavedTranscriptPath(sessionDir);
784
- const adapter = claudeCode;
785
- let interactions;
786
- if (transcriptPath) {
787
- const allInteractions = await adapter.extractInteractions(transcriptPath);
788
- interactions = prompts.length > 0 && allInteractions.length > 0 ? allInteractions.slice(-prompts.length) : prompts.map((p) => ({ prompt: p, response: null }));
789
- } else {
790
- 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
+ }
791
697
  }
792
- const entry = buildEntry({
793
- sessionId,
794
- interactions,
795
- commitFiles,
796
- aiFiles
797
- });
798
- await writeNote(commitSha, entry);
799
- 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("");
800
727
  }
801
- async function readSavedTranscriptPath(sessionDir) {
802
- const saved = join6(sessionDir, "transcript_path");
803
- if (!existsSync7(saved)) return null;
804
- const p = (await readFile5(saved, "utf-8")).trim();
805
- 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
+ }
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 > 500 ? response.slice(0, 500) + "\u2026" : response;
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(`<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
+ );
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 > 800 ? response.slice(0, 800) + "\u2026" : response;
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() + "\n\n" + marked + after;
966
+ return `${before.trimEnd()}
967
+
968
+ ${marked}${after}`;
1011
969
  }
1012
- return existingBody.trimEnd() + "\n\n" + marked;
970
+ return `${existingBody.trimEnd()}
971
+
972
+ ${marked}`;
1013
973
  }
1014
974
  async function updatePrDescription(prNumber, section) {
1015
- const { stdout: bodyJson } = await execFileAsync2("gh", [
1016
- "pr",
1017
- "view",
1018
- prNumber,
1019
- "--json",
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("no commits found between HEAD and " + base);
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 = 5;
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.0";
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;