claude-code-memory-explorer 0.15.0 → 0.17.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/lib/net-guard.js CHANGED
@@ -245,7 +245,7 @@ function createNetGuard(config = {}) {
245
245
  * @param {{maxHeaderSize?: number, onUpgrade?: (req: any, socket: any, head: Buffer) => void}} [opts]
246
246
  */
247
247
  function listenLoopback(app, port, onReady, opts = {}) {
248
- const http = require('http');
248
+ const http = require('node:http');
249
249
  const serverOpts = opts.maxHeaderSize ? { maxHeaderSize: opts.maxHeaderSize } : {};
250
250
  const makeServer = (handler) => {
251
251
  const s = http.createServer(serverOpts, handler);
@@ -21,9 +21,9 @@
21
21
  // npm packages and cannot import from each other. Run scripts/sync-security-lib.sh
22
22
  // after editing.
23
23
 
24
- const { spawn } = require('child_process');
25
- const path = require('path');
26
- const fs = require('fs');
24
+ const { spawn } = require('node:child_process');
25
+ const path = require('node:path');
26
+ const fs = require('node:fs');
27
27
 
28
28
  const IS_WIN = process.platform === 'win32';
29
29
 
@@ -37,6 +37,7 @@ const VSCODE_FAMILY = new Set([
37
37
  // escape it. Inside "..." cmd treats & | < > ^ ( ) as literal, so only these
38
38
  // matter: '"' ends the quoted run, '%' triggers variable expansion whose value
39
39
  // could itself contain a quote. Neither is legal in an NTFS path anyway.
40
+ // biome-ignore lint/suspicious/noControlCharactersInRegex: NUL is one of the rejected characters
40
41
  const WIN_CMD_FORBIDDEN = /["%\r\n\u0000]/;
41
42
 
42
43
  function fail(message, status) {
@@ -168,6 +169,7 @@ function resolveEditor() {
168
169
  */
169
170
  function assertOpenTarget(value, label = 'path') {
170
171
  if (typeof value !== 'string' || !value.trim()) throw fail(`${label} must be a non-empty string`, 400);
172
+ // biome-ignore lint/suspicious/noControlCharactersInRegex: NUL is one of the rejected characters
171
173
  if (/[\r\n\u0000]/.test(value)) throw fail(`${label} contains invalid characters`, 400);
172
174
  const resolved = path.resolve(value);
173
175
  if (!fs.existsSync(resolved)) throw fail(`${label} does not exist`, 404);
package/package.json CHANGED
@@ -1,8 +1,9 @@
1
1
  {
2
2
  "name": "claude-code-memory-explorer",
3
- "version": "0.15.0",
3
+ "version": "0.17.0",
4
4
  "description": "Memory explorer dashboard for Claude Code",
5
5
  "main": "server.js",
6
+ "type": "commonjs",
6
7
  "bin": {
7
8
  "claude-code-memory-explorer": "server.js"
8
9
  },
package/public/app.js CHANGED
@@ -2147,24 +2147,36 @@ document.addEventListener('keydown', (e) => {
2147
2147
  // 'π'), so the key alone cannot identify the binding. The hub owns the keymap and normalizes;
2148
2148
  // these tests only decide whether a press is the hub's to handle.
2149
2149
  document.addEventListener('keydown', (e) => {
2150
- const fwd = () => {
2151
- e.preventDefault();
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();
2156
- }
2157
- // Own branch: the Alt+digit case below requires !ctrlKey. The hub owns the Ctrl+Alt+letter
2158
- // keymap and ignores unbound letters.
2159
- if (e.ctrlKey && e.altKey && !e.shiftKey && !e.metaKey && (/^[a-z]$/i.test(e.key) || /^Key[A-Z]$/.test(e.code))) {
2160
- fwd();
2161
- }
2162
- if (e.altKey && !e.ctrlKey && !e.shiftKey && !e.metaKey && (/^[1-9]$/.test(e.key) || /^Digit[1-9]$/.test(e.code))) {
2163
- fwd();
2164
- }
2150
+ if (!isHubKey(e)) return;
2151
+ e.preventDefault();
2152
+ hubPost({ type: 'hub:keydown', key: e.key, code: e.code, ctrl: e.ctrlKey, alt: e.altKey, shift: e.shiftKey });
2165
2153
  });
2166
2154
  })();
2167
2155
 
2156
+ // The combos the hub binds, from its hub:keys message. Null until one arrives: a hub from before
2157
+ // hub:keys sends none, and the fallback filter below is what such a hub expects.
2158
+ let hubKeys = null;
2159
+
2160
+ // A copy of the hub's comboOf(): its names must match the hub:keys list.
2161
+ function hubCombo(e) {
2162
+ const lower = (e.key || '').toLowerCase();
2163
+ const m = /^(?:Key|Digit)([A-Z1-9])$/.exec(e.code || '');
2164
+ const key = /^[a-z1-9]$/.test(lower) ? lower : m ? m[1].toLowerCase() : e.key;
2165
+ const mods = [e.ctrlKey && 'ctrl', e.altKey && 'alt', e.shiftKey && 'shift', e.metaKey && 'meta'];
2166
+ return [...mods, key].filter(Boolean).join('+');
2167
+ }
2168
+
2169
+ function isHubKey(e) {
2170
+ if (hubKeys) return hubKeys.has(hubCombo(e));
2171
+ if (e.ctrlKey && e.altKey && (e.key === 'ArrowLeft' || e.key === 'ArrowRight')) return true;
2172
+ // Own branch: the Alt+digit case below requires !ctrlKey. The hub owns the Ctrl+Alt+letter
2173
+ // keymap and ignores unbound letters.
2174
+ if (e.ctrlKey && e.altKey && !e.shiftKey && !e.metaKey && (/^[a-z]$/i.test(e.key) || /^Key[A-Z]$/.test(e.code))) {
2175
+ return true;
2176
+ }
2177
+ return e.altKey && !e.ctrlKey && !e.shiftKey && !e.metaKey && (/^[1-9]$/.test(e.key) || /^Digit[1-9]$/.test(e.code));
2178
+ }
2179
+
2168
2180
  function _hubNavigate(app, url) {
2169
2181
  if (!window.__HUB__?.enabled) return;
2170
2182
  hubPost({ type: 'hub:navigate', app, url });
@@ -2212,6 +2224,14 @@ function hubPost(message) {
2212
2224
  });
2213
2225
  })();
2214
2226
 
2227
+ (function initHubKeys() {
2228
+ window.addEventListener('message', (e) => {
2229
+ if (e.source !== window.parent || e.origin !== hubOrigin()) return;
2230
+ if (e.data?.type !== 'hub:keys' || !Array.isArray(e.data.keys)) return;
2231
+ hubKeys = new Set(e.data.keys.filter((k) => typeof k === 'string'));
2232
+ });
2233
+ })();
2234
+
2215
2235
  // Set synchronously when the hub pushes a project, so the DOMContentLoaded restore below can't
2216
2236
  // let a stale localStorage recent win the race against the hub's choice.
2217
2237
  let hubProjectPath = null;
package/public/style.css CHANGED
@@ -11,7 +11,7 @@
11
11
  --text-muted: #7d808a;
12
12
  --accent: #e86f33;
13
13
  --accent-text: #f0a070;
14
- --accent-dim: rgba(232, 111, 51, 0.22);
14
+ --accent-dim: rgba(232, 111, 51, 0.25);
15
15
  --accent-glow: rgba(232, 111, 51, 0.55);
16
16
  --success: #3ecf8e;
17
17
  --success-dim: rgba(62, 207, 142, 0.18);
@@ -1239,15 +1239,15 @@ body.light {
1239
1239
  color-scheme: light;
1240
1240
  --bg-deep: #e8e6e3;
1241
1241
  --bg-surface: #efede9;
1242
- --bg-elevated: #f7f4f0;
1242
+ --bg-elevated: #fbfaf9;
1243
1243
  --bg-hover: #d2d0cc;
1244
1244
  --border: #cfcbc4;
1245
1245
  --text-primary: #0a0a0a;
1246
- --text-secondary: #444444;
1247
- --text-tertiary: #666666;
1248
- --text-muted: #888888;
1246
+ --text-secondary: #2c2c2c;
1247
+ --text-tertiary: #565656;
1248
+ --text-muted: #767676;
1249
1249
  --accent-text: #a0501c;
1250
- --accent-dim: rgba(232, 111, 51, 0.18);
1250
+ --accent-dim: rgba(184, 90, 32, 0.18);
1251
1251
  --accent-glow: rgba(232, 111, 51, 0.5);
1252
1252
  --success: #1a8a5a;
1253
1253
  --shadow-1: 0 1px 2px rgba(0, 0, 0, 0.08), 0 1px 4px rgba(0, 0, 0, 0.05);
package/public/themes.css CHANGED
@@ -38,6 +38,38 @@ body.light .theme-swatch-vitesse { --sw-bg: #f7f7f7; --sw-accent: #1c6b48; --sw-
38
38
  .theme-swatch-synthwave { --sw-bg: #262335; --sw-accent: #ff7edb; --sw-ink: #ffffff; --sw-border: #495495; }
39
39
  body.light .theme-swatch-synthwave { --sw-bg: #efecf6; --sw-accent: #c936a6; --sw-ink: #241b2f; --sw-border: #c5bcdd; }
40
40
 
41
+ /* Ember */
42
+ body:not([data-color-theme]):not(.light) {
43
+ --accent: #e86f33;
44
+ --accent-text: #f0a070;
45
+ --accent-dim: rgba(232,111,51,0.25);
46
+ --accent-glow: rgba(232, 111, 51, 0.55);
47
+ --bg-deep: #101114;
48
+ --bg-surface: #16181c;
49
+ --bg-elevated: #1e2025;
50
+ --bg-hover: #282a30;
51
+ --border: #363840;
52
+ --text-primary: #f0f1f3;
53
+ --text-secondary: #c2c4c9;
54
+ --text-tertiary: #9a9da5;
55
+ --text-muted: #7d808a;
56
+ }
57
+ body.light:not([data-color-theme]) {
58
+ --accent: #e86f33;
59
+ --accent-text: #a0501c;
60
+ --accent-dim: rgba(184,90,32,0.18);
61
+ --accent-glow: rgba(232, 111, 51, 0.5);
62
+ --bg-deep: #e8e6e3;
63
+ --bg-surface: #efede9;
64
+ --bg-elevated: #fbfaf9;
65
+ --bg-hover: #d2d0cc;
66
+ --border: #cfcbc4;
67
+ --text-primary: #0a0a0a;
68
+ --text-secondary: #2c2c2c;
69
+ --text-tertiary: #565656;
70
+ --text-muted: #767676;
71
+ }
72
+
41
73
  /* Gruvbox */
42
74
  body[data-color-theme="gruvbox"]:not(.light) {
43
75
  --accent: #fe8019;
package/server.js CHANGED
@@ -2,11 +2,11 @@
2
2
  'use strict';
3
3
 
4
4
  const express = require('express');
5
- const path = require('path');
6
- const fs = require('fs');
7
- const os = require('os');
8
- const { execFileSync, spawn } = require('child_process');
9
- const crypto = require('crypto');
5
+ const path = require('node:path');
6
+ const fs = require('node:fs');
7
+ const os = require('node:os');
8
+ const { execFileSync, spawn } = require('node:child_process');
9
+ const crypto = require('node:crypto');
10
10
  const { assertOpenTarget, openInEditor } = require('./lib/open-editor');
11
11
  const { createNetGuard } = require('./lib/net-guard');
12
12
 
@@ -156,8 +156,7 @@ function parseImports(content) {
156
156
  const softLinks = [];
157
157
  // Match @path/to/file.ext — must contain / or \ to be a file path import
158
158
  const re = /@(~?[\w./-]+\/[\w./-]+|~\/[\w./-]+)/g;
159
- let m;
160
- while ((m = re.exec(content)) !== null) {
159
+ for (const m of content.matchAll(re)) {
161
160
  if (m[1].includes('.') && !m[1].includes('/')) continue;
162
161
  // Skip npm scoped packages (e.g. @biomejs/biome) — require file extension in last segment
163
162
  const lastSeg = m[1].split('/').pop();
@@ -166,12 +165,12 @@ function parseImports(content) {
166
165
  }
167
166
  // Also match standalone @filename.md references (no path separator needed)
168
167
  const re2 = /(?:^|\s)@([\w-]+\.md)\b/gm;
169
- while ((m = re2.exec(content)) !== null) {
168
+ for (const m of content.matchAll(re2)) {
170
169
  if (!imports.includes(m[1])) imports.push(m[1]);
171
170
  }
172
171
  // Match markdown links [text](path.md) — soft references, don't change load type
173
172
  const re3 = /\[.*?\]\(((?!https?:\/\/)[^)]+\.md)\)/g;
174
- while ((m = re3.exec(content)) !== null) {
173
+ for (const m of content.matchAll(re3)) {
175
174
  if (!imports.includes(m[1]) && !softLinks.includes(m[1])) softLinks.push(m[1]);
176
175
  }
177
176
  return { imports, softLinks };
@@ -216,7 +215,7 @@ function spreadImports(filePath, content) {
216
215
  }
217
216
 
218
217
  function hasPathsFilter(frontmatter) {
219
- if (!frontmatter || !frontmatter.paths) return false;
218
+ if (!frontmatter?.paths) return false;
220
219
  return Array.isArray(frontmatter.paths) ? frontmatter.paths.length > 0 : true;
221
220
  }
222
221
 
@@ -1343,7 +1342,7 @@ app.get('/api/rules/match', (req, res) => {
1343
1342
  if (!filePath) return res.status(400).json({ error: 'file query param required' });
1344
1343
  const rules = getStack().filter(s => s.scope === 'rule');
1345
1344
  const matched = rules.filter(r => {
1346
- if (!r.frontmatter || !r.frontmatter.paths) return true;
1345
+ if (!r.frontmatter?.paths) return true;
1347
1346
  const patterns = Array.isArray(r.frontmatter.paths) ? r.frontmatter.paths : [r.frontmatter.paths];
1348
1347
  return micromatch.isMatch(filePath, patterns);
1349
1348
  });