@aitytech/agentkits-memory 1.0.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (110) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +267 -149
  3. package/assets/agentkits-memory-add-memory.png +0 -0
  4. package/assets/agentkits-memory-memory-detail.png +0 -0
  5. package/assets/agentkits-memory-memory-list.png +0 -0
  6. package/assets/logo.svg +24 -0
  7. package/dist/better-sqlite3-backend.d.ts +192 -0
  8. package/dist/better-sqlite3-backend.d.ts.map +1 -0
  9. package/dist/better-sqlite3-backend.js +801 -0
  10. package/dist/better-sqlite3-backend.js.map +1 -0
  11. package/dist/cli/save.js +0 -0
  12. package/dist/cli/setup.d.ts +6 -2
  13. package/dist/cli/setup.d.ts.map +1 -1
  14. package/dist/cli/setup.js +289 -42
  15. package/dist/cli/setup.js.map +1 -1
  16. package/dist/cli/viewer.js +25 -56
  17. package/dist/cli/viewer.js.map +1 -1
  18. package/dist/cli/web-viewer.d.ts +14 -0
  19. package/dist/cli/web-viewer.d.ts.map +1 -0
  20. package/dist/cli/web-viewer.js +1769 -0
  21. package/dist/cli/web-viewer.js.map +1 -0
  22. package/dist/embeddings/embedding-cache.d.ts +131 -0
  23. package/dist/embeddings/embedding-cache.d.ts.map +1 -0
  24. package/dist/embeddings/embedding-cache.js +217 -0
  25. package/dist/embeddings/embedding-cache.js.map +1 -0
  26. package/dist/embeddings/index.d.ts +11 -0
  27. package/dist/embeddings/index.d.ts.map +1 -0
  28. package/dist/embeddings/index.js +11 -0
  29. package/dist/embeddings/index.js.map +1 -0
  30. package/dist/embeddings/local-embeddings.d.ts +140 -0
  31. package/dist/embeddings/local-embeddings.d.ts.map +1 -0
  32. package/dist/embeddings/local-embeddings.js +293 -0
  33. package/dist/embeddings/local-embeddings.js.map +1 -0
  34. package/dist/hooks/context.d.ts +6 -1
  35. package/dist/hooks/context.d.ts.map +1 -1
  36. package/dist/hooks/context.js +12 -2
  37. package/dist/hooks/context.js.map +1 -1
  38. package/dist/hooks/observation.d.ts +6 -1
  39. package/dist/hooks/observation.d.ts.map +1 -1
  40. package/dist/hooks/observation.js +12 -2
  41. package/dist/hooks/observation.js.map +1 -1
  42. package/dist/hooks/service.d.ts +1 -6
  43. package/dist/hooks/service.d.ts.map +1 -1
  44. package/dist/hooks/service.js +33 -85
  45. package/dist/hooks/service.js.map +1 -1
  46. package/dist/hooks/session-init.d.ts +6 -1
  47. package/dist/hooks/session-init.d.ts.map +1 -1
  48. package/dist/hooks/session-init.js +12 -2
  49. package/dist/hooks/session-init.js.map +1 -1
  50. package/dist/hooks/summarize.d.ts +6 -1
  51. package/dist/hooks/summarize.d.ts.map +1 -1
  52. package/dist/hooks/summarize.js +12 -2
  53. package/dist/hooks/summarize.js.map +1 -1
  54. package/dist/index.d.ts +10 -17
  55. package/dist/index.d.ts.map +1 -1
  56. package/dist/index.js +172 -94
  57. package/dist/index.js.map +1 -1
  58. package/dist/mcp/server.js +17 -3
  59. package/dist/mcp/server.js.map +1 -1
  60. package/dist/migration.js +3 -3
  61. package/dist/migration.js.map +1 -1
  62. package/dist/search/hybrid-search.d.ts +262 -0
  63. package/dist/search/hybrid-search.d.ts.map +1 -0
  64. package/dist/search/hybrid-search.js +688 -0
  65. package/dist/search/hybrid-search.js.map +1 -0
  66. package/dist/search/index.d.ts +13 -0
  67. package/dist/search/index.d.ts.map +1 -0
  68. package/dist/search/index.js +13 -0
  69. package/dist/search/index.js.map +1 -0
  70. package/dist/search/token-economics.d.ts +161 -0
  71. package/dist/search/token-economics.d.ts.map +1 -0
  72. package/dist/search/token-economics.js +239 -0
  73. package/dist/search/token-economics.js.map +1 -0
  74. package/dist/types.d.ts +0 -68
  75. package/dist/types.d.ts.map +1 -1
  76. package/dist/types.js.map +1 -1
  77. package/package.json +23 -8
  78. package/src/__tests__/better-sqlite3-backend.test.ts +1466 -0
  79. package/src/__tests__/cache-manager.test.ts +499 -0
  80. package/src/__tests__/embedding-integration.test.ts +481 -0
  81. package/src/__tests__/hnsw-index.test.ts +727 -0
  82. package/src/__tests__/index.test.ts +432 -0
  83. package/src/better-sqlite3-backend.ts +1000 -0
  84. package/src/cli/setup.ts +358 -47
  85. package/src/cli/viewer.ts +28 -63
  86. package/src/cli/web-viewer.ts +1956 -0
  87. package/src/embeddings/__tests__/embedding-cache.test.ts +269 -0
  88. package/src/embeddings/__tests__/local-embeddings.test.ts +495 -0
  89. package/src/embeddings/embedding-cache.ts +318 -0
  90. package/src/embeddings/index.ts +20 -0
  91. package/src/embeddings/local-embeddings.ts +419 -0
  92. package/src/hooks/__tests__/handlers.test.ts +58 -17
  93. package/src/hooks/__tests__/integration.test.ts +77 -26
  94. package/src/hooks/context.ts +13 -2
  95. package/src/hooks/observation.ts +13 -2
  96. package/src/hooks/service.ts +39 -100
  97. package/src/hooks/session-init.ts +13 -2
  98. package/src/hooks/summarize.ts +13 -2
  99. package/src/index.ts +210 -116
  100. package/src/mcp/server.ts +20 -3
  101. package/src/search/__tests__/hybrid-search.test.ts +669 -0
  102. package/src/search/__tests__/token-economics.test.ts +276 -0
  103. package/src/search/hybrid-search.ts +968 -0
  104. package/src/search/index.ts +29 -0
  105. package/src/search/token-economics.ts +367 -0
  106. package/src/types.ts +0 -96
  107. package/src/__tests__/sqljs-backend.test.ts +0 -410
  108. package/src/migration.ts +0 -574
  109. package/src/sql.js.d.ts +0 -70
  110. package/src/sqljs-backend.ts +0 -789
package/src/cli/setup.ts CHANGED
@@ -2,14 +2,18 @@
2
2
  /**
3
3
  * AgentKits Memory Setup CLI
4
4
  *
5
- * Sets up memory hooks in a project's .claude/settings.json
5
+ * Sets up memory hooks, MCP server, and downloads embedding model.
6
+ * Supports multiple AI tools: Claude Code, Cursor, Windsurf, etc.
6
7
  *
7
8
  * Usage:
8
9
  * npx agentkits-memory-setup [options]
9
10
  *
10
11
  * Options:
11
12
  * --project-dir=X Project directory (default: cwd)
12
- * --force Overwrite existing hooks
13
+ * --force Overwrite existing configuration
14
+ * --skip-model Skip embedding model download
15
+ * --skip-mcp Skip MCP server configuration
16
+ * --show-hooks Show full hooks JSON for manual configuration
13
17
  * --json Output result as JSON
14
18
  *
15
19
  * @module @agentkits/memory/cli/setup
@@ -17,6 +21,7 @@
17
21
 
18
22
  import * as fs from 'node:fs';
19
23
  import * as path from 'node:path';
24
+ import { LocalEmbeddingsService } from '../embeddings/local-embeddings.js';
20
25
 
21
26
  const args = process.argv.slice(2);
22
27
 
@@ -27,6 +32,8 @@ interface HookEntry {
27
32
 
28
33
  interface HooksConfig {
29
34
  SessionStart?: HookEntry[];
35
+ UserPromptSubmit?: HookEntry[];
36
+ PostToolUse?: HookEntry[];
30
37
  Stop?: HookEntry[];
31
38
  PreCompact?: HookEntry[];
32
39
  [key: string]: HookEntry[] | undefined;
@@ -34,10 +41,36 @@ interface HooksConfig {
34
41
 
35
42
  interface ClaudeSettings {
36
43
  hooks?: HooksConfig;
44
+ mcpServers?: Record<string, McpServerConfig>;
37
45
  [key: string]: unknown;
38
46
  }
39
47
 
48
+ interface McpServerConfig {
49
+ command: string;
50
+ args?: string[];
51
+ env?: Record<string, string>;
52
+ }
53
+
54
+ interface McpConfig {
55
+ mcpServers: Record<string, McpServerConfig>;
56
+ }
57
+
58
+ const MEMORY_MCP_SERVER: McpServerConfig = {
59
+ command: 'npx',
60
+ args: ['-y', 'agentkits-memory-server'],
61
+ };
62
+
63
+ /**
64
+ * Memory hooks for Claude Code lifecycle events
65
+ *
66
+ * Hook Events:
67
+ * - SessionStart: Load memory context at session start
68
+ * - UserPromptSubmit: Capture user intent when prompt submitted
69
+ * - PostToolUse: Capture observations after tool execution (Edit, Write, Bash, Task)
70
+ * - Stop: Generate summary when session ends
71
+ */
40
72
  const MEMORY_HOOKS: HooksConfig = {
73
+ // Load memory context at session start
41
74
  SessionStart: [
42
75
  {
43
76
  matcher: '',
@@ -45,11 +78,42 @@ const MEMORY_HOOKS: HooksConfig = {
45
78
  {
46
79
  type: 'command',
47
80
  command: 'npx --yes agentkits-memory-hook context',
81
+ timeout: 15,
82
+ },
83
+ ],
84
+ },
85
+ ],
86
+
87
+ // Capture user intent when prompt submitted
88
+ UserPromptSubmit: [
89
+ {
90
+ matcher: '',
91
+ hooks: [
92
+ {
93
+ type: 'command',
94
+ command: 'npx --yes agentkits-memory-hook session-init',
48
95
  timeout: 10,
49
96
  },
50
97
  ],
51
98
  },
52
99
  ],
100
+
101
+ // Capture observations after tool execution
102
+ // Only track meaningful tools: Edit, Write, Bash, Task
103
+ PostToolUse: [
104
+ {
105
+ matcher: 'Edit|Write|Bash|Task',
106
+ hooks: [
107
+ {
108
+ type: 'command',
109
+ command: 'npx --yes agentkits-memory-hook observation',
110
+ timeout: 15,
111
+ },
112
+ ],
113
+ },
114
+ ],
115
+
116
+ // Generate summary when session ends
53
117
  Stop: [
54
118
  {
55
119
  matcher: '',
@@ -57,7 +121,7 @@ const MEMORY_HOOKS: HooksConfig = {
57
121
  {
58
122
  type: 'command',
59
123
  command: 'npx --yes agentkits-memory-hook summarize',
60
- timeout: 10,
124
+ timeout: 15,
61
125
  },
62
126
  ],
63
127
  },
@@ -81,37 +145,222 @@ function parseArgs(): Record<string, string | boolean> {
81
145
  return parsed;
82
146
  }
83
147
 
148
+ interface MergeResult {
149
+ merged: HooksConfig;
150
+ added: string[];
151
+ skipped: string[];
152
+ manualRequired: string[];
153
+ }
154
+
155
+ /**
156
+ * Merge memory hooks with existing hooks configuration
157
+ * Always preserves existing hooks, never overwrites
158
+ */
84
159
  function mergeHooks(
85
160
  existing: HooksConfig | undefined,
86
161
  newHooks: HooksConfig,
87
162
  force: boolean
88
- ): HooksConfig {
89
- if (!existing || force) {
90
- return { ...existing, ...newHooks };
163
+ ): MergeResult {
164
+ const result: MergeResult = {
165
+ merged: { ...existing },
166
+ added: [],
167
+ skipped: [],
168
+ manualRequired: [],
169
+ };
170
+
171
+ // If no existing hooks, add all memory hooks
172
+ if (!existing) {
173
+ result.merged = { ...newHooks };
174
+ result.added = Object.keys(newHooks);
175
+ return result;
91
176
  }
92
177
 
93
- const merged: HooksConfig = { ...existing };
178
+ for (const [event, memoryHooks] of Object.entries(newHooks)) {
179
+ if (!memoryHooks) continue;
94
180
 
95
- for (const [event, hooks] of Object.entries(newHooks)) {
96
- if (!hooks) continue;
181
+ const existingHooks = result.merged[event];
97
182
 
98
- const existingHooks = merged[event];
183
+ // Case 1: No existing hooks for this event → add ours
99
184
  if (!existingHooks) {
100
- merged[event] = hooks;
101
- } else {
102
- // Check if memory hook already exists
103
- const hasMemoryHook = existingHooks.some((h: HookEntry) =>
104
- h.hooks.some((hook) => hook.command.includes('agentkits-memory'))
185
+ result.merged[event] = memoryHooks;
186
+ result.added.push(event);
187
+ continue;
188
+ }
189
+
190
+ // Case 2: Check if our memory hook already exists
191
+ const hasMemoryHook = existingHooks.some((h: HookEntry) =>
192
+ h.hooks.some((hook) => hook.command.includes('agentkits-memory'))
193
+ );
194
+
195
+ if (hasMemoryHook) {
196
+ if (force) {
197
+ // Remove old memory hooks, add new ones
198
+ const filtered = existingHooks.filter(
199
+ (h: HookEntry) => !h.hooks.some((hook) => hook.command.includes('agentkits-memory'))
200
+ );
201
+ result.merged[event] = [...filtered, ...memoryHooks];
202
+ result.added.push(`${event} (updated)`);
203
+ } else {
204
+ result.skipped.push(`${event} (already configured)`);
205
+ }
206
+ continue;
207
+ }
208
+
209
+ // Case 3: PostToolUse with different matcher - needs attention
210
+ if (event === 'PostToolUse') {
211
+ const ourMatcher = memoryHooks[0]?.matcher || '';
212
+ const existingMatchers = existingHooks.map((h: HookEntry) => h.matcher || '');
213
+
214
+ // Check for potential conflicts (different matchers that might overlap)
215
+ const hasConflict = existingMatchers.some(
216
+ (m) => m && m !== ourMatcher && (m === '*' || ourMatcher.split('|').some((t) => m.includes(t)))
105
217
  );
106
218
 
107
- if (!hasMemoryHook) {
108
- // Append memory hooks
109
- merged[event] = [...existingHooks, ...hooks];
219
+ if (hasConflict) {
220
+ // Add anyway but warn user
221
+ result.merged[event] = [...existingHooks, ...memoryHooks];
222
+ result.added.push(event);
223
+ result.manualRequired.push(
224
+ `PostToolUse: Added with matcher "${ourMatcher}", existing has different matchers - verify no conflicts`
225
+ );
226
+ continue;
110
227
  }
111
228
  }
229
+
230
+ // Case 4: Append our hooks to existing
231
+ result.merged[event] = [...existingHooks, ...memoryHooks];
232
+ result.added.push(event);
112
233
  }
113
234
 
114
- return merged;
235
+ return result;
236
+ }
237
+
238
+ /**
239
+ * Configure MCP server for different AI tools
240
+ * Creates/updates config files for: Claude Code, Cursor, Windsurf, etc.
241
+ */
242
+ function configureMcp(
243
+ projectDir: string,
244
+ claudeSettings: ClaudeSettings,
245
+ force: boolean,
246
+ asJson: boolean
247
+ ): { configured: string[]; skipped: string[] } {
248
+ const configured: string[] = [];
249
+ const skipped: string[] = [];
250
+
251
+ // 1. Add to Claude Code settings.json (mcpServers key)
252
+ // Always merge with existing servers, never overwrite
253
+ if (!claudeSettings.mcpServers) {
254
+ claudeSettings.mcpServers = {};
255
+ }
256
+
257
+ if (!claudeSettings.mcpServers.memory || force) {
258
+ claudeSettings.mcpServers.memory = MEMORY_MCP_SERVER;
259
+ configured.push('Claude Code (.claude/settings.json)');
260
+ } else {
261
+ skipped.push('Claude Code (already configured)');
262
+ }
263
+
264
+ // 2. Create/update root .mcp.json for other tools (Cursor, Windsurf, Claude Code, etc.)
265
+ // Always merge with existing servers, never overwrite
266
+ const mcpJsonPath = path.join(projectDir, '.mcp.json');
267
+ try {
268
+ let existing: McpConfig = { mcpServers: {} };
269
+
270
+ // Load existing config if present
271
+ if (fs.existsSync(mcpJsonPath)) {
272
+ try {
273
+ existing = JSON.parse(fs.readFileSync(mcpJsonPath, 'utf-8')) as McpConfig;
274
+ existing.mcpServers = existing.mcpServers || {};
275
+ } catch {
276
+ // If parse fails, start fresh but warn
277
+ if (!asJson) {
278
+ console.warn(' ⚠ .mcp.json parse error, creating new config');
279
+ }
280
+ existing = { mcpServers: {} };
281
+ }
282
+ }
283
+
284
+ // Add or update memory server
285
+ if (!existing.mcpServers.memory || force) {
286
+ existing.mcpServers.memory = MEMORY_MCP_SERVER;
287
+ fs.writeFileSync(mcpJsonPath, JSON.stringify(existing, null, 2));
288
+ configured.push('Universal (.mcp.json)');
289
+ } else {
290
+ skipped.push('.mcp.json (already configured)');
291
+ }
292
+ } catch (error) {
293
+ skipped.push(`.mcp.json (error: ${error instanceof Error ? error.message : 'unknown'})`);
294
+ }
295
+
296
+ if (!asJson && configured.length > 0) {
297
+ console.log('\nšŸ”Œ MCP Server configured for:');
298
+ for (const tool of configured) {
299
+ console.log(` āœ“ ${tool}`);
300
+ }
301
+ }
302
+
303
+ return { configured, skipped };
304
+ }
305
+
306
+ async function downloadModel(cacheDir: string, asJson: boolean): Promise<boolean> {
307
+ if (!asJson) {
308
+ console.log('\nšŸ“„ Downloading embedding model...');
309
+ console.log(' Model: multilingual-e5-small (~470MB)');
310
+ console.log(' This enables semantic search in 100+ languages.\n');
311
+ }
312
+
313
+ try {
314
+ const embeddingsService = new LocalEmbeddingsService({
315
+ showProgress: !asJson,
316
+ cacheDir: path.join(cacheDir, 'embeddings-cache'),
317
+ });
318
+
319
+ await embeddingsService.initialize();
320
+
321
+ // Verify model works with a test embedding
322
+ const testResult = await embeddingsService.embed('Test embedding');
323
+
324
+ if (testResult.embedding.length !== 384) {
325
+ throw new Error(`Unexpected embedding dimension: ${testResult.embedding.length}`);
326
+ }
327
+
328
+ if (!asJson) {
329
+ console.log(' āœ“ Model downloaded and verified\n');
330
+ }
331
+
332
+ return true;
333
+ } catch (error) {
334
+ if (!asJson) {
335
+ console.error(' ⚠ Model download failed:', error instanceof Error ? error.message : error);
336
+ console.log(' Model will be downloaded on first use.\n');
337
+ }
338
+ return false;
339
+ }
340
+ }
341
+
342
+ /**
343
+ * Print full hooks configuration for manual setup
344
+ */
345
+ function printHooksConfig(): void {
346
+ console.log('\n' + '━'.repeat(60));
347
+ console.log('šŸ“‹ MEMORY HOOKS CONFIGURATION\n');
348
+ console.log('Copy and paste this JSON into your settings file.\n');
349
+
350
+ console.log('For Claude Code: .claude/settings.json');
351
+ console.log('─'.repeat(40));
352
+ console.log(JSON.stringify({ hooks: MEMORY_HOOKS }, null, 2));
353
+
354
+ console.log('\nFor Cursor/Windsurf: .cursor/mcp.json or settings');
355
+ console.log('─'.repeat(40));
356
+ console.log('Add hooks section with the same configuration above.\n');
357
+
358
+ console.log('Hook Events:');
359
+ console.log(' • SessionStart - Load memory context when session begins');
360
+ console.log(' • UserPromptSubmit - Capture user intent on each prompt');
361
+ console.log(' • PostToolUse - Record actions (Edit, Write, Bash, Task)');
362
+ console.log(' • Stop - Generate summary when session ends');
363
+ console.log('━'.repeat(60) + '\n');
115
364
  }
116
365
 
117
366
  async function main() {
@@ -119,12 +368,25 @@ async function main() {
119
368
  const projectDir = (options['project-dir'] as string) || process.env.CLAUDE_PROJECT_DIR || process.cwd();
120
369
  const force = !!options.force;
121
370
  const asJson = !!options.json;
371
+ const skipModel = !!options['skip-model'];
372
+ const skipMcp = !!options['skip-mcp'];
373
+ const showHooks = !!options['show-hooks'];
374
+
375
+ // Just show hooks config and exit
376
+ if (showHooks) {
377
+ printHooksConfig();
378
+ return;
379
+ }
122
380
 
123
381
  const claudeDir = path.join(projectDir, '.claude');
124
382
  const settingsPath = path.join(claudeDir, 'settings.json');
125
383
  const memoryDir = path.join(claudeDir, 'memory');
126
384
 
127
385
  try {
386
+ if (!asJson) {
387
+ console.log('\n🧠 AgentKits Memory Setup\n');
388
+ }
389
+
128
390
  // Create directories
129
391
  if (!fs.existsSync(claudeDir)) {
130
392
  fs.mkdirSync(claudeDir, { recursive: true });
@@ -141,49 +403,98 @@ async function main() {
141
403
  }
142
404
 
143
405
  // Merge hooks
144
- settings.hooks = mergeHooks(settings.hooks, MEMORY_HOOKS, force);
406
+ const hooksResult = mergeHooks(settings.hooks, MEMORY_HOOKS, force);
407
+ settings.hooks = hooksResult.merged;
408
+
409
+ // Configure MCP server
410
+ let mcpResult = { configured: [] as string[], skipped: [] as string[] };
411
+ if (!skipMcp) {
412
+ mcpResult = configureMcp(projectDir, settings, force, asJson);
413
+ }
145
414
 
146
415
  // Write settings
147
416
  fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
148
417
 
149
- // Create initial memory files if they don't exist
150
- const activeContextPath = path.join(memoryDir, 'active-context.md');
151
- if (!fs.existsSync(activeContextPath)) {
152
- fs.writeFileSync(
153
- activeContextPath,
154
- `# Active Context
155
-
156
- **Task**: None
157
- **Status**: Ready
158
- **Updated**: ${new Date().toISOString()}
159
-
160
- ## Current Focus
161
-
162
- No active task.
163
-
164
- ## Notes
165
-
166
- Memory system initialized.
167
- `
168
- );
418
+ // Download embedding model
419
+ let modelDownloaded = false;
420
+ if (!skipModel) {
421
+ modelDownloaded = await downloadModel(memoryDir, asJson);
169
422
  }
170
423
 
171
424
  const result = {
172
425
  success: true,
173
426
  settingsPath,
174
427
  memoryDir,
175
- hooksAdded: Object.keys(MEMORY_HOOKS),
176
- message: 'Memory hooks configured successfully',
428
+ hooksAdded: hooksResult.added,
429
+ hooksSkipped: hooksResult.skipped,
430
+ hooksManualRequired: hooksResult.manualRequired,
431
+ mcpConfigured: mcpResult.configured,
432
+ modelDownloaded,
433
+ message: 'Memory setup complete',
177
434
  };
178
435
 
179
436
  if (asJson) {
180
437
  console.log(JSON.stringify(result, null, 2));
181
438
  } else {
182
- console.log('\nāœ… AgentKits Memory Setup Complete\n');
183
- console.log(`Settings: ${settingsPath}`);
184
- console.log(`Memory: ${memoryDir}`);
185
- console.log(`\nHooks added: ${result.hooksAdded.join(', ')}`);
186
- console.log('\nRestart Claude Code to activate memory hooks.\n');
439
+ console.log('āœ… Setup Complete\n');
440
+ console.log(`šŸ“ Settings: ${settingsPath}`);
441
+ console.log(`šŸ“ Memory: ${memoryDir}`);
442
+
443
+ // Show hooks status
444
+ if (hooksResult.added.length > 0) {
445
+ console.log(`\nšŸ“‹ Hooks added: ${hooksResult.added.join(', ')}`);
446
+ }
447
+ if (hooksResult.skipped.length > 0) {
448
+ console.log(` Skipped: ${hooksResult.skipped.join(', ')}`);
449
+ }
450
+
451
+ // Show manual action required
452
+ if (hooksResult.manualRequired.length > 0) {
453
+ console.log('\nāš ļø Manual review recommended:');
454
+ for (const msg of hooksResult.manualRequired) {
455
+ console.log(` • ${msg}`);
456
+ }
457
+ }
458
+
459
+ // Model status
460
+ if (modelDownloaded) {
461
+ console.log('\nšŸ“¦ Model: Downloaded and ready');
462
+ } else if (skipModel) {
463
+ console.log('\nšŸ“¦ Model: Skipped (will download on first use)');
464
+ }
465
+
466
+ console.log('\nšŸ‘‰ Restart your AI tool to activate.');
467
+ console.log('šŸ’” Open web viewer: npx agentkits-memory-web');
468
+ console.log('šŸ“‹ Show hooks config: npx agentkits-memory-setup --show-hooks\n');
469
+
470
+ // Show manual hook instructions if some hooks couldn't be added
471
+ const allHookEvents = Object.keys(MEMORY_HOOKS);
472
+ const addedEvents = hooksResult.added.map((h) => h.replace(/ \(.*\)$/, ''));
473
+ const missingEvents = allHookEvents.filter(
474
+ (e) => !addedEvents.includes(e) && !hooksResult.skipped.some((s) => s.startsWith(e))
475
+ );
476
+
477
+ if (missingEvents.length > 0) {
478
+ console.log('━'.repeat(60));
479
+ console.log('šŸ“ MANUAL SETUP REQUIRED\n');
480
+ console.log(`Some hooks could not be auto-configured.`);
481
+ console.log(`Missing: ${missingEvents.join(', ')}\n`);
482
+ console.log(`To add manually:`);
483
+ console.log(`1. Open: ${settingsPath}`);
484
+ console.log(`2. Add/merge the following into the "hooks" section:\n`);
485
+
486
+ // Generate copy-paste JSON for missing hooks only
487
+ const missingHooksJson: Record<string, HookEntry[]> = {};
488
+ for (const event of missingEvents) {
489
+ const hookConfig = MEMORY_HOOKS[event];
490
+ if (hookConfig) {
491
+ missingHooksJson[event] = hookConfig;
492
+ }
493
+ }
494
+
495
+ console.log(JSON.stringify(missingHooksJson, null, 2));
496
+ console.log('\n━'.repeat(60));
497
+ }
187
498
  }
188
499
  } catch (error) {
189
500
  const result = {
package/src/cli/viewer.ts CHANGED
@@ -20,8 +20,8 @@
20
20
 
21
21
  import * as fs from 'node:fs';
22
22
  import * as path from 'node:path';
23
- import { createRequire } from 'node:module';
24
- import initSqlJs, { Database as SqlJsDatabase } from 'sql.js';
23
+ import Database from 'better-sqlite3';
24
+ import type { Database as BetterDatabase } from 'better-sqlite3';
25
25
 
26
26
  const args = process.argv.slice(2);
27
27
  const projectDir = process.env.CLAUDE_PROJECT_DIR || process.cwd();
@@ -37,54 +37,45 @@ function parseArgs(): Record<string, string | boolean> {
37
37
  return parsed;
38
38
  }
39
39
 
40
- async function loadDatabase(): Promise<SqlJsDatabase> {
41
- const require = createRequire(import.meta.url);
42
- const sqlJsPath = require.resolve('sql.js');
43
-
44
- const SQL = await initSqlJs({
45
- locateFile: (file: string) => path.join(path.dirname(sqlJsPath), file),
46
- });
47
-
40
+ function loadDatabase(): BetterDatabase | null {
48
41
  const dbPath = path.join(projectDir, '.claude/memory/memory.db');
49
42
 
50
43
  if (fs.existsSync(dbPath)) {
51
- const buffer = fs.readFileSync(dbPath);
52
- return new SQL.Database(new Uint8Array(buffer));
44
+ return new Database(dbPath);
53
45
  } else {
54
46
  console.log(`\nšŸ“­ No database found at: ${dbPath}\n`);
55
47
  console.log('Run Claude Code with memory MCP server to create entries.');
56
- process.exit(0);
48
+ return null;
57
49
  }
58
50
  }
59
51
 
60
- async function main() {
52
+ function main() {
61
53
  const options = parseArgs();
62
54
  const limit = parseInt(options.limit as string, 10) || 20;
63
55
  const namespace = options.namespace as string | undefined;
64
56
  const asJson = !!options.json;
65
57
 
66
58
  try {
67
- const db = await loadDatabase();
59
+ const db = loadDatabase();
60
+ if (!db) {
61
+ process.exit(0);
62
+ }
68
63
 
69
64
  if (options.stats) {
70
65
  // Get stats
71
- const totalResult = db.exec('SELECT COUNT(*) as count FROM memory_entries');
72
- const total = totalResult[0]?.values[0]?.[0] || 0;
66
+ const totalRow = db.prepare('SELECT COUNT(*) as count FROM memory_entries').get() as { count: number };
67
+ const total = totalRow?.count || 0;
73
68
 
74
- const nsResult = db.exec('SELECT namespace, COUNT(*) FROM memory_entries GROUP BY namespace');
69
+ const nsRows = db.prepare('SELECT namespace, COUNT(*) as count FROM memory_entries GROUP BY namespace').all() as { namespace: string; count: number }[];
75
70
  const byNamespace: Record<string, number> = {};
76
- if (nsResult[0]) {
77
- for (const row of nsResult[0].values) {
78
- byNamespace[row[0] as string] = row[1] as number;
79
- }
71
+ for (const row of nsRows) {
72
+ byNamespace[row.namespace] = row.count;
80
73
  }
81
74
 
82
- const typeResult = db.exec('SELECT type, COUNT(*) FROM memory_entries GROUP BY type');
75
+ const typeRows = db.prepare('SELECT type, COUNT(*) as count FROM memory_entries GROUP BY type').all() as { type: string; count: number }[];
83
76
  const byType: Record<string, number> = {};
84
- if (typeResult[0]) {
85
- for (const row of typeResult[0].values) {
86
- byType[row[0] as string] = row[1] as number;
87
- }
77
+ for (const row of typeRows) {
78
+ byType[row.type] = row.count;
88
79
  }
89
80
 
90
81
  if (asJson) {
@@ -107,32 +98,23 @@ async function main() {
107
98
  }
108
99
 
109
100
  if (options.export) {
110
- const result = db.exec('SELECT * FROM memory_entries');
111
- if (!result[0]) {
101
+ const rows = db.prepare('SELECT * FROM memory_entries').all() as Record<string, unknown>[];
102
+ if (rows.length === 0) {
112
103
  console.log('No entries to export.');
113
104
  db.close();
114
105
  return;
115
106
  }
116
107
 
117
- const columns = result[0].columns;
118
- const entries = result[0].values.map(row => {
119
- const entry: Record<string, unknown> = {};
120
- columns.forEach((col, i) => {
121
- entry[col] = row[i];
122
- });
123
- return entry;
124
- });
125
-
126
108
  const filename = `memory-export-${Date.now()}.json`;
127
- fs.writeFileSync(filename, JSON.stringify({ entries, exportedAt: new Date().toISOString() }, null, 2));
128
- console.log(`āœ“ Exported ${entries.length} entries to ${filename}`);
109
+ fs.writeFileSync(filename, JSON.stringify({ entries: rows, exportedAt: new Date().toISOString() }, null, 2));
110
+ console.log(`āœ“ Exported ${rows.length} entries to ${filename}`);
129
111
  db.close();
130
112
  return;
131
113
  }
132
114
 
133
115
  // Default: list entries
134
116
  let query = 'SELECT id, key, content, type, namespace, tags, created_at FROM memory_entries';
135
- const params: string[] = [];
117
+ const params: (string | number)[] = [];
136
118
 
137
119
  if (namespace) {
138
120
  query += ' WHERE namespace = ?';
@@ -140,12 +122,9 @@ async function main() {
140
122
  }
141
123
 
142
124
  query += ' ORDER BY created_at DESC LIMIT ?';
143
- params.push(String(limit));
125
+ params.push(limit);
144
126
 
145
- const stmt = db.prepare(query);
146
- stmt.bind(params);
147
-
148
- const entries: Array<{
127
+ const entries = db.prepare(query).all(...params) as {
149
128
  id: string;
150
129
  key: string;
151
130
  content: string;
@@ -153,21 +132,7 @@ async function main() {
153
132
  namespace: string;
154
133
  tags: string;
155
134
  created_at: number;
156
- }> = [];
157
-
158
- while (stmt.step()) {
159
- const row = stmt.getAsObject();
160
- entries.push({
161
- id: row.id as string,
162
- key: row.key as string,
163
- content: row.content as string,
164
- type: row.type as string,
165
- namespace: row.namespace as string,
166
- tags: row.tags as string,
167
- created_at: row.created_at as number,
168
- });
169
- }
170
- stmt.free();
135
+ }[];
171
136
 
172
137
  if (entries.length === 0) {
173
138
  console.log('\nšŸ“­ No memories found in database.\n');
@@ -201,8 +166,8 @@ async function main() {
201
166
  }
202
167
 
203
168
  // Get total count
204
- const countResult = db.exec('SELECT COUNT(*) FROM memory_entries');
205
- const totalCount = countResult[0]?.values[0]?.[0] || entries.length;
169
+ const countRow = db.prepare('SELECT COUNT(*) as count FROM memory_entries').get() as { count: number };
170
+ const totalCount = countRow?.count || entries.length;
206
171
 
207
172
  console.log(`\nShowing ${entries.length} of ${totalCount} total entries`);
208
173
  console.log('Use --limit=N to see more, --namespace=X to filter\n');