@pcircle/memesh 2.9.0 ā 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/dist/mcp/ToolDefinitions.d.ts.map +1 -1
- package/dist/mcp/ToolDefinitions.js +0 -104
- package/dist/mcp/ToolDefinitions.js.map +1 -1
- package/package.json +2 -1
- package/plugin.json +1 -1
- 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 +15 -7
|
@@ -0,0 +1,673 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Stop Hook - Claude Code Event-Driven Hooks
|
|
5
|
+
*
|
|
6
|
+
* Triggered when a Claude Code session ends (normal or forced termination).
|
|
7
|
+
*
|
|
8
|
+
* Features:
|
|
9
|
+
* - Analyzes current session's tool patterns and workflows
|
|
10
|
+
* - Generates recommendations for next session
|
|
11
|
+
* - Updates session context (quota, learned patterns)
|
|
12
|
+
* - Saves session key points to MeMesh
|
|
13
|
+
* - Cleans up old key points (>30 days retention)
|
|
14
|
+
* - Displays session summary with patterns and suggestions
|
|
15
|
+
* - Archives current session data
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import {
|
|
19
|
+
STATE_DIR,
|
|
20
|
+
MEMESH_DB_PATH,
|
|
21
|
+
THRESHOLDS,
|
|
22
|
+
readJSONFile,
|
|
23
|
+
writeJSONFile,
|
|
24
|
+
sqliteQuery,
|
|
25
|
+
sqliteBatch,
|
|
26
|
+
sqliteBatchEntity,
|
|
27
|
+
calculateDuration,
|
|
28
|
+
getDateString,
|
|
29
|
+
ensureDir,
|
|
30
|
+
logError,
|
|
31
|
+
} from './hook-utils.js';
|
|
32
|
+
import fs from 'fs';
|
|
33
|
+
import path from 'path';
|
|
34
|
+
|
|
35
|
+
// ============================================================================
|
|
36
|
+
// File Paths
|
|
37
|
+
// ============================================================================
|
|
38
|
+
|
|
39
|
+
const CURRENT_SESSION_FILE = path.join(STATE_DIR, 'current-session.json');
|
|
40
|
+
const RECOMMENDATIONS_FILE = path.join(STATE_DIR, 'recommendations.json');
|
|
41
|
+
const SESSION_CONTEXT_FILE = path.join(STATE_DIR, 'session-context.json');
|
|
42
|
+
const SESSIONS_ARCHIVE_DIR = path.join(STATE_DIR, 'sessions');
|
|
43
|
+
const LAST_SESSION_CACHE_FILE = path.join(STATE_DIR, 'last-session-summary.json');
|
|
44
|
+
|
|
45
|
+
// Ensure archive directory exists
|
|
46
|
+
if (!fs.existsSync(SESSIONS_ARCHIVE_DIR)) {
|
|
47
|
+
fs.mkdirSync(SESSIONS_ARCHIVE_DIR, { recursive: true });
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// ============================================================================
|
|
51
|
+
// Pattern Analysis
|
|
52
|
+
// ============================================================================
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Analyze tool patterns from session state
|
|
56
|
+
* @param {Object} sessionState - Current session state
|
|
57
|
+
* @returns {Array} Detected patterns
|
|
58
|
+
*/
|
|
59
|
+
function analyzeToolPatterns(sessionState) {
|
|
60
|
+
const patterns = [];
|
|
61
|
+
|
|
62
|
+
if (!sessionState.toolCalls || sessionState.toolCalls.length === 0) {
|
|
63
|
+
return patterns;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Count tool frequency
|
|
67
|
+
const toolFrequency = {};
|
|
68
|
+
sessionState.toolCalls.forEach(tc => {
|
|
69
|
+
toolFrequency[tc.toolName] = (toolFrequency[tc.toolName] || 0) + 1;
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// Find most used tools
|
|
73
|
+
const mostUsedTools = Object.entries(toolFrequency)
|
|
74
|
+
.sort((a, b) => b[1] - a[1])
|
|
75
|
+
.slice(0, 3);
|
|
76
|
+
|
|
77
|
+
if (mostUsedTools.length > 0) {
|
|
78
|
+
patterns.push({
|
|
79
|
+
type: 'MOST_USED_TOOLS',
|
|
80
|
+
description: mostUsedTools.map(([tool, count]) => `${tool} (${count}x)`).join(', '),
|
|
81
|
+
severity: 'info',
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Check READ_BEFORE_EDIT compliance
|
|
86
|
+
let editWithoutRead = 0;
|
|
87
|
+
let editWithRead = 0;
|
|
88
|
+
|
|
89
|
+
for (let i = 0; i < sessionState.toolCalls.length; i++) {
|
|
90
|
+
const tool = sessionState.toolCalls[i];
|
|
91
|
+
if (tool.toolName === 'Edit') {
|
|
92
|
+
const recentReads = sessionState.toolCalls
|
|
93
|
+
.slice(Math.max(0, i - 5), i)
|
|
94
|
+
.filter(t => t.toolName === 'Read');
|
|
95
|
+
|
|
96
|
+
if (recentReads.length > 0) {
|
|
97
|
+
editWithRead++;
|
|
98
|
+
} else {
|
|
99
|
+
editWithoutRead++;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (editWithRead + editWithoutRead > 0) {
|
|
105
|
+
const compliance = (editWithRead / (editWithRead + editWithoutRead) * 100).toFixed(0);
|
|
106
|
+
patterns.push({
|
|
107
|
+
type: 'READ_BEFORE_EDIT_COMPLIANCE',
|
|
108
|
+
description: `READ_BEFORE_EDIT compliance: ${compliance}%`,
|
|
109
|
+
severity: compliance >= 80 ? 'info' : 'warning',
|
|
110
|
+
suggestion: compliance < 80 ? 'Read files before editing to avoid errors' : 'Good practice!',
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Detect Git workflow
|
|
115
|
+
const gitOps = sessionState.toolCalls.filter(tc =>
|
|
116
|
+
tc.toolName === 'Bash' && ['git add', 'git commit', 'git push', 'git branch'].some(cmd =>
|
|
117
|
+
tc.arguments?.command?.includes(cmd)
|
|
118
|
+
)
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
if (gitOps.length >= 3) {
|
|
122
|
+
patterns.push({
|
|
123
|
+
type: 'GIT_WORKFLOW',
|
|
124
|
+
description: `Executed ${gitOps.length} Git operations`,
|
|
125
|
+
severity: 'info',
|
|
126
|
+
suggestion: 'Consider loading devops-git-workflows skill next time',
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Detect frontend work
|
|
131
|
+
const frontendOps = sessionState.toolCalls.filter(tc =>
|
|
132
|
+
['Edit', 'Write', 'Read'].includes(tc.toolName) &&
|
|
133
|
+
['.tsx', '.jsx', '.vue', '.css'].some(ext => tc.arguments?.file_path?.endsWith(ext))
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
if (frontendOps.length >= 5) {
|
|
137
|
+
patterns.push({
|
|
138
|
+
type: 'FRONTEND_WORK',
|
|
139
|
+
description: `Modified ${frontendOps.length} frontend files`,
|
|
140
|
+
severity: 'info',
|
|
141
|
+
suggestion: 'Consider loading frontend-design skill next time',
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Detect slow operations
|
|
146
|
+
const slowOps = sessionState.toolCalls.filter(tc => tc.duration && tc.duration > THRESHOLDS.SLOW_EXECUTION);
|
|
147
|
+
if (slowOps.length > 0) {
|
|
148
|
+
patterns.push({
|
|
149
|
+
type: 'SLOW_OPERATIONS',
|
|
150
|
+
description: `${slowOps.length} tools took >5 seconds`,
|
|
151
|
+
severity: 'warning',
|
|
152
|
+
suggestion: 'Consider optimizing these operations or using faster alternatives',
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Detect failures
|
|
157
|
+
const failures = sessionState.toolCalls.filter(tc => tc.success === false);
|
|
158
|
+
if (failures.length > 0) {
|
|
159
|
+
patterns.push({
|
|
160
|
+
type: 'EXECUTION_FAILURES',
|
|
161
|
+
description: `${failures.length} tool executions failed`,
|
|
162
|
+
severity: 'error',
|
|
163
|
+
suggestion: 'Review and fix failure causes',
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return patterns;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// ============================================================================
|
|
171
|
+
// Recommendations
|
|
172
|
+
// ============================================================================
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Save recommendations for next session
|
|
176
|
+
* @param {Array} patterns - Detected patterns
|
|
177
|
+
* @param {Object} sessionState - Current session state
|
|
178
|
+
*/
|
|
179
|
+
function saveRecommendations(patterns, sessionState) {
|
|
180
|
+
const recommendations = readJSONFile(RECOMMENDATIONS_FILE, {
|
|
181
|
+
recommendedSkills: [],
|
|
182
|
+
detectedPatterns: [],
|
|
183
|
+
warnings: [],
|
|
184
|
+
lastUpdated: null,
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
// Add skill recommendations based on patterns
|
|
188
|
+
patterns.forEach(pattern => {
|
|
189
|
+
if (pattern.suggestion && pattern.suggestion.includes('skill')) {
|
|
190
|
+
const skillMatch = pattern.suggestion.match(/loading\s+(.+?)\s+skill/);
|
|
191
|
+
if (skillMatch) {
|
|
192
|
+
const skillName = skillMatch[1];
|
|
193
|
+
const existing = recommendations.recommendedSkills.find(s => s.name === skillName);
|
|
194
|
+
|
|
195
|
+
if (!existing) {
|
|
196
|
+
recommendations.recommendedSkills.push({
|
|
197
|
+
name: skillName,
|
|
198
|
+
reason: pattern.description,
|
|
199
|
+
priority: pattern.type.includes('GIT') || pattern.type.includes('FRONTEND') ? 'high' : 'medium',
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
// Keep only top 5 skills
|
|
207
|
+
recommendations.recommendedSkills = recommendations.recommendedSkills.slice(0, 5);
|
|
208
|
+
|
|
209
|
+
// Merge patterns with existing (keep last 10)
|
|
210
|
+
patterns.forEach(pattern => {
|
|
211
|
+
recommendations.detectedPatterns.unshift({
|
|
212
|
+
description: pattern.description,
|
|
213
|
+
suggestion: pattern.suggestion || '',
|
|
214
|
+
timestamp: new Date().toISOString(),
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
recommendations.detectedPatterns = recommendations.detectedPatterns.slice(0, 10);
|
|
218
|
+
|
|
219
|
+
// Add warnings from error/warning severity patterns
|
|
220
|
+
patterns.filter(p => p.severity === 'warning' || p.severity === 'error').forEach(pattern => {
|
|
221
|
+
if (pattern.suggestion) {
|
|
222
|
+
recommendations.warnings.unshift(pattern.suggestion);
|
|
223
|
+
} else {
|
|
224
|
+
recommendations.warnings.unshift(pattern.description);
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
recommendations.warnings = recommendations.warnings.slice(0, 5);
|
|
228
|
+
|
|
229
|
+
recommendations.lastUpdated = new Date().toISOString();
|
|
230
|
+
|
|
231
|
+
writeJSONFile(RECOMMENDATIONS_FILE, recommendations);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// ============================================================================
|
|
235
|
+
// Session Context Update
|
|
236
|
+
// ============================================================================
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Update session context (quota, patterns)
|
|
240
|
+
* @param {Object} sessionState - Current session state
|
|
241
|
+
* @returns {Object} Updated session context
|
|
242
|
+
*/
|
|
243
|
+
function updateSessionContext(sessionState) {
|
|
244
|
+
const sessionContext = readJSONFile(SESSION_CONTEXT_FILE, {
|
|
245
|
+
tokenQuota: { used: 0, limit: 200000 },
|
|
246
|
+
learnedPatterns: [],
|
|
247
|
+
lastSessionDate: null,
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
// Calculate total token usage from session
|
|
251
|
+
let totalTokens = 0;
|
|
252
|
+
if (sessionState.toolCalls) {
|
|
253
|
+
sessionState.toolCalls.forEach(tc => {
|
|
254
|
+
if (tc.tokenUsage) {
|
|
255
|
+
totalTokens += tc.tokenUsage;
|
|
256
|
+
}
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Update quota
|
|
261
|
+
sessionContext.tokenQuota.used = Math.min(
|
|
262
|
+
sessionContext.tokenQuota.used + totalTokens,
|
|
263
|
+
sessionContext.tokenQuota.limit
|
|
264
|
+
);
|
|
265
|
+
|
|
266
|
+
// Add learned patterns
|
|
267
|
+
if (sessionState.patterns) {
|
|
268
|
+
sessionState.patterns.forEach(pattern => {
|
|
269
|
+
const existing = sessionContext.learnedPatterns.find(p => p.type === pattern.type);
|
|
270
|
+
if (!existing) {
|
|
271
|
+
sessionContext.learnedPatterns.push({
|
|
272
|
+
type: pattern.type,
|
|
273
|
+
count: pattern.count,
|
|
274
|
+
lastSeen: new Date().toISOString(),
|
|
275
|
+
});
|
|
276
|
+
} else {
|
|
277
|
+
existing.count += pattern.count;
|
|
278
|
+
existing.lastSeen = new Date().toISOString();
|
|
279
|
+
}
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
sessionContext.lastSessionDate = new Date().toISOString();
|
|
284
|
+
|
|
285
|
+
writeJSONFile(SESSION_CONTEXT_FILE, sessionContext);
|
|
286
|
+
|
|
287
|
+
return sessionContext;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// ============================================================================
|
|
291
|
+
// MeMesh Memory Save
|
|
292
|
+
// ============================================================================
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Extract comprehensive key points from session for end summary
|
|
296
|
+
* @param {Object} sessionState - Current session state
|
|
297
|
+
* @param {Array} patterns - Analyzed patterns
|
|
298
|
+
* @returns {Array<string>} Key points
|
|
299
|
+
*/
|
|
300
|
+
function extractSessionKeyPoints(sessionState, patterns) {
|
|
301
|
+
const keyPoints = [];
|
|
302
|
+
|
|
303
|
+
if (!sessionState) {
|
|
304
|
+
return keyPoints;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// 1. Session overview
|
|
308
|
+
const duration = calculateDuration(sessionState.startTime);
|
|
309
|
+
const toolCount = sessionState.toolCalls?.length || 0;
|
|
310
|
+
keyPoints.push(`[SESSION] Duration: ${duration}, Tools used: ${toolCount}`);
|
|
311
|
+
|
|
312
|
+
// 2. Files modified (task summary)
|
|
313
|
+
if (sessionState.toolCalls) {
|
|
314
|
+
const fileOps = {};
|
|
315
|
+
sessionState.toolCalls.forEach(tc => {
|
|
316
|
+
if (['Edit', 'Write'].includes(tc.toolName) && tc.arguments?.file_path) {
|
|
317
|
+
const filePath = tc.arguments.file_path;
|
|
318
|
+
fileOps[filePath] = (fileOps[filePath] || 0) + 1;
|
|
319
|
+
}
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
const modifiedFiles = Object.keys(fileOps);
|
|
323
|
+
if (modifiedFiles.length > 0) {
|
|
324
|
+
// Group by directory
|
|
325
|
+
const dirs = {};
|
|
326
|
+
modifiedFiles.forEach(f => {
|
|
327
|
+
const dir = path.dirname(f);
|
|
328
|
+
if (!dirs[dir]) dirs[dir] = [];
|
|
329
|
+
dirs[dir].push(path.basename(f));
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
const summary = Object.entries(dirs)
|
|
333
|
+
.map(([dir, files]) => {
|
|
334
|
+
const shortDir = dir.split('/').slice(-2).join('/');
|
|
335
|
+
return `${shortDir}: ${files.slice(0, 3).join(', ')}${files.length > 3 ? '...' : ''}`;
|
|
336
|
+
})
|
|
337
|
+
.slice(0, 3)
|
|
338
|
+
.join(' | ');
|
|
339
|
+
|
|
340
|
+
keyPoints.push(`[WORK] ${modifiedFiles.length} files modified: ${summary}`);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// 3. Git operations (commits = completed work)
|
|
345
|
+
if (sessionState.toolCalls) {
|
|
346
|
+
const gitCommits = sessionState.toolCalls.filter(tc =>
|
|
347
|
+
tc.toolName === 'Bash' && tc.arguments?.command?.includes('git commit')
|
|
348
|
+
);
|
|
349
|
+
if (gitCommits.length > 0) {
|
|
350
|
+
keyPoints.push(`[COMMIT] ${gitCommits.length} commit(s) made`);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// 4. Problems encountered
|
|
355
|
+
if (sessionState.toolCalls) {
|
|
356
|
+
const failures = sessionState.toolCalls.filter(tc => tc.success === false);
|
|
357
|
+
if (failures.length > 0) {
|
|
358
|
+
const failedTools = [...new Set(failures.map(f => f.toolName))];
|
|
359
|
+
keyPoints.push(`[ISSUE] ${failures.length} failures: ${failedTools.join(', ')}`);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// 5. Detected patterns (learnings)
|
|
364
|
+
if (patterns && patterns.length > 0) {
|
|
365
|
+
const significantPatterns = patterns
|
|
366
|
+
.filter(p => p.severity === 'warning' || p.severity === 'error' || p.suggestion)
|
|
367
|
+
.slice(0, 3);
|
|
368
|
+
|
|
369
|
+
significantPatterns.forEach(p => {
|
|
370
|
+
if (p.suggestion) {
|
|
371
|
+
keyPoints.push(`[LEARN] ${p.description} -> ${p.suggestion}`);
|
|
372
|
+
} else {
|
|
373
|
+
keyPoints.push(`[NOTE] ${p.description}`);
|
|
374
|
+
}
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// 6. Most used tools (work focus)
|
|
379
|
+
if (sessionState.toolCalls && sessionState.toolCalls.length > 5) {
|
|
380
|
+
const toolCounts = {};
|
|
381
|
+
sessionState.toolCalls.forEach(tc => {
|
|
382
|
+
toolCounts[tc.toolName] = (toolCounts[tc.toolName] || 0) + 1;
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
const topTools = Object.entries(toolCounts)
|
|
386
|
+
.sort((a, b) => b[1] - a[1])
|
|
387
|
+
.slice(0, 3)
|
|
388
|
+
.map(([tool, count]) => `${tool}(${count})`)
|
|
389
|
+
.join(', ');
|
|
390
|
+
|
|
391
|
+
keyPoints.push(`[FOCUS] Top tools: ${topTools}`);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
return keyPoints;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Save session key points to MeMesh on session end.
|
|
399
|
+
* Uses sqliteBatchEntity for performance (3 spawns instead of N).
|
|
400
|
+
* @param {Object} sessionState - Current session state
|
|
401
|
+
* @param {Array} patterns - Analyzed patterns
|
|
402
|
+
* @returns {boolean} True if saved successfully
|
|
403
|
+
*/
|
|
404
|
+
function saveSessionKeyPointsOnEnd(sessionState, patterns) {
|
|
405
|
+
try {
|
|
406
|
+
if (!fs.existsSync(MEMESH_DB_PATH)) {
|
|
407
|
+
console.log('š§ MeMesh: Database not found, skipping memory save');
|
|
408
|
+
return false;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
const keyPoints = extractSessionKeyPoints(sessionState, patterns);
|
|
412
|
+
if (keyPoints.length === 0) {
|
|
413
|
+
return false;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
const entityName = `session_end_${Date.now()}`;
|
|
417
|
+
const startTime = new Date(sessionState.startTime);
|
|
418
|
+
const duration = Math.round((Date.now() - startTime.getTime()) / 1000 / 60);
|
|
419
|
+
|
|
420
|
+
const metadata = JSON.stringify({
|
|
421
|
+
duration: `${duration}m`,
|
|
422
|
+
toolCount: sessionState.toolCalls?.length || 0,
|
|
423
|
+
saveReason: 'session_end',
|
|
424
|
+
patternCount: patterns.length,
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
const today = getDateString();
|
|
428
|
+
const tags = ['session_end', 'auto_saved', today];
|
|
429
|
+
|
|
430
|
+
// Batch: entity + observations + tags in 2 process spawns (was ~10)
|
|
431
|
+
const entityId = sqliteBatchEntity(
|
|
432
|
+
MEMESH_DB_PATH,
|
|
433
|
+
{ name: entityName, type: 'session_keypoint', metadata },
|
|
434
|
+
keyPoints,
|
|
435
|
+
tags
|
|
436
|
+
);
|
|
437
|
+
|
|
438
|
+
if (entityId === null) {
|
|
439
|
+
return false;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
console.log(`š§ MeMesh: Saved ${keyPoints.length} key points to memory`);
|
|
443
|
+
return true;
|
|
444
|
+
} catch (error) {
|
|
445
|
+
console.error(`š§ MeMesh: Failed to save session key points: ${error.message}`);
|
|
446
|
+
return false;
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/**
|
|
451
|
+
* Clean up old key points (older than retention period).
|
|
452
|
+
* Uses batch delete for performance.
|
|
453
|
+
*/
|
|
454
|
+
function cleanupOldKeyPoints() {
|
|
455
|
+
try {
|
|
456
|
+
if (!fs.existsSync(MEMESH_DB_PATH)) {
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
const cutoffDate = new Date();
|
|
461
|
+
cutoffDate.setDate(cutoffDate.getDate() - THRESHOLDS.RETENTION_DAYS);
|
|
462
|
+
const cutoffISO = cutoffDate.toISOString();
|
|
463
|
+
|
|
464
|
+
// Count old entries
|
|
465
|
+
const countResult = sqliteQuery(
|
|
466
|
+
MEMESH_DB_PATH,
|
|
467
|
+
'SELECT COUNT(*) FROM entities WHERE type = ? AND created_at < ?',
|
|
468
|
+
['session_keypoint', cutoffISO]
|
|
469
|
+
);
|
|
470
|
+
|
|
471
|
+
const oldCount = parseInt(countResult, 10) || 0;
|
|
472
|
+
|
|
473
|
+
if (oldCount > 0) {
|
|
474
|
+
// Batch delete: tags + entities in 2 statements (was N+2 spawns)
|
|
475
|
+
sqliteBatch(MEMESH_DB_PATH, [
|
|
476
|
+
{
|
|
477
|
+
query: `DELETE FROM tags WHERE entity_id IN (
|
|
478
|
+
SELECT id FROM entities WHERE type = ? AND created_at < ?
|
|
479
|
+
)`,
|
|
480
|
+
params: ['session_keypoint', cutoffISO],
|
|
481
|
+
},
|
|
482
|
+
{
|
|
483
|
+
query: 'DELETE FROM entities WHERE type = ? AND created_at < ?',
|
|
484
|
+
params: ['session_keypoint', cutoffISO],
|
|
485
|
+
},
|
|
486
|
+
]);
|
|
487
|
+
|
|
488
|
+
console.log(`š§ MeMesh: Cleaned up ${oldCount} expired memories (>${THRESHOLDS.RETENTION_DAYS} days)`);
|
|
489
|
+
}
|
|
490
|
+
} catch (error) {
|
|
491
|
+
console.error(`š§ MeMesh: Cleanup failed: ${error.message}`);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// ============================================================================
|
|
496
|
+
// Session Cache (for fast startup)
|
|
497
|
+
// ============================================================================
|
|
498
|
+
|
|
499
|
+
/**
|
|
500
|
+
* Write session summary cache for fast recall on next startup.
|
|
501
|
+
* Session-start.js reads this instead of querying SQLite.
|
|
502
|
+
* @param {Object} sessionState - Current session state
|
|
503
|
+
* @param {Array} patterns - Analyzed patterns
|
|
504
|
+
*/
|
|
505
|
+
function writeSessionCache(sessionState, patterns) {
|
|
506
|
+
try {
|
|
507
|
+
const keyPoints = extractSessionKeyPoints(sessionState, patterns);
|
|
508
|
+
const startTime = new Date(sessionState.startTime);
|
|
509
|
+
const duration = Math.round((Date.now() - startTime.getTime()) / 1000 / 60);
|
|
510
|
+
|
|
511
|
+
const cache = {
|
|
512
|
+
savedAt: new Date().toISOString(),
|
|
513
|
+
duration: `${duration}m`,
|
|
514
|
+
toolCount: sessionState.toolCalls?.length || 0,
|
|
515
|
+
keyPoints,
|
|
516
|
+
};
|
|
517
|
+
|
|
518
|
+
writeJSONFile(LAST_SESSION_CACHE_FILE, cache);
|
|
519
|
+
} catch (error) {
|
|
520
|
+
logError('writeSessionCache', error);
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
// ============================================================================
|
|
525
|
+
// Session Archive
|
|
526
|
+
// ============================================================================
|
|
527
|
+
|
|
528
|
+
/**
|
|
529
|
+
* Archive current session
|
|
530
|
+
* @param {Object} sessionState - Current session state
|
|
531
|
+
*/
|
|
532
|
+
function archiveSession(sessionState) {
|
|
533
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
534
|
+
const archiveFile = path.join(SESSIONS_ARCHIVE_DIR, `session-${timestamp}.json`);
|
|
535
|
+
|
|
536
|
+
writeJSONFile(archiveFile, sessionState);
|
|
537
|
+
|
|
538
|
+
// Keep only last N sessions (configurable via THRESHOLDS)
|
|
539
|
+
try {
|
|
540
|
+
// Ensure directory exists before reading
|
|
541
|
+
ensureDir(SESSIONS_ARCHIVE_DIR);
|
|
542
|
+
|
|
543
|
+
const sessions = fs.readdirSync(SESSIONS_ARCHIVE_DIR)
|
|
544
|
+
.filter(f => f.startsWith('session-'))
|
|
545
|
+
.sort()
|
|
546
|
+
.reverse();
|
|
547
|
+
|
|
548
|
+
const maxSessions = THRESHOLDS.MAX_ARCHIVED_SESSIONS;
|
|
549
|
+
if (sessions.length > maxSessions) {
|
|
550
|
+
sessions.slice(maxSessions).forEach(f => {
|
|
551
|
+
try {
|
|
552
|
+
fs.unlinkSync(path.join(SESSIONS_ARCHIVE_DIR, f));
|
|
553
|
+
} catch (error) {
|
|
554
|
+
logError('archiveSession.unlink', error);
|
|
555
|
+
}
|
|
556
|
+
});
|
|
557
|
+
}
|
|
558
|
+
} catch (error) {
|
|
559
|
+
logError('archiveSession.readdir', error);
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
// ============================================================================
|
|
564
|
+
// Display Summary
|
|
565
|
+
// ============================================================================
|
|
566
|
+
|
|
567
|
+
/**
|
|
568
|
+
* Display session summary
|
|
569
|
+
* @param {Object} sessionState - Current session state
|
|
570
|
+
* @param {Array} patterns - Detected patterns
|
|
571
|
+
* @param {Object} sessionContext - Session context
|
|
572
|
+
*/
|
|
573
|
+
function displaySessionSummary(sessionState, patterns, sessionContext) {
|
|
574
|
+
console.log('\nš Session Summary\n');
|
|
575
|
+
|
|
576
|
+
// Duration
|
|
577
|
+
const duration = calculateDuration(sessionState.startTime);
|
|
578
|
+
console.log(`ā±ļø Duration: ${duration}`);
|
|
579
|
+
|
|
580
|
+
// Tool executions
|
|
581
|
+
const totalTools = sessionState.toolCalls?.length || 0;
|
|
582
|
+
const successTools = sessionState.toolCalls?.filter(t => t.success !== false).length || 0;
|
|
583
|
+
const failedTools = totalTools - successTools;
|
|
584
|
+
|
|
585
|
+
console.log(`š ļø Tool executions: ${totalTools} (success: ${successTools}, failed: ${failedTools})`);
|
|
586
|
+
|
|
587
|
+
// Detected patterns
|
|
588
|
+
if (patterns.length > 0) {
|
|
589
|
+
console.log('\n⨠Detected patterns:');
|
|
590
|
+
patterns.slice(0, 5).forEach(pattern => {
|
|
591
|
+
const emoji = pattern.severity === 'error' ? 'ā' : pattern.severity === 'warning' ? 'ā ļø' : 'ā
';
|
|
592
|
+
console.log(` ${emoji} ${pattern.description}`);
|
|
593
|
+
if (pattern.suggestion) {
|
|
594
|
+
console.log(` š” ${pattern.suggestion}`);
|
|
595
|
+
}
|
|
596
|
+
});
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
// Recommendations for next session
|
|
600
|
+
const recommendations = readJSONFile(RECOMMENDATIONS_FILE, { recommendedSkills: [] });
|
|
601
|
+
if (recommendations.recommendedSkills?.length > 0) {
|
|
602
|
+
console.log('\nš” Recommended for next session:');
|
|
603
|
+
recommendations.recommendedSkills.slice(0, 3).forEach(skill => {
|
|
604
|
+
console.log(` ⢠${skill.name} (${skill.reason})`);
|
|
605
|
+
});
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
// Quota status (guard against division by zero)
|
|
609
|
+
const quotaLimit = sessionContext.tokenQuota?.limit || 1;
|
|
610
|
+
const quotaUsed = sessionContext.tokenQuota?.used || 0;
|
|
611
|
+
const quotaPercentNum = (quotaUsed / quotaLimit) * 100;
|
|
612
|
+
const quotaEmoji = quotaPercentNum > 80 ? 'š“' : quotaPercentNum > 50 ? 'š”' : 'š¢';
|
|
613
|
+
console.log(`\n${quotaEmoji} Token quota: ${quotaPercentNum.toFixed(1)}% (${quotaUsed.toLocaleString()} / ${quotaLimit.toLocaleString()})`);
|
|
614
|
+
|
|
615
|
+
console.log('\nā
Session state saved\n');
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
// ============================================================================
|
|
619
|
+
// Main Stop Hook Logic
|
|
620
|
+
// ============================================================================
|
|
621
|
+
|
|
622
|
+
function stopHook() {
|
|
623
|
+
console.log('\nš Smart-Agents Session Ending...\n');
|
|
624
|
+
|
|
625
|
+
// Read current session state
|
|
626
|
+
const sessionState = readJSONFile(CURRENT_SESSION_FILE, {
|
|
627
|
+
startTime: new Date().toISOString(),
|
|
628
|
+
toolCalls: [],
|
|
629
|
+
patterns: [],
|
|
630
|
+
});
|
|
631
|
+
|
|
632
|
+
// Analyze patterns
|
|
633
|
+
const patterns = analyzeToolPatterns(sessionState);
|
|
634
|
+
|
|
635
|
+
// Save recommendations for next session
|
|
636
|
+
saveRecommendations(patterns, sessionState);
|
|
637
|
+
|
|
638
|
+
// Update session context
|
|
639
|
+
const sessionContext = updateSessionContext(sessionState);
|
|
640
|
+
|
|
641
|
+
// Archive session
|
|
642
|
+
archiveSession(sessionState);
|
|
643
|
+
|
|
644
|
+
// Save session key points to MeMesh
|
|
645
|
+
saveSessionKeyPointsOnEnd(sessionState, patterns);
|
|
646
|
+
|
|
647
|
+
// Write session cache for fast startup next time (1A.2)
|
|
648
|
+
writeSessionCache(sessionState, patterns);
|
|
649
|
+
|
|
650
|
+
// Clean up old key points (>30 days)
|
|
651
|
+
cleanupOldKeyPoints();
|
|
652
|
+
|
|
653
|
+
// Display summary
|
|
654
|
+
displaySessionSummary(sessionState, patterns, sessionContext);
|
|
655
|
+
|
|
656
|
+
// Clean up current session file
|
|
657
|
+
try {
|
|
658
|
+
fs.unlinkSync(CURRENT_SESSION_FILE);
|
|
659
|
+
} catch {
|
|
660
|
+
// Ignore if file doesn't exist
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
// ============================================================================
|
|
665
|
+
// Execute
|
|
666
|
+
// ============================================================================
|
|
667
|
+
|
|
668
|
+
try {
|
|
669
|
+
stopHook();
|
|
670
|
+
} catch (error) {
|
|
671
|
+
console.error('ā Stop hook error:', error.message);
|
|
672
|
+
process.exit(0); // Never block Claude Code on hook errors
|
|
673
|
+
}
|