claude-code-memory-explorer 0.4.0 → 0.5.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/package.json +1 -1
- package/public/app.js +116 -53
- package/public/index.html +2 -2
- package/public/style.css +42 -21
- package/server.js +27 -16
package/package.json
CHANGED
package/public/app.js
CHANGED
|
@@ -258,6 +258,7 @@ const LOAD_ICONS = {
|
|
|
258
258
|
conditional: '\u25CB',
|
|
259
259
|
ondemand: '\u25CC',
|
|
260
260
|
import: '@',
|
|
261
|
+
link: '\u2197',
|
|
261
262
|
};
|
|
262
263
|
const LOAD_TITLES = {
|
|
263
264
|
always: 'Always loaded',
|
|
@@ -265,6 +266,7 @@ const LOAD_TITLES = {
|
|
|
265
266
|
conditional: 'Conditional (path-scoped)',
|
|
266
267
|
ondemand: 'On-demand',
|
|
267
268
|
import: 'Imported by parent file',
|
|
269
|
+
link: 'Referenced via markdown link (not auto-loaded)',
|
|
268
270
|
};
|
|
269
271
|
|
|
270
272
|
let treeIndex = null;
|
|
@@ -282,17 +284,12 @@ function getTreeIndex() {
|
|
|
282
284
|
groups[s.scope].push(s);
|
|
283
285
|
}
|
|
284
286
|
}
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
if (children) for (const c of children) collect(c);
|
|
290
|
-
}
|
|
291
|
-
for (const scope of SCOPE_ORDER) {
|
|
292
|
-
const items = groups[scope];
|
|
293
|
-
if (items) for (const item of items) collect(item);
|
|
287
|
+
// Default-expand parents whose children are real discovered files (e.g. auto-memory
|
|
288
|
+
// dirs indexed by MEMORY.md). Pure @import/link references (importedBy set) stay collapsed.
|
|
289
|
+
for (const parentId in childrenOf) {
|
|
290
|
+
if (childrenOf[parentId].some((c) => !c.importedBy)) expandedItems.add(parentId);
|
|
294
291
|
}
|
|
295
|
-
treeIndex = { groups, childrenOf
|
|
292
|
+
treeIndex = { groups, childrenOf };
|
|
296
293
|
return treeIndex;
|
|
297
294
|
}
|
|
298
295
|
|
|
@@ -315,16 +312,21 @@ function renderTree() {
|
|
|
315
312
|
const loadIcon = LOAD_ICONS[item.load] || '';
|
|
316
313
|
const loadTitle = LOAD_TITLES[item.load] || item.load;
|
|
317
314
|
const meta = `${item.lines}L`;
|
|
318
|
-
const isConditional = item.load === 'conditional' || item.load === 'ondemand';
|
|
315
|
+
const isConditional = item.load === 'conditional' || item.load === 'ondemand' || item.load === 'link';
|
|
319
316
|
const pad = indent ? ` style="padding-left:${12 + indent * 16}px"` : '';
|
|
320
317
|
const muted = item.scope === 'agent-memory' ? ' tree-agent-item' : '';
|
|
318
|
+
const children = childrenOf[item.id];
|
|
319
|
+
const expanded = children && isItemExpanded(item.id);
|
|
320
|
+
const chevron = children
|
|
321
|
+
? `<span class="tree-chevron" onclick="_toggleItem(event,'${escJs(item.id)}')" title="${children.length} referenced">${expanded ? '▾' : '▸'}</span>`
|
|
322
|
+
: '<span class="tree-chevron-spacer"></span>';
|
|
321
323
|
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}>`;
|
|
324
|
+
h += chevron;
|
|
322
325
|
h += `<span class="load-icon" title="${loadTitle}" style="color:var(--scope-${item.scope})">${loadIcon}</span>`;
|
|
323
326
|
h += `<span class="file-name">${esc(item.name)}</span>`;
|
|
324
327
|
h += `<span class="file-meta">${meta}</span>`;
|
|
325
328
|
h += '</div>';
|
|
326
|
-
|
|
327
|
-
if (children) {
|
|
329
|
+
if (expanded) {
|
|
328
330
|
for (const child of children) h += renderItem(child, (indent || 0) + 1);
|
|
329
331
|
}
|
|
330
332
|
return h;
|
|
@@ -396,6 +398,26 @@ function expandGroupForItem(item) {
|
|
|
396
398
|
if (scope && isGroupCollapsed(scope)) setGroupCollapsed(scope, false);
|
|
397
399
|
}
|
|
398
400
|
|
|
401
|
+
// Referenced/child files are collapsed by default; expand on click. Only top-level
|
|
402
|
+
// "root" files (those not pulled in via an import/link) are shown until expanded.
|
|
403
|
+
const expandedItems = new Set();
|
|
404
|
+
function isItemExpanded(id) {
|
|
405
|
+
return expandedItems.has(id);
|
|
406
|
+
}
|
|
407
|
+
function _toggleItem(event, id) {
|
|
408
|
+
event.stopPropagation();
|
|
409
|
+
if (expandedItems.has(id)) expandedItems.delete(id);
|
|
410
|
+
else expandedItems.add(id);
|
|
411
|
+
renderTree();
|
|
412
|
+
}
|
|
413
|
+
function expandItemAncestors(item) {
|
|
414
|
+
let cur = item;
|
|
415
|
+
while (cur?.parentId) {
|
|
416
|
+
expandedItems.add(cur.parentId);
|
|
417
|
+
cur = stackData.find((s) => s.id === cur.parentId);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
399
421
|
function pushFileState(id) {
|
|
400
422
|
const url = id ? `#${encodeURIComponent(id)}` : location.pathname;
|
|
401
423
|
history.pushState({ fileId: id }, '', url);
|
|
@@ -403,6 +425,13 @@ function pushFileState(id) {
|
|
|
403
425
|
|
|
404
426
|
function selectFile(id, pushState = true) {
|
|
405
427
|
selectedFileId = selectedFileId === id ? null : id;
|
|
428
|
+
if (selectedFileId) {
|
|
429
|
+
const item = stackData.find((s) => s.id === selectedFileId);
|
|
430
|
+
if (item) {
|
|
431
|
+
expandGroupForItem(item);
|
|
432
|
+
expandItemAncestors(item);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
406
435
|
if (pushState) pushFileState(selectedFileId);
|
|
407
436
|
renderTree();
|
|
408
437
|
renderPreview();
|
|
@@ -413,47 +442,76 @@ function scrollToSelected() {
|
|
|
413
442
|
if (el) el.scrollIntoView({ block: 'nearest' });
|
|
414
443
|
}
|
|
415
444
|
|
|
445
|
+
function currentItem() {
|
|
446
|
+
return stackData.find((s) => s.id === selectedFileId) || null;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
function selectNav(id) {
|
|
450
|
+
selectedFileId = id;
|
|
451
|
+
pushFileState(id);
|
|
452
|
+
renderTree();
|
|
453
|
+
renderPreview();
|
|
454
|
+
scrollToSelected();
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// Visible traversal order: collapsed subtrees and collapsed groups are skipped.
|
|
458
|
+
function getVisibleOrder() {
|
|
459
|
+
const { groups, childrenOf } = getTreeIndex();
|
|
460
|
+
const order = [];
|
|
461
|
+
const walk = (item) => {
|
|
462
|
+
order.push(item);
|
|
463
|
+
if (isItemExpanded(item.id)) {
|
|
464
|
+
const children = childrenOf[item.id];
|
|
465
|
+
if (children) for (const c of children) walk(c);
|
|
466
|
+
}
|
|
467
|
+
};
|
|
468
|
+
for (const scope of SCOPE_ORDER) {
|
|
469
|
+
if (isGroupCollapsed(scope)) continue;
|
|
470
|
+
const items = groups[scope];
|
|
471
|
+
if (items) for (const item of items) walk(item);
|
|
472
|
+
}
|
|
473
|
+
return order;
|
|
474
|
+
}
|
|
475
|
+
|
|
416
476
|
function navigateTree(direction) {
|
|
417
|
-
const
|
|
418
|
-
if (!
|
|
419
|
-
let idx =
|
|
477
|
+
const order = getVisibleOrder();
|
|
478
|
+
if (!order.length) return;
|
|
479
|
+
let idx = order.findIndex((s) => s.id === selectedFileId);
|
|
420
480
|
if (idx === -1) {
|
|
421
|
-
idx = direction > 0 ? 0 :
|
|
481
|
+
idx = direction > 0 ? 0 : order.length - 1;
|
|
422
482
|
} else {
|
|
423
483
|
idx += direction;
|
|
424
|
-
if (idx < 0) idx =
|
|
425
|
-
if (idx >=
|
|
484
|
+
if (idx < 0) idx = order.length - 1;
|
|
485
|
+
if (idx >= order.length) idx = 0;
|
|
426
486
|
}
|
|
427
|
-
|
|
428
|
-
selectedFileId = target.id;
|
|
429
|
-
expandGroupForItem(target);
|
|
430
|
-
pushFileState(selectedFileId);
|
|
431
|
-
renderTree();
|
|
432
|
-
renderPreview();
|
|
433
|
-
scrollToSelected();
|
|
487
|
+
selectNav(order[idx].id);
|
|
434
488
|
}
|
|
435
489
|
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
const
|
|
439
|
-
if (!
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
490
|
+
// l / Right: expand a collapsed node, descend into an expanded one, leaf = no-op.
|
|
491
|
+
function expandOrDescend() {
|
|
492
|
+
const item = currentItem();
|
|
493
|
+
if (!item) return navigateTree(1);
|
|
494
|
+
const children = getTreeIndex().childrenOf[item.id];
|
|
495
|
+
if (!children?.length) return;
|
|
496
|
+
if (!isItemExpanded(item.id)) {
|
|
497
|
+
expandedItems.add(item.id);
|
|
498
|
+
renderTree();
|
|
445
499
|
} else {
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
500
|
+
selectNav(children[0].id);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// h / Left: collapse an expanded node, otherwise ascend to its parent.
|
|
505
|
+
function collapseOrAscend() {
|
|
506
|
+
const item = currentItem();
|
|
507
|
+
if (!item) return navigateTree(-1);
|
|
508
|
+
const children = getTreeIndex().childrenOf[item.id];
|
|
509
|
+
if (children?.length && isItemExpanded(item.id)) {
|
|
510
|
+
expandedItems.delete(item.id);
|
|
511
|
+
renderTree();
|
|
512
|
+
} else if (item.parentId) {
|
|
513
|
+
selectNav(item.parentId);
|
|
514
|
+
}
|
|
457
515
|
}
|
|
458
516
|
|
|
459
517
|
// #endregion RENDER_TREE
|
|
@@ -821,21 +879,23 @@ document.addEventListener('keydown', (e) => {
|
|
|
821
879
|
e.preventDefault();
|
|
822
880
|
changeProject();
|
|
823
881
|
}
|
|
824
|
-
|
|
882
|
+
// Skip when modifiers are held (e.g. hub's Ctrl+Alt+Arrow app switching).
|
|
883
|
+
const plain = !e.ctrlKey && !e.altKey && !e.metaKey;
|
|
884
|
+
if (plain && (e.key === 'j' || e.key === 'ArrowDown')) {
|
|
825
885
|
e.preventDefault();
|
|
826
886
|
navigateTree(1);
|
|
827
887
|
}
|
|
828
|
-
if (e.key === 'k' || e.key === 'ArrowUp') {
|
|
888
|
+
if (plain && (e.key === 'k' || e.key === 'ArrowUp')) {
|
|
829
889
|
e.preventDefault();
|
|
830
890
|
navigateTree(-1);
|
|
831
891
|
}
|
|
832
|
-
if (e.key === 'h' || e.key === 'ArrowLeft') {
|
|
892
|
+
if (plain && (e.key === 'h' || e.key === 'ArrowLeft')) {
|
|
833
893
|
e.preventDefault();
|
|
834
|
-
|
|
894
|
+
collapseOrAscend();
|
|
835
895
|
}
|
|
836
|
-
if (e.key === 'l' || e.key === 'ArrowRight') {
|
|
896
|
+
if (plain && (e.key === 'l' || e.key === 'ArrowRight')) {
|
|
837
897
|
e.preventDefault();
|
|
838
|
-
|
|
898
|
+
expandOrDescend();
|
|
839
899
|
}
|
|
840
900
|
if (e.key === 'Enter' && selectedFileId) renderPreview();
|
|
841
901
|
if (e.key === 'e' && selectedFileId) {
|
|
@@ -979,8 +1039,11 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|
|
979
1039
|
await loadData();
|
|
980
1040
|
// Restore file selection from hash
|
|
981
1041
|
const hash = decodeURIComponent(location.hash.slice(1));
|
|
982
|
-
|
|
1042
|
+
const hashItem = hash && stackData.find((s) => s.id === hash);
|
|
1043
|
+
if (hashItem) {
|
|
983
1044
|
selectedFileId = hash;
|
|
1045
|
+
expandGroupForItem(hashItem);
|
|
1046
|
+
expandItemAncestors(hashItem);
|
|
984
1047
|
renderTree();
|
|
985
1048
|
renderPreview();
|
|
986
1049
|
}
|
package/public/index.html
CHANGED
|
@@ -157,8 +157,8 @@
|
|
|
157
157
|
<table class="shortcut-table">
|
|
158
158
|
<tr><td><kbd>j</kbd> / <kbd>↓</kbd></td><td>Next item</td></tr>
|
|
159
159
|
<tr><td><kbd>k</kbd> / <kbd>↑</kbd></td><td>Previous item</td></tr>
|
|
160
|
-
<tr><td><kbd>
|
|
161
|
-
<tr><td><kbd>
|
|
160
|
+
<tr><td><kbd>l</kbd> / <kbd>→</kbd></td><td>Expand / enter child</td></tr>
|
|
161
|
+
<tr><td><kbd>h</kbd> / <kbd>←</kbd></td><td>Collapse / parent</td></tr>
|
|
162
162
|
<tr><td><kbd>Enter</kbd></td><td>Select item</td></tr>
|
|
163
163
|
<tr><td><kbd>Esc</kbd></td><td>Close modal</td></tr>
|
|
164
164
|
</table>
|
package/public/style.css
CHANGED
|
@@ -42,13 +42,12 @@ body {
|
|
|
42
42
|
background: var(--bg-deep);
|
|
43
43
|
color: var(--text-primary);
|
|
44
44
|
font-family: var(--font-sans);
|
|
45
|
-
font-size:
|
|
45
|
+
font-size: 14px;
|
|
46
46
|
line-height: 1.5;
|
|
47
|
-
height:
|
|
47
|
+
height: 100vh;
|
|
48
48
|
display: flex;
|
|
49
49
|
flex-direction: column;
|
|
50
50
|
overflow: hidden;
|
|
51
|
-
zoom: 1.25;
|
|
52
51
|
}
|
|
53
52
|
.mono {
|
|
54
53
|
font-family: var(--font-mono);
|
|
@@ -299,10 +298,10 @@ body {
|
|
|
299
298
|
color: var(--text-muted);
|
|
300
299
|
}
|
|
301
300
|
.tree-agent-item .file-name {
|
|
302
|
-
opacity: 0.
|
|
301
|
+
opacity: 0.78;
|
|
303
302
|
}
|
|
304
303
|
body.light .tree-agent-item .file-name {
|
|
305
|
-
opacity: 0.
|
|
304
|
+
opacity: 0.86;
|
|
306
305
|
}
|
|
307
306
|
.tree-item {
|
|
308
307
|
padding: 5px 12px 5px 24px;
|
|
@@ -321,6 +320,25 @@ body.light .tree-agent-item .file-name {
|
|
|
321
320
|
background: var(--accent-dim);
|
|
322
321
|
color: var(--accent);
|
|
323
322
|
}
|
|
323
|
+
.tree-item .tree-chevron,
|
|
324
|
+
.tree-item .tree-chevron-spacer {
|
|
325
|
+
width: 12px;
|
|
326
|
+
flex-shrink: 0;
|
|
327
|
+
text-align: center;
|
|
328
|
+
}
|
|
329
|
+
.tree-item .tree-chevron {
|
|
330
|
+
font-size: 9px;
|
|
331
|
+
color: var(--text-muted);
|
|
332
|
+
cursor: pointer;
|
|
333
|
+
border-radius: 3px;
|
|
334
|
+
transition:
|
|
335
|
+
color 0.1s,
|
|
336
|
+
background 0.1s;
|
|
337
|
+
}
|
|
338
|
+
.tree-item .tree-chevron:hover {
|
|
339
|
+
color: var(--accent-text);
|
|
340
|
+
background: var(--accent-dim);
|
|
341
|
+
}
|
|
324
342
|
.tree-item .load-icon {
|
|
325
343
|
font-size: 10px;
|
|
326
344
|
width: 12px;
|
|
@@ -329,7 +347,7 @@ body.light .tree-agent-item .file-name {
|
|
|
329
347
|
}
|
|
330
348
|
.tree-item .file-name {
|
|
331
349
|
font-family: var(--font-mono);
|
|
332
|
-
font-size:
|
|
350
|
+
font-size: 13px;
|
|
333
351
|
flex: 1;
|
|
334
352
|
overflow: hidden;
|
|
335
353
|
text-overflow: ellipsis;
|
|
@@ -342,13 +360,13 @@ body.light .tree-agent-item .file-name {
|
|
|
342
360
|
flex-shrink: 0;
|
|
343
361
|
}
|
|
344
362
|
.tree-child .file-name {
|
|
345
|
-
opacity: 0.
|
|
363
|
+
opacity: 0.82;
|
|
346
364
|
}
|
|
347
365
|
.tree-conditional .file-name {
|
|
348
|
-
opacity: 0.
|
|
366
|
+
opacity: 0.68;
|
|
349
367
|
}
|
|
350
368
|
body.light .tree-conditional .file-name {
|
|
351
|
-
opacity: 0.
|
|
369
|
+
opacity: 0.8;
|
|
352
370
|
}
|
|
353
371
|
/* #endregion TREE */
|
|
354
372
|
|
|
@@ -366,8 +384,8 @@ body.light .tree-conditional .file-name {
|
|
|
366
384
|
}
|
|
367
385
|
.preview-title .file-path {
|
|
368
386
|
font-family: var(--font-mono);
|
|
369
|
-
font-size:
|
|
370
|
-
color: var(--text-
|
|
387
|
+
font-size: 12px;
|
|
388
|
+
color: var(--text-tertiary);
|
|
371
389
|
overflow: hidden;
|
|
372
390
|
text-overflow: ellipsis;
|
|
373
391
|
}
|
|
@@ -383,10 +401,10 @@ body.light .tree-conditional .file-name {
|
|
|
383
401
|
margin-top: 6px;
|
|
384
402
|
}
|
|
385
403
|
.preview-filepath {
|
|
386
|
-
padding:
|
|
404
|
+
padding: 5px 16px;
|
|
387
405
|
font-family: var(--font-mono);
|
|
388
|
-
font-size:
|
|
389
|
-
color: var(--text-
|
|
406
|
+
font-size: 12px;
|
|
407
|
+
color: var(--text-tertiary);
|
|
390
408
|
background: var(--bg-deep);
|
|
391
409
|
border-bottom: 1px solid var(--border);
|
|
392
410
|
overflow: hidden;
|
|
@@ -395,9 +413,9 @@ body.light .tree-conditional .file-name {
|
|
|
395
413
|
}
|
|
396
414
|
.preview-code {
|
|
397
415
|
font-family: var(--font-mono);
|
|
398
|
-
font-size:
|
|
416
|
+
font-size: 13px;
|
|
399
417
|
line-height: 1.6;
|
|
400
|
-
color: var(--text-
|
|
418
|
+
color: var(--text-primary);
|
|
401
419
|
white-space: pre-wrap;
|
|
402
420
|
word-break: break-word;
|
|
403
421
|
padding: 12px 16px;
|
|
@@ -860,8 +878,8 @@ kbd {
|
|
|
860
878
|
color: var(--error);
|
|
861
879
|
}
|
|
862
880
|
.toast.success {
|
|
863
|
-
border-color:
|
|
864
|
-
color:
|
|
881
|
+
border-color: var(--success);
|
|
882
|
+
color: var(--success);
|
|
865
883
|
}
|
|
866
884
|
@keyframes slideIn {
|
|
867
885
|
from {
|
|
@@ -878,15 +896,18 @@ kbd {
|
|
|
878
896
|
/* #region LIGHT_THEME */
|
|
879
897
|
body.light {
|
|
880
898
|
--bg-deep: #e8e6e3;
|
|
881
|
-
--bg-surface: #
|
|
882
|
-
--bg-elevated: #
|
|
899
|
+
--bg-surface: #efede9;
|
|
900
|
+
--bg-elevated: #fbfaf9;
|
|
883
901
|
--bg-hover: #d2d0cc;
|
|
884
|
-
--border: #
|
|
902
|
+
--border: #cfcbc4;
|
|
885
903
|
--text-primary: #0a0a0a;
|
|
886
904
|
--text-secondary: #444444;
|
|
887
905
|
--text-tertiary: #666666;
|
|
888
906
|
--text-muted: #888888;
|
|
889
907
|
--accent-text: #b85a20;
|
|
908
|
+
--accent-dim: rgba(232, 111, 51, 0.18);
|
|
909
|
+
--accent-glow: rgba(232, 111, 51, 0.5);
|
|
910
|
+
--success: #1a8a5a;
|
|
890
911
|
}
|
|
891
912
|
/* #endregion LIGHT_THEME */
|
|
892
913
|
|
package/server.js
CHANGED
|
@@ -17,21 +17,19 @@ function getArg(name) {
|
|
|
17
17
|
return process.argv[eqIdx + 1] || null;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
const expandHome = (p) => (p || '').replace(/^~/, os.homedir());
|
|
21
|
+
|
|
20
22
|
const PORT = getArg('port') || process.env.PORT || 3459;
|
|
21
23
|
const AUTO_OPEN = process.argv.includes('--open');
|
|
22
24
|
const claudeDirArg = getArg('dir');
|
|
23
|
-
const CLAUDE_DIR = claudeDirArg
|
|
24
|
-
? claudeDirArg.replace(/^~/, os.homedir())
|
|
25
|
-
: path.join(os.homedir(), '.claude');
|
|
25
|
+
const CLAUDE_DIR = claudeDirArg ? expandHome(claudeDirArg) : path.join(os.homedir(), '.claude');
|
|
26
26
|
const projectDirArg = getArg('project');
|
|
27
27
|
|
|
28
28
|
// #endregion CLI_ARGS
|
|
29
29
|
|
|
30
30
|
// #region STATE
|
|
31
31
|
|
|
32
|
-
let currentProjectPath = projectDirArg
|
|
33
|
-
? path.resolve(projectDirArg.replace(/^~/, os.homedir()))
|
|
34
|
-
: process.cwd();
|
|
32
|
+
let currentProjectPath = projectDirArg ? path.resolve(expandHome(projectDirArg)) : process.cwd();
|
|
35
33
|
|
|
36
34
|
const cache = {};
|
|
37
35
|
const CACHE_TTL = 30_000;
|
|
@@ -129,7 +127,7 @@ function parseImports(content) {
|
|
|
129
127
|
function resolveImport(importPath, fromFile) {
|
|
130
128
|
let resolved = importPath;
|
|
131
129
|
if (resolved.startsWith('~')) {
|
|
132
|
-
resolved = resolved
|
|
130
|
+
resolved = expandHome(resolved);
|
|
133
131
|
} else {
|
|
134
132
|
resolved = path.resolve(path.dirname(fromFile), resolved);
|
|
135
133
|
}
|
|
@@ -325,7 +323,7 @@ function discoverMemorySources(projectPath) {
|
|
|
325
323
|
}
|
|
326
324
|
}
|
|
327
325
|
|
|
328
|
-
// 6. Auto memory
|
|
326
|
+
// 6. Auto memory (projects base honors `autoMemoryDirectory` user setting)
|
|
329
327
|
const memoryDir = findMemoryDir(projectPath);
|
|
330
328
|
if (memoryDir && fs.existsSync(memoryDir)) {
|
|
331
329
|
pushMemoryDir(memoryDir, 'memory', 'memory');
|
|
@@ -376,10 +374,10 @@ function discoverMemorySources(projectPath) {
|
|
|
376
374
|
if (!info) continue;
|
|
377
375
|
const { resolved: imports, resolvedSoft: softImports, unresolved: unresolvedImports } = resolveAllImports(imp, info.content);
|
|
378
376
|
const source = {
|
|
379
|
-
id: `import-${
|
|
377
|
+
id: `import-${imp.replace(/[^a-zA-Z0-9]/g, '-')}`,
|
|
380
378
|
name: path.basename(imp),
|
|
381
379
|
scope: parent.scope,
|
|
382
|
-
load: 'import',
|
|
380
|
+
load: hard ? 'import' : 'link',
|
|
383
381
|
...info,
|
|
384
382
|
importedBy: parent.path,
|
|
385
383
|
parentId: parent.id,
|
|
@@ -428,7 +426,7 @@ function getAncestorDirs(projectPath) {
|
|
|
428
426
|
}
|
|
429
427
|
|
|
430
428
|
function findMemoryDir(projectPath) {
|
|
431
|
-
const projectsDir =
|
|
429
|
+
const projectsDir = getProjectsBaseDir();
|
|
432
430
|
if (!fs.existsSync(projectsDir)) return null;
|
|
433
431
|
const encoded = encodeProjectPath(projectPath);
|
|
434
432
|
const memDir = path.join(projectsDir, encoded, 'memory');
|
|
@@ -467,8 +465,17 @@ function encodeProjectPath(projectPath) {
|
|
|
467
465
|
.replace(/\//g, '-');
|
|
468
466
|
}
|
|
469
467
|
|
|
468
|
+
// User-scope `autoMemoryDirectory` only — Claude Code rejects this key from project/local settings for security.
|
|
469
|
+
function getProjectsBaseDir() {
|
|
470
|
+
let settings = {};
|
|
471
|
+
try { settings = JSON.parse(fs.readFileSync(path.join(CLAUDE_DIR, 'settings.json'), 'utf-8')); } catch { /* no settings */ }
|
|
472
|
+
const raw = settings.autoMemoryDirectory;
|
|
473
|
+
if (typeof raw === 'string' && raw.trim()) return path.resolve(expandHome(raw));
|
|
474
|
+
return path.join(CLAUDE_DIR, 'projects');
|
|
475
|
+
}
|
|
476
|
+
|
|
470
477
|
function findMemoryDirBySubstring(projectPath) {
|
|
471
|
-
const projectsDir =
|
|
478
|
+
const projectsDir = getProjectsBaseDir();
|
|
472
479
|
if (!fs.existsSync(projectsDir)) return null;
|
|
473
480
|
// Match by last 2-3 path segments
|
|
474
481
|
const segments = projectPath.replace(/\\/g, '/').split('/').filter(Boolean);
|
|
@@ -525,7 +532,7 @@ app.get('/api/project', (_req, res) => {
|
|
|
525
532
|
app.put('/api/project', (req, res) => {
|
|
526
533
|
const { path: dirPath } = req.body;
|
|
527
534
|
if (!dirPath) return res.status(400).json({ error: 'path required' });
|
|
528
|
-
const resolved = path.resolve(dirPath
|
|
535
|
+
const resolved = path.resolve(expandHome(dirPath));
|
|
529
536
|
if (!fs.existsSync(resolved)) return res.status(404).json({ error: 'directory not found' });
|
|
530
537
|
if (resolved !== path.resolve(currentProjectPath)) {
|
|
531
538
|
currentProjectPath = resolved;
|
|
@@ -541,8 +548,12 @@ app.post('/api/refresh', (_req, res) => {
|
|
|
541
548
|
|
|
542
549
|
function resolveAllowedPath(filePath) {
|
|
543
550
|
if (!filePath) return { error: { status: 400, message: 'path required' } };
|
|
544
|
-
const resolved = path.resolve(filePath
|
|
545
|
-
const roots = [
|
|
551
|
+
const resolved = path.resolve(expandHome(filePath));
|
|
552
|
+
const roots = [
|
|
553
|
+
path.resolve(CLAUDE_DIR),
|
|
554
|
+
path.resolve(currentProjectPath),
|
|
555
|
+
getProjectsBaseDir(),
|
|
556
|
+
];
|
|
546
557
|
const inRoot = roots.some((r) => resolved === r || resolved.startsWith(r + path.sep));
|
|
547
558
|
if (!inRoot) return { error: { status: 403, message: 'path outside allowed roots' } };
|
|
548
559
|
return { resolved };
|
|
@@ -597,7 +608,7 @@ app.post('/api/memory/cleanup-orphans', (req, res) => {
|
|
|
597
608
|
app.post('/api/open-in-editor', (req, res) => {
|
|
598
609
|
const { path: filePath } = req.body;
|
|
599
610
|
if (!filePath) return res.status(400).json({ error: 'path required' });
|
|
600
|
-
const resolved = filePath
|
|
611
|
+
const resolved = expandHome(filePath);
|
|
601
612
|
if (!fs.existsSync(resolved)) return res.status(404).json({ error: 'file not found' });
|
|
602
613
|
const { exec } = require('child_process');
|
|
603
614
|
exec(`code "${resolved}"`, (err) => {
|