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.
@@ -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, '&quot;')}">\${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, '&quot;')}">\${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
- });