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 +15 -0
- package/README.md +62 -0
- package/cli.js +1 -1
- package/lib/commands.js +83 -0
- package/package.json +1 -1
- package/template/.claude/hooks/auto-handoff.cjs +353 -0
- package/template/.claude/hooks/memory-loader.cjs +208 -0
- package/template/.claude/hooks/memory-saver.cjs +268 -0
- package/template/.claude/settings.json +40 -0
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
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
|
@@ -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": {},
|