claude-code-memory-explorer 0.4.1 → 0.5.1

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 CHANGED
@@ -12,7 +12,7 @@
12
12
  npx claude-code-memory-explorer --open
13
13
  ```
14
14
 
15
- Open http://localhost:3459 (or use `--open` to auto-launch the browser).
15
+ Open http://localhost:3544 (or use `--open` to auto-launch the browser).
16
16
 
17
17
  That's it. No config — the dashboard reads your existing Claude Code memory files.
18
18
 
@@ -41,7 +41,7 @@ npx claude-code-memory-explorer --dir=~/.claude-work # Custom Claude config di
41
41
  npx claude-code-memory-explorer --project=/path/to/project # Specify project path
42
42
  ```
43
43
 
44
- If port 3459 is in use, the server falls back to a random available port.
44
+ If port 3544 is in use, the server falls back to a random available port.
45
45
 
46
46
  ### Global install
47
47
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-code-memory-explorer",
3
- "version": "0.4.1",
3
+ "version": "0.5.1",
4
4
  "description": "Memory explorer dashboard for Claude Code",
5
5
  "main": "server.js",
6
6
  "bin": {
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
- const navOrder = [];
286
- function collect(item) {
287
- navOrder.push(item);
288
- const children = childrenOf[item.id];
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, navOrder };
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
- const children = childrenOf[item.id];
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 { navOrder } = getTreeIndex();
418
- if (!navOrder.length) return;
419
- let idx = navOrder.findIndex((s) => s.id === selectedFileId);
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 : navOrder.length - 1;
481
+ idx = direction > 0 ? 0 : order.length - 1;
422
482
  } else {
423
483
  idx += direction;
424
- if (idx < 0) idx = navOrder.length - 1;
425
- if (idx >= navOrder.length) idx = 0;
484
+ if (idx < 0) idx = order.length - 1;
485
+ if (idx >= order.length) idx = 0;
426
486
  }
427
- const target = navOrder[idx];
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
- function navigateGroup(direction) {
437
- const { groups } = getTreeIndex();
438
- const activeScopes = SCOPE_ORDER.filter((sc) => groups[sc]?.length);
439
- if (!activeScopes.length) return;
440
-
441
- const current = stackData.find((s) => s.id === selectedFileId);
442
- let scopeIdx = activeScopes.indexOf(getItemScope(current));
443
- if (scopeIdx === -1) {
444
- scopeIdx = direction > 0 ? 0 : activeScopes.length - 1;
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
- scopeIdx += direction;
447
- if (scopeIdx < 0) scopeIdx = activeScopes.length - 1;
448
- if (scopeIdx >= activeScopes.length) scopeIdx = 0;
449
- }
450
- const target = groups[activeScopes[scopeIdx]][0];
451
- selectedFileId = target.id;
452
- expandGroupForItem(target);
453
- pushFileState(selectedFileId);
454
- renderTree();
455
- renderPreview();
456
- scrollToSelected();
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
- if (e.key === 'j' || e.key === 'ArrowDown') {
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
- navigateGroup(-1);
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
- navigateGroup(1);
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
- if (hash && stackData.find((s) => s.id === hash)) {
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>&darr;</kbd></td><td>Next item</td></tr>
159
159
  <tr><td><kbd>k</kbd> / <kbd>&uarr;</kbd></td><td>Previous item</td></tr>
160
- <tr><td><kbd>h</kbd> / <kbd>&larr;</kbd></td><td>Previous group</td></tr>
161
- <tr><td><kbd>l</kbd> / <kbd>&rarr;</kbd></td><td>Next group</td></tr>
160
+ <tr><td><kbd>l</kbd> / <kbd>&rarr;</kbd></td><td>Expand / enter child</td></tr>
161
+ <tr><td><kbd>h</kbd> / <kbd>&larr;</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: 13px;
45
+ font-size: 14px;
46
46
  line-height: 1.5;
47
- height: calc(100vh / 1.25);
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.7;
301
+ opacity: 0.78;
303
302
  }
304
303
  body.light .tree-agent-item .file-name {
305
- opacity: 0.8;
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: 11px;
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.75;
363
+ opacity: 0.82;
346
364
  }
347
365
  .tree-conditional .file-name {
348
- opacity: 0.55;
366
+ opacity: 0.68;
349
367
  }
350
368
  body.light .tree-conditional .file-name {
351
- opacity: 0.75;
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: 11px;
370
- color: var(--text-muted);
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: 4px 16px;
404
+ padding: 5px 16px;
387
405
  font-family: var(--font-mono);
388
- font-size: 10px;
389
- color: var(--text-muted);
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: 11px;
416
+ font-size: 13px;
399
417
  line-height: 1.6;
400
- color: var(--text-secondary);
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: #2ea043;
864
- color: #2ea043;
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: #f4f3f1;
882
- --bg-elevated: #dddbd8;
899
+ --bg-surface: #efede9;
900
+ --bg-elevated: #fbfaf9;
883
901
  --bg-hover: #d2d0cc;
884
- --border: #a09b94;
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
@@ -19,7 +19,7 @@ function getArg(name) {
19
19
 
20
20
  const expandHome = (p) => (p || '').replace(/^~/, os.homedir());
21
21
 
22
- const PORT = getArg('port') || process.env.PORT || 3459;
22
+ const PORT = getArg('port') || process.env.PORT || 3544;
23
23
  const AUTO_OPEN = process.argv.includes('--open');
24
24
  const claudeDirArg = getArg('dir');
25
25
  const CLAUDE_DIR = claudeDirArg ? expandHome(claudeDirArg) : path.join(os.homedir(), '.claude');
@@ -374,10 +374,10 @@ function discoverMemorySources(projectPath) {
374
374
  if (!info) continue;
375
375
  const { resolved: imports, resolvedSoft: softImports, unresolved: unresolvedImports } = resolveAllImports(imp, info.content);
376
376
  const source = {
377
- id: `import-${path.basename(imp, '.md')}-${depth}`,
377
+ id: `import-${imp.replace(/[^a-zA-Z0-9]/g, '-')}`,
378
378
  name: path.basename(imp),
379
379
  scope: parent.scope,
380
- load: 'import',
380
+ load: hard ? 'import' : 'link',
381
381
  ...info,
382
382
  importedBy: parent.path,
383
383
  parentId: parent.id,