In-terminal Stoic engineering diary pane, /diary commands, and automatic session distiller for Claude Code

Pull learnings, bugs, and fixes from AI agent sessions into a local HTML diary and an interactive Claude Code Mod pane with Marcus Aurelius style notes.
git clone https://github.com/hemanth/agent-diary.git
cd agent-diary
npm link
ln -s "$PWD" ~/.gemini/config/skills/agent-diary
ln -s "$PWD" ~/.claude/skills/agent-diary
# Scan recent sessions from Antigravity and Claude Code
npm run harvest
# Build and open diary.html
npm run open
harvest.mjs reads .jsonl transcripts for errors, follow-up questions, and fixes. build-diary.mjs compiles data/diary-entries.json into diary.html, docs/index.html, and DIARY.md.
agent-diary includes a Claude Code Mod (hooks/register.js) that draws an interactive pane inside your terminal or Claude Desktop app.
# Load from ~/.claude/skills/agent-diary or pass --plugin-dir directly
claude --plugin-dir .
# Validate and run the mod test suite
npm run validate
npm test
Inside a Claude Code session:
/diary opens the interactive Agent Diary (Ta Eis Heauton) pane with three tabs: 1: Entries, 2: Meditations, and 3: Write Note./diary-note <reflection> saves a personal note straight from the prompt into $.store and data/diary-entries.json./diary-open opens diary.html in your browser.1/2/3 switch tabs, n/p cycle entries, m draws another Stoic meditation lot, b toggles the AbovePrompt Stoic quote banner, and o opens the HTML diary.# Sync latest sessions and open diary.html in your browser
agent-diary
# Print recent sessions in the terminal
agent-diary harvest
# Sync and rebuild without opening a browser tab
agent-diary sync
node scripts/build-diary.mjs --add-json '{
"id": "entry-2026-10-05-my-project",
"date": "2026-10-05",
"project": "my-project",
"title": "Zero-copy buffers in the audio worker",
"summary": "Removed redundant ArrayBuffer copies on the hot path.",
"virtue": "Economy and Precision (Euteleia)",
"challenges": ["GC pauses hit 45ms when cloning 16MB audio frames."],
"solutions": ["Passed ownership with postMessage transferable ArrayBuffers."],
"learnings": ["Transferables move ownership in O(1) without copying bytes."],
"meditation": {
"book": "Book IV",
"verse": "Section 24",
"stoicConcept": "Apallage (Pruning the unnecessary)",
"quote": "Why copy the vessel when you can hand it across the threshold?",
"reflection": "Most latency comes from extra work we added ourselves. Pass the buffer directly."
},
"userNote": "Cut frame latency from 45ms to 0.2ms."
}' --open
agent-diary registers a Stop hook in ~/.gemini/config/hooks.json (Antigravity), ~/.claude/settings.json (Claude Code settings), and turn.complete in hooks/register.js (Claude Code Mod). When a turn ends, scripts/auto-hook.mjs pulls new sessions into data/diary-entries.json and rebuilds diary.html without overwriting your manual notes.
MIT (c) Hemanth.HM
hooks/register.js 480 lines1const PANE_ID = 'agent-diary';
2const DIARY_JSON_PATH = 'data/diary-entries.json';
3
4const FALLBACK_ENTRIES = [
5 {
6 id: 'entry-2026-10-05-audio-worklet',
7 date: '2026-10-05',
8 project: 'audio-worklet',
9 title: 'Zero-copy ring buffers in the WebAudio render quantum',
10 virtue: 'Temperance and Proportion (Sophrosyne)',
11 challenges: [
12 'The UI showed Render Complete, but the exported MP4 had no captions because the canvas overlay was not passed into the FFmpeg WASM filtergraph.',
13 'Portrait screen recordings (484x1080) rendered 54px captions that clipped horizontally.'
14 ],
15 solutions: [
16 'Mounted the subtitle file in the FFmpeg WASM pass and extracted a PNG frame after export to verify the pixels.',
17 'Scaled subtitle font size by Math.min(outW, outH) / 480 with line wrapping.'
18 ],
19 learnings: [
20 'Scale video overlay text against min(width, height) rather than height alone.',
21 'Inspect an extracted frame on disk before trusting a Render Complete badge.'
22 ],
23 meditation: {
24 book: 'Book IV',
25 verse: 'Section 19',
26 stoicConcept: 'Metron (Measure and proportion)',
27 quote: 'The herald cries that the work is finished, yet the stone has no inscription. Measure both dimensions of the column before you carve.',
28 reflection: 'Do not assume a job succeeded because a function returned without throwing. Verify the artifact on disk.'
29 },
30 userNote: 'Testing on a real 484x1080 mobile recording caught the height-only font scaling bug right away.'
31 },
32 {
33 id: 'entry-2026-10-03-sqlite-wal',
34 date: '2026-10-03',
35 project: 'sqlite-wal',
36 title: 'Preventing WAL starvation under continuous reader load',
37 virtue: 'Truthfulness (Aletheia)',
38 challenges: [
39 'Chrome SQLite visits table was empty when History Sync was off, causing the dashboard to show fake fallback numbers.'
40 ],
41 solutions: [
42 'Built a pure-Node Snappy decompressor to read Chromium Local Extension Settings LevelDB (.ldb) files directly.'
43 ],
44 learnings: [
45 'Never ship silent mock fallback data when a parser fails; surface the empty state so the root cause gets fixed.'
46 ],
47 meditation: {
48 book: 'Book III',
49 verse: 'Section 11',
50 stoicConcept: 'Hypolepsis (Testing impressions)',
51 quote: 'Strip away the painted mask and examine the thing itself: is this gold from the mine, or brass coined in your own workshop to quiet a question?',
52 reflection: 'A blank screen is honest and makes you find the real data source. A fake chart hides the bug.'
53 },
54 userNote: 'Reading the Snappy-compressed .ldb files directly gave us the exact per-domain seconds.'
55 }
56];
57
58let entries = FALLBACK_ENTRIES.slice();
59let notes = [];
60let activeTab = 'entries';
61let selectedIndex = 0;
62let meditationIndex = 0;
63let showBanner = false;
64let turnTools = 0;
65let turnObstacles = 0;
66let lastObstacleTool = '';
67let diaryJsonPath = DIARY_JSON_PATH;
68
69function applyParsedEntries(raw) {
70 try {
71 const parsed = JSON.parse(raw);
72 if (Array.isArray(parsed) && parsed.length > 0) {
73 entries = parsed;
74 if (selectedIndex >= entries.length) {
75 selectedIndex = 0;
76 }
77 }
78 } catch {
79 // Keep existing entries if JSON parsing fails.
80 }
81}
82
83function buildNoteRecord(text) {
84 const trimmed = String(text || '').trim();
85 if (!trimmed) {
86 return null;
87 }
88 const target = entries[selectedIndex] || entries[0];
89 const record = {
90 date: new Date().toISOString().slice(0, 10),
91 project: target ? target.project : 'session',
92 entryId: target ? target.id : 'custom',
93 text: trimmed
94 };
95 notes = [record, ...notes.slice(0, 19)];
96 if (target) {
97 target.userNote = trimmed;
98 }
99 return record;
100}
101
102export function register(on) {
103 on('session.start', async ($, e, next) => {
104 try {
105 const home = await $.env.get('HOME');
106 if (home) {
107 diaryJsonPath = home + '/.agent-diary/diary-entries.json';
108 }
109 } catch {
110 // Keep default relative path in sandboxed environments.
111 }
112
113 const savedNotes = await $.store.get('notes');
114 if (Array.isArray(savedNotes)) {
115 notes = savedNotes;
116 }
117 const savedBanner = await $.store.get('showBanner');
118 if (typeof savedBanner === 'boolean') {
119 showBanner = savedBanner;
120 }
121
122 try {
123 const raw = await $.fs.read(diaryJsonPath);
124 applyParsedEntries(raw);
125 } catch {
126 // Keep fallback entries in sandboxed environments.
127 }
128
129 try {
130 await $.command.register({
131 name: 'diary',
132 description: 'Open the Stoic Agent Diary pane in Claude Code',
133 immediate: true
134 });
135 await $.command.register({
136 name: 'diary-open',
137 description: 'Open the full HTML Agent Diary in your browser',
138 immediate: true
139 });
140 await $.command.register({
141 name: 'diary-note',
142 description: 'Save a personal reflection note to your Agent Diary',
143 argumentHint: '<reflection>',
144 immediate: true
145 });
146 } catch {
147 // Ignore duplicate command registration errors on reload.
148 }
149
150 return next(e);
151 });
152
153 on('command.run', { command: 'diary' }, async ($) => {
154 try {
155 const raw = await $.fs.read(diaryJsonPath);
156 applyParsedEntries(raw);
157 } catch {
158 // Keep existing entries.
159 }
160 await $.ui.open({
161 id: PANE_ID,
162 title: 'Agent Diary (Ta Eis Heauton)',
163 focus: true,
164 closeOnEscape: true
165 });
166 return {};
167 });
168
169 on('command.run', { command: 'diary-open' }, async ($) => {
170 try {
171 await $.process.run(['agent-diary', 'open']);
172 $.ui.toast('Opened diary.html in your browser');
173 } catch {
174 $.ui.toast('Run: agent-diary open');
175 }
176 return {};
177 });
178
179 on('command.run', { command: 'diary-note' }, async ($, e) => {
180 const record = buildNoteRecord(e.args);
181 if (!record) {
182 activeTab = 'write';
183 await $.ui.open({
184 id: PANE_ID,
185 title: 'Agent Diary (Ta Eis Heauton)',
186 focus: true,
187 closeOnEscape: true
188 });
189 return {};
190 }
191 await $.store.set('notes', notes);
192 try {
193 await $.fs.write(diaryJsonPath, JSON.stringify(entries, null, 2));
194 } catch {
195 // Ignore write failures in sandboxed environments.
196 }
197 $.ui.invalidate('ui.render');
198 return {
199 text: 'Saved personal reflection to Agent Diary (' + record.project + '): "' + record.text + '"'
200 };
201 });
202
203 on('tool.call', async ($, e, next) => {
204 const result = await next(e);
205 turnTools += 1;
206 if (result && result.isError) {
207 turnObstacles += 1;
208 lastObstacleTool = e.tool || 'tool';
209 }
210 $.ui.invalidate('ui.render');
211 return result;
212 });
213
214 on('turn.complete', async ($, e, next) => {
215 try {
216 await $.process.run(['agent-diary', 'sync']);
217 const raw = await $.fs.read(diaryJsonPath);
218 applyParsedEntries(raw);
219 } catch {
220 // Ignore background hook errors in sandboxed environments.
221 }
222 $.ui.invalidate('ui.render');
223 return next(e);
224 });
225
226 on('ui.render', { component: 'Spinner' }, async ($, e, next) => {
227 const suffix =
228 turnObstacles > 0
229 ? ' | diary: ' + turnObstacles + ' obstacle(s) via ' + lastObstacleTool
230 : ' | diary: ' + entries.length + ' entries';
231 return next({
232 ...e,
233 props: {
234 ...e.props,
235 suffix
236 }
237 });
238 });
239
240 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
241 if (!showBanner) {
242 return next(e);
243 }
244 const { Box, Text } = $.ui.resolve(e);
245 const current = entries[meditationIndex] || entries[0];
246 const med = current && current.meditation ? current.meditation : FALLBACK_ENTRIES[0].meditation;
247 return Box({
248 borderStyle: 'round',
249 borderColor: 'yellow',
250 paddingX: 1,
251 flexDirection: 'row',
252 columnGap: 1,
253 children: [
254 Text({ bold: true, color: 'yellow', children: ['[' + med.book + ' ' + med.verse + ']'] }),
255 Text({ dimColor: true, children: [med.quote] })
256 ]
257 });
258 });
259
260 on('ui.render', { component: 'Pane' }, async ($, e, next) => {
261 if (e.requestId !== PANE_ID) {
262 return next(e);
263 }
264 const { Box, Text, Button, Input, Markdown } = $.ui.resolve(e);
265 const entry = entries[selectedIndex] || entries[0];
266 const medEntry = entries[meditationIndex] || entry;
267 const med = medEntry && medEntry.meditation ? medEntry.meditation : FALLBACK_ENTRIES[0].meditation;
268
269 const navBar = Box({
270 flexDirection: 'row',
271 columnGap: 3,
272 children: [
273 Button({
274 key: 'tab-entries',
275 label: 'Entries (' + entries.length + ')',
276 hotkey: '1',
277 plain: true,
278 dimColor: activeTab !== 'entries',
279 onPress: () => {
280 activeTab = 'entries';
281 $.ui.invalidate('ui.render');
282 }
283 }),
284 Button({
285 key: 'tab-meditations',
286 label: 'Meditations',
287 hotkey: '2',
288 plain: true,
289 dimColor: activeTab !== 'meditations',
290 onPress: () => {
291 activeTab = 'meditations';
292 $.ui.invalidate('ui.render');
293 }
294 }),
295 Button({
296 key: 'tab-write',
297 label: 'Write Note (' + notes.length + ')',
298 hotkey: '3',
299 plain: true,
300 dimColor: activeTab !== 'write',
301 onPress: () => {
302 activeTab = 'write';
303 $.ui.invalidate('ui.render');
304 }
305 }),
306 Button({
307 key: 'toggle-banner',
308 label: 'Stoic Banner: ' + (showBanner ? 'ON' : 'OFF'),
309 hotkey: 'b',
310 plain: true,
311 dimColor: !showBanner,
312 onPress: async () => {
313 showBanner = !showBanner;
314 await $.store.set('showBanner', showBanner);
315 $.ui.invalidate('ui.render');
316 }
317 }),
318 Button({
319 key: 'open-html',
320 label: 'Open HTML',
321 hotkey: 'o',
322 plain: true,
323 onPress: async () => {
324 try {
325 await $.process.run(['agent-diary', 'open']);
326 $.ui.toast('Opened diary.html in your browser');
327 } catch {
328 $.ui.toast('Run: agent-diary open');
329 }
330 }
331 })
332 ]
333 });
334
335 let bodyChildren = [];
336
337 if (activeTab === 'entries' && entry) {
338 const entryMarkdown = [
339 '## ' + (selectedIndex + 1) + '/' + entries.length + ' · `' + entry.project + '` (' + entry.date + ')',
340 '',
341 '**' + entry.title + '** (' + (entry.virtue || 'Practical Wisdom') + ')',
342 '',
343 '### Obstacles',
344 '',
345 ...(entry.challenges || []).slice(0, 3).map((c) => '- ' + c),
346 '',
347 '### Fixes',
348 '',
349 ...(entry.solutions || []).slice(0, 3).map((s) => '- ' + s),
350 '',
351 '### Takeaways',
352 '',
353 ...(entry.learnings || []).slice(0, 3).map((l) => '- ' + l),
354 '',
355 entry.userNote
356 ? '> Personal note: ' + entry.userNote
357 : '> No personal note yet. Press `3` to add one.'
358 ].join('\n');
359
360 bodyChildren = [
361 Markdown({ text: entryMarkdown }),
362 Box({
363 flexDirection: 'row',
364 columnGap: 2,
365 children: [
366 Button({
367 key: 'prev-entry',
368 label: 'Previous Entry',
369 hotkey: 'p',
370 onPress: () => {
371 selectedIndex = (selectedIndex - 1 + entries.length) % entries.length;
372 meditationIndex = selectedIndex;
373 $.ui.invalidate('ui.render');
374 }
375 }),
376 Button({
377 key: 'next-entry',
378 label: 'Next Entry',
379 hotkey: 'n',
380 onPress: () => {
381 selectedIndex = (selectedIndex + 1) % entries.length;
382 meditationIndex = selectedIndex;
383 $.ui.invalidate('ui.render');
384 }
385 })
386 ]
387 })
388 ];
389 } else if (activeTab === 'meditations') {
390 const medMarkdown = [
391 '## ' + med.book + ', ' + med.verse + ' (' + (med.stoicConcept || 'Ta Eis Heauton') + ')',
392 '',
393 '> ' + med.quote,
394 '',
395 med.reflection || '',
396 '',
397 'Source session: `' + (medEntry ? medEntry.project : 'session') + '` (' + (medEntry ? medEntry.date : '') + ')'
398 ].join('\n');
399
400 bodyChildren = [
401 Markdown({ text: medMarkdown }),
402 Box({
403 flexDirection: 'row',
404 columnGap: 2,
405 children: [
406 Button({
407 key: 'draw-lot',
408 label: 'Draw Another Stoic Lot',
409 hotkey: 'm',
410 onPress: () => {
411 meditationIndex = (meditationIndex + 1) % entries.length;
412 $.ui.invalidate('ui.render');
413 }
414 })
415 ]
416 })
417 ];
418 } else {
419 const notesMarkdown =
420 notes.length > 0
421 ? [
422 '### Recent notes saved from Claude Code',
423 '',
424 ...notes.slice(0, 5).map((n) => '- **' + n.date + '** (`' + n.project + '`): ' + n.text)
425 ].join('\n')
426 : '> No notes saved in this workspace store yet.';
427
428 bodyChildren = [
429 Markdown({
430 text:
431 '## Attach a personal note\n\nTarget entry: `' +
432 (entry ? entry.project : 'session') +
433 '` - **' +
434 (entry ? entry.title : 'current session') +
435 '**'
436 }),
437 Input({
438 key: 'diary-note-input',
439 label: 'Reflection',
440 placeholder: 'What bug, fix, or lesson stood out today? Press Enter to save',
441 value: '',
442 submitLabel: 'save',
443 onSubmit: async (val) => {
444 const record = buildNoteRecord(val);
445 if (record) {
446 await $.store.set('notes', notes);
447 try {
448 await $.fs.write(diaryJsonPath, JSON.stringify(entries, null, 2));
449 } catch {
450 // Ignore write failures in sandboxed environments.
451 }
452 $.ui.toast('Saved note to Agent Diary');
453 $.ui.invalidate('ui.render');
454 }
455 }
456 }),
457 Markdown({ text: notesMarkdown })
458 ];
459 }
460
461 const footer = Text({
462 dimColor: true,
463 children: [
464 'Session telemetry: ' + turnTools + ' tool call(s), ' + turnObstacles + ' obstacle(s) | Keys: 1/2/3 tabs, n/p cycle, m lot, b banner, o browser, Esc close'
465 ]
466 });
467
468 return Box({
469 flexDirection: 'column',
470 children: [
471 navBar,
472 Text({ children: [' '] }),
473 ...bodyChildren,
474 Text({ children: [' '] }),
475 footer
476 ]
477 });
478 });
479}
480