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 +1 -1
- package/public/app.js +68 -10
- package/public/index.html +21 -3
- package/public/style.css +11 -4
- package/server.js +47 -6
package/package.json
CHANGED
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
|
-
|
|
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
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
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')">✕</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
|
|
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
|
-
|
|
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
|
-
|
|
557
|
-
|
|
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
|
|
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' });
|