claude-code-memory-explorer 0.2.6 → 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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-code-memory-explorer",
3
- "version": "0.2.6",
3
+ "version": "0.3.0",
4
4
  "description": "Memory explorer dashboard for Claude Code",
5
5
  "main": "server.js",
6
6
  "bin": {
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', 'user-memory', 'project', 'rule', 'memory'];
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
- let h = `<div class="tree-item${sel}${indent ? ' tree-child' : ''}${isConditional ? ' tree-conditional' : ''}" data-id="${esc(item.id)}" title="${esc(item.path)}" onclick="selectFile('${escJs(item.id)}')"${pad}>`;
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
- html += `<div class="tree-group-header"><span class="scope-dot" style="color:var(--scope-${scope})">\u25CF</span> ${esc(label)} <span style="opacity:0.5">${items.length}</span></div>`;
338
- for (const item of items) html += renderItem(item, 0);
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
- selectedFileId = navOrder[idx].id;
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
- const currentScope = current?.parentId ? stackData.find((s) => s.id === current.parentId)?.scope : current?.scope;
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
- selectedFileId = groups[activeScopes[scopeIdx]][0].id;
450
+ const target = groups[activeScopes[scopeIdx]][0];
451
+ selectedFileId = target.id;
452
+ expandGroupForItem(target);
394
453
  pushFileState(selectedFileId);
395
454
  renderTree();
396
455
  renderPreview();
@@ -402,7 +461,7 @@ function navigateGroup(direction) {
402
461
  // #region MEMORY_INDEX
403
462
 
404
463
  function isMemoryIndex(source) {
405
- return (source.scope === 'memory' || source.scope === 'user-memory') && source.name === 'MEMORY.md';
464
+ return (source.scope === 'memory' || source.scope === 'agent-memory') && source.name === 'MEMORY.md';
406
465
  }
407
466
 
408
467
  function parseMemoryIndex(content) {
@@ -518,7 +577,7 @@ async function renderPreview() {
518
577
  return;
519
578
  }
520
579
 
521
- if ((source.scope === 'memory' || source.scope === 'user-memory') && source.load === 'startup' && source.maxLines) {
580
+ if ((source.scope === 'memory' || source.scope === 'agent-memory') && source.load === 'startup' && source.maxLines) {
522
581
  const lines = content.split('\n');
523
582
  const cutoff = source.maxLines;
524
583
  if (lines.length > cutoff) {
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-user-memory: #9ab87a;
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;
@@ -491,9 +534,9 @@ body.light .tree-conditional .file-name {
491
534
  background: rgba(196, 149, 106, 0.18);
492
535
  color: var(--scope-memory);
493
536
  }
494
- .scope-user-memory {
495
- background: rgba(154, 184, 122, 0.18);
496
- color: var(--scope-user-memory);
537
+ .scope-agent-memory {
538
+ background: rgba(138, 168, 163, 0.18);
539
+ color: var(--scope-agent-memory);
497
540
  }
498
541
  .scope-policy {
499
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'
@@ -219,16 +223,16 @@ function discoverMemorySources(projectPath) {
219
223
  const ancestors = getAncestorDirs(projectPath);
220
224
  const seenPaths = new Set(sources.map(s => s.path));
221
225
  for (const dir of ancestors) {
222
- for (const name of ['CLAUDE.md', '.claude/CLAUDE.md']) {
223
- const filePath = path.join(dir, name);
226
+ const isProjectRoot = path.resolve(dir) === path.resolve(projectPath);
227
+ for (const rel of CLAUDE_MD_VARIANTS) {
228
+ const filePath = path.join(dir, rel);
224
229
  if (seenPaths.has(filePath)) continue;
225
230
  const info = fileInfo(filePath);
226
231
  if (!info) continue;
227
232
  seenPaths.add(filePath);
228
- const isProjectRoot = path.resolve(dir) === path.resolve(projectPath);
229
233
  sources.push({
230
- id: `project-claude-md-${dir.replace(/[^a-zA-Z0-9]/g, '-')}`,
231
- name: name.includes('/') ? '.claude/CLAUDE.md' : 'CLAUDE.md',
234
+ id: `project-claude-md-${slug(rel)}-${slug(dir)}`,
235
+ name: rel,
232
236
  scope: 'project',
233
237
  load: 'always',
234
238
  ...info,
@@ -236,23 +240,6 @@ function discoverMemorySources(projectPath) {
236
240
  isProjectRoot,
237
241
  ...spreadImports(info.path, info.content),
238
242
  });
239
- break;
240
- }
241
-
242
- const localPath = path.join(dir, 'CLAUDE.local.md');
243
- if (seenPaths.has(localPath)) continue;
244
- const localInfo = fileInfo(localPath);
245
- if (localInfo) {
246
- seenPaths.add(localPath);
247
- sources.push({
248
- id: `local-claude-md-${dir.replace(/[^a-zA-Z0-9]/g, '-')}`,
249
- name: 'CLAUDE.local.md',
250
- scope: 'project',
251
- load: 'always',
252
- ...localInfo,
253
- dir,
254
- ...spreadImports(localInfo.path, localInfo.content),
255
- });
256
243
  }
257
244
  }
258
245
 
@@ -265,7 +252,7 @@ function discoverMemorySources(projectPath) {
265
252
  for (const entry of entries) {
266
253
  if (!entry.isDirectory() || SKIP_DIRS.has(entry.name) || entry.name.startsWith('.')) continue;
267
254
  const subdir = path.join(dir, entry.name);
268
- for (const name of ['CLAUDE.md', '.claude/CLAUDE.md']) {
255
+ for (const name of CLAUDE_MD_VARIANTS) {
269
256
  const filePath = path.join(subdir, name);
270
257
  if (seenPaths.has(filePath)) continue;
271
258
  const info = fileInfo(filePath);
@@ -273,8 +260,8 @@ function discoverMemorySources(projectPath) {
273
260
  seenPaths.add(filePath);
274
261
  const rel = path.relative(projectPath, subdir).replace(/\\/g, '/');
275
262
  sources.push({
276
- id: `project-claude-md-${subdir.replace(/[^a-zA-Z0-9]/g, '-')}`,
277
- name: `${rel}/${name.includes('/') ? '.claude/CLAUDE.md' : 'CLAUDE.md'}`,
263
+ id: `project-claude-md-${slug(name)}-${slug(subdir)}`,
264
+ name: `${rel}/${name}`,
278
265
  scope: 'project',
279
266
  load: 'tree',
280
267
  ...info,
@@ -306,67 +293,61 @@ function discoverMemorySources(projectPath) {
306
293
  }
307
294
  }
308
295
 
309
- // 6. Auto memory
310
- const memoryDir = findMemoryDir(projectPath);
311
- if (memoryDir && fs.existsSync(memoryDir)) {
312
- const memoryMd = path.join(memoryDir, 'MEMORY.md');
296
+ function pushMemoryDir(dir, scope, idPrefix, extraFields = {}) {
297
+ const memoryMd = path.join(dir, 'MEMORY.md');
313
298
  const memInfo = fileInfo(memoryMd);
314
299
  if (memInfo) {
315
300
  sources.push({
316
- id: 'memory-index',
301
+ id: `${idPrefix}-index`,
317
302
  name: 'MEMORY.md',
318
- scope: 'memory',
303
+ scope,
319
304
  load: 'startup',
305
+ ...extraFields,
320
306
  ...memInfo,
321
307
  maxLines: 200,
322
308
  maxBytes: 25 * 1024,
323
309
  ...spreadImports(memInfo.path, memInfo.content),
324
310
  });
325
311
  }
326
- for (const file of findMdFiles(memoryDir)) {
312
+ for (const file of findMdFiles(dir)) {
327
313
  if (path.basename(file) === 'MEMORY.md') continue;
328
314
  const info = fileInfo(file);
329
315
  if (!info) continue;
330
316
  sources.push({
331
- id: `memory-${path.basename(file, '.md')}`,
317
+ id: `${idPrefix}-${path.basename(file, '.md')}`,
332
318
  name: path.basename(file),
333
- scope: 'memory',
319
+ scope,
334
320
  load: 'ondemand',
321
+ ...extraFields,
335
322
  ...info,
336
323
  ...spreadImports(info.path, info.content),
337
324
  });
338
325
  }
339
326
  }
340
327
 
341
- // 7. User-level memory (~/.claude/memory/)
342
- const userMemoryDir = path.join(os.homedir(), '.claude', 'memory');
343
- if (fs.existsSync(userMemoryDir)) {
344
- const userMemoryMd = path.join(userMemoryDir, 'MEMORY.md');
345
- const userMemInfo = fileInfo(userMemoryMd);
346
- if (userMemInfo) {
347
- sources.push({
348
- id: 'user-memory-index',
349
- name: 'MEMORY.md',
350
- scope: 'user-memory',
351
- load: 'startup',
352
- ...userMemInfo,
353
- maxLines: 200,
354
- maxBytes: 25 * 1024,
355
- ...spreadImports(userMemInfo.path, userMemInfo.content),
356
- });
357
- }
358
- for (const file of findMdFiles(userMemoryDir)) {
359
- if (path.basename(file) === 'MEMORY.md') continue;
360
- const info = fileInfo(file);
361
- if (!info) continue;
362
- sources.push({
363
- id: `user-memory-${path.basename(file, '.md')}`,
364
- name: path.basename(file),
365
- scope: 'user-memory',
366
- load: 'ondemand',
367
- ...info,
368
- ...spreadImports(info.path, info.content),
369
- });
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 });
370
351
  }
371
352
  }
372
353
 
@@ -384,7 +365,7 @@ function discoverMemorySources(projectPath) {
384
365
  for (const { imp, parent, hard } of batch) {
385
366
  if (seen.has(imp)) {
386
367
  const existing = sourceByPath[imp];
387
- if (existing && !existing.parentId && existing.scope === 'memory') {
368
+ if (existing && !existing.parentId && (existing.scope === 'memory' || existing.scope === 'agent-memory')) {
388
369
  existing.parentId = parent.id;
389
370
  if (hard) existing.load = 'import';
390
371
  }