claude-code-memory-explorer 0.6.0 → 0.9.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
@@ -4,7 +4,11 @@
4
4
  [![license](https://img.shields.io/npm/l/claude-code-memory-explorer)](LICENSE)
5
5
  [![npm downloads](https://img.shields.io/npm/dm/claude-code-memory-explorer)](https://www.npmjs.com/package/claude-code-memory-explorer)
6
6
 
7
- > See everything Claude Code knows about your project — CLAUDE.md files, rules, auto memory, and imports.
7
+ **[Live Demo & Docs](https://nikiforovall.blog/claude-code-memory/)**
8
+
9
+ > See everything Claude Code knows about your project — CLAUDE.md files, rules, auto memory, agent memory, and imports.
10
+
11
+ ![Memory explorer overview](assets/shot-overview.png)
8
12
 
9
13
  ## Getting Started
10
14
 
@@ -12,81 +16,35 @@
12
16
  npx claude-code-memory-explorer --open
13
17
  ```
14
18
 
15
- Open http://localhost:3544 (or use `--open` to auto-launch the browser).
16
-
17
- That's it. No config — the dashboard reads your existing Claude Code memory files.
18
-
19
- ![Dark theme](assets/main-dark.png)
20
- ![Light theme](assets/main-light.png)
19
+ That's it — no config. The dashboard scans the Claude Code memory locations for the current project and renders the full stack. Completely read-only; nothing leaves your machine.
21
20
 
22
21
  ## Features
23
22
 
24
- - **Full memory stack** — User CLAUDE.md, project CLAUDE.md, CLAUDE.local.md, rules, auto memory, and managed policies
25
- - **Import resolution** — Follows `@path/to/file.md` references and `[text](file.md)` markdown links up to 5 levels deep
26
- - **Rules inspection** — Shows path-scoped frontmatter (`paths`, `type`, `name`) with conditional load indicators
27
- - **Auto memory** — Visualizes MEMORY.md with startup cutoff line, on-demand topic files, and frontmatter badges
28
- - **Tree + preview** — Left panel grouped by scope (user/project/rules/memory), right panel with syntax-highlighted preview
29
- - **Keyboard-driven** — j/k navigation, h/l group jump, e to open in editor, t for theme, ? for help
30
- - **Resizable sidebar** — Drag handle between tree and preview, width persisted
31
- - **Browser history** — Back/forward navigates file selection, bookmarkable URLs
32
- - **Dark & light theme** — Dark default, light toggle with `t` key
33
- - **Hub integration** — Works as a tab in Claude Code Hub alongside Cost and Marketplace
23
+ - **Full memory stack** — user CLAUDE.md, project CLAUDE.md, CLAUDE.local.md, rules, auto memory, agent memory, and managed policies in one view
24
+ - **Import resolution** — follows `@path/to/file.md` references and `[text](file.md)` markdown links up to 5 levels deep, clickable in the preview
25
+ - **Rules inspection** — path-scoped frontmatter (`paths`, `type`, `name`) with conditional load indicators
26
+ - **Auto memory** — MEMORY.md with startup badge, on-demand topic files, and frontmatter badges
27
+ - **Agent memory** — subagent persistent memory at user, project, and local scope, grouped per agent
28
+ - **Keyboard-driven** — j/k navigation, h/l group jump, e to open in editor, Shift+P project picker, ? for help
29
+ - **17 color themes** — Ember, Gruvbox, Catppuccin, Tokyo Night, Dracula, Nord, and more — each in light and dark, PWA installable
30
+ - **Hub integration** — runs standalone or as a tab in [Claude Code Hub](https://github.com/NikiforovAll/claude-code-hub) alongside Kanban, Cost, and Marketplace
34
31
 
35
- ## Configuration
32
+ ![Path-scoped rule with frontmatter badges](assets/shot-rules.png)
36
33
 
37
- ```bash
38
- PORT=8080 npx claude-code-memory-explorer # Custom port
39
- npx claude-code-memory-explorer --open # Auto-open browser
40
- npx claude-code-memory-explorer --dir=~/.claude-work # Custom Claude config dir
41
- npx claude-code-memory-explorer --project=/path/to/project # Specify project path
42
- ```
34
+ ![Auto memory index with topic files](assets/shot-auto-memory.png)
43
35
 
44
- If port 3544 is in use, the server falls back to a random available port.
36
+ ![Dark theme — Tokyo Night](assets/shot-dark-theme.png)
45
37
 
46
- ### Global install
38
+ ## Configuration
47
39
 
48
- ```bash
49
- npm install -g claude-code-memory-explorer
50
- claude-code-memory-explorer --open
40
+ ```
41
+ PORT=8080 Custom port (default: 3544, falls back if busy)
42
+ --dir <path> Custom Claude config dir (default: ~/.claude)
43
+ --project <path> Project to inspect (default: current directory)
44
+ --open Open browser on start
51
45
  ```
52
46
 
53
- ## How It Works
54
-
55
- Claude Code stores memory across multiple locations:
56
-
57
- | Source | Path | Load Behavior |
58
- |--------|------|---------------|
59
- | Managed policy | `/etc/claude-code/CLAUDE.md` | Always |
60
- | User CLAUDE.md | `~/.claude/CLAUDE.md` | Always |
61
- | Project CLAUDE.md | `./CLAUDE.md` or `./.claude/CLAUDE.md` | Always |
62
- | CLAUDE.local.md | `./CLAUDE.local.md` | Always |
63
- | Rules | `.claude/rules/*.md` | Conditional (path-scoped) |
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 |
66
-
67
- The dashboard:
68
- 1. **Scans** all memory locations — `~/.claude/`, ancestor directories, project rules, auto memory
69
- 2. **Parses** YAML frontmatter in rules files for path globs and metadata
70
- 3. **Resolves** `@import` chains recursively (max 5 levels), tracking both hard imports and soft markdown links
71
- 4. **Groups** sources by scope with parent-child nesting for imports
72
- 5. **Renders** with Highlight.js syntax highlighting — no build step, vanilla JS
73
-
74
- Nothing is modified — the dashboard is read-only.
75
-
76
- ### Memory Path Encoding
77
-
78
- Claude Code encodes project paths for auto memory storage: `C:\Users\me\dev\myproject` becomes `C--Users-me-dev-myproject` under `~/.claude/projects/`. The dashboard tries exact match first, then falls back to substring matching.
79
-
80
- ## FAQ
81
-
82
- **Does it modify any files?**
83
- No. Completely read-only — only reads markdown files from the Claude Code memory locations.
84
-
85
- **Does it work with Claude Code Hub?**
86
- Yes. Exposes `/hub-config` endpoint for hub tab integration.
87
-
88
- **Can I switch projects?**
89
- Yes. Use the project picker (Shift+P) or pass `?project=/path` as a URL parameter.
47
+ The config dir can also be set via the `CLAUDE_CONFIG_DIR` environment variable. Switch projects at runtime with the picker (Shift+P) or `?project=/path`.
90
48
 
91
49
  ## License
92
50
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-code-memory-explorer",
3
- "version": "0.6.0",
3
+ "version": "0.9.0",
4
4
  "description": "Memory explorer dashboard for Claude Code",
5
5
  "main": "server.js",
6
6
  "bin": {
package/public/app.js CHANGED
@@ -137,7 +137,7 @@ function buildThemeMenu() {
137
137
  const menu = document.getElementById('themeMenu');
138
138
  menu.innerHTML = COLOR_THEMES.map(
139
139
  ([id, label]) =>
140
- `<button type="button" class="theme-menu-item" data-theme-id="${id}"
140
+ `<button type="button" class="theme-menu-item theme-swatch-${id}" data-theme-id="${id}"
141
141
  onclick="event.stopPropagation(); setColorTheme('${id}'); toggleThemeMenu()">
142
142
  <span class="theme-swatch theme-swatch-${id}"><i class="sw-bg"></i><i class="sw-accent"></i><i class="sw-ink"></i></span>${label}
143
143
  </button>`,
@@ -194,6 +194,20 @@ async function loadProject() {
194
194
  document.getElementById('projectBtn').title = projectData.path;
195
195
  }
196
196
 
197
+ // Shared by the project picker, the boot restore, and the hub project shim. Throws with the
198
+ // server's error message so callers can surface it directly.
199
+ async function putProject(dirPath) {
200
+ const res = await fetch('/api/project', {
201
+ method: 'PUT',
202
+ headers: { 'Content-Type': 'application/json' },
203
+ body: JSON.stringify({ path: dirPath }),
204
+ });
205
+ if (!res.ok) {
206
+ const err = await res.json().catch(() => ({}));
207
+ throw new Error(err.error || `${res.status}`);
208
+ }
209
+ }
210
+
197
211
  function changeProject() {
198
212
  const current = document.getElementById('projectBtn').title;
199
213
  document.getElementById('projectPathInput').value = current;
@@ -209,20 +223,10 @@ async function submitProjectPicker() {
209
223
  btn.disabled = true;
210
224
  btn.textContent = 'Switching...';
211
225
  try {
212
- const res = await fetch('/api/project', {
213
- method: 'PUT',
214
- headers: { 'Content-Type': 'application/json' },
215
- body: JSON.stringify({ path: dirPath }),
216
- });
217
- if (!res.ok) {
218
- const err = await res.json();
219
- showToast(err.error, 'error');
220
- return;
221
- }
226
+ await putProject(dirPath);
222
227
  closeModal('projectPickerModal');
223
228
  addRecentProject(dirPath);
224
- await loadProject();
225
- await loadData();
229
+ await Promise.all([loadProject(), loadData()]);
226
230
  showToast('Project switched', 'success');
227
231
  } catch (err) {
228
232
  showToast(err.message, 'error');
@@ -977,14 +981,24 @@ document.addEventListener('keydown', (e) => {
977
981
  .catch(() => ({}));
978
982
  if (!cfg.enabled) return;
979
983
  window.__HUB__ = cfg;
984
+ const fwd = (e) =>
985
+ window.parent?.postMessage(
986
+ { type: 'hub:keydown', key: e.key, ctrl: e.ctrlKey, alt: e.altKey, shift: e.shiftKey },
987
+ '*',
988
+ );
980
989
  document.addEventListener('keydown', (e) => {
981
990
  if (e.ctrlKey && e.altKey && (e.key === 'ArrowLeft' || e.key === 'ArrowRight')) {
982
991
  e.preventDefault();
983
- window.parent?.postMessage({ type: 'hub:keydown', key: e.key }, '*');
992
+ fwd(e);
993
+ }
994
+ // Its own branch: the Alt+digit case below requires !ctrlKey.
995
+ if (e.ctrlKey && e.altKey && !e.shiftKey && !e.metaKey && e.key.toLowerCase() === 'p') {
996
+ e.preventDefault();
997
+ fwd(e);
984
998
  }
985
999
  if (e.altKey && !e.ctrlKey && !e.shiftKey && !e.metaKey && /^[1-9]$/.test(e.key)) {
986
1000
  e.preventDefault();
987
- window.parent?.postMessage({ type: 'hub:keydown', key: e.key }, '*');
1001
+ fwd(e);
988
1002
  }
989
1003
  });
990
1004
  })();
@@ -994,10 +1008,12 @@ function _hubNavigate(app, url) {
994
1008
  window.parent?.postMessage({ type: 'hub:navigate', app, url }, '*');
995
1009
  }
996
1010
 
1011
+ // Hoisted out of initHubTheme so initHubProject can share it.
1012
+ const hubOrigin = () => (window.__HUB__?.url ? new URL(window.__HUB__.url).origin : null);
1013
+
997
1014
  (function initHubTheme() {
998
1015
  const getTheme = () => (document.body.classList.contains('light') ? 'light' : 'dark');
999
1016
  const getColorTheme = () => document.body.dataset.colorTheme || 'ember';
1000
- const hubOrigin = () => (window.__HUB__?.url ? new URL(window.__HUB__.url).origin : null);
1001
1017
  // lastTheme/lastColorTheme are updated synchronously when applying a hub
1002
1018
  // message, so the (async) observer sees no diff and doesn't echo it back.
1003
1019
  let lastTheme = getTheme();
@@ -1028,6 +1044,32 @@ function _hubNavigate(app, url) {
1028
1044
  });
1029
1045
  })();
1030
1046
 
1047
+ // Set synchronously when the hub pushes a project, so the DOMContentLoaded restore below can't
1048
+ // let a stale localStorage recent win the race against the hub's choice.
1049
+ let hubProjectPath = null;
1050
+
1051
+ (function initHubProject() {
1052
+ let lastApplied = null;
1053
+ window.addEventListener('message', async (e) => {
1054
+ if (e.source !== window.parent || e.origin !== hubOrigin()) return;
1055
+ if (e.data?.type !== 'hub:project') return;
1056
+ const dirPath = e.data.project;
1057
+ if (typeof dirPath !== 'string' || !dirPath) return;
1058
+ hubProjectPath = dirPath;
1059
+ // Dedupes against the last applied value, not just an in-flight one: the hub re-posts on
1060
+ // every iframe load, so without this each load would PUT and reload twice.
1061
+ if (lastApplied === dirPath) return;
1062
+ lastApplied = dirPath;
1063
+ try {
1064
+ await putProject(dirPath);
1065
+ await Promise.all([loadProject(), loadData()]);
1066
+ } catch (err) {
1067
+ lastApplied = null;
1068
+ console.warn('hub:project failed:', err.message);
1069
+ }
1070
+ });
1071
+ })();
1072
+
1031
1073
  // #endregion HUB_INTEGRATION
1032
1074
 
1033
1075
  // #region RESIZE
@@ -1086,16 +1128,21 @@ document.addEventListener('DOMContentLoaded', async () => {
1086
1128
  // Handle ?project= query param, else restore last-used project from localStorage
1087
1129
  const params = new URLSearchParams(location.search);
1088
1130
  let desiredProject = params.get('project');
1089
- if (!desiredProject) desiredProject = getRecentProjects()[0] || null;
1131
+ // A hub-pushed project outranks the localStorage recent: initHub() is fire-and-forget at script
1132
+ // eval, so hub:project can land before or during this block.
1133
+ if (!desiredProject) desiredProject = hubProjectPath || getRecentProjects()[0] || null;
1090
1134
  if (desiredProject) {
1091
1135
  try {
1092
- const res = await fetch('/api/project', {
1093
- method: 'PUT',
1094
- headers: { 'Content-Type': 'application/json' },
1095
- body: JSON.stringify({ path: desiredProject }),
1096
- });
1097
- if (!res.ok && params.has('project')) showToast('Failed to switch project', 'error');
1098
- } catch {}
1136
+ await putProject(desiredProject);
1137
+ } catch {
1138
+ if (params.has('project')) showToast('Failed to switch project', 'error');
1139
+ }
1140
+ // A push that arrived while the PUT above was in flight must still win.
1141
+ if (hubProjectPath && hubProjectPath !== desiredProject) {
1142
+ try {
1143
+ await putProject(hubProjectPath);
1144
+ } catch {}
1145
+ }
1099
1146
  }
1100
1147
  if (params.has('project')) {
1101
1148
  params.delete('project');
package/public/style.css CHANGED
@@ -202,8 +202,11 @@ body {
202
202
  background: var(--bg-hover);
203
203
  color: var(--text-primary);
204
204
  }
205
+ /* Highlighted with the row's *own* theme accent rather than the active theme's, so the selected
206
+ row previews the theme it names. --sw-accent comes from the theme-swatch-<id> class on the row. */
205
207
  .theme-menu-item.on {
206
- color: var(--accent-text);
208
+ color: var(--sw-accent, var(--accent-text));
209
+ background: color-mix(in srgb, var(--sw-accent, var(--accent-text)) 16%, transparent);
207
210
  }
208
211
  /* 3-dot palette preview: surface, accent, ink (colors from themes.css vars) */
209
212
  .theme-swatch {
package/server.js CHANGED
@@ -21,7 +21,7 @@ const expandHome = (p) => (p || '').replace(/^~/, os.homedir());
21
21
 
22
22
  const PORT = getArg('port') || process.env.PORT || 3544;
23
23
  const AUTO_OPEN = process.argv.includes('--open');
24
- const claudeDirArg = getArg('dir');
24
+ const claudeDirArg = getArg('dir') || process.env.CLAUDE_CONFIG_DIR || process.env.CLAUDE_DIR;
25
25
  const CLAUDE_DIR = claudeDirArg ? expandHome(claudeDirArg) : path.join(os.homedir(), '.claude');
26
26
  const projectDirArg = getArg('project');
27
27
 
@@ -334,7 +334,7 @@ function discoverMemorySources(projectPath) {
334
334
  // project: <project>/.claude/agent-memory/<agent>/
335
335
  // local: <project>/.claude/agent-memory-local/<agent>/
336
336
  const agentMemoryRoots = [
337
- { root: path.join(os.homedir(), '.claude', 'agent-memory'), agentScope: 'user' },
337
+ { root: path.join(CLAUDE_DIR, 'agent-memory'), agentScope: 'user' },
338
338
  { root: path.join(projectPath, '.claude', 'agent-memory'), agentScope: 'project' },
339
339
  { root: path.join(projectPath, '.claude', 'agent-memory-local'), agentScope: 'local' },
340
340
  ];