@pcircle/memesh 2.8.11 → 2.9.1

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.
Files changed (79) hide show
  1. package/LICENSE +21 -661
  2. package/README.de.md +171 -0
  3. package/README.es.md +171 -0
  4. package/README.fr.md +171 -0
  5. package/README.id.md +171 -0
  6. package/README.ja.md +171 -0
  7. package/README.ko.md +171 -0
  8. package/README.md +73 -100
  9. package/README.th.md +171 -0
  10. package/README.vi.md +171 -0
  11. package/README.zh-CN.md +171 -0
  12. package/README.zh-TW.md +71 -98
  13. package/dist/knowledge-graph/index.d.ts +22 -1
  14. package/dist/knowledge-graph/index.d.ts.map +1 -1
  15. package/dist/knowledge-graph/index.js +144 -3
  16. package/dist/knowledge-graph/index.js.map +1 -1
  17. package/dist/mcp/ServerInitializer.d.ts.map +1 -1
  18. package/dist/mcp/ServerInitializer.js +1 -1
  19. package/dist/mcp/ServerInitializer.js.map +1 -1
  20. package/dist/mcp/ToolDefinitions.d.ts.map +1 -1
  21. package/dist/mcp/ToolDefinitions.js +47 -55
  22. package/dist/mcp/ToolDefinitions.js.map +1 -1
  23. package/dist/mcp/ToolRouter.d.ts.map +1 -1
  24. package/dist/mcp/ToolRouter.js +4 -4
  25. package/dist/mcp/ToolRouter.js.map +1 -1
  26. package/dist/mcp/daemon/StdioProxyClient.d.ts.map +1 -1
  27. package/dist/mcp/daemon/StdioProxyClient.js +9 -1
  28. package/dist/mcp/daemon/StdioProxyClient.js.map +1 -1
  29. package/dist/mcp/handlers/BuddyHandlers.d.ts +3 -1
  30. package/dist/mcp/handlers/BuddyHandlers.d.ts.map +1 -1
  31. package/dist/mcp/handlers/BuddyHandlers.js +6 -5
  32. package/dist/mcp/handlers/BuddyHandlers.js.map +1 -1
  33. package/dist/mcp/handlers/ToolHandlers.d.ts.map +1 -1
  34. package/dist/mcp/handlers/ToolHandlers.js +1 -2
  35. package/dist/mcp/handlers/ToolHandlers.js.map +1 -1
  36. package/dist/mcp/resources/quick-reference.md +1 -1
  37. package/dist/mcp/schemas/OutputSchemas.d.ts +116 -53
  38. package/dist/mcp/schemas/OutputSchemas.d.ts.map +1 -1
  39. package/dist/mcp/schemas/OutputSchemas.js +64 -26
  40. package/dist/mcp/schemas/OutputSchemas.js.map +1 -1
  41. package/dist/mcp/server-bootstrap.js +89 -9
  42. package/dist/mcp/server-bootstrap.js.map +1 -1
  43. package/dist/mcp/tools/buddy-do.d.ts +2 -1
  44. package/dist/mcp/tools/buddy-do.d.ts.map +1 -1
  45. package/dist/mcp/tools/buddy-do.js +91 -4
  46. package/dist/mcp/tools/buddy-do.js.map +1 -1
  47. package/dist/mcp/tools/buddy-remember.d.ts +0 -5
  48. package/dist/mcp/tools/buddy-remember.d.ts.map +1 -1
  49. package/dist/mcp/tools/buddy-remember.js.map +1 -1
  50. package/dist/mcp/tools/memesh-agent-register.d.ts +20 -0
  51. package/dist/mcp/tools/memesh-agent-register.d.ts.map +1 -0
  52. package/dist/mcp/tools/memesh-agent-register.js +80 -0
  53. package/dist/mcp/tools/memesh-agent-register.js.map +1 -0
  54. package/dist/mcp/tools/memesh-cloud-sync.js +27 -8
  55. package/dist/mcp/tools/memesh-cloud-sync.js.map +1 -1
  56. package/dist/mcp/tools/memesh-metrics.d.ts +13 -0
  57. package/dist/mcp/tools/memesh-metrics.d.ts.map +1 -0
  58. package/dist/mcp/tools/memesh-metrics.js +193 -0
  59. package/dist/mcp/tools/memesh-metrics.js.map +1 -0
  60. package/dist/memory/UnifiedMemoryStore.d.ts +1 -1
  61. package/dist/memory/UnifiedMemoryStore.d.ts.map +1 -1
  62. package/dist/memory/UnifiedMemoryStore.js +4 -3
  63. package/dist/memory/UnifiedMemoryStore.js.map +1 -1
  64. package/package.json +9 -12
  65. package/plugin.json +2 -2
  66. package/scripts/hooks/README.md +230 -0
  67. package/scripts/hooks/__tests__/hook-test-harness.js +218 -0
  68. package/scripts/hooks/__tests__/hooks.test.js +267 -0
  69. package/scripts/hooks/hook-utils.js +899 -0
  70. package/scripts/hooks/post-commit.js +307 -0
  71. package/scripts/hooks/post-tool-use.js +812 -0
  72. package/scripts/hooks/pre-tool-use.js +462 -0
  73. package/scripts/hooks/session-start.js +544 -0
  74. package/scripts/hooks/stop.js +673 -0
  75. package/scripts/hooks/subagent-stop.js +184 -0
  76. package/scripts/hooks/templates/planning-template.md +46 -0
  77. package/scripts/postinstall-lib.js +8 -4
  78. package/scripts/postinstall-new.js +110 -7
  79. package/scripts/skills/comprehensive-code-review/SKILL.md +276 -0
@@ -0,0 +1,812 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * PostToolUse Hook - Claude Code Event-Driven Hooks
5
+ *
6
+ * Triggered after each tool execution in Claude Code.
7
+ *
8
+ * Features (Silent Observer):
9
+ * - Reads tool execution data from stdin
10
+ * - Detects patterns (READ_BEFORE_EDIT, Git workflows, Frontend work, Search patterns)
11
+ * - Detects anomalies (slow execution, high tokens, failures, quota warnings)
12
+ * - Updates recommendations.json incrementally
13
+ * - Updates current-session.json
14
+ * - Auto-saves key points to MeMesh when token threshold reached
15
+ * - Runs silently (no console output - non-intrusive)
16
+ */
17
+
18
+ import {
19
+ STATE_DIR,
20
+ MEMESH_DB_PATH,
21
+ THRESHOLDS,
22
+ readJSONFile,
23
+ writeJSONFile,
24
+ writeJSONFileAsync,
25
+ sqliteBatchEntity,
26
+ readStdin,
27
+ logError,
28
+ logMemorySave,
29
+ getDateString,
30
+ isPlanFile,
31
+ parsePlanSteps,
32
+ derivePlanName,
33
+ sqliteQueryJSON,
34
+ updateEntityMetadata,
35
+ addObservation,
36
+ } from './hook-utils.js';
37
+ import fs from 'fs';
38
+ import path from 'path';
39
+
40
+ // ============================================================================
41
+ // File Paths
42
+ // ============================================================================
43
+
44
+ const RECOMMENDATIONS_FILE = path.join(STATE_DIR, 'recommendations.json');
45
+ const CURRENT_SESSION_FILE = path.join(STATE_DIR, 'current-session.json');
46
+ const SESSION_CONTEXT_FILE = path.join(STATE_DIR, 'session-context.json');
47
+
48
+ // ============================================================================
49
+ // Pattern Detection
50
+ // ============================================================================
51
+
52
+ /**
53
+ * Pattern Detector - Analyzes tool usage patterns
54
+ */
55
+ class PatternDetector {
56
+ constructor() {
57
+ this.recentTools = [];
58
+ }
59
+
60
+ /**
61
+ * Add a tool call to the recent tools list
62
+ * @param {Object} toolData - Tool execution data
63
+ */
64
+ addToolCall(toolData) {
65
+ this.recentTools.push({
66
+ toolName: toolData.toolName,
67
+ args: toolData.arguments,
68
+ timestamp: new Date().toISOString(),
69
+ });
70
+
71
+ // Keep only last 10 (using slice instead of shift for better performance)
72
+ if (this.recentTools.length > 10) {
73
+ this.recentTools = this.recentTools.slice(-10);
74
+ }
75
+ }
76
+
77
+ /**
78
+ * Detect patterns in recent tool usage
79
+ * @param {Object} currentTool - Current tool execution data
80
+ * @returns {Array} Detected patterns
81
+ */
82
+ detectPatterns(currentTool) {
83
+ const patterns = [];
84
+
85
+ if (!currentTool || !currentTool.toolName) {
86
+ return patterns;
87
+ }
88
+
89
+ const toolArgs = currentTool.arguments || {};
90
+
91
+ // Pattern 1: READ_BEFORE_EDIT
92
+ if (currentTool.toolName === 'Edit') {
93
+ const filePath = toolArgs.file_path;
94
+ if (filePath) {
95
+ const recentReads = this.recentTools.filter(t =>
96
+ t.toolName === 'Read' && t.args?.file_path === filePath
97
+ );
98
+ if (recentReads.length > 0) {
99
+ patterns.push({
100
+ type: 'READ_BEFORE_EDIT',
101
+ description: 'Read before Edit - correct behavior',
102
+ severity: 'info',
103
+ });
104
+ } else {
105
+ patterns.push({
106
+ type: 'EDIT_WITHOUT_READ',
107
+ description: `Edit ${path.basename(filePath)} without prior Read`,
108
+ severity: 'warning',
109
+ });
110
+ }
111
+ }
112
+ }
113
+
114
+ // Pattern 2: Git Workflow
115
+ const gitCommands = ['git add', 'git commit', 'git push', 'git branch', 'git checkout'];
116
+ if (currentTool.toolName === 'Bash' && toolArgs.command) {
117
+ const cmd = toolArgs.command;
118
+ if (gitCommands.some(gitCmd => cmd.includes(gitCmd))) {
119
+ const recentGitOps = this.recentTools.filter(t =>
120
+ t.toolName === 'Bash' && gitCommands.some(gc => t.args?.command?.includes(gc))
121
+ ).length;
122
+
123
+ if (recentGitOps >= 2) {
124
+ patterns.push({
125
+ type: 'GIT_WORKFLOW',
126
+ description: `Git workflow detected (${recentGitOps + 1} operations)`,
127
+ severity: 'info',
128
+ suggestion: 'Consider loading devops-git-workflows skill',
129
+ });
130
+ }
131
+ }
132
+ }
133
+
134
+ // Pattern 3: Frontend Work
135
+ const frontendExtensions = ['.tsx', '.jsx', '.vue', '.svelte', '.css', '.scss'];
136
+ if (['Edit', 'Write', 'Read'].includes(currentTool.toolName)) {
137
+ const filePath = toolArgs.file_path;
138
+ if (filePath && frontendExtensions.some(ext => filePath.endsWith(ext))) {
139
+ const recentFrontendOps = this.recentTools.filter(t =>
140
+ ['Edit', 'Write', 'Read'].includes(t.toolName) &&
141
+ frontendExtensions.some(ext => t.args?.file_path?.endsWith(ext))
142
+ ).length;
143
+
144
+ if (recentFrontendOps >= 2) {
145
+ patterns.push({
146
+ type: 'FRONTEND_WORK',
147
+ description: `Frontend work detected (${recentFrontendOps + 1} files)`,
148
+ severity: 'info',
149
+ suggestion: 'Consider loading frontend-design skill',
150
+ });
151
+ }
152
+ }
153
+ }
154
+
155
+ // Pattern 4: Intensive Search
156
+ if (['Grep', 'Glob'].includes(currentTool.toolName)) {
157
+ const recentSearches = this.recentTools.filter(t =>
158
+ ['Grep', 'Glob'].includes(t.toolName)
159
+ ).length;
160
+
161
+ if (recentSearches >= 3) {
162
+ patterns.push({
163
+ type: 'INTENSIVE_SEARCH',
164
+ description: `Multiple search operations (${recentSearches + 1} times)`,
165
+ severity: 'info',
166
+ });
167
+ }
168
+ }
169
+
170
+ return patterns;
171
+ }
172
+ }
173
+
174
+ // ============================================================================
175
+ // Anomaly Detection
176
+ // ============================================================================
177
+
178
+ /**
179
+ * Detect anomalies in tool execution
180
+ * @param {Object} toolData - Tool execution data
181
+ * @param {Object} sessionContext - Session context with quota info
182
+ * @returns {Array} Detected anomalies
183
+ */
184
+ function detectAnomalies(toolData, sessionContext) {
185
+ const anomalies = [];
186
+
187
+ // Anomaly 1: Slow Execution
188
+ if (toolData.duration && toolData.duration > THRESHOLDS.SLOW_EXECUTION) {
189
+ anomalies.push({
190
+ type: 'SLOW_EXECUTION',
191
+ description: `${toolData.toolName} took ${(toolData.duration / 1000).toFixed(1)}s (slow)`,
192
+ severity: 'warning',
193
+ });
194
+ }
195
+
196
+ // Anomaly 2: High Token Usage
197
+ if (toolData.tokensUsed && toolData.tokensUsed > THRESHOLDS.HIGH_TOKENS) {
198
+ anomalies.push({
199
+ type: 'HIGH_TOKENS',
200
+ description: `${toolData.toolName} used ${toolData.tokensUsed} tokens (high usage)`,
201
+ severity: 'warning',
202
+ });
203
+ }
204
+
205
+ // Anomaly 3: Execution Failure
206
+ if (toolData.success === false) {
207
+ anomalies.push({
208
+ type: 'EXECUTION_FAILURE',
209
+ description: `${toolData.toolName} execution failed`,
210
+ severity: 'error',
211
+ });
212
+ }
213
+
214
+ // Anomaly 4: Quota Warning
215
+ if (sessionContext.tokenQuota) {
216
+ const quotaUsed = sessionContext.tokenQuota.used + (toolData.tokensUsed || 0);
217
+ const quotaPercentage = quotaUsed / sessionContext.tokenQuota.limit;
218
+
219
+ if (quotaPercentage > THRESHOLDS.QUOTA_WARNING) {
220
+ anomalies.push({
221
+ type: 'QUOTA_WARNING',
222
+ description: `Token quota at ${(quotaPercentage * 100).toFixed(1)}%`,
223
+ severity: 'warning',
224
+ });
225
+ }
226
+ }
227
+
228
+ return anomalies;
229
+ }
230
+
231
+ // ============================================================================
232
+ // Recommendations Update
233
+ // ============================================================================
234
+
235
+ /**
236
+ * Update recommendations based on detected patterns and anomalies
237
+ * @param {Array} patterns - Detected patterns
238
+ * @param {Array} anomalies - Detected anomalies
239
+ */
240
+ function updateRecommendations(patterns, anomalies) {
241
+ const recommendations = readJSONFile(RECOMMENDATIONS_FILE, {
242
+ recommendedSkills: [],
243
+ detectedPatterns: [],
244
+ warnings: [],
245
+ lastUpdated: null,
246
+ });
247
+
248
+ // Add new skills based on patterns
249
+ patterns.forEach(pattern => {
250
+ if (pattern.suggestion && pattern.suggestion.includes('skill')) {
251
+ const skillMatch = pattern.suggestion.match(/loading\s+(.+?)\s+skill/);
252
+ if (skillMatch) {
253
+ const skillName = skillMatch[1];
254
+ const existing = recommendations.recommendedSkills.find(s => s.name === skillName);
255
+ if (!existing) {
256
+ recommendations.recommendedSkills.push({
257
+ name: skillName,
258
+ reason: pattern.description,
259
+ priority: 'medium',
260
+ });
261
+ }
262
+ }
263
+ }
264
+ });
265
+
266
+ // Add detected patterns (keep last 10)
267
+ patterns.forEach(pattern => {
268
+ if (pattern && pattern.description) {
269
+ recommendations.detectedPatterns.unshift({
270
+ description: pattern.description,
271
+ suggestion: pattern.suggestion || '',
272
+ timestamp: new Date().toISOString(),
273
+ });
274
+ }
275
+ });
276
+ recommendations.detectedPatterns = recommendations.detectedPatterns.slice(0, 10);
277
+
278
+ // Add warnings from anomalies (keep last 5)
279
+ anomalies
280
+ .filter(a => a.severity === 'warning' || a.severity === 'error')
281
+ .forEach(anomaly => {
282
+ recommendations.warnings.unshift(anomaly.description);
283
+ });
284
+ recommendations.warnings = recommendations.warnings.slice(0, 5);
285
+
286
+ recommendations.lastUpdated = new Date().toISOString();
287
+
288
+ // Async write — non-blocking, caller awaits via pendingWrites
289
+ return writeJSONFileAsync(RECOMMENDATIONS_FILE, recommendations);
290
+ }
291
+
292
+ // ============================================================================
293
+ // Session Update
294
+ // ============================================================================
295
+
296
+ /**
297
+ * Update current session with tool call data
298
+ * @param {Object} toolData - Tool execution data
299
+ * @param {Array} patterns - Detected patterns
300
+ * @param {Array} anomalies - Detected anomalies
301
+ */
302
+ function updateCurrentSession(toolData, patterns, anomalies) {
303
+ const currentSession = readJSONFile(CURRENT_SESSION_FILE, {
304
+ startTime: new Date().toISOString(),
305
+ toolCalls: [],
306
+ patterns: [],
307
+ });
308
+
309
+ // Add tool call record
310
+ currentSession.toolCalls.push({
311
+ timestamp: new Date().toISOString(),
312
+ toolName: toolData.toolName,
313
+ arguments: toolData.arguments,
314
+ duration: toolData.duration,
315
+ success: toolData.success,
316
+ tokenUsage: toolData.tokensUsed,
317
+ });
318
+
319
+ // Cap toolCalls to prevent unbounded growth in long sessions
320
+ const MAX_TOOL_CALLS = 1000;
321
+ if (currentSession.toolCalls.length > MAX_TOOL_CALLS) {
322
+ currentSession.toolCalls = currentSession.toolCalls.slice(-MAX_TOOL_CALLS);
323
+ }
324
+
325
+ // Track file modifications and test executions (for dry-run gate)
326
+ trackFileModifications(toolData, currentSession);
327
+ trackTestExecutions(toolData, currentSession);
328
+
329
+ // Update pattern counts
330
+ patterns.forEach(pattern => {
331
+ const existing = currentSession.patterns.find(p => p.type === pattern.type);
332
+ if (!existing) {
333
+ currentSession.patterns.push({
334
+ type: pattern.type,
335
+ count: 1,
336
+ firstDetected: new Date().toISOString(),
337
+ });
338
+ } else {
339
+ existing.count++;
340
+ }
341
+ });
342
+
343
+ // Async write — session file is read on next call, eventual consistency is fine
344
+ // Return promise so caller can track it in pendingWrites
345
+ const writePromise = writeJSONFileAsync(CURRENT_SESSION_FILE, currentSession);
346
+
347
+ return { session: currentSession, writePromise };
348
+ }
349
+
350
+ /**
351
+ * Update session context (quota tracking)
352
+ * @param {Object} toolData - Tool execution data
353
+ * @returns {{ sessionContext: Object, writePromise: Promise<boolean> }}
354
+ */
355
+ function updateSessionContext(toolData) {
356
+ const sessionContext = readJSONFile(SESSION_CONTEXT_FILE, {
357
+ tokenQuota: { used: 0, limit: 200000 },
358
+ learnedPatterns: [],
359
+ lastSessionDate: null,
360
+ lastSaveTokens: 0,
361
+ });
362
+
363
+ // Update token usage
364
+ if (toolData.tokensUsed) {
365
+ sessionContext.tokenQuota.used += toolData.tokensUsed;
366
+ }
367
+
368
+ sessionContext.lastSessionDate = new Date().toISOString();
369
+
370
+ const writePromise = writeJSONFileAsync(SESSION_CONTEXT_FILE, sessionContext);
371
+
372
+ return { sessionContext, writePromise };
373
+ }
374
+
375
+ // ============================================================================
376
+ // MeMesh Key Points Auto-Save
377
+ // ============================================================================
378
+
379
+ /**
380
+ * Extract key points from session state
381
+ * @param {Object} sessionState - Current session state
382
+ * @returns {Array<string>} Extracted key points
383
+ */
384
+ function extractKeyPoints(sessionState) {
385
+ const keyPoints = [];
386
+
387
+ if (!sessionState?.toolCalls?.length) {
388
+ return keyPoints;
389
+ }
390
+
391
+ // 1. Identify completed file operations
392
+ const fileOps = {};
393
+ sessionState.toolCalls.forEach(tc => {
394
+ if (['Edit', 'Write'].includes(tc.toolName) && tc.arguments?.file_path) {
395
+ const filePath = tc.arguments.file_path;
396
+ fileOps[filePath] = (fileOps[filePath] || 0) + 1;
397
+ }
398
+ });
399
+
400
+ const modifiedFiles = Object.keys(fileOps);
401
+ if (modifiedFiles.length > 0) {
402
+ const summary = modifiedFiles.length > 5
403
+ ? `${modifiedFiles.slice(0, 5).map(f => path.basename(f)).join(', ')} (+${modifiedFiles.length - 5} more)`
404
+ : modifiedFiles.map(f => path.basename(f)).join(', ');
405
+ keyPoints.push(`[TASK] Modified files: ${summary}`);
406
+ }
407
+
408
+ // 2. Identify failures
409
+ const failures = sessionState.toolCalls.filter(tc => tc.success === false);
410
+ if (failures.length > 0) {
411
+ const failedTools = [...new Set(failures.map(f => f.toolName))];
412
+ keyPoints.push(`[PROBLEM] ${failures.length} tool failures: ${failedTools.join(', ')}`);
413
+ }
414
+
415
+ // 3. Git operations
416
+ const gitCommits = sessionState.toolCalls.filter(tc =>
417
+ tc.toolName === 'Bash' && tc.arguments?.command?.includes('git commit')
418
+ );
419
+ if (gitCommits.length > 0) {
420
+ keyPoints.push(`[DECISION] Made ${gitCommits.length} git commit(s)`);
421
+ }
422
+
423
+ // 4. Detected patterns
424
+ if (sessionState.patterns?.length > 0) {
425
+ const patternSummary = sessionState.patterns
426
+ .filter(p => p.count > 2)
427
+ .map(p => `${p.type}(${p.count})`)
428
+ .join(', ');
429
+ if (patternSummary) {
430
+ keyPoints.push(`[PATTERN] Recurring patterns: ${patternSummary}`);
431
+ }
432
+ }
433
+
434
+ // 5. Work scope indicator
435
+ const toolCounts = {};
436
+ sessionState.toolCalls.forEach(tc => {
437
+ toolCounts[tc.toolName] = (toolCounts[tc.toolName] || 0) + 1;
438
+ });
439
+ const topTools = Object.entries(toolCounts)
440
+ .sort((a, b) => b[1] - a[1])
441
+ .slice(0, 3)
442
+ .map(([tool, count]) => `${tool}:${count}`)
443
+ .join(', ');
444
+ keyPoints.push(`[SCOPE] Tool usage: ${topTools}, total: ${sessionState.toolCalls.length}`);
445
+
446
+ return keyPoints;
447
+ }
448
+
449
+ /**
450
+ * Save conversation key points to MeMesh knowledge graph.
451
+ * Uses sqliteBatchEntity for performance (3 spawns instead of N).
452
+ * @param {Object} sessionState - Current session state
453
+ * @param {Object} sessionContext - Session context
454
+ * @returns {boolean} True if saved successfully
455
+ */
456
+ function saveConversationKeyPoints(sessionState, sessionContext) {
457
+ try {
458
+ if (!fs.existsSync(MEMESH_DB_PATH)) {
459
+ logError('saveConversationKeyPoints', `MeMesh DB not found: ${MEMESH_DB_PATH}`);
460
+ return false;
461
+ }
462
+
463
+ const keyPoints = extractKeyPoints(sessionState);
464
+ if (keyPoints.length === 0) {
465
+ return false;
466
+ }
467
+
468
+ const entityName = `session_keypoints_${Date.now()}`;
469
+
470
+ const metadata = JSON.stringify({
471
+ tokensSaved: sessionContext.tokenQuota?.used || 0,
472
+ toolCount: sessionState.toolCalls?.length || 0,
473
+ saveReason: 'token_threshold',
474
+ });
475
+
476
+ const tags = ['auto_saved', 'token_trigger', getDateString()];
477
+
478
+ const entityId = sqliteBatchEntity(
479
+ MEMESH_DB_PATH,
480
+ { name: entityName, type: 'session_keypoint', metadata },
481
+ keyPoints,
482
+ tags
483
+ );
484
+
485
+ if (entityId === null) {
486
+ return false;
487
+ }
488
+
489
+ logMemorySave(`🧠 MeMesh: Saved ${keyPoints.length} key points (tokens: ${sessionContext.tokenQuota?.used})`);
490
+
491
+ return true;
492
+ } catch (error) {
493
+ logError('saveConversationKeyPoints', error);
494
+ return false;
495
+ }
496
+ }
497
+
498
+ /**
499
+ * Check if token threshold reached and save key points
500
+ * @param {Object} sessionState - Current session state
501
+ * @param {Object} sessionContext - Session context
502
+ * @returns {boolean} True if saved
503
+ */
504
+ function checkAndSaveKeyPoints(sessionState, sessionContext) {
505
+ try {
506
+ const lastSaveTokens = sessionContext.lastSaveTokens || 0;
507
+ const currentTokens = sessionContext.tokenQuota?.used || 0;
508
+ const tokensSinceLastSave = currentTokens - lastSaveTokens;
509
+
510
+ if (tokensSinceLastSave >= THRESHOLDS.TOKEN_SAVE) {
511
+ const saved = saveConversationKeyPoints(sessionState, sessionContext);
512
+
513
+ if (saved) {
514
+ sessionContext.lastSaveTokens = currentTokens;
515
+ writeJSONFile(SESSION_CONTEXT_FILE, sessionContext);
516
+ }
517
+
518
+ return saved;
519
+ }
520
+
521
+ return false;
522
+ } catch (error) {
523
+ logError('checkAndSaveKeyPoints', error);
524
+ return false;
525
+ }
526
+ }
527
+
528
+ // ============================================================================
529
+ // File Modification & Test Tracking (for dry-run gate in pre-tool-use.js)
530
+ // ============================================================================
531
+
532
+ /**
533
+ * Track file modifications from Write/Edit tool calls.
534
+ * Stores modified file paths in session state.
535
+ * @param {Object} toolData - Normalized tool data
536
+ * @param {Object} currentSession - Current session state (mutated in place)
537
+ */
538
+ function trackFileModifications(toolData, currentSession) {
539
+ if (!['Edit', 'Write'].includes(toolData.toolName)) return;
540
+
541
+ const filePath = toolData.arguments?.file_path;
542
+ if (!filePath) return;
543
+
544
+ if (!currentSession.modifiedFiles) {
545
+ currentSession.modifiedFiles = [];
546
+ }
547
+
548
+ if (!currentSession.modifiedFiles.includes(filePath)) {
549
+ currentSession.modifiedFiles.push(filePath);
550
+ }
551
+ }
552
+
553
+ /** Patterns that indicate test execution in a Bash command */
554
+ const TEST_PATTERNS = [
555
+ /vitest\s+(run|watch)?/,
556
+ /jest\b/,
557
+ /npm\s+test/,
558
+ /npm\s+run\s+test/,
559
+ /npx\s+vitest/,
560
+ /npx\s+jest/,
561
+ /tsc\s+--noEmit/,
562
+ /node\s+--check\s/,
563
+ /bun\s+test/,
564
+ /pytest\b/,
565
+ ];
566
+
567
+ /**
568
+ * Track test executions from Bash tool calls.
569
+ * Marks tested files/directories in session state.
570
+ * @param {Object} toolData - Normalized tool data
571
+ * @param {Object} currentSession - Current session state (mutated in place)
572
+ */
573
+ function trackTestExecutions(toolData, currentSession) {
574
+ if (toolData.toolName !== 'Bash') return;
575
+ if (!toolData.success) return;
576
+
577
+ const cmd = toolData.arguments?.command || '';
578
+ const isTest = TEST_PATTERNS.some(pattern => pattern.test(cmd));
579
+ if (!isTest) return;
580
+
581
+ if (!currentSession.testedFiles) {
582
+ currentSession.testedFiles = [];
583
+ }
584
+
585
+ currentSession.lastTestRun = new Date().toISOString();
586
+
587
+ // Extract test target path if provided
588
+ // e.g., "vitest run src/auth" → mark all modified files under src/auth/ as tested
589
+ const pathMatch = cmd.match(/(?:vitest|jest|node\s+--check)\s+(?:run\s+)?(\S+)/);
590
+ const testTarget = pathMatch ? pathMatch[1] : null;
591
+
592
+ if (testTarget && currentSession.modifiedFiles) {
593
+ // Mark files under the test target directory/path as tested
594
+ // Use path-prefix match: "src/auth" matches "src/auth/middleware.ts" but NOT "src/auth-utils/helper.ts"
595
+ for (const modFile of currentSession.modifiedFiles) {
596
+ const isMatch = modFile === testTarget ||
597
+ modFile.startsWith(testTarget + '/') ||
598
+ modFile.startsWith(testTarget + path.sep);
599
+ if (isMatch && !currentSession.testedFiles.includes(modFile)) {
600
+ currentSession.testedFiles.push(modFile);
601
+ }
602
+ }
603
+ } else if (!testTarget && currentSession.modifiedFiles) {
604
+ // Full test run (no specific target) — mark all modified files as tested
605
+ for (const modFile of currentSession.modifiedFiles) {
606
+ if (!currentSession.testedFiles.includes(modFile)) {
607
+ currentSession.testedFiles.push(modFile);
608
+ }
609
+ }
610
+ }
611
+ }
612
+
613
+ // ============================================================================
614
+ // Code Review Tracking
615
+ // ============================================================================
616
+
617
+ /**
618
+ * Check if this tool call is a code review invocation.
619
+ * Detects both Skill tool usage and Task tool dispatching code reviewers.
620
+ * @param {Object} toolData - Normalized tool data
621
+ * @returns {boolean}
622
+ */
623
+ function isCodeReviewInvocation(toolData) {
624
+ // Skill tool with code review
625
+ if (toolData.toolName === 'Skill') {
626
+ const name = toolData.arguments?.name || toolData.arguments?.skill_name || '';
627
+ return /code.?review|comprehensive.?code.?review/i.test(name);
628
+ }
629
+
630
+ // Task tool dispatching code reviewer subagent
631
+ if (toolData.toolName === 'Task') {
632
+ const subagentType = toolData.arguments?.subagent_type || '';
633
+ return /code.?review/i.test(subagentType);
634
+ }
635
+
636
+ return false;
637
+ }
638
+
639
+ /**
640
+ * Mark code review as done in session state.
641
+ * This flag is checked by pre-tool-use.js before git commits.
642
+ */
643
+ function markCodeReviewDone() {
644
+ const session = readJSONFile(CURRENT_SESSION_FILE, {});
645
+ session.codeReviewDone = true;
646
+ session.codeReviewTimestamp = new Date().toISOString();
647
+ writeJSONFile(CURRENT_SESSION_FILE, session);
648
+ }
649
+
650
+ // ============================================================================
651
+ // Tool Data Normalization
652
+ // ============================================================================
653
+
654
+ /**
655
+ * Normalize tool data from Claude Code format
656
+ * @param {Object} raw - Raw tool data from stdin
657
+ * @returns {Object} Normalized tool data
658
+ */
659
+ function normalizeToolData(raw) {
660
+ return {
661
+ toolName: raw.tool_name || raw.toolName || 'unknown',
662
+ arguments: raw.tool_input || raw.arguments || {},
663
+ duration: raw.duration_ms || raw.duration || 0,
664
+ success: raw.success !== false,
665
+ tokensUsed: raw.tokens_used || raw.tokensUsed || 0,
666
+ _raw: raw,
667
+ };
668
+ }
669
+
670
+ // ============================================================================
671
+ // Plan File Detection (Beta)
672
+ // ============================================================================
673
+
674
+ /**
675
+ * Detect plan file creation and save to KG.
676
+ * Triggered when Write tool targets a plan file path.
677
+ * @param {Object} toolData - Normalized tool data
678
+ */
679
+ function detectPlanFile(toolData) {
680
+ if (toolData.toolName !== 'Write') return;
681
+
682
+ const filePath = toolData.arguments?.file_path;
683
+ if (!isPlanFile(filePath)) return;
684
+
685
+ try {
686
+ // Read the file content
687
+ if (!fs.existsSync(filePath)) return;
688
+ const content = fs.readFileSync(filePath, 'utf-8');
689
+
690
+ const steps = parsePlanSteps(content);
691
+ if (steps.length === 0) return;
692
+
693
+ if (!fs.existsSync(MEMESH_DB_PATH)) return;
694
+
695
+ const planName = derivePlanName(filePath);
696
+ const entityName = `Plan: ${planName}`;
697
+
698
+ const newMetadata = {
699
+ sourceFile: filePath,
700
+ totalSteps: steps.length,
701
+ completed: steps.filter(s => s.completed).length,
702
+ status: 'active',
703
+ stepsDetail: steps,
704
+ };
705
+
706
+ // Check if plan entity already exists (re-save scenario)
707
+ const existing = sqliteQueryJSON(MEMESH_DB_PATH,
708
+ 'SELECT id FROM entities WHERE name = ?', [entityName]);
709
+
710
+ if (existing && existing.length > 0) {
711
+ // Upsert: update metadata and add observation for the re-save
712
+ updateEntityMetadata(MEMESH_DB_PATH, entityName, newMetadata);
713
+ addObservation(MEMESH_DB_PATH, entityName,
714
+ `Plan re-saved: ${steps.length} steps (${steps.filter(s => s.completed).length} completed)`);
715
+ logMemorySave(`Plan updated: ${planName} (${steps.length} steps)`);
716
+ } else {
717
+ // First save: create entity with observations and tags
718
+ const observations = steps.map(s => `Step ${s.number}: ${s.description}`);
719
+ const tags = ['plan', 'active', `plan:${planName}`, 'scope:project'];
720
+
721
+ sqliteBatchEntity(MEMESH_DB_PATH,
722
+ { name: entityName, type: 'workflow_checkpoint', metadata: JSON.stringify(newMetadata) },
723
+ observations, tags
724
+ );
725
+ logMemorySave(`Plan detected: ${planName} (${steps.length} steps)`);
726
+ }
727
+ } catch (error) {
728
+ logError('detectPlanFile', error);
729
+ }
730
+ }
731
+
732
+ // ============================================================================
733
+ // Main PostToolUse Logic
734
+ // ============================================================================
735
+
736
+ async function postToolUse() {
737
+ try {
738
+ // Read stdin with timeout
739
+ const input = await readStdin(3000);
740
+
741
+ if (!input || input.trim() === '') {
742
+ process.exit(0);
743
+ }
744
+
745
+ // Parse and normalize tool data
746
+ const rawData = JSON.parse(input);
747
+ const toolData = normalizeToolData(rawData);
748
+
749
+ // Track code review invocations (for pre-commit enforcement)
750
+ if (isCodeReviewInvocation(toolData)) {
751
+ markCodeReviewDone();
752
+ }
753
+
754
+ // Detect plan file creation (beta)
755
+ detectPlanFile(toolData);
756
+
757
+ // Initialize pattern detector
758
+ const detector = new PatternDetector();
759
+
760
+ // Load recent tools from current session
761
+ const currentSession = readJSONFile(CURRENT_SESSION_FILE, { toolCalls: [] });
762
+ currentSession.toolCalls.slice(-10).forEach(tc => {
763
+ detector.addToolCall({
764
+ toolName: tc.toolName,
765
+ arguments: tc.arguments || {},
766
+ });
767
+ });
768
+
769
+ // Add current tool
770
+ detector.addToolCall(toolData);
771
+
772
+ // Detect patterns
773
+ const patterns = detector.detectPatterns(toolData);
774
+
775
+ // Update session context (for quota tracking) — returns sync data + async write promise
776
+ const { sessionContext, writePromise: contextWritePromise } = updateSessionContext(toolData);
777
+
778
+ // Detect anomalies
779
+ const anomalies = detectAnomalies(toolData, sessionContext);
780
+
781
+ // Fire async writes in parallel
782
+ const pendingWrites = [contextWritePromise];
783
+
784
+ // Update recommendations incrementally
785
+ if (patterns.length > 0 || anomalies.length > 0) {
786
+ pendingWrites.push(updateRecommendations(patterns, anomalies));
787
+ }
788
+
789
+ // Update current session (async write)
790
+ const { session: updatedSession, writePromise: sessionWritePromise } =
791
+ updateCurrentSession(toolData, patterns, anomalies);
792
+ pendingWrites.push(sessionWritePromise);
793
+
794
+ // Check token threshold and save key points if needed
795
+ checkAndSaveKeyPoints(updatedSession, sessionContext);
796
+
797
+ // Wait for all async writes to complete before exit
798
+ await Promise.all(pendingWrites);
799
+
800
+ // Silent exit
801
+ process.exit(0);
802
+ } catch (error) {
803
+ logError('PostToolUse', error);
804
+ process.exit(0); // Never block Claude Code on hook errors
805
+ }
806
+ }
807
+
808
+ // ============================================================================
809
+ // Execute
810
+ // ============================================================================
811
+
812
+ postToolUse();