@aitytech/agentkits-memory 1.0.0 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +223 -154
- package/assets/agentkits-memory-add-memory.png +0 -0
- package/assets/agentkits-memory-memory-detail.png +0 -0
- package/assets/agentkits-memory-memory-list.png +0 -0
- package/assets/logo.svg +24 -0
- package/dist/cli/web-viewer.d.ts +13 -0
- package/dist/cli/web-viewer.d.ts.map +1 -0
- package/dist/cli/web-viewer.js +1119 -0
- package/dist/cli/web-viewer.js.map +1 -0
- package/package.json +19 -6
- package/src/cli/web-viewer.ts +1202 -0
|
@@ -0,0 +1,1202 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* AgentKits Memory Web Viewer
|
|
4
|
+
*
|
|
5
|
+
* Web-based viewer for memory database with CRUD support.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* npx agentkits-memory-web [--port=1905]
|
|
9
|
+
*
|
|
10
|
+
* @module @aitytech/agentkits-memory/cli/web-viewer
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import * as http from 'node:http';
|
|
14
|
+
import * as fs from 'node:fs';
|
|
15
|
+
import * as path from 'node:path';
|
|
16
|
+
import { createRequire } from 'node:module';
|
|
17
|
+
import initSqlJs, { Database as SqlJsDatabase } from 'sql.js';
|
|
18
|
+
|
|
19
|
+
const args = process.argv.slice(2);
|
|
20
|
+
const projectDir = process.env.CLAUDE_PROJECT_DIR || process.cwd();
|
|
21
|
+
|
|
22
|
+
function parseArgs(): Record<string, string | boolean> {
|
|
23
|
+
const parsed: Record<string, string | boolean> = {};
|
|
24
|
+
for (const arg of args) {
|
|
25
|
+
if (arg.startsWith('--')) {
|
|
26
|
+
const [key, value] = arg.slice(2).split('=');
|
|
27
|
+
parsed[key] = value ?? true;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return parsed;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const options = parseArgs();
|
|
34
|
+
const PORT = parseInt(options.port as string, 10) || 1905;
|
|
35
|
+
|
|
36
|
+
const dbDir = path.join(projectDir, '.claude/memory');
|
|
37
|
+
const dbPath = path.join(dbDir, 'memory.db');
|
|
38
|
+
|
|
39
|
+
let SQL: Awaited<ReturnType<typeof initSqlJs>>;
|
|
40
|
+
|
|
41
|
+
async function initSQL(): Promise<void> {
|
|
42
|
+
if (!SQL) {
|
|
43
|
+
const require = createRequire(import.meta.url);
|
|
44
|
+
const sqlJsPath = require.resolve('sql.js');
|
|
45
|
+
SQL = await initSqlJs({
|
|
46
|
+
locateFile: (file: string) => path.join(path.dirname(sqlJsPath), file),
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function loadOrCreateDatabase(): Promise<SqlJsDatabase> {
|
|
52
|
+
await initSQL();
|
|
53
|
+
|
|
54
|
+
// Ensure directory exists
|
|
55
|
+
if (!fs.existsSync(dbDir)) {
|
|
56
|
+
fs.mkdirSync(dbDir, { recursive: true });
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
let db: SqlJsDatabase;
|
|
60
|
+
|
|
61
|
+
if (fs.existsSync(dbPath)) {
|
|
62
|
+
const buffer = fs.readFileSync(dbPath);
|
|
63
|
+
db = new SQL.Database(new Uint8Array(buffer));
|
|
64
|
+
} else {
|
|
65
|
+
db = new SQL.Database();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Create table if not exists
|
|
69
|
+
db.run(`
|
|
70
|
+
CREATE TABLE IF NOT EXISTS memory_entries (
|
|
71
|
+
id TEXT PRIMARY KEY,
|
|
72
|
+
key TEXT NOT NULL,
|
|
73
|
+
content TEXT NOT NULL,
|
|
74
|
+
type TEXT DEFAULT 'semantic',
|
|
75
|
+
namespace TEXT DEFAULT 'general',
|
|
76
|
+
tags TEXT DEFAULT '[]',
|
|
77
|
+
metadata TEXT DEFAULT '{}',
|
|
78
|
+
embedding BLOB,
|
|
79
|
+
created_at INTEGER NOT NULL,
|
|
80
|
+
updated_at INTEGER NOT NULL,
|
|
81
|
+
accessed_at INTEGER,
|
|
82
|
+
access_count INTEGER DEFAULT 0,
|
|
83
|
+
importance REAL DEFAULT 0.5,
|
|
84
|
+
decay_rate REAL DEFAULT 0.1
|
|
85
|
+
)
|
|
86
|
+
`);
|
|
87
|
+
|
|
88
|
+
db.run(`CREATE INDEX IF NOT EXISTS idx_namespace ON memory_entries(namespace)`);
|
|
89
|
+
db.run(`CREATE INDEX IF NOT EXISTS idx_key ON memory_entries(key)`);
|
|
90
|
+
db.run(`CREATE INDEX IF NOT EXISTS idx_created ON memory_entries(created_at)`);
|
|
91
|
+
|
|
92
|
+
return db;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function saveDatabase(db: SqlJsDatabase): void {
|
|
96
|
+
const data = db.export();
|
|
97
|
+
const buffer = Buffer.from(data);
|
|
98
|
+
fs.writeFileSync(dbPath, buffer);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function generateId(): string {
|
|
102
|
+
return `mem_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function getStats(db: SqlJsDatabase): {
|
|
106
|
+
total: number;
|
|
107
|
+
byNamespace: Record<string, number>;
|
|
108
|
+
byType: Record<string, number>;
|
|
109
|
+
} {
|
|
110
|
+
const totalResult = db.exec('SELECT COUNT(*) as count FROM memory_entries');
|
|
111
|
+
const total = (totalResult[0]?.values[0]?.[0] as number) || 0;
|
|
112
|
+
|
|
113
|
+
const nsResult = db.exec('SELECT namespace, COUNT(*) FROM memory_entries GROUP BY namespace');
|
|
114
|
+
const byNamespace: Record<string, number> = {};
|
|
115
|
+
if (nsResult[0]) {
|
|
116
|
+
for (const row of nsResult[0].values) {
|
|
117
|
+
byNamespace[row[0] as string] = row[1] as number;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const typeResult = db.exec('SELECT type, COUNT(*) FROM memory_entries GROUP BY type');
|
|
122
|
+
const byType: Record<string, number> = {};
|
|
123
|
+
if (typeResult[0]) {
|
|
124
|
+
for (const row of typeResult[0].values) {
|
|
125
|
+
byType[row[0] as string] = row[1] as number;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return { total, byNamespace, byType };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function getEntries(
|
|
133
|
+
db: SqlJsDatabase,
|
|
134
|
+
namespace?: string,
|
|
135
|
+
limit = 50,
|
|
136
|
+
offset = 0,
|
|
137
|
+
search?: string
|
|
138
|
+
): Array<{
|
|
139
|
+
id: string;
|
|
140
|
+
key: string;
|
|
141
|
+
content: string;
|
|
142
|
+
type: string;
|
|
143
|
+
namespace: string;
|
|
144
|
+
tags: string[];
|
|
145
|
+
created_at: number;
|
|
146
|
+
updated_at: number;
|
|
147
|
+
}> {
|
|
148
|
+
let query = 'SELECT id, key, content, type, namespace, tags, created_at, updated_at FROM memory_entries';
|
|
149
|
+
const conditions: string[] = [];
|
|
150
|
+
const params: (string | number)[] = [];
|
|
151
|
+
|
|
152
|
+
if (namespace) {
|
|
153
|
+
conditions.push('namespace = ?');
|
|
154
|
+
params.push(namespace);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (search) {
|
|
158
|
+
conditions.push('(content LIKE ? OR key LIKE ? OR tags LIKE ?)');
|
|
159
|
+
const searchPattern = `%${search}%`;
|
|
160
|
+
params.push(searchPattern, searchPattern, searchPattern);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (conditions.length > 0) {
|
|
164
|
+
query += ' WHERE ' + conditions.join(' AND ');
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
query += ' ORDER BY created_at DESC LIMIT ? OFFSET ?';
|
|
168
|
+
params.push(limit, offset);
|
|
169
|
+
|
|
170
|
+
const stmt = db.prepare(query);
|
|
171
|
+
stmt.bind(params);
|
|
172
|
+
|
|
173
|
+
const entries: Array<{
|
|
174
|
+
id: string;
|
|
175
|
+
key: string;
|
|
176
|
+
content: string;
|
|
177
|
+
type: string;
|
|
178
|
+
namespace: string;
|
|
179
|
+
tags: string[];
|
|
180
|
+
created_at: number;
|
|
181
|
+
updated_at: number;
|
|
182
|
+
}> = [];
|
|
183
|
+
|
|
184
|
+
while (stmt.step()) {
|
|
185
|
+
const row = stmt.getAsObject();
|
|
186
|
+
entries.push({
|
|
187
|
+
id: row.id as string,
|
|
188
|
+
key: row.key as string,
|
|
189
|
+
content: row.content as string,
|
|
190
|
+
type: row.type as string,
|
|
191
|
+
namespace: row.namespace as string,
|
|
192
|
+
tags: JSON.parse((row.tags as string) || '[]'),
|
|
193
|
+
created_at: row.created_at as number,
|
|
194
|
+
updated_at: row.updated_at as number,
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
stmt.free();
|
|
198
|
+
|
|
199
|
+
return entries;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function getHTML(): string {
|
|
203
|
+
return `<!DOCTYPE html>
|
|
204
|
+
<html lang="en">
|
|
205
|
+
<head>
|
|
206
|
+
<meta charset="UTF-8">
|
|
207
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
208
|
+
<title>AgentKits Memory Viewer</title>
|
|
209
|
+
<style>
|
|
210
|
+
:root {
|
|
211
|
+
--bg-primary: #0F172A;
|
|
212
|
+
--bg-secondary: #1E293B;
|
|
213
|
+
--bg-card: #334155;
|
|
214
|
+
--text-primary: #F8FAFC;
|
|
215
|
+
--text-secondary: #94A3B8;
|
|
216
|
+
--text-muted: #64748B;
|
|
217
|
+
--border: #475569;
|
|
218
|
+
--accent: #3B82F6;
|
|
219
|
+
--accent-hover: #2563EB;
|
|
220
|
+
--success: #22C55E;
|
|
221
|
+
--warning: #F59E0B;
|
|
222
|
+
--error: #EF4444;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
226
|
+
|
|
227
|
+
body {
|
|
228
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
|
229
|
+
background: var(--bg-primary);
|
|
230
|
+
color: var(--text-primary);
|
|
231
|
+
min-height: 100vh;
|
|
232
|
+
line-height: 1.5;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
.container { max-width: 1400px; margin: 0 auto; padding: 24px; }
|
|
236
|
+
|
|
237
|
+
header {
|
|
238
|
+
display: flex;
|
|
239
|
+
justify-content: space-between;
|
|
240
|
+
align-items: center;
|
|
241
|
+
margin-bottom: 32px;
|
|
242
|
+
padding-bottom: 24px;
|
|
243
|
+
border-bottom: 1px solid var(--border);
|
|
244
|
+
flex-wrap: wrap;
|
|
245
|
+
gap: 16px;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
.logo { display: flex; align-items: center; gap: 12px; }
|
|
249
|
+
|
|
250
|
+
.logo-icon {
|
|
251
|
+
width: 40px; height: 40px;
|
|
252
|
+
background: linear-gradient(135deg, var(--accent), #8B5CF6);
|
|
253
|
+
border-radius: 10px;
|
|
254
|
+
display: flex; align-items: center; justify-content: center;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
.logo-icon svg { width: 24px; height: 24px; fill: white; }
|
|
258
|
+
h1 { font-size: 24px; font-weight: 600; }
|
|
259
|
+
.subtitle { font-size: 14px; color: var(--text-secondary); }
|
|
260
|
+
|
|
261
|
+
.header-actions { display: flex; gap: 12px; }
|
|
262
|
+
|
|
263
|
+
.stats-grid {
|
|
264
|
+
display: grid;
|
|
265
|
+
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
266
|
+
gap: 16px;
|
|
267
|
+
margin-bottom: 32px;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
.stat-card {
|
|
271
|
+
background: var(--bg-secondary);
|
|
272
|
+
border-radius: 12px;
|
|
273
|
+
padding: 20px;
|
|
274
|
+
border: 1px solid var(--border);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
.stat-label {
|
|
278
|
+
font-size: 13px;
|
|
279
|
+
color: var(--text-secondary);
|
|
280
|
+
text-transform: uppercase;
|
|
281
|
+
letter-spacing: 0.5px;
|
|
282
|
+
margin-bottom: 8px;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
.stat-value { font-size: 32px; font-weight: 700; color: var(--text-primary); }
|
|
286
|
+
|
|
287
|
+
.controls { display: flex; gap: 16px; margin-bottom: 24px; flex-wrap: wrap; }
|
|
288
|
+
|
|
289
|
+
.search-box { flex: 1; min-width: 250px; position: relative; }
|
|
290
|
+
|
|
291
|
+
.search-box input {
|
|
292
|
+
width: 100%;
|
|
293
|
+
padding: 12px 16px 12px 44px;
|
|
294
|
+
background: var(--bg-secondary);
|
|
295
|
+
border: 1px solid var(--border);
|
|
296
|
+
border-radius: 8px;
|
|
297
|
+
color: var(--text-primary);
|
|
298
|
+
font-size: 14px;
|
|
299
|
+
transition: border-color 0.2s, box-shadow 0.2s;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
.search-box input:focus {
|
|
303
|
+
outline: none;
|
|
304
|
+
border-color: var(--accent);
|
|
305
|
+
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.2);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
.search-box input::placeholder { color: var(--text-muted); }
|
|
309
|
+
|
|
310
|
+
.search-box svg {
|
|
311
|
+
position: absolute;
|
|
312
|
+
left: 14px;
|
|
313
|
+
top: 50%;
|
|
314
|
+
transform: translateY(-50%);
|
|
315
|
+
width: 18px; height: 18px;
|
|
316
|
+
fill: var(--text-muted);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
.entries-list { display: flex; flex-direction: column; gap: 12px; }
|
|
320
|
+
|
|
321
|
+
.entry-card {
|
|
322
|
+
background: var(--bg-secondary);
|
|
323
|
+
border: 1px solid var(--border);
|
|
324
|
+
border-radius: 12px;
|
|
325
|
+
padding: 20px;
|
|
326
|
+
cursor: pointer;
|
|
327
|
+
transition: border-color 0.2s, transform 0.2s;
|
|
328
|
+
position: relative;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
.entry-card:hover {
|
|
332
|
+
border-color: var(--accent);
|
|
333
|
+
transform: translateY(-2px);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
.entry-header {
|
|
337
|
+
display: flex;
|
|
338
|
+
justify-content: space-between;
|
|
339
|
+
align-items: flex-start;
|
|
340
|
+
margin-bottom: 12px;
|
|
341
|
+
gap: 12px;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
.entry-key { font-weight: 600; font-size: 15px; color: var(--text-primary); word-break: break-word; }
|
|
345
|
+
|
|
346
|
+
.entry-namespace {
|
|
347
|
+
font-size: 12px;
|
|
348
|
+
padding: 4px 10px;
|
|
349
|
+
background: var(--bg-card);
|
|
350
|
+
border-radius: 6px;
|
|
351
|
+
color: var(--text-secondary);
|
|
352
|
+
white-space: nowrap;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
.entry-content {
|
|
356
|
+
font-size: 14px;
|
|
357
|
+
color: var(--text-secondary);
|
|
358
|
+
line-height: 1.6;
|
|
359
|
+
margin-bottom: 12px;
|
|
360
|
+
white-space: pre-wrap;
|
|
361
|
+
word-break: break-word;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
.entry-content.truncated {
|
|
365
|
+
display: -webkit-box;
|
|
366
|
+
-webkit-line-clamp: 3;
|
|
367
|
+
-webkit-box-orient: vertical;
|
|
368
|
+
overflow: hidden;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
.entry-footer {
|
|
372
|
+
display: flex;
|
|
373
|
+
justify-content: space-between;
|
|
374
|
+
align-items: center;
|
|
375
|
+
flex-wrap: wrap;
|
|
376
|
+
gap: 8px;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
.entry-tags { display: flex; gap: 6px; flex-wrap: wrap; }
|
|
380
|
+
|
|
381
|
+
.tag {
|
|
382
|
+
font-size: 11px;
|
|
383
|
+
padding: 3px 8px;
|
|
384
|
+
background: rgba(59, 130, 246, 0.2);
|
|
385
|
+
color: var(--accent);
|
|
386
|
+
border-radius: 4px;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
.entry-date { font-size: 12px; color: var(--text-muted); }
|
|
390
|
+
|
|
391
|
+
.empty-state { text-align: center; padding: 60px 20px; color: var(--text-secondary); }
|
|
392
|
+
.empty-state svg { width: 64px; height: 64px; fill: var(--text-muted); margin-bottom: 16px; }
|
|
393
|
+
.empty-state h3 { font-size: 18px; margin-bottom: 8px; color: var(--text-primary); }
|
|
394
|
+
|
|
395
|
+
.loading { display: flex; justify-content: center; padding: 40px; }
|
|
396
|
+
|
|
397
|
+
.spinner {
|
|
398
|
+
width: 32px; height: 32px;
|
|
399
|
+
border: 3px solid var(--border);
|
|
400
|
+
border-top-color: var(--accent);
|
|
401
|
+
border-radius: 50%;
|
|
402
|
+
animation: spin 0.8s linear infinite;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
@keyframes spin { to { transform: rotate(360deg); } }
|
|
406
|
+
|
|
407
|
+
.namespace-pills { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 24px; }
|
|
408
|
+
|
|
409
|
+
.namespace-pill {
|
|
410
|
+
padding: 8px 16px;
|
|
411
|
+
background: var(--bg-secondary);
|
|
412
|
+
border: 1px solid var(--border);
|
|
413
|
+
border-radius: 20px;
|
|
414
|
+
font-size: 13px;
|
|
415
|
+
color: var(--text-secondary);
|
|
416
|
+
cursor: pointer;
|
|
417
|
+
transition: all 0.2s;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
.namespace-pill:hover { border-color: var(--accent); color: var(--text-primary); }
|
|
421
|
+
.namespace-pill.active { background: var(--accent); border-color: var(--accent); color: white; }
|
|
422
|
+
.namespace-pill .count { margin-left: 6px; font-size: 11px; opacity: 0.7; }
|
|
423
|
+
|
|
424
|
+
.modal-overlay {
|
|
425
|
+
display: none;
|
|
426
|
+
position: fixed;
|
|
427
|
+
inset: 0;
|
|
428
|
+
background: rgba(0, 0, 0, 0.7);
|
|
429
|
+
z-index: 100;
|
|
430
|
+
align-items: center;
|
|
431
|
+
justify-content: center;
|
|
432
|
+
padding: 24px;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
.modal-overlay.active { display: flex; }
|
|
436
|
+
|
|
437
|
+
.modal {
|
|
438
|
+
background: var(--bg-secondary);
|
|
439
|
+
border-radius: 16px;
|
|
440
|
+
max-width: 700px;
|
|
441
|
+
width: 100%;
|
|
442
|
+
max-height: 90vh;
|
|
443
|
+
overflow: hidden;
|
|
444
|
+
display: flex;
|
|
445
|
+
flex-direction: column;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
.modal-header {
|
|
449
|
+
display: flex;
|
|
450
|
+
justify-content: space-between;
|
|
451
|
+
align-items: center;
|
|
452
|
+
padding: 20px 24px;
|
|
453
|
+
border-bottom: 1px solid var(--border);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
.modal-title { font-size: 18px; font-weight: 600; }
|
|
457
|
+
|
|
458
|
+
.modal-close {
|
|
459
|
+
background: none;
|
|
460
|
+
border: none;
|
|
461
|
+
color: var(--text-secondary);
|
|
462
|
+
cursor: pointer;
|
|
463
|
+
padding: 8px;
|
|
464
|
+
border-radius: 6px;
|
|
465
|
+
transition: background 0.2s;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
.modal-close:hover { background: var(--bg-card); }
|
|
469
|
+
.modal-close svg { width: 20px; height: 20px; fill: currentColor; }
|
|
470
|
+
|
|
471
|
+
.modal-body { padding: 24px; overflow-y: auto; }
|
|
472
|
+
|
|
473
|
+
.detail-row { margin-bottom: 20px; }
|
|
474
|
+
|
|
475
|
+
.detail-label {
|
|
476
|
+
font-size: 12px;
|
|
477
|
+
color: var(--text-muted);
|
|
478
|
+
text-transform: uppercase;
|
|
479
|
+
letter-spacing: 0.5px;
|
|
480
|
+
margin-bottom: 6px;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
.detail-value {
|
|
484
|
+
font-size: 14px;
|
|
485
|
+
color: var(--text-primary);
|
|
486
|
+
white-space: pre-wrap;
|
|
487
|
+
word-break: break-word;
|
|
488
|
+
line-height: 1.6;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
.detail-value.content {
|
|
492
|
+
background: var(--bg-card);
|
|
493
|
+
padding: 16px;
|
|
494
|
+
border-radius: 8px;
|
|
495
|
+
max-height: 200px;
|
|
496
|
+
overflow-y: auto;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
.pagination { display: flex; justify-content: center; gap: 8px; margin-top: 24px; }
|
|
500
|
+
|
|
501
|
+
.btn {
|
|
502
|
+
padding: 10px 18px;
|
|
503
|
+
background: var(--bg-secondary);
|
|
504
|
+
border: 1px solid var(--border);
|
|
505
|
+
border-radius: 8px;
|
|
506
|
+
color: var(--text-primary);
|
|
507
|
+
cursor: pointer;
|
|
508
|
+
font-size: 14px;
|
|
509
|
+
transition: all 0.2s;
|
|
510
|
+
display: inline-flex;
|
|
511
|
+
align-items: center;
|
|
512
|
+
gap: 8px;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
.btn:hover:not(:disabled) { border-color: var(--accent); }
|
|
516
|
+
.btn:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
517
|
+
|
|
518
|
+
.btn-primary { background: var(--accent); border-color: var(--accent); color: white; }
|
|
519
|
+
.btn-primary:hover { background: var(--accent-hover); border-color: var(--accent-hover); }
|
|
520
|
+
|
|
521
|
+
.btn-danger { background: var(--error); border-color: var(--error); color: white; }
|
|
522
|
+
.btn-danger:hover { background: #DC2626; border-color: #DC2626; }
|
|
523
|
+
|
|
524
|
+
.btn-success { background: var(--success); border-color: var(--success); color: white; }
|
|
525
|
+
.btn-success:hover { background: #16A34A; border-color: #16A34A; }
|
|
526
|
+
|
|
527
|
+
.btn svg { width: 16px; height: 16px; fill: currentColor; }
|
|
528
|
+
|
|
529
|
+
.form-group { margin-bottom: 20px; }
|
|
530
|
+
|
|
531
|
+
.form-group label {
|
|
532
|
+
display: block;
|
|
533
|
+
font-size: 13px;
|
|
534
|
+
color: var(--text-secondary);
|
|
535
|
+
margin-bottom: 8px;
|
|
536
|
+
text-transform: uppercase;
|
|
537
|
+
letter-spacing: 0.5px;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
.form-group input,
|
|
541
|
+
.form-group textarea,
|
|
542
|
+
.form-group select {
|
|
543
|
+
width: 100%;
|
|
544
|
+
padding: 12px 16px;
|
|
545
|
+
background: var(--bg-card);
|
|
546
|
+
border: 1px solid var(--border);
|
|
547
|
+
border-radius: 8px;
|
|
548
|
+
color: var(--text-primary);
|
|
549
|
+
font-size: 14px;
|
|
550
|
+
font-family: inherit;
|
|
551
|
+
transition: border-color 0.2s;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
.form-group input:focus,
|
|
555
|
+
.form-group textarea:focus,
|
|
556
|
+
.form-group select:focus {
|
|
557
|
+
outline: none;
|
|
558
|
+
border-color: var(--accent);
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
.form-group textarea { min-height: 120px; resize: vertical; }
|
|
562
|
+
|
|
563
|
+
.modal-footer {
|
|
564
|
+
display: flex;
|
|
565
|
+
justify-content: flex-end;
|
|
566
|
+
gap: 12px;
|
|
567
|
+
padding: 16px 24px;
|
|
568
|
+
border-top: 1px solid var(--border);
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
.detail-actions {
|
|
572
|
+
display: flex;
|
|
573
|
+
gap: 12px;
|
|
574
|
+
margin-top: 24px;
|
|
575
|
+
padding-top: 20px;
|
|
576
|
+
border-top: 1px solid var(--border);
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
.toast {
|
|
580
|
+
position: fixed;
|
|
581
|
+
bottom: 24px;
|
|
582
|
+
right: 24px;
|
|
583
|
+
padding: 16px 24px;
|
|
584
|
+
background: var(--bg-card);
|
|
585
|
+
border: 1px solid var(--border);
|
|
586
|
+
border-radius: 8px;
|
|
587
|
+
color: var(--text-primary);
|
|
588
|
+
font-size: 14px;
|
|
589
|
+
z-index: 200;
|
|
590
|
+
animation: slideIn 0.3s ease;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
.toast.success { border-color: var(--success); background: rgba(34, 197, 94, 0.1); }
|
|
594
|
+
.toast.error { border-color: var(--error); background: rgba(239, 68, 68, 0.1); }
|
|
595
|
+
|
|
596
|
+
@keyframes slideIn {
|
|
597
|
+
from { transform: translateX(100%); opacity: 0; }
|
|
598
|
+
to { transform: translateX(0); opacity: 1; }
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
@media (max-width: 768px) {
|
|
602
|
+
.container { padding: 16px; }
|
|
603
|
+
header { flex-direction: column; align-items: flex-start; }
|
|
604
|
+
.stats-grid { grid-template-columns: repeat(2, 1fr); }
|
|
605
|
+
.controls { flex-direction: column; }
|
|
606
|
+
.search-box { min-width: 100%; }
|
|
607
|
+
}
|
|
608
|
+
</style>
|
|
609
|
+
</head>
|
|
610
|
+
<body>
|
|
611
|
+
<div class="container">
|
|
612
|
+
<header>
|
|
613
|
+
<div class="logo">
|
|
614
|
+
<div class="logo-icon">
|
|
615
|
+
<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>
|
|
616
|
+
</div>
|
|
617
|
+
<div>
|
|
618
|
+
<h1>Memory Viewer</h1>
|
|
619
|
+
<p class="subtitle">AgentKits Memory Database</p>
|
|
620
|
+
</div>
|
|
621
|
+
</div>
|
|
622
|
+
<div class="header-actions">
|
|
623
|
+
<button class="btn btn-primary" onclick="openAddModal()">
|
|
624
|
+
<svg viewBox="0 0 24 24"><path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/></svg>
|
|
625
|
+
Add Memory
|
|
626
|
+
</button>
|
|
627
|
+
<button class="btn" onclick="loadData()">
|
|
628
|
+
<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>
|
|
629
|
+
Refresh
|
|
630
|
+
</button>
|
|
631
|
+
</div>
|
|
632
|
+
</header>
|
|
633
|
+
|
|
634
|
+
<div id="stats-container" class="stats-grid"></div>
|
|
635
|
+
<div id="namespace-pills" class="namespace-pills"></div>
|
|
636
|
+
|
|
637
|
+
<div class="controls">
|
|
638
|
+
<div class="search-box">
|
|
639
|
+
<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>
|
|
640
|
+
<input type="text" id="search-input" placeholder="Search memories..." oninput="debounceSearch()">
|
|
641
|
+
</div>
|
|
642
|
+
</div>
|
|
643
|
+
|
|
644
|
+
<div id="entries-container" class="entries-list">
|
|
645
|
+
<div class="loading"><div class="spinner"></div></div>
|
|
646
|
+
</div>
|
|
647
|
+
|
|
648
|
+
<div id="pagination" class="pagination"></div>
|
|
649
|
+
</div>
|
|
650
|
+
|
|
651
|
+
<!-- Detail Modal -->
|
|
652
|
+
<div id="detail-modal" class="modal-overlay" onclick="closeDetailModal(event)">
|
|
653
|
+
<div class="modal" onclick="event.stopPropagation()">
|
|
654
|
+
<div class="modal-header">
|
|
655
|
+
<span class="modal-title">Memory Details</span>
|
|
656
|
+
<button class="modal-close" onclick="closeDetailModal()">
|
|
657
|
+
<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>
|
|
658
|
+
</button>
|
|
659
|
+
</div>
|
|
660
|
+
<div class="modal-body" id="detail-body"></div>
|
|
661
|
+
</div>
|
|
662
|
+
</div>
|
|
663
|
+
|
|
664
|
+
<!-- Add/Edit Modal -->
|
|
665
|
+
<div id="form-modal" class="modal-overlay" onclick="closeFormModal(event)">
|
|
666
|
+
<div class="modal" onclick="event.stopPropagation()">
|
|
667
|
+
<div class="modal-header">
|
|
668
|
+
<span class="modal-title" id="form-title">Add Memory</span>
|
|
669
|
+
<button class="modal-close" onclick="closeFormModal()">
|
|
670
|
+
<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>
|
|
671
|
+
</button>
|
|
672
|
+
</div>
|
|
673
|
+
<div class="modal-body">
|
|
674
|
+
<input type="hidden" id="form-id">
|
|
675
|
+
<div class="form-group">
|
|
676
|
+
<label for="form-key">Key</label>
|
|
677
|
+
<input type="text" id="form-key" placeholder="e.g., auth-pattern, api-design">
|
|
678
|
+
</div>
|
|
679
|
+
<div class="form-group">
|
|
680
|
+
<label for="form-namespace">Namespace</label>
|
|
681
|
+
<select id="form-namespace">
|
|
682
|
+
<option value="patterns">patterns</option>
|
|
683
|
+
<option value="decisions">decisions</option>
|
|
684
|
+
<option value="errors">errors</option>
|
|
685
|
+
<option value="context">context</option>
|
|
686
|
+
<option value="active-context">active-context</option>
|
|
687
|
+
<option value="session-state">session-state</option>
|
|
688
|
+
<option value="progress">progress</option>
|
|
689
|
+
<option value="general">general</option>
|
|
690
|
+
</select>
|
|
691
|
+
</div>
|
|
692
|
+
<div class="form-group">
|
|
693
|
+
<label for="form-type">Type</label>
|
|
694
|
+
<select id="form-type">
|
|
695
|
+
<option value="semantic">semantic</option>
|
|
696
|
+
<option value="episodic">episodic</option>
|
|
697
|
+
<option value="procedural">procedural</option>
|
|
698
|
+
</select>
|
|
699
|
+
</div>
|
|
700
|
+
<div class="form-group">
|
|
701
|
+
<label for="form-content">Content</label>
|
|
702
|
+
<textarea id="form-content" placeholder="Enter the memory content..."></textarea>
|
|
703
|
+
</div>
|
|
704
|
+
<div class="form-group">
|
|
705
|
+
<label for="form-tags">Tags (comma-separated)</label>
|
|
706
|
+
<input type="text" id="form-tags" placeholder="e.g., auth, security, api">
|
|
707
|
+
</div>
|
|
708
|
+
</div>
|
|
709
|
+
<div class="modal-footer">
|
|
710
|
+
<button class="btn" onclick="closeFormModal()">Cancel</button>
|
|
711
|
+
<button class="btn btn-primary" onclick="saveEntry()">Save</button>
|
|
712
|
+
</div>
|
|
713
|
+
</div>
|
|
714
|
+
</div>
|
|
715
|
+
|
|
716
|
+
<!-- Delete Confirmation Modal -->
|
|
717
|
+
<div id="delete-modal" class="modal-overlay" onclick="closeDeleteModal(event)">
|
|
718
|
+
<div class="modal" style="max-width: 400px;" onclick="event.stopPropagation()">
|
|
719
|
+
<div class="modal-header">
|
|
720
|
+
<span class="modal-title">Delete Memory</span>
|
|
721
|
+
<button class="modal-close" onclick="closeDeleteModal()">
|
|
722
|
+
<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>
|
|
723
|
+
</button>
|
|
724
|
+
</div>
|
|
725
|
+
<div class="modal-body">
|
|
726
|
+
<p>Are you sure you want to delete this memory? This action cannot be undone.</p>
|
|
727
|
+
<input type="hidden" id="delete-id">
|
|
728
|
+
</div>
|
|
729
|
+
<div class="modal-footer">
|
|
730
|
+
<button class="btn" onclick="closeDeleteModal()">Cancel</button>
|
|
731
|
+
<button class="btn btn-danger" onclick="confirmDelete()">Delete</button>
|
|
732
|
+
</div>
|
|
733
|
+
</div>
|
|
734
|
+
</div>
|
|
735
|
+
|
|
736
|
+
<script>
|
|
737
|
+
let currentNamespace = '';
|
|
738
|
+
let currentSearch = '';
|
|
739
|
+
let currentPage = 0;
|
|
740
|
+
const pageSize = 20;
|
|
741
|
+
let stats = { total: 0, byNamespace: {}, byType: {} };
|
|
742
|
+
let debounceTimer = null;
|
|
743
|
+
|
|
744
|
+
async function loadData() {
|
|
745
|
+
try {
|
|
746
|
+
const statsRes = await fetch('/api/stats');
|
|
747
|
+
stats = await statsRes.json();
|
|
748
|
+
renderStats();
|
|
749
|
+
renderNamespacePills();
|
|
750
|
+
await loadEntries();
|
|
751
|
+
} catch (error) {
|
|
752
|
+
console.error('Failed to load data:', error);
|
|
753
|
+
showToast('Failed to load data', 'error');
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
function renderStats() {
|
|
758
|
+
const container = document.getElementById('stats-container');
|
|
759
|
+
container.innerHTML = \`
|
|
760
|
+
<div class="stat-card">
|
|
761
|
+
<div class="stat-label">Total Memories</div>
|
|
762
|
+
<div class="stat-value">\${stats.total || 0}</div>
|
|
763
|
+
</div>
|
|
764
|
+
<div class="stat-card">
|
|
765
|
+
<div class="stat-label">Namespaces</div>
|
|
766
|
+
<div class="stat-value">\${Object.keys(stats.byNamespace || {}).length}</div>
|
|
767
|
+
</div>
|
|
768
|
+
<div class="stat-card">
|
|
769
|
+
<div class="stat-label">Types</div>
|
|
770
|
+
<div class="stat-value">\${Object.keys(stats.byType || {}).length}</div>
|
|
771
|
+
</div>
|
|
772
|
+
\`;
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
function renderNamespacePills() {
|
|
776
|
+
const container = document.getElementById('namespace-pills');
|
|
777
|
+
const pills = ['<span class="namespace-pill' + (currentNamespace === '' ? ' active' : '') + '" onclick="filterNamespace(\\'\\')">All<span class="count">' + (stats.total || 0) + '</span></span>'];
|
|
778
|
+
|
|
779
|
+
for (const [ns, count] of Object.entries(stats.byNamespace || {})) {
|
|
780
|
+
pills.push(\`<span class="namespace-pill\${currentNamespace === ns ? ' active' : ''}" onclick="filterNamespace('\${ns}')">\${ns}<span class="count">\${count}</span></span>\`);
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
container.innerHTML = pills.join('');
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
async function loadEntries() {
|
|
787
|
+
const container = document.getElementById('entries-container');
|
|
788
|
+
container.innerHTML = '<div class="loading"><div class="spinner"></div></div>';
|
|
789
|
+
|
|
790
|
+
const params = new URLSearchParams({ limit: pageSize, offset: currentPage * pageSize });
|
|
791
|
+
if (currentNamespace) params.set('namespace', currentNamespace);
|
|
792
|
+
if (currentSearch) params.set('search', currentSearch);
|
|
793
|
+
|
|
794
|
+
try {
|
|
795
|
+
const res = await fetch('/api/entries?' + params);
|
|
796
|
+
const entries = await res.json();
|
|
797
|
+
|
|
798
|
+
if (!Array.isArray(entries) || entries.length === 0) {
|
|
799
|
+
container.innerHTML = \`
|
|
800
|
+
<div class="empty-state">
|
|
801
|
+
<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>
|
|
802
|
+
<h3>No memories found</h3>
|
|
803
|
+
<p>Click "Add Memory" to create your first entry</p>
|
|
804
|
+
</div>
|
|
805
|
+
\`;
|
|
806
|
+
} else {
|
|
807
|
+
container.innerHTML = entries.map(entry => \`
|
|
808
|
+
<div class="entry-card" onclick="showDetail('\${entry.id}')">
|
|
809
|
+
<div class="entry-header">
|
|
810
|
+
<span class="entry-key">\${escapeHtml(entry.key)}</span>
|
|
811
|
+
<span class="entry-namespace">\${entry.namespace}</span>
|
|
812
|
+
</div>
|
|
813
|
+
<div class="entry-content truncated">\${escapeHtml(entry.content)}</div>
|
|
814
|
+
<div class="entry-footer">
|
|
815
|
+
<div class="entry-tags">
|
|
816
|
+
\${(entry.tags || []).map(tag => \`<span class="tag">\${escapeHtml(tag)}</span>\`).join('')}
|
|
817
|
+
</div>
|
|
818
|
+
<span class="entry-date">\${formatDate(entry.created_at)}</span>
|
|
819
|
+
</div>
|
|
820
|
+
</div>
|
|
821
|
+
\`).join('');
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
renderPagination(entries.length);
|
|
825
|
+
} catch (error) {
|
|
826
|
+
container.innerHTML = '<div class="empty-state"><h3>No memories yet</h3><p>Click "Add Memory" to get started</p></div>';
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
function renderPagination(currentCount) {
|
|
831
|
+
const container = document.getElementById('pagination');
|
|
832
|
+
const hasMore = currentCount === pageSize;
|
|
833
|
+
const hasPrev = currentPage > 0;
|
|
834
|
+
|
|
835
|
+
container.innerHTML = \`
|
|
836
|
+
<button class="btn" \${!hasPrev ? 'disabled' : ''} onclick="prevPage()">Previous</button>
|
|
837
|
+
<button class="btn" \${!hasMore ? 'disabled' : ''} onclick="nextPage()">Next</button>
|
|
838
|
+
\`;
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
function filterNamespace(ns) {
|
|
842
|
+
currentNamespace = ns;
|
|
843
|
+
currentPage = 0;
|
|
844
|
+
renderNamespacePills();
|
|
845
|
+
loadEntries();
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
function debounceSearch() {
|
|
849
|
+
clearTimeout(debounceTimer);
|
|
850
|
+
debounceTimer = setTimeout(() => {
|
|
851
|
+
currentSearch = document.getElementById('search-input').value;
|
|
852
|
+
currentPage = 0;
|
|
853
|
+
loadEntries();
|
|
854
|
+
}, 300);
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
function prevPage() { if (currentPage > 0) { currentPage--; loadEntries(); } }
|
|
858
|
+
function nextPage() { currentPage++; loadEntries(); }
|
|
859
|
+
|
|
860
|
+
async function showDetail(id) {
|
|
861
|
+
try {
|
|
862
|
+
const res = await fetch('/api/entry/' + id);
|
|
863
|
+
const entry = await res.json();
|
|
864
|
+
|
|
865
|
+
document.getElementById('detail-body').innerHTML = \`
|
|
866
|
+
<div class="detail-row">
|
|
867
|
+
<div class="detail-label">Key</div>
|
|
868
|
+
<div class="detail-value">\${escapeHtml(entry.key)}</div>
|
|
869
|
+
</div>
|
|
870
|
+
<div class="detail-row">
|
|
871
|
+
<div class="detail-label">Namespace</div>
|
|
872
|
+
<div class="detail-value">\${entry.namespace}</div>
|
|
873
|
+
</div>
|
|
874
|
+
<div class="detail-row">
|
|
875
|
+
<div class="detail-label">Type</div>
|
|
876
|
+
<div class="detail-value">\${entry.type}</div>
|
|
877
|
+
</div>
|
|
878
|
+
<div class="detail-row">
|
|
879
|
+
<div class="detail-label">Content</div>
|
|
880
|
+
<div class="detail-value content">\${escapeHtml(entry.content)}</div>
|
|
881
|
+
</div>
|
|
882
|
+
<div class="detail-row">
|
|
883
|
+
<div class="detail-label">Tags</div>
|
|
884
|
+
<div class="detail-value">\${(entry.tags || []).join(', ') || 'None'}</div>
|
|
885
|
+
</div>
|
|
886
|
+
<div class="detail-row">
|
|
887
|
+
<div class="detail-label">Created</div>
|
|
888
|
+
<div class="detail-value">\${new Date(entry.created_at).toLocaleString()}</div>
|
|
889
|
+
</div>
|
|
890
|
+
<div class="detail-actions">
|
|
891
|
+
<button class="btn btn-primary" onclick="openEditModal('\${entry.id}')">
|
|
892
|
+
<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>
|
|
893
|
+
Edit
|
|
894
|
+
</button>
|
|
895
|
+
<button class="btn btn-danger" onclick="openDeleteModal('\${entry.id}')">
|
|
896
|
+
<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>
|
|
897
|
+
Delete
|
|
898
|
+
</button>
|
|
899
|
+
</div>
|
|
900
|
+
\`;
|
|
901
|
+
|
|
902
|
+
document.getElementById('detail-modal').classList.add('active');
|
|
903
|
+
} catch (error) {
|
|
904
|
+
showToast('Failed to load entry', 'error');
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
function closeDetailModal(event) {
|
|
909
|
+
if (!event || event.target.id === 'detail-modal') {
|
|
910
|
+
document.getElementById('detail-modal').classList.remove('active');
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
function openAddModal() {
|
|
915
|
+
document.getElementById('form-title').textContent = 'Add Memory';
|
|
916
|
+
document.getElementById('form-id').value = '';
|
|
917
|
+
document.getElementById('form-key').value = '';
|
|
918
|
+
document.getElementById('form-namespace').value = 'patterns';
|
|
919
|
+
document.getElementById('form-type').value = 'semantic';
|
|
920
|
+
document.getElementById('form-content').value = '';
|
|
921
|
+
document.getElementById('form-tags').value = '';
|
|
922
|
+
document.getElementById('form-modal').classList.add('active');
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
async function openEditModal(id) {
|
|
926
|
+
closeDetailModal();
|
|
927
|
+
const res = await fetch('/api/entry/' + id);
|
|
928
|
+
const entry = await res.json();
|
|
929
|
+
|
|
930
|
+
document.getElementById('form-title').textContent = 'Edit Memory';
|
|
931
|
+
document.getElementById('form-id').value = entry.id;
|
|
932
|
+
document.getElementById('form-key').value = entry.key;
|
|
933
|
+
document.getElementById('form-namespace').value = entry.namespace;
|
|
934
|
+
document.getElementById('form-type').value = entry.type;
|
|
935
|
+
document.getElementById('form-content').value = entry.content;
|
|
936
|
+
document.getElementById('form-tags').value = (entry.tags || []).join(', ');
|
|
937
|
+
document.getElementById('form-modal').classList.add('active');
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
function closeFormModal(event) {
|
|
941
|
+
if (!event || event.target.id === 'form-modal') {
|
|
942
|
+
document.getElementById('form-modal').classList.remove('active');
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
async function saveEntry() {
|
|
947
|
+
const id = document.getElementById('form-id').value;
|
|
948
|
+
const data = {
|
|
949
|
+
key: document.getElementById('form-key').value.trim(),
|
|
950
|
+
namespace: document.getElementById('form-namespace').value,
|
|
951
|
+
type: document.getElementById('form-type').value,
|
|
952
|
+
content: document.getElementById('form-content').value.trim(),
|
|
953
|
+
tags: document.getElementById('form-tags').value.split(',').map(t => t.trim()).filter(Boolean),
|
|
954
|
+
};
|
|
955
|
+
|
|
956
|
+
if (!data.key || !data.content) {
|
|
957
|
+
showToast('Key and Content are required', 'error');
|
|
958
|
+
return;
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
try {
|
|
962
|
+
const method = id ? 'PUT' : 'POST';
|
|
963
|
+
const url = id ? '/api/entry/' + id : '/api/entries';
|
|
964
|
+
const res = await fetch(url, {
|
|
965
|
+
method,
|
|
966
|
+
headers: { 'Content-Type': 'application/json' },
|
|
967
|
+
body: JSON.stringify(data),
|
|
968
|
+
});
|
|
969
|
+
|
|
970
|
+
if (res.ok) {
|
|
971
|
+
closeFormModal();
|
|
972
|
+
showToast(id ? 'Memory updated' : 'Memory created', 'success');
|
|
973
|
+
loadData();
|
|
974
|
+
} else {
|
|
975
|
+
const err = await res.json();
|
|
976
|
+
showToast(err.error || 'Failed to save', 'error');
|
|
977
|
+
}
|
|
978
|
+
} catch (error) {
|
|
979
|
+
showToast('Failed to save', 'error');
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
function openDeleteModal(id) {
|
|
984
|
+
closeDetailModal();
|
|
985
|
+
document.getElementById('delete-id').value = id;
|
|
986
|
+
document.getElementById('delete-modal').classList.add('active');
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
function closeDeleteModal(event) {
|
|
990
|
+
if (!event || event.target.id === 'delete-modal') {
|
|
991
|
+
document.getElementById('delete-modal').classList.remove('active');
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
async function confirmDelete() {
|
|
996
|
+
const id = document.getElementById('delete-id').value;
|
|
997
|
+
try {
|
|
998
|
+
const res = await fetch('/api/entry/' + id, { method: 'DELETE' });
|
|
999
|
+
if (res.ok) {
|
|
1000
|
+
closeDeleteModal();
|
|
1001
|
+
showToast('Memory deleted', 'success');
|
|
1002
|
+
loadData();
|
|
1003
|
+
} else {
|
|
1004
|
+
showToast('Failed to delete', 'error');
|
|
1005
|
+
}
|
|
1006
|
+
} catch (error) {
|
|
1007
|
+
showToast('Failed to delete', 'error');
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
function showToast(message, type = 'success') {
|
|
1012
|
+
const toast = document.createElement('div');
|
|
1013
|
+
toast.className = 'toast ' + type;
|
|
1014
|
+
toast.textContent = message;
|
|
1015
|
+
document.body.appendChild(toast);
|
|
1016
|
+
setTimeout(() => toast.remove(), 3000);
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
function escapeHtml(text) {
|
|
1020
|
+
if (!text) return '';
|
|
1021
|
+
const div = document.createElement('div');
|
|
1022
|
+
div.textContent = text;
|
|
1023
|
+
return div.innerHTML;
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
function formatDate(timestamp) {
|
|
1027
|
+
if (!timestamp) return 'Unknown';
|
|
1028
|
+
const date = new Date(timestamp);
|
|
1029
|
+
const now = new Date();
|
|
1030
|
+
const diff = now - date;
|
|
1031
|
+
|
|
1032
|
+
if (diff < 60000) return 'Just now';
|
|
1033
|
+
if (diff < 3600000) return Math.floor(diff / 60000) + 'm ago';
|
|
1034
|
+
if (diff < 86400000) return Math.floor(diff / 3600000) + 'h ago';
|
|
1035
|
+
if (diff < 604800000) return Math.floor(diff / 86400000) + 'd ago';
|
|
1036
|
+
|
|
1037
|
+
return date.toLocaleDateString();
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
document.addEventListener('keydown', (e) => {
|
|
1041
|
+
if (e.key === 'Escape') {
|
|
1042
|
+
closeDetailModal();
|
|
1043
|
+
closeFormModal();
|
|
1044
|
+
closeDeleteModal();
|
|
1045
|
+
}
|
|
1046
|
+
});
|
|
1047
|
+
|
|
1048
|
+
loadData();
|
|
1049
|
+
</script>
|
|
1050
|
+
</body>
|
|
1051
|
+
</html>`;
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
async function readBody(req: http.IncomingMessage): Promise<string> {
|
|
1055
|
+
return new Promise((resolve, reject) => {
|
|
1056
|
+
let body = '';
|
|
1057
|
+
req.on('data', chunk => body += chunk);
|
|
1058
|
+
req.on('end', () => resolve(body));
|
|
1059
|
+
req.on('error', reject);
|
|
1060
|
+
});
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
async function handleRequest(
|
|
1064
|
+
req: http.IncomingMessage,
|
|
1065
|
+
res: http.ServerResponse
|
|
1066
|
+
): Promise<void> {
|
|
1067
|
+
const url = new URL(req.url || '/', `http://localhost:${PORT}`);
|
|
1068
|
+
const method = req.method || 'GET';
|
|
1069
|
+
|
|
1070
|
+
res.setHeader('Content-Type', 'application/json');
|
|
1071
|
+
|
|
1072
|
+
try {
|
|
1073
|
+
const db = await loadOrCreateDatabase();
|
|
1074
|
+
|
|
1075
|
+
// Serve HTML
|
|
1076
|
+
if (url.pathname === '/' && method === 'GET') {
|
|
1077
|
+
res.setHeader('Content-Type', 'text/html');
|
|
1078
|
+
res.writeHead(200);
|
|
1079
|
+
res.end(getHTML());
|
|
1080
|
+
db.close();
|
|
1081
|
+
return;
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
// GET stats
|
|
1085
|
+
if (url.pathname === '/api/stats' && method === 'GET') {
|
|
1086
|
+
const stats = getStats(db);
|
|
1087
|
+
res.writeHead(200);
|
|
1088
|
+
res.end(JSON.stringify(stats));
|
|
1089
|
+
db.close();
|
|
1090
|
+
return;
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
// GET entries
|
|
1094
|
+
if (url.pathname === '/api/entries' && method === 'GET') {
|
|
1095
|
+
const namespace = url.searchParams.get('namespace') || undefined;
|
|
1096
|
+
const limit = parseInt(url.searchParams.get('limit') || '50', 10);
|
|
1097
|
+
const offset = parseInt(url.searchParams.get('offset') || '0', 10);
|
|
1098
|
+
const search = url.searchParams.get('search') || undefined;
|
|
1099
|
+
|
|
1100
|
+
const entries = getEntries(db, namespace, limit, offset, search);
|
|
1101
|
+
res.writeHead(200);
|
|
1102
|
+
res.end(JSON.stringify(entries));
|
|
1103
|
+
db.close();
|
|
1104
|
+
return;
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
// POST create entry
|
|
1108
|
+
if (url.pathname === '/api/entries' && method === 'POST') {
|
|
1109
|
+
const body = await readBody(req);
|
|
1110
|
+
const data = JSON.parse(body);
|
|
1111
|
+
const now = Date.now();
|
|
1112
|
+
const id = generateId();
|
|
1113
|
+
|
|
1114
|
+
db.run(
|
|
1115
|
+
`INSERT INTO memory_entries (id, key, content, type, namespace, tags, created_at, updated_at)
|
|
1116
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
1117
|
+
[id, data.key, data.content, data.type || 'semantic', data.namespace || 'general', JSON.stringify(data.tags || []), now, now]
|
|
1118
|
+
);
|
|
1119
|
+
|
|
1120
|
+
saveDatabase(db);
|
|
1121
|
+
res.writeHead(201);
|
|
1122
|
+
res.end(JSON.stringify({ id, success: true }));
|
|
1123
|
+
db.close();
|
|
1124
|
+
return;
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
// GET single entry
|
|
1128
|
+
if (url.pathname.startsWith('/api/entry/') && method === 'GET') {
|
|
1129
|
+
const id = url.pathname.split('/').pop();
|
|
1130
|
+
const stmt = db.prepare('SELECT * FROM memory_entries WHERE id = ?');
|
|
1131
|
+
stmt.bind([id]);
|
|
1132
|
+
|
|
1133
|
+
if (stmt.step()) {
|
|
1134
|
+
const row = stmt.getAsObject();
|
|
1135
|
+
res.writeHead(200);
|
|
1136
|
+
res.end(JSON.stringify({
|
|
1137
|
+
id: row.id,
|
|
1138
|
+
key: row.key,
|
|
1139
|
+
content: row.content,
|
|
1140
|
+
type: row.type,
|
|
1141
|
+
namespace: row.namespace,
|
|
1142
|
+
tags: JSON.parse((row.tags as string) || '[]'),
|
|
1143
|
+
created_at: row.created_at,
|
|
1144
|
+
updated_at: row.updated_at,
|
|
1145
|
+
}));
|
|
1146
|
+
} else {
|
|
1147
|
+
res.writeHead(404);
|
|
1148
|
+
res.end(JSON.stringify({ error: 'Entry not found' }));
|
|
1149
|
+
}
|
|
1150
|
+
stmt.free();
|
|
1151
|
+
db.close();
|
|
1152
|
+
return;
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
// PUT update entry
|
|
1156
|
+
if (url.pathname.startsWith('/api/entry/') && method === 'PUT') {
|
|
1157
|
+
const id = url.pathname.split('/').pop();
|
|
1158
|
+
const body = await readBody(req);
|
|
1159
|
+
const data = JSON.parse(body);
|
|
1160
|
+
const now = Date.now();
|
|
1161
|
+
|
|
1162
|
+
db.run(
|
|
1163
|
+
`UPDATE memory_entries SET key = ?, content = ?, type = ?, namespace = ?, tags = ?, updated_at = ?
|
|
1164
|
+
WHERE id = ?`,
|
|
1165
|
+
[data.key, data.content, data.type, data.namespace, JSON.stringify(data.tags || []), now, id]
|
|
1166
|
+
);
|
|
1167
|
+
|
|
1168
|
+
saveDatabase(db);
|
|
1169
|
+
res.writeHead(200);
|
|
1170
|
+
res.end(JSON.stringify({ success: true }));
|
|
1171
|
+
db.close();
|
|
1172
|
+
return;
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
// DELETE entry
|
|
1176
|
+
if (url.pathname.startsWith('/api/entry/') && method === 'DELETE') {
|
|
1177
|
+
const id = url.pathname.split('/').pop();
|
|
1178
|
+
db.run('DELETE FROM memory_entries WHERE id = ?', [id]);
|
|
1179
|
+
saveDatabase(db);
|
|
1180
|
+
res.writeHead(200);
|
|
1181
|
+
res.end(JSON.stringify({ success: true }));
|
|
1182
|
+
db.close();
|
|
1183
|
+
return;
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
res.writeHead(404);
|
|
1187
|
+
res.end(JSON.stringify({ error: 'Not found' }));
|
|
1188
|
+
db.close();
|
|
1189
|
+
} catch (error) {
|
|
1190
|
+
res.writeHead(500);
|
|
1191
|
+
res.end(JSON.stringify({ error: error instanceof Error ? error.message : 'Internal error' }));
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
const server = http.createServer(handleRequest);
|
|
1196
|
+
|
|
1197
|
+
server.listen(PORT, () => {
|
|
1198
|
+
console.log(`\n AgentKits Memory Viewer\n`);
|
|
1199
|
+
console.log(` Local: http://localhost:${PORT}`);
|
|
1200
|
+
console.log(` Database: ${dbPath}\n`);
|
|
1201
|
+
console.log(` Press Ctrl+C to stop\n`);
|
|
1202
|
+
});
|