@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.
@@ -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
+ }