claude-code-memory-explorer 0.2.4 → 0.2.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-code-memory-explorer",
3
- "version": "0.2.4",
3
+ "version": "0.2.5",
4
4
  "description": "Memory explorer dashboard for Claude Code",
5
5
  "main": "server.js",
6
6
  "bin": {
package/public/app.js CHANGED
@@ -457,8 +457,10 @@ async function renderPreview() {
457
457
  html += '<div class="preview-title">';
458
458
  html += `<span class="scope-badge scope-${source.scope}">${esc(source.scope)}</span>`;
459
459
  html += `<span class="file-path">${esc(source.name)}</span>`;
460
+ html += '<div class="preview-actions">';
460
461
  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>`;
461
- html += '</div>';
462
+ 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
+ html += '</div></div>';
462
464
 
463
465
  // Badges row
464
466
  html += '<div class="preview-badges">';
@@ -530,6 +532,48 @@ function formatBytes(b) {
530
532
  return `${(b / 1024).toFixed(1)}KB`;
531
533
  }
532
534
 
535
+ function _confirmDeleteFile(filePath, fileName) {
536
+ const modal = document.getElementById('deleteConfirmModal');
537
+ modal.dataset.filePath = filePath;
538
+ document.getElementById('deleteFileName').textContent = fileName;
539
+ document.getElementById('deleteFilePath').textContent = filePath;
540
+ modal.classList.add('open');
541
+ }
542
+
543
+ async function _submitDeleteFile() {
544
+ const modal = document.getElementById('deleteConfirmModal');
545
+ const filePath = modal.dataset.filePath;
546
+ if (!filePath) return;
547
+ const btn = document.getElementById('deleteConfirmSubmit');
548
+ btn.disabled = true;
549
+ btn.textContent = 'Deleting...';
550
+ try {
551
+ const res = await fetch('/api/file', {
552
+ method: 'DELETE',
553
+ headers: { 'Content-Type': 'application/json' },
554
+ body: JSON.stringify({ path: filePath }),
555
+ });
556
+ if (!res.ok) {
557
+ const msg = await res
558
+ .json()
559
+ .then((e) => e.error)
560
+ .catch(() => `Delete failed (${res.status})`);
561
+ showToast(msg, 'error');
562
+ return;
563
+ }
564
+ closeModal('deleteConfirmModal');
565
+ delete modal.dataset.filePath;
566
+ selectedFileId = null;
567
+ await loadData();
568
+ showToast('File deleted', 'success');
569
+ } catch (err) {
570
+ showToast(err.message, 'error');
571
+ } finally {
572
+ btn.disabled = false;
573
+ btn.textContent = 'Delete';
574
+ }
575
+ }
576
+
533
577
  async function openInEditor(filePath) {
534
578
  showToast('Opening...', 'info');
535
579
  try {
@@ -617,7 +661,7 @@ async function refreshData() {
617
661
  try {
618
662
  await fetch('/api/refresh', { method: 'POST' });
619
663
  await loadData();
620
- showToast('Refreshed', 'success');
664
+ showToast('Data Refreshed', 'success');
621
665
  } catch (err) {
622
666
  showToast(err.message, 'error');
623
667
  }
@@ -765,18 +809,21 @@ document.addEventListener('DOMContentLoaded', async () => {
765
809
  loadTheme();
766
810
  initResize();
767
811
  bindModalKeys('projectPathInput', 'projectPickerModal', submitProjectPicker);
768
- // Handle ?project= query param
812
+ // Handle ?project= query param, else restore last-used project from localStorage
769
813
  const params = new URLSearchParams(location.search);
770
- if (params.has('project')) {
771
- const projectPath = params.get('project');
814
+ let desiredProject = params.get('project');
815
+ if (!desiredProject) desiredProject = getRecentProjects()[0] || null;
816
+ if (desiredProject) {
772
817
  try {
773
818
  const res = await fetch('/api/project', {
774
819
  method: 'PUT',
775
820
  headers: { 'Content-Type': 'application/json' },
776
- body: JSON.stringify({ path: projectPath }),
821
+ body: JSON.stringify({ path: desiredProject }),
777
822
  });
778
- if (!res.ok) showToast('Failed to switch project', 'error');
823
+ if (!res.ok && params.has('project')) showToast('Failed to switch project', 'error');
779
824
  } catch {}
825
+ }
826
+ if (params.has('project')) {
780
827
  params.delete('project');
781
828
  const qs = params.toString();
782
829
  history.replaceState(null, '', qs ? `?${qs}` : location.pathname + location.hash);
package/public/index.html CHANGED
@@ -104,6 +104,24 @@
104
104
  </div>
105
105
  </div>
106
106
 
107
+ <!-- Delete confirmation modal -->
108
+ <div class="modal-overlay" id="deleteConfirmModal" onclick="if(event.target===this)closeModal('deleteConfirmModal')">
109
+ <div class="modal">
110
+ <div class="modal-header">
111
+ <h3>Delete file?</h3>
112
+ <button class="modal-close" onclick="closeModal('deleteConfirmModal')">&#10005;</button>
113
+ </div>
114
+ <div class="modal-body">
115
+ <p class="modal-desc">This will permanently delete <strong id="deleteFileName"></strong> from disk.</p>
116
+ <div class="modal-filepath" id="deleteFilePath"></div>
117
+ <div class="modal-actions">
118
+ <button class="action-btn" onclick="closeModal('deleteConfirmModal')">Cancel</button>
119
+ <button class="action-btn danger" id="deleteConfirmSubmit" onclick="_submitDeleteFile()">Delete</button>
120
+ </div>
121
+ </div>
122
+ </div>
123
+ </div>
124
+
107
125
  <!-- Help modal -->
108
126
  <div class="modal-overlay" id="helpModal" onclick="if(event.target===this)toggleHelpModal()">
109
127
  <div class="modal">
package/public/style.css CHANGED
@@ -665,10 +665,33 @@ body.light .tree-conditional .file-name {
665
665
  .action-btn.primary:hover {
666
666
  filter: brightness(1.1);
667
667
  }
668
+ .action-btn.danger {
669
+ background: var(--error);
670
+ border-color: var(--error);
671
+ color: #fff;
672
+ }
673
+ .action-btn.danger:hover {
674
+ filter: brightness(1.1);
675
+ }
676
+ .modal-filepath {
677
+ font-family: 'IBM Plex Mono', monospace;
678
+ font-size: 11px;
679
+ color: var(--text-tertiary);
680
+ background: var(--bg-deep);
681
+ border: 1px solid var(--border);
682
+ border-radius: 4px;
683
+ padding: 6px 8px;
684
+ margin: 8px 0 12px;
685
+ word-break: break-all;
686
+ }
687
+ .preview-actions {
688
+ margin-left: auto;
689
+ display: inline-flex;
690
+ gap: 4px;
691
+ }
668
692
  .action-btn.small {
669
693
  padding: 2px 6px;
670
694
  font-size: 10px;
671
- margin-left: auto;
672
695
  display: inline-flex;
673
696
  align-items: center;
674
697
  justify-content: center;
@@ -787,8 +810,8 @@ kbd {
787
810
  color: var(--error);
788
811
  }
789
812
  .toast.success {
790
- border-color: var(--success);
791
- color: var(--success);
813
+ border-color: #2ea043;
814
+ color: #2ea043;
792
815
  }
793
816
  @keyframes slideIn {
794
817
  from {
package/server.js CHANGED
@@ -5,6 +5,7 @@ const express = require('express');
5
5
  const path = require('path');
6
6
  const fs = require('fs');
7
7
  const os = require('os');
8
+ const { execFileSync } = require('child_process');
8
9
 
9
10
  // #region CLI_ARGS
10
11
 
@@ -446,15 +447,38 @@ function findMemoryDir(projectPath) {
446
447
  const encoded = encodeProjectPath(projectPath);
447
448
  const memDir = path.join(projectsDir, encoded, 'memory');
448
449
  if (fs.existsSync(memDir)) return memDir;
450
+ const main = findMainWorktreePath(projectPath);
451
+ if (main) {
452
+ const mainMem = path.join(projectsDir, encodeProjectPath(main), 'memory');
453
+ if (fs.existsSync(mainMem)) return mainMem;
454
+ }
449
455
  return findMemoryDirBySubstring(projectPath);
450
456
  }
451
457
 
458
+ function findMainWorktreePath(projectPath) {
459
+ try {
460
+ const out = execFileSync('git', ['-C', projectPath, 'rev-parse', '--git-common-dir'], {
461
+ encoding: 'utf8',
462
+ stdio: ['ignore', 'pipe', 'ignore'],
463
+ timeout: 2000,
464
+ }).trim();
465
+ if (!out) return null;
466
+ const resolved = path.resolve(projectPath, out);
467
+ if (path.basename(resolved) !== '.git') return null;
468
+ const main = path.dirname(resolved);
469
+ if (main === path.resolve(projectPath)) return null;
470
+ return main;
471
+ } catch {
472
+ return null;
473
+ }
474
+ }
475
+
452
476
  function encodeProjectPath(projectPath) {
453
- // Claude Code uses double-dash encoding: C--Users-nikiforovall-dev-foo
477
+ // Claude Code encoding: `:` and `/` both become `-`, so `C:\Users\foo` → `C--Users-foo`
454
478
  return projectPath
455
479
  .replace(/\\/g, '/')
456
- .replace(/^([A-Za-z]):/, '$1') // strip colon but keep drive letter
457
- .replace(/\//g, '-'); // slashes become single dash; drive letter boundary becomes double dash naturally
480
+ .replace(/:/g, '-')
481
+ .replace(/\//g, '-');
458
482
  }
459
483
 
460
484
  function findMemoryDirBySubstring(projectPath) {
@@ -517,8 +541,10 @@ app.put('/api/project', (req, res) => {
517
541
  if (!dirPath) return res.status(400).json({ error: 'path required' });
518
542
  const resolved = path.resolve(dirPath.replace(/^~/, os.homedir()));
519
543
  if (!fs.existsSync(resolved)) return res.status(404).json({ error: 'directory not found' });
520
- currentProjectPath = resolved;
521
- clearCache();
544
+ if (resolved !== path.resolve(currentProjectPath)) {
545
+ currentProjectPath = resolved;
546
+ clearCache();
547
+ }
522
548
  res.json({ path: currentProjectPath, name: path.basename(currentProjectPath) });
523
549
  });
524
550
 
@@ -527,6 +553,25 @@ app.post('/api/refresh', (_req, res) => {
527
553
  res.json({ ok: true });
528
554
  });
529
555
 
556
+ app.delete('/api/file', (req, res) => {
557
+ const { path: filePath } = req.body || {};
558
+ if (!filePath) return res.status(400).json({ error: 'path required' });
559
+ const resolved = path.resolve(filePath.replace(/^~/, os.homedir()));
560
+ const roots = [path.resolve(CLAUDE_DIR), path.resolve(currentProjectPath)];
561
+ 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' });
563
+ try {
564
+ const stat = fs.statSync(resolved);
565
+ if (!stat.isFile()) return res.status(400).json({ error: 'not a file' });
566
+ fs.unlinkSync(resolved);
567
+ clearCache();
568
+ res.json({ ok: true });
569
+ } catch (err) {
570
+ if (err.code === 'ENOENT') return res.status(404).json({ error: 'file not found' });
571
+ res.status(500).json({ error: err.message });
572
+ }
573
+ });
574
+
530
575
  app.post('/api/open-in-editor', (req, res) => {
531
576
  const { path: filePath } = req.body;
532
577
  if (!filePath) return res.status(400).json({ error: 'path required' });