@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.
- package/LICENSE +21 -661
- package/README.de.md +171 -0
- package/README.es.md +171 -0
- package/README.fr.md +171 -0
- package/README.id.md +171 -0
- package/README.ja.md +171 -0
- package/README.ko.md +171 -0
- package/README.md +73 -100
- package/README.th.md +171 -0
- package/README.vi.md +171 -0
- package/README.zh-CN.md +171 -0
- package/README.zh-TW.md +71 -98
- package/dist/knowledge-graph/index.d.ts +22 -1
- package/dist/knowledge-graph/index.d.ts.map +1 -1
- package/dist/knowledge-graph/index.js +144 -3
- package/dist/knowledge-graph/index.js.map +1 -1
- package/dist/mcp/ServerInitializer.d.ts.map +1 -1
- package/dist/mcp/ServerInitializer.js +1 -1
- package/dist/mcp/ServerInitializer.js.map +1 -1
- package/dist/mcp/ToolDefinitions.d.ts.map +1 -1
- package/dist/mcp/ToolDefinitions.js +47 -55
- package/dist/mcp/ToolDefinitions.js.map +1 -1
- package/dist/mcp/ToolRouter.d.ts.map +1 -1
- package/dist/mcp/ToolRouter.js +4 -4
- package/dist/mcp/ToolRouter.js.map +1 -1
- package/dist/mcp/daemon/StdioProxyClient.d.ts.map +1 -1
- package/dist/mcp/daemon/StdioProxyClient.js +9 -1
- package/dist/mcp/daemon/StdioProxyClient.js.map +1 -1
- package/dist/mcp/handlers/BuddyHandlers.d.ts +3 -1
- package/dist/mcp/handlers/BuddyHandlers.d.ts.map +1 -1
- package/dist/mcp/handlers/BuddyHandlers.js +6 -5
- package/dist/mcp/handlers/BuddyHandlers.js.map +1 -1
- package/dist/mcp/handlers/ToolHandlers.d.ts.map +1 -1
- package/dist/mcp/handlers/ToolHandlers.js +1 -2
- package/dist/mcp/handlers/ToolHandlers.js.map +1 -1
- package/dist/mcp/resources/quick-reference.md +1 -1
- package/dist/mcp/schemas/OutputSchemas.d.ts +116 -53
- package/dist/mcp/schemas/OutputSchemas.d.ts.map +1 -1
- package/dist/mcp/schemas/OutputSchemas.js +64 -26
- package/dist/mcp/schemas/OutputSchemas.js.map +1 -1
- package/dist/mcp/server-bootstrap.js +89 -9
- package/dist/mcp/server-bootstrap.js.map +1 -1
- package/dist/mcp/tools/buddy-do.d.ts +2 -1
- package/dist/mcp/tools/buddy-do.d.ts.map +1 -1
- package/dist/mcp/tools/buddy-do.js +91 -4
- package/dist/mcp/tools/buddy-do.js.map +1 -1
- package/dist/mcp/tools/buddy-remember.d.ts +0 -5
- package/dist/mcp/tools/buddy-remember.d.ts.map +1 -1
- package/dist/mcp/tools/buddy-remember.js.map +1 -1
- package/dist/mcp/tools/memesh-agent-register.d.ts +20 -0
- package/dist/mcp/tools/memesh-agent-register.d.ts.map +1 -0
- package/dist/mcp/tools/memesh-agent-register.js +80 -0
- package/dist/mcp/tools/memesh-agent-register.js.map +1 -0
- package/dist/mcp/tools/memesh-cloud-sync.js +27 -8
- package/dist/mcp/tools/memesh-cloud-sync.js.map +1 -1
- package/dist/mcp/tools/memesh-metrics.d.ts +13 -0
- package/dist/mcp/tools/memesh-metrics.d.ts.map +1 -0
- package/dist/mcp/tools/memesh-metrics.js +193 -0
- package/dist/mcp/tools/memesh-metrics.js.map +1 -0
- package/dist/memory/UnifiedMemoryStore.d.ts +1 -1
- package/dist/memory/UnifiedMemoryStore.d.ts.map +1 -1
- package/dist/memory/UnifiedMemoryStore.js +4 -3
- package/dist/memory/UnifiedMemoryStore.js.map +1 -1
- package/package.json +9 -12
- package/plugin.json +2 -2
- package/scripts/hooks/README.md +230 -0
- package/scripts/hooks/__tests__/hook-test-harness.js +218 -0
- package/scripts/hooks/__tests__/hooks.test.js +267 -0
- package/scripts/hooks/hook-utils.js +899 -0
- package/scripts/hooks/post-commit.js +307 -0
- package/scripts/hooks/post-tool-use.js +812 -0
- package/scripts/hooks/pre-tool-use.js +462 -0
- package/scripts/hooks/session-start.js +544 -0
- package/scripts/hooks/stop.js +673 -0
- package/scripts/hooks/subagent-stop.js +184 -0
- package/scripts/hooks/templates/planning-template.md +46 -0
- package/scripts/postinstall-lib.js +8 -4
- package/scripts/postinstall-new.js +110 -7
- 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();
|