@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
@@ -0,0 +1,1769 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * AgentKits Memory Web Viewer
4
+ *
5
+ * Web-based viewer for memory database with hybrid search support.
6
+ * Uses ProjectMemoryService for vector + text search.
7
+ *
8
+ * Usage:
9
+ * npx agentkits-memory-web [--port=1905]
10
+ *
11
+ * @module @aitytech/agentkits-memory/cli/web-viewer
12
+ */
13
+ import * as http from 'node:http';
14
+ import * as path from 'node:path';
15
+ import Database from 'better-sqlite3';
16
+ import { HybridSearchEngine, LocalEmbeddingsService } from '../index.js';
17
+ const args = process.argv.slice(2);
18
+ const projectDir = process.env.CLAUDE_PROJECT_DIR || process.cwd();
19
+ // Embeddings service singleton
20
+ let _embeddingsService = null;
21
+ function parseArgs() {
22
+ const parsed = {};
23
+ for (const arg of args) {
24
+ if (arg.startsWith('--')) {
25
+ const [key, value] = arg.slice(2).split('=');
26
+ parsed[key] = value ?? true;
27
+ }
28
+ }
29
+ return parsed;
30
+ }
31
+ const options = parseArgs();
32
+ const PORT = parseInt(options.port, 10) || 1905;
33
+ const dbDir = path.join(projectDir, '.claude/memory');
34
+ const dbPath = path.join(dbDir, 'memory.db');
35
+ // Singleton database and search engine
36
+ let _searchEngine = null;
37
+ let _db = null;
38
+ /**
39
+ * Get direct database access
40
+ */
41
+ function getDatabase() {
42
+ if (_db)
43
+ return _db;
44
+ _db = new Database(dbPath);
45
+ _db.pragma('journal_mode = WAL');
46
+ return _db;
47
+ }
48
+ /**
49
+ * Get or initialize embeddings service
50
+ */
51
+ async function getEmbeddingsService() {
52
+ if (_embeddingsService)
53
+ return _embeddingsService;
54
+ _embeddingsService = new LocalEmbeddingsService({
55
+ cacheDir: path.join(dbDir, 'embeddings-cache'),
56
+ });
57
+ await _embeddingsService.initialize();
58
+ return _embeddingsService;
59
+ }
60
+ /**
61
+ * Get or initialize the HybridSearchEngine with embeddings
62
+ */
63
+ async function getSearchEngine() {
64
+ if (_searchEngine)
65
+ return _searchEngine;
66
+ const db = getDatabase();
67
+ const embeddings = await getEmbeddingsService();
68
+ // Create embedding generator function
69
+ const embeddingGenerator = async (text) => {
70
+ const result = await embeddings.embed(text);
71
+ return result.embedding;
72
+ };
73
+ _searchEngine = new HybridSearchEngine(db, {}, embeddingGenerator);
74
+ await _searchEngine.initialize();
75
+ return _searchEngine;
76
+ }
77
+ /**
78
+ * Get database statistics using direct SQL (faster for stats queries)
79
+ */
80
+ function getStats(db) {
81
+ const totalRow = db.prepare('SELECT COUNT(*) as count FROM memory_entries').get();
82
+ const total = totalRow?.count || 0;
83
+ const nsRows = db.prepare('SELECT namespace, COUNT(*) as count FROM memory_entries GROUP BY namespace').all();
84
+ const byNamespace = {};
85
+ for (const row of nsRows) {
86
+ byNamespace[row.namespace] = row.count;
87
+ }
88
+ const typeRows = db.prepare('SELECT type, COUNT(*) as count FROM memory_entries GROUP BY type').all();
89
+ const byType = {};
90
+ for (const row of typeRows) {
91
+ byType[row.type] = row.count;
92
+ }
93
+ // Calculate token economics
94
+ const contentRow = db.prepare('SELECT SUM(LENGTH(content)) as total_chars, COUNT(*) as count FROM memory_entries').get();
95
+ const totalCharacters = contentRow?.total_chars || 0;
96
+ const entryCount = contentRow?.count || 0;
97
+ // Estimate tokens (~4 chars per token)
98
+ const totalTokens = Math.ceil(totalCharacters / 4);
99
+ const avgTokensPerEntry = entryCount > 0 ? Math.ceil(totalTokens / entryCount) : 0;
100
+ // Estimated savings: if you had to rediscover this info each time
101
+ // Assume 5x overhead for discovery vs recall
102
+ const estimatedSavings = totalTokens * 5;
103
+ return {
104
+ total,
105
+ byNamespace,
106
+ byType,
107
+ tokenEconomics: {
108
+ totalTokens,
109
+ avgTokensPerEntry,
110
+ totalCharacters,
111
+ estimatedSavings,
112
+ },
113
+ };
114
+ }
115
+ /**
116
+ * Get entries with optional search (standard listing)
117
+ */
118
+ function getEntries(db, namespace, limit = 50, offset = 0, search) {
119
+ // Standard query without search
120
+ if (!search || !search.trim()) {
121
+ let query = 'SELECT id, key, content, type, namespace, tags, embedding, created_at, updated_at FROM memory_entries';
122
+ const conditions = [];
123
+ const params = [];
124
+ if (namespace) {
125
+ conditions.push('namespace = ?');
126
+ params.push(namespace);
127
+ }
128
+ if (conditions.length > 0) {
129
+ query += ' WHERE ' + conditions.join(' AND ');
130
+ }
131
+ query += ' ORDER BY created_at DESC LIMIT ? OFFSET ?';
132
+ params.push(limit, offset);
133
+ const rows = db.prepare(query).all(...params);
134
+ return rows.map((row) => ({
135
+ id: row.id,
136
+ key: row.key,
137
+ content: row.content,
138
+ type: row.type,
139
+ namespace: row.namespace,
140
+ tags: JSON.parse(row.tags || '[]'),
141
+ created_at: row.created_at,
142
+ updated_at: row.updated_at,
143
+ hasEmbedding: !!(row.embedding && row.embedding.length > 0),
144
+ }));
145
+ }
146
+ // Use FTS5 search for better CJK support
147
+ const sanitizedSearch = search.trim().replace(/"/g, '""');
148
+ let ftsQuery = `
149
+ SELECT m.id, m.key, m.content, m.type, m.namespace, m.tags, m.embedding, m.created_at, m.updated_at
150
+ FROM memory_entries m
151
+ INNER JOIN memory_fts f ON m.id = f.id
152
+ WHERE memory_fts MATCH '"${sanitizedSearch}"'
153
+ `;
154
+ if (namespace) {
155
+ ftsQuery += ` AND m.namespace = ?`;
156
+ }
157
+ ftsQuery += ` ORDER BY m.created_at DESC LIMIT ? OFFSET ?`;
158
+ try {
159
+ const params = namespace ? [namespace, limit, offset] : [limit, offset];
160
+ const rows = db.prepare(ftsQuery).all(...params);
161
+ return rows.map((row) => ({
162
+ id: row.id,
163
+ key: row.key,
164
+ content: row.content,
165
+ type: row.type,
166
+ namespace: row.namespace,
167
+ tags: JSON.parse(row.tags || '[]'),
168
+ created_at: row.created_at,
169
+ updated_at: row.updated_at,
170
+ hasEmbedding: !!(row.embedding && row.embedding.length > 0),
171
+ }));
172
+ }
173
+ catch {
174
+ // Fallback to LIKE if FTS fails
175
+ console.warn('[WebViewer] FTS search failed, falling back to LIKE');
176
+ let query = 'SELECT id, key, content, type, namespace, tags, embedding, created_at, updated_at FROM memory_entries';
177
+ const conditions = [];
178
+ const params = [];
179
+ if (namespace) {
180
+ conditions.push('namespace = ?');
181
+ params.push(namespace);
182
+ }
183
+ conditions.push('(content LIKE ? OR key LIKE ? OR tags LIKE ?)');
184
+ const searchPattern = `%${search}%`;
185
+ params.push(searchPattern, searchPattern, searchPattern);
186
+ query += ' WHERE ' + conditions.join(' AND ');
187
+ query += ' ORDER BY created_at DESC LIMIT ? OFFSET ?';
188
+ params.push(limit, offset);
189
+ const rows = db.prepare(query).all(...params);
190
+ return rows.map((row) => ({
191
+ id: row.id,
192
+ key: row.key,
193
+ content: row.content,
194
+ type: row.type,
195
+ namespace: row.namespace,
196
+ tags: JSON.parse(row.tags || '[]'),
197
+ created_at: row.created_at,
198
+ updated_at: row.updated_at,
199
+ hasEmbedding: !!(row.embedding && row.embedding.length > 0),
200
+ }));
201
+ }
202
+ }
203
+ /**
204
+ * Search entries using HybridSearchEngine
205
+ * Supports hybrid (text + vector), text-only, or vector-only search
206
+ */
207
+ async function searchEntries(searchEngine, query, options = {}) {
208
+ const { type = 'hybrid', namespace, limit = 20 } = options;
209
+ // Use searchCompact for efficient search with scores
210
+ const results = await searchEngine.searchCompact(query, {
211
+ limit,
212
+ namespace,
213
+ includeKeyword: type === 'hybrid' || type === 'text',
214
+ includeSemantic: type === 'hybrid' || type === 'vector',
215
+ });
216
+ // Fetch full entries for the results
217
+ const db = getDatabase();
218
+ const entries = [];
219
+ for (const result of results) {
220
+ const row = db.prepare(`
221
+ SELECT id, key, content, type, namespace, tags, embedding, created_at, updated_at
222
+ FROM memory_entries WHERE id = ?
223
+ `).get(result.id);
224
+ if (row) {
225
+ entries.push({
226
+ id: row.id,
227
+ key: row.key,
228
+ content: row.content,
229
+ type: row.type,
230
+ namespace: row.namespace,
231
+ tags: JSON.parse(row.tags || '[]'),
232
+ created_at: row.created_at,
233
+ updated_at: row.updated_at,
234
+ score: result.score,
235
+ hasEmbedding: !!(row.embedding && row.embedding.length > 0),
236
+ });
237
+ }
238
+ }
239
+ return entries;
240
+ }
241
+ function getHTML() {
242
+ return `<!DOCTYPE html>
243
+ <html lang="en">
244
+ <head>
245
+ <meta charset="UTF-8">
246
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
247
+ <title>AgentKits Memory Viewer</title>
248
+ <link rel="icon" type="image/svg+xml" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cdefs%3E%3ClinearGradient id='g' x1='0%25' y1='0%25' x2='100%25' y2='100%25'%3E%3Cstop offset='0%25' stop-color='%233B82F6'/%3E%3Cstop offset='100%25' stop-color='%238B5CF6'/%3E%3C/linearGradient%3E%3C/defs%3E%3Ccircle cx='12' cy='12' r='10' fill='url(%23g)'/%3E%3Cpath d='M10 14.17l-3.17-3.17-1.42 1.41L10 17l8-8-1.41-1.41z' fill='white'/%3E%3C/svg%3E">
249
+ <style>
250
+ :root {
251
+ --bg-primary: #0F172A;
252
+ --bg-secondary: #1E293B;
253
+ --bg-card: #334155;
254
+ --text-primary: #F8FAFC;
255
+ --text-secondary: #94A3B8;
256
+ --text-muted: #64748B;
257
+ --border: #475569;
258
+ --accent: #3B82F6;
259
+ --accent-hover: #2563EB;
260
+ --success: #22C55E;
261
+ --warning: #F59E0B;
262
+ --error: #EF4444;
263
+ }
264
+
265
+ * { margin: 0; padding: 0; box-sizing: border-box; }
266
+
267
+ body {
268
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
269
+ background: var(--bg-primary);
270
+ color: var(--text-primary);
271
+ min-height: 100vh;
272
+ line-height: 1.5;
273
+ }
274
+
275
+ .container { max-width: 1400px; margin: 0 auto; padding: 24px; }
276
+
277
+ header {
278
+ display: flex;
279
+ justify-content: space-between;
280
+ align-items: center;
281
+ margin-bottom: 32px;
282
+ padding-bottom: 24px;
283
+ border-bottom: 1px solid var(--border);
284
+ flex-wrap: wrap;
285
+ gap: 16px;
286
+ }
287
+
288
+ .logo { display: flex; align-items: center; gap: 12px; }
289
+
290
+ .logo-icon {
291
+ width: 40px; height: 40px;
292
+ background: linear-gradient(135deg, var(--accent), #8B5CF6);
293
+ border-radius: 10px;
294
+ display: flex; align-items: center; justify-content: center;
295
+ }
296
+
297
+ .logo-icon svg { width: 24px; height: 24px; fill: white; }
298
+ h1 { font-size: 24px; font-weight: 600; }
299
+ .subtitle { font-size: 14px; color: var(--text-secondary); }
300
+
301
+ .header-actions { display: flex; gap: 12px; }
302
+
303
+ .stats-grid {
304
+ display: grid;
305
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
306
+ gap: 16px;
307
+ margin-bottom: 32px;
308
+ }
309
+
310
+ .stat-card {
311
+ background: var(--bg-secondary);
312
+ border-radius: 12px;
313
+ padding: 20px;
314
+ border: 1px solid var(--border);
315
+ }
316
+
317
+ .stat-label {
318
+ font-size: 13px;
319
+ color: var(--text-secondary);
320
+ text-transform: uppercase;
321
+ letter-spacing: 0.5px;
322
+ margin-bottom: 8px;
323
+ }
324
+
325
+ .stat-value { font-size: 32px; font-weight: 700; color: var(--text-primary); }
326
+
327
+ .controls { display: flex; gap: 16px; margin-bottom: 24px; flex-wrap: wrap; }
328
+
329
+ .search-box { flex: 1; min-width: 250px; position: relative; }
330
+
331
+ .search-box input {
332
+ width: 100%;
333
+ padding: 12px 16px 12px 44px;
334
+ background: var(--bg-secondary);
335
+ border: 1px solid var(--border);
336
+ border-radius: 8px;
337
+ color: var(--text-primary);
338
+ font-size: 14px;
339
+ transition: border-color 0.2s, box-shadow 0.2s;
340
+ }
341
+
342
+ .search-box input:focus {
343
+ outline: none;
344
+ border-color: var(--accent);
345
+ box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.2);
346
+ }
347
+
348
+ .search-box input::placeholder { color: var(--text-muted); }
349
+
350
+ .search-box svg {
351
+ position: absolute;
352
+ left: 14px;
353
+ top: 50%;
354
+ transform: translateY(-50%);
355
+ width: 18px; height: 18px;
356
+ fill: var(--text-muted);
357
+ }
358
+
359
+ .search-type-select {
360
+ padding: 12px 16px;
361
+ background: var(--bg-secondary);
362
+ border: 1px solid var(--border);
363
+ border-radius: 8px;
364
+ color: var(--text-primary);
365
+ font-size: 14px;
366
+ cursor: pointer;
367
+ min-width: 180px;
368
+ transition: border-color 0.2s;
369
+ }
370
+
371
+ .search-type-select:focus {
372
+ outline: none;
373
+ border-color: var(--accent);
374
+ }
375
+
376
+ .search-type-select:hover { border-color: var(--accent); }
377
+
378
+ .score-badge {
379
+ font-size: 11px;
380
+ padding: 3px 8px;
381
+ background: linear-gradient(135deg, rgba(59, 130, 246, 0.2), rgba(139, 92, 246, 0.2));
382
+ color: var(--accent);
383
+ border-radius: 4px;
384
+ font-weight: 600;
385
+ white-space: nowrap;
386
+ }
387
+
388
+ .vector-badge {
389
+ display: inline-flex;
390
+ align-items: center;
391
+ font-size: 10px;
392
+ font-weight: 600;
393
+ padding: 2px 6px;
394
+ border-radius: 4px;
395
+ text-transform: uppercase;
396
+ letter-spacing: 0.5px;
397
+ }
398
+
399
+ .vector-badge.has-vector {
400
+ background: rgba(34, 197, 94, 0.15);
401
+ color: #22C55E;
402
+ }
403
+
404
+ .vector-badge.no-vector {
405
+ background: rgba(100, 116, 139, 0.1);
406
+ color: var(--text-muted);
407
+ }
408
+
409
+ .embedding-stats {
410
+ display: flex;
411
+ gap: 16px;
412
+ margin-bottom: 20px;
413
+ padding: 16px;
414
+ background: var(--bg-card);
415
+ border-radius: 8px;
416
+ }
417
+
418
+ .embedding-stat {
419
+ text-align: center;
420
+ flex: 1;
421
+ }
422
+
423
+ .embedding-stat-value {
424
+ font-size: 24px;
425
+ font-weight: 700;
426
+ color: var(--text-primary);
427
+ }
428
+
429
+ .embedding-stat-label {
430
+ font-size: 12px;
431
+ color: var(--text-muted);
432
+ text-transform: uppercase;
433
+ }
434
+
435
+ .embedding-stat-value.success { color: var(--success); }
436
+ .embedding-stat-value.warning { color: var(--warning); }
437
+
438
+ .progress-bar {
439
+ height: 8px;
440
+ background: var(--bg-card);
441
+ border-radius: 4px;
442
+ overflow: hidden;
443
+ margin: 16px 0;
444
+ }
445
+
446
+ .progress-bar-fill {
447
+ height: 100%;
448
+ background: linear-gradient(90deg, var(--accent), #8B5CF6);
449
+ border-radius: 4px;
450
+ transition: width 0.3s ease;
451
+ }
452
+
453
+ .progress-text {
454
+ text-align: center;
455
+ font-size: 14px;
456
+ color: var(--text-secondary);
457
+ margin-top: 8px;
458
+ }
459
+
460
+ .btn-icon {
461
+ padding: 10px;
462
+ min-width: auto;
463
+ }
464
+
465
+ .embedding-section {
466
+ margin-top: 20px;
467
+ padding: 16px;
468
+ background: var(--bg-card);
469
+ border-radius: 8px;
470
+ border: 1px solid var(--border);
471
+ }
472
+
473
+ .embedding-header {
474
+ display: flex;
475
+ align-items: center;
476
+ gap: 12px;
477
+ margin-bottom: 12px;
478
+ }
479
+
480
+ .embedding-status {
481
+ display: inline-flex;
482
+ align-items: center;
483
+ gap: 6px;
484
+ font-size: 13px;
485
+ padding: 4px 10px;
486
+ border-radius: 6px;
487
+ }
488
+
489
+ .embedding-status.has-embedding {
490
+ background: rgba(34, 197, 94, 0.15);
491
+ color: #22C55E;
492
+ }
493
+
494
+ .embedding-status.no-embedding {
495
+ background: rgba(239, 68, 68, 0.15);
496
+ color: #EF4444;
497
+ }
498
+
499
+ .embedding-status svg {
500
+ width: 14px;
501
+ height: 14px;
502
+ fill: currentColor;
503
+ }
504
+
505
+ .embedding-dims {
506
+ font-size: 12px;
507
+ color: var(--text-muted);
508
+ }
509
+
510
+ .embedding-viz {
511
+ display: flex;
512
+ align-items: flex-end;
513
+ gap: 2px;
514
+ height: 40px;
515
+ margin-top: 12px;
516
+ padding: 8px;
517
+ background: var(--bg-primary);
518
+ border-radius: 6px;
519
+ overflow: hidden;
520
+ }
521
+
522
+ .embedding-bar {
523
+ flex: 1;
524
+ min-width: 4px;
525
+ border-radius: 2px 2px 0 0;
526
+ transition: height 0.3s ease;
527
+ }
528
+
529
+ .embedding-bar.positive { background: linear-gradient(to top, #3B82F6, #60A5FA); }
530
+ .embedding-bar.negative { background: linear-gradient(to top, #8B5CF6, #A78BFA); }
531
+
532
+ .embedding-legend {
533
+ display: flex;
534
+ justify-content: space-between;
535
+ margin-top: 8px;
536
+ font-size: 11px;
537
+ color: var(--text-muted);
538
+ }
539
+
540
+ .embedding-legend-item {
541
+ display: flex;
542
+ align-items: center;
543
+ gap: 4px;
544
+ }
545
+
546
+ .legend-dot {
547
+ width: 8px;
548
+ height: 8px;
549
+ border-radius: 50%;
550
+ }
551
+
552
+ .legend-dot.positive { background: #3B82F6; }
553
+ .legend-dot.negative { background: #8B5CF6; }
554
+
555
+ .entries-list { display: flex; flex-direction: column; gap: 12px; }
556
+
557
+ .entry-card {
558
+ background: var(--bg-secondary);
559
+ border: 1px solid var(--border);
560
+ border-radius: 12px;
561
+ padding: 20px;
562
+ cursor: pointer;
563
+ transition: border-color 0.2s, transform 0.2s;
564
+ position: relative;
565
+ }
566
+
567
+ .entry-card:hover {
568
+ border-color: var(--accent);
569
+ transform: translateY(-2px);
570
+ }
571
+
572
+ .entry-header {
573
+ display: flex;
574
+ justify-content: space-between;
575
+ align-items: flex-start;
576
+ margin-bottom: 12px;
577
+ gap: 12px;
578
+ }
579
+
580
+ .entry-key { font-weight: 600; font-size: 15px; color: var(--text-primary); word-break: break-word; flex: 1; }
581
+
582
+ .entry-badges {
583
+ display: flex;
584
+ align-items: center;
585
+ gap: 8px;
586
+ flex-shrink: 0;
587
+ }
588
+
589
+ .entry-namespace {
590
+ font-size: 12px;
591
+ padding: 4px 10px;
592
+ background: var(--bg-card);
593
+ border-radius: 6px;
594
+ color: var(--text-secondary);
595
+ white-space: nowrap;
596
+ }
597
+
598
+ .entry-content {
599
+ font-size: 14px;
600
+ color: var(--text-secondary);
601
+ line-height: 1.6;
602
+ margin-bottom: 12px;
603
+ white-space: pre-wrap;
604
+ word-break: break-word;
605
+ }
606
+
607
+ .entry-content.truncated {
608
+ display: -webkit-box;
609
+ -webkit-line-clamp: 3;
610
+ -webkit-box-orient: vertical;
611
+ overflow: hidden;
612
+ }
613
+
614
+ .entry-footer {
615
+ display: flex;
616
+ justify-content: space-between;
617
+ align-items: center;
618
+ flex-wrap: wrap;
619
+ gap: 8px;
620
+ }
621
+
622
+ .entry-tags { display: flex; gap: 6px; flex-wrap: wrap; }
623
+
624
+ .tag {
625
+ font-size: 11px;
626
+ padding: 3px 8px;
627
+ background: rgba(59, 130, 246, 0.2);
628
+ color: var(--accent);
629
+ border-radius: 4px;
630
+ }
631
+
632
+ .entry-date { font-size: 12px; color: var(--text-muted); }
633
+
634
+ .empty-state { text-align: center; padding: 60px 20px; color: var(--text-secondary); }
635
+ .empty-state svg { width: 64px; height: 64px; fill: var(--text-muted); margin-bottom: 16px; }
636
+ .empty-state h3 { font-size: 18px; margin-bottom: 8px; color: var(--text-primary); }
637
+
638
+ .loading { display: flex; justify-content: center; padding: 40px; }
639
+
640
+ .spinner {
641
+ width: 32px; height: 32px;
642
+ border: 3px solid var(--border);
643
+ border-top-color: var(--accent);
644
+ border-radius: 50%;
645
+ animation: spin 0.8s linear infinite;
646
+ }
647
+
648
+ @keyframes spin { to { transform: rotate(360deg); } }
649
+
650
+ .namespace-pills { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 24px; }
651
+
652
+ .namespace-pill {
653
+ padding: 8px 16px;
654
+ background: var(--bg-secondary);
655
+ border: 1px solid var(--border);
656
+ border-radius: 20px;
657
+ font-size: 13px;
658
+ color: var(--text-secondary);
659
+ cursor: pointer;
660
+ transition: all 0.2s;
661
+ }
662
+
663
+ .namespace-pill:hover { border-color: var(--accent); color: var(--text-primary); }
664
+ .namespace-pill.active { background: var(--accent); border-color: var(--accent); color: white; }
665
+ .namespace-pill .count { margin-left: 6px; font-size: 11px; opacity: 0.7; }
666
+
667
+ .modal-overlay {
668
+ display: none;
669
+ position: fixed;
670
+ inset: 0;
671
+ background: rgba(0, 0, 0, 0.7);
672
+ z-index: 100;
673
+ align-items: center;
674
+ justify-content: center;
675
+ padding: 24px;
676
+ }
677
+
678
+ .modal-overlay.active { display: flex; }
679
+
680
+ .modal {
681
+ background: var(--bg-secondary);
682
+ border-radius: 16px;
683
+ max-width: 700px;
684
+ width: 100%;
685
+ max-height: 90vh;
686
+ overflow: hidden;
687
+ display: flex;
688
+ flex-direction: column;
689
+ }
690
+
691
+ .modal-header {
692
+ display: flex;
693
+ justify-content: space-between;
694
+ align-items: center;
695
+ padding: 20px 24px;
696
+ border-bottom: 1px solid var(--border);
697
+ }
698
+
699
+ .modal-title { font-size: 18px; font-weight: 600; }
700
+
701
+ .modal-close {
702
+ background: none;
703
+ border: none;
704
+ color: var(--text-secondary);
705
+ cursor: pointer;
706
+ padding: 8px;
707
+ border-radius: 6px;
708
+ transition: background 0.2s;
709
+ }
710
+
711
+ .modal-close:hover { background: var(--bg-card); }
712
+ .modal-close svg { width: 20px; height: 20px; fill: currentColor; }
713
+
714
+ .modal-body { padding: 24px; overflow-y: auto; }
715
+
716
+ .detail-row { margin-bottom: 20px; }
717
+
718
+ .detail-label {
719
+ font-size: 12px;
720
+ color: var(--text-muted);
721
+ text-transform: uppercase;
722
+ letter-spacing: 0.5px;
723
+ margin-bottom: 6px;
724
+ }
725
+
726
+ .detail-value {
727
+ font-size: 14px;
728
+ color: var(--text-primary);
729
+ white-space: pre-wrap;
730
+ word-break: break-word;
731
+ line-height: 1.6;
732
+ }
733
+
734
+ .detail-value.content {
735
+ background: var(--bg-card);
736
+ padding: 16px;
737
+ border-radius: 8px;
738
+ max-height: 200px;
739
+ overflow-y: auto;
740
+ }
741
+
742
+ .pagination { display: flex; justify-content: center; gap: 8px; margin-top: 24px; }
743
+
744
+ .btn {
745
+ padding: 10px 18px;
746
+ background: var(--bg-secondary);
747
+ border: 1px solid var(--border);
748
+ border-radius: 8px;
749
+ color: var(--text-primary);
750
+ cursor: pointer;
751
+ font-size: 14px;
752
+ transition: all 0.2s;
753
+ display: inline-flex;
754
+ align-items: center;
755
+ gap: 8px;
756
+ }
757
+
758
+ .btn:hover:not(:disabled) { border-color: var(--accent); }
759
+ .btn:disabled { opacity: 0.5; cursor: not-allowed; }
760
+
761
+ .btn-primary { background: var(--accent); border-color: var(--accent); color: white; }
762
+ .btn-primary:hover { background: var(--accent-hover); border-color: var(--accent-hover); }
763
+
764
+ .btn-danger { background: var(--error); border-color: var(--error); color: white; }
765
+ .btn-danger:hover { background: #DC2626; border-color: #DC2626; }
766
+
767
+ .btn-success { background: var(--success); border-color: var(--success); color: white; }
768
+ .btn-success:hover { background: #16A34A; border-color: #16A34A; }
769
+
770
+ .btn svg { width: 16px; height: 16px; fill: currentColor; }
771
+
772
+ .form-group { margin-bottom: 20px; }
773
+
774
+ .form-group label {
775
+ display: block;
776
+ font-size: 13px;
777
+ color: var(--text-secondary);
778
+ margin-bottom: 8px;
779
+ text-transform: uppercase;
780
+ letter-spacing: 0.5px;
781
+ }
782
+
783
+ .form-group input,
784
+ .form-group textarea,
785
+ .form-group select {
786
+ width: 100%;
787
+ padding: 12px 16px;
788
+ background: var(--bg-card);
789
+ border: 1px solid var(--border);
790
+ border-radius: 8px;
791
+ color: var(--text-primary);
792
+ font-size: 14px;
793
+ font-family: inherit;
794
+ transition: border-color 0.2s;
795
+ }
796
+
797
+ .form-group input:focus,
798
+ .form-group textarea:focus,
799
+ .form-group select:focus {
800
+ outline: none;
801
+ border-color: var(--accent);
802
+ }
803
+
804
+ .form-group textarea { min-height: 120px; resize: vertical; }
805
+
806
+ .modal-footer {
807
+ display: flex;
808
+ justify-content: flex-end;
809
+ gap: 12px;
810
+ padding: 16px 24px;
811
+ border-top: 1px solid var(--border);
812
+ }
813
+
814
+ .detail-actions {
815
+ display: flex;
816
+ gap: 12px;
817
+ margin-top: 24px;
818
+ padding-top: 20px;
819
+ border-top: 1px solid var(--border);
820
+ }
821
+
822
+ .toast {
823
+ position: fixed;
824
+ bottom: 24px;
825
+ right: 24px;
826
+ padding: 16px 24px;
827
+ background: var(--bg-card);
828
+ border: 1px solid var(--border);
829
+ border-radius: 8px;
830
+ color: var(--text-primary);
831
+ font-size: 14px;
832
+ z-index: 200;
833
+ animation: slideIn 0.3s ease;
834
+ }
835
+
836
+ .toast.success { border-color: var(--success); background: rgba(34, 197, 94, 0.1); }
837
+ .toast.error { border-color: var(--error); background: rgba(239, 68, 68, 0.1); }
838
+
839
+ @keyframes slideIn {
840
+ from { transform: translateX(100%); opacity: 0; }
841
+ to { transform: translateX(0); opacity: 1; }
842
+ }
843
+
844
+ @media (max-width: 768px) {
845
+ .container { padding: 16px; }
846
+ header { flex-direction: column; align-items: flex-start; }
847
+ .stats-grid { grid-template-columns: repeat(2, 1fr); }
848
+ .controls { flex-direction: column; }
849
+ .search-box { min-width: 100%; }
850
+ }
851
+ </style>
852
+ </head>
853
+ <body>
854
+ <div class="container">
855
+ <header>
856
+ <div class="logo">
857
+ <div class="logo-icon">
858
+ <svg viewBox="0 0 24 24"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/></svg>
859
+ </div>
860
+ <div>
861
+ <h1>Memory Viewer</h1>
862
+ <p class="subtitle">AgentKits Memory Database</p>
863
+ </div>
864
+ </div>
865
+ <div class="header-actions">
866
+ <button class="btn btn-primary" onclick="openAddModal()">
867
+ <svg viewBox="0 0 24 24"><path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/></svg>
868
+ Add Memory
869
+ </button>
870
+ <button class="btn" onclick="openEmbeddingModal()">
871
+ <svg viewBox="0 0 24 24"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/></svg>
872
+ Embeddings
873
+ </button>
874
+ <button class="btn" onclick="loadData()">
875
+ <svg viewBox="0 0 24 24"><path d="M17.65 6.35C16.2 4.9 14.21 4 12 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08c-.82 2.33-3.04 4-5.65 4-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z"/></svg>
876
+ Refresh
877
+ </button>
878
+ </div>
879
+ </header>
880
+
881
+ <div id="stats-container" class="stats-grid"></div>
882
+ <div id="namespace-pills" class="namespace-pills"></div>
883
+
884
+ <div class="controls">
885
+ <div class="search-box">
886
+ <svg viewBox="0 0 24 24"><path d="M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z"/></svg>
887
+ <input type="text" id="search-input" placeholder="Search memories..." oninput="debounceSearch()">
888
+ </div>
889
+ <select id="search-type" class="search-type-select" onchange="debounceSearch()">
890
+ <option value="hybrid">Hybrid (Text + Semantic)</option>
891
+ <option value="text">Text Only (FTS5)</option>
892
+ <option value="vector">Semantic Only (Vector)</option>
893
+ </select>
894
+ </div>
895
+
896
+ <div id="entries-container" class="entries-list">
897
+ <div class="loading"><div class="spinner"></div></div>
898
+ </div>
899
+
900
+ <div id="pagination" class="pagination"></div>
901
+ </div>
902
+
903
+ <!-- Detail Modal -->
904
+ <div id="detail-modal" class="modal-overlay" onclick="closeDetailModal(event)">
905
+ <div class="modal" onclick="event.stopPropagation()">
906
+ <div class="modal-header">
907
+ <span class="modal-title">Memory Details</span>
908
+ <button class="modal-close" onclick="closeDetailModal()">
909
+ <svg viewBox="0 0 24 24"><path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/></svg>
910
+ </button>
911
+ </div>
912
+ <div class="modal-body" id="detail-body"></div>
913
+ </div>
914
+ </div>
915
+
916
+ <!-- Add/Edit Modal -->
917
+ <div id="form-modal" class="modal-overlay" onclick="closeFormModal(event)">
918
+ <div class="modal" onclick="event.stopPropagation()">
919
+ <div class="modal-header">
920
+ <span class="modal-title" id="form-title">Add Memory</span>
921
+ <button class="modal-close" onclick="closeFormModal()">
922
+ <svg viewBox="0 0 24 24"><path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/></svg>
923
+ </button>
924
+ </div>
925
+ <div class="modal-body">
926
+ <input type="hidden" id="form-id">
927
+ <div class="form-group">
928
+ <label for="form-key">Key</label>
929
+ <input type="text" id="form-key" placeholder="e.g., auth-pattern, api-design">
930
+ </div>
931
+ <div class="form-group">
932
+ <label for="form-namespace">Namespace</label>
933
+ <select id="form-namespace">
934
+ <option value="patterns">patterns</option>
935
+ <option value="decisions">decisions</option>
936
+ <option value="errors">errors</option>
937
+ <option value="context">context</option>
938
+ <option value="active-context">active-context</option>
939
+ <option value="session-state">session-state</option>
940
+ <option value="progress">progress</option>
941
+ <option value="general">general</option>
942
+ </select>
943
+ </div>
944
+ <div class="form-group">
945
+ <label for="form-type">Type</label>
946
+ <select id="form-type">
947
+ <option value="semantic">semantic</option>
948
+ <option value="episodic">episodic</option>
949
+ <option value="procedural">procedural</option>
950
+ </select>
951
+ </div>
952
+ <div class="form-group">
953
+ <label for="form-content">Content</label>
954
+ <textarea id="form-content" placeholder="Enter the memory content..."></textarea>
955
+ </div>
956
+ <div class="form-group">
957
+ <label for="form-tags">Tags (comma-separated)</label>
958
+ <input type="text" id="form-tags" placeholder="e.g., auth, security, api">
959
+ </div>
960
+ </div>
961
+ <div class="modal-footer">
962
+ <button class="btn" onclick="closeFormModal()">Cancel</button>
963
+ <button class="btn btn-primary" onclick="saveEntry()">Save</button>
964
+ </div>
965
+ </div>
966
+ </div>
967
+
968
+ <!-- Delete Confirmation Modal -->
969
+ <div id="delete-modal" class="modal-overlay" onclick="closeDeleteModal(event)">
970
+ <div class="modal" style="max-width: 400px;" onclick="event.stopPropagation()">
971
+ <div class="modal-header">
972
+ <span class="modal-title">Delete Memory</span>
973
+ <button class="modal-close" onclick="closeDeleteModal()">
974
+ <svg viewBox="0 0 24 24"><path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/></svg>
975
+ </button>
976
+ </div>
977
+ <div class="modal-body">
978
+ <p>Are you sure you want to delete this memory? This action cannot be undone.</p>
979
+ <input type="hidden" id="delete-id">
980
+ </div>
981
+ <div class="modal-footer">
982
+ <button class="btn" onclick="closeDeleteModal()">Cancel</button>
983
+ <button class="btn btn-danger" onclick="confirmDelete()">Delete</button>
984
+ </div>
985
+ </div>
986
+ </div>
987
+
988
+ <!-- Embedding Management Modal -->
989
+ <div id="embedding-modal" class="modal-overlay" onclick="closeEmbeddingModal(event)">
990
+ <div class="modal" style="max-width: 500px;" onclick="event.stopPropagation()">
991
+ <div class="modal-header">
992
+ <span class="modal-title">Manage Embeddings</span>
993
+ <button class="modal-close" onclick="closeEmbeddingModal()">
994
+ <svg viewBox="0 0 24 24"><path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/></svg>
995
+ </button>
996
+ </div>
997
+ <div class="modal-body" id="embedding-modal-body">
998
+ <div class="embedding-stats" id="embedding-stats">
999
+ <div class="embedding-stat">
1000
+ <div class="embedding-stat-value" id="stat-total">-</div>
1001
+ <div class="embedding-stat-label">Total</div>
1002
+ </div>
1003
+ <div class="embedding-stat">
1004
+ <div class="embedding-stat-value success" id="stat-with">-</div>
1005
+ <div class="embedding-stat-label">With Embedding</div>
1006
+ </div>
1007
+ <div class="embedding-stat">
1008
+ <div class="embedding-stat-value warning" id="stat-without">-</div>
1009
+ <div class="embedding-stat-label">Without</div>
1010
+ </div>
1011
+ </div>
1012
+ <p style="font-size: 14px; color: var(--text-secondary); margin-bottom: 12px;">
1013
+ Vector embeddings enable semantic search - finding memories by meaning, not just keywords.
1014
+ </p>
1015
+ <p style="font-size: 12px; color: var(--text-muted); margin-bottom: 20px; padding: 10px; background: var(--bg-card); border-radius: 6px;">
1016
+ <strong>Model:</strong> multilingual-e5-small (100+ languages, optimized for retrieval)
1017
+ </p>
1018
+ <div id="embedding-progress" style="display: none;">
1019
+ <div class="progress-bar">
1020
+ <div class="progress-bar-fill" id="progress-fill" style="width: 0%"></div>
1021
+ </div>
1022
+ <div class="progress-text" id="progress-text">Processing...</div>
1023
+ </div>
1024
+ </div>
1025
+ <div class="modal-footer" id="embedding-modal-footer">
1026
+ <button class="btn" onclick="closeEmbeddingModal()">Close</button>
1027
+ <button class="btn btn-primary" id="btn-generate-missing" onclick="generateEmbeddings('missing')">
1028
+ <svg viewBox="0 0 24 24" style="width:16px;height:16px;fill:currentColor"><path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/></svg>
1029
+ Generate Missing
1030
+ </button>
1031
+ <button class="btn btn-success" id="btn-regenerate-all" onclick="generateEmbeddings('all')">
1032
+ <svg viewBox="0 0 24 24" style="width:16px;height:16px;fill:currentColor"><path d="M17.65 6.35C16.2 4.9 14.21 4 12 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08c-.82 2.33-3.04 4-5.65 4-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z"/></svg>
1033
+ Re-generate All
1034
+ </button>
1035
+ </div>
1036
+ </div>
1037
+ </div>
1038
+
1039
+ <script>
1040
+ let currentNamespace = '';
1041
+ let currentSearch = '';
1042
+ let currentPage = 0;
1043
+ const pageSize = 20;
1044
+ let stats = { total: 0, byNamespace: {}, byType: {} };
1045
+ let debounceTimer = null;
1046
+
1047
+ async function loadData() {
1048
+ try {
1049
+ const statsRes = await fetch('/api/stats');
1050
+ stats = await statsRes.json();
1051
+ renderStats();
1052
+ renderNamespacePills();
1053
+ await loadEntries();
1054
+ } catch (error) {
1055
+ console.error('Failed to load data:', error);
1056
+ showToast('Failed to load data', 'error');
1057
+ }
1058
+ }
1059
+
1060
+ function renderStats() {
1061
+ const container = document.getElementById('stats-container');
1062
+ container.innerHTML = \`
1063
+ <div class="stat-card">
1064
+ <div class="stat-label">Total Memories</div>
1065
+ <div class="stat-value">\${stats.total || 0}</div>
1066
+ </div>
1067
+ <div class="stat-card">
1068
+ <div class="stat-label">Namespaces</div>
1069
+ <div class="stat-value">\${Object.keys(stats.byNamespace || {}).length}</div>
1070
+ </div>
1071
+ \`;
1072
+ }
1073
+
1074
+ function renderNamespacePills() {
1075
+ const container = document.getElementById('namespace-pills');
1076
+ const pills = ['<span class="namespace-pill' + (currentNamespace === '' ? ' active' : '') + '" onclick="filterNamespace(\\'\\')">All<span class="count">' + (stats.total || 0) + '</span></span>'];
1077
+
1078
+ for (const [ns, count] of Object.entries(stats.byNamespace || {})) {
1079
+ pills.push(\`<span class="namespace-pill\${currentNamespace === ns ? ' active' : ''}" onclick="filterNamespace('\${ns}')">\${ns}<span class="count">\${count}</span></span>\`);
1080
+ }
1081
+
1082
+ container.innerHTML = pills.join('');
1083
+ }
1084
+
1085
+ async function loadEntries() {
1086
+ const container = document.getElementById('entries-container');
1087
+ container.innerHTML = '<div class="loading"><div class="spinner"></div></div>';
1088
+
1089
+ try {
1090
+ let entries;
1091
+
1092
+ if (currentSearch && currentSearch.trim()) {
1093
+ // Use hybrid search endpoint when searching
1094
+ const searchType = document.getElementById('search-type').value;
1095
+ const params = new URLSearchParams({
1096
+ q: currentSearch,
1097
+ type: searchType,
1098
+ limit: String(pageSize)
1099
+ });
1100
+ if (currentNamespace) params.set('namespace', currentNamespace);
1101
+
1102
+ const res = await fetch('/api/search?' + params);
1103
+ entries = await res.json();
1104
+ } else {
1105
+ // Use standard entries endpoint for listing
1106
+ const params = new URLSearchParams({
1107
+ limit: String(pageSize),
1108
+ offset: String(currentPage * pageSize)
1109
+ });
1110
+ if (currentNamespace) params.set('namespace', currentNamespace);
1111
+
1112
+ const res = await fetch('/api/entries?' + params);
1113
+ entries = await res.json();
1114
+ }
1115
+
1116
+ if (!Array.isArray(entries) || entries.length === 0) {
1117
+ container.innerHTML = \`
1118
+ <div class="empty-state">
1119
+ <svg viewBox="0 0 24 24"><path d="M20 6h-8l-2-2H4c-1.1 0-1.99.9-1.99 2L2 18c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2zm-1 12H5c-.55 0-1-.45-1-1v-1h16v1c0 .55-.45 1-1 1zm1-4H4V8h16v6z"/></svg>
1120
+ <h3>No memories found</h3>
1121
+ <p>Click "Add Memory" to create your first entry</p>
1122
+ </div>
1123
+ \`;
1124
+ } else {
1125
+ container.innerHTML = entries.map(entry => \`
1126
+ <div class="entry-card" onclick="showDetail('\${entry.id}')">
1127
+ <div class="entry-header">
1128
+ <span class="entry-key">\${escapeHtml(entry.key)}</span>
1129
+ <div class="entry-badges">
1130
+ \${entry.score !== undefined ? \`<span class="score-badge">\${(entry.score * 100).toFixed(1)}%</span>\` : ''}
1131
+ \${entry.hasEmbedding ?
1132
+ \`<span class="vector-badge has-vector" title="Vector embedding enabled">Vec</span>\` :
1133
+ \`<span class="vector-badge no-vector" title="No vector embedding">--</span>\`
1134
+ }
1135
+ <span class="entry-namespace">\${entry.namespace}</span>
1136
+ </div>
1137
+ </div>
1138
+ <div class="entry-content truncated">\${escapeHtml(entry.content)}</div>
1139
+ <div class="entry-footer">
1140
+ <div class="entry-tags">
1141
+ \${(entry.tags || []).map(tag => \`<span class="tag">\${escapeHtml(tag)}</span>\`).join('')}
1142
+ </div>
1143
+ <span class="entry-date">\${formatDate(entry.created_at)}</span>
1144
+ </div>
1145
+ </div>
1146
+ \`).join('');
1147
+ }
1148
+
1149
+ renderPagination(entries.length);
1150
+ } catch (error) {
1151
+ container.innerHTML = '<div class="empty-state"><h3>No memories yet</h3><p>Click "Add Memory" to get started</p></div>';
1152
+ }
1153
+ }
1154
+
1155
+ function renderPagination(currentCount) {
1156
+ const container = document.getElementById('pagination');
1157
+ const hasMore = currentCount === pageSize;
1158
+ const hasPrev = currentPage > 0;
1159
+
1160
+ container.innerHTML = \`
1161
+ <button class="btn" \${!hasPrev ? 'disabled' : ''} onclick="prevPage()">Previous</button>
1162
+ <button class="btn" \${!hasMore ? 'disabled' : ''} onclick="nextPage()">Next</button>
1163
+ \`;
1164
+ }
1165
+
1166
+ function filterNamespace(ns) {
1167
+ currentNamespace = ns;
1168
+ currentPage = 0;
1169
+ renderNamespacePills();
1170
+ loadEntries();
1171
+ }
1172
+
1173
+ function debounceSearch() {
1174
+ clearTimeout(debounceTimer);
1175
+ debounceTimer = setTimeout(() => {
1176
+ currentSearch = document.getElementById('search-input').value;
1177
+ currentPage = 0;
1178
+ loadEntries();
1179
+ }, 300);
1180
+ }
1181
+
1182
+ function prevPage() { if (currentPage > 0) { currentPage--; loadEntries(); } }
1183
+ function nextPage() { currentPage++; loadEntries(); }
1184
+
1185
+ function renderEmbeddingViz(embedding) {
1186
+ if (!embedding || !embedding.hasEmbedding) {
1187
+ return \`
1188
+ <div class="embedding-section">
1189
+ <div class="embedding-header">
1190
+ <span class="embedding-status no-embedding">
1191
+ <svg viewBox="0 0 24 24"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/></svg>
1192
+ No Embedding
1193
+ </span>
1194
+ </div>
1195
+ <p style="font-size: 12px; color: var(--text-muted); margin: 0;">
1196
+ This entry doesn't have a vector embedding. Edit and save to generate one.
1197
+ </p>
1198
+ </div>
1199
+ \`;
1200
+ }
1201
+
1202
+ const preview = embedding.preview || [];
1203
+ const maxVal = Math.max(...preview.map(Math.abs), 0.001);
1204
+
1205
+ const bars = preview.map(val => {
1206
+ const height = Math.abs(val) / maxVal * 100;
1207
+ const isPositive = val >= 0;
1208
+ return \`<div class="embedding-bar \${isPositive ? 'positive' : 'negative'}" style="height: \${height}%" title="\${val.toFixed(4)}"></div>\`;
1209
+ }).join('');
1210
+
1211
+ return \`
1212
+ <div class="embedding-section">
1213
+ <div class="embedding-header">
1214
+ <span class="embedding-status has-embedding">
1215
+ <svg viewBox="0 0 24 24"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/></svg>
1216
+ Vector Enabled
1217
+ </span>
1218
+ <span class="embedding-dims">\${embedding.dimensions}D</span>
1219
+ </div>
1220
+ <div class="embedding-viz">
1221
+ \${bars}
1222
+ </div>
1223
+ <div class="embedding-legend">
1224
+ <div class="embedding-legend-item">
1225
+ <span class="legend-dot positive"></span>
1226
+ Positive values
1227
+ </div>
1228
+ <div class="embedding-legend-item">
1229
+ <span class="legend-dot negative"></span>
1230
+ Negative values
1231
+ </div>
1232
+ <span>First 20 dimensions</span>
1233
+ </div>
1234
+ </div>
1235
+ \`;
1236
+ }
1237
+
1238
+ async function showDetail(id) {
1239
+ try {
1240
+ const res = await fetch('/api/entry/' + id);
1241
+ const entry = await res.json();
1242
+
1243
+ document.getElementById('detail-body').innerHTML = \`
1244
+ <div class="detail-row">
1245
+ <div class="detail-label">Key</div>
1246
+ <div class="detail-value">\${escapeHtml(entry.key)}</div>
1247
+ </div>
1248
+ <div class="detail-row">
1249
+ <div class="detail-label">Namespace</div>
1250
+ <div class="detail-value">\${entry.namespace}</div>
1251
+ </div>
1252
+ <div class="detail-row">
1253
+ <div class="detail-label">Type</div>
1254
+ <div class="detail-value">\${entry.type}</div>
1255
+ </div>
1256
+ <div class="detail-row">
1257
+ <div class="detail-label">Content</div>
1258
+ <div class="detail-value content">\${escapeHtml(entry.content)}</div>
1259
+ </div>
1260
+ <div class="detail-row">
1261
+ <div class="detail-label">Tags</div>
1262
+ <div class="detail-value">\${(entry.tags || []).join(', ') || 'None'}</div>
1263
+ </div>
1264
+ <div class="detail-row">
1265
+ <div class="detail-label">Created</div>
1266
+ <div class="detail-value">\${new Date(entry.created_at).toLocaleString()}</div>
1267
+ </div>
1268
+ \${renderEmbeddingViz(entry.embedding)}
1269
+ <div class="detail-actions">
1270
+ <button class="btn btn-primary" onclick="openEditModal('\${entry.id}')">
1271
+ <svg viewBox="0 0 24 24"><path d="M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04c.39-.39.39-1.02 0-1.41l-2.34-2.34c-.39-.39-1.02-.39-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z"/></svg>
1272
+ Edit
1273
+ </button>
1274
+ <button class="btn btn-danger" onclick="openDeleteModal('\${entry.id}')">
1275
+ <svg viewBox="0 0 24 24"><path d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"/></svg>
1276
+ Delete
1277
+ </button>
1278
+ </div>
1279
+ \`;
1280
+
1281
+ document.getElementById('detail-modal').classList.add('active');
1282
+ } catch (error) {
1283
+ showToast('Failed to load entry', 'error');
1284
+ }
1285
+ }
1286
+
1287
+ function closeDetailModal(event) {
1288
+ if (!event || event.target.id === 'detail-modal') {
1289
+ document.getElementById('detail-modal').classList.remove('active');
1290
+ }
1291
+ }
1292
+
1293
+ function openAddModal() {
1294
+ document.getElementById('form-title').textContent = 'Add Memory';
1295
+ document.getElementById('form-id').value = '';
1296
+ document.getElementById('form-key').value = '';
1297
+ document.getElementById('form-namespace').value = 'patterns';
1298
+ document.getElementById('form-type').value = 'semantic';
1299
+ document.getElementById('form-content').value = '';
1300
+ document.getElementById('form-tags').value = '';
1301
+ document.getElementById('form-modal').classList.add('active');
1302
+ }
1303
+
1304
+ async function openEditModal(id) {
1305
+ closeDetailModal();
1306
+ const res = await fetch('/api/entry/' + id);
1307
+ const entry = await res.json();
1308
+
1309
+ document.getElementById('form-title').textContent = 'Edit Memory';
1310
+ document.getElementById('form-id').value = entry.id;
1311
+ document.getElementById('form-key').value = entry.key;
1312
+ document.getElementById('form-namespace').value = entry.namespace;
1313
+ document.getElementById('form-type').value = entry.type;
1314
+ document.getElementById('form-content').value = entry.content;
1315
+ document.getElementById('form-tags').value = (entry.tags || []).join(', ');
1316
+ document.getElementById('form-modal').classList.add('active');
1317
+ }
1318
+
1319
+ function closeFormModal(event) {
1320
+ if (!event || event.target.id === 'form-modal') {
1321
+ document.getElementById('form-modal').classList.remove('active');
1322
+ }
1323
+ }
1324
+
1325
+ async function saveEntry() {
1326
+ const id = document.getElementById('form-id').value;
1327
+ const data = {
1328
+ key: document.getElementById('form-key').value.trim(),
1329
+ namespace: document.getElementById('form-namespace').value,
1330
+ type: document.getElementById('form-type').value,
1331
+ content: document.getElementById('form-content').value.trim(),
1332
+ tags: document.getElementById('form-tags').value.split(',').map(t => t.trim()).filter(Boolean),
1333
+ };
1334
+
1335
+ if (!data.key || !data.content) {
1336
+ showToast('Key and Content are required', 'error');
1337
+ return;
1338
+ }
1339
+
1340
+ try {
1341
+ const method = id ? 'PUT' : 'POST';
1342
+ const url = id ? '/api/entry/' + id : '/api/entries';
1343
+ const res = await fetch(url, {
1344
+ method,
1345
+ headers: { 'Content-Type': 'application/json' },
1346
+ body: JSON.stringify(data),
1347
+ });
1348
+
1349
+ if (res.ok) {
1350
+ closeFormModal();
1351
+ showToast(id ? 'Memory updated' : 'Memory created', 'success');
1352
+ loadData();
1353
+ } else {
1354
+ const err = await res.json();
1355
+ showToast(err.error || 'Failed to save', 'error');
1356
+ }
1357
+ } catch (error) {
1358
+ showToast('Failed to save', 'error');
1359
+ }
1360
+ }
1361
+
1362
+ function openDeleteModal(id) {
1363
+ closeDetailModal();
1364
+ document.getElementById('delete-id').value = id;
1365
+ document.getElementById('delete-modal').classList.add('active');
1366
+ }
1367
+
1368
+ function closeDeleteModal(event) {
1369
+ if (!event || event.target.id === 'delete-modal') {
1370
+ document.getElementById('delete-modal').classList.remove('active');
1371
+ }
1372
+ }
1373
+
1374
+ async function confirmDelete() {
1375
+ const id = document.getElementById('delete-id').value;
1376
+ try {
1377
+ const res = await fetch('/api/entry/' + id, { method: 'DELETE' });
1378
+ if (res.ok) {
1379
+ closeDeleteModal();
1380
+ showToast('Memory deleted', 'success');
1381
+ loadData();
1382
+ } else {
1383
+ showToast('Failed to delete', 'error');
1384
+ }
1385
+ } catch (error) {
1386
+ showToast('Failed to delete', 'error');
1387
+ }
1388
+ }
1389
+
1390
+ async function openEmbeddingModal() {
1391
+ document.getElementById('embedding-modal').classList.add('active');
1392
+ document.getElementById('embedding-progress').style.display = 'none';
1393
+ document.getElementById('embedding-modal-footer').style.display = 'flex';
1394
+
1395
+ // Load embedding stats
1396
+ try {
1397
+ const res = await fetch('/api/embeddings/stats');
1398
+ const embeddingStats = await res.json();
1399
+ document.getElementById('stat-total').textContent = embeddingStats.total;
1400
+ document.getElementById('stat-with').textContent = embeddingStats.withEmbedding;
1401
+ document.getElementById('stat-without').textContent = embeddingStats.withoutEmbedding;
1402
+
1403
+ // Disable buttons if nothing to do
1404
+ document.getElementById('btn-generate-missing').disabled = embeddingStats.withoutEmbedding === 0;
1405
+ document.getElementById('btn-regenerate-all').disabled = embeddingStats.total === 0;
1406
+ } catch (error) {
1407
+ console.error('Failed to load embedding stats:', error);
1408
+ }
1409
+ }
1410
+
1411
+ function closeEmbeddingModal(event) {
1412
+ if (!event || event.target.id === 'embedding-modal') {
1413
+ document.getElementById('embedding-modal').classList.remove('active');
1414
+ }
1415
+ }
1416
+
1417
+ async function generateEmbeddings(mode) {
1418
+ const progressEl = document.getElementById('embedding-progress');
1419
+ const progressFill = document.getElementById('progress-fill');
1420
+ const progressText = document.getElementById('progress-text');
1421
+ const footerEl = document.getElementById('embedding-modal-footer');
1422
+
1423
+ // Show progress, hide buttons
1424
+ progressEl.style.display = 'block';
1425
+ footerEl.style.display = 'none';
1426
+ progressFill.style.width = '0%';
1427
+ progressText.textContent = mode === 'missing' ? 'Generating missing embeddings...' : 'Re-generating all embeddings...';
1428
+
1429
+ // Animate progress bar while waiting
1430
+ let progress = 0;
1431
+ const interval = setInterval(() => {
1432
+ progress += Math.random() * 10;
1433
+ if (progress > 90) progress = 90;
1434
+ progressFill.style.width = progress + '%';
1435
+ }, 200);
1436
+
1437
+ try {
1438
+ const res = await fetch('/api/embeddings/generate', {
1439
+ method: 'POST',
1440
+ headers: { 'Content-Type': 'application/json' },
1441
+ body: JSON.stringify({ mode }),
1442
+ });
1443
+
1444
+ clearInterval(interval);
1445
+ progressFill.style.width = '100%';
1446
+
1447
+ const result = await res.json();
1448
+ progressText.textContent = result.message || 'Done!';
1449
+
1450
+ setTimeout(() => {
1451
+ showToast(result.message || 'Embeddings generated', 'success');
1452
+ closeEmbeddingModal();
1453
+ loadData();
1454
+ }, 1000);
1455
+ } catch (error) {
1456
+ clearInterval(interval);
1457
+ progressFill.style.width = '0%';
1458
+ progressText.textContent = 'Failed to generate embeddings';
1459
+ showToast('Failed to generate embeddings', 'error');
1460
+
1461
+ // Re-show buttons after error
1462
+ setTimeout(() => {
1463
+ progressEl.style.display = 'none';
1464
+ footerEl.style.display = 'flex';
1465
+ }, 2000);
1466
+ }
1467
+ }
1468
+
1469
+ function showToast(message, type = 'success') {
1470
+ const toast = document.createElement('div');
1471
+ toast.className = 'toast ' + type;
1472
+ toast.textContent = message;
1473
+ document.body.appendChild(toast);
1474
+ setTimeout(() => toast.remove(), 3000);
1475
+ }
1476
+
1477
+ function escapeHtml(text) {
1478
+ if (!text) return '';
1479
+ const div = document.createElement('div');
1480
+ div.textContent = text;
1481
+ return div.innerHTML;
1482
+ }
1483
+
1484
+ function formatDate(timestamp) {
1485
+ if (!timestamp) return 'Unknown';
1486
+ const date = new Date(timestamp);
1487
+ const now = new Date();
1488
+ const diff = now - date;
1489
+
1490
+ if (diff < 60000) return 'Just now';
1491
+ if (diff < 3600000) return Math.floor(diff / 60000) + 'm ago';
1492
+ if (diff < 86400000) return Math.floor(diff / 3600000) + 'h ago';
1493
+ if (diff < 604800000) return Math.floor(diff / 86400000) + 'd ago';
1494
+
1495
+ return date.toLocaleDateString();
1496
+ }
1497
+
1498
+ document.addEventListener('keydown', (e) => {
1499
+ if (e.key === 'Escape') {
1500
+ closeDetailModal();
1501
+ closeFormModal();
1502
+ closeDeleteModal();
1503
+ closeEmbeddingModal();
1504
+ }
1505
+ });
1506
+
1507
+ loadData();
1508
+ </script>
1509
+ </body>
1510
+ </html>`;
1511
+ }
1512
+ async function readBody(req) {
1513
+ return new Promise((resolve, reject) => {
1514
+ let body = '';
1515
+ req.on('data', chunk => body += chunk);
1516
+ req.on('end', () => resolve(body));
1517
+ req.on('error', reject);
1518
+ });
1519
+ }
1520
+ function handleRequest(req, res) {
1521
+ const url = new URL(req.url || '/', `http://localhost:${PORT}`);
1522
+ const method = req.method || 'GET';
1523
+ res.setHeader('Content-Type', 'application/json');
1524
+ try {
1525
+ const db = getDatabase();
1526
+ // Serve HTML
1527
+ if (url.pathname === '/' && method === 'GET') {
1528
+ res.setHeader('Content-Type', 'text/html');
1529
+ res.writeHead(200);
1530
+ res.end(getHTML());
1531
+ return;
1532
+ }
1533
+ // GET stats
1534
+ if (url.pathname === '/api/stats' && method === 'GET') {
1535
+ const stats = getStats(db);
1536
+ res.writeHead(200);
1537
+ res.end(JSON.stringify(stats));
1538
+ return;
1539
+ }
1540
+ // GET entries (standard listing with optional FTS search)
1541
+ if (url.pathname === '/api/entries' && method === 'GET') {
1542
+ const namespace = url.searchParams.get('namespace') || undefined;
1543
+ const limit = parseInt(url.searchParams.get('limit') || '50', 10);
1544
+ const offset = parseInt(url.searchParams.get('offset') || '0', 10);
1545
+ const search = url.searchParams.get('search') || undefined;
1546
+ const entries = getEntries(db, namespace, limit, offset, search);
1547
+ res.writeHead(200);
1548
+ res.end(JSON.stringify(entries));
1549
+ return;
1550
+ }
1551
+ // GET hybrid search (new endpoint with vector support)
1552
+ if (url.pathname === '/api/search' && method === 'GET') {
1553
+ const query = url.searchParams.get('q') || '';
1554
+ const searchType = (url.searchParams.get('type') || 'hybrid');
1555
+ const limit = parseInt(url.searchParams.get('limit') || '20', 10);
1556
+ const namespace = url.searchParams.get('namespace') || undefined;
1557
+ getSearchEngine()
1558
+ .then((searchEngine) => searchEntries(searchEngine, query, { type: searchType, namespace, limit }))
1559
+ .then((results) => {
1560
+ res.writeHead(200);
1561
+ res.end(JSON.stringify(results));
1562
+ })
1563
+ .catch((error) => {
1564
+ res.writeHead(500);
1565
+ res.end(JSON.stringify({ error: error instanceof Error ? error.message : 'Search failed' }));
1566
+ });
1567
+ return;
1568
+ }
1569
+ // POST create entry (direct DB for compatibility with existing schema)
1570
+ if (url.pathname === '/api/entries' && method === 'POST') {
1571
+ readBody(req)
1572
+ .then(async (body) => {
1573
+ const data = JSON.parse(body);
1574
+ const now = Date.now();
1575
+ const id = `mem_${now}_${Math.random().toString(36).slice(2, 10)}`;
1576
+ const tags = JSON.stringify(data.tags || []);
1577
+ // Generate embedding for the content
1578
+ let embeddingBuffer = null;
1579
+ try {
1580
+ const embeddingsService = await getEmbeddingsService();
1581
+ const result = await embeddingsService.embed(data.content);
1582
+ embeddingBuffer = Buffer.from(result.embedding.buffer);
1583
+ }
1584
+ catch (e) {
1585
+ console.warn('[WebViewer] Failed to generate embedding:', e);
1586
+ }
1587
+ db.prepare(`INSERT INTO memory_entries (id, key, content, type, namespace, tags, embedding, created_at, updated_at)
1588
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(id, data.key, data.content, data.type || 'semantic', data.namespace || 'general', tags, embeddingBuffer, now, now);
1589
+ res.writeHead(201);
1590
+ res.end(JSON.stringify({ id, success: true }));
1591
+ })
1592
+ .catch((error) => {
1593
+ res.writeHead(500);
1594
+ res.end(JSON.stringify({ error: error instanceof Error ? error.message : 'Internal error' }));
1595
+ });
1596
+ return;
1597
+ }
1598
+ // GET single entry
1599
+ if (url.pathname.startsWith('/api/entry/') && method === 'GET') {
1600
+ const id = url.pathname.split('/').pop();
1601
+ const row = db.prepare('SELECT * FROM memory_entries WHERE id = ?').get(id);
1602
+ if (row) {
1603
+ // Extract embedding info for visualization
1604
+ let embeddingInfo = {
1605
+ hasEmbedding: false,
1606
+ };
1607
+ if (row.embedding && row.embedding.length > 0) {
1608
+ const embedding = new Float32Array(row.embedding.buffer.slice(row.embedding.byteOffset, row.embedding.byteOffset + row.embedding.byteLength));
1609
+ // Get first 20 values for preview visualization
1610
+ const preview = Array.from(embedding.slice(0, 20));
1611
+ embeddingInfo = {
1612
+ hasEmbedding: true,
1613
+ dimensions: embedding.length,
1614
+ preview,
1615
+ };
1616
+ }
1617
+ res.writeHead(200);
1618
+ res.end(JSON.stringify({
1619
+ id: row.id,
1620
+ key: row.key,
1621
+ content: row.content,
1622
+ type: row.type,
1623
+ namespace: row.namespace,
1624
+ tags: JSON.parse(row.tags || '[]'),
1625
+ created_at: row.created_at,
1626
+ updated_at: row.updated_at,
1627
+ embedding: embeddingInfo,
1628
+ }));
1629
+ }
1630
+ else {
1631
+ res.writeHead(404);
1632
+ res.end(JSON.stringify({ error: 'Entry not found' }));
1633
+ }
1634
+ return;
1635
+ }
1636
+ // PUT update entry (direct DB for full field updates)
1637
+ if (url.pathname.startsWith('/api/entry/') && method === 'PUT') {
1638
+ const id = url.pathname.split('/').pop();
1639
+ if (!id) {
1640
+ res.writeHead(400);
1641
+ res.end(JSON.stringify({ error: 'Missing entry ID' }));
1642
+ return;
1643
+ }
1644
+ readBody(req)
1645
+ .then(async (body) => {
1646
+ const data = JSON.parse(body);
1647
+ const now = Date.now();
1648
+ const tags = JSON.stringify(data.tags || []);
1649
+ // Generate embedding for the updated content
1650
+ let embeddingBuffer = null;
1651
+ try {
1652
+ const embeddingsService = await getEmbeddingsService();
1653
+ const result = await embeddingsService.embed(data.content);
1654
+ embeddingBuffer = Buffer.from(result.embedding.buffer);
1655
+ }
1656
+ catch (e) {
1657
+ console.warn('[WebViewer] Failed to generate embedding:', e);
1658
+ }
1659
+ // Update with embedding
1660
+ const result = db.prepare(`UPDATE memory_entries SET key = ?, content = ?, type = ?, namespace = ?, tags = ?, embedding = ?, updated_at = ?
1661
+ WHERE id = ?`).run(data.key, data.content, data.type, data.namespace, tags, embeddingBuffer, now, id);
1662
+ if (result.changes > 0) {
1663
+ res.writeHead(200);
1664
+ res.end(JSON.stringify({ success: true }));
1665
+ }
1666
+ else {
1667
+ res.writeHead(404);
1668
+ res.end(JSON.stringify({ error: 'Entry not found' }));
1669
+ }
1670
+ })
1671
+ .catch((error) => {
1672
+ res.writeHead(500);
1673
+ res.end(JSON.stringify({ error: error instanceof Error ? error.message : 'Internal error' }));
1674
+ });
1675
+ return;
1676
+ }
1677
+ // DELETE entry (direct DB for compatibility)
1678
+ if (url.pathname.startsWith('/api/entry/') && method === 'DELETE') {
1679
+ const id = url.pathname.split('/').pop();
1680
+ if (!id) {
1681
+ res.writeHead(400);
1682
+ res.end(JSON.stringify({ error: 'Missing entry ID' }));
1683
+ return;
1684
+ }
1685
+ const result = db.prepare('DELETE FROM memory_entries WHERE id = ?').run(id);
1686
+ if (result.changes > 0) {
1687
+ res.writeHead(200);
1688
+ res.end(JSON.stringify({ success: true }));
1689
+ }
1690
+ else {
1691
+ res.writeHead(404);
1692
+ res.end(JSON.stringify({ error: 'Entry not found' }));
1693
+ }
1694
+ return;
1695
+ }
1696
+ // GET embedding stats
1697
+ if (url.pathname === '/api/embeddings/stats' && method === 'GET') {
1698
+ const totalRow = db.prepare('SELECT COUNT(*) as count FROM memory_entries').get();
1699
+ const withEmbeddingRow = db.prepare('SELECT COUNT(*) as count FROM memory_entries WHERE embedding IS NOT NULL AND LENGTH(embedding) > 0').get();
1700
+ res.writeHead(200);
1701
+ res.end(JSON.stringify({
1702
+ total: totalRow?.count || 0,
1703
+ withEmbedding: withEmbeddingRow?.count || 0,
1704
+ withoutEmbedding: (totalRow?.count || 0) - (withEmbeddingRow?.count || 0),
1705
+ }));
1706
+ return;
1707
+ }
1708
+ // POST batch generate embeddings
1709
+ if (url.pathname === '/api/embeddings/generate' && method === 'POST') {
1710
+ readBody(req)
1711
+ .then(async (body) => {
1712
+ const options = JSON.parse(body || '{}');
1713
+ const mode = options.mode || 'missing';
1714
+ // Get entries to process
1715
+ const query = mode === 'missing'
1716
+ ? 'SELECT id, content FROM memory_entries WHERE embedding IS NULL OR LENGTH(embedding) = 0'
1717
+ : 'SELECT id, content FROM memory_entries';
1718
+ const entries = db.prepare(query).all();
1719
+ if (entries.length === 0) {
1720
+ res.writeHead(200);
1721
+ res.end(JSON.stringify({ processed: 0, success: 0, failed: 0, message: 'No entries to process' }));
1722
+ return;
1723
+ }
1724
+ const embeddingsService = await getEmbeddingsService();
1725
+ let success = 0;
1726
+ let failed = 0;
1727
+ const updateStmt = db.prepare('UPDATE memory_entries SET embedding = ?, updated_at = ? WHERE id = ?');
1728
+ for (const entry of entries) {
1729
+ try {
1730
+ const result = await embeddingsService.embed(entry.content);
1731
+ const embeddingBuffer = Buffer.from(result.embedding.buffer);
1732
+ updateStmt.run(embeddingBuffer, Date.now(), entry.id);
1733
+ success++;
1734
+ }
1735
+ catch (e) {
1736
+ console.warn(`[WebViewer] Failed to generate embedding for ${entry.id}:`, e);
1737
+ failed++;
1738
+ }
1739
+ }
1740
+ res.writeHead(200);
1741
+ res.end(JSON.stringify({
1742
+ processed: entries.length,
1743
+ success,
1744
+ failed,
1745
+ message: `Generated embeddings for ${success} entries${failed > 0 ? `, ${failed} failed` : ''}`,
1746
+ }));
1747
+ })
1748
+ .catch((error) => {
1749
+ res.writeHead(500);
1750
+ res.end(JSON.stringify({ error: error instanceof Error ? error.message : 'Internal error' }));
1751
+ });
1752
+ return;
1753
+ }
1754
+ res.writeHead(404);
1755
+ res.end(JSON.stringify({ error: 'Not found' }));
1756
+ }
1757
+ catch (error) {
1758
+ res.writeHead(500);
1759
+ res.end(JSON.stringify({ error: error instanceof Error ? error.message : 'Internal error' }));
1760
+ }
1761
+ }
1762
+ const server = http.createServer(handleRequest);
1763
+ server.listen(PORT, () => {
1764
+ console.log(`\n AgentKits Memory Viewer\n`);
1765
+ console.log(` Local: http://localhost:${PORT}`);
1766
+ console.log(` Database: ${dbPath}\n`);
1767
+ console.log(` Press Ctrl+C to stop\n`);
1768
+ });
1769
+ //# sourceMappingURL=web-viewer.js.map