@jungjaehoon/mama-server 1.4.3 → 1.4.5

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 CHANGED
@@ -179,6 +179,26 @@ curl -X POST http://127.0.0.1:3847/embed/batch \
179
179
  - **Automatic**: Starts with MCP server, no extra configuration needed
180
180
  - **Secure**: localhost only (127.0.0.1), no external access
181
181
 
182
+ ## Graph Viewer
183
+
184
+ Interactive visualization of your reasoning graph.
185
+
186
+ **Access:** `http://localhost:3847/viewer`
187
+
188
+ **Features:**
189
+
190
+ - Network graph with physics simulation
191
+ - Checkpoint timeline sidebar
192
+ - Draggable detail panel
193
+ - Topic filtering and search
194
+
195
+ ## Environment Variables
196
+
197
+ | Variable | Default | Description |
198
+ | ---------------- | -------------------------- | -------------------------- |
199
+ | `MAMA_DB_PATH` | `~/.claude/mama-memory.db` | SQLite database location |
200
+ | `MAMA_HTTP_PORT` | `3847` | HTTP embedding server port |
201
+
182
202
  ## Technical Details
183
203
 
184
204
  - **Database:** SQLite + sqlite-vec extension
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jungjaehoon/mama-server",
3
- "version": "1.4.3",
3
+ "version": "1.4.5",
4
4
  "description": "MAMA MCP Server - Memory-Augmented MCP Assistant for Claude Code & Desktop",
5
5
  "main": "src/server.js",
6
6
  "bin": {
@@ -85,6 +85,39 @@ async function getAllEdges() {
85
85
  }));
86
86
  }
87
87
 
88
+ /**
89
+ * Get all checkpoints
90
+ *
91
+ * @returns {Promise<Array>} Array of checkpoint objects
92
+ */
93
+ async function getAllCheckpoints() {
94
+ const adapter = getAdapter();
95
+
96
+ const stmt = adapter.prepare(`
97
+ SELECT
98
+ id,
99
+ timestamp,
100
+ summary,
101
+ open_files,
102
+ next_steps,
103
+ status
104
+ FROM checkpoints
105
+ ORDER BY timestamp DESC
106
+ LIMIT 50
107
+ `);
108
+
109
+ const rows = stmt.all();
110
+
111
+ return rows.map((row) => ({
112
+ id: row.id,
113
+ timestamp: row.timestamp,
114
+ summary: row.summary,
115
+ open_files: row.open_files ? JSON.parse(row.open_files) : [],
116
+ next_steps: row.next_steps,
117
+ status: row.status,
118
+ }));
119
+ }
120
+
88
121
  /**
89
122
  * Get unique topics from nodes
90
123
  *
@@ -455,6 +488,39 @@ async function handleSimilarRequest(req, res, params) {
455
488
  }
456
489
  }
457
490
 
491
+ /**
492
+ * Handle GET /checkpoints request - list all checkpoints
493
+ *
494
+ * @param {Object} req - HTTP request
495
+ * @param {Object} res - HTTP response
496
+ */
497
+ async function handleCheckpointsRequest(req, res) {
498
+ try {
499
+ // Ensure DB is initialized
500
+ await initDB();
501
+
502
+ const checkpoints = await getAllCheckpoints();
503
+
504
+ res.writeHead(200);
505
+ res.end(
506
+ JSON.stringify({
507
+ checkpoints,
508
+ count: checkpoints.length,
509
+ })
510
+ );
511
+ } catch (error) {
512
+ console.error(`[GraphAPI] Checkpoints error: ${error.message}`);
513
+ res.writeHead(500);
514
+ res.end(
515
+ JSON.stringify({
516
+ error: true,
517
+ code: 'CHECKPOINTS_FAILED',
518
+ message: error.message,
519
+ })
520
+ );
521
+ }
522
+ }
523
+
458
524
  /**
459
525
  * Create route handler for graph API
460
526
  *
@@ -506,6 +572,12 @@ function createGraphHandler() {
506
572
  return true; // Request handled
507
573
  }
508
574
 
575
+ // Route: GET /checkpoints - list all checkpoints
576
+ if (pathname === '/checkpoints' && req.method === 'GET') {
577
+ await handleCheckpointsRequest(req, res);
578
+ return true; // Request handled
579
+ }
580
+
509
581
  return false; // Request not handled
510
582
  };
511
583
  }
@@ -515,6 +587,7 @@ module.exports = {
515
587
  // Exported for testing
516
588
  getAllNodes,
517
589
  getAllEdges,
590
+ getAllCheckpoints,
518
591
  getUniqueTopics,
519
592
  filterNodesByTopic,
520
593
  filterEdgesByNodes,
@@ -83,11 +83,11 @@ header h1 {
83
83
  position: relative;
84
84
  }
85
85
 
86
- /* Detail Panel */
86
+ /* Detail Panel - draggable */
87
87
  #detail-panel {
88
88
  display: none;
89
89
  position: fixed;
90
- right: 20px;
90
+ right: 340px;
91
91
  top: 80px;
92
92
  width: 350px;
93
93
  max-height: calc(100vh - 100px);
@@ -97,6 +97,7 @@ header h1 {
97
97
  padding: 16px;
98
98
  overflow-y: auto;
99
99
  box-shadow: 0 4px 20px rgba(0, 0, 0, 0.4);
100
+ z-index: 300;
100
101
  }
101
102
 
102
103
  #detail-panel.visible {
@@ -107,6 +108,8 @@ header h1 {
107
108
  color: #a0a0ff;
108
109
  margin-bottom: 12px;
109
110
  font-size: 14px;
111
+ cursor: move;
112
+ user-select: none;
110
113
  }
111
114
 
112
115
  #detail-panel .field {
@@ -426,3 +429,234 @@ header h1 {
426
429
  #legend-panel.collapsed + .legend-toggle {
427
430
  display: block;
428
431
  }
432
+
433
+ /* Checkpoint Sidebar Panel */
434
+ #checkpoint-panel {
435
+ position: fixed;
436
+ right: 0;
437
+ top: 50px;
438
+ width: 320px;
439
+ height: calc(100vh - 50px);
440
+ background: #16213e;
441
+ border-left: 1px solid #4a4a6a;
442
+ display: flex;
443
+ flex-direction: column;
444
+ z-index: 200;
445
+ }
446
+
447
+ #checkpoint-panel.hidden {
448
+ display: none;
449
+ }
450
+
451
+ .checkpoint-header {
452
+ display: flex;
453
+ justify-content: space-between;
454
+ align-items: center;
455
+ padding: 12px 16px;
456
+ border-bottom: 1px solid #4a4a6a;
457
+ }
458
+
459
+ .checkpoint-header h4 {
460
+ color: #a0a0ff;
461
+ font-size: 14px;
462
+ font-weight: 600;
463
+ margin: 0;
464
+ }
465
+
466
+ .checkpoint-list {
467
+ flex: 1;
468
+ overflow-y: auto;
469
+ padding: 8px;
470
+ }
471
+
472
+ /* Custom scrollbar for dark theme */
473
+ .checkpoint-list::-webkit-scrollbar,
474
+ .checkpoint-section-content::-webkit-scrollbar,
475
+ #detail-panel::-webkit-scrollbar,
476
+ .reasoning-content::-webkit-scrollbar,
477
+ .similar-list::-webkit-scrollbar {
478
+ width: 6px;
479
+ }
480
+
481
+ .checkpoint-list::-webkit-scrollbar-track,
482
+ .checkpoint-section-content::-webkit-scrollbar-track,
483
+ #detail-panel::-webkit-scrollbar-track,
484
+ .reasoning-content::-webkit-scrollbar-track,
485
+ .similar-list::-webkit-scrollbar-track {
486
+ background: #1a1a2e;
487
+ border-radius: 3px;
488
+ }
489
+
490
+ .checkpoint-list::-webkit-scrollbar-thumb,
491
+ .checkpoint-section-content::-webkit-scrollbar-thumb,
492
+ #detail-panel::-webkit-scrollbar-thumb,
493
+ .reasoning-content::-webkit-scrollbar-thumb,
494
+ .similar-list::-webkit-scrollbar-thumb {
495
+ background: #4a4a6a;
496
+ border-radius: 3px;
497
+ }
498
+
499
+ .checkpoint-list::-webkit-scrollbar-thumb:hover,
500
+ .checkpoint-section-content::-webkit-scrollbar-thumb:hover,
501
+ #detail-panel::-webkit-scrollbar-thumb:hover,
502
+ .reasoning-content::-webkit-scrollbar-thumb:hover,
503
+ .similar-list::-webkit-scrollbar-thumb:hover {
504
+ background: #5a5a7a;
505
+ }
506
+
507
+ /* Horizontal scrollbar (for graph container) */
508
+ #graph-container::-webkit-scrollbar {
509
+ height: 6px;
510
+ width: 6px;
511
+ }
512
+
513
+ #graph-container::-webkit-scrollbar-track {
514
+ background: #1a1a2e;
515
+ }
516
+
517
+ #graph-container::-webkit-scrollbar-thumb {
518
+ background: #4a4a6a;
519
+ border-radius: 3px;
520
+ }
521
+
522
+ #graph-container::-webkit-scrollbar-thumb:hover {
523
+ background: #5a5a7a;
524
+ }
525
+
526
+ /* Firefox scrollbar support */
527
+ * {
528
+ scrollbar-width: thin;
529
+ scrollbar-color: #4a4a6a #1a1a2e;
530
+ }
531
+
532
+ .loading-checkpoints {
533
+ color: #666;
534
+ font-style: italic;
535
+ text-align: center;
536
+ padding: 20px;
537
+ }
538
+
539
+ .checkpoint-item {
540
+ background: #1a1a2e;
541
+ border: 1px solid #3a3a5a;
542
+ border-radius: 6px;
543
+ padding: 12px;
544
+ margin-bottom: 8px;
545
+ cursor: pointer;
546
+ transition: all 0.2s;
547
+ }
548
+
549
+ .checkpoint-item:hover {
550
+ border-color: #a0a0ff;
551
+ background: #1e1e3e;
552
+ }
553
+
554
+ .checkpoint-item.expanded {
555
+ border-color: #a0a0ff;
556
+ }
557
+
558
+ .checkpoint-time {
559
+ font-size: 11px;
560
+ color: #888;
561
+ margin-bottom: 6px;
562
+ }
563
+
564
+ .checkpoint-summary {
565
+ font-size: 12px;
566
+ color: #e0e0e0;
567
+ line-height: 1.4;
568
+ max-height: 60px;
569
+ overflow: hidden;
570
+ text-overflow: ellipsis;
571
+ }
572
+
573
+ .checkpoint-item.expanded .checkpoint-summary {
574
+ max-height: none;
575
+ }
576
+
577
+ .checkpoint-details {
578
+ display: none;
579
+ margin-top: 10px;
580
+ padding-top: 10px;
581
+ border-top: 1px solid #3a3a5a;
582
+ }
583
+
584
+ .checkpoint-item.expanded .checkpoint-details {
585
+ display: block;
586
+ }
587
+
588
+ .checkpoint-section {
589
+ margin-bottom: 8px;
590
+ }
591
+
592
+ .checkpoint-section-title {
593
+ font-size: 10px;
594
+ color: #888;
595
+ text-transform: uppercase;
596
+ margin-bottom: 4px;
597
+ }
598
+
599
+ .checkpoint-section-content {
600
+ font-size: 11px;
601
+ color: #aaa;
602
+ white-space: pre-wrap;
603
+ max-height: 100px;
604
+ overflow-y: auto;
605
+ }
606
+
607
+ .checkpoint-files {
608
+ display: flex;
609
+ flex-wrap: wrap;
610
+ gap: 4px;
611
+ }
612
+
613
+ .checkpoint-file {
614
+ font-size: 10px;
615
+ color: #6366f1;
616
+ background: #2a2a4e;
617
+ padding: 2px 6px;
618
+ border-radius: 3px;
619
+ }
620
+
621
+ .checkpoint-related {
622
+ display: flex;
623
+ flex-wrap: wrap;
624
+ gap: 4px;
625
+ margin-top: 4px;
626
+ }
627
+
628
+ .checkpoint-related-link {
629
+ font-size: 10px;
630
+ color: #22c55e;
631
+ background: #1a3a2e;
632
+ padding: 2px 6px;
633
+ border-radius: 3px;
634
+ cursor: pointer;
635
+ }
636
+
637
+ .checkpoint-related-link:hover {
638
+ background: #2a4a3e;
639
+ }
640
+
641
+ .checkpoint-toggle {
642
+ position: fixed;
643
+ right: 20px;
644
+ top: 60px;
645
+ background: #16213e;
646
+ border: 1px solid #4a4a6a;
647
+ border-radius: 4px;
648
+ padding: 8px 12px;
649
+ color: #a0a0ff;
650
+ cursor: pointer;
651
+ font-size: 12px;
652
+ z-index: 99;
653
+ display: none;
654
+ }
655
+
656
+ .checkpoint-toggle:hover {
657
+ background: #1a2a4e;
658
+ }
659
+
660
+ #checkpoint-panel.hidden ~ .checkpoint-toggle {
661
+ display: block;
662
+ }
@@ -63,6 +63,18 @@
63
63
  </div>
64
64
  </div>
65
65
 
66
+ <!-- Checkpoint Sidebar Panel -->
67
+ <div id="checkpoint-panel">
68
+ <div class="checkpoint-header">
69
+ <h4>📋 Checkpoints</h4>
70
+ <button class="close-btn" onclick="toggleCheckpoints()">&times;</button>
71
+ </div>
72
+ <div class="checkpoint-list" id="checkpoint-list">
73
+ <div class="loading-checkpoints">Loading...</div>
74
+ </div>
75
+ </div>
76
+ <button class="checkpoint-toggle" id="checkpoint-toggle" onclick="toggleCheckpoints()">📋 Checkpoints</button>
77
+
66
78
  <!-- Legend Panel -->
67
79
  <div id="legend-panel">
68
80
  <button class="close-btn" onclick="toggleLegend()" style="position:absolute;top:8px;right:8px;">&times;</button>
@@ -822,8 +822,210 @@ document.addEventListener('keydown', (e) => {
822
822
  }
823
823
  });
824
824
 
825
+ // Checkpoint Panel Functions
826
+ let checkpointsData = [];
827
+
828
+ // Toggle checkpoint panel visibility (called from HTML onclick)
829
+ // eslint-disable-next-line no-unused-vars
830
+ function toggleCheckpoints() {
831
+ const panel = document.getElementById('checkpoint-panel');
832
+ panel.classList.toggle('hidden');
833
+ }
834
+
835
+ // Fetch checkpoints from API
836
+ async function fetchCheckpoints() {
837
+ try {
838
+ const response = await fetch('/checkpoints');
839
+ if (!response.ok) {
840
+ throw new Error(`HTTP ${response.status}`);
841
+ }
842
+ const data = await response.json();
843
+ checkpointsData = data.checkpoints || [];
844
+ renderCheckpoints();
845
+ } catch (error) {
846
+ console.error('[MAMA] Failed to fetch checkpoints:', error);
847
+ document.getElementById('checkpoint-list').innerHTML =
848
+ `<div class="loading-checkpoints" style="color:#f66">Failed to load: ${error.message}</div>`;
849
+ }
850
+ }
851
+
852
+ // Render checkpoints list
853
+ function renderCheckpoints() {
854
+ const container = document.getElementById('checkpoint-list');
855
+
856
+ if (checkpointsData.length === 0) {
857
+ container.innerHTML = '<div class="loading-checkpoints">No checkpoints found</div>';
858
+ return;
859
+ }
860
+
861
+ const html = checkpointsData
862
+ .map(
863
+ (cp, idx) => `
864
+ <div class="checkpoint-item" onclick="expandCheckpoint(${idx})">
865
+ <div class="checkpoint-time">${formatCheckpointTime(cp.timestamp)}</div>
866
+ <div class="checkpoint-summary">${escapeHtml(extractFirstLine(cp.summary))}</div>
867
+ <div class="checkpoint-details">
868
+ ${
869
+ cp.summary
870
+ ? `
871
+ <div class="checkpoint-section">
872
+ <div class="checkpoint-section-title">Summary</div>
873
+ <div class="checkpoint-section-content">${escapeHtml(cp.summary)}</div>
874
+ </div>
875
+ `
876
+ : ''
877
+ }
878
+ ${
879
+ cp.next_steps
880
+ ? `
881
+ <div class="checkpoint-section">
882
+ <div class="checkpoint-section-title">Next Steps</div>
883
+ <div class="checkpoint-section-content">${escapeHtml(cp.next_steps)}</div>
884
+ </div>
885
+ `
886
+ : ''
887
+ }
888
+ ${
889
+ cp.open_files && cp.open_files.length > 0
890
+ ? `
891
+ <div class="checkpoint-section">
892
+ <div class="checkpoint-section-title">Open Files</div>
893
+ <div class="checkpoint-files">
894
+ ${cp.open_files.map((f) => `<span class="checkpoint-file">${escapeHtml(f.split('/').pop())}</span>`).join('')}
895
+ </div>
896
+ </div>
897
+ `
898
+ : ''
899
+ }
900
+ ${renderRelatedDecisions(cp.summary)}
901
+ </div>
902
+ </div>
903
+ `
904
+ )
905
+ .join('');
906
+
907
+ container.innerHTML = html;
908
+ }
909
+
910
+ // Format checkpoint timestamp
911
+ function formatCheckpointTime(timestamp) {
912
+ const date = new Date(timestamp);
913
+ const now = new Date();
914
+ const diff = now - date;
915
+
916
+ if (diff < 3600000) {
917
+ const mins = Math.floor(diff / 60000);
918
+ return `${mins}m ago`;
919
+ }
920
+ if (diff < 86400000) {
921
+ const hours = Math.floor(diff / 3600000);
922
+ return `${hours}h ago`;
923
+ }
924
+
925
+ return (
926
+ date.toLocaleDateString() +
927
+ ' ' +
928
+ date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
929
+ );
930
+ }
931
+
932
+ // Extract first meaningful line from summary
933
+ function extractFirstLine(summary) {
934
+ if (!summary) {
935
+ return 'No summary';
936
+ }
937
+ const lines = summary.split('\n').filter((l) => l.trim() && !l.startsWith('**'));
938
+ return lines[0] || summary.substring(0, 100);
939
+ }
940
+
941
+ // Extract and render related decisions from summary
942
+ function renderRelatedDecisions(summary) {
943
+ if (!summary) {
944
+ return '';
945
+ }
946
+
947
+ // Match patterns like "decision_xxx" or "Related decisions: xxx, yyy"
948
+ const decisionPattern = /decision_[a-z0-9_]+/gi;
949
+ const matches = summary.match(decisionPattern);
950
+
951
+ if (!matches || matches.length === 0) {
952
+ return '';
953
+ }
954
+
955
+ const uniqueDecisions = [...new Set(matches)];
956
+
957
+ return `
958
+ <div class="checkpoint-section">
959
+ <div class="checkpoint-section-title">Related Decisions</div>
960
+ <div class="checkpoint-related">
961
+ ${uniqueDecisions.map((d) => `<span class="checkpoint-related-link" onclick="event.stopPropagation(); navigateToDecision('${d}')">${d.substring(9, 30)}...</span>`).join('')}
962
+ </div>
963
+ </div>
964
+ `;
965
+ }
966
+
967
+ // Expand/collapse checkpoint item
968
+ // eslint-disable-next-line no-unused-vars
969
+ function expandCheckpoint(idx) {
970
+ const items = document.querySelectorAll('.checkpoint-item');
971
+ items.forEach((item, i) => {
972
+ if (i === idx) {
973
+ item.classList.toggle('expanded');
974
+ } else {
975
+ item.classList.remove('expanded');
976
+ }
977
+ });
978
+ }
979
+
980
+ // Navigate to a decision in the graph (from checkpoint related link)
981
+ // eslint-disable-next-line no-unused-vars
982
+ function navigateToDecision(decisionId) {
983
+ // Close checkpoint panel
984
+ document.getElementById('checkpoint-panel').classList.remove('visible');
985
+
986
+ // Use existing navigateToNode function
987
+ navigateToNode(decisionId);
988
+ }
989
+
990
+ // Make detail panel draggable
991
+ function initDraggablePanel() {
992
+ const panel = document.getElementById('detail-panel');
993
+ const header = panel.querySelector('h3');
994
+ let isDragging = false;
995
+ let offsetX, offsetY;
996
+
997
+ header.addEventListener('mousedown', (e) => {
998
+ isDragging = true;
999
+ offsetX = e.clientX - panel.offsetLeft;
1000
+ offsetY = e.clientY - panel.offsetTop;
1001
+ panel.style.transition = 'none';
1002
+ });
1003
+
1004
+ document.addEventListener('mousemove', (e) => {
1005
+ if (!isDragging) {
1006
+ return;
1007
+ }
1008
+ const x = Math.max(0, Math.min(e.clientX - offsetX, window.innerWidth - panel.offsetWidth));
1009
+ const y = Math.max(50, Math.min(e.clientY - offsetY, window.innerHeight - panel.offsetHeight));
1010
+ panel.style.left = x + 'px';
1011
+ panel.style.top = y + 'px';
1012
+ panel.style.right = 'auto';
1013
+ });
1014
+
1015
+ document.addEventListener('mouseup', () => {
1016
+ isDragging = false;
1017
+ panel.style.transition = '';
1018
+ });
1019
+ }
1020
+
825
1021
  // Initialize on page load
826
1022
  document.addEventListener('DOMContentLoaded', async () => {
1023
+ // Initialize draggable panel
1024
+ initDraggablePanel();
1025
+
1026
+ // Load checkpoints (panel is visible by default)
1027
+ fetchCheckpoints();
1028
+
827
1029
  try {
828
1030
  const data = await fetchGraphData();
829
1031
  if (data.nodes.length === 0) {