sumulige-claude 1.3.2 โ†’ 1.3.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,18 @@
1
+ ## [1.3.3](https://github.com/sumulige/sumulige-claude/compare/v1.3.2...v1.3.3) (2026-01-22)
2
+
3
+ ### โœจ New Features
4
+
5
+ - **`smc sync --hooks`**: Incremental hooks update for existing projects
6
+ - Adds new lifecycle hooks without overwriting customizations
7
+ - Merges SessionStart/SessionEnd/PreCompact into existing settings.json
8
+
9
+ ### ๐Ÿ“ Documentation
10
+
11
+ - Add Layer 7: Lifecycle Hooks section to README
12
+ - Document update methods for other projects
13
+
14
+ ---
15
+
1
16
  ## [1.3.2](https://github.com/sumulige/sumulige-claude/compare/v1.3.1...v1.3.2) (2026-01-22)
2
17
 
3
18
  ### โœจ New Features
package/README.md CHANGED
@@ -400,6 +400,68 @@ smc skill:create my-skill
400
400
 
401
401
  ---
402
402
 
403
+ ## Layer 7: Lifecycle Hooks / ็ฌฌไธƒๅฑ‚๏ผš็”Ÿๅ‘ฝๅ‘จๆœŸ้’ฉๅญ
404
+
405
+ ### Auto-Sync System / ่‡ชๅŠจๅŒๆญฅ็ณป็ปŸ
406
+
407
+ > v1.3.2: ๅˆฉ็”จ Claude Code ๅฎ˜ๆ–น Hook ไบ‹ไปถๅฎž็Žฐ่ฎฐๅฟ†่‡ชๅŠจๅŒๆญฅ
408
+
409
+ ```
410
+ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
411
+ โ”‚ SessionStart โ”€โ”€โ–บ memory-loader.cjs โ”‚
412
+ โ”‚ โ””โ”€ ่‡ชๅŠจๅŠ ่ฝฝ MEMORY.md, ANCHORS.md, TODO ็Šถๆ€ โ”‚
413
+ โ”‚ โ”‚
414
+ โ”‚ PreCompact โ”€โ”€โ–บ auto-handoff.cjs โ”‚
415
+ โ”‚ โ””โ”€ ไธŠไธ‹ๆ–‡ๅŽ‹็ผฉๅ‰่‡ชๅŠจ็”Ÿๆˆ handoff ๆ–‡ๆกฃ โ”‚
416
+ โ”‚ โ”‚
417
+ โ”‚ SessionEnd โ”€โ”€โ–บ memory-saver.cjs โ”‚
418
+ โ”‚ โ””โ”€ ไผš่ฏ็ป“ๆŸ่‡ชๅŠจไฟๅญ˜ๆ‘˜่ฆๅˆฐ MEMORY.md โ”‚
419
+ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
420
+ ```
421
+
422
+ ### Hook Files / ้’ฉๅญๆ–‡ไปถ
423
+
424
+ | Hook | ๆ–‡ไปถ | ่งฆๅ‘ๆ—ถๆœบ | ๅŠŸ่ƒฝ |
425
+ |------|------|----------|------|
426
+ | `SessionStart` | `memory-loader.cjs` | ไผš่ฏๅผ€ๅง‹ | ๅŠ ่ฝฝ่ฎฐๅฟ†ใ€้”š็‚นใ€TODO |
427
+ | `SessionEnd` | `memory-saver.cjs` | ไผš่ฏ็ป“ๆŸ | ไฟๅญ˜ๆ‘˜่ฆใ€ๅฝ’ๆกฃไผš่ฏ |
428
+ | `PreCompact` | `auto-handoff.cjs` | ไธŠไธ‹ๆ–‡ๅŽ‹็ผฉๅ‰ | ็”Ÿๆˆ handoff ไฟๆŠคไธŠไธ‹ๆ–‡ |
429
+
430
+ ### Handoff Documents / ไบคๆŽฅๆ–‡ๆกฃ
431
+
432
+ ๅฝ“ไธŠไธ‹ๆ–‡ๅณๅฐ†่ขซๅŽ‹็ผฉๆ—ถ๏ผŒ่‡ชๅŠจ็”ŸๆˆไบคๆŽฅๆ–‡ๆกฃ๏ผš
433
+
434
+ ```
435
+ .claude/handoffs/
436
+ โ”œโ”€โ”€ LATEST.md # ๆœ€ๆ–ฐไบคๆŽฅๆ–‡ๆกฃ
437
+ โ”œโ”€โ”€ INDEX.md # ไบคๆŽฅๆ–‡ๆกฃ็ดขๅผ•
438
+ โ””โ”€โ”€ handoff_*.md # ๅކๅฒไบคๆŽฅๆ–‡ๆกฃ
439
+ ```
440
+
441
+ ๆฏไธชไบคๆŽฅๆ–‡ๆกฃๅŒ…ๅซ๏ผš
442
+ - ไผš่ฏไฟกๆฏ๏ผˆ้กน็›ฎใ€็‰ˆๆœฌใ€ๅผ€ๅง‹ๆ—ถ้—ด๏ผ‰
443
+ - ๆดป่ทƒ TODOs ๅˆ—่กจ
444
+ - ๆœ€่ฟ‘ไฟฎๆ”น็š„ๆ–‡ไปถ
445
+ - ๆขๅคๅ‘ฝไปค
446
+
447
+ ### Update Hooks / ๆ›ดๆ–ฐ้’ฉๅญ
448
+
449
+ ๅ…ถไป–้กน็›ฎๅฆ‚ไฝ•่Žทๅ–ๆ–ฐ hooks๏ผš
450
+
451
+ ```bash
452
+ # ๆ–นๅผ 1: ๅฎŒๆ•ดๆ›ดๆ–ฐ๏ผˆๆŽจ่๏ผ‰
453
+ smc template --force
454
+
455
+ # ๆ–นๅผ 2: ๅขž้‡ๅŒๆญฅ๏ผˆไป…ๆ›ดๆ–ฐ hooks๏ผ‰
456
+ smc sync --hooks
457
+
458
+ # ๆ–นๅผ 3: ๆ‰‹ๅŠจๅฎ‰่ฃ…
459
+ npm update -g sumulige-claude
460
+ smc template
461
+ ```
462
+
463
+ ---
464
+
403
465
  ## Documentation / ๆ–‡ๆกฃ
404
466
 
405
467
  - **[Development Guide / ๅผ€ๅ‘ๆŒ‡ๅ—](docs/DEVELOPMENT.md)** - Architecture, adding skills / ๆžถๆž„ใ€ๆทปๅŠ ๆŠ€่ƒฝ
package/cli.js CHANGED
@@ -30,7 +30,7 @@ const COMMANDS = {
30
30
  },
31
31
  sync: {
32
32
  help: 'Sync to current project (auto-migrates old format)',
33
- args: '[--check-update]'
33
+ args: '[--check-update] [--hooks]'
34
34
  },
35
35
  migrate: {
36
36
  help: 'Migrate old hooks format to new',
package/lib/commands.js CHANGED
@@ -335,6 +335,7 @@ const commands = {
335
335
  // -------------------------------------------------------------------------
336
336
  sync: async (...args) => {
337
337
  const forceCheckUpdate = args.includes("--check-update");
338
+ const syncHooks = args.includes("--hooks");
338
339
 
339
340
  console.log("๐Ÿ”„ Syncing Sumulige Claude to current project...");
340
341
  console.log("");
@@ -387,6 +388,88 @@ const commands = {
387
388
  }
388
389
  }
389
390
 
391
+ // Sync hooks if --hooks flag is provided
392
+ if (syncHooks) {
393
+ const templateHooksDir = path.join(TEMPLATE_DIR, ".claude", "hooks");
394
+ const projectHooksDir = path.join(projectConfigDir, "hooks");
395
+ const templateSettingsFile = path.join(
396
+ TEMPLATE_DIR,
397
+ ".claude",
398
+ "settings.json",
399
+ );
400
+ const projectSettingsFile = path.join(projectConfigDir, "settings.json");
401
+
402
+ if (fs.existsSync(templateHooksDir)) {
403
+ ensureDir(projectHooksDir);
404
+
405
+ // Sync hook files (only add new ones, don't overwrite)
406
+ const hookFiles = fs
407
+ .readdirSync(templateHooksDir)
408
+ .filter((f) => f.endsWith(".cjs"));
409
+ let syncedCount = 0;
410
+
411
+ hookFiles.forEach((hookFile) => {
412
+ const src = path.join(templateHooksDir, hookFile);
413
+ const dest = path.join(projectHooksDir, hookFile);
414
+
415
+ if (!fs.existsSync(dest)) {
416
+ fs.copyFileSync(src, dest);
417
+ setExecutablePermission(dest);
418
+ syncedCount++;
419
+ console.log(` โœ… Added ${hookFile}`);
420
+ }
421
+ });
422
+
423
+ if (syncedCount > 0) {
424
+ console.log(`โœ… Synced ${syncedCount} new hook(s)`);
425
+ } else {
426
+ console.log("โœ… Hooks up to date");
427
+ }
428
+ }
429
+
430
+ // Sync settings.json (merge new lifecycle hooks)
431
+ if (
432
+ fs.existsSync(templateSettingsFile) &&
433
+ fs.existsSync(projectSettingsFile)
434
+ ) {
435
+ try {
436
+ const templateSettings = JSON.parse(
437
+ fs.readFileSync(templateSettingsFile, "utf-8"),
438
+ );
439
+ const projectSettings = JSON.parse(
440
+ fs.readFileSync(projectSettingsFile, "utf-8"),
441
+ );
442
+
443
+ // Merge new lifecycle hooks
444
+ const lifecycleHooks = [
445
+ "SessionStart",
446
+ "SessionEnd",
447
+ "PreCompact",
448
+ "env",
449
+ ];
450
+ let updated = false;
451
+
452
+ lifecycleHooks.forEach((hook) => {
453
+ if (templateSettings[hook] && !projectSettings[hook]) {
454
+ projectSettings[hook] = templateSettings[hook];
455
+ updated = true;
456
+ console.log(` โœ… Added ${hook} hook`);
457
+ }
458
+ });
459
+
460
+ if (updated) {
461
+ fs.writeFileSync(
462
+ projectSettingsFile,
463
+ JSON.stringify(projectSettings, null, 2),
464
+ );
465
+ console.log("โœ… Updated settings.json");
466
+ }
467
+ } catch (e) {
468
+ console.log("โš ๏ธ Failed to merge settings.json");
469
+ }
470
+ }
471
+ }
472
+
390
473
  // Sync todos directory structure
391
474
  const todosTemplateDir = path.join(TEMPLATE_DIR, "development", "todos");
392
475
  const todosProjectDir = path.join(projectDir, "development", "todos");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sumulige-claude",
3
- "version": "1.3.2",
3
+ "version": "1.3.3",
4
4
  "description": "The Best Agent Harness for Claude Code",
5
5
  "main": "cli.js",
6
6
  "bin": {
@@ -0,0 +1,353 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Auto Handoff Hook - PreCompact Context Preservation
4
+ *
5
+ * Claude Official Hook: PreCompact
6
+ * Triggered: Before conversation context is compressed
7
+ *
8
+ * Features:
9
+ * - Auto-generate handoff document before context compression
10
+ * - Preserve critical context that might be lost during compaction
11
+ * - Save current state including progress, blockers, and next steps
12
+ *
13
+ * Environment Variables:
14
+ * - CLAUDE_PROJECT_DIR: Project directory path
15
+ * - CLAUDE_SESSION_ID: Unique session identifier
16
+ * - CLAUDE_CONVERSATION_ID: Conversation identifier
17
+ */
18
+
19
+ const fs = require('fs');
20
+ const path = require('path');
21
+
22
+ const PROJECT_DIR = process.env.CLAUDE_PROJECT_DIR || process.cwd();
23
+ const CLAUDE_DIR = path.join(PROJECT_DIR, '.claude');
24
+ const HANDOFFS_DIR = path.join(CLAUDE_DIR, 'handoffs');
25
+ const SESSION_STATE_FILE = path.join(CLAUDE_DIR, '.session-state.json');
26
+ const STATE_FILE = path.join(PROJECT_DIR, 'development', 'todos', '.state.json');
27
+ const SESSION_ID = process.env.CLAUDE_SESSION_ID || 'unknown';
28
+ const CONVERSATION_ID = process.env.CLAUDE_CONVERSATION_ID || 'unknown';
29
+
30
+ /**
31
+ * Ensure handoffs directory exists
32
+ */
33
+ function ensureHandoffsDir() {
34
+ if (!fs.existsSync(HANDOFFS_DIR)) {
35
+ fs.mkdirSync(HANDOFFS_DIR, { recursive: true });
36
+ }
37
+ }
38
+
39
+ /**
40
+ * Load current session state
41
+ */
42
+ function loadSessionState() {
43
+ if (!fs.existsSync(SESSION_STATE_FILE)) {
44
+ return {
45
+ session: { project: path.basename(PROJECT_DIR) },
46
+ memory: { entries: 0 },
47
+ todos: { active: 0, completed: 0 }
48
+ };
49
+ }
50
+
51
+ try {
52
+ return JSON.parse(fs.readFileSync(SESSION_STATE_FILE, 'utf-8'));
53
+ } catch (e) {
54
+ return {
55
+ session: { project: path.basename(PROJECT_DIR) },
56
+ memory: { entries: 0 },
57
+ todos: { active: 0, completed: 0 }
58
+ };
59
+ }
60
+ }
61
+
62
+ /**
63
+ * Load active TODOs
64
+ */
65
+ function loadActiveTodos() {
66
+ const todosDir = path.join(PROJECT_DIR, 'development', 'todos', 'active');
67
+ if (!fs.existsSync(todosDir)) {
68
+ return [];
69
+ }
70
+
71
+ try {
72
+ const files = fs.readdirSync(todosDir)
73
+ .filter(f => f.endsWith('.md') && f !== '_README.md');
74
+
75
+ return files.map(f => {
76
+ const content = fs.readFileSync(path.join(todosDir, f), 'utf-8');
77
+ const titleMatch = content.match(/^#\s+(.+)$/m);
78
+ return {
79
+ file: f,
80
+ title: titleMatch ? titleMatch[1] : path.basename(f, '.md')
81
+ };
82
+ });
83
+ } catch (e) {
84
+ return [];
85
+ }
86
+ }
87
+
88
+ /**
89
+ * Get recently modified files
90
+ */
91
+ function getRecentlyModifiedFiles(hours = 24) {
92
+ const recentFiles = [];
93
+ const cutoff = Date.now() - (hours * 60 * 60 * 1000);
94
+
95
+ // Check common source directories
96
+ const sourceDirs = ['src', 'lib', '.claude', 'development'];
97
+
98
+ for (const dir of sourceDirs) {
99
+ const fullPath = path.join(PROJECT_DIR, dir);
100
+ if (!fs.existsSync(fullPath)) continue;
101
+
102
+ try {
103
+ const walkDir = (dirPath, depth = 0) => {
104
+ if (depth > 3) return; // Limit depth
105
+
106
+ const items = fs.readdirSync(dirPath, { withFileTypes: true });
107
+ for (const item of items) {
108
+ const itemPath = path.join(dirPath, item.name);
109
+
110
+ if (item.isDirectory() && !item.name.startsWith('.') && item.name !== 'node_modules') {
111
+ walkDir(itemPath, depth + 1);
112
+ } else if (item.isFile()) {
113
+ try {
114
+ const stat = fs.statSync(itemPath);
115
+ if (stat.mtimeMs > cutoff) {
116
+ const relativePath = path.relative(PROJECT_DIR, itemPath);
117
+ recentFiles.push({
118
+ path: relativePath,
119
+ modified: stat.mtime.toISOString()
120
+ });
121
+ }
122
+ } catch (e) {
123
+ // Ignore stat errors
124
+ }
125
+ }
126
+ }
127
+ };
128
+
129
+ walkDir(fullPath);
130
+ } catch (e) {
131
+ // Ignore directory errors
132
+ }
133
+ }
134
+
135
+ // Sort by modification time (most recent first)
136
+ recentFiles.sort((a, b) => b.modified.localeCompare(a.modified));
137
+
138
+ return recentFiles.slice(0, 20); // Return top 20
139
+ }
140
+
141
+ /**
142
+ * Generate handoff document
143
+ */
144
+ function generateHandoff(sessionState) {
145
+ const now = new Date();
146
+ const todos = loadActiveTodos();
147
+ const recentFiles = getRecentlyModifiedFiles();
148
+
149
+ let content = `# Handoff: Pre-Compact Context Preservation
150
+
151
+ > Auto-generated before context compression
152
+ > Date: ${now.toISOString()}
153
+ > Session: ${SESSION_ID}
154
+ > Conversation: ${CONVERSATION_ID}
155
+
156
+ ---
157
+
158
+ ## Session Info
159
+
160
+ - **Project**: ${sessionState.session?.project || 'unknown'}
161
+ - **Version**: ${sessionState.session?.version || 'unknown'}
162
+ - **Start Time**: ${sessionState.session?.startTime || 'unknown'}
163
+
164
+ ---
165
+
166
+ ## Memory State
167
+
168
+ - **Entries Loaded**: ${sessionState.memory?.entries || 0}
169
+ - **Anchors Modules**: ${sessionState.anchors?.modules || 0}
170
+
171
+ ---
172
+
173
+ ## Active TODOs (${todos.length})
174
+
175
+ `;
176
+
177
+ if (todos.length > 0) {
178
+ todos.forEach(todo => {
179
+ content += `- [ ] ${todo.title} (\`${todo.file}\`)\n`;
180
+ });
181
+ } else {
182
+ content += `*No active TODOs*\n`;
183
+ }
184
+
185
+ content += `
186
+
187
+ ---
188
+
189
+ ## Recently Modified Files (Last 24h)
190
+
191
+ `;
192
+
193
+ if (recentFiles.length > 0) {
194
+ recentFiles.slice(0, 10).forEach(f => {
195
+ content += `- \`${f.path}\` (${f.modified.split('T')[0]})\n`;
196
+ });
197
+ if (recentFiles.length > 10) {
198
+ content += `- *...and ${recentFiles.length - 10} more files*\n`;
199
+ }
200
+ } else {
201
+ content += `*No recently modified files*\n`;
202
+ }
203
+
204
+ content += `
205
+
206
+ ---
207
+
208
+ ## Context Preservation Notes
209
+
210
+ **Important**: This handoff was auto-generated before context compaction.
211
+ The following information should be re-loaded after compaction:
212
+
213
+ 1. Read \`.claude/MEMORY.md\` for recent session context
214
+ 2. Check \`development/todos/INDEX.md\` for task status
215
+ 3. Review recent git commits for code changes
216
+
217
+ ---
218
+
219
+ ## Recovery Commands
220
+
221
+ \`\`\`bash
222
+ # View recent memory
223
+ cat .claude/MEMORY.md | head -100
224
+
225
+ # Check active TODOs
226
+ ls development/todos/active/
227
+
228
+ # View recent changes
229
+ git log --oneline -10
230
+ git status
231
+ \`\`\`
232
+
233
+ ---
234
+
235
+ *Auto-generated by auto-handoff.cjs at ${now.toISOString()}*
236
+ `;
237
+
238
+ return content;
239
+ }
240
+
241
+ /**
242
+ * Save handoff document
243
+ */
244
+ function saveHandoff(content) {
245
+ ensureHandoffsDir();
246
+
247
+ const now = new Date();
248
+ const filename = `handoff_${now.toISOString().replace(/[:.]/g, '-')}.md`;
249
+ const filepath = path.join(HANDOFFS_DIR, filename);
250
+
251
+ fs.writeFileSync(filepath, content);
252
+
253
+ // Also save as latest handoff for easy access
254
+ const latestPath = path.join(HANDOFFS_DIR, 'LATEST.md');
255
+ fs.writeFileSync(latestPath, content);
256
+
257
+ // Clean up old handoffs (keep last 10)
258
+ const files = fs.readdirSync(HANDOFFS_DIR)
259
+ .filter(f => f.startsWith('handoff_') && f.endsWith('.md'))
260
+ .sort()
261
+ .reverse();
262
+
263
+ if (files.length > 10) {
264
+ files.slice(10).forEach(f => {
265
+ try {
266
+ fs.unlinkSync(path.join(HANDOFFS_DIR, f));
267
+ } catch (e) {
268
+ // Ignore deletion errors
269
+ }
270
+ });
271
+ }
272
+
273
+ return filename;
274
+ }
275
+
276
+ /**
277
+ * Update handoffs index
278
+ */
279
+ function updateHandoffsIndex() {
280
+ const indexPath = path.join(HANDOFFS_DIR, 'INDEX.md');
281
+
282
+ const files = fs.readdirSync(HANDOFFS_DIR)
283
+ .filter(f => f.startsWith('handoff_') && f.endsWith('.md'))
284
+ .sort()
285
+ .reverse();
286
+
287
+ let content = `# Handoffs Index
288
+
289
+ > Auto-generated context preservation documents
290
+ > Updated: ${new Date().toISOString()}
291
+
292
+ ---
293
+
294
+ ## Recent Handoffs (${files.length})
295
+
296
+ `;
297
+
298
+ files.slice(0, 20).forEach(f => {
299
+ const filepath = path.join(HANDOFFS_DIR, f);
300
+ const stat = fs.statSync(filepath);
301
+ content += `- [${f}](./${f}) - ${stat.mtime.toISOString()}\n`;
302
+ });
303
+
304
+ content += `
305
+
306
+ ---
307
+
308
+ ## Latest Handoff
309
+
310
+ See [LATEST.md](./LATEST.md) for the most recent context snapshot.
311
+
312
+ ---
313
+
314
+ *Index maintained by auto-handoff.cjs*
315
+ `;
316
+
317
+ fs.writeFileSync(indexPath, content);
318
+ }
319
+
320
+ /**
321
+ * Main execution
322
+ */
323
+ function main() {
324
+ try {
325
+ const sessionState = loadSessionState();
326
+ const content = generateHandoff(sessionState);
327
+ const filename = saveHandoff(content);
328
+ updateHandoffsIndex();
329
+
330
+ console.log(`\nโšก PreCompact: Context preserved โ†’ ${filename}`);
331
+ console.log(` Recovery: .claude/handoffs/LATEST.md\n`);
332
+
333
+ process.exit(0);
334
+ } catch (e) {
335
+ // Silent failure - don't interrupt compaction
336
+ console.error(`PreCompact handoff error: ${e.message}`);
337
+ process.exit(0);
338
+ }
339
+ }
340
+
341
+ // Run
342
+ if (require.main === module) {
343
+ main();
344
+ }
345
+
346
+ module.exports = {
347
+ loadSessionState,
348
+ loadActiveTodos,
349
+ getRecentlyModifiedFiles,
350
+ generateHandoff,
351
+ saveHandoff,
352
+ updateHandoffsIndex
353
+ };
@@ -0,0 +1,208 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Memory Loader Hook - SessionStart Auto-Load System
4
+ *
5
+ * Claude Official Hook: SessionStart
6
+ * Triggered: Once at the beginning of each session
7
+ *
8
+ * Features:
9
+ * - Auto-load MEMORY.md for recent context
10
+ * - Auto-load ANCHORS.md for module navigation
11
+ * - Restore TODO state from .state.json
12
+ * - Inject session context summary
13
+ *
14
+ * Environment Variables:
15
+ * - CLAUDE_PROJECT_DIR: Project directory path
16
+ * - CLAUDE_SESSION_ID: Unique session identifier
17
+ */
18
+
19
+ const fs = require('fs');
20
+ const path = require('path');
21
+
22
+ const PROJECT_DIR = process.env.CLAUDE_PROJECT_DIR || process.cwd();
23
+ const CLAUDE_DIR = path.join(PROJECT_DIR, '.claude');
24
+ const MEMORY_FILE = path.join(CLAUDE_DIR, 'MEMORY.md');
25
+ const ANCHORS_FILE = path.join(CLAUDE_DIR, 'ANCHORS.md');
26
+ const STATE_FILE = path.join(PROJECT_DIR, 'development', 'todos', '.state.json');
27
+ const SESSION_ID = process.env.CLAUDE_SESSION_ID || 'unknown';
28
+
29
+ /**
30
+ * Load memory file content (recent entries only)
31
+ */
32
+ function loadMemory(days = 7) {
33
+ if (!fs.existsSync(MEMORY_FILE)) {
34
+ return { exists: false, content: '', entries: 0 };
35
+ }
36
+
37
+ const content = fs.readFileSync(MEMORY_FILE, 'utf-8');
38
+ const entries = content.split('## ').slice(1, days + 1);
39
+
40
+ return {
41
+ exists: true,
42
+ content: entries.length > 0 ? '## ' + entries.join('## ') : '',
43
+ entries: entries.length
44
+ };
45
+ }
46
+
47
+ /**
48
+ * Load anchors file for quick navigation
49
+ */
50
+ function loadAnchors() {
51
+ if (!fs.existsSync(ANCHORS_FILE)) {
52
+ return { exists: false, content: '', modules: 0 };
53
+ }
54
+
55
+ const content = fs.readFileSync(ANCHORS_FILE, 'utf-8');
56
+ const moduleMatches = content.match(/##\s+[\w-]+/g) || [];
57
+
58
+ return {
59
+ exists: true,
60
+ content: content,
61
+ modules: moduleMatches.length
62
+ };
63
+ }
64
+
65
+ /**
66
+ * Load TODO state
67
+ */
68
+ function loadTodoState() {
69
+ if (!fs.existsSync(STATE_FILE)) {
70
+ return { exists: false, active: 0, completed: 0 };
71
+ }
72
+
73
+ try {
74
+ const state = JSON.parse(fs.readFileSync(STATE_FILE, 'utf-8'));
75
+ return {
76
+ exists: true,
77
+ active: state.active?.length || 0,
78
+ completed: state.completed?.length || 0,
79
+ lastUpdated: state.lastUpdated || null
80
+ };
81
+ } catch (e) {
82
+ return { exists: false, active: 0, completed: 0 };
83
+ }
84
+ }
85
+
86
+ /**
87
+ * Get project info
88
+ */
89
+ function getProjectInfo() {
90
+ const pkgPath = path.join(PROJECT_DIR, 'package.json');
91
+ if (!fs.existsSync(pkgPath)) {
92
+ return { name: path.basename(PROJECT_DIR), version: 'unknown' };
93
+ }
94
+
95
+ try {
96
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
97
+ return {
98
+ name: pkg.name || path.basename(PROJECT_DIR),
99
+ version: pkg.version || 'unknown'
100
+ };
101
+ } catch (e) {
102
+ return { name: path.basename(PROJECT_DIR), version: 'unknown' };
103
+ }
104
+ }
105
+
106
+ /**
107
+ * Generate session start context
108
+ */
109
+ function generateSessionContext() {
110
+ const memory = loadMemory();
111
+ const anchors = loadAnchors();
112
+ const todos = loadTodoState();
113
+ const project = getProjectInfo();
114
+
115
+ const timestamp = new Date().toISOString();
116
+
117
+ // Build context object
118
+ const context = {
119
+ session: {
120
+ id: SESSION_ID,
121
+ startTime: timestamp,
122
+ project: project.name,
123
+ version: project.version
124
+ },
125
+ memory: {
126
+ loaded: memory.exists,
127
+ entries: memory.entries
128
+ },
129
+ anchors: {
130
+ loaded: anchors.exists,
131
+ modules: anchors.modules
132
+ },
133
+ todos: {
134
+ loaded: todos.exists,
135
+ active: todos.active,
136
+ completed: todos.completed
137
+ }
138
+ };
139
+
140
+ // Save session state
141
+ const sessionStateFile = path.join(CLAUDE_DIR, '.session-state.json');
142
+ try {
143
+ fs.writeFileSync(sessionStateFile, JSON.stringify(context, null, 2));
144
+ } catch (e) {
145
+ // Ignore write errors
146
+ }
147
+
148
+ return context;
149
+ }
150
+
151
+ /**
152
+ * Format session summary for output
153
+ */
154
+ function formatSessionSummary(context) {
155
+ let summary = '';
156
+
157
+ summary += `\n๐Ÿ“‚ Session: ${context.session.project} v${context.session.version}\n`;
158
+
159
+ if (context.memory.loaded && context.memory.entries > 0) {
160
+ summary += `๐Ÿ’พ Memory: ${context.memory.entries} entries loaded\n`;
161
+ }
162
+
163
+ if (context.anchors.loaded && context.anchors.modules > 0) {
164
+ summary += `๐Ÿ”– Anchors: ${context.anchors.modules} modules indexed\n`;
165
+ }
166
+
167
+ if (context.todos.loaded && (context.todos.active > 0 || context.todos.completed > 0)) {
168
+ summary += `๐Ÿ“‹ TODOs: ${context.todos.active} active, ${context.todos.completed} completed\n`;
169
+ }
170
+
171
+ return summary;
172
+ }
173
+
174
+ /**
175
+ * Main execution
176
+ */
177
+ function main() {
178
+ try {
179
+ const context = generateSessionContext();
180
+
181
+ // Only output summary if there's meaningful context
182
+ const hasContext = context.memory.entries > 0 ||
183
+ context.anchors.modules > 0 ||
184
+ context.todos.active > 0;
185
+
186
+ if (hasContext) {
187
+ console.log(formatSessionSummary(context));
188
+ }
189
+
190
+ process.exit(0);
191
+ } catch (e) {
192
+ // Silent failure - don't interrupt session
193
+ process.exit(0);
194
+ }
195
+ }
196
+
197
+ // Run
198
+ if (require.main === module) {
199
+ main();
200
+ }
201
+
202
+ module.exports = {
203
+ loadMemory,
204
+ loadAnchors,
205
+ loadTodoState,
206
+ generateSessionContext,
207
+ formatSessionSummary
208
+ };
@@ -0,0 +1,268 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Memory Saver Hook - SessionEnd Auto-Save System
4
+ *
5
+ * Claude Official Hook: SessionEnd
6
+ * Triggered: Once at the end of each session
7
+ *
8
+ * Features:
9
+ * - Auto-save session summary to MEMORY.md
10
+ * - Sync TODO state changes
11
+ * - Archive session state
12
+ * - Generate session statistics
13
+ *
14
+ * Environment Variables:
15
+ * - CLAUDE_PROJECT_DIR: Project directory path
16
+ * - CLAUDE_SESSION_ID: Unique session identifier
17
+ */
18
+
19
+ const fs = require('fs');
20
+ const path = require('path');
21
+
22
+ const PROJECT_DIR = process.env.CLAUDE_PROJECT_DIR || process.cwd();
23
+ const CLAUDE_DIR = path.join(PROJECT_DIR, '.claude');
24
+ const MEMORY_FILE = path.join(CLAUDE_DIR, 'MEMORY.md');
25
+ const SESSION_STATE_FILE = path.join(CLAUDE_DIR, '.session-state.json');
26
+ const SESSIONS_DIR = path.join(CLAUDE_DIR, 'sessions');
27
+ const STATE_FILE = path.join(PROJECT_DIR, 'development', 'todos', '.state.json');
28
+ const SESSION_ID = process.env.CLAUDE_SESSION_ID || 'unknown';
29
+
30
+ /**
31
+ * Ensure directories exist
32
+ */
33
+ function ensureDirectories() {
34
+ [CLAUDE_DIR, SESSIONS_DIR].forEach(dir => {
35
+ if (!fs.existsSync(dir)) {
36
+ fs.mkdirSync(dir, { recursive: true });
37
+ }
38
+ });
39
+ }
40
+
41
+ /**
42
+ * Load session state from SessionStart
43
+ */
44
+ function loadSessionState() {
45
+ if (!fs.existsSync(SESSION_STATE_FILE)) {
46
+ return null;
47
+ }
48
+
49
+ try {
50
+ return JSON.parse(fs.readFileSync(SESSION_STATE_FILE, 'utf-8'));
51
+ } catch (e) {
52
+ return null;
53
+ }
54
+ }
55
+
56
+ /**
57
+ * Calculate session duration
58
+ */
59
+ function calculateDuration(startTime) {
60
+ if (!startTime) return 'unknown';
61
+
62
+ const start = new Date(startTime);
63
+ const now = new Date();
64
+ const diffMs = now - start;
65
+ const diffMins = Math.floor(diffMs / 60000);
66
+
67
+ if (diffMins < 1) return 'less than 1 minute';
68
+ if (diffMins < 60) return `${diffMins} minutes`;
69
+
70
+ const hours = Math.floor(diffMins / 60);
71
+ const mins = diffMins % 60;
72
+ return `${hours}h ${mins}m`;
73
+ }
74
+
75
+ /**
76
+ * Save session summary to MEMORY.md
77
+ */
78
+ function saveToMemory(sessionState) {
79
+ ensureDirectories();
80
+
81
+ const now = new Date();
82
+ const dateStr = now.toISOString().split('T')[0];
83
+ const duration = sessionState?.session?.startTime
84
+ ? calculateDuration(sessionState.session.startTime)
85
+ : 'unknown';
86
+
87
+ // Generate session entry
88
+ const entry = `### Session ${now.toISOString()}
89
+
90
+ - **Duration**: ${duration}
91
+ - **Project**: ${sessionState?.session?.project || 'unknown'}
92
+ - **Memory entries**: ${sessionState?.memory?.entries || 0}
93
+ - **TODOs**: ${sessionState?.todos?.active || 0} active, ${sessionState?.todos?.completed || 0} completed
94
+
95
+ `;
96
+
97
+ // Read existing memory
98
+ let content = '';
99
+ if (fs.existsSync(MEMORY_FILE)) {
100
+ content = fs.readFileSync(MEMORY_FILE, 'utf-8');
101
+ }
102
+
103
+ // Check if today's section exists
104
+ const todaySection = `## ${dateStr}`;
105
+ if (content.includes(todaySection)) {
106
+ // Append to today's section
107
+ const parts = content.split(todaySection);
108
+ const beforeToday = parts[0];
109
+ const afterHeader = parts[1];
110
+
111
+ // Find next section or end
112
+ const nextSectionMatch = afterHeader.match(/\n## \d{4}-\d{2}-\d{2}/);
113
+ if (nextSectionMatch) {
114
+ const insertPoint = nextSectionMatch.index;
115
+ const todayContent = afterHeader.slice(0, insertPoint);
116
+ const restContent = afterHeader.slice(insertPoint);
117
+ content = beforeToday + todaySection + todayContent + entry + restContent;
118
+ } else {
119
+ content = beforeToday + todaySection + afterHeader + entry;
120
+ }
121
+ } else {
122
+ // Create new day section at the top
123
+ const header = content.startsWith('#') ? '' : '# Memory\n\n<!-- Project memory updated by AI -->\n\n';
124
+ const existingContent = content.replace(/^# Memory\n+(?:<!-- [^>]+ -->\n+)?/, '');
125
+ content = header + `${todaySection}\n\n${entry}` + existingContent;
126
+ }
127
+
128
+ // Keep only last 7 days
129
+ const sections = content.split(/(?=\n## \d{4}-\d{2}-\d{2})/);
130
+ const header = sections[0];
131
+ const daySections = sections.slice(1, 8); // Keep 7 days max
132
+ content = header + daySections.join('');
133
+
134
+ fs.writeFileSync(MEMORY_FILE, content.trim() + '\n');
135
+ }
136
+
137
+ /**
138
+ * Archive session state
139
+ */
140
+ function archiveSession(sessionState) {
141
+ ensureDirectories();
142
+
143
+ const now = new Date();
144
+ const filename = `session_${now.toISOString().replace(/[:.]/g, '-')}.json`;
145
+ const filepath = path.join(SESSIONS_DIR, filename);
146
+
147
+ const archiveData = {
148
+ ...sessionState,
149
+ endTime: now.toISOString(),
150
+ duration: sessionState?.session?.startTime
151
+ ? calculateDuration(sessionState.session.startTime)
152
+ : 'unknown'
153
+ };
154
+
155
+ fs.writeFileSync(filepath, JSON.stringify(archiveData, null, 2));
156
+
157
+ // Clean up old sessions (keep last 20)
158
+ const files = fs.readdirSync(SESSIONS_DIR)
159
+ .filter(f => f.startsWith('session_') && f.endsWith('.json'))
160
+ .sort()
161
+ .reverse();
162
+
163
+ if (files.length > 20) {
164
+ files.slice(20).forEach(f => {
165
+ try {
166
+ fs.unlinkSync(path.join(SESSIONS_DIR, f));
167
+ } catch (e) {
168
+ // Ignore deletion errors
169
+ }
170
+ });
171
+ }
172
+
173
+ return filename;
174
+ }
175
+
176
+ /**
177
+ * Update TODO state sync timestamp
178
+ */
179
+ function syncTodoState() {
180
+ if (!fs.existsSync(STATE_FILE)) {
181
+ return;
182
+ }
183
+
184
+ try {
185
+ const state = JSON.parse(fs.readFileSync(STATE_FILE, 'utf-8'));
186
+ state.lastSynced = new Date().toISOString();
187
+ state.sessionId = SESSION_ID;
188
+ fs.writeFileSync(STATE_FILE, JSON.stringify(state, null, 2));
189
+ } catch (e) {
190
+ // Ignore sync errors
191
+ }
192
+ }
193
+
194
+ /**
195
+ * Clean up session state file
196
+ */
197
+ function cleanupSessionState() {
198
+ if (fs.existsSync(SESSION_STATE_FILE)) {
199
+ try {
200
+ fs.unlinkSync(SESSION_STATE_FILE);
201
+ } catch (e) {
202
+ // Ignore cleanup errors
203
+ }
204
+ }
205
+ }
206
+
207
+ /**
208
+ * Format session end summary
209
+ */
210
+ function formatEndSummary(sessionState, archiveFile) {
211
+ let summary = '';
212
+
213
+ const duration = sessionState?.session?.startTime
214
+ ? calculateDuration(sessionState.session.startTime)
215
+ : 'unknown';
216
+
217
+ summary += `\nโœ… Session ended (${duration})\n`;
218
+ summary += `๐Ÿ’พ Memory saved to MEMORY.md\n`;
219
+ summary += `๐Ÿ“ Archived: ${archiveFile}\n`;
220
+
221
+ return summary;
222
+ }
223
+
224
+ /**
225
+ * Main execution
226
+ */
227
+ function main() {
228
+ try {
229
+ const sessionState = loadSessionState();
230
+
231
+ if (sessionState) {
232
+ // Save to memory
233
+ saveToMemory(sessionState);
234
+
235
+ // Archive session
236
+ const archiveFile = archiveSession(sessionState);
237
+
238
+ // Sync TODO state
239
+ syncTodoState();
240
+
241
+ // Output summary
242
+ console.log(formatEndSummary(sessionState, archiveFile));
243
+ }
244
+
245
+ // Clean up
246
+ cleanupSessionState();
247
+
248
+ process.exit(0);
249
+ } catch (e) {
250
+ // Silent failure - don't interrupt session end
251
+ cleanupSessionState();
252
+ process.exit(0);
253
+ }
254
+ }
255
+
256
+ // Run
257
+ if (require.main === module) {
258
+ main();
259
+ }
260
+
261
+ module.exports = {
262
+ loadSessionState,
263
+ saveToMemory,
264
+ archiveSession,
265
+ syncTodoState,
266
+ calculateDuration,
267
+ formatEndSummary
268
+ };
@@ -1,4 +1,44 @@
1
1
  {
2
+ "env": {
3
+ "ENABLE_TOOL_SEARCH": "true",
4
+ "DISABLE_AUTOUPDATER": "1"
5
+ },
6
+ "SessionStart": [
7
+ {
8
+ "matcher": {},
9
+ "hooks": [
10
+ {
11
+ "type": "command",
12
+ "command": "node \"$CLAUDE_PROJECT_DIR\"/.claude/hooks/memory-loader.cjs",
13
+ "timeout": 2000
14
+ }
15
+ ]
16
+ }
17
+ ],
18
+ "SessionEnd": [
19
+ {
20
+ "matcher": {},
21
+ "hooks": [
22
+ {
23
+ "type": "command",
24
+ "command": "node \"$CLAUDE_PROJECT_DIR\"/.claude/hooks/memory-saver.cjs",
25
+ "timeout": 3000
26
+ }
27
+ ]
28
+ }
29
+ ],
30
+ "PreCompact": [
31
+ {
32
+ "matcher": {},
33
+ "hooks": [
34
+ {
35
+ "type": "command",
36
+ "command": "node \"$CLAUDE_PROJECT_DIR\"/.claude/hooks/auto-handoff.cjs",
37
+ "timeout": 5000
38
+ }
39
+ ]
40
+ }
41
+ ],
2
42
  "UserPromptSubmit": [
3
43
  {
4
44
  "matcher": {},