sumulige-claude 1.3.2 → 1.4.0

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.
@@ -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
+ };
@@ -13,6 +13,10 @@ const PROJECT_DIR = process.env.CLAUDE_PROJECT_DIR || process.cwd();
13
13
  const RAG_DIR = path.join(PROJECT_DIR, '.claude/rag');
14
14
  const SKILL_INDEX_FILE = path.join(RAG_DIR, 'skill-index.json');
15
15
  const SKILLS_DIR = path.join(PROJECT_DIR, '.claude/skills');
16
+ const CACHE_FILE = path.join(RAG_DIR, '.match-cache.json');
17
+
18
+ // 缓存配置
19
+ const CACHE_TTL = 300000; // 5分钟缓存有效期
16
20
 
17
21
  // 技能关键词匹配权重
18
22
  const KEYWORD_WEIGHTS = {
@@ -21,6 +25,67 @@ const KEYWORD_WEIGHTS = {
21
25
  related: 0.5 // 相关匹配
22
26
  };
23
27
 
28
+ // 简单哈希函数
29
+ function hashInput(input) {
30
+ let hash = 0;
31
+ for (let i = 0; i < input.length; i++) {
32
+ const char = input.charCodeAt(i);
33
+ hash = ((hash << 5) - hash) + char;
34
+ hash = hash & hash;
35
+ }
36
+ return hash.toString(16);
37
+ }
38
+
39
+ // 加载缓存
40
+ function loadCache() {
41
+ if (!fs.existsSync(CACHE_FILE)) {
42
+ return {};
43
+ }
44
+ try {
45
+ return JSON.parse(fs.readFileSync(CACHE_FILE, 'utf-8'));
46
+ } catch (e) {
47
+ return {};
48
+ }
49
+ }
50
+
51
+ // 保存缓存
52
+ function saveCache(cache) {
53
+ try {
54
+ // 清理过期条目
55
+ const now = Date.now();
56
+ const cleaned = {};
57
+ for (const [key, value] of Object.entries(cache)) {
58
+ if (now - value.timestamp < (value.ttl || CACHE_TTL)) {
59
+ cleaned[key] = value;
60
+ }
61
+ }
62
+ fs.writeFileSync(CACHE_FILE, JSON.stringify(cleaned, null, 2));
63
+ } catch (e) {
64
+ // 忽略保存错误
65
+ }
66
+ }
67
+
68
+ // 从缓存获取结果
69
+ function getCachedResult(inputHash) {
70
+ const cache = loadCache();
71
+ const cached = cache[inputHash];
72
+ if (cached && Date.now() - cached.timestamp < (cached.ttl || CACHE_TTL)) {
73
+ return cached.result;
74
+ }
75
+ return null;
76
+ }
77
+
78
+ // 保存结果到缓存
79
+ function setCachedResult(inputHash, result) {
80
+ const cache = loadCache();
81
+ cache[inputHash] = {
82
+ result,
83
+ timestamp: Date.now(),
84
+ ttl: CACHE_TTL
85
+ };
86
+ saveCache(cache);
87
+ }
88
+
24
89
  // 加载技能索引
25
90
  function loadSkillIndex() {
26
91
  if (!fs.existsSync(SKILL_INDEX_FILE)) {
@@ -134,12 +199,27 @@ function main() {
134
199
  process.exit(0);
135
200
  }
136
201
 
137
- const analysis = analyzeInput(toolInput);
138
- if (analysis.keywords.length === 0) {
139
- process.exit(0);
202
+ // 检查缓存
203
+ const inputHash = hashInput(toolInput);
204
+ const cachedResult = getCachedResult(inputHash);
205
+
206
+ let matches;
207
+ if (cachedResult) {
208
+ // 使用缓存结果
209
+ matches = cachedResult;
210
+ } else {
211
+ // 分析并匹配
212
+ const analysis = analyzeInput(toolInput);
213
+ if (analysis.keywords.length === 0) {
214
+ process.exit(0);
215
+ }
216
+
217
+ matches = matchSkills(analysis, skillIndex);
218
+
219
+ // 缓存结果
220
+ setCachedResult(inputHash, matches);
140
221
  }
141
222
 
142
- const matches = matchSkills(analysis, skillIndex);
143
223
  if (matches.length === 0) {
144
224
  process.exit(0);
145
225
  }