claude-session-continuity-mcp 1.17.0 → 1.17.2
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/README.md +14 -6
- package/dist/hooks/install.js +0 -0
- package/dist/hooks/post-tool-use.js +0 -0
- package/dist/hooks/pre-compact.js +0 -0
- package/dist/hooks/session-end.js +0 -0
- package/dist/hooks/session-start.js +0 -0
- package/dist/hooks/user-prompt-submit.js +0 -0
- package/dist/index.js +24 -2
- package/dist/schemas.d.ts +54 -54
- package/package.json +2 -4
- package/dist/dashboard-v2.d.ts +0 -11
- package/dist/dashboard-v2.js +0 -1321
- package/dist/dashboard.d.ts +0 -2
- package/dist/dashboard.js +0 -1196
- package/dist/index-old.d.ts +0 -2
- package/dist/index-old.js +0 -3426
- package/dist/index-v3.d.ts +0 -16
- package/dist/index-v3.js +0 -1024
package/dist/dashboard-v2.js
DELETED
|
@@ -1,1321 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* Project Manager MCP Dashboard v2
|
|
4
|
-
* Modern dashboard with:
|
|
5
|
-
* - Tailwind-inspired design (CSS-only, no build step)
|
|
6
|
-
* - Real-time updates
|
|
7
|
-
* - Project timeline view
|
|
8
|
-
* - Memory graph visualization
|
|
9
|
-
* - Context snapshot viewer
|
|
10
|
-
*/
|
|
11
|
-
import * as http from 'http';
|
|
12
|
-
import * as url from 'url';
|
|
13
|
-
import Database from 'better-sqlite3';
|
|
14
|
-
import * as path from 'path';
|
|
15
|
-
const WORKSPACE_ROOT = process.env.WORKSPACE_ROOT || '/Users/ibyeongchang/Documents/dev/ai-service-generator';
|
|
16
|
-
const DB_PATH = path.join(WORKSPACE_ROOT, '.claude', 'sessions.db');
|
|
17
|
-
const PORT = parseInt(process.env.PORT || '8000');
|
|
18
|
-
const db = new Database(DB_PATH);
|
|
19
|
-
function getStats() {
|
|
20
|
-
const memoriesCount = db.prepare('SELECT COUNT(*) as count FROM memories').get().count;
|
|
21
|
-
const sessionsCount = db.prepare('SELECT COUNT(*) as count FROM sessions').get().count;
|
|
22
|
-
const relationsCount = db.prepare('SELECT COUNT(*) as count FROM memory_relations').get().count;
|
|
23
|
-
const patternsCount = db.prepare('SELECT COUNT(*) as count FROM work_patterns').get().count;
|
|
24
|
-
let embeddingsCount = 0;
|
|
25
|
-
try {
|
|
26
|
-
embeddingsCount = db.prepare('SELECT COUNT(*) as count FROM embeddings').get().count;
|
|
27
|
-
}
|
|
28
|
-
catch { /* ignore */ }
|
|
29
|
-
const memoryTypes = db.prepare('SELECT memory_type, COUNT(*) as count FROM memories GROUP BY memory_type').all();
|
|
30
|
-
const projects = db.prepare('SELECT DISTINCT project FROM memories WHERE project IS NOT NULL UNION SELECT DISTINCT project FROM sessions WHERE project IS NOT NULL').all();
|
|
31
|
-
// 최근 활동
|
|
32
|
-
const recentActivity = db.prepare(`
|
|
33
|
-
SELECT 'memory' as type, content, created_at as timestamp, project
|
|
34
|
-
FROM memories
|
|
35
|
-
ORDER BY created_at DESC
|
|
36
|
-
LIMIT 5
|
|
37
|
-
`).all();
|
|
38
|
-
return {
|
|
39
|
-
memories: memoriesCount,
|
|
40
|
-
sessions: sessionsCount,
|
|
41
|
-
relations: relationsCount,
|
|
42
|
-
patterns: patternsCount,
|
|
43
|
-
embeddings: embeddingsCount,
|
|
44
|
-
embeddingCoverage: memoriesCount > 0 ? Math.round((embeddingsCount / memoriesCount) * 100) : 100,
|
|
45
|
-
projects,
|
|
46
|
-
memoryTypes,
|
|
47
|
-
recentActivity
|
|
48
|
-
};
|
|
49
|
-
}
|
|
50
|
-
function getProjectContext(project) {
|
|
51
|
-
try {
|
|
52
|
-
const projectContext = db.prepare('SELECT * FROM project_context WHERE project = ?').get(project);
|
|
53
|
-
const activeContext = db.prepare('SELECT * FROM active_context WHERE project = ?').get(project);
|
|
54
|
-
const tasks = db.prepare(`
|
|
55
|
-
SELECT id, title, status, priority
|
|
56
|
-
FROM tasks
|
|
57
|
-
WHERE project = ? AND status IN ('pending', 'in_progress')
|
|
58
|
-
ORDER BY priority DESC, created_at DESC
|
|
59
|
-
LIMIT 5
|
|
60
|
-
`).all(project);
|
|
61
|
-
return {
|
|
62
|
-
project,
|
|
63
|
-
fixed: {
|
|
64
|
-
techStack: projectContext?.tech_stack ? JSON.parse(projectContext.tech_stack) : {},
|
|
65
|
-
architectureDecisions: projectContext?.architecture_decisions ? JSON.parse(projectContext.architecture_decisions) : [],
|
|
66
|
-
codePatterns: projectContext?.code_patterns ? JSON.parse(projectContext.code_patterns) : [],
|
|
67
|
-
specialNotes: projectContext?.special_notes || null
|
|
68
|
-
},
|
|
69
|
-
active: {
|
|
70
|
-
currentState: activeContext?.current_state || 'No active context',
|
|
71
|
-
recentFiles: activeContext?.recent_files ? JSON.parse(activeContext.recent_files) : [],
|
|
72
|
-
blockers: activeContext?.blockers || null,
|
|
73
|
-
lastVerification: activeContext?.last_verification || null,
|
|
74
|
-
updatedAt: activeContext?.updated_at || null
|
|
75
|
-
},
|
|
76
|
-
pendingTasks: tasks
|
|
77
|
-
};
|
|
78
|
-
}
|
|
79
|
-
catch {
|
|
80
|
-
return null;
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
function getMemories(params) {
|
|
84
|
-
const type = params.get('type');
|
|
85
|
-
const project = params.get('project');
|
|
86
|
-
const search = params.get('search');
|
|
87
|
-
const limit = parseInt(params.get('limit') || '50');
|
|
88
|
-
let sql = 'SELECT * FROM memories WHERE 1=1';
|
|
89
|
-
const sqlParams = [];
|
|
90
|
-
if (type) {
|
|
91
|
-
sql += ' AND memory_type = ?';
|
|
92
|
-
sqlParams.push(type);
|
|
93
|
-
}
|
|
94
|
-
if (project) {
|
|
95
|
-
sql += ' AND project = ?';
|
|
96
|
-
sqlParams.push(project);
|
|
97
|
-
}
|
|
98
|
-
if (search) {
|
|
99
|
-
sql += ' AND (content LIKE ? OR tags LIKE ?)';
|
|
100
|
-
sqlParams.push(`%${search}%`, `%${search}%`);
|
|
101
|
-
}
|
|
102
|
-
sql += ' ORDER BY created_at DESC LIMIT ?';
|
|
103
|
-
sqlParams.push(limit);
|
|
104
|
-
return db.prepare(sql).all(...sqlParams);
|
|
105
|
-
}
|
|
106
|
-
function getMemory(id) {
|
|
107
|
-
return db.prepare('SELECT * FROM memories WHERE id = ?').get(id);
|
|
108
|
-
}
|
|
109
|
-
function updateMemory(id, data) {
|
|
110
|
-
const updates = [];
|
|
111
|
-
const params = [];
|
|
112
|
-
if (data.content !== undefined) {
|
|
113
|
-
updates.push('content = ?');
|
|
114
|
-
params.push(data.content);
|
|
115
|
-
}
|
|
116
|
-
if (data.tags !== undefined) {
|
|
117
|
-
updates.push('tags = ?');
|
|
118
|
-
params.push(JSON.stringify(data.tags));
|
|
119
|
-
}
|
|
120
|
-
if (data.importance !== undefined) {
|
|
121
|
-
updates.push('importance = ?');
|
|
122
|
-
params.push(data.importance);
|
|
123
|
-
}
|
|
124
|
-
if (data.memory_type !== undefined) {
|
|
125
|
-
updates.push('memory_type = ?');
|
|
126
|
-
params.push(data.memory_type);
|
|
127
|
-
}
|
|
128
|
-
if (updates.length === 0)
|
|
129
|
-
return { success: false, message: 'No updates' };
|
|
130
|
-
params.push(id);
|
|
131
|
-
const result = db.prepare(`UPDATE memories SET ${updates.join(', ')} WHERE id = ?`).run(...params);
|
|
132
|
-
return { success: result.changes > 0 };
|
|
133
|
-
}
|
|
134
|
-
function deleteMemoryById(id) {
|
|
135
|
-
const result = db.prepare('DELETE FROM memories WHERE id = ?').run(id);
|
|
136
|
-
return { success: result.changes > 0 };
|
|
137
|
-
}
|
|
138
|
-
function getTimeline(project) {
|
|
139
|
-
let sql = `
|
|
140
|
-
SELECT
|
|
141
|
-
'memory' as event_type,
|
|
142
|
-
id,
|
|
143
|
-
content as title,
|
|
144
|
-
memory_type as subtype,
|
|
145
|
-
created_at as timestamp,
|
|
146
|
-
project
|
|
147
|
-
FROM memories
|
|
148
|
-
${project ? 'WHERE project = ?' : ''}
|
|
149
|
-
UNION ALL
|
|
150
|
-
SELECT
|
|
151
|
-
'session' as event_type,
|
|
152
|
-
id,
|
|
153
|
-
last_work as title,
|
|
154
|
-
current_status as subtype,
|
|
155
|
-
timestamp,
|
|
156
|
-
project
|
|
157
|
-
FROM sessions
|
|
158
|
-
${project ? 'WHERE project = ?' : ''}
|
|
159
|
-
ORDER BY timestamp DESC
|
|
160
|
-
LIMIT 100
|
|
161
|
-
`;
|
|
162
|
-
return project
|
|
163
|
-
? db.prepare(sql).all(project, project)
|
|
164
|
-
: db.prepare(sql).all();
|
|
165
|
-
}
|
|
166
|
-
function getRelations(memoryId) {
|
|
167
|
-
if (memoryId) {
|
|
168
|
-
return db.prepare(`
|
|
169
|
-
SELECT r.*,
|
|
170
|
-
s.content as source_content, s.memory_type as source_type,
|
|
171
|
-
t.content as target_content, t.memory_type as target_type
|
|
172
|
-
FROM memory_relations r
|
|
173
|
-
JOIN memories s ON r.source_id = s.id
|
|
174
|
-
JOIN memories t ON r.target_id = t.id
|
|
175
|
-
WHERE r.source_id = ? OR r.target_id = ?
|
|
176
|
-
`).all(memoryId, memoryId);
|
|
177
|
-
}
|
|
178
|
-
return db.prepare(`
|
|
179
|
-
SELECT r.*,
|
|
180
|
-
s.content as source_content, s.memory_type as source_type,
|
|
181
|
-
t.content as target_content, t.memory_type as target_type
|
|
182
|
-
FROM memory_relations r
|
|
183
|
-
JOIN memories s ON r.source_id = s.id
|
|
184
|
-
JOIN memories t ON r.target_id = t.id
|
|
185
|
-
LIMIT 100
|
|
186
|
-
`).all();
|
|
187
|
-
}
|
|
188
|
-
// ===== HTML 템플릿 (Modern Design) =====
|
|
189
|
-
const HTML_TEMPLATE = `<!DOCTYPE html>
|
|
190
|
-
<html lang="en" class="dark">
|
|
191
|
-
<head>
|
|
192
|
-
<meta charset="UTF-8">
|
|
193
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
194
|
-
<title>Project Manager MCP</title>
|
|
195
|
-
<style>
|
|
196
|
-
/* ===== Reset & Variables ===== */
|
|
197
|
-
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
198
|
-
|
|
199
|
-
:root {
|
|
200
|
-
/* Colors - Slate palette (not purple gradient!) */
|
|
201
|
-
--bg-primary: #0f172a;
|
|
202
|
-
--bg-secondary: #1e293b;
|
|
203
|
-
--bg-tertiary: #334155;
|
|
204
|
-
--bg-hover: #475569;
|
|
205
|
-
|
|
206
|
-
--text-primary: #f1f5f9;
|
|
207
|
-
--text-secondary: #94a3b8;
|
|
208
|
-
--text-muted: #64748b;
|
|
209
|
-
|
|
210
|
-
--accent-primary: #0ea5e9; /* Sky blue */
|
|
211
|
-
--accent-success: #22c55e; /* Green */
|
|
212
|
-
--accent-warning: #f59e0b; /* Amber */
|
|
213
|
-
--accent-error: #ef4444; /* Red */
|
|
214
|
-
--accent-purple: #8b5cf6; /* Purple for learning */
|
|
215
|
-
|
|
216
|
-
/* Spacing */
|
|
217
|
-
--space-1: 4px;
|
|
218
|
-
--space-2: 8px;
|
|
219
|
-
--space-3: 12px;
|
|
220
|
-
--space-4: 16px;
|
|
221
|
-
--space-5: 20px;
|
|
222
|
-
--space-6: 24px;
|
|
223
|
-
--space-8: 32px;
|
|
224
|
-
|
|
225
|
-
/* Border radius */
|
|
226
|
-
--radius-sm: 6px;
|
|
227
|
-
--radius-md: 8px;
|
|
228
|
-
--radius-lg: 12px;
|
|
229
|
-
--radius-xl: 16px;
|
|
230
|
-
|
|
231
|
-
/* Font */
|
|
232
|
-
--font-sans: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
233
|
-
--font-mono: 'JetBrains Mono', 'Fira Code', Consolas, monospace;
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
body {
|
|
237
|
-
font-family: var(--font-sans);
|
|
238
|
-
background: var(--bg-primary);
|
|
239
|
-
color: var(--text-primary);
|
|
240
|
-
line-height: 1.6;
|
|
241
|
-
min-height: 100vh;
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
/* ===== Layout ===== */
|
|
245
|
-
.app {
|
|
246
|
-
display: grid;
|
|
247
|
-
grid-template-columns: 280px 1fr;
|
|
248
|
-
min-height: 100vh;
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
.sidebar {
|
|
252
|
-
background: var(--bg-secondary);
|
|
253
|
-
border-right: 1px solid var(--bg-tertiary);
|
|
254
|
-
padding: var(--space-6);
|
|
255
|
-
position: sticky;
|
|
256
|
-
top: 0;
|
|
257
|
-
height: 100vh;
|
|
258
|
-
overflow-y: auto;
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
.main {
|
|
262
|
-
padding: var(--space-6);
|
|
263
|
-
overflow-y: auto;
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
/* ===== Sidebar ===== */
|
|
267
|
-
.logo {
|
|
268
|
-
display: flex;
|
|
269
|
-
align-items: center;
|
|
270
|
-
gap: var(--space-3);
|
|
271
|
-
margin-bottom: var(--space-8);
|
|
272
|
-
font-size: 1.25rem;
|
|
273
|
-
font-weight: 600;
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
.logo-icon {
|
|
277
|
-
width: 40px;
|
|
278
|
-
height: 40px;
|
|
279
|
-
background: linear-gradient(135deg, var(--accent-primary), var(--accent-purple));
|
|
280
|
-
border-radius: var(--radius-md);
|
|
281
|
-
display: flex;
|
|
282
|
-
align-items: center;
|
|
283
|
-
justify-content: center;
|
|
284
|
-
font-size: 1.5rem;
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
.nav { list-style: none; }
|
|
288
|
-
|
|
289
|
-
.nav-item {
|
|
290
|
-
display: flex;
|
|
291
|
-
align-items: center;
|
|
292
|
-
gap: var(--space-3);
|
|
293
|
-
padding: var(--space-3) var(--space-4);
|
|
294
|
-
border-radius: var(--radius-md);
|
|
295
|
-
color: var(--text-secondary);
|
|
296
|
-
cursor: pointer;
|
|
297
|
-
transition: all 0.2s;
|
|
298
|
-
margin-bottom: var(--space-1);
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
.nav-item:hover { background: var(--bg-tertiary); color: var(--text-primary); }
|
|
302
|
-
.nav-item.active { background: var(--accent-primary); color: white; }
|
|
303
|
-
|
|
304
|
-
.nav-section {
|
|
305
|
-
margin-top: var(--space-6);
|
|
306
|
-
margin-bottom: var(--space-2);
|
|
307
|
-
font-size: 0.75rem;
|
|
308
|
-
font-weight: 500;
|
|
309
|
-
color: var(--text-muted);
|
|
310
|
-
text-transform: uppercase;
|
|
311
|
-
letter-spacing: 0.05em;
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
/* ===== Stats Cards ===== */
|
|
315
|
-
.stats-grid {
|
|
316
|
-
display: grid;
|
|
317
|
-
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
318
|
-
gap: var(--space-4);
|
|
319
|
-
margin-bottom: var(--space-6);
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
.stat-card {
|
|
323
|
-
background: var(--bg-secondary);
|
|
324
|
-
border: 1px solid var(--bg-tertiary);
|
|
325
|
-
border-radius: var(--radius-lg);
|
|
326
|
-
padding: var(--space-5);
|
|
327
|
-
transition: transform 0.2s, box-shadow 0.2s;
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
.stat-card:hover {
|
|
331
|
-
transform: translateY(-2px);
|
|
332
|
-
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
.stat-label {
|
|
336
|
-
font-size: 0.875rem;
|
|
337
|
-
color: var(--text-secondary);
|
|
338
|
-
margin-bottom: var(--space-2);
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
.stat-value {
|
|
342
|
-
font-size: 2rem;
|
|
343
|
-
font-weight: 700;
|
|
344
|
-
color: var(--accent-primary);
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
.stat-change {
|
|
348
|
-
font-size: 0.75rem;
|
|
349
|
-
color: var(--accent-success);
|
|
350
|
-
margin-top: var(--space-1);
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
/* ===== Cards ===== */
|
|
354
|
-
.card {
|
|
355
|
-
background: var(--bg-secondary);
|
|
356
|
-
border: 1px solid var(--bg-tertiary);
|
|
357
|
-
border-radius: var(--radius-lg);
|
|
358
|
-
overflow: hidden;
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
.card-header {
|
|
362
|
-
padding: var(--space-4) var(--space-5);
|
|
363
|
-
border-bottom: 1px solid var(--bg-tertiary);
|
|
364
|
-
display: flex;
|
|
365
|
-
align-items: center;
|
|
366
|
-
justify-content: space-between;
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
.card-title {
|
|
370
|
-
font-size: 1rem;
|
|
371
|
-
font-weight: 600;
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
.card-body { padding: var(--space-5); }
|
|
375
|
-
|
|
376
|
-
/* ===== Table ===== */
|
|
377
|
-
.table-container { overflow-x: auto; }
|
|
378
|
-
|
|
379
|
-
table { width: 100%; border-collapse: collapse; }
|
|
380
|
-
|
|
381
|
-
th, td {
|
|
382
|
-
padding: var(--space-3) var(--space-4);
|
|
383
|
-
text-align: left;
|
|
384
|
-
border-bottom: 1px solid var(--bg-tertiary);
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
th {
|
|
388
|
-
font-size: 0.75rem;
|
|
389
|
-
font-weight: 500;
|
|
390
|
-
color: var(--text-muted);
|
|
391
|
-
text-transform: uppercase;
|
|
392
|
-
letter-spacing: 0.05em;
|
|
393
|
-
background: var(--bg-primary);
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
tr:hover td { background: var(--bg-tertiary); }
|
|
397
|
-
|
|
398
|
-
/* ===== Tags/Badges ===== */
|
|
399
|
-
.badge {
|
|
400
|
-
display: inline-flex;
|
|
401
|
-
align-items: center;
|
|
402
|
-
padding: var(--space-1) var(--space-2);
|
|
403
|
-
border-radius: 9999px;
|
|
404
|
-
font-size: 0.75rem;
|
|
405
|
-
font-weight: 500;
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
.badge-observation { background: rgba(14, 165, 233, 0.2); color: var(--accent-primary); }
|
|
409
|
-
.badge-decision { background: rgba(245, 158, 11, 0.2); color: var(--accent-warning); }
|
|
410
|
-
.badge-learning { background: rgba(139, 92, 246, 0.2); color: var(--accent-purple); }
|
|
411
|
-
.badge-error { background: rgba(239, 68, 68, 0.2); color: var(--accent-error); }
|
|
412
|
-
.badge-pattern { background: rgba(34, 197, 94, 0.2); color: var(--accent-success); }
|
|
413
|
-
.badge-preference { background: rgba(236, 72, 153, 0.2); color: #ec4899; }
|
|
414
|
-
|
|
415
|
-
.importance {
|
|
416
|
-
display: inline-flex;
|
|
417
|
-
align-items: center;
|
|
418
|
-
gap: var(--space-1);
|
|
419
|
-
padding: var(--space-1) var(--space-2);
|
|
420
|
-
background: var(--bg-tertiary);
|
|
421
|
-
border-radius: var(--radius-sm);
|
|
422
|
-
font-size: 0.75rem;
|
|
423
|
-
}
|
|
424
|
-
.importance.high { background: rgba(239, 68, 68, 0.2); color: var(--accent-error); }
|
|
425
|
-
.importance.medium { background: rgba(245, 158, 11, 0.2); color: var(--accent-warning); }
|
|
426
|
-
|
|
427
|
-
/* ===== Buttons ===== */
|
|
428
|
-
.btn {
|
|
429
|
-
display: inline-flex;
|
|
430
|
-
align-items: center;
|
|
431
|
-
gap: var(--space-2);
|
|
432
|
-
padding: var(--space-2) var(--space-4);
|
|
433
|
-
border: none;
|
|
434
|
-
border-radius: var(--radius-md);
|
|
435
|
-
font-size: 0.875rem;
|
|
436
|
-
font-weight: 500;
|
|
437
|
-
cursor: pointer;
|
|
438
|
-
transition: all 0.2s;
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
.btn-primary { background: var(--accent-primary); color: white; }
|
|
442
|
-
.btn-primary:hover { background: #0284c7; }
|
|
443
|
-
|
|
444
|
-
.btn-ghost { background: transparent; color: var(--text-secondary); }
|
|
445
|
-
.btn-ghost:hover { background: var(--bg-tertiary); color: var(--text-primary); }
|
|
446
|
-
|
|
447
|
-
.btn-danger { background: var(--accent-error); color: white; }
|
|
448
|
-
.btn-danger:hover { background: #dc2626; }
|
|
449
|
-
|
|
450
|
-
/* ===== Form Elements ===== */
|
|
451
|
-
.input, .select {
|
|
452
|
-
padding: var(--space-2) var(--space-3);
|
|
453
|
-
background: var(--bg-primary);
|
|
454
|
-
border: 1px solid var(--bg-tertiary);
|
|
455
|
-
border-radius: var(--radius-md);
|
|
456
|
-
color: var(--text-primary);
|
|
457
|
-
font-size: 0.875rem;
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
.input:focus, .select:focus {
|
|
461
|
-
outline: none;
|
|
462
|
-
border-color: var(--accent-primary);
|
|
463
|
-
box-shadow: 0 0 0 3px rgba(14, 165, 233, 0.2);
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
.search-box {
|
|
467
|
-
display: flex;
|
|
468
|
-
gap: var(--space-2);
|
|
469
|
-
margin-bottom: var(--space-4);
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
.search-box .input { flex: 1; }
|
|
473
|
-
|
|
474
|
-
/* ===== Timeline ===== */
|
|
475
|
-
.timeline {
|
|
476
|
-
position: relative;
|
|
477
|
-
padding-left: var(--space-6);
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
.timeline::before {
|
|
481
|
-
content: '';
|
|
482
|
-
position: absolute;
|
|
483
|
-
left: 8px;
|
|
484
|
-
top: 0;
|
|
485
|
-
bottom: 0;
|
|
486
|
-
width: 2px;
|
|
487
|
-
background: var(--bg-tertiary);
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
.timeline-item {
|
|
491
|
-
position: relative;
|
|
492
|
-
padding-bottom: var(--space-5);
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
.timeline-item::before {
|
|
496
|
-
content: '';
|
|
497
|
-
position: absolute;
|
|
498
|
-
left: -22px;
|
|
499
|
-
top: 4px;
|
|
500
|
-
width: 12px;
|
|
501
|
-
height: 12px;
|
|
502
|
-
background: var(--accent-primary);
|
|
503
|
-
border-radius: 50%;
|
|
504
|
-
border: 2px solid var(--bg-secondary);
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
.timeline-item.memory::before { background: var(--accent-purple); }
|
|
508
|
-
.timeline-item.session::before { background: var(--accent-success); }
|
|
509
|
-
|
|
510
|
-
.timeline-content {
|
|
511
|
-
background: var(--bg-tertiary);
|
|
512
|
-
padding: var(--space-3) var(--space-4);
|
|
513
|
-
border-radius: var(--radius-md);
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
.timeline-time {
|
|
517
|
-
font-size: 0.75rem;
|
|
518
|
-
color: var(--text-muted);
|
|
519
|
-
margin-top: var(--space-1);
|
|
520
|
-
}
|
|
521
|
-
|
|
522
|
-
/* ===== Context View ===== */
|
|
523
|
-
.context-section {
|
|
524
|
-
margin-bottom: var(--space-5);
|
|
525
|
-
}
|
|
526
|
-
|
|
527
|
-
.context-section-title {
|
|
528
|
-
font-size: 0.875rem;
|
|
529
|
-
font-weight: 600;
|
|
530
|
-
color: var(--text-secondary);
|
|
531
|
-
margin-bottom: var(--space-2);
|
|
532
|
-
display: flex;
|
|
533
|
-
align-items: center;
|
|
534
|
-
gap: var(--space-2);
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
.context-list {
|
|
538
|
-
list-style: none;
|
|
539
|
-
}
|
|
540
|
-
|
|
541
|
-
.context-list li {
|
|
542
|
-
padding: var(--space-2) 0;
|
|
543
|
-
border-bottom: 1px solid var(--bg-tertiary);
|
|
544
|
-
font-size: 0.875rem;
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
.context-list li:last-child { border-bottom: none; }
|
|
548
|
-
|
|
549
|
-
.context-code {
|
|
550
|
-
font-family: var(--font-mono);
|
|
551
|
-
font-size: 0.8rem;
|
|
552
|
-
background: var(--bg-primary);
|
|
553
|
-
padding: var(--space-1) var(--space-2);
|
|
554
|
-
border-radius: var(--radius-sm);
|
|
555
|
-
}
|
|
556
|
-
|
|
557
|
-
/* ===== Modal ===== */
|
|
558
|
-
.modal-overlay {
|
|
559
|
-
display: none;
|
|
560
|
-
position: fixed;
|
|
561
|
-
inset: 0;
|
|
562
|
-
background: rgba(0,0,0,0.7);
|
|
563
|
-
backdrop-filter: blur(4px);
|
|
564
|
-
justify-content: center;
|
|
565
|
-
align-items: center;
|
|
566
|
-
z-index: 1000;
|
|
567
|
-
}
|
|
568
|
-
|
|
569
|
-
.modal-overlay.active { display: flex; }
|
|
570
|
-
|
|
571
|
-
.modal {
|
|
572
|
-
background: var(--bg-secondary);
|
|
573
|
-
border-radius: var(--radius-xl);
|
|
574
|
-
width: 90%;
|
|
575
|
-
max-width: 600px;
|
|
576
|
-
max-height: 80vh;
|
|
577
|
-
overflow-y: auto;
|
|
578
|
-
}
|
|
579
|
-
|
|
580
|
-
.modal-header {
|
|
581
|
-
padding: var(--space-5);
|
|
582
|
-
border-bottom: 1px solid var(--bg-tertiary);
|
|
583
|
-
display: flex;
|
|
584
|
-
align-items: center;
|
|
585
|
-
justify-content: space-between;
|
|
586
|
-
}
|
|
587
|
-
|
|
588
|
-
.modal-body { padding: var(--space-5); }
|
|
589
|
-
|
|
590
|
-
.modal-footer {
|
|
591
|
-
padding: var(--space-4) var(--space-5);
|
|
592
|
-
border-top: 1px solid var(--bg-tertiary);
|
|
593
|
-
display: flex;
|
|
594
|
-
justify-content: flex-end;
|
|
595
|
-
gap: var(--space-2);
|
|
596
|
-
}
|
|
597
|
-
|
|
598
|
-
/* ===== Toast ===== */
|
|
599
|
-
.toast {
|
|
600
|
-
position: fixed;
|
|
601
|
-
bottom: var(--space-5);
|
|
602
|
-
right: var(--space-5);
|
|
603
|
-
padding: var(--space-3) var(--space-5);
|
|
604
|
-
background: var(--accent-success);
|
|
605
|
-
color: white;
|
|
606
|
-
border-radius: var(--radius-md);
|
|
607
|
-
transform: translateY(100px);
|
|
608
|
-
opacity: 0;
|
|
609
|
-
transition: all 0.3s;
|
|
610
|
-
z-index: 1001;
|
|
611
|
-
}
|
|
612
|
-
|
|
613
|
-
.toast.show { transform: translateY(0); opacity: 1; }
|
|
614
|
-
.toast.error { background: var(--accent-error); }
|
|
615
|
-
|
|
616
|
-
/* ===== Utilities ===== */
|
|
617
|
-
.truncate {
|
|
618
|
-
overflow: hidden;
|
|
619
|
-
text-overflow: ellipsis;
|
|
620
|
-
white-space: nowrap;
|
|
621
|
-
max-width: 300px;
|
|
622
|
-
}
|
|
623
|
-
|
|
624
|
-
.empty-state {
|
|
625
|
-
text-align: center;
|
|
626
|
-
padding: var(--space-8);
|
|
627
|
-
color: var(--text-muted);
|
|
628
|
-
}
|
|
629
|
-
|
|
630
|
-
.empty-state-icon { font-size: 3rem; margin-bottom: var(--space-4); }
|
|
631
|
-
|
|
632
|
-
.grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: var(--space-4); }
|
|
633
|
-
|
|
634
|
-
/* ===== Responsive ===== */
|
|
635
|
-
@media (max-width: 768px) {
|
|
636
|
-
.app { grid-template-columns: 1fr; }
|
|
637
|
-
.sidebar {
|
|
638
|
-
position: fixed;
|
|
639
|
-
left: -280px;
|
|
640
|
-
transition: left 0.3s;
|
|
641
|
-
z-index: 100;
|
|
642
|
-
}
|
|
643
|
-
.sidebar.open { left: 0; }
|
|
644
|
-
.stats-grid { grid-template-columns: 1fr 1fr; }
|
|
645
|
-
.grid-2 { grid-template-columns: 1fr; }
|
|
646
|
-
}
|
|
647
|
-
</style>
|
|
648
|
-
</head>
|
|
649
|
-
<body>
|
|
650
|
-
<div class="app">
|
|
651
|
-
<!-- Sidebar -->
|
|
652
|
-
<aside class="sidebar">
|
|
653
|
-
<div class="logo">
|
|
654
|
-
<div class="logo-icon">🧠</div>
|
|
655
|
-
<span>MCP Dashboard</span>
|
|
656
|
-
</div>
|
|
657
|
-
|
|
658
|
-
<ul class="nav" id="nav">
|
|
659
|
-
<li class="nav-item active" data-view="overview">📊 Overview</li>
|
|
660
|
-
<li class="nav-item" data-view="memories">💾 Memories</li>
|
|
661
|
-
<li class="nav-item" data-view="timeline">📅 Timeline</li>
|
|
662
|
-
<li class="nav-item" data-view="context">🎯 Context</li>
|
|
663
|
-
<li class="nav-item" data-view="relations">🔗 Relations</li>
|
|
664
|
-
</ul>
|
|
665
|
-
|
|
666
|
-
<div class="nav-section">Projects</div>
|
|
667
|
-
<ul class="nav" id="project-nav"></ul>
|
|
668
|
-
</aside>
|
|
669
|
-
|
|
670
|
-
<!-- Main Content -->
|
|
671
|
-
<main class="main" id="main-content">
|
|
672
|
-
<!-- Content will be rendered here -->
|
|
673
|
-
</main>
|
|
674
|
-
</div>
|
|
675
|
-
|
|
676
|
-
<!-- Modal -->
|
|
677
|
-
<div class="modal-overlay" id="modal">
|
|
678
|
-
<div class="modal">
|
|
679
|
-
<div class="modal-header">
|
|
680
|
-
<h3 id="modal-title">Modal Title</h3>
|
|
681
|
-
<button class="btn btn-ghost" onclick="closeModal()">✕</button>
|
|
682
|
-
</div>
|
|
683
|
-
<div class="modal-body" id="modal-body"></div>
|
|
684
|
-
<div class="modal-footer" id="modal-footer"></div>
|
|
685
|
-
</div>
|
|
686
|
-
</div>
|
|
687
|
-
|
|
688
|
-
<!-- Toast -->
|
|
689
|
-
<div class="toast" id="toast"></div>
|
|
690
|
-
|
|
691
|
-
<script>
|
|
692
|
-
// State
|
|
693
|
-
let currentView = 'overview';
|
|
694
|
-
let currentProject = null;
|
|
695
|
-
let stats = {};
|
|
696
|
-
|
|
697
|
-
// API
|
|
698
|
-
async function api(endpoint, options = {}) {
|
|
699
|
-
const res = await fetch('/api' + endpoint, {
|
|
700
|
-
...options,
|
|
701
|
-
headers: { 'Content-Type': 'application/json', ...options.headers }
|
|
702
|
-
});
|
|
703
|
-
return res.json();
|
|
704
|
-
}
|
|
705
|
-
|
|
706
|
-
// Toast
|
|
707
|
-
function showToast(message, isError = false) {
|
|
708
|
-
const toast = document.getElementById('toast');
|
|
709
|
-
toast.textContent = message;
|
|
710
|
-
toast.className = 'toast show' + (isError ? ' error' : '');
|
|
711
|
-
setTimeout(() => toast.className = 'toast', 3000);
|
|
712
|
-
}
|
|
713
|
-
|
|
714
|
-
// Modal
|
|
715
|
-
function openModal(title, bodyHtml, footerHtml = '') {
|
|
716
|
-
document.getElementById('modal-title').textContent = title;
|
|
717
|
-
document.getElementById('modal-body').innerHTML = bodyHtml;
|
|
718
|
-
document.getElementById('modal-footer').innerHTML = footerHtml;
|
|
719
|
-
document.getElementById('modal').classList.add('active');
|
|
720
|
-
}
|
|
721
|
-
|
|
722
|
-
function closeModal() {
|
|
723
|
-
document.getElementById('modal').classList.remove('active');
|
|
724
|
-
}
|
|
725
|
-
|
|
726
|
-
// Navigation
|
|
727
|
-
document.querySelectorAll('.nav-item').forEach(item => {
|
|
728
|
-
item.addEventListener('click', () => {
|
|
729
|
-
document.querySelectorAll('.nav-item').forEach(i => i.classList.remove('active'));
|
|
730
|
-
item.classList.add('active');
|
|
731
|
-
currentView = item.dataset.view;
|
|
732
|
-
currentProject = item.dataset.project || null;
|
|
733
|
-
render();
|
|
734
|
-
});
|
|
735
|
-
});
|
|
736
|
-
|
|
737
|
-
// Render functions
|
|
738
|
-
async function render() {
|
|
739
|
-
const main = document.getElementById('main-content');
|
|
740
|
-
|
|
741
|
-
switch (currentView) {
|
|
742
|
-
case 'overview':
|
|
743
|
-
main.innerHTML = await renderOverview();
|
|
744
|
-
break;
|
|
745
|
-
case 'memories':
|
|
746
|
-
main.innerHTML = await renderMemories();
|
|
747
|
-
break;
|
|
748
|
-
case 'timeline':
|
|
749
|
-
main.innerHTML = await renderTimeline();
|
|
750
|
-
break;
|
|
751
|
-
case 'context':
|
|
752
|
-
main.innerHTML = await renderContext();
|
|
753
|
-
break;
|
|
754
|
-
case 'relations':
|
|
755
|
-
main.innerHTML = await renderRelations();
|
|
756
|
-
break;
|
|
757
|
-
}
|
|
758
|
-
}
|
|
759
|
-
|
|
760
|
-
async function renderOverview() {
|
|
761
|
-
stats = await api('/stats');
|
|
762
|
-
|
|
763
|
-
// Update project nav
|
|
764
|
-
const projectNav = document.getElementById('project-nav');
|
|
765
|
-
projectNav.innerHTML = stats.projects?.map(p => \`
|
|
766
|
-
<li class="nav-item" data-view="context" data-project="\${p.project}">
|
|
767
|
-
📁 \${p.project}
|
|
768
|
-
</li>
|
|
769
|
-
\`).join('') || '<li class="nav-item" style="opacity:0.5">No projects</li>';
|
|
770
|
-
|
|
771
|
-
// Re-bind events
|
|
772
|
-
projectNav.querySelectorAll('.nav-item').forEach(item => {
|
|
773
|
-
item.addEventListener('click', () => {
|
|
774
|
-
document.querySelectorAll('.nav-item').forEach(i => i.classList.remove('active'));
|
|
775
|
-
item.classList.add('active');
|
|
776
|
-
currentView = item.dataset.view;
|
|
777
|
-
currentProject = item.dataset.project;
|
|
778
|
-
render();
|
|
779
|
-
});
|
|
780
|
-
});
|
|
781
|
-
|
|
782
|
-
return \`
|
|
783
|
-
<h1 style="margin-bottom: var(--space-6)">Dashboard Overview</h1>
|
|
784
|
-
|
|
785
|
-
<div class="stats-grid">
|
|
786
|
-
<div class="stat-card">
|
|
787
|
-
<div class="stat-label">Total Memories</div>
|
|
788
|
-
<div class="stat-value">\${stats.memories}</div>
|
|
789
|
-
</div>
|
|
790
|
-
<div class="stat-card">
|
|
791
|
-
<div class="stat-label">Sessions</div>
|
|
792
|
-
<div class="stat-value">\${stats.sessions}</div>
|
|
793
|
-
</div>
|
|
794
|
-
<div class="stat-card">
|
|
795
|
-
<div class="stat-label">Relations</div>
|
|
796
|
-
<div class="stat-value">\${stats.relations}</div>
|
|
797
|
-
</div>
|
|
798
|
-
<div class="stat-card">
|
|
799
|
-
<div class="stat-label">Embeddings</div>
|
|
800
|
-
<div class="stat-value">\${stats.embeddings || 0}</div>
|
|
801
|
-
<div class="stat-change">\${stats.embeddingCoverage}% coverage</div>
|
|
802
|
-
</div>
|
|
803
|
-
</div>
|
|
804
|
-
|
|
805
|
-
<div class="grid-2">
|
|
806
|
-
<div class="card">
|
|
807
|
-
<div class="card-header">
|
|
808
|
-
<span class="card-title">Memory Types</span>
|
|
809
|
-
</div>
|
|
810
|
-
<div class="card-body">
|
|
811
|
-
\${stats.memoryTypes?.map(t => \`
|
|
812
|
-
<div style="display: flex; justify-content: space-between; margin-bottom: var(--space-2);">
|
|
813
|
-
<span class="badge badge-\${t.memory_type}">\${t.memory_type}</span>
|
|
814
|
-
<span>\${t.count}</span>
|
|
815
|
-
</div>
|
|
816
|
-
\`).join('') || 'No memories'}
|
|
817
|
-
</div>
|
|
818
|
-
</div>
|
|
819
|
-
|
|
820
|
-
<div class="card">
|
|
821
|
-
<div class="card-header">
|
|
822
|
-
<span class="card-title">Recent Activity</span>
|
|
823
|
-
</div>
|
|
824
|
-
<div class="card-body">
|
|
825
|
-
<div class="timeline">
|
|
826
|
-
\${stats.recentActivity?.slice(0, 5).map(a => \`
|
|
827
|
-
<div class="timeline-item \${a.type}">
|
|
828
|
-
<div class="timeline-content">
|
|
829
|
-
<div class="truncate">\${a.content?.substring(0, 100) || 'No content'}</div>
|
|
830
|
-
<div class="timeline-time">\${new Date(a.timestamp).toLocaleString()}</div>
|
|
831
|
-
</div>
|
|
832
|
-
</div>
|
|
833
|
-
\`).join('') || '<div class="empty-state">No recent activity</div>'}
|
|
834
|
-
</div>
|
|
835
|
-
</div>
|
|
836
|
-
</div>
|
|
837
|
-
</div>
|
|
838
|
-
\`;
|
|
839
|
-
}
|
|
840
|
-
|
|
841
|
-
async function renderMemories() {
|
|
842
|
-
const memories = await api('/memories?limit=50');
|
|
843
|
-
|
|
844
|
-
return \`
|
|
845
|
-
<h1 style="margin-bottom: var(--space-6)">Memories</h1>
|
|
846
|
-
|
|
847
|
-
<div class="search-box">
|
|
848
|
-
<input type="text" class="input" id="search" placeholder="Search memories..." onkeyup="debounce(searchMemories, 300)()">
|
|
849
|
-
<select class="select" id="type-filter" onchange="searchMemories()">
|
|
850
|
-
<option value="">All Types</option>
|
|
851
|
-
<option value="observation">Observation</option>
|
|
852
|
-
<option value="decision">Decision</option>
|
|
853
|
-
<option value="learning">Learning</option>
|
|
854
|
-
<option value="error">Error</option>
|
|
855
|
-
<option value="pattern">Pattern</option>
|
|
856
|
-
</select>
|
|
857
|
-
</div>
|
|
858
|
-
|
|
859
|
-
<div class="card">
|
|
860
|
-
<div class="table-container">
|
|
861
|
-
<table>
|
|
862
|
-
<thead>
|
|
863
|
-
<tr>
|
|
864
|
-
<th>ID</th>
|
|
865
|
-
<th>Type</th>
|
|
866
|
-
<th>Content</th>
|
|
867
|
-
<th>Project</th>
|
|
868
|
-
<th>Importance</th>
|
|
869
|
-
<th>Actions</th>
|
|
870
|
-
</tr>
|
|
871
|
-
</thead>
|
|
872
|
-
<tbody id="memories-tbody">
|
|
873
|
-
\${memories.length === 0 ? '<tr><td colspan="6" class="empty-state">No memories found</td></tr>' : ''}
|
|
874
|
-
\${memories.map(m => \`
|
|
875
|
-
<tr>
|
|
876
|
-
<td>\${m.id}</td>
|
|
877
|
-
<td><span class="badge badge-\${m.memory_type}">\${m.memory_type}</span></td>
|
|
878
|
-
<td class="truncate" title="\${m.content?.replace(/"/g, '"')}">\${m.content}</td>
|
|
879
|
-
<td>\${m.project || '-'}</td>
|
|
880
|
-
<td>
|
|
881
|
-
<span class="importance \${m.importance >= 8 ? 'high' : m.importance >= 5 ? 'medium' : ''}">
|
|
882
|
-
⭐ \${m.importance}
|
|
883
|
-
</span>
|
|
884
|
-
</td>
|
|
885
|
-
<td>
|
|
886
|
-
<button class="btn btn-ghost" onclick="viewMemory(\${m.id})">View</button>
|
|
887
|
-
<button class="btn btn-ghost" onclick="editMemory(\${m.id})">Edit</button>
|
|
888
|
-
<button class="btn btn-danger" onclick="deleteMemory(\${m.id})">×</button>
|
|
889
|
-
</td>
|
|
890
|
-
</tr>
|
|
891
|
-
\`).join('')}
|
|
892
|
-
</tbody>
|
|
893
|
-
</table>
|
|
894
|
-
</div>
|
|
895
|
-
</div>
|
|
896
|
-
\`;
|
|
897
|
-
}
|
|
898
|
-
|
|
899
|
-
async function renderTimeline() {
|
|
900
|
-
const timeline = await api('/timeline' + (currentProject ? '?project=' + currentProject : ''));
|
|
901
|
-
|
|
902
|
-
return \`
|
|
903
|
-
<h1 style="margin-bottom: var(--space-6)">Timeline \${currentProject ? '- ' + currentProject : ''}</h1>
|
|
904
|
-
|
|
905
|
-
<div class="card">
|
|
906
|
-
<div class="card-body">
|
|
907
|
-
<div class="timeline">
|
|
908
|
-
\${timeline.length === 0 ? '<div class="empty-state"><div class="empty-state-icon">📅</div>No timeline events</div>' : ''}
|
|
909
|
-
\${timeline.map(e => \`
|
|
910
|
-
<div class="timeline-item \${e.event_type}">
|
|
911
|
-
<div class="timeline-content">
|
|
912
|
-
<div style="display: flex; justify-content: space-between; margin-bottom: var(--space-1);">
|
|
913
|
-
<span class="badge badge-\${e.subtype || 'observation'}">\${e.subtype || e.event_type}</span>
|
|
914
|
-
<span style="font-size: 0.75rem; color: var(--text-muted)">\${e.project || ''}</span>
|
|
915
|
-
</div>
|
|
916
|
-
<div class="truncate">\${e.title}</div>
|
|
917
|
-
<div class="timeline-time">\${new Date(e.timestamp).toLocaleString()}</div>
|
|
918
|
-
</div>
|
|
919
|
-
</div>
|
|
920
|
-
\`).join('')}
|
|
921
|
-
</div>
|
|
922
|
-
</div>
|
|
923
|
-
</div>
|
|
924
|
-
\`;
|
|
925
|
-
}
|
|
926
|
-
|
|
927
|
-
async function renderContext() {
|
|
928
|
-
if (!currentProject) {
|
|
929
|
-
return \`
|
|
930
|
-
<h1 style="margin-bottom: var(--space-6)">Project Context</h1>
|
|
931
|
-
<div class="card">
|
|
932
|
-
<div class="card-body empty-state">
|
|
933
|
-
<div class="empty-state-icon">🎯</div>
|
|
934
|
-
<p>Select a project from the sidebar to view its context</p>
|
|
935
|
-
</div>
|
|
936
|
-
</div>
|
|
937
|
-
\`;
|
|
938
|
-
}
|
|
939
|
-
|
|
940
|
-
const context = await api('/context/' + currentProject);
|
|
941
|
-
|
|
942
|
-
if (!context) {
|
|
943
|
-
return \`
|
|
944
|
-
<h1 style="margin-bottom: var(--space-6)">Project Context - \${currentProject}</h1>
|
|
945
|
-
<div class="card">
|
|
946
|
-
<div class="card-body empty-state">
|
|
947
|
-
<div class="empty-state-icon">📭</div>
|
|
948
|
-
<p>No context found for this project</p>
|
|
949
|
-
</div>
|
|
950
|
-
</div>
|
|
951
|
-
\`;
|
|
952
|
-
}
|
|
953
|
-
|
|
954
|
-
return \`
|
|
955
|
-
<h1 style="margin-bottom: var(--space-6)">Project Context - \${currentProject}</h1>
|
|
956
|
-
|
|
957
|
-
<div class="grid-2">
|
|
958
|
-
<div class="card">
|
|
959
|
-
<div class="card-header">
|
|
960
|
-
<span class="card-title">📌 Fixed Context</span>
|
|
961
|
-
</div>
|
|
962
|
-
<div class="card-body">
|
|
963
|
-
<div class="context-section">
|
|
964
|
-
<div class="context-section-title">🛠 Tech Stack</div>
|
|
965
|
-
<ul class="context-list">
|
|
966
|
-
\${Object.entries(context.fixed?.techStack || {}).map(([k, v]) => \`
|
|
967
|
-
<li><strong>\${k}:</strong> \${v}</li>
|
|
968
|
-
\`).join('') || '<li>Not set</li>'}
|
|
969
|
-
</ul>
|
|
970
|
-
</div>
|
|
971
|
-
|
|
972
|
-
<div class="context-section">
|
|
973
|
-
<div class="context-section-title">🏗 Architecture Decisions</div>
|
|
974
|
-
<ul class="context-list">
|
|
975
|
-
\${context.fixed?.architectureDecisions?.map(d => \`<li>\${d}</li>\`).join('') || '<li>None</li>'}
|
|
976
|
-
</ul>
|
|
977
|
-
</div>
|
|
978
|
-
|
|
979
|
-
<div class="context-section">
|
|
980
|
-
<div class="context-section-title">📝 Code Patterns</div>
|
|
981
|
-
<ul class="context-list">
|
|
982
|
-
\${context.fixed?.codePatterns?.map(p => \`<li class="context-code">\${p}</li>\`).join('') || '<li>None</li>'}
|
|
983
|
-
</ul>
|
|
984
|
-
</div>
|
|
985
|
-
</div>
|
|
986
|
-
</div>
|
|
987
|
-
|
|
988
|
-
<div class="card">
|
|
989
|
-
<div class="card-header">
|
|
990
|
-
<span class="card-title">⚡ Active Context</span>
|
|
991
|
-
</div>
|
|
992
|
-
<div class="card-body">
|
|
993
|
-
<div class="context-section">
|
|
994
|
-
<div class="context-section-title">📍 Current State</div>
|
|
995
|
-
<p>\${context.active?.currentState || 'No active state'}</p>
|
|
996
|
-
</div>
|
|
997
|
-
|
|
998
|
-
<div class="context-section">
|
|
999
|
-
<div class="context-section-title">📁 Recent Files</div>
|
|
1000
|
-
<ul class="context-list">
|
|
1001
|
-
\${context.active?.recentFiles?.map(f => \`<li class="context-code">\${f}</li>\`).join('') || '<li>None</li>'}
|
|
1002
|
-
</ul>
|
|
1003
|
-
</div>
|
|
1004
|
-
|
|
1005
|
-
<div class="context-section">
|
|
1006
|
-
<div class="context-section-title">✅ Last Verification</div>
|
|
1007
|
-
<p>
|
|
1008
|
-
\${context.active?.lastVerification
|
|
1009
|
-
? \`<span class="badge badge-\${context.active.lastVerification === 'passed' ? 'pattern' : 'error'}">\${context.active.lastVerification}</span>\`
|
|
1010
|
-
: 'Not run'}
|
|
1011
|
-
</p>
|
|
1012
|
-
</div>
|
|
1013
|
-
|
|
1014
|
-
\${context.active?.blockers ? \`
|
|
1015
|
-
<div class="context-section">
|
|
1016
|
-
<div class="context-section-title" style="color: var(--accent-error)">🚫 Blockers</div>
|
|
1017
|
-
<p>\${context.active.blockers}</p>
|
|
1018
|
-
</div>
|
|
1019
|
-
\` : ''}
|
|
1020
|
-
</div>
|
|
1021
|
-
</div>
|
|
1022
|
-
</div>
|
|
1023
|
-
|
|
1024
|
-
\${context.pendingTasks?.length > 0 ? \`
|
|
1025
|
-
<div class="card" style="margin-top: var(--space-4)">
|
|
1026
|
-
<div class="card-header">
|
|
1027
|
-
<span class="card-title">📋 Pending Tasks</span>
|
|
1028
|
-
</div>
|
|
1029
|
-
<div class="table-container">
|
|
1030
|
-
<table>
|
|
1031
|
-
<thead>
|
|
1032
|
-
<tr>
|
|
1033
|
-
<th>ID</th>
|
|
1034
|
-
<th>Title</th>
|
|
1035
|
-
<th>Status</th>
|
|
1036
|
-
<th>Priority</th>
|
|
1037
|
-
</tr>
|
|
1038
|
-
</thead>
|
|
1039
|
-
<tbody>
|
|
1040
|
-
\${context.pendingTasks.map(t => \`
|
|
1041
|
-
<tr>
|
|
1042
|
-
<td>\${t.id}</td>
|
|
1043
|
-
<td>\${t.title}</td>
|
|
1044
|
-
<td><span class="badge badge-\${t.status === 'in_progress' ? 'learning' : 'observation'}">\${t.status}</span></td>
|
|
1045
|
-
<td><span class="importance \${t.priority >= 8 ? 'high' : t.priority >= 5 ? 'medium' : ''}">P\${t.priority}</span></td>
|
|
1046
|
-
</tr>
|
|
1047
|
-
\`).join('')}
|
|
1048
|
-
</tbody>
|
|
1049
|
-
</table>
|
|
1050
|
-
</div>
|
|
1051
|
-
</div>
|
|
1052
|
-
\` : ''}
|
|
1053
|
-
\`;
|
|
1054
|
-
}
|
|
1055
|
-
|
|
1056
|
-
async function renderRelations() {
|
|
1057
|
-
const relations = await api('/relations');
|
|
1058
|
-
|
|
1059
|
-
return \`
|
|
1060
|
-
<h1 style="margin-bottom: var(--space-6)">Memory Relations</h1>
|
|
1061
|
-
|
|
1062
|
-
<div class="card">
|
|
1063
|
-
<div class="table-container">
|
|
1064
|
-
<table>
|
|
1065
|
-
<thead>
|
|
1066
|
-
<tr>
|
|
1067
|
-
<th>Source</th>
|
|
1068
|
-
<th>Relation</th>
|
|
1069
|
-
<th>Target</th>
|
|
1070
|
-
<th>Strength</th>
|
|
1071
|
-
</tr>
|
|
1072
|
-
</thead>
|
|
1073
|
-
<tbody>
|
|
1074
|
-
\${relations.length === 0 ? '<tr><td colspan="4" class="empty-state">No relations found</td></tr>' : ''}
|
|
1075
|
-
\${relations.map(r => \`
|
|
1076
|
-
<tr>
|
|
1077
|
-
<td>
|
|
1078
|
-
<span class="badge badge-\${r.source_type}">\${r.source_type}</span>
|
|
1079
|
-
<div class="truncate" style="max-width: 200px">\${r.source_content}</div>
|
|
1080
|
-
</td>
|
|
1081
|
-
<td><strong>\${r.relation_type}</strong></td>
|
|
1082
|
-
<td>
|
|
1083
|
-
<span class="badge badge-\${r.target_type}">\${r.target_type}</span>
|
|
1084
|
-
<div class="truncate" style="max-width: 200px">\${r.target_content}</div>
|
|
1085
|
-
</td>
|
|
1086
|
-
<td>\${r.strength}</td>
|
|
1087
|
-
</tr>
|
|
1088
|
-
\`).join('')}
|
|
1089
|
-
</tbody>
|
|
1090
|
-
</table>
|
|
1091
|
-
</div>
|
|
1092
|
-
</div>
|
|
1093
|
-
\`;
|
|
1094
|
-
}
|
|
1095
|
-
|
|
1096
|
-
// Memory actions
|
|
1097
|
-
async function viewMemory(id) {
|
|
1098
|
-
const m = await api('/memories/' + id);
|
|
1099
|
-
const tags = JSON.parse(m.tags || '[]');
|
|
1100
|
-
|
|
1101
|
-
openModal('Memory #' + m.id, \`
|
|
1102
|
-
<div style="margin-bottom: var(--space-4)">
|
|
1103
|
-
<span class="badge badge-\${m.memory_type}">\${m.memory_type}</span>
|
|
1104
|
-
<span class="importance \${m.importance >= 8 ? 'high' : m.importance >= 5 ? 'medium' : ''}">⭐ \${m.importance}</span>
|
|
1105
|
-
</div>
|
|
1106
|
-
<div style="background: var(--bg-primary); padding: var(--space-4); border-radius: var(--radius-md); white-space: pre-wrap; margin-bottom: var(--space-4);">\${m.content}</div>
|
|
1107
|
-
<p><strong>Tags:</strong> \${tags.map(t => \`<span class="badge badge-observation">\${t}</span>\`).join(' ') || 'None'}</p>
|
|
1108
|
-
<p><strong>Project:</strong> \${m.project || 'None'}</p>
|
|
1109
|
-
<p><strong>Created:</strong> \${new Date(m.created_at).toLocaleString()}</p>
|
|
1110
|
-
\`, \`
|
|
1111
|
-
<button class="btn btn-ghost" onclick="closeModal()">Close</button>
|
|
1112
|
-
<button class="btn btn-primary" onclick="editMemory(\${m.id})">Edit</button>
|
|
1113
|
-
\`);
|
|
1114
|
-
}
|
|
1115
|
-
|
|
1116
|
-
async function editMemory(id) {
|
|
1117
|
-
const m = await api('/memories/' + id);
|
|
1118
|
-
const tags = JSON.parse(m.tags || '[]');
|
|
1119
|
-
|
|
1120
|
-
openModal('Edit Memory #' + m.id, \`
|
|
1121
|
-
<label style="display: block; margin-bottom: var(--space-2); color: var(--text-secondary)">Content</label>
|
|
1122
|
-
<textarea id="edit-content" class="input" style="width: 100%; min-height: 120px; resize: vertical;">\${m.content}</textarea>
|
|
1123
|
-
|
|
1124
|
-
<label style="display: block; margin: var(--space-4) 0 var(--space-2); color: var(--text-secondary)">Type</label>
|
|
1125
|
-
<select id="edit-type" class="select" style="width: 100%">
|
|
1126
|
-
\${['observation', 'decision', 'learning', 'error', 'pattern', 'preference'].map(t =>
|
|
1127
|
-
\`<option value="\${t}" \${t === m.memory_type ? 'selected' : ''}>\${t}</option>\`
|
|
1128
|
-
).join('')}
|
|
1129
|
-
</select>
|
|
1130
|
-
|
|
1131
|
-
<label style="display: block; margin: var(--space-4) 0 var(--space-2); color: var(--text-secondary)">Tags (comma-separated)</label>
|
|
1132
|
-
<input type="text" id="edit-tags" class="input" style="width: 100%" value="\${tags.join(', ')}">
|
|
1133
|
-
|
|
1134
|
-
<label style="display: block; margin: var(--space-4) 0 var(--space-2); color: var(--text-secondary)">Importance (1-10)</label>
|
|
1135
|
-
<input type="number" id="edit-importance" class="input" style="width: 100%" value="\${m.importance}" min="1" max="10">
|
|
1136
|
-
\`, \`
|
|
1137
|
-
<button class="btn btn-ghost" onclick="closeModal()">Cancel</button>
|
|
1138
|
-
<button class="btn btn-primary" onclick="saveMemory(\${m.id})">Save</button>
|
|
1139
|
-
\`);
|
|
1140
|
-
}
|
|
1141
|
-
|
|
1142
|
-
async function saveMemory(id) {
|
|
1143
|
-
const content = document.getElementById('edit-content').value;
|
|
1144
|
-
const memory_type = document.getElementById('edit-type').value;
|
|
1145
|
-
const tags = document.getElementById('edit-tags').value.split(',').map(t => t.trim()).filter(Boolean);
|
|
1146
|
-
const importance = parseInt(document.getElementById('edit-importance').value);
|
|
1147
|
-
|
|
1148
|
-
await api('/memories/' + id, {
|
|
1149
|
-
method: 'PUT',
|
|
1150
|
-
body: JSON.stringify({ content, memory_type, tags, importance })
|
|
1151
|
-
});
|
|
1152
|
-
|
|
1153
|
-
closeModal();
|
|
1154
|
-
showToast('Memory updated');
|
|
1155
|
-
render();
|
|
1156
|
-
}
|
|
1157
|
-
|
|
1158
|
-
async function deleteMemory(id) {
|
|
1159
|
-
if (!confirm('Delete this memory?')) return;
|
|
1160
|
-
await api('/memories/' + id, { method: 'DELETE' });
|
|
1161
|
-
showToast('Memory deleted');
|
|
1162
|
-
render();
|
|
1163
|
-
}
|
|
1164
|
-
|
|
1165
|
-
async function searchMemories() {
|
|
1166
|
-
const search = document.getElementById('search')?.value || '';
|
|
1167
|
-
const type = document.getElementById('type-filter')?.value || '';
|
|
1168
|
-
|
|
1169
|
-
const params = new URLSearchParams();
|
|
1170
|
-
if (search) params.set('search', search);
|
|
1171
|
-
if (type) params.set('type', type);
|
|
1172
|
-
|
|
1173
|
-
const memories = await api('/memories?' + params);
|
|
1174
|
-
const tbody = document.getElementById('memories-tbody');
|
|
1175
|
-
|
|
1176
|
-
if (memories.length === 0) {
|
|
1177
|
-
tbody.innerHTML = '<tr><td colspan="6" class="empty-state">No memories found</td></tr>';
|
|
1178
|
-
return;
|
|
1179
|
-
}
|
|
1180
|
-
|
|
1181
|
-
tbody.innerHTML = memories.map(m => \`
|
|
1182
|
-
<tr>
|
|
1183
|
-
<td>\${m.id}</td>
|
|
1184
|
-
<td><span class="badge badge-\${m.memory_type}">\${m.memory_type}</span></td>
|
|
1185
|
-
<td class="truncate" title="\${m.content?.replace(/"/g, '"')}">\${m.content}</td>
|
|
1186
|
-
<td>\${m.project || '-'}</td>
|
|
1187
|
-
<td>
|
|
1188
|
-
<span class="importance \${m.importance >= 8 ? 'high' : m.importance >= 5 ? 'medium' : ''}">
|
|
1189
|
-
⭐ \${m.importance}
|
|
1190
|
-
</span>
|
|
1191
|
-
</td>
|
|
1192
|
-
<td>
|
|
1193
|
-
<button class="btn btn-ghost" onclick="viewMemory(\${m.id})">View</button>
|
|
1194
|
-
<button class="btn btn-ghost" onclick="editMemory(\${m.id})">Edit</button>
|
|
1195
|
-
<button class="btn btn-danger" onclick="deleteMemory(\${m.id})">×</button>
|
|
1196
|
-
</td>
|
|
1197
|
-
</tr>
|
|
1198
|
-
\`).join('');
|
|
1199
|
-
}
|
|
1200
|
-
|
|
1201
|
-
// Debounce
|
|
1202
|
-
function debounce(fn, delay) {
|
|
1203
|
-
let timeout;
|
|
1204
|
-
return function() {
|
|
1205
|
-
clearTimeout(timeout);
|
|
1206
|
-
timeout = setTimeout(fn, delay);
|
|
1207
|
-
};
|
|
1208
|
-
}
|
|
1209
|
-
|
|
1210
|
-
// Keyboard shortcuts
|
|
1211
|
-
document.addEventListener('keydown', e => {
|
|
1212
|
-
if (e.key === 'Escape') closeModal();
|
|
1213
|
-
});
|
|
1214
|
-
|
|
1215
|
-
// Init
|
|
1216
|
-
render();
|
|
1217
|
-
</script>
|
|
1218
|
-
</body>
|
|
1219
|
-
</html>`;
|
|
1220
|
-
// ===== HTTP 서버 =====
|
|
1221
|
-
const server = http.createServer(async (req, res) => {
|
|
1222
|
-
const parsedUrl = url.parse(req.url || '', true);
|
|
1223
|
-
const pathname = parsedUrl.pathname || '/';
|
|
1224
|
-
// CORS
|
|
1225
|
-
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
1226
|
-
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
|
|
1227
|
-
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
|
|
1228
|
-
if (req.method === 'OPTIONS') {
|
|
1229
|
-
res.writeHead(200);
|
|
1230
|
-
res.end();
|
|
1231
|
-
return;
|
|
1232
|
-
}
|
|
1233
|
-
const json = (data, status = 200) => {
|
|
1234
|
-
res.writeHead(status, { 'Content-Type': 'application/json' });
|
|
1235
|
-
res.end(JSON.stringify(data));
|
|
1236
|
-
};
|
|
1237
|
-
const parseBody = () => {
|
|
1238
|
-
return new Promise((resolve) => {
|
|
1239
|
-
let body = '';
|
|
1240
|
-
req.on('data', chunk => body += chunk);
|
|
1241
|
-
req.on('end', () => {
|
|
1242
|
-
try {
|
|
1243
|
-
resolve(JSON.parse(body));
|
|
1244
|
-
}
|
|
1245
|
-
catch {
|
|
1246
|
-
resolve({});
|
|
1247
|
-
}
|
|
1248
|
-
});
|
|
1249
|
-
});
|
|
1250
|
-
};
|
|
1251
|
-
try {
|
|
1252
|
-
// API Routes
|
|
1253
|
-
if (pathname.startsWith('/api')) {
|
|
1254
|
-
const apiPath = pathname.slice(4);
|
|
1255
|
-
const params = new URLSearchParams(parsedUrl.search || '');
|
|
1256
|
-
if (apiPath === '/stats' && req.method === 'GET') {
|
|
1257
|
-
return json(getStats());
|
|
1258
|
-
}
|
|
1259
|
-
if (apiPath === '/memories' && req.method === 'GET') {
|
|
1260
|
-
return json(getMemories(params));
|
|
1261
|
-
}
|
|
1262
|
-
const memoryMatch = apiPath.match(/^\/memories\/(\d+)$/);
|
|
1263
|
-
if (memoryMatch) {
|
|
1264
|
-
const id = parseInt(memoryMatch[1]);
|
|
1265
|
-
if (req.method === 'GET')
|
|
1266
|
-
return json(getMemory(id));
|
|
1267
|
-
if (req.method === 'PUT') {
|
|
1268
|
-
const body = await parseBody();
|
|
1269
|
-
return json(updateMemory(id, body));
|
|
1270
|
-
}
|
|
1271
|
-
if (req.method === 'DELETE')
|
|
1272
|
-
return json(deleteMemoryById(id));
|
|
1273
|
-
}
|
|
1274
|
-
if (apiPath === '/timeline' && req.method === 'GET') {
|
|
1275
|
-
const project = params.get('project') || undefined;
|
|
1276
|
-
return json(getTimeline(project));
|
|
1277
|
-
}
|
|
1278
|
-
const contextMatch = apiPath.match(/^\/context\/(.+)$/);
|
|
1279
|
-
if (contextMatch && req.method === 'GET') {
|
|
1280
|
-
return json(getProjectContext(contextMatch[1]));
|
|
1281
|
-
}
|
|
1282
|
-
if (apiPath === '/relations' && req.method === 'GET') {
|
|
1283
|
-
const memoryId = params.get('memoryId');
|
|
1284
|
-
return json(getRelations(memoryId ? parseInt(memoryId) : undefined));
|
|
1285
|
-
}
|
|
1286
|
-
return json({ error: 'Not found' }, 404);
|
|
1287
|
-
}
|
|
1288
|
-
// HTML
|
|
1289
|
-
if (pathname === '/' || pathname === '/index.html') {
|
|
1290
|
-
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
1291
|
-
res.end(HTML_TEMPLATE);
|
|
1292
|
-
return;
|
|
1293
|
-
}
|
|
1294
|
-
res.writeHead(404, { 'Content-Type': 'text/plain' });
|
|
1295
|
-
res.end('Not Found');
|
|
1296
|
-
}
|
|
1297
|
-
catch (error) {
|
|
1298
|
-
console.error('Error:', error);
|
|
1299
|
-
json({ error: String(error) }, 500);
|
|
1300
|
-
}
|
|
1301
|
-
});
|
|
1302
|
-
server.listen(PORT, '127.0.0.1', () => {
|
|
1303
|
-
console.log(`
|
|
1304
|
-
╔══════════════════════════════════════════════════════════════╗
|
|
1305
|
-
║ ║
|
|
1306
|
-
║ 🧠 Project Manager MCP Dashboard v2 ║
|
|
1307
|
-
║ ║
|
|
1308
|
-
║ Open: http://127.0.0.1:${PORT} ║
|
|
1309
|
-
║ DB: ${DB_PATH}
|
|
1310
|
-
║ ║
|
|
1311
|
-
║ Features: ║
|
|
1312
|
-
║ • Modern Tailwind-inspired design ║
|
|
1313
|
-
║ • Project context viewer ║
|
|
1314
|
-
║ • Timeline view ║
|
|
1315
|
-
║ • Memory graph ║
|
|
1316
|
-
║ ║
|
|
1317
|
-
║ Press Ctrl+C to stop ║
|
|
1318
|
-
║ ║
|
|
1319
|
-
╚══════════════════════════════════════════════════════════════╝
|
|
1320
|
-
`);
|
|
1321
|
-
});
|