claude-code-memory-explorer 0.2.5 → 0.2.6

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-code-memory-explorer",
3
- "version": "0.2.5",
3
+ "version": "0.2.6",
4
4
  "description": "Memory explorer dashboard for Claude Code",
5
5
  "main": "server.js",
6
6
  "bin": {
package/public/app.js CHANGED
@@ -401,6 +401,10 @@ function navigateGroup(direction) {
401
401
 
402
402
  // #region MEMORY_INDEX
403
403
 
404
+ function isMemoryIndex(source) {
405
+ return (source.scope === 'memory' || source.scope === 'user-memory') && source.name === 'MEMORY.md';
406
+ }
407
+
404
408
  function parseMemoryIndex(content) {
405
409
  const entries = [];
406
410
  for (const line of content.split('\n')) {
@@ -458,6 +462,12 @@ async function renderPreview() {
458
462
  html += `<span class="scope-badge scope-${source.scope}">${esc(source.scope)}</span>`;
459
463
  html += `<span class="file-path">${esc(source.name)}</span>`;
460
464
  html += '<div class="preview-actions">';
465
+ if (isMemoryIndex(source)) {
466
+ const broken = (source.unresolvedImports || []).length;
467
+ if (broken) {
468
+ html += `<button class="action-btn small" onclick="_cleanupOrphans('${escJs(source.path)}')" title="Cleanup ${broken} orphaned refs"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18"/><path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/><path d="M12 11v6"/></svg><span style="margin-left:3px">${broken}</span></button>`;
469
+ }
470
+ }
461
471
  html += `<button class="action-btn small" onclick="openInEditor('${escJs(source.path)}')" title="Open in VS Code"><svg width="12" height="12" viewBox="0 0 24 24" fill="currentColor" stroke="none"><path d="M17.583 2.207a1.1 1.1 0 0 1 1.541.033l2.636 2.636a1.1 1.1 0 0 1 .033 1.541L10.68 17.53a1.1 1.1 0 0 1-.345.247l-4.56 1.903a.55.55 0 0 1-.725-.725l1.903-4.56a1.1 1.1 0 0 1 .247-.345zm.902 1.87-8.794 8.793-.946 2.268 2.268-.946 8.794-8.793z"/></svg></button>`;
462
472
  html += `<button class="action-btn small" onclick="_confirmDeleteFile('${escJs(source.path)}','${escJs(source.name)}')" title="Delete file"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/><path d="M10 11v6M14 11v6"/><path d="M9 6V4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2"/></svg></button>`;
463
473
  html += '</div></div>';
@@ -474,10 +484,13 @@ async function renderPreview() {
474
484
  }
475
485
  html += '</div>';
476
486
 
477
- // Imports — only show children (files that have this source as parent)
487
+ const content = fileData.content || '';
488
+ const memoryEntries = isMemoryIndex(source) ? parseMemoryIndex(content) : [];
489
+ const showMemoryTable = memoryEntries.length > 0;
490
+
478
491
  const children = stackData.filter((s) => s.parentId === source.id);
479
492
  const unresolved = source.unresolvedImports || [];
480
- if (children.length || unresolved.length) {
493
+ if (!showMemoryTable && (children.length || unresolved.length)) {
481
494
  html += '<div class="preview-imports">';
482
495
  for (const child of children) {
483
496
  html += `<a class="import-link" href="#" onclick="selectFile('${escJs(child.id)}');return false" title="${esc(child.path)}">${esc(child.name)}</a>`;
@@ -493,19 +506,16 @@ async function renderPreview() {
493
506
  // File path
494
507
  html += `<div class="preview-filepath">${esc(source.path)}</div>`;
495
508
 
496
- // Content — with cutoff line for auto memory startup files
497
- const content = fileData.content || '';
498
509
  const hl = (text) => {
499
510
  const { text: processed, placeholders } = linkifyImports(text, source.id);
500
511
  return restorePlaceholders(highlightSource(processed, source.name), placeholders);
501
512
  };
502
513
 
503
- // Memory index view — render structured table for MEMORY.md index files
504
- if ((source.scope === 'memory' || source.scope === 'user-memory') && source.name === 'MEMORY.md') {
505
- const entries = parseMemoryIndex(content);
506
- if (entries.length) {
507
- html += renderMemoryIndexTable(entries);
508
- }
514
+ if (showMemoryTable) {
515
+ html += renderMemoryIndexTable(memoryEntries);
516
+ html += `<pre class="preview-code"><code>${hl(content)}</code></pre>`;
517
+ panel.innerHTML = html;
518
+ return;
509
519
  }
510
520
 
511
521
  if ((source.scope === 'memory' || source.scope === 'user-memory') && source.load === 'startup' && source.maxLines) {
@@ -574,6 +584,54 @@ async function _submitDeleteFile() {
574
584
  }
575
585
  }
576
586
 
587
+ function _cleanupOrphans(filePath) {
588
+ const source = stackData.find((s) => s.path === filePath);
589
+ const broken = source?.unresolvedImports || [];
590
+ if (!broken.length) {
591
+ showToast('No orphaned refs', 'info');
592
+ return;
593
+ }
594
+ const modal = document.getElementById('cleanupOrphansModal');
595
+ modal.dataset.filePath = filePath;
596
+ document.getElementById('cleanupOrphansCount').textContent = broken.length;
597
+ document.getElementById('cleanupOrphansList').innerHTML = broken.map((r) => `<li>${esc(r)}</li>`).join('');
598
+ modal.classList.add('open');
599
+ }
600
+
601
+ async function _submitCleanupOrphans() {
602
+ const modal = document.getElementById('cleanupOrphansModal');
603
+ const filePath = modal.dataset.filePath;
604
+ if (!filePath) return;
605
+ const btn = document.getElementById('cleanupOrphansSubmit');
606
+ btn.disabled = true;
607
+ btn.textContent = 'Cleaning...';
608
+ try {
609
+ const res = await fetch('/api/memory/cleanup-orphans', {
610
+ method: 'POST',
611
+ headers: { 'Content-Type': 'application/json' },
612
+ body: JSON.stringify({ path: filePath }),
613
+ });
614
+ if (!res.ok) {
615
+ const msg = await res
616
+ .json()
617
+ .then((e) => e.error)
618
+ .catch(() => `Cleanup failed (${res.status})`);
619
+ showToast(msg, 'error');
620
+ return;
621
+ }
622
+ const data = await res.json();
623
+ closeModal('cleanupOrphansModal');
624
+ delete modal.dataset.filePath;
625
+ await loadData();
626
+ showToast(`Removed ${data.removed.length} orphaned ref${data.removed.length === 1 ? '' : 's'}`, 'success');
627
+ } catch (err) {
628
+ showToast(err.message, 'error');
629
+ } finally {
630
+ btn.disabled = false;
631
+ btn.textContent = 'Remove';
632
+ }
633
+ }
634
+
577
635
  async function openInEditor(filePath) {
578
636
  showToast('Opening...', 'info');
579
637
  try {
package/public/index.html CHANGED
@@ -34,12 +34,12 @@
34
34
  <button class="topbar-btn" id="refreshBtn" title="Refresh (r)" onclick="refreshData()">
35
35
  <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="23 4 23 10 17 10"/><path d="M20.49 15a9 9 0 11-2.12-9.36L23 10"/></svg>
36
36
  </button>
37
- <button class="topbar-btn" id="helpBtn" title="Keyboard shortcuts (?)" onclick="toggleHelpModal()">
38
- <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><circle cx="12" cy="17" r="0.5" fill="currentColor"/></svg>
39
- </button>
40
37
  <button class="topbar-btn" id="themeBtn" title="Toggle theme (t)" onclick="toggleTheme()">
41
38
  <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/><line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/></svg>
42
39
  </button>
40
+ <button class="topbar-btn" id="helpBtn" title="Keyboard shortcuts (?)" onclick="toggleHelpModal()">
41
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><circle cx="12" cy="17" r="0.5" fill="currentColor"/></svg>
42
+ </button>
43
43
  <a class="topbar-btn" href="https://github.com/NikiforovAll/claude-code-memory" target="_blank" rel="noopener" title="GitHub">
44
44
  <svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor" stroke="none"><path d="M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385.6.105.825-.255.825-.57 0-.285-.015-1.23-.015-2.235-3.015.555-3.795-.735-4.035-1.41-.135-.345-.72-1.41-1.23-1.695-.42-.225-1.02-.78-.015-.795.945-.015 1.62.87 1.845 1.23 1.08 1.815 2.805 1.305 3.495.99.105-.78.42-1.305.765-1.605-2.67-.3-5.46-1.335-5.46-5.925 0-1.305.465-2.385 1.23-3.225-.12-.3-.54-1.53.12-3.18 0 0 1.005-.315 3.3 1.23.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23.66 1.65.24 2.88.12 3.18.765.84 1.23 1.905 1.23 3.225 0 4.605-2.805 5.625-5.475 5.925.435.375.81 1.095.81 2.22 0 1.605-.015 2.895-.015 3.3 0 .315.225.69.825.57A12.02 12.02 0 0024 12c0-6.63-5.37-12-12-12z"/></svg>
45
45
  </a>
@@ -122,6 +122,24 @@
122
122
  </div>
123
123
  </div>
124
124
 
125
+ <!-- Cleanup orphans modal -->
126
+ <div class="modal-overlay" id="cleanupOrphansModal" onclick="if(event.target===this)closeModal('cleanupOrphansModal')">
127
+ <div class="modal">
128
+ <div class="modal-header">
129
+ <h3>Cleanup orphaned refs?</h3>
130
+ <button class="modal-close" onclick="closeModal('cleanupOrphansModal')">&#10005;</button>
131
+ </div>
132
+ <div class="modal-body">
133
+ <p class="modal-desc">Remove <strong id="cleanupOrphansCount"></strong> bullet line(s) from MEMORY.md that point to files missing on disk.</p>
134
+ <ul id="cleanupOrphansList" class="modal-orphan-list"></ul>
135
+ <div class="modal-actions">
136
+ <button class="action-btn" onclick="closeModal('cleanupOrphansModal')">Cancel</button>
137
+ <button class="action-btn danger" id="cleanupOrphansSubmit" onclick="_submitCleanupOrphans()">Remove</button>
138
+ </div>
139
+ </div>
140
+ </div>
141
+ </div>
142
+
125
143
  <!-- Help modal -->
126
144
  <div class="modal-overlay" id="helpModal" onclick="if(event.target===this)toggleHelpModal()">
127
145
  <div class="modal">
package/public/style.css CHANGED
@@ -411,20 +411,27 @@ body.light .tree-conditional .file-name {
411
411
  }
412
412
  .memory-index-row {
413
413
  display: grid;
414
- grid-template-columns: 72px 160px 1fr;
414
+ grid-template-columns: 72px max-content 1fr;
415
415
  align-items: baseline;
416
416
  gap: 8px;
417
417
  padding: 3px 0;
418
418
  }
419
419
  .memory-index-type {
420
- flex-shrink: 0;
420
+ text-align: center;
421
+ justify-self: center;
422
+ }
423
+ .modal-orphan-list {
424
+ margin: 0;
425
+ padding: 8px 12px 8px 28px;
426
+ list-style: disc;
427
+ font-family: var(--font-mono);
428
+ font-size: 11px;
429
+ color: var(--text-muted);
421
430
  }
422
431
  .memory-index-name {
423
432
  font-size: 11px;
424
433
  font-family: var(--font-mono);
425
434
  white-space: nowrap;
426
- overflow: hidden;
427
- text-overflow: ellipsis;
428
435
  }
429
436
  .memory-index-desc {
430
437
  font-size: 10px;
package/server.js CHANGED
@@ -160,6 +160,11 @@ function spreadImports(filePath, content) {
160
160
  return { imports: resolved, softImports: resolvedSoft, unresolvedImports: unresolved };
161
161
  }
162
162
 
163
+ function hasPathsFilter(frontmatter) {
164
+ if (!frontmatter || !frontmatter.paths) return false;
165
+ return Array.isArray(frontmatter.paths) ? frontmatter.paths.length > 0 : true;
166
+ }
167
+
163
168
  function discoverMemorySources(projectPath) {
164
169
  const sources = [];
165
170
 
@@ -202,7 +207,7 @@ function discoverMemorySources(projectPath) {
202
207
  id: `user-rule-${path.basename(file, '.md')}`,
203
208
  name: path.basename(file),
204
209
  scope: 'rule',
205
- load: 'conditional',
210
+ load: hasPathsFilter(info.frontmatter) ? 'conditional' : 'always',
206
211
  ...info,
207
212
  ruleSource: 'user',
208
213
  ...spreadImports(info.path, info.content),
@@ -293,7 +298,7 @@ function discoverMemorySources(projectPath) {
293
298
  id: `project-rule-${path.basename(file, '.md')}`,
294
299
  name: path.basename(file),
295
300
  scope: 'rule',
296
- load: 'conditional',
301
+ load: hasPathsFilter(info.frontmatter) ? 'conditional' : 'always',
297
302
  ...info,
298
303
  ruleSource: 'project',
299
304
  ...spreadImports(info.path, info.content),
@@ -553,13 +558,18 @@ app.post('/api/refresh', (_req, res) => {
553
558
  res.json({ ok: true });
554
559
  });
555
560
 
556
- app.delete('/api/file', (req, res) => {
557
- const { path: filePath } = req.body || {};
558
- if (!filePath) return res.status(400).json({ error: 'path required' });
561
+ function resolveAllowedPath(filePath) {
562
+ if (!filePath) return { error: { status: 400, message: 'path required' } };
559
563
  const resolved = path.resolve(filePath.replace(/^~/, os.homedir()));
560
564
  const roots = [path.resolve(CLAUDE_DIR), path.resolve(currentProjectPath)];
561
565
  const inRoot = roots.some((r) => resolved === r || resolved.startsWith(r + path.sep));
562
- if (!inRoot) return res.status(403).json({ error: 'path outside allowed roots' });
566
+ if (!inRoot) return { error: { status: 403, message: 'path outside allowed roots' } };
567
+ return { resolved };
568
+ }
569
+
570
+ app.delete('/api/file', (req, res) => {
571
+ const { resolved, error } = resolveAllowedPath(req.body?.path);
572
+ if (error) return res.status(error.status).json({ error: error.message });
563
573
  try {
564
574
  const stat = fs.statSync(resolved);
565
575
  if (!stat.isFile()) return res.status(400).json({ error: 'not a file' });
@@ -572,6 +582,37 @@ app.delete('/api/file', (req, res) => {
572
582
  }
573
583
  });
574
584
 
585
+ app.post('/api/memory/cleanup-orphans', (req, res) => {
586
+ const { resolved, error } = resolveAllowedPath(req.body?.path);
587
+ if (error) return res.status(error.status).json({ error: error.message });
588
+ if (path.basename(resolved) !== 'MEMORY.md') return res.status(400).json({ error: 'not a MEMORY.md file' });
589
+ try {
590
+ const content = fs.readFileSync(resolved, 'utf-8');
591
+ const dir = path.dirname(resolved);
592
+ const lines = content.split('\n');
593
+ const removed = [];
594
+ const kept = [];
595
+ const linkRe = /^\s*-\s*\[([^\]]+)\]\(([^)]+\.md)\)/;
596
+ for (const line of lines) {
597
+ const m = line.match(linkRe);
598
+ if (m) {
599
+ const target = path.resolve(dir, m[2]);
600
+ if (!fs.existsSync(target)) {
601
+ removed.push({ name: m[1], file: m[2] });
602
+ continue;
603
+ }
604
+ }
605
+ kept.push(line);
606
+ }
607
+ if (removed.length === 0) return res.json({ ok: true, removed: [] });
608
+ fs.writeFileSync(resolved, kept.join('\n'), 'utf-8');
609
+ clearCache();
610
+ res.json({ ok: true, removed });
611
+ } catch (err) {
612
+ res.status(500).json({ error: err.message });
613
+ }
614
+ });
615
+
575
616
  app.post('/api/open-in-editor', (req, res) => {
576
617
  const { path: filePath } = req.body;
577
618
  if (!filePath) return res.status(400).json({ error: 'path required' });