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 +1 -1
- package/public/app.js +54 -7
- package/public/index.html +18 -0
- package/public/style.css +26 -3
- package/server.js +50 -5
package/package.json
CHANGED
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 += '
|
|
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
|
-
|
|
771
|
-
|
|
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:
|
|
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')">✕</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:
|
|
791
|
-
color:
|
|
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
|
|
477
|
+
// Claude Code encoding: `:` and `/` both become `-`, so `C:\Users\foo` → `C--Users-foo`
|
|
454
478
|
return projectPath
|
|
455
479
|
.replace(/\\/g, '/')
|
|
456
|
-
.replace(
|
|
457
|
-
.replace(/\//g, '-');
|
|
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
|
|
521
|
-
|
|
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' });
|