claude-code-memory-explorer 0.2.4 → 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 +122 -17
- package/public/index.html +39 -3
- package/public/style.css +37 -7
- package/server.js +93 -7
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')) {
|
|
@@ -457,8 +461,16 @@ async function renderPreview() {
|
|
|
457
461
|
html += '<div class="preview-title">';
|
|
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>`;
|
|
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
|
+
}
|
|
460
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>`;
|
|
461
|
-
html += '
|
|
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>`;
|
|
473
|
+
html += '</div></div>';
|
|
462
474
|
|
|
463
475
|
// Badges row
|
|
464
476
|
html += '<div class="preview-badges">';
|
|
@@ -472,10 +484,13 @@ async function renderPreview() {
|
|
|
472
484
|
}
|
|
473
485
|
html += '</div>';
|
|
474
486
|
|
|
475
|
-
|
|
487
|
+
const content = fileData.content || '';
|
|
488
|
+
const memoryEntries = isMemoryIndex(source) ? parseMemoryIndex(content) : [];
|
|
489
|
+
const showMemoryTable = memoryEntries.length > 0;
|
|
490
|
+
|
|
476
491
|
const children = stackData.filter((s) => s.parentId === source.id);
|
|
477
492
|
const unresolved = source.unresolvedImports || [];
|
|
478
|
-
if (children.length || unresolved.length) {
|
|
493
|
+
if (!showMemoryTable && (children.length || unresolved.length)) {
|
|
479
494
|
html += '<div class="preview-imports">';
|
|
480
495
|
for (const child of children) {
|
|
481
496
|
html += `<a class="import-link" href="#" onclick="selectFile('${escJs(child.id)}');return false" title="${esc(child.path)}">${esc(child.name)}</a>`;
|
|
@@ -491,19 +506,16 @@ async function renderPreview() {
|
|
|
491
506
|
// File path
|
|
492
507
|
html += `<div class="preview-filepath">${esc(source.path)}</div>`;
|
|
493
508
|
|
|
494
|
-
// Content — with cutoff line for auto memory startup files
|
|
495
|
-
const content = fileData.content || '';
|
|
496
509
|
const hl = (text) => {
|
|
497
510
|
const { text: processed, placeholders } = linkifyImports(text, source.id);
|
|
498
511
|
return restorePlaceholders(highlightSource(processed, source.name), placeholders);
|
|
499
512
|
};
|
|
500
513
|
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
}
|
|
514
|
+
if (showMemoryTable) {
|
|
515
|
+
html += renderMemoryIndexTable(memoryEntries);
|
|
516
|
+
html += `<pre class="preview-code"><code>${hl(content)}</code></pre>`;
|
|
517
|
+
panel.innerHTML = html;
|
|
518
|
+
return;
|
|
507
519
|
}
|
|
508
520
|
|
|
509
521
|
if ((source.scope === 'memory' || source.scope === 'user-memory') && source.load === 'startup' && source.maxLines) {
|
|
@@ -530,6 +542,96 @@ function formatBytes(b) {
|
|
|
530
542
|
return `${(b / 1024).toFixed(1)}KB`;
|
|
531
543
|
}
|
|
532
544
|
|
|
545
|
+
function _confirmDeleteFile(filePath, fileName) {
|
|
546
|
+
const modal = document.getElementById('deleteConfirmModal');
|
|
547
|
+
modal.dataset.filePath = filePath;
|
|
548
|
+
document.getElementById('deleteFileName').textContent = fileName;
|
|
549
|
+
document.getElementById('deleteFilePath').textContent = filePath;
|
|
550
|
+
modal.classList.add('open');
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
async function _submitDeleteFile() {
|
|
554
|
+
const modal = document.getElementById('deleteConfirmModal');
|
|
555
|
+
const filePath = modal.dataset.filePath;
|
|
556
|
+
if (!filePath) return;
|
|
557
|
+
const btn = document.getElementById('deleteConfirmSubmit');
|
|
558
|
+
btn.disabled = true;
|
|
559
|
+
btn.textContent = 'Deleting...';
|
|
560
|
+
try {
|
|
561
|
+
const res = await fetch('/api/file', {
|
|
562
|
+
method: 'DELETE',
|
|
563
|
+
headers: { 'Content-Type': 'application/json' },
|
|
564
|
+
body: JSON.stringify({ path: filePath }),
|
|
565
|
+
});
|
|
566
|
+
if (!res.ok) {
|
|
567
|
+
const msg = await res
|
|
568
|
+
.json()
|
|
569
|
+
.then((e) => e.error)
|
|
570
|
+
.catch(() => `Delete failed (${res.status})`);
|
|
571
|
+
showToast(msg, 'error');
|
|
572
|
+
return;
|
|
573
|
+
}
|
|
574
|
+
closeModal('deleteConfirmModal');
|
|
575
|
+
delete modal.dataset.filePath;
|
|
576
|
+
selectedFileId = null;
|
|
577
|
+
await loadData();
|
|
578
|
+
showToast('File deleted', 'success');
|
|
579
|
+
} catch (err) {
|
|
580
|
+
showToast(err.message, 'error');
|
|
581
|
+
} finally {
|
|
582
|
+
btn.disabled = false;
|
|
583
|
+
btn.textContent = 'Delete';
|
|
584
|
+
}
|
|
585
|
+
}
|
|
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
|
+
|
|
533
635
|
async function openInEditor(filePath) {
|
|
534
636
|
showToast('Opening...', 'info');
|
|
535
637
|
try {
|
|
@@ -617,7 +719,7 @@ async function refreshData() {
|
|
|
617
719
|
try {
|
|
618
720
|
await fetch('/api/refresh', { method: 'POST' });
|
|
619
721
|
await loadData();
|
|
620
|
-
showToast('Refreshed', 'success');
|
|
722
|
+
showToast('Data Refreshed', 'success');
|
|
621
723
|
} catch (err) {
|
|
622
724
|
showToast(err.message, 'error');
|
|
623
725
|
}
|
|
@@ -765,18 +867,21 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|
|
765
867
|
loadTheme();
|
|
766
868
|
initResize();
|
|
767
869
|
bindModalKeys('projectPathInput', 'projectPickerModal', submitProjectPicker);
|
|
768
|
-
// Handle ?project= query param
|
|
870
|
+
// Handle ?project= query param, else restore last-used project from localStorage
|
|
769
871
|
const params = new URLSearchParams(location.search);
|
|
770
|
-
|
|
771
|
-
|
|
872
|
+
let desiredProject = params.get('project');
|
|
873
|
+
if (!desiredProject) desiredProject = getRecentProjects()[0] || null;
|
|
874
|
+
if (desiredProject) {
|
|
772
875
|
try {
|
|
773
876
|
const res = await fetch('/api/project', {
|
|
774
877
|
method: 'PUT',
|
|
775
878
|
headers: { 'Content-Type': 'application/json' },
|
|
776
|
-
body: JSON.stringify({ path:
|
|
879
|
+
body: JSON.stringify({ path: desiredProject }),
|
|
777
880
|
});
|
|
778
|
-
if (!res.ok) showToast('Failed to switch project', 'error');
|
|
881
|
+
if (!res.ok && params.has('project')) showToast('Failed to switch project', 'error');
|
|
779
882
|
} catch {}
|
|
883
|
+
}
|
|
884
|
+
if (params.has('project')) {
|
|
780
885
|
params.delete('project');
|
|
781
886
|
const qs = params.toString();
|
|
782
887
|
history.replaceState(null, '', qs ? `?${qs}` : location.pathname + location.hash);
|
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>
|
|
@@ -104,6 +104,42 @@
|
|
|
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
|
+
|
|
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
|
+
|
|
107
143
|
<!-- Help modal -->
|
|
108
144
|
<div class="modal-overlay" id="helpModal" onclick="if(event.target===this)toggleHelpModal()">
|
|
109
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;
|
|
@@ -665,10 +672,33 @@ body.light .tree-conditional .file-name {
|
|
|
665
672
|
.action-btn.primary:hover {
|
|
666
673
|
filter: brightness(1.1);
|
|
667
674
|
}
|
|
675
|
+
.action-btn.danger {
|
|
676
|
+
background: var(--error);
|
|
677
|
+
border-color: var(--error);
|
|
678
|
+
color: #fff;
|
|
679
|
+
}
|
|
680
|
+
.action-btn.danger:hover {
|
|
681
|
+
filter: brightness(1.1);
|
|
682
|
+
}
|
|
683
|
+
.modal-filepath {
|
|
684
|
+
font-family: 'IBM Plex Mono', monospace;
|
|
685
|
+
font-size: 11px;
|
|
686
|
+
color: var(--text-tertiary);
|
|
687
|
+
background: var(--bg-deep);
|
|
688
|
+
border: 1px solid var(--border);
|
|
689
|
+
border-radius: 4px;
|
|
690
|
+
padding: 6px 8px;
|
|
691
|
+
margin: 8px 0 12px;
|
|
692
|
+
word-break: break-all;
|
|
693
|
+
}
|
|
694
|
+
.preview-actions {
|
|
695
|
+
margin-left: auto;
|
|
696
|
+
display: inline-flex;
|
|
697
|
+
gap: 4px;
|
|
698
|
+
}
|
|
668
699
|
.action-btn.small {
|
|
669
700
|
padding: 2px 6px;
|
|
670
701
|
font-size: 10px;
|
|
671
|
-
margin-left: auto;
|
|
672
702
|
display: inline-flex;
|
|
673
703
|
align-items: center;
|
|
674
704
|
justify-content: center;
|
|
@@ -787,8 +817,8 @@ kbd {
|
|
|
787
817
|
color: var(--error);
|
|
788
818
|
}
|
|
789
819
|
.toast.success {
|
|
790
|
-
border-color:
|
|
791
|
-
color:
|
|
820
|
+
border-color: #2ea043;
|
|
821
|
+
color: #2ea043;
|
|
792
822
|
}
|
|
793
823
|
@keyframes slideIn {
|
|
794
824
|
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
|
|
|
@@ -159,6 +160,11 @@ function spreadImports(filePath, content) {
|
|
|
159
160
|
return { imports: resolved, softImports: resolvedSoft, unresolvedImports: unresolved };
|
|
160
161
|
}
|
|
161
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
|
+
|
|
162
168
|
function discoverMemorySources(projectPath) {
|
|
163
169
|
const sources = [];
|
|
164
170
|
|
|
@@ -201,7 +207,7 @@ function discoverMemorySources(projectPath) {
|
|
|
201
207
|
id: `user-rule-${path.basename(file, '.md')}`,
|
|
202
208
|
name: path.basename(file),
|
|
203
209
|
scope: 'rule',
|
|
204
|
-
load: 'conditional',
|
|
210
|
+
load: hasPathsFilter(info.frontmatter) ? 'conditional' : 'always',
|
|
205
211
|
...info,
|
|
206
212
|
ruleSource: 'user',
|
|
207
213
|
...spreadImports(info.path, info.content),
|
|
@@ -292,7 +298,7 @@ function discoverMemorySources(projectPath) {
|
|
|
292
298
|
id: `project-rule-${path.basename(file, '.md')}`,
|
|
293
299
|
name: path.basename(file),
|
|
294
300
|
scope: 'rule',
|
|
295
|
-
load: 'conditional',
|
|
301
|
+
load: hasPathsFilter(info.frontmatter) ? 'conditional' : 'always',
|
|
296
302
|
...info,
|
|
297
303
|
ruleSource: 'project',
|
|
298
304
|
...spreadImports(info.path, info.content),
|
|
@@ -446,15 +452,38 @@ function findMemoryDir(projectPath) {
|
|
|
446
452
|
const encoded = encodeProjectPath(projectPath);
|
|
447
453
|
const memDir = path.join(projectsDir, encoded, 'memory');
|
|
448
454
|
if (fs.existsSync(memDir)) return memDir;
|
|
455
|
+
const main = findMainWorktreePath(projectPath);
|
|
456
|
+
if (main) {
|
|
457
|
+
const mainMem = path.join(projectsDir, encodeProjectPath(main), 'memory');
|
|
458
|
+
if (fs.existsSync(mainMem)) return mainMem;
|
|
459
|
+
}
|
|
449
460
|
return findMemoryDirBySubstring(projectPath);
|
|
450
461
|
}
|
|
451
462
|
|
|
463
|
+
function findMainWorktreePath(projectPath) {
|
|
464
|
+
try {
|
|
465
|
+
const out = execFileSync('git', ['-C', projectPath, 'rev-parse', '--git-common-dir'], {
|
|
466
|
+
encoding: 'utf8',
|
|
467
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
468
|
+
timeout: 2000,
|
|
469
|
+
}).trim();
|
|
470
|
+
if (!out) return null;
|
|
471
|
+
const resolved = path.resolve(projectPath, out);
|
|
472
|
+
if (path.basename(resolved) !== '.git') return null;
|
|
473
|
+
const main = path.dirname(resolved);
|
|
474
|
+
if (main === path.resolve(projectPath)) return null;
|
|
475
|
+
return main;
|
|
476
|
+
} catch {
|
|
477
|
+
return null;
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
452
481
|
function encodeProjectPath(projectPath) {
|
|
453
|
-
// Claude Code
|
|
482
|
+
// Claude Code encoding: `:` and `/` both become `-`, so `C:\Users\foo` → `C--Users-foo`
|
|
454
483
|
return projectPath
|
|
455
484
|
.replace(/\\/g, '/')
|
|
456
|
-
.replace(
|
|
457
|
-
.replace(/\//g, '-');
|
|
485
|
+
.replace(/:/g, '-')
|
|
486
|
+
.replace(/\//g, '-');
|
|
458
487
|
}
|
|
459
488
|
|
|
460
489
|
function findMemoryDirBySubstring(projectPath) {
|
|
@@ -517,8 +546,10 @@ app.put('/api/project', (req, res) => {
|
|
|
517
546
|
if (!dirPath) return res.status(400).json({ error: 'path required' });
|
|
518
547
|
const resolved = path.resolve(dirPath.replace(/^~/, os.homedir()));
|
|
519
548
|
if (!fs.existsSync(resolved)) return res.status(404).json({ error: 'directory not found' });
|
|
520
|
-
currentProjectPath
|
|
521
|
-
|
|
549
|
+
if (resolved !== path.resolve(currentProjectPath)) {
|
|
550
|
+
currentProjectPath = resolved;
|
|
551
|
+
clearCache();
|
|
552
|
+
}
|
|
522
553
|
res.json({ path: currentProjectPath, name: path.basename(currentProjectPath) });
|
|
523
554
|
});
|
|
524
555
|
|
|
@@ -527,6 +558,61 @@ app.post('/api/refresh', (_req, res) => {
|
|
|
527
558
|
res.json({ ok: true });
|
|
528
559
|
});
|
|
529
560
|
|
|
561
|
+
function resolveAllowedPath(filePath) {
|
|
562
|
+
if (!filePath) return { error: { status: 400, message: 'path required' } };
|
|
563
|
+
const resolved = path.resolve(filePath.replace(/^~/, os.homedir()));
|
|
564
|
+
const roots = [path.resolve(CLAUDE_DIR), path.resolve(currentProjectPath)];
|
|
565
|
+
const inRoot = roots.some((r) => resolved === r || resolved.startsWith(r + path.sep));
|
|
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 });
|
|
573
|
+
try {
|
|
574
|
+
const stat = fs.statSync(resolved);
|
|
575
|
+
if (!stat.isFile()) return res.status(400).json({ error: 'not a file' });
|
|
576
|
+
fs.unlinkSync(resolved);
|
|
577
|
+
clearCache();
|
|
578
|
+
res.json({ ok: true });
|
|
579
|
+
} catch (err) {
|
|
580
|
+
if (err.code === 'ENOENT') return res.status(404).json({ error: 'file not found' });
|
|
581
|
+
res.status(500).json({ error: err.message });
|
|
582
|
+
}
|
|
583
|
+
});
|
|
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
|
+
|
|
530
616
|
app.post('/api/open-in-editor', (req, res) => {
|
|
531
617
|
const { path: filePath } = req.body;
|
|
532
618
|
if (!filePath) return res.status(400).json({ error: 'path required' });
|