claude-memory-layer 1.0.15 → 1.0.17

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/src/cli/index.ts CHANGED
@@ -32,6 +32,7 @@ interface ClaudeSettings {
32
32
  PostToolUse?: Array<{ matcher: string; hooks: Array<{ type: string; command: string }> }>;
33
33
  SessionStart?: Array<{ matcher: string; hooks: Array<{ type: string; command: string }> }>;
34
34
  Stop?: Array<{ matcher: string; hooks: Array<{ type: string; command: string }> }>;
35
+ SessionEnd?: Array<{ matcher: string; hooks: Array<{ type: string; command: string }> }>;
35
36
  };
36
37
  [key: string]: unknown;
37
38
  }
@@ -79,30 +80,39 @@ function saveClaudeSettings(settings: ClaudeSettings): void {
79
80
  fs.renameSync(tempPath, CLAUDE_SETTINGS_PATH);
80
81
  }
81
82
 
83
+ const REQUIRED_HOOK_FILES = [
84
+ 'user-prompt-submit.js',
85
+ 'post-tool-use.js',
86
+ 'session-start.js',
87
+ 'stop.js',
88
+ 'session-end.js'
89
+ ] as const;
90
+
91
+ function hasHook(settings: ClaudeSettings, hookName: keyof NonNullable<ClaudeSettings['hooks']>, commandFragment: string): boolean {
92
+ const hookEntries = settings.hooks?.[hookName];
93
+ if (!hookEntries) return false;
94
+ return hookEntries.some((entry) => entry.hooks?.some((hook) => hook.command?.includes(commandFragment)));
95
+ }
96
+
82
97
  function getHooksConfig(pluginPath: string): ClaudeSettings['hooks'] {
98
+ const makeHook = (fileName: string) => [
99
+ {
100
+ matcher: '',
101
+ hooks: [
102
+ {
103
+ type: 'command',
104
+ command: `node ${path.join(pluginPath, 'hooks', fileName)}`
105
+ }
106
+ ]
107
+ }
108
+ ];
109
+
83
110
  return {
84
- UserPromptSubmit: [
85
- {
86
- matcher: '',
87
- hooks: [
88
- {
89
- type: 'command',
90
- command: `node ${path.join(pluginPath, 'hooks', 'user-prompt-submit.js')}`
91
- }
92
- ]
93
- }
94
- ],
95
- PostToolUse: [
96
- {
97
- matcher: '',
98
- hooks: [
99
- {
100
- type: 'command',
101
- command: `node ${path.join(pluginPath, 'hooks', 'post-tool-use.js')}`
102
- }
103
- ]
104
- }
105
- ]
111
+ SessionStart: makeHook('session-start.js'),
112
+ UserPromptSubmit: makeHook('user-prompt-submit.js'),
113
+ PostToolUse: makeHook('post-tool-use.js'),
114
+ Stop: makeHook('stop.js'),
115
+ SessionEnd: makeHook('session-end.js')
106
116
  };
107
117
  }
108
118
 
@@ -129,9 +139,12 @@ program
129
139
  const pluginPath = options.path || getPluginPath();
130
140
 
131
141
  // Verify hooks exist
132
- const userPromptHook = path.join(pluginPath, 'hooks', 'user-prompt-submit.js');
133
- if (!fs.existsSync(userPromptHook)) {
142
+ const missingHooks = REQUIRED_HOOK_FILES.filter((file) =>
143
+ !fs.existsSync(path.join(pluginPath, 'hooks', file))
144
+ );
145
+ if (missingHooks.length > 0) {
134
146
  console.error(`\n❌ Hook files not found at: ${pluginPath}`);
147
+ console.error(` Missing: ${missingHooks.join(', ')}`);
135
148
  console.error(' Make sure you have built the plugin with "npm run build"');
136
149
  process.exit(1);
137
150
  }
@@ -151,8 +164,11 @@ program
151
164
 
152
165
  console.log('\n✅ Claude Memory Layer installed!\n');
153
166
  console.log('Hooks registered:');
167
+ console.log(' - SessionStart: Register session -> project mapping');
154
168
  console.log(' - UserPromptSubmit: Memory retrieval on user input');
155
- console.log(' - PostToolUse: Store tool observations\n');
169
+ console.log(' - PostToolUse: Store tool observations');
170
+ console.log(' - Stop: Store assistant responses');
171
+ console.log(' - SessionEnd: Persist session summary\n');
156
172
  console.log('Plugin path:', pluginPath);
157
173
  console.log('\n⚠️ Restart Claude Code for changes to take effect.\n');
158
174
  console.log('Commands:');
@@ -183,8 +199,11 @@ program
183
199
  }
184
200
 
185
201
  // Remove our hooks
202
+ delete settings.hooks.SessionStart;
186
203
  delete settings.hooks.UserPromptSubmit;
187
204
  delete settings.hooks.PostToolUse;
205
+ delete settings.hooks.Stop;
206
+ delete settings.hooks.SessionEnd;
188
207
 
189
208
  // Clean up empty hooks object
190
209
  if (Object.keys(settings.hooks).length === 0) {
@@ -219,19 +238,22 @@ program
219
238
  console.log('\n🧠 Claude Memory Layer Status\n');
220
239
 
221
240
  // Check hooks
222
- const hasUserPromptHook = settings.hooks?.UserPromptSubmit?.some(h =>
223
- h.hooks?.some(hook => hook.command?.includes('user-prompt-submit'))
224
- );
225
- const hasPostToolHook = settings.hooks?.PostToolUse?.some(h =>
226
- h.hooks?.some(hook => hook.command?.includes('post-tool-use'))
227
- );
241
+ const hasSessionStartHook = hasHook(settings, 'SessionStart', 'session-start');
242
+ const hasUserPromptHook = hasHook(settings, 'UserPromptSubmit', 'user-prompt-submit');
243
+ const hasPostToolHook = hasHook(settings, 'PostToolUse', 'post-tool-use');
244
+ const hasStopHook = hasHook(settings, 'Stop', 'stop');
245
+ const hasSessionEndHook = hasHook(settings, 'SessionEnd', 'session-end');
228
246
 
229
247
  console.log('Hooks:');
248
+ console.log(` SessionStart: ${hasSessionStartHook ? '✅ Installed' : '❌ Not installed'}`);
230
249
  console.log(` UserPromptSubmit: ${hasUserPromptHook ? '✅ Installed' : '❌ Not installed'}`);
231
250
  console.log(` PostToolUse: ${hasPostToolHook ? '✅ Installed' : '❌ Not installed'}`);
251
+ console.log(` Stop: ${hasStopHook ? '✅ Installed' : '❌ Not installed'}`);
252
+ console.log(` SessionEnd: ${hasSessionEndHook ? '✅ Installed' : '❌ Not installed'}`);
232
253
 
233
254
  // Check plugin files
234
- const hooksExist = fs.existsSync(path.join(pluginPath, 'hooks', 'user-prompt-submit.js'));
255
+ const hooksExist = REQUIRED_HOOK_FILES
256
+ .every((file) => fs.existsSync(path.join(pluginPath, 'hooks', file)));
235
257
  console.log(`\nPlugin files: ${hooksExist ? '✅ Found' : '❌ Not found'}`);
236
258
  console.log(` Path: ${pluginPath}`);
237
259
 
@@ -239,7 +261,7 @@ program
239
261
  const dashboardRunning = await isServerRunning(37777);
240
262
  console.log(`\nDashboard: ${dashboardRunning ? '✅ Running at http://localhost:37777' : '⏹️ Not running'}`);
241
263
 
242
- if (!hasUserPromptHook || !hasPostToolHook) {
264
+ if (!hasSessionStartHook || !hasUserPromptHook || !hasPostToolHook || !hasStopHook || !hasSessionEndHook) {
243
265
  console.log('\n💡 Run "claude-memory-layer install" to set up hooks.\n');
244
266
  } else {
245
267
  console.log('\n✅ Plugin is fully installed and configured.\n');
@@ -1446,28 +1446,33 @@ export class SQLiteEventStore {
1446
1446
  }>> {
1447
1447
  await this.initialize();
1448
1448
 
1449
- const rows = sqliteAll<Record<string, unknown>>(
1450
- this.db,
1451
- `SELECT * FROM retrieval_traces ORDER BY created_at DESC LIMIT ?`,
1452
- [limit]
1453
- );
1449
+ try {
1450
+ const rows = sqliteAll<Record<string, unknown>>(
1451
+ this.db,
1452
+ `SELECT * FROM retrieval_traces ORDER BY created_at DESC LIMIT ?`,
1453
+ [limit]
1454
+ );
1454
1455
 
1455
- return rows.map((row) => ({
1456
- traceId: row.trace_id as string,
1457
- sessionId: (row.session_id as string) || undefined,
1458
- projectHash: (row.project_hash as string) || undefined,
1459
- queryText: row.query_text as string,
1460
- strategy: (row.strategy as string) || undefined,
1461
- candidateEventIds: row.candidate_event_ids ? JSON.parse(row.candidate_event_ids as string) : [],
1462
- selectedEventIds: row.selected_event_ids ? JSON.parse(row.selected_event_ids as string) : [],
1463
- candidateDetails: row.candidate_details_json ? JSON.parse(row.candidate_details_json as string) : [],
1464
- selectedDetails: row.selected_details_json ? JSON.parse(row.selected_details_json as string) : [],
1465
- candidateCount: Number(row.candidate_count || 0),
1466
- selectedCount: Number(row.selected_count || 0),
1467
- confidence: (row.confidence as string) || undefined,
1468
- fallbackTrace: row.fallback_trace ? JSON.parse(row.fallback_trace as string) : [],
1469
- createdAt: toDateFromSQLite(row.created_at),
1470
- }));
1456
+ return rows.map((row) => ({
1457
+ traceId: row.trace_id as string,
1458
+ sessionId: (row.session_id as string) || undefined,
1459
+ projectHash: (row.project_hash as string) || undefined,
1460
+ queryText: row.query_text as string,
1461
+ strategy: (row.strategy as string) || undefined,
1462
+ candidateEventIds: row.candidate_event_ids ? JSON.parse(row.candidate_event_ids as string) : [],
1463
+ selectedEventIds: row.selected_event_ids ? JSON.parse(row.selected_event_ids as string) : [],
1464
+ candidateDetails: row.candidate_details_json ? JSON.parse(row.candidate_details_json as string) : [],
1465
+ selectedDetails: row.selected_details_json ? JSON.parse(row.selected_details_json as string) : [],
1466
+ candidateCount: Number(row.candidate_count || 0),
1467
+ selectedCount: Number(row.selected_count || 0),
1468
+ confidence: (row.confidence as string) || undefined,
1469
+ fallbackTrace: row.fallback_trace ? JSON.parse(row.fallback_trace as string) : [],
1470
+ createdAt: toDateFromSQLite(row.created_at),
1471
+ }));
1472
+ } catch (err: any) {
1473
+ if (err?.message?.includes('no such table')) return [];
1474
+ throw err;
1475
+ }
1471
1476
  }
1472
1477
 
1473
1478
  async getRetrievalTraceStats(): Promise<{
@@ -1478,26 +1483,33 @@ export class SQLiteEventStore {
1478
1483
  }> {
1479
1484
  await this.initialize();
1480
1485
 
1481
- const row = sqliteGet<Record<string, unknown>>(
1482
- this.db,
1483
- `SELECT
1484
- COUNT(*) as total_queries,
1485
- AVG(candidate_count) as avg_candidate_count,
1486
- AVG(selected_count) as avg_selected_count,
1487
- CASE
1488
- WHEN SUM(candidate_count) > 0 THEN (SUM(selected_count) * 1.0 / SUM(candidate_count))
1489
- ELSE 0
1490
- END as selection_rate
1491
- FROM retrieval_traces`,
1492
- []
1493
- );
1486
+ try {
1487
+ const row = sqliteGet<Record<string, unknown>>(
1488
+ this.db,
1489
+ `SELECT
1490
+ COUNT(*) as total_queries,
1491
+ AVG(candidate_count) as avg_candidate_count,
1492
+ AVG(selected_count) as avg_selected_count,
1493
+ CASE
1494
+ WHEN SUM(candidate_count) > 0 THEN (SUM(selected_count) * 1.0 / SUM(candidate_count))
1495
+ ELSE 0
1496
+ END as selection_rate
1497
+ FROM retrieval_traces`,
1498
+ []
1499
+ );
1494
1500
 
1495
- return {
1496
- totalQueries: Number(row?.total_queries || 0),
1497
- avgCandidateCount: Number(row?.avg_candidate_count || 0),
1498
- avgSelectedCount: Number(row?.avg_selected_count || 0),
1499
- selectionRate: Number(row?.selection_rate || 0),
1500
- };
1501
+ return {
1502
+ totalQueries: Number(row?.total_queries || 0),
1503
+ avgCandidateCount: Number(row?.avg_candidate_count || 0),
1504
+ avgSelectedCount: Number(row?.avg_selected_count || 0),
1505
+ selectionRate: Number(row?.selection_rate || 0),
1506
+ };
1507
+ } catch (err: any) {
1508
+ if (err?.message?.includes('no such table')) {
1509
+ return { totalQueries: 0, avgCandidateCount: 0, avgSelectedCount: 0, selectionRate: 0 };
1510
+ }
1511
+ throw err;
1512
+ }
1501
1513
  }
1502
1514
 
1503
1515
  /**
@@ -17,7 +17,9 @@ import type { UserPromptSubmitInput, UserPromptSubmitOutput } from '../core/type
17
17
 
18
18
  // Configuration
19
19
  const MAX_MEMORIES = parseInt(process.env.CLAUDE_MEMORY_MAX_COUNT || '5');
20
- const MIN_SCORE = parseFloat(process.env.CLAUDE_MEMORY_MIN_SCORE || '0.3');
20
+ // Tuned default for noise/recall balance on shopping_assistant-like corpus
21
+ const BASE_MIN_SCORE = parseFloat(process.env.CLAUDE_MEMORY_MIN_SCORE || '0.4');
22
+ const FALLBACK_MIN_SCORE = parseFloat(process.env.CLAUDE_MEMORY_FALLBACK_MIN_SCORE || '0.3');
21
23
  const ENABLE_SEARCH = process.env.CLAUDE_MEMORY_SEARCH !== 'false';
22
24
 
23
25
  /**
@@ -32,6 +34,14 @@ function shouldStorePrompt(prompt: string): boolean {
32
34
  return true;
33
35
  }
34
36
 
37
+
38
+ function getDynamicMinScore(prompt: string): number {
39
+ const len = prompt.trim().length;
40
+ if (len <= 20) return Math.min(0.55, BASE_MIN_SCORE + 0.1); // short query → stricter
41
+ if (len >= 80) return Math.max(0.3, BASE_MIN_SCORE - 0.05); // long query → slightly looser
42
+ return BASE_MIN_SCORE;
43
+ }
44
+
35
45
  async function main(): Promise<void> {
36
46
  // Read input from stdin
37
47
  const inputData = await readStdin();
@@ -61,11 +71,20 @@ async function main(): Promise<void> {
61
71
 
62
72
  // Fast keyword search if enabled
63
73
  if (ENABLE_SEARCH && input.prompt.length > 10) {
64
- const results = await memoryService.keywordSearch(input.prompt, {
74
+ const minScore = getDynamicMinScore(input.prompt);
75
+ let results = await memoryService.keywordSearch(input.prompt, {
65
76
  topK: MAX_MEMORIES,
66
- minScore: MIN_SCORE
77
+ minScore
67
78
  });
68
79
 
80
+ // recall rescue: if nothing found at tuned threshold, retry with fallback floor
81
+ if (results.length === 0 && FALLBACK_MIN_SCORE < minScore) {
82
+ results = await memoryService.keywordSearch(input.prompt, {
83
+ topK: MAX_MEMORIES,
84
+ minScore: FALLBACK_MIN_SCORE
85
+ });
86
+ }
87
+
69
88
  if (results.length > 0) {
70
89
  // Increment access count for found memories
71
90
  const eventIds = results.map(r => r.event.id);