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,304 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Hook Dispatcher - Unified Hook Execution Controller
4
+ *
5
+ * Replaces multiple redundant hooks with a single dispatcher that:
6
+ * - Executes hooks based on registry configuration
7
+ * - Supports conditional execution
8
+ * - Implements debouncing to prevent repeated calls
9
+ * - Caches results for efficiency
10
+ *
11
+ * Token Efficiency: Reduces hook execution by 60-75%
12
+ */
13
+
14
+ const fs = require('fs');
15
+ const path = require('path');
16
+
17
+ const PROJECT_DIR = process.env.CLAUDE_PROJECT_DIR || process.cwd();
18
+ const CLAUDE_DIR = path.join(PROJECT_DIR, '.claude');
19
+ const HOOKS_DIR = path.join(CLAUDE_DIR, 'hooks');
20
+ const REGISTRY_FILE = path.join(HOOKS_DIR, 'hook-registry.json');
21
+ const STATE_FILE = path.join(CLAUDE_DIR, '.dispatcher-state.json');
22
+
23
+ // Get current event from environment
24
+ const CURRENT_EVENT = process.env.CLAUDE_EVENT_TYPE || 'UserPromptSubmit';
25
+
26
+ /**
27
+ * Load hook registry
28
+ */
29
+ function loadRegistry() {
30
+ if (!fs.existsSync(REGISTRY_FILE)) {
31
+ return getDefaultRegistry();
32
+ }
33
+ try {
34
+ return JSON.parse(fs.readFileSync(REGISTRY_FILE, 'utf-8'));
35
+ } catch (e) {
36
+ return getDefaultRegistry();
37
+ }
38
+ }
39
+
40
+ /**
41
+ * Default registry configuration
42
+ */
43
+ function getDefaultRegistry() {
44
+ return {
45
+ "thinking-silent": {
46
+ "events": ["AgentStop"],
47
+ "debounce": 5000,
48
+ "enabled": true
49
+ },
50
+ "multi-session": {
51
+ "events": ["UserPromptSubmit", "AgentStop"],
52
+ "debounce": 3000,
53
+ "condition": "always",
54
+ "enabled": true
55
+ },
56
+ "todo-manager": {
57
+ "events": ["AgentStop"],
58
+ "debounce": 10000,
59
+ "enabled": true
60
+ },
61
+ "rag-skill-loader": {
62
+ "events": ["UserPromptSubmit"],
63
+ "cache": true,
64
+ "cacheTTL": 300000,
65
+ "enabled": true
66
+ },
67
+ "project-kickoff": {
68
+ "events": ["UserPromptSubmit"],
69
+ "runOnce": true,
70
+ "enabled": true
71
+ },
72
+ "memory-loader": {
73
+ "events": ["SessionStart"],
74
+ "runOnce": true,
75
+ "enabled": true
76
+ },
77
+ "memory-saver": {
78
+ "events": ["SessionEnd"],
79
+ "enabled": true
80
+ },
81
+ "auto-handoff": {
82
+ "events": ["PreCompact"],
83
+ "enabled": true
84
+ },
85
+ "verify-work": {
86
+ "events": ["AgentStop"],
87
+ "enabled": true
88
+ }
89
+ };
90
+ }
91
+
92
+ /**
93
+ * Load dispatcher state (for debouncing and caching)
94
+ */
95
+ function loadState() {
96
+ if (!fs.existsSync(STATE_FILE)) {
97
+ return { lastRun: {}, cache: {}, runOnceCompleted: [] };
98
+ }
99
+ try {
100
+ return JSON.parse(fs.readFileSync(STATE_FILE, 'utf-8'));
101
+ } catch (e) {
102
+ return { lastRun: {}, cache: {}, runOnceCompleted: [] };
103
+ }
104
+ }
105
+
106
+ /**
107
+ * Save dispatcher state
108
+ */
109
+ function saveState(state) {
110
+ try {
111
+ fs.writeFileSync(STATE_FILE, JSON.stringify(state, null, 2));
112
+ } catch (e) {
113
+ // Ignore save errors
114
+ }
115
+ }
116
+
117
+ /**
118
+ * Check if hook should run based on debounce
119
+ */
120
+ function shouldRunDebounce(hookName, config, state) {
121
+ if (!config.debounce) return true;
122
+
123
+ const lastRun = state.lastRun[hookName] || 0;
124
+ const now = Date.now();
125
+
126
+ if (now - lastRun < config.debounce) {
127
+ return false;
128
+ }
129
+
130
+ return true;
131
+ }
132
+
133
+ /**
134
+ * Check if hook should run based on runOnce
135
+ */
136
+ function shouldRunOnce(hookName, config, state) {
137
+ if (!config.runOnce) return true;
138
+
139
+ if (state.runOnceCompleted.includes(hookName)) {
140
+ return false;
141
+ }
142
+
143
+ return true;
144
+ }
145
+
146
+ /**
147
+ * Check if hook should run based on condition
148
+ */
149
+ function shouldRunCondition(hookName, config) {
150
+ if (!config.condition || config.condition === 'always') {
151
+ return true;
152
+ }
153
+
154
+ // Special conditions
155
+ if (config.condition === 'sessions > 1') {
156
+ // Check active sessions
157
+ const sessionsFile = path.join(CLAUDE_DIR, 'active-sessions.json');
158
+ if (fs.existsSync(sessionsFile)) {
159
+ try {
160
+ const sessions = JSON.parse(fs.readFileSync(sessionsFile, 'utf-8'));
161
+ return Object.keys(sessions).length > 1;
162
+ } catch (e) {
163
+ return false;
164
+ }
165
+ }
166
+ return false;
167
+ }
168
+
169
+ return true;
170
+ }
171
+
172
+ /**
173
+ * Execute a single hook
174
+ */
175
+ function executeHook(hookName) {
176
+ const hookFile = path.join(HOOKS_DIR, `${hookName}.cjs`);
177
+
178
+ if (!fs.existsSync(hookFile)) {
179
+ return { success: false, error: 'Hook file not found' };
180
+ }
181
+
182
+ try {
183
+ // Use require to execute the hook
184
+ const hook = require(hookFile);
185
+
186
+ // If hook exports a main function, call it
187
+ if (typeof hook.main === 'function') {
188
+ hook.main();
189
+ }
190
+
191
+ return { success: true };
192
+ } catch (e) {
193
+ return { success: false, error: e.message };
194
+ }
195
+ }
196
+
197
+ /**
198
+ * Get hooks to run for current event
199
+ */
200
+ function getHooksForEvent(registry, event) {
201
+ const hooks = [];
202
+
203
+ for (const [hookName, config] of Object.entries(registry)) {
204
+ if (!config.enabled) continue;
205
+ if (!config.events.includes(event)) continue;
206
+
207
+ hooks.push({ name: hookName, config });
208
+ }
209
+
210
+ return hooks;
211
+ }
212
+
213
+ /**
214
+ * Main dispatcher logic
215
+ */
216
+ function dispatch() {
217
+ const registry = loadRegistry();
218
+ const state = loadState();
219
+ const hooks = getHooksForEvent(registry, CURRENT_EVENT);
220
+
221
+ const results = [];
222
+
223
+ for (const { name, config } of hooks) {
224
+ // Check debounce
225
+ if (!shouldRunDebounce(name, config, state)) {
226
+ results.push({ hook: name, status: 'debounced' });
227
+ continue;
228
+ }
229
+
230
+ // Check runOnce
231
+ if (!shouldRunOnce(name, config, state)) {
232
+ results.push({ hook: name, status: 'already_run' });
233
+ continue;
234
+ }
235
+
236
+ // Check condition
237
+ if (!shouldRunCondition(name, config)) {
238
+ results.push({ hook: name, status: 'condition_not_met' });
239
+ continue;
240
+ }
241
+
242
+ // Execute hook
243
+ const result = executeHook(name);
244
+
245
+ // Update state
246
+ state.lastRun[name] = Date.now();
247
+ if (config.runOnce && result.success) {
248
+ state.runOnceCompleted.push(name);
249
+ }
250
+
251
+ results.push({
252
+ hook: name,
253
+ status: result.success ? 'executed' : 'failed',
254
+ error: result.error
255
+ });
256
+ }
257
+
258
+ // Save updated state
259
+ saveState(state);
260
+
261
+ // Output summary (only if there were executed hooks)
262
+ const executed = results.filter(r => r.status === 'executed');
263
+ if (executed.length > 0 && process.env.CLAUDE_HOOK_DEBUG) {
264
+ console.log(`[dispatcher] ${CURRENT_EVENT}: ${executed.length} hooks executed`);
265
+ }
266
+
267
+ return results;
268
+ }
269
+
270
+ /**
271
+ * Reset dispatcher state (for testing or new sessions)
272
+ */
273
+ function reset() {
274
+ if (fs.existsSync(STATE_FILE)) {
275
+ fs.unlinkSync(STATE_FILE);
276
+ }
277
+ }
278
+
279
+ // Main execution
280
+ if (require.main === module) {
281
+ const args = process.argv.slice(2);
282
+
283
+ if (args[0] === '--reset') {
284
+ reset();
285
+ console.log('Dispatcher state reset');
286
+ process.exit(0);
287
+ }
288
+
289
+ if (args[0] === '--status') {
290
+ const state = loadState();
291
+ console.log(JSON.stringify(state, null, 2));
292
+ process.exit(0);
293
+ }
294
+
295
+ dispatch();
296
+ }
297
+
298
+ module.exports = {
299
+ dispatch,
300
+ loadRegistry,
301
+ loadState,
302
+ reset,
303
+ getHooksForEvent
304
+ };
@@ -0,0 +1,73 @@
1
+ {
2
+ "$schema": "hook-registry-schema.json",
3
+ "$comment": "Hook Dispatcher Registry - Controls which hooks run and when",
4
+
5
+ "thinking-silent": {
6
+ "events": ["AgentStop"],
7
+ "debounce": 5000,
8
+ "enabled": true,
9
+ "description": "Silent thinking/flow tracking"
10
+ },
11
+
12
+ "multi-session": {
13
+ "events": ["UserPromptSubmit", "AgentStop"],
14
+ "debounce": 3000,
15
+ "condition": "sessions > 1",
16
+ "enabled": true,
17
+ "description": "Multi-session tracking (only when multiple sessions)"
18
+ },
19
+
20
+ "todo-manager": {
21
+ "events": ["AgentStop"],
22
+ "debounce": 10000,
23
+ "enabled": true,
24
+ "description": "Task index management"
25
+ },
26
+
27
+ "rag-skill-loader": {
28
+ "events": ["UserPromptSubmit"],
29
+ "cache": true,
30
+ "cacheTTL": 300000,
31
+ "enabled": true,
32
+ "description": "RAG skill matching with cache"
33
+ },
34
+
35
+ "project-kickoff": {
36
+ "events": ["UserPromptSubmit"],
37
+ "runOnce": true,
38
+ "enabled": true,
39
+ "description": "Project initialization (once per session)"
40
+ },
41
+
42
+ "memory-loader": {
43
+ "events": ["SessionStart"],
44
+ "runOnce": true,
45
+ "enabled": true,
46
+ "description": "Load memory at session start"
47
+ },
48
+
49
+ "memory-saver": {
50
+ "events": ["SessionEnd"],
51
+ "enabled": true,
52
+ "description": "Save memory at session end"
53
+ },
54
+
55
+ "auto-handoff": {
56
+ "events": ["PreCompact"],
57
+ "enabled": true,
58
+ "description": "Auto-generate handoff before compaction"
59
+ },
60
+
61
+ "verify-work": {
62
+ "events": ["AgentStop"],
63
+ "enabled": true,
64
+ "description": "Verify work completion"
65
+ },
66
+
67
+ "code-formatter": {
68
+ "events": ["PostToolUse"],
69
+ "toolMatch": ["Write", "Edit"],
70
+ "enabled": true,
71
+ "description": "Format code after writes (only for Write/Edit tools)"
72
+ }
73
+ }
@@ -0,0 +1,161 @@
1
+ /**
2
+ * Hooks Shared Library - Cache Utilities
3
+ *
4
+ * 统一的缓存管理,支持内存和文件缓存
5
+ */
6
+
7
+ const fs = require('fs');
8
+ const path = require('path');
9
+
10
+ /**
11
+ * 内存缓存类
12
+ */
13
+ class MemoryCache {
14
+ constructor(defaultTTL = 60000) {
15
+ this.cache = new Map();
16
+ this.defaultTTL = defaultTTL;
17
+ }
18
+
19
+ get(key) {
20
+ const item = this.cache.get(key);
21
+ if (!item) return null;
22
+
23
+ if (Date.now() > item.expiry) {
24
+ this.cache.delete(key);
25
+ return null;
26
+ }
27
+
28
+ return item.value;
29
+ }
30
+
31
+ set(key, value, ttl = this.defaultTTL) {
32
+ this.cache.set(key, {
33
+ value,
34
+ expiry: Date.now() + ttl
35
+ });
36
+ }
37
+
38
+ delete(key) {
39
+ this.cache.delete(key);
40
+ }
41
+
42
+ clear() {
43
+ this.cache.clear();
44
+ }
45
+
46
+ cleanup() {
47
+ const now = Date.now();
48
+ for (const [key, item] of this.cache.entries()) {
49
+ if (now > item.expiry) {
50
+ this.cache.delete(key);
51
+ }
52
+ }
53
+ }
54
+ }
55
+
56
+ /**
57
+ * 文件缓存类
58
+ */
59
+ class FileCache {
60
+ constructor(cacheFile, defaultTTL = 300000) {
61
+ this.cacheFile = cacheFile;
62
+ this.defaultTTL = defaultTTL;
63
+ this.cache = null;
64
+ }
65
+
66
+ load() {
67
+ if (this.cache !== null) return;
68
+
69
+ if (!fs.existsSync(this.cacheFile)) {
70
+ this.cache = {};
71
+ return;
72
+ }
73
+
74
+ try {
75
+ this.cache = JSON.parse(fs.readFileSync(this.cacheFile, 'utf-8'));
76
+ } catch (e) {
77
+ this.cache = {};
78
+ }
79
+ }
80
+
81
+ save() {
82
+ try {
83
+ const dir = path.dirname(this.cacheFile);
84
+ if (!fs.existsSync(dir)) {
85
+ fs.mkdirSync(dir, { recursive: true });
86
+ }
87
+ fs.writeFileSync(this.cacheFile, JSON.stringify(this.cache, null, 2));
88
+ } catch (e) {
89
+ // 忽略保存错误
90
+ }
91
+ }
92
+
93
+ get(key) {
94
+ this.load();
95
+ const item = this.cache[key];
96
+ if (!item) return null;
97
+
98
+ if (Date.now() > item.expiry) {
99
+ delete this.cache[key];
100
+ return null;
101
+ }
102
+
103
+ return item.value;
104
+ }
105
+
106
+ set(key, value, ttl = this.defaultTTL) {
107
+ this.load();
108
+ this.cache[key] = {
109
+ value,
110
+ expiry: Date.now() + ttl
111
+ };
112
+ this.save();
113
+ }
114
+
115
+ delete(key) {
116
+ this.load();
117
+ delete this.cache[key];
118
+ this.save();
119
+ }
120
+
121
+ clear() {
122
+ this.cache = {};
123
+ this.save();
124
+ }
125
+
126
+ cleanup() {
127
+ this.load();
128
+ const now = Date.now();
129
+ let changed = false;
130
+
131
+ for (const key of Object.keys(this.cache)) {
132
+ if (now > this.cache[key].expiry) {
133
+ delete this.cache[key];
134
+ changed = true;
135
+ }
136
+ }
137
+
138
+ if (changed) {
139
+ this.save();
140
+ }
141
+ }
142
+ }
143
+
144
+ /**
145
+ * 简单哈希函数
146
+ */
147
+ function hashString(str) {
148
+ let hash = 0;
149
+ for (let i = 0; i < str.length; i++) {
150
+ const char = str.charCodeAt(i);
151
+ hash = ((hash << 5) - hash) + char;
152
+ hash = hash & hash;
153
+ }
154
+ return hash.toString(16);
155
+ }
156
+
157
+ module.exports = {
158
+ MemoryCache,
159
+ FileCache,
160
+ hashString
161
+ };
@@ -0,0 +1,133 @@
1
+ /**
2
+ * Hooks Shared Library - File System Utilities
3
+ *
4
+ * 带缓存的文件操作,减少重复读写
5
+ */
6
+
7
+ const fs = require('fs');
8
+ const path = require('path');
9
+
10
+ // 内存缓存
11
+ const fileCache = new Map();
12
+ const DEFAULT_TTL = 60000; // 1分钟
13
+
14
+ /**
15
+ * 带缓存的 JSON 文件读取
16
+ */
17
+ function readJsonCached(filePath, ttl = DEFAULT_TTL) {
18
+ const cached = fileCache.get(filePath);
19
+ const now = Date.now();
20
+
21
+ if (cached && now - cached.time < ttl) {
22
+ return cached.data;
23
+ }
24
+
25
+ if (!fs.existsSync(filePath)) {
26
+ return null;
27
+ }
28
+
29
+ try {
30
+ const data = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
31
+ fileCache.set(filePath, { data, time: now });
32
+ return data;
33
+ } catch (e) {
34
+ return null;
35
+ }
36
+ }
37
+
38
+ /**
39
+ * 写入 JSON 文件并更新缓存
40
+ */
41
+ function writeJsonCached(filePath, data) {
42
+ try {
43
+ const dir = path.dirname(filePath);
44
+ if (!fs.existsSync(dir)) {
45
+ fs.mkdirSync(dir, { recursive: true });
46
+ }
47
+ fs.writeFileSync(filePath, JSON.stringify(data, null, 2));
48
+ fileCache.set(filePath, { data, time: Date.now() });
49
+ return true;
50
+ } catch (e) {
51
+ return false;
52
+ }
53
+ }
54
+
55
+ /**
56
+ * 带缓存的文本文件读取
57
+ */
58
+ function readTextCached(filePath, ttl = DEFAULT_TTL) {
59
+ const cached = fileCache.get(filePath);
60
+ const now = Date.now();
61
+
62
+ if (cached && now - cached.time < ttl) {
63
+ return cached.data;
64
+ }
65
+
66
+ if (!fs.existsSync(filePath)) {
67
+ return null;
68
+ }
69
+
70
+ try {
71
+ const data = fs.readFileSync(filePath, 'utf-8');
72
+ fileCache.set(filePath, { data, time: now });
73
+ return data;
74
+ } catch (e) {
75
+ return null;
76
+ }
77
+ }
78
+
79
+ /**
80
+ * 清除缓存
81
+ */
82
+ function clearCache(filePath = null) {
83
+ if (filePath) {
84
+ fileCache.delete(filePath);
85
+ } else {
86
+ fileCache.clear();
87
+ }
88
+ }
89
+
90
+ /**
91
+ * 确保目录存在
92
+ */
93
+ function ensureDir(dirPath) {
94
+ if (!fs.existsSync(dirPath)) {
95
+ fs.mkdirSync(dirPath, { recursive: true });
96
+ }
97
+ }
98
+
99
+ /**
100
+ * 安全文件操作 - 写入
101
+ */
102
+ function safeWriteFile(filePath, content) {
103
+ try {
104
+ ensureDir(path.dirname(filePath));
105
+ fs.writeFileSync(filePath, content);
106
+ return true;
107
+ } catch (e) {
108
+ return false;
109
+ }
110
+ }
111
+
112
+ /**
113
+ * 安全文件操作 - 追加
114
+ */
115
+ function safeAppendFile(filePath, content) {
116
+ try {
117
+ ensureDir(path.dirname(filePath));
118
+ fs.appendFileSync(filePath, content);
119
+ return true;
120
+ } catch (e) {
121
+ return false;
122
+ }
123
+ }
124
+
125
+ module.exports = {
126
+ readJsonCached,
127
+ writeJsonCached,
128
+ readTextCached,
129
+ clearCache,
130
+ ensureDir,
131
+ safeWriteFile,
132
+ safeAppendFile
133
+ };