claude-code-memory-explorer 0.2.5 → 0.3.0
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 +1 -0
- package/package.json +1 -1
- package/public/app.js +137 -20
- package/public/index.html +21 -3
- package/public/style.css +58 -8
- package/server.js +92 -70
package/README.md
CHANGED
|
@@ -62,6 +62,7 @@ Claude Code stores memory across multiple locations:
|
|
|
62
62
|
| CLAUDE.local.md | `./CLAUDE.local.md` | Always |
|
|
63
63
|
| Rules | `.claude/rules/*.md` | Conditional (path-scoped) |
|
|
64
64
|
| Auto memory | `~/.claude/projects/<encoded>/memory/` | Startup (MEMORY.md) or on-demand |
|
|
65
|
+
| Agent memory | `~/.claude/agent-memory/<agent>/`, `./.claude/agent-memory/<agent>/`, `./.claude/agent-memory-local/<agent>/` | Startup (MEMORY.md) or on-demand |
|
|
65
66
|
|
|
66
67
|
The dashboard:
|
|
67
68
|
1. **Scans** all memory locations — `~/.claude/`, ancestor directories, project rules, auto memory
|
package/package.json
CHANGED
package/public/app.js
CHANGED
|
@@ -243,14 +243,14 @@ function bindModalKeys(inputId, modalId, submitFn) {
|
|
|
243
243
|
|
|
244
244
|
// #region RENDER_TREE
|
|
245
245
|
|
|
246
|
-
const SCOPE_ORDER = ['policy', 'user', '
|
|
246
|
+
const SCOPE_ORDER = ['policy', 'user', 'project', 'rule', 'memory', 'agent-memory'];
|
|
247
247
|
const SCOPE_LABELS = {
|
|
248
248
|
policy: 'Managed Policy',
|
|
249
|
-
'user-memory': 'User Memory',
|
|
250
249
|
user: 'User',
|
|
251
250
|
project: 'Project',
|
|
252
251
|
rule: 'Rules',
|
|
253
252
|
memory: 'Auto Memory',
|
|
253
|
+
'agent-memory': 'Agent Memory',
|
|
254
254
|
};
|
|
255
255
|
const LOAD_ICONS = {
|
|
256
256
|
always: '\u25CF',
|
|
@@ -317,7 +317,8 @@ function renderTree() {
|
|
|
317
317
|
const meta = `${item.lines}L`;
|
|
318
318
|
const isConditional = item.load === 'conditional' || item.load === 'ondemand';
|
|
319
319
|
const pad = indent ? ` style="padding-left:${12 + indent * 16}px"` : '';
|
|
320
|
-
|
|
320
|
+
const muted = item.scope === 'agent-memory' ? ' tree-agent-item' : '';
|
|
321
|
+
let h = `<div class="tree-item${sel}${indent ? ' tree-child' : ''}${isConditional ? ' tree-conditional' : ''}${muted}" data-id="${esc(item.id)}" title="${esc(item.path)}" onclick="selectFile('${escJs(item.id)}')"${pad}>`;
|
|
321
322
|
h += `<span class="load-icon" title="${loadTitle}" style="color:var(--scope-${item.scope})">${loadIcon}</span>`;
|
|
322
323
|
h += `<span class="file-name">${esc(item.name)}</span>`;
|
|
323
324
|
h += `<span class="file-meta">${meta}</span>`;
|
|
@@ -334,12 +335,67 @@ function renderTree() {
|
|
|
334
335
|
const items = groups[scope];
|
|
335
336
|
if (!items) continue;
|
|
336
337
|
const label = SCOPE_LABELS[scope] || scope;
|
|
337
|
-
|
|
338
|
-
|
|
338
|
+
const collapsible = scope === 'agent-memory';
|
|
339
|
+
const collapsed = collapsible && isGroupCollapsed(scope);
|
|
340
|
+
const chevron = collapsible ? `<span class="group-chevron">${collapsed ? '\u25B8' : '\u25BE'}</span>` : '';
|
|
341
|
+
const headerClass = collapsible ? 'tree-group-header tree-group-clickable' : 'tree-group-header';
|
|
342
|
+
const onclick = collapsible ? ` onclick="_toggleGroup('${escJs(scope)}')"` : '';
|
|
343
|
+
html += `<div class="${headerClass}" data-scope="${esc(scope)}"${onclick}>${chevron}<span class="scope-dot" style="color:var(--scope-${scope})">\u25CF</span> ${esc(label)} <span style="opacity:0.5">${items.length}</span></div>`;
|
|
344
|
+
if (collapsed) continue;
|
|
345
|
+
if (scope === 'agent-memory') {
|
|
346
|
+
const sorted = [...items].sort((a, b) => {
|
|
347
|
+
const so = ['user', 'project', 'local'];
|
|
348
|
+
return (
|
|
349
|
+
so.indexOf(a.agentScope) - so.indexOf(b.agentScope) ||
|
|
350
|
+
a.agentName.localeCompare(b.agentName) ||
|
|
351
|
+
(a.name === 'MEMORY.md' ? -1 : 1)
|
|
352
|
+
);
|
|
353
|
+
});
|
|
354
|
+
let lastKey = null;
|
|
355
|
+
for (const item of sorted) {
|
|
356
|
+
const key = `${item.agentScope}/${item.agentName}`;
|
|
357
|
+
if (key !== lastKey) {
|
|
358
|
+
html += `<div class="tree-subgroup-header" title="${esc(item.agentScope)} scope"><span class="agent-name">${esc(item.agentName)}</span><span class="agent-scope-tag">${esc(item.agentScope)}</span></div>`;
|
|
359
|
+
lastKey = key;
|
|
360
|
+
}
|
|
361
|
+
html += renderItem(item, 1);
|
|
362
|
+
}
|
|
363
|
+
} else {
|
|
364
|
+
for (const item of items) html += renderItem(item, 0);
|
|
365
|
+
}
|
|
339
366
|
}
|
|
340
367
|
container.innerHTML = html;
|
|
341
368
|
}
|
|
342
369
|
|
|
370
|
+
const COLLAPSED_DEFAULTS = ['agent-memory'];
|
|
371
|
+
function getCollapsedGroups() {
|
|
372
|
+
try {
|
|
373
|
+
const stored = localStorage.getItem('collapsedGroups');
|
|
374
|
+
if (stored !== null) return new Set(JSON.parse(stored));
|
|
375
|
+
} catch {}
|
|
376
|
+
return new Set(COLLAPSED_DEFAULTS);
|
|
377
|
+
}
|
|
378
|
+
function isGroupCollapsed(scope) {
|
|
379
|
+
return getCollapsedGroups().has(scope);
|
|
380
|
+
}
|
|
381
|
+
function setGroupCollapsed(scope, collapsed) {
|
|
382
|
+
const set = getCollapsedGroups();
|
|
383
|
+
if (collapsed) set.add(scope);
|
|
384
|
+
else set.delete(scope);
|
|
385
|
+
localStorage.setItem('collapsedGroups', JSON.stringify([...set]));
|
|
386
|
+
}
|
|
387
|
+
function _toggleGroup(scope) {
|
|
388
|
+
setGroupCollapsed(scope, !isGroupCollapsed(scope));
|
|
389
|
+
renderTree();
|
|
390
|
+
}
|
|
391
|
+
function getItemScope(item) {
|
|
392
|
+
return item?.parentId ? stackData.find((s) => s.id === item.parentId)?.scope : item?.scope;
|
|
393
|
+
}
|
|
394
|
+
function expandGroupForItem(item) {
|
|
395
|
+
const scope = getItemScope(item);
|
|
396
|
+
if (scope && isGroupCollapsed(scope)) setGroupCollapsed(scope, false);
|
|
397
|
+
}
|
|
398
|
+
|
|
343
399
|
function pushFileState(id) {
|
|
344
400
|
const url = id ? `#${encodeURIComponent(id)}` : location.pathname;
|
|
345
401
|
history.pushState({ fileId: id }, '', url);
|
|
@@ -368,7 +424,9 @@ function navigateTree(direction) {
|
|
|
368
424
|
if (idx < 0) idx = navOrder.length - 1;
|
|
369
425
|
if (idx >= navOrder.length) idx = 0;
|
|
370
426
|
}
|
|
371
|
-
|
|
427
|
+
const target = navOrder[idx];
|
|
428
|
+
selectedFileId = target.id;
|
|
429
|
+
expandGroupForItem(target);
|
|
372
430
|
pushFileState(selectedFileId);
|
|
373
431
|
renderTree();
|
|
374
432
|
renderPreview();
|
|
@@ -381,8 +439,7 @@ function navigateGroup(direction) {
|
|
|
381
439
|
if (!activeScopes.length) return;
|
|
382
440
|
|
|
383
441
|
const current = stackData.find((s) => s.id === selectedFileId);
|
|
384
|
-
|
|
385
|
-
let scopeIdx = activeScopes.indexOf(currentScope);
|
|
442
|
+
let scopeIdx = activeScopes.indexOf(getItemScope(current));
|
|
386
443
|
if (scopeIdx === -1) {
|
|
387
444
|
scopeIdx = direction > 0 ? 0 : activeScopes.length - 1;
|
|
388
445
|
} else {
|
|
@@ -390,7 +447,9 @@ function navigateGroup(direction) {
|
|
|
390
447
|
if (scopeIdx < 0) scopeIdx = activeScopes.length - 1;
|
|
391
448
|
if (scopeIdx >= activeScopes.length) scopeIdx = 0;
|
|
392
449
|
}
|
|
393
|
-
|
|
450
|
+
const target = groups[activeScopes[scopeIdx]][0];
|
|
451
|
+
selectedFileId = target.id;
|
|
452
|
+
expandGroupForItem(target);
|
|
394
453
|
pushFileState(selectedFileId);
|
|
395
454
|
renderTree();
|
|
396
455
|
renderPreview();
|
|
@@ -401,6 +460,10 @@ function navigateGroup(direction) {
|
|
|
401
460
|
|
|
402
461
|
// #region MEMORY_INDEX
|
|
403
462
|
|
|
463
|
+
function isMemoryIndex(source) {
|
|
464
|
+
return (source.scope === 'memory' || source.scope === 'agent-memory') && source.name === 'MEMORY.md';
|
|
465
|
+
}
|
|
466
|
+
|
|
404
467
|
function parseMemoryIndex(content) {
|
|
405
468
|
const entries = [];
|
|
406
469
|
for (const line of content.split('\n')) {
|
|
@@ -458,6 +521,12 @@ async function renderPreview() {
|
|
|
458
521
|
html += `<span class="scope-badge scope-${source.scope}">${esc(source.scope)}</span>`;
|
|
459
522
|
html += `<span class="file-path">${esc(source.name)}</span>`;
|
|
460
523
|
html += '<div class="preview-actions">';
|
|
524
|
+
if (isMemoryIndex(source)) {
|
|
525
|
+
const broken = (source.unresolvedImports || []).length;
|
|
526
|
+
if (broken) {
|
|
527
|
+
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>`;
|
|
528
|
+
}
|
|
529
|
+
}
|
|
461
530
|
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
531
|
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
532
|
html += '</div></div>';
|
|
@@ -474,10 +543,13 @@ async function renderPreview() {
|
|
|
474
543
|
}
|
|
475
544
|
html += '</div>';
|
|
476
545
|
|
|
477
|
-
|
|
546
|
+
const content = fileData.content || '';
|
|
547
|
+
const memoryEntries = isMemoryIndex(source) ? parseMemoryIndex(content) : [];
|
|
548
|
+
const showMemoryTable = memoryEntries.length > 0;
|
|
549
|
+
|
|
478
550
|
const children = stackData.filter((s) => s.parentId === source.id);
|
|
479
551
|
const unresolved = source.unresolvedImports || [];
|
|
480
|
-
if (children.length || unresolved.length) {
|
|
552
|
+
if (!showMemoryTable && (children.length || unresolved.length)) {
|
|
481
553
|
html += '<div class="preview-imports">';
|
|
482
554
|
for (const child of children) {
|
|
483
555
|
html += `<a class="import-link" href="#" onclick="selectFile('${escJs(child.id)}');return false" title="${esc(child.path)}">${esc(child.name)}</a>`;
|
|
@@ -493,22 +565,19 @@ async function renderPreview() {
|
|
|
493
565
|
// File path
|
|
494
566
|
html += `<div class="preview-filepath">${esc(source.path)}</div>`;
|
|
495
567
|
|
|
496
|
-
// Content — with cutoff line for auto memory startup files
|
|
497
|
-
const content = fileData.content || '';
|
|
498
568
|
const hl = (text) => {
|
|
499
569
|
const { text: processed, placeholders } = linkifyImports(text, source.id);
|
|
500
570
|
return restorePlaceholders(highlightSource(processed, source.name), placeholders);
|
|
501
571
|
};
|
|
502
572
|
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
}
|
|
573
|
+
if (showMemoryTable) {
|
|
574
|
+
html += renderMemoryIndexTable(memoryEntries);
|
|
575
|
+
html += `<pre class="preview-code"><code>${hl(content)}</code></pre>`;
|
|
576
|
+
panel.innerHTML = html;
|
|
577
|
+
return;
|
|
509
578
|
}
|
|
510
579
|
|
|
511
|
-
if ((source.scope === 'memory' || source.scope === '
|
|
580
|
+
if ((source.scope === 'memory' || source.scope === 'agent-memory') && source.load === 'startup' && source.maxLines) {
|
|
512
581
|
const lines = content.split('\n');
|
|
513
582
|
const cutoff = source.maxLines;
|
|
514
583
|
if (lines.length > cutoff) {
|
|
@@ -574,6 +643,54 @@ async function _submitDeleteFile() {
|
|
|
574
643
|
}
|
|
575
644
|
}
|
|
576
645
|
|
|
646
|
+
function _cleanupOrphans(filePath) {
|
|
647
|
+
const source = stackData.find((s) => s.path === filePath);
|
|
648
|
+
const broken = source?.unresolvedImports || [];
|
|
649
|
+
if (!broken.length) {
|
|
650
|
+
showToast('No orphaned refs', 'info');
|
|
651
|
+
return;
|
|
652
|
+
}
|
|
653
|
+
const modal = document.getElementById('cleanupOrphansModal');
|
|
654
|
+
modal.dataset.filePath = filePath;
|
|
655
|
+
document.getElementById('cleanupOrphansCount').textContent = broken.length;
|
|
656
|
+
document.getElementById('cleanupOrphansList').innerHTML = broken.map((r) => `<li>${esc(r)}</li>`).join('');
|
|
657
|
+
modal.classList.add('open');
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
async function _submitCleanupOrphans() {
|
|
661
|
+
const modal = document.getElementById('cleanupOrphansModal');
|
|
662
|
+
const filePath = modal.dataset.filePath;
|
|
663
|
+
if (!filePath) return;
|
|
664
|
+
const btn = document.getElementById('cleanupOrphansSubmit');
|
|
665
|
+
btn.disabled = true;
|
|
666
|
+
btn.textContent = 'Cleaning...';
|
|
667
|
+
try {
|
|
668
|
+
const res = await fetch('/api/memory/cleanup-orphans', {
|
|
669
|
+
method: 'POST',
|
|
670
|
+
headers: { 'Content-Type': 'application/json' },
|
|
671
|
+
body: JSON.stringify({ path: filePath }),
|
|
672
|
+
});
|
|
673
|
+
if (!res.ok) {
|
|
674
|
+
const msg = await res
|
|
675
|
+
.json()
|
|
676
|
+
.then((e) => e.error)
|
|
677
|
+
.catch(() => `Cleanup failed (${res.status})`);
|
|
678
|
+
showToast(msg, 'error');
|
|
679
|
+
return;
|
|
680
|
+
}
|
|
681
|
+
const data = await res.json();
|
|
682
|
+
closeModal('cleanupOrphansModal');
|
|
683
|
+
delete modal.dataset.filePath;
|
|
684
|
+
await loadData();
|
|
685
|
+
showToast(`Removed ${data.removed.length} orphaned ref${data.removed.length === 1 ? '' : 's'}`, 'success');
|
|
686
|
+
} catch (err) {
|
|
687
|
+
showToast(err.message, 'error');
|
|
688
|
+
} finally {
|
|
689
|
+
btn.disabled = false;
|
|
690
|
+
btn.textContent = 'Remove';
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
|
|
577
694
|
async function openInEditor(filePath) {
|
|
578
695
|
showToast('Opening...', 'info');
|
|
579
696
|
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
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
--scope-local: #8a9bb5;
|
|
25
25
|
--scope-rule: #a89bc4;
|
|
26
26
|
--scope-memory: #c4a06a;
|
|
27
|
-
--scope-
|
|
27
|
+
--scope-agent-memory: #8aa8a3;
|
|
28
28
|
--scope-policy: #b58a8a;
|
|
29
29
|
--font-mono: 'IBM Plex Mono', monospace;
|
|
30
30
|
--font-serif: 'Playfair Display', serif;
|
|
@@ -261,6 +261,49 @@ body {
|
|
|
261
261
|
align-items: center;
|
|
262
262
|
gap: 6px;
|
|
263
263
|
}
|
|
264
|
+
.tree-subgroup-header {
|
|
265
|
+
padding: 6px 12px 2px 20px;
|
|
266
|
+
font-size: 11px;
|
|
267
|
+
font-family: var(--font-mono);
|
|
268
|
+
color: var(--text-muted);
|
|
269
|
+
display: flex;
|
|
270
|
+
align-items: center;
|
|
271
|
+
gap: 8px;
|
|
272
|
+
opacity: 0.85;
|
|
273
|
+
}
|
|
274
|
+
.tree-subgroup-header .agent-name {
|
|
275
|
+
color: var(--text-tertiary);
|
|
276
|
+
}
|
|
277
|
+
.tree-subgroup-header .agent-scope-tag {
|
|
278
|
+
font-size: 9px;
|
|
279
|
+
font-weight: 600;
|
|
280
|
+
text-transform: uppercase;
|
|
281
|
+
letter-spacing: 0.4px;
|
|
282
|
+
padding: 1px 5px;
|
|
283
|
+
border-radius: 3px;
|
|
284
|
+
background: rgba(138, 168, 163, 0.12);
|
|
285
|
+
color: var(--scope-agent-memory);
|
|
286
|
+
opacity: 0.85;
|
|
287
|
+
}
|
|
288
|
+
.tree-group-clickable {
|
|
289
|
+
cursor: pointer;
|
|
290
|
+
user-select: none;
|
|
291
|
+
}
|
|
292
|
+
.tree-group-clickable:hover {
|
|
293
|
+
color: var(--text-secondary);
|
|
294
|
+
}
|
|
295
|
+
.group-chevron {
|
|
296
|
+
display: inline-block;
|
|
297
|
+
width: 10px;
|
|
298
|
+
font-size: 9px;
|
|
299
|
+
color: var(--text-muted);
|
|
300
|
+
}
|
|
301
|
+
.tree-agent-item .file-name {
|
|
302
|
+
opacity: 0.7;
|
|
303
|
+
}
|
|
304
|
+
body.light .tree-agent-item .file-name {
|
|
305
|
+
opacity: 0.8;
|
|
306
|
+
}
|
|
264
307
|
.tree-item {
|
|
265
308
|
padding: 5px 12px 5px 24px;
|
|
266
309
|
cursor: pointer;
|
|
@@ -411,20 +454,27 @@ body.light .tree-conditional .file-name {
|
|
|
411
454
|
}
|
|
412
455
|
.memory-index-row {
|
|
413
456
|
display: grid;
|
|
414
|
-
grid-template-columns: 72px
|
|
457
|
+
grid-template-columns: 72px max-content 1fr;
|
|
415
458
|
align-items: baseline;
|
|
416
459
|
gap: 8px;
|
|
417
460
|
padding: 3px 0;
|
|
418
461
|
}
|
|
419
462
|
.memory-index-type {
|
|
420
|
-
|
|
463
|
+
text-align: center;
|
|
464
|
+
justify-self: center;
|
|
465
|
+
}
|
|
466
|
+
.modal-orphan-list {
|
|
467
|
+
margin: 0;
|
|
468
|
+
padding: 8px 12px 8px 28px;
|
|
469
|
+
list-style: disc;
|
|
470
|
+
font-family: var(--font-mono);
|
|
471
|
+
font-size: 11px;
|
|
472
|
+
color: var(--text-muted);
|
|
421
473
|
}
|
|
422
474
|
.memory-index-name {
|
|
423
475
|
font-size: 11px;
|
|
424
476
|
font-family: var(--font-mono);
|
|
425
477
|
white-space: nowrap;
|
|
426
|
-
overflow: hidden;
|
|
427
|
-
text-overflow: ellipsis;
|
|
428
478
|
}
|
|
429
479
|
.memory-index-desc {
|
|
430
480
|
font-size: 10px;
|
|
@@ -484,9 +534,9 @@ body.light .tree-conditional .file-name {
|
|
|
484
534
|
background: rgba(196, 149, 106, 0.18);
|
|
485
535
|
color: var(--scope-memory);
|
|
486
536
|
}
|
|
487
|
-
.scope-
|
|
488
|
-
background: rgba(
|
|
489
|
-
color: var(--scope-
|
|
537
|
+
.scope-agent-memory {
|
|
538
|
+
background: rgba(138, 168, 163, 0.18);
|
|
539
|
+
color: var(--scope-agent-memory);
|
|
490
540
|
}
|
|
491
541
|
.scope-policy {
|
|
492
542
|
background: rgba(181, 138, 138, 0.18);
|
package/server.js
CHANGED
|
@@ -52,6 +52,10 @@ function clearCache() {
|
|
|
52
52
|
|
|
53
53
|
// #region FILESYSTEM_SCANNING
|
|
54
54
|
|
|
55
|
+
const CLAUDE_MD_VARIANTS = ['CLAUDE.md', '.claude/CLAUDE.md', 'CLAUDE.local.md', '.claude/CLAUDE.local.md'];
|
|
56
|
+
|
|
57
|
+
const slug = s => s.replace(/[^a-zA-Z0-9]/g, '-');
|
|
58
|
+
|
|
55
59
|
const MANAGED_POLICY_PATHS = process.platform === 'win32'
|
|
56
60
|
? [path.join(process.env.ProgramFiles || 'C:\\Program Files', 'ClaudeCode', 'CLAUDE.md')]
|
|
57
61
|
: process.platform === 'darwin'
|
|
@@ -160,6 +164,11 @@ function spreadImports(filePath, content) {
|
|
|
160
164
|
return { imports: resolved, softImports: resolvedSoft, unresolvedImports: unresolved };
|
|
161
165
|
}
|
|
162
166
|
|
|
167
|
+
function hasPathsFilter(frontmatter) {
|
|
168
|
+
if (!frontmatter || !frontmatter.paths) return false;
|
|
169
|
+
return Array.isArray(frontmatter.paths) ? frontmatter.paths.length > 0 : true;
|
|
170
|
+
}
|
|
171
|
+
|
|
163
172
|
function discoverMemorySources(projectPath) {
|
|
164
173
|
const sources = [];
|
|
165
174
|
|
|
@@ -202,7 +211,7 @@ function discoverMemorySources(projectPath) {
|
|
|
202
211
|
id: `user-rule-${path.basename(file, '.md')}`,
|
|
203
212
|
name: path.basename(file),
|
|
204
213
|
scope: 'rule',
|
|
205
|
-
load: 'conditional',
|
|
214
|
+
load: hasPathsFilter(info.frontmatter) ? 'conditional' : 'always',
|
|
206
215
|
...info,
|
|
207
216
|
ruleSource: 'user',
|
|
208
217
|
...spreadImports(info.path, info.content),
|
|
@@ -214,16 +223,16 @@ function discoverMemorySources(projectPath) {
|
|
|
214
223
|
const ancestors = getAncestorDirs(projectPath);
|
|
215
224
|
const seenPaths = new Set(sources.map(s => s.path));
|
|
216
225
|
for (const dir of ancestors) {
|
|
217
|
-
|
|
218
|
-
|
|
226
|
+
const isProjectRoot = path.resolve(dir) === path.resolve(projectPath);
|
|
227
|
+
for (const rel of CLAUDE_MD_VARIANTS) {
|
|
228
|
+
const filePath = path.join(dir, rel);
|
|
219
229
|
if (seenPaths.has(filePath)) continue;
|
|
220
230
|
const info = fileInfo(filePath);
|
|
221
231
|
if (!info) continue;
|
|
222
232
|
seenPaths.add(filePath);
|
|
223
|
-
const isProjectRoot = path.resolve(dir) === path.resolve(projectPath);
|
|
224
233
|
sources.push({
|
|
225
|
-
id: `project-claude-md-${
|
|
226
|
-
name:
|
|
234
|
+
id: `project-claude-md-${slug(rel)}-${slug(dir)}`,
|
|
235
|
+
name: rel,
|
|
227
236
|
scope: 'project',
|
|
228
237
|
load: 'always',
|
|
229
238
|
...info,
|
|
@@ -231,23 +240,6 @@ function discoverMemorySources(projectPath) {
|
|
|
231
240
|
isProjectRoot,
|
|
232
241
|
...spreadImports(info.path, info.content),
|
|
233
242
|
});
|
|
234
|
-
break;
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
const localPath = path.join(dir, 'CLAUDE.local.md');
|
|
238
|
-
if (seenPaths.has(localPath)) continue;
|
|
239
|
-
const localInfo = fileInfo(localPath);
|
|
240
|
-
if (localInfo) {
|
|
241
|
-
seenPaths.add(localPath);
|
|
242
|
-
sources.push({
|
|
243
|
-
id: `local-claude-md-${dir.replace(/[^a-zA-Z0-9]/g, '-')}`,
|
|
244
|
-
name: 'CLAUDE.local.md',
|
|
245
|
-
scope: 'project',
|
|
246
|
-
load: 'always',
|
|
247
|
-
...localInfo,
|
|
248
|
-
dir,
|
|
249
|
-
...spreadImports(localInfo.path, localInfo.content),
|
|
250
|
-
});
|
|
251
243
|
}
|
|
252
244
|
}
|
|
253
245
|
|
|
@@ -260,7 +252,7 @@ function discoverMemorySources(projectPath) {
|
|
|
260
252
|
for (const entry of entries) {
|
|
261
253
|
if (!entry.isDirectory() || SKIP_DIRS.has(entry.name) || entry.name.startsWith('.')) continue;
|
|
262
254
|
const subdir = path.join(dir, entry.name);
|
|
263
|
-
for (const name of
|
|
255
|
+
for (const name of CLAUDE_MD_VARIANTS) {
|
|
264
256
|
const filePath = path.join(subdir, name);
|
|
265
257
|
if (seenPaths.has(filePath)) continue;
|
|
266
258
|
const info = fileInfo(filePath);
|
|
@@ -268,8 +260,8 @@ function discoverMemorySources(projectPath) {
|
|
|
268
260
|
seenPaths.add(filePath);
|
|
269
261
|
const rel = path.relative(projectPath, subdir).replace(/\\/g, '/');
|
|
270
262
|
sources.push({
|
|
271
|
-
id: `project-claude-md-${
|
|
272
|
-
name: `${rel}/${name
|
|
263
|
+
id: `project-claude-md-${slug(name)}-${slug(subdir)}`,
|
|
264
|
+
name: `${rel}/${name}`,
|
|
273
265
|
scope: 'project',
|
|
274
266
|
load: 'tree',
|
|
275
267
|
...info,
|
|
@@ -293,7 +285,7 @@ function discoverMemorySources(projectPath) {
|
|
|
293
285
|
id: `project-rule-${path.basename(file, '.md')}`,
|
|
294
286
|
name: path.basename(file),
|
|
295
287
|
scope: 'rule',
|
|
296
|
-
load: 'conditional',
|
|
288
|
+
load: hasPathsFilter(info.frontmatter) ? 'conditional' : 'always',
|
|
297
289
|
...info,
|
|
298
290
|
ruleSource: 'project',
|
|
299
291
|
...spreadImports(info.path, info.content),
|
|
@@ -301,67 +293,61 @@ function discoverMemorySources(projectPath) {
|
|
|
301
293
|
}
|
|
302
294
|
}
|
|
303
295
|
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
if (memoryDir && fs.existsSync(memoryDir)) {
|
|
307
|
-
const memoryMd = path.join(memoryDir, 'MEMORY.md');
|
|
296
|
+
function pushMemoryDir(dir, scope, idPrefix, extraFields = {}) {
|
|
297
|
+
const memoryMd = path.join(dir, 'MEMORY.md');
|
|
308
298
|
const memInfo = fileInfo(memoryMd);
|
|
309
299
|
if (memInfo) {
|
|
310
300
|
sources.push({
|
|
311
|
-
id:
|
|
301
|
+
id: `${idPrefix}-index`,
|
|
312
302
|
name: 'MEMORY.md',
|
|
313
|
-
scope
|
|
303
|
+
scope,
|
|
314
304
|
load: 'startup',
|
|
305
|
+
...extraFields,
|
|
315
306
|
...memInfo,
|
|
316
307
|
maxLines: 200,
|
|
317
308
|
maxBytes: 25 * 1024,
|
|
318
309
|
...spreadImports(memInfo.path, memInfo.content),
|
|
319
310
|
});
|
|
320
311
|
}
|
|
321
|
-
for (const file of findMdFiles(
|
|
312
|
+
for (const file of findMdFiles(dir)) {
|
|
322
313
|
if (path.basename(file) === 'MEMORY.md') continue;
|
|
323
314
|
const info = fileInfo(file);
|
|
324
315
|
if (!info) continue;
|
|
325
316
|
sources.push({
|
|
326
|
-
id:
|
|
317
|
+
id: `${idPrefix}-${path.basename(file, '.md')}`,
|
|
327
318
|
name: path.basename(file),
|
|
328
|
-
scope
|
|
319
|
+
scope,
|
|
329
320
|
load: 'ondemand',
|
|
321
|
+
...extraFields,
|
|
330
322
|
...info,
|
|
331
323
|
...spreadImports(info.path, info.content),
|
|
332
324
|
});
|
|
333
325
|
}
|
|
334
326
|
}
|
|
335
327
|
|
|
336
|
-
//
|
|
337
|
-
const
|
|
338
|
-
if (fs.existsSync(
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
name: path.basename(file),
|
|
360
|
-
scope: 'user-memory',
|
|
361
|
-
load: 'ondemand',
|
|
362
|
-
...info,
|
|
363
|
-
...spreadImports(info.path, info.content),
|
|
364
|
-
});
|
|
328
|
+
// 6. Auto memory
|
|
329
|
+
const memoryDir = findMemoryDir(projectPath);
|
|
330
|
+
if (memoryDir && fs.existsSync(memoryDir)) {
|
|
331
|
+
pushMemoryDir(memoryDir, 'memory', 'memory');
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// 7. Subagent persistent memory (https://code.claude.com/docs/en/sub-agents#enable-persistent-memory)
|
|
335
|
+
// user: ~/.claude/agent-memory/<agent>/
|
|
336
|
+
// project: <project>/.claude/agent-memory/<agent>/
|
|
337
|
+
// local: <project>/.claude/agent-memory-local/<agent>/
|
|
338
|
+
const agentMemoryRoots = [
|
|
339
|
+
{ root: path.join(os.homedir(), '.claude', 'agent-memory'), agentScope: 'user' },
|
|
340
|
+
{ root: path.join(projectPath, '.claude', 'agent-memory'), agentScope: 'project' },
|
|
341
|
+
{ root: path.join(projectPath, '.claude', 'agent-memory-local'), agentScope: 'local' },
|
|
342
|
+
];
|
|
343
|
+
for (const { root, agentScope } of agentMemoryRoots) {
|
|
344
|
+
let agentDirs;
|
|
345
|
+
try { agentDirs = fs.readdirSync(root, { withFileTypes: true }); } catch { continue; }
|
|
346
|
+
for (const entry of agentDirs) {
|
|
347
|
+
if (!entry.isDirectory()) continue;
|
|
348
|
+
const agentName = entry.name;
|
|
349
|
+
const idPrefix = `agent-memory-${agentScope}-${agentName.replace(/[^a-zA-Z0-9]/g, '-')}`;
|
|
350
|
+
pushMemoryDir(path.join(root, agentName), 'agent-memory', idPrefix, { agentScope, agentName });
|
|
365
351
|
}
|
|
366
352
|
}
|
|
367
353
|
|
|
@@ -379,7 +365,7 @@ function discoverMemorySources(projectPath) {
|
|
|
379
365
|
for (const { imp, parent, hard } of batch) {
|
|
380
366
|
if (seen.has(imp)) {
|
|
381
367
|
const existing = sourceByPath[imp];
|
|
382
|
-
if (existing && !existing.parentId && existing.scope === 'memory') {
|
|
368
|
+
if (existing && !existing.parentId && (existing.scope === 'memory' || existing.scope === 'agent-memory')) {
|
|
383
369
|
existing.parentId = parent.id;
|
|
384
370
|
if (hard) existing.load = 'import';
|
|
385
371
|
}
|
|
@@ -553,13 +539,18 @@ app.post('/api/refresh', (_req, res) => {
|
|
|
553
539
|
res.json({ ok: true });
|
|
554
540
|
});
|
|
555
541
|
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
if (!filePath) return res.status(400).json({ error: 'path required' });
|
|
542
|
+
function resolveAllowedPath(filePath) {
|
|
543
|
+
if (!filePath) return { error: { status: 400, message: 'path required' } };
|
|
559
544
|
const resolved = path.resolve(filePath.replace(/^~/, os.homedir()));
|
|
560
545
|
const roots = [path.resolve(CLAUDE_DIR), path.resolve(currentProjectPath)];
|
|
561
546
|
const inRoot = roots.some((r) => resolved === r || resolved.startsWith(r + path.sep));
|
|
562
|
-
if (!inRoot) return
|
|
547
|
+
if (!inRoot) return { error: { status: 403, message: 'path outside allowed roots' } };
|
|
548
|
+
return { resolved };
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
app.delete('/api/file', (req, res) => {
|
|
552
|
+
const { resolved, error } = resolveAllowedPath(req.body?.path);
|
|
553
|
+
if (error) return res.status(error.status).json({ error: error.message });
|
|
563
554
|
try {
|
|
564
555
|
const stat = fs.statSync(resolved);
|
|
565
556
|
if (!stat.isFile()) return res.status(400).json({ error: 'not a file' });
|
|
@@ -572,6 +563,37 @@ app.delete('/api/file', (req, res) => {
|
|
|
572
563
|
}
|
|
573
564
|
});
|
|
574
565
|
|
|
566
|
+
app.post('/api/memory/cleanup-orphans', (req, res) => {
|
|
567
|
+
const { resolved, error } = resolveAllowedPath(req.body?.path);
|
|
568
|
+
if (error) return res.status(error.status).json({ error: error.message });
|
|
569
|
+
if (path.basename(resolved) !== 'MEMORY.md') return res.status(400).json({ error: 'not a MEMORY.md file' });
|
|
570
|
+
try {
|
|
571
|
+
const content = fs.readFileSync(resolved, 'utf-8');
|
|
572
|
+
const dir = path.dirname(resolved);
|
|
573
|
+
const lines = content.split('\n');
|
|
574
|
+
const removed = [];
|
|
575
|
+
const kept = [];
|
|
576
|
+
const linkRe = /^\s*-\s*\[([^\]]+)\]\(([^)]+\.md)\)/;
|
|
577
|
+
for (const line of lines) {
|
|
578
|
+
const m = line.match(linkRe);
|
|
579
|
+
if (m) {
|
|
580
|
+
const target = path.resolve(dir, m[2]);
|
|
581
|
+
if (!fs.existsSync(target)) {
|
|
582
|
+
removed.push({ name: m[1], file: m[2] });
|
|
583
|
+
continue;
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
kept.push(line);
|
|
587
|
+
}
|
|
588
|
+
if (removed.length === 0) return res.json({ ok: true, removed: [] });
|
|
589
|
+
fs.writeFileSync(resolved, kept.join('\n'), 'utf-8');
|
|
590
|
+
clearCache();
|
|
591
|
+
res.json({ ok: true, removed });
|
|
592
|
+
} catch (err) {
|
|
593
|
+
res.status(500).json({ error: err.message });
|
|
594
|
+
}
|
|
595
|
+
});
|
|
596
|
+
|
|
575
597
|
app.post('/api/open-in-editor', (req, res) => {
|
|
576
598
|
const { path: filePath } = req.body;
|
|
577
599
|
if (!filePath) return res.status(400).json({ error: 'path required' });
|