claude-code-memory-explorer 0.14.0 → 0.15.0-rc.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
@@ -4,38 +4,70 @@
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
- **[Live Demo & Docs](https://nikiforovall.blog/claude-code-memory/)**
7
+ See every memory file that Claude Code loads for a project, and let Claude find what is stale, false, or in conflict.
8
8
 
9
- > See everything Claude Code knows about your project — and find what's stale, false, or conflicting.
9
+ **[Documentation](https://nikiforovall.blog/claude-code-memory/)**
10
10
 
11
- ![Memory Diagnoser overview](assets/overview.png)
11
+ <picture>
12
+ <source media="(prefers-color-scheme: dark)" srcset="website/public/shots/themes/ember-h3-hub-memory-dark.webp">
13
+ <img alt="Memory Diagnoser with memory files grouped by user, project, rules, auto memory, and agent memory, the project CLAUDE.md open with four findings, and the summary cards at the bottom" src="website/public/shots/themes/ember-h3-hub-memory-light.webp">
14
+ </picture>
12
15
 
13
- ## Getting Started
16
+ ## Getting started
17
+
18
+ You need Node.js 20 or later. Open a terminal in your project directory and run:
14
19
 
15
20
  ```bash
16
21
  npx claude-code-memory-explorer --open
17
22
  ```
18
23
 
19
- No config. The dashboard scans the Claude Code memory locations for the current project and renders the full stack. Browsing is read-only; nothing leaves your machine.
24
+ The app scans the memory files for the current directory and opens the page in your browser. The default port is 3544. You do not need a config file. Browsing changes nothing on disk.
25
+
26
+ To run an analysis, you also need Claude Code, installed and logged in. See [Getting started](https://nikiforovall.blog/claude-code-memory/getting-started/).
20
27
 
21
28
  ## Features
22
29
 
23
- - **Full memory stack** — user/project/local CLAUDE.md, rules, auto memory, agent memory, managed policies, and resolved `@import` chains in one view
24
- - **Claude analysis** — audit memory files with headless Claude: health score, per-file findings (stale, false, or conflicting claims) with suggested fixes, merged across runs
25
- - **Keyboard-driven** — j/k navigation, e to open in editor, Shift+P project picker, ? for help
26
- - **17 color themes** — each in light and dark, PWA installable
27
- - **Hub integration** — runs standalone or as a tab in [Claude Code Hub](https://github.com/NikiforovAll/claude-code-hub)
30
+ - **Full memory stack.** Managed policy, user and project `CLAUDE.md` files (including `CLAUDE.local.md` and nested files in subdirectories), user and project rules, project skills, auto memory, agent memory, and the files they import or link, in one tree. See [What Claude Code loads](https://nikiforovall.blog/claude-code-memory/guides/memory-stack/).
31
+ - **Load types.** Each file shows when Claude Code loads it: always, startup, conditional, on-demand, tree, import, or link.
32
+ - **Memory footprint.** A bar and summary cards show the files, characters, and bytes that can load in each session, split by scope. The app does not estimate tokens.
33
+ - **Rules with paths.** A rule with `paths` in its frontmatter shows as conditional, with its globs as badges in the file view.
34
+ - **Auto memory and agent memory.** A `MEMORY.md` index shows as a table of its linked notes, with a mark at the 200-line startup cutoff. A cleanup button removes index lines that point to missing files.
35
+ - **Analyze with Claude.** Runs `claude -p` on your machine with one reviewer subagent per file. Reviewers check each claim against your repository. You get a health score, findings such as duplicates, contradictions, and stale facts, and a suggested fix for each. Copy one finding as a prompt, or several as a fix plan. Each run shows its cost. See [Analyze memory with Claude](https://nikiforovall.blog/claude-code-memory/guides/analyze/).
36
+ - **Keyboard-driven.** <kbd>↓</kbd> <kbd>↑</kbd> or <kbd>j</kbd> <kbd>k</kbd> move through the tree, <kbd>e</kbd> opens the file in your editor, <kbd>Shift</kbd>+<kbd>P</kbd> switches the project, and <kbd>?</kbd> shows all keys. See [Keyboard shortcuts](https://nikiforovall.blog/claude-code-memory/reference/shortcuts/).
37
+ - **17 color themes**, each in light and dark. The app installs as a progressive web app.
38
+ - **Claude Code Hub.** Runs on its own or as a tab in [Claude Code Hub](https://github.com/NikiforovAll/claude-code-hub), with project and theme sync. See [Run inside Claude Code Hub](https://nikiforovall.blog/claude-code-memory/reference/hub/).
39
+
40
+ <picture>
41
+ <source media="(prefers-color-scheme: dark)" srcset="website/public/shots/themes/ember-memory-analysis-dark.webp">
42
+ <img alt="Claude analysis: health 77 of 100, counts by severity, a memory map colored by worst finding, and the list of findings with suggested fixes" src="website/public/shots/themes/ember-memory-analysis-light.webp">
43
+ </picture>
44
+
45
+ <picture>
46
+ <source media="(prefers-color-scheme: dark)" srcset="website/public/shots/themes/ember-h3c-hub-memory-auto-memory-dark.webp">
47
+ <img alt="The auto memory MEMORY.md with a startup badge, its index table of linked notes, and three findings above the content" src="website/public/shots/themes/ember-h3c-hub-memory-auto-memory-light.webp">
48
+ </picture>
49
+
50
+ <picture>
51
+ <source media="(prefers-color-scheme: dark)" srcset="website/public/shots/themes/ember-h3d-hub-memory-rule-dark.webp">
52
+ <img alt="The api-design rule with a conditional badge, its paths frontmatter as badges, and one finding" src="website/public/shots/themes/ember-h3d-hub-memory-rule-light.webp">
53
+ </picture>
28
54
 
29
55
  ## Configuration
30
56
 
31
- ```
32
- PORT=8080 Custom port (default: 3544, falls back if busy)
33
- --dir <path> Custom Claude config dir (default: ~/.claude)
34
- --project <path> Project to inspect (default: current directory)
35
- --open Open browser on start
36
- ```
57
+ | Flag | What it does | Default |
58
+ | --- | --- | --- |
59
+ | `--port <n>` | Port to listen on. If the port is busy, the app uses a random free port and prints it. | `PORT`, else `3544` |
60
+ | `--open` | Opens the page in your browser after start. | Off |
61
+ | `--dir <path>` | Claude config dir to read. | `CLAUDE_CONFIG_DIR`, else `CLAUDE_DIR`, else `~/.claude` |
62
+ | `--project <path>` | Project the server starts on. | The current directory |
63
+ | `--host <addr>` | Address to bind. | `HOST`, else `127.0.0.1` |
64
+ | `--allowed-hosts <list>` | Comma-separated extra `Host` header values to accept. | `ALLOWED_HOSTS`, else none |
65
+
66
+ The app has no authentication. Bind a non-loopback address only on a network you trust.
67
+
68
+ To look at a different project, use the project picker (<kbd>Shift</kbd>+<kbd>P</kbd>) or add `?project=/path` to the URL. The pencil button and <kbd>e</kbd> run the editor in `EDITOR`, or `code` when it is not set. Analyze saves its runs in `<config dir>/memory-analysis/`.
37
69
 
38
- The config dir can also be set via the `CLAUDE_CONFIG_DIR` environment variable; the analyzer's headless `claude` run inherits it. Switch projects at runtime with the picker (Shift+P) or `?project=/path`.
70
+ For all options and the files the app reads and writes, see [CLI and configuration](https://nikiforovall.blog/claude-code-memory/reference/configuration/). For common problems, see [Troubleshooting](https://nikiforovall.blog/claude-code-memory/reference/troubleshooting/).
39
71
 
40
72
  ## License
41
73
 
package/lib/net-guard.js CHANGED
@@ -181,6 +181,16 @@ function createNetGuard(config = {}) {
181
181
  return next();
182
182
  }
183
183
 
184
+ // For a WebSocket upgrade, which originGuard must not judge: the handshake is a
185
+ // GET, and WebSocket has no CORS, so any page can open one. Browsers always send
186
+ // Origin on the handshake, so a missing or `null` one is refused, not trusted.
187
+ // Returns null when allowed, or the reason it was refused.
188
+ function upgradeVerdict(req) {
189
+ if (!hostAllowed(parseHostHeader(req.headers.host))) return 'unrecognized Host header';
190
+ if (originVerdict(req.headers.origin) !== true) return 'cross-origin or missing Origin';
191
+ return null;
192
+ }
193
+
184
194
  // Constant for the life of the process, and frameGuard runs ahead of
185
195
  // express.static — so this is built once rather than per asset fetch.
186
196
  //
@@ -229,12 +239,19 @@ function createNetGuard(config = {}) {
229
239
  * @param {any} app express app (or any http request handler)
230
240
  * @param {number} port 0 selects a free port
231
241
  * @param {(port: number) => void} [onReady]
232
- * @param {{maxHeaderSize?: number}} [opts]
242
+ * `onUpgrade` is attached to both servers; a caller that attached it to the
243
+ * returned one alone would drop every WebSocket that arrives over the other family.
244
+ *
245
+ * @param {{maxHeaderSize?: number, onUpgrade?: (req: any, socket: any, head: Buffer) => void}} [opts]
233
246
  */
234
247
  function listenLoopback(app, port, onReady, opts = {}) {
235
248
  const http = require('http');
236
249
  const serverOpts = opts.maxHeaderSize ? { maxHeaderSize: opts.maxHeaderSize } : {};
237
- const makeServer = (handler) => http.createServer(serverOpts, handler);
250
+ const makeServer = (handler) => {
251
+ const s = http.createServer(serverOpts, handler);
252
+ if (opts.onUpgrade) s.on('upgrade', opts.onUpgrade);
253
+ return s;
254
+ };
238
255
  const server = makeServer(app);
239
256
  let secondary = null;
240
257
 
@@ -261,7 +278,7 @@ function createNetGuard(config = {}) {
261
278
  return `WARNING: listening on ${BIND_HOST} - reachable from your network, with no authentication.`;
262
279
  }
263
280
 
264
- return { BIND_HOST, ALLOWED_HOSTS, hostGuard, originGuard, frameGuard, listenLoopback, exposureWarning };
281
+ return { BIND_HOST, ALLOWED_HOSTS, EXPOSED, hostGuard, originGuard, upgradeVerdict, frameGuard, listenLoopback, exposureWarning };
265
282
  }
266
283
 
267
284
  module.exports = { createNetGuard, parseHostHeader, isLoopbackAddress };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-code-memory-explorer",
3
- "version": "0.14.0",
3
+ "version": "0.15.0-rc.1",
4
4
  "description": "Memory explorer dashboard for Claude Code",
5
5
  "main": "server.js",
6
6
  "bin": {
package/public/app.js CHANGED
@@ -4,7 +4,7 @@ let projectData = null;
4
4
  let stackData = [];
5
5
  let summaryData = null;
6
6
  let selectedFileId = null;
7
- let healthViewOpen = true; // health dashboard is the default view; selecting a file exits it
7
+ let healthViewOpen = false; // opt-in view; the Analyze button toggles it, selecting a file exits it
8
8
 
9
9
  // #endregion STATE
10
10
 
@@ -209,6 +209,8 @@ async function loadProject() {
209
209
  // Shared by the project picker, the boot restore, and the hub project shim. Throws with the
210
210
  // server's error message so callers can surface it directly.
211
211
  async function putProject(dirPath) {
212
+ // Recents can only be written under the right key once the config dir is known.
213
+ await projectInfoPromise;
212
214
  const res = await fetch('/api/project', {
213
215
  method: 'PUT',
214
216
  headers: { 'Content-Type': 'application/json' },
@@ -248,10 +250,40 @@ async function submitProjectPicker() {
248
250
  }
249
251
 
250
252
  const RECENT_PROJECTS_MAX = 20;
253
+ const LEGACY_RECENTS_KEY = 'recentProjects';
254
+
255
+ // Under the hub every config dir's memory app is proxied on one public port, so localStorage is
256
+ // shared between them. Recents are keyed by config dir to keep each one's project separate.
257
+ let claudeConfigDir = '';
258
+ // Retries like the initial load below: in a hub iframe the server may not answer yet.
259
+ const projectInfoPromise = (async () => {
260
+ for (let attempt = 0; attempt < 5; attempt++) {
261
+ try {
262
+ const info = await fetchJSON('/api/project');
263
+ claudeConfigDir = info.configDir || '';
264
+ migrateLegacyRecents();
265
+ return info;
266
+ } catch {
267
+ await new Promise((r) => setTimeout(r, 500));
268
+ }
269
+ }
270
+ return {};
271
+ })();
272
+
273
+ function recentsKey() {
274
+ return claudeConfigDir ? `${LEGACY_RECENTS_KEY}::${claudeConfigDir}` : LEGACY_RECENTS_KEY;
275
+ }
276
+
277
+ function migrateLegacyRecents() {
278
+ const legacy = localStorage.getItem(LEGACY_RECENTS_KEY);
279
+ if (!legacy || !claudeConfigDir) return;
280
+ if (!localStorage.getItem(recentsKey())) localStorage.setItem(recentsKey(), legacy);
281
+ localStorage.removeItem(LEGACY_RECENTS_KEY);
282
+ }
251
283
 
252
284
  function getRecentProjects() {
253
285
  try {
254
- return JSON.parse(localStorage.getItem('recentProjects') || '[]');
286
+ return JSON.parse(localStorage.getItem(recentsKey()) || '[]');
255
287
  } catch {
256
288
  return [];
257
289
  }
@@ -260,7 +292,7 @@ function getRecentProjects() {
260
292
  function addRecentProject(p) {
261
293
  const recent = getRecentProjects().filter((r) => r !== p);
262
294
  recent.unshift(p);
263
- localStorage.setItem('recentProjects', JSON.stringify(recent.slice(0, RECENT_PROJECTS_MAX)));
295
+ localStorage.setItem(recentsKey(), JSON.stringify(recent.slice(0, RECENT_PROJECTS_MAX)));
264
296
  }
265
297
 
266
298
  // The recents list is the switcher; typing a path is the secondary act behind "+ Add path".
@@ -330,7 +362,7 @@ function _removeRecentProject(idx, e) {
330
362
  e.stopPropagation();
331
363
  const target = pickerRows[idx];
332
364
  if (!target) return;
333
- localStorage.setItem('recentProjects', JSON.stringify(getRecentProjects().filter((p) => p !== target)));
365
+ localStorage.setItem(recentsKey(), JSON.stringify(getRecentProjects().filter((p) => p !== target)));
334
366
  renderProjectPicker();
335
367
  }
336
368
 
@@ -356,6 +388,9 @@ function initProjectPicker() {
356
388
  e.preventDefault();
357
389
  e.stopPropagation();
358
390
  });
391
+ const modal = document.getElementById('projectPickerModal');
392
+ onEscapeBlur(filter, modal, () => closeModal('projectPickerModal'));
393
+ onEscapeBlur(document.getElementById('projectPathInput'), modal, () => setPickerMode(false));
359
394
  }
360
395
 
361
396
  // #endregion PROJECT
@@ -366,6 +401,22 @@ function closeModal(id) {
366
401
  document.getElementById(id).classList.remove('open');
367
402
  }
368
403
 
404
+ // Vimium eats Escape inside a text input and only blurs it, so the page never sees the key.
405
+ // A blur that no click in the modal caused, while the window keeps focus and the input is
406
+ // still shown, is that Escape.
407
+ function onEscapeBlur(input, modal, handler) {
408
+ let pointerDown = false;
409
+ modal.addEventListener('mousedown', () => {
410
+ pointerDown = true;
411
+ });
412
+ document.addEventListener('mouseup', () => {
413
+ pointerDown = false;
414
+ });
415
+ input.addEventListener('blur', () => {
416
+ if (!pointerDown && document.hasFocus() && input.checkVisibility()) handler();
417
+ });
418
+ }
419
+
369
420
  // Each entry pairs a left and a right group onto the same grid rows, so their
370
421
  // headings sit level and the shorter group just leaves empty rows.
371
422
  // `combo` joins the keys with a plus (a chord) instead of listing them as
@@ -2092,21 +2143,24 @@ document.addEventListener('keydown', (e) => {
2092
2143
  .catch(() => ({}));
2093
2144
  if (!cfg.enabled) return;
2094
2145
  window.__HUB__ = cfg;
2095
- const fwd = (e) => hubPost({ type: 'hub:keydown', key: e.key, ctrl: e.ctrlKey, alt: e.altKey, shift: e.shiftKey });
2146
+ // e.code travels with e.key because macOS composes Option+<key> into a character (Option+P is
2147
+ // 'π'), so the key alone cannot identify the binding. The hub owns the keymap and normalizes;
2148
+ // these tests only decide whether a press is the hub's to handle.
2096
2149
  document.addEventListener('keydown', (e) => {
2097
- if (e.ctrlKey && e.altKey && (e.key === 'ArrowLeft' || e.key === 'ArrowRight')) {
2150
+ const fwd = () => {
2098
2151
  e.preventDefault();
2099
- fwd(e);
2152
+ hubPost({ type: 'hub:keydown', key: e.key, code: e.code, ctrl: e.ctrlKey, alt: e.altKey, shift: e.shiftKey });
2153
+ };
2154
+ if (e.ctrlKey && e.altKey && (e.key === 'ArrowLeft' || e.key === 'ArrowRight')) {
2155
+ fwd();
2100
2156
  }
2101
2157
  // Own branch: the Alt+digit case below requires !ctrlKey. The hub owns the Ctrl+Alt+letter
2102
2158
  // keymap and ignores unbound letters.
2103
- if (e.ctrlKey && e.altKey && !e.shiftKey && !e.metaKey && /^[a-z]$/i.test(e.key)) {
2104
- e.preventDefault();
2105
- fwd(e);
2159
+ if (e.ctrlKey && e.altKey && !e.shiftKey && !e.metaKey && (/^[a-z]$/i.test(e.key) || /^Key[A-Z]$/.test(e.code))) {
2160
+ fwd();
2106
2161
  }
2107
- if (e.altKey && !e.ctrlKey && !e.shiftKey && !e.metaKey && /^[1-9]$/.test(e.key)) {
2108
- e.preventDefault();
2109
- fwd(e);
2162
+ if (e.altKey && !e.ctrlKey && !e.shiftKey && !e.metaKey && (/^[1-9]$/.test(e.key) || /^Digit[1-9]$/.test(e.code))) {
2163
+ fwd();
2110
2164
  }
2111
2165
  });
2112
2166
  })();
@@ -2248,6 +2302,7 @@ document.addEventListener('DOMContentLoaded', async () => {
2248
2302
  let desiredProject = params.get('project');
2249
2303
  // A hub-pushed project outranks the localStorage recent: initHub() is fire-and-forget at script
2250
2304
  // eval, so hub:project can land before or during this block.
2305
+ await projectInfoPromise;
2251
2306
  if (!desiredProject) desiredProject = hubProjectPath || getRecentProjects()[0] || null;
2252
2307
  if (desiredProject) {
2253
2308
  try {
Binary file
Binary file
package/public/index.html CHANGED
@@ -14,6 +14,11 @@
14
14
  <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/highlight.min.js"></script>
15
15
  <link rel="manifest" href="/manifest.json">
16
16
  <link rel="icon" type="image/svg+xml" href="/icons/icon-svg.svg">
17
+ <!-- Chrome uses the SVG only in the tab strip; app windows, shortcuts and the
18
+ history list need a raster icon or they fall back to /favicon.ico and then to
19
+ whatever icon localhost was last seen with, which is a sibling app's. -->
20
+ <link rel="icon" type="image/png" sizes="192x192" href="/icons/icon-192.png">
21
+ <link rel="icon" type="image/png" sizes="512x512" href="/icons/icon-512.png">
17
22
  <meta name="theme-color" content="#101114" media="(prefers-color-scheme: dark)">
18
23
  <meta name="theme-color" content="#e8e6e3" media="(prefers-color-scheme: light)">
19
24
  <meta name="apple-mobile-web-app-capable" content="yes">
package/public/style.css CHANGED
@@ -1246,7 +1246,7 @@ body.light {
1246
1246
  --text-secondary: #444444;
1247
1247
  --text-tertiary: #666666;
1248
1248
  --text-muted: #888888;
1249
- --accent-text: #b85a20;
1249
+ --accent-text: #a0501c;
1250
1250
  --accent-dim: rgba(232, 111, 51, 0.18);
1251
1251
  --accent-glow: rgba(232, 111, 51, 0.5);
1252
1252
  --success: #1a8a5a;
package/public/themes.css CHANGED
@@ -11,18 +11,18 @@ body.light .theme-swatch-gruvbox { --sw-bg: #f3eee0; --sw-accent: #d65d0e; --sw-
11
11
  body.light .theme-swatch-catppuccin { --sw-bg: #eff1f5; --sw-accent: #8839ef; --sw-ink: #4c4f69; --sw-border: #bcc0cc; }
12
12
  .theme-swatch-tokyo-night { --sw-bg: #1a1b26; --sw-accent: #7aa2f7; --sw-ink: #c0caf5; --sw-border: #3b4261; }
13
13
  body.light .theme-swatch-tokyo-night { --sw-bg: #e9eaf0; --sw-accent: #2e7de9; --sw-ink: #343b58; --sw-border: #a8aecb; }
14
- .theme-swatch-solarized { --sw-bg: #073642; --sw-accent: #cb4b16; --sw-ink: #93a1a1; --sw-border: #586e75; }
15
- body.light .theme-swatch-solarized { --sw-bg: #f5efdc; --sw-accent: #cb4b16; --sw-ink: #073642; --sw-border: #d3cbb7; }
14
+ .theme-swatch-solarized { --sw-bg: #073642; --sw-accent: #c96f47; --sw-ink: #e2dfd0; --sw-border: #586e75; }
15
+ body.light .theme-swatch-solarized { --sw-bg: #fdf6e3; --sw-accent: #2280c2; --sw-ink: #073642; --sw-border: #cdc4ab; }
16
16
  .theme-swatch-dracula { --sw-bg: #282a36; --sw-accent: #bd93f9; --sw-ink: #f8f8f2; --sw-border: #565a75; }
17
17
  body.light .theme-swatch-dracula { --sw-bg: #f0f0f5; --sw-accent: #7c3aed; --sw-ink: #16161e; --sw-border: #c9c9d6; }
18
18
  .theme-swatch-nord { --sw-bg: #2e3440; --sw-accent: #88c0d0; --sw-ink: #eceff4; --sw-border: #4c566a; }
19
19
  body.light .theme-swatch-nord { --sw-bg: #eceff4; --sw-accent: #5e81ac; --sw-ink: #2e3440; --sw-border: #c2c9d6; }
20
20
  .theme-swatch-rose-pine { --sw-bg: #1f1d2e; --sw-accent: #c4a7e7; --sw-ink: #e0def4; --sw-border: #403d52; }
21
- body.light .theme-swatch-rose-pine { --sw-bg: #faf4ed; --sw-accent: #907aa9; --sw-ink: #575279; --sw-border: #dfdad9; }
21
+ body.light .theme-swatch-rose-pine { --sw-bg: #faf4ed; --sw-accent: #907aa9; --sw-ink: #575279; --sw-border: #cecacd; }
22
22
  .theme-swatch-everforest { --sw-bg: #2d353b; --sw-accent: #a7c080; --sw-ink: #d3c6aa; --sw-border: #475258; }
23
- body.light .theme-swatch-everforest { --sw-bg: #fdf6e3; --sw-accent: #6f8352; --sw-ink: #4d5960; --sw-border: #d8d3ba; }
23
+ body.light .theme-swatch-everforest { --sw-bg: #f5f4e8; --sw-accent: #6f8352; --sw-ink: #3d484e; --sw-border: #d3d2bf; }
24
24
  .theme-swatch-kanagawa { --sw-bg: #1f1f28; --sw-accent: #7e9cd8; --sw-ink: #dcd7ba; --sw-border: #54546d; }
25
- body.light .theme-swatch-kanagawa { --sw-bg: #f2ecbc; --sw-accent: #4d699b; --sw-ink: #545464; --sw-border: #c7bf94; }
25
+ body.light .theme-swatch-kanagawa { --sw-bg: #f3f0e4; --sw-accent: #4d699b; --sw-ink: #545464; --sw-border: #d2ccb6; }
26
26
  .theme-swatch-one-dark { --sw-bg: #282c34; --sw-accent: #61afef; --sw-ink: #d7dae0; --sw-border: #4b5263; }
27
27
  body.light .theme-swatch-one-dark { --sw-bg: #fafafa; --sw-accent: #4078f2; --sw-ink: #383a42; --sw-border: #d4d4d6; }
28
28
  .theme-swatch-night-owl { --sw-bg: #0b2942; --sw-accent: #82aaff; --sw-ink: #d6deeb; --sw-border: #5f7e97; }
@@ -32,7 +32,7 @@ body.light .theme-swatch-monokai { --sw-bg: #f1efed; --sw-accent: #c08a00; --sw-
32
32
  .theme-swatch-github { --sw-bg: #24292e; --sw-accent: #58a6ff; --sw-ink: #e1e4e8; --sw-border: #444d56; }
33
33
  body.light .theme-swatch-github { --sw-bg: #f6f8fa; --sw-accent: #0366d6; --sw-ink: #24292e; --sw-border: #d1d5da; }
34
34
  .theme-swatch-ayu { --sw-bg: #10141c; --sw-accent: #e6b450; --sw-ink: #bfbdb6; --sw-border: #2d3343; }
35
- body.light .theme-swatch-ayu { --sw-bg: #f8f9fa; --sw-accent: #f2ae49; --sw-ink: #3d4149; --sw-border: #d8dde2; }
35
+ body.light .theme-swatch-ayu { --sw-bg: #f8f9fa; --sw-accent: #c97612; --sw-ink: #3d4149; --sw-border: #d8dde2; }
36
36
  .theme-swatch-vitesse { --sw-bg: #181818; --sw-accent: #4d9375; --sw-ink: #dbd7ca; --sw-border: #333333; }
37
37
  body.light .theme-swatch-vitesse { --sw-bg: #f7f7f7; --sw-accent: #1c6b48; --sw-ink: #393a34; --sw-border: #d6d6d6; }
38
38
  .theme-swatch-synthwave { --sw-bg: #262335; --sw-accent: #ff7edb; --sw-ink: #ffffff; --sw-border: #495495; }
@@ -136,34 +136,36 @@ body.light[data-color-theme="tokyo-night"] {
136
136
 
137
137
  /* Solarized */
138
138
  body[data-color-theme="solarized"]:not(.light) {
139
- --accent: #cb4b16;
140
- --accent-text: #e9663a;
141
- --accent-dim: rgba(203,75,22,0.25);
142
- --accent-glow: rgba(203, 75, 22, 0.55);
139
+ --accent: #c96f47;
140
+ --accent-text: #d48a6a;
141
+ --accent-dim: rgba(201,111,71,0.25);
142
+ --accent-glow: rgba(201, 111, 71, 0.55);
143
143
  --bg-deep: #002b36;
144
144
  --bg-surface: #073642;
145
145
  --bg-elevated: #0a4250;
146
146
  --bg-hover: #11505f;
147
147
  --border: #586e75;
148
- --text-primary: #93a1a1;
149
- --text-secondary: #839496;
150
- --text-tertiary: #657b83;
151
- --text-muted: #586e75;
148
+ --text-primary: #e2dfd0;
149
+ --text-secondary: #b7c0bf;
150
+ --text-tertiary: #93a1a1;
151
+ --text-muted: #839496;
152
152
  }
153
153
  body.light[data-color-theme="solarized"] {
154
- --accent: #cb4b16;
155
- --accent-text: #b34a12;
156
- --accent-dim: rgba(203,75,22,0.18);
157
- --accent-glow: rgba(203, 75, 22, 0.5);
154
+ --accent: #2280c2;
155
+ --accent-text: #1b6aa3;
156
+ --accent-dim: rgba(34,128,194,0.18);
157
+ --accent-glow: rgba(34, 128, 194, 0.5);
158
158
  --bg-deep: #eee8d5;
159
- --bg-surface: #f5efdc;
160
- --bg-elevated: #fdf6e3;
161
- --bg-hover: #e4ddc8;
162
- --border: #d3cbb7;
159
+ --bg-surface: #fdf6e3;
160
+ --bg-elevated: #fffbef;
161
+ --bg-hover: #efe8d3;
162
+ --border: #cdc4ab;
163
163
  --text-primary: #073642;
164
- --text-secondary: #586e75;
165
- --text-tertiary: #657b83;
166
- --text-muted: #839496;
164
+ --text-secondary: #4f6369;
165
+ --text-tertiary: #586e75;
166
+ --text-muted: #728589;
167
+ --sidebar-bg: #e6dfc7;
168
+ --sidebar-item-bg: #f3ecd6;
167
169
  }
168
170
 
169
171
  /* Dracula */
@@ -255,11 +257,11 @@ body.light[data-color-theme="rose-pine"] {
255
257
  --bg-surface: #faf4ed;
256
258
  --bg-elevated: #fffaf3;
257
259
  --bg-hover: #ebdfd4;
258
- --border: #dfdad9;
260
+ --border: #cecacd;
259
261
  --text-primary: #575279;
260
262
  --text-secondary: #635e87;
261
- --text-tertiary: #797593;
262
- --text-muted: #9893a5;
263
+ --text-tertiary: #6e6a88;
264
+ --text-muted: #857f96;
263
265
  }
264
266
 
265
267
  /* Everforest */
@@ -283,15 +285,15 @@ body.light[data-color-theme="everforest"] {
283
285
  --accent-text: #56683f;
284
286
  --accent-dim: rgba(111,131,82,0.18);
285
287
  --accent-glow: rgba(111, 131, 82, 0.5);
286
- --bg-deep: #f4f0d9;
287
- --bg-surface: #fdf6e3;
288
- --bg-elevated: #fffbef;
289
- --bg-hover: #efebd4;
290
- --border: #d8d3ba;
291
- --text-primary: #4d5960;
292
- --text-secondary: #5c6a72;
293
- --text-tertiary: #7a8478;
294
- --text-muted: #939f91;
288
+ --bg-deep: #ebeadb;
289
+ --bg-surface: #f5f4e8;
290
+ --bg-elevated: #fbfbf4;
291
+ --bg-hover: #e5e4d3;
292
+ --border: #d3d2bf;
293
+ --text-primary: #3d484e;
294
+ --text-secondary: #55636b;
295
+ --text-tertiary: #66705f;
296
+ --text-muted: #7a8577;
295
297
  }
296
298
 
297
299
  /* Kanagawa */
@@ -315,11 +317,11 @@ body.light[data-color-theme="kanagawa"] {
315
317
  --accent-text: #38537f;
316
318
  --accent-dim: rgba(77,105,155,0.18);
317
319
  --accent-glow: rgba(77, 105, 155, 0.5);
318
- --bg-deep: #e5ddb0;
319
- --bg-surface: #f2ecbc;
320
- --bg-elevated: #faf5d2;
321
- --bg-hover: #dcd5ac;
322
- --border: #c7bf94;
320
+ --bg-deep: #e9e5d6;
321
+ --bg-surface: #f3f0e4;
322
+ --bg-elevated: #faf8f1;
323
+ --bg-hover: #e2ddcb;
324
+ --border: #d2ccb6;
323
325
  --text-primary: #545464;
324
326
  --text-secondary: #66667a;
325
327
  --text-tertiary: #716e61;
@@ -408,7 +410,7 @@ body[data-color-theme="monokai"]:not(.light) {
408
410
  }
409
411
  body.light[data-color-theme="monokai"] {
410
412
  --accent: #c08a00;
411
- --accent-text: #9a6e00;
413
+ --accent-text: #856000;
412
414
  --accent-dim: rgba(192,138,0,0.18);
413
415
  --accent-glow: rgba(192, 138, 0, 0.5);
414
416
  --bg-deep: #e9e6e4;
@@ -471,10 +473,10 @@ body[data-color-theme="ayu"]:not(.light) {
471
473
  --text-muted: #6c7380;
472
474
  }
473
475
  body.light[data-color-theme="ayu"] {
474
- --accent: #f2ae49;
475
- --accent-text: #b87514;
476
- --accent-dim: rgba(242,174,73,0.18);
477
- --accent-glow: rgba(242, 174, 73, 0.5);
476
+ --accent: #c97612;
477
+ --accent-text: #a65e0c;
478
+ --accent-dim: rgba(201,118,18,0.18);
479
+ --accent-glow: rgba(201, 118, 18, 0.5);
478
480
  --bg-deep: #eff1f3;
479
481
  --bg-surface: #f8f9fa;
480
482
  --bg-elevated: #fcfcfc;
package/server.js CHANGED
@@ -622,7 +622,7 @@ app.get('/hub-config', (_req, res) => {
622
622
  });
623
623
 
624
624
  app.get('/api/project', (_req, res) => {
625
- res.json({ path: currentProjectPath, name: path.basename(currentProjectPath) });
625
+ res.json({ path: currentProjectPath, name: path.basename(currentProjectPath), configDir: CLAUDE_DIR });
626
626
  });
627
627
 
628
628
  app.put('/api/project', (req, res) => {