claude-code-memory-explorer 0.17.0 → 1.1.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.
Files changed (30) hide show
  1. package/hub-app.json +25 -0
  2. package/lib/net-guard.js +6 -1
  3. package/package.json +2 -1
  4. package/public/app.js +52 -103
  5. package/public/fonts/OFL-IBM-Plex.txt +93 -0
  6. package/public/fonts/OFL-Playfair-Display.txt +93 -0
  7. package/public/fonts/fonts.css +244 -0
  8. package/public/fonts/ibm-plex-mono-cyrillic-400.woff2 +0 -0
  9. package/public/fonts/ibm-plex-mono-cyrillic-500.woff2 +0 -0
  10. package/public/fonts/ibm-plex-mono-cyrillic-600.woff2 +0 -0
  11. package/public/fonts/ibm-plex-mono-cyrillic-ext-400.woff2 +0 -0
  12. package/public/fonts/ibm-plex-mono-cyrillic-ext-500.woff2 +0 -0
  13. package/public/fonts/ibm-plex-mono-cyrillic-ext-600.woff2 +0 -0
  14. package/public/fonts/ibm-plex-mono-latin-400.woff2 +0 -0
  15. package/public/fonts/ibm-plex-mono-latin-500.woff2 +0 -0
  16. package/public/fonts/ibm-plex-mono-latin-600.woff2 +0 -0
  17. package/public/fonts/ibm-plex-mono-latin-ext-400.woff2 +0 -0
  18. package/public/fonts/ibm-plex-mono-latin-ext-500.woff2 +0 -0
  19. package/public/fonts/ibm-plex-mono-latin-ext-600.woff2 +0 -0
  20. package/public/fonts/ibm-plex-mono-vietnamese-400.woff2 +0 -0
  21. package/public/fonts/ibm-plex-mono-vietnamese-500.woff2 +0 -0
  22. package/public/fonts/ibm-plex-mono-vietnamese-600.woff2 +0 -0
  23. package/public/fonts/playfair-display-cyrillic-400.woff2 +0 -0
  24. package/public/fonts/playfair-display-latin-400.woff2 +0 -0
  25. package/public/fonts/playfair-display-latin-ext-400.woff2 +0 -0
  26. package/public/fonts/playfair-display-vietnamese-400.woff2 +0 -0
  27. package/public/index.html +3 -3
  28. package/public/style.css +24 -0
  29. package/public/vendor/claude-hub-sdk.js +50 -0
  30. package/server.js +2 -10
package/hub-app.json ADDED
@@ -0,0 +1,25 @@
1
+ {
2
+ "manifest": 1,
3
+ "id": "memory",
4
+ "name": "Memory Diagnoser",
5
+ "icon": "database",
6
+ "run": { "entry": "server.js", "defaultPort": 3544 },
7
+ "loading": {
8
+ "verbs": [
9
+ "Recalling memories…",
10
+ "Walking the tree…",
11
+ "Dusting off notes…",
12
+ "Reading CLAUDE.md…",
13
+ "Following imports…",
14
+ "Tracing the context…",
15
+ "Flipping through pages…",
16
+ "Connecting the dots…",
17
+ "Checking what sticks…"
18
+ ]
19
+ },
20
+ "actions": {
21
+ "handles": {
22
+ "project.memory": { "params": { "project": "string?" }, "url": "?project={project}", "mode": "message" }
23
+ }
24
+ }
25
+ }
package/lib/net-guard.js CHANGED
@@ -36,6 +36,7 @@ const PORT_RE = /^[0-9]{1,5}$/;
36
36
  // The port is validated but not compared — the listen(0) fallbacks make the real
37
37
  // port dynamic, so only the host is allowlisted.
38
38
  const validPort = (p) => PORT_RE.test(p) && Number(p) >= 1 && Number(p) <= 65535;
39
+ const KEEP_ALIVE_MS = 65000;
39
40
 
40
41
  function isLoopbackAddress(host) {
41
42
  if (!host) return false;
@@ -249,6 +250,10 @@ function createNetGuard(config = {}) {
249
250
  const serverOpts = opts.maxHeaderSize ? { maxHeaderSize: opts.maxHeaderSize } : {};
250
251
  const makeServer = (handler) => {
251
252
  const s = http.createServer(serverOpts, handler);
253
+ // Node's 5 s default closes idle sockets that the hub proxy and the browser would reuse, and a
254
+ // fresh loopback connect is the one that some Windows network filters drop. The hub's agent
255
+ // drops its idle sockets before this, so it never reuses one the server is closing.
256
+ s.keepAliveTimeout = KEEP_ALIVE_MS;
252
257
  if (opts.onUpgrade) s.on('upgrade', opts.onUpgrade);
253
258
  return s;
254
259
  };
@@ -281,4 +286,4 @@ function createNetGuard(config = {}) {
281
286
  return { BIND_HOST, ALLOWED_HOSTS, EXPOSED, hostGuard, originGuard, upgradeVerdict, frameGuard, listenLoopback, exposureWarning };
282
287
  }
283
288
 
284
- module.exports = { createNetGuard, parseHostHeader, isLoopbackAddress };
289
+ module.exports = { createNetGuard, parseHostHeader, isLoopbackAddress, KEEP_ALIVE_MS };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-code-memory-explorer",
3
- "version": "0.17.0",
3
+ "version": "1.1.0",
4
4
  "description": "Memory explorer dashboard for Claude Code",
5
5
  "main": "server.js",
6
6
  "type": "commonjs",
@@ -46,6 +46,7 @@
46
46
  "license": "MIT",
47
47
  "files": [
48
48
  "server.js",
49
+ "hub-app.json",
49
50
  "lib/",
50
51
  "public/",
51
52
  "README.md",
package/public/app.js CHANGED
@@ -145,15 +145,17 @@ function setColorTheme(id) {
145
145
  syncColorThemeMenu(id);
146
146
  }
147
147
 
148
- function buildThemeMenu() {
148
+ function buildThemeMenu(themes = COLOR_THEMES) {
149
149
  const menu = document.getElementById('themeMenu');
150
- menu.innerHTML = COLOR_THEMES.map(
151
- ([id, label]) =>
152
- `<button type="button" class="theme-menu-item theme-swatch-${id}" data-theme-id="${id}"
150
+ menu.innerHTML = themes
151
+ .map(
152
+ ([id, label]) =>
153
+ `<button type="button" class="theme-menu-item theme-swatch-${id}" data-theme-id="${id}"
153
154
  onclick="event.stopPropagation(); setColorTheme('${id}'); toggleThemeMenu()">
154
- <span class="theme-swatch theme-swatch-${id}"><i class="sw-bg"></i><i class="sw-accent"></i><i class="sw-ink"></i></span>${label}
155
+ <span class="theme-swatch theme-swatch-${id}"><i class="sw-bg"></i><i class="sw-accent"></i><i class="sw-ink"></i></span>${esc(label)}
155
156
  </button>`,
156
- ).join('');
157
+ )
158
+ .join('');
157
159
  }
158
160
 
159
161
  // biome-ignore lint/correctness/noUnusedVariables: called from topbar markup
@@ -453,6 +455,8 @@ const SHORTCUT_PAIRS = [
453
455
  { keys: ['Ctrl', 'Alt', '←/→'], combo: true, label: 'Previous / next hub app' },
454
456
  { keys: ['Alt', '1…9'], combo: true, label: 'Jump to hub app by number' },
455
457
  { keys: ['Ctrl', 'Alt', 'P'], combo: true, label: 'Project picker' },
458
+ { keys: ['Ctrl', 'Alt', 'W'], combo: true, label: 'Config dir picker' },
459
+ { keys: ['Ctrl', 'Alt', 'A'], combo: true, label: 'App launcher' },
456
460
  ],
457
461
  },
458
462
  ],
@@ -490,7 +494,7 @@ function buildHelpShortcuts() {
490
494
  function toggleHelpModal() {
491
495
  const list = document.getElementById('helpShortcuts');
492
496
  if (!list.childElementCount) list.innerHTML = buildHelpShortcuts();
493
- list.classList.toggle('sc-standalone', !window.__HUB__?.enabled);
497
+ list.classList.toggle('sc-standalone', hub.status === 'standalone');
494
498
  document.getElementById('helpModal').classList.toggle('open');
495
499
  }
496
500
 
@@ -2137,117 +2141,47 @@ document.addEventListener('keydown', (e) => {
2137
2141
 
2138
2142
  // #region HUB_INTEGRATION
2139
2143
 
2140
- (async function initHub() {
2141
- const cfg = await fetch('/hub-config')
2142
- .then((r) => r.json())
2143
- .catch(() => ({}));
2144
- if (!cfg.enabled) return;
2145
- window.__HUB__ = cfg;
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.
2149
- document.addEventListener('keydown', (e) => {
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 });
2153
- });
2154
- })();
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
-
2180
- function _hubNavigate(app, url) {
2181
- if (!window.__HUB__?.enabled) return;
2182
- hubPost({ type: 'hub:navigate', app, url });
2183
- }
2184
-
2185
- // Hoisted out of initHubTheme so initHubProject can share it.
2186
- const hubOrigin = () => (window.__HUB__?.url ? new URL(window.__HUB__.url).origin : null);
2144
+ const hub = ClaudeHub.connect();
2187
2145
 
2188
- // Every send is addressed to the hub explicitly. With targetOrigin '*' any page that
2189
- // framed this app also received the forwarded keystrokes and navigation intents.
2190
- function hubPost(message) {
2191
- const origin = hubOrigin();
2192
- if (origin) window.parent?.postMessage(message, origin);
2193
- }
2146
+ document.getElementById('helpDocs')?.addEventListener('click', (e) => {
2147
+ e.preventDefault();
2148
+ hub.openExternal(e.currentTarget.href);
2149
+ });
2194
2150
 
2195
2151
  (function initHubTheme() {
2196
2152
  const getTheme = () => (document.body.classList.contains('light') ? 'light' : 'dark');
2197
2153
  const getColorTheme = () => document.body.dataset.colorTheme || 'ember';
2198
- // lastTheme/lastColorTheme are updated synchronously when applying a hub
2199
- // message, so the (async) observer sees no diff and doesn't echo it back.
2200
- let lastTheme = getTheme();
2201
- let lastColorTheme = getColorTheme();
2202
- window.addEventListener('message', (e) => {
2203
- if (e.source !== window.parent || e.origin !== hubOrigin()) return;
2204
- if (e.data?.type !== 'hub:theme') return;
2205
- if (typeof e.data.colorTheme === 'string' && e.data.colorTheme !== getColorTheme()) {
2206
- setColorTheme(e.data.colorTheme);
2207
- lastColorTheme = getColorTheme();
2208
- }
2209
- if (getTheme() !== e.data.theme) {
2210
- window.toggleTheme();
2211
- lastTheme = getTheme();
2212
- }
2154
+ const report = hub.bindTheme({
2155
+ get: () => ({ theme: getTheme(), colorTheme: getColorTheme() }),
2156
+ set: ({ theme, colorTheme }) => {
2157
+ if (colorTheme !== getColorTheme()) setColorTheme(colorTheme);
2158
+ if (theme !== getTheme()) window.toggleTheme();
2159
+ },
2213
2160
  });
2214
- new MutationObserver(() => {
2215
- const t = getTheme();
2216
- const ct = getColorTheme();
2217
- if (t === lastTheme && ct === lastColorTheme) return;
2218
- lastTheme = t;
2219
- lastColorTheme = ct;
2220
- hubPost({ type: 'hub:theme', theme: t, colorTheme: ct });
2221
- }).observe(document.body, {
2161
+ new MutationObserver(report).observe(document.body, {
2222
2162
  attributes: true,
2223
2163
  attributeFilter: ['class', 'data-color-theme'],
2224
2164
  });
2225
2165
  })();
2226
2166
 
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
- })();
2167
+ // Under the hub the picker lists the hub's themes, which include the user's own (protocol section 6, rule 5).
2168
+ hub.onThemes((themes) => {
2169
+ buildThemeMenu(themes.map((t) => [t.id, t.label]));
2170
+ syncColorThemeMenu(document.body.dataset.colorTheme);
2171
+ });
2234
2172
 
2235
2173
  // Set synchronously when the hub pushes a project, so the DOMContentLoaded restore below can't
2236
2174
  // let a stale localStorage recent win the race against the hub's choice.
2237
2175
  let hubProjectPath = null;
2176
+ // Shared with the DOMContentLoaded restore so a boot restore and a hub push never apply the same project twice.
2177
+ let lastAppliedProject = null;
2238
2178
 
2239
2179
  (function initHubProject() {
2240
- let lastApplied = null;
2241
- window.addEventListener('message', async (e) => {
2242
- if (e.source !== window.parent || e.origin !== hubOrigin()) return;
2243
- if (e.data?.type !== 'hub:project') return;
2244
- const dirPath = e.data.project;
2245
- if (typeof dirPath !== 'string' || !dirPath) return;
2246
- hubProjectPath = dirPath;
2180
+ async function applyProject(dirPath) {
2247
2181
  // Dedupes against the last applied value, not just an in-flight one: the hub re-posts on
2248
2182
  // every iframe load, so without this each load would PUT and reload twice.
2249
- if (lastApplied === dirPath) return;
2250
- lastApplied = dirPath;
2183
+ if (lastAppliedProject === dirPath) return;
2184
+ lastAppliedProject = dirPath;
2251
2185
  try {
2252
2186
  await putProject(dirPath);
2253
2187
  // The hub does not persist its own scope, so a hub-pushed project must become the recent
@@ -2256,9 +2190,22 @@ let hubProjectPath = null;
2256
2190
  addRecentProject(dirPath);
2257
2191
  await Promise.all([loadProject(), loadData()]);
2258
2192
  } catch (err) {
2259
- lastApplied = null;
2260
- console.warn('hub:project failed:', err.message);
2193
+ lastAppliedProject = null;
2194
+ throw err;
2261
2195
  }
2196
+ }
2197
+
2198
+ hub.subscribe('project.changed', (p) => {
2199
+ const dirPath = typeof p?.project === 'string' && p.project ? p.project : null;
2200
+ if (!dirPath) return;
2201
+ hubProjectPath = dirPath;
2202
+ applyProject(dirPath).catch((err) => console.warn('project.changed failed:', err.message));
2203
+ });
2204
+
2205
+ // A link moves this app off the hub's project, and applyProject records that, so the next
2206
+ // project.changed still applies.
2207
+ hub.handle('project.memory', (p) => {
2208
+ if (p.project) applyProject(p.project).catch(() => showToast('Failed to switch project', 'error'));
2262
2209
  });
2263
2210
  })();
2264
2211
 
@@ -2320,11 +2267,13 @@ document.addEventListener('DOMContentLoaded', async () => {
2320
2267
  // Handle ?project= query param, else restore last-used project from localStorage
2321
2268
  const params = new URLSearchParams(location.search);
2322
2269
  let desiredProject = params.get('project');
2323
- // A hub-pushed project outranks the localStorage recent: initHub() is fire-and-forget at script
2324
- // eval, so hub:project can land before or during this block.
2270
+ // A hub-pushed project outranks the localStorage recent: ClaudeHub.connect() is fire-and-forget at script
2271
+ // eval, so project.changed can land before or during this block.
2325
2272
  await projectInfoPromise;
2326
2273
  if (!desiredProject) desiredProject = hubProjectPath || getRecentProjects()[0] || null;
2327
2274
  if (desiredProject) {
2275
+ // Set before the PUT: the hub's push can land while it is in flight.
2276
+ lastAppliedProject = desiredProject;
2328
2277
  try {
2329
2278
  await putProject(desiredProject);
2330
2279
  } catch {
@@ -0,0 +1,93 @@
1
+ Copyright © 2017 IBM Corp. with Reserved Font Name "Plex"
2
+
3
+ This Font Software is licensed under the SIL Open Font License, Version 1.1.
4
+
5
+ This license is copied below, and is also available with a FAQ at: http://scripts.sil.org/OFL
6
+
7
+
8
+ -----------------------------------------------------------
9
+ SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
10
+ -----------------------------------------------------------
11
+
12
+ PREAMBLE
13
+ The goals of the Open Font License (OFL) are to stimulate worldwide
14
+ development of collaborative font projects, to support the font creation
15
+ efforts of academic and linguistic communities, and to provide a free and
16
+ open framework in which fonts may be shared and improved in partnership
17
+ with others.
18
+
19
+ The OFL allows the licensed fonts to be used, studied, modified and
20
+ redistributed freely as long as they are not sold by themselves. The
21
+ fonts, including any derivative works, can be bundled, embedded,
22
+ redistributed and/or sold with any software provided that any reserved
23
+ names are not used by derivative works. The fonts and derivatives,
24
+ however, cannot be released under any other type of license. The
25
+ requirement for fonts to remain under this license does not apply
26
+ to any document created using the fonts or their derivatives.
27
+
28
+ DEFINITIONS
29
+ "Font Software" refers to the set of files released by the Copyright
30
+ Holder(s) under this license and clearly marked as such. This may
31
+ include source files, build scripts and documentation.
32
+
33
+ "Reserved Font Name" refers to any names specified as such after the
34
+ copyright statement(s).
35
+
36
+ "Original Version" refers to the collection of Font Software components as
37
+ distributed by the Copyright Holder(s).
38
+
39
+ "Modified Version" refers to any derivative made by adding to, deleting,
40
+ or substituting -- in part or in whole -- any of the components of the
41
+ Original Version, by changing formats or by porting the Font Software to a
42
+ new environment.
43
+
44
+ "Author" refers to any designer, engineer, programmer, technical
45
+ writer or other person who contributed to the Font Software.
46
+
47
+ PERMISSION & CONDITIONS
48
+ Permission is hereby granted, free of charge, to any person obtaining
49
+ a copy of the Font Software, to use, study, copy, merge, embed, modify,
50
+ redistribute, and sell modified and unmodified copies of the Font
51
+ Software, subject to the following conditions:
52
+
53
+ 1) Neither the Font Software nor any of its individual components,
54
+ in Original or Modified Versions, may be sold by itself.
55
+
56
+ 2) Original or Modified Versions of the Font Software may be bundled,
57
+ redistributed and/or sold with any software, provided that each copy
58
+ contains the above copyright notice and this license. These can be
59
+ included either as stand-alone text files, human-readable headers or
60
+ in the appropriate machine-readable metadata fields within text or
61
+ binary files as long as those fields can be easily viewed by the user.
62
+
63
+ 3) No Modified Version of the Font Software may use the Reserved Font
64
+ Name(s) unless explicit written permission is granted by the corresponding
65
+ Copyright Holder. This restriction only applies to the primary font name as
66
+ presented to the users.
67
+
68
+ 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
69
+ Software shall not be used to promote, endorse or advertise any
70
+ Modified Version, except to acknowledge the contribution(s) of the
71
+ Copyright Holder(s) and the Author(s) or with their explicit written
72
+ permission.
73
+
74
+ 5) The Font Software, modified or unmodified, in part or in whole,
75
+ must be distributed entirely under this license, and must not be
76
+ distributed under any other license. The requirement for fonts to
77
+ remain under this license does not apply to any document created
78
+ using the Font Software.
79
+
80
+ TERMINATION
81
+ This license becomes null and void if any of the above conditions are
82
+ not met.
83
+
84
+ DISCLAIMER
85
+ THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
86
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
87
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
88
+ OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
89
+ COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
90
+ INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
91
+ DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
92
+ FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
93
+ OTHER DEALINGS IN THE FONT SOFTWARE.
@@ -0,0 +1,93 @@
1
+ Copyright 2017 The Playfair Display Project Authors (https://github.com/clauseggers/Playfair-Display), with Reserved Font Name "Playfair Display"
2
+
3
+ This Font Software is licensed under the SIL Open Font License, Version 1.1.
4
+ This license is copied below, and is also available with a FAQ at:
5
+ http://scripts.sil.org/OFL
6
+
7
+
8
+ -----------------------------------------------------------
9
+ SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
10
+ -----------------------------------------------------------
11
+
12
+ PREAMBLE
13
+ The goals of the Open Font License (OFL) are to stimulate worldwide
14
+ development of collaborative font projects, to support the font creation
15
+ efforts of academic and linguistic communities, and to provide a free and
16
+ open framework in which fonts may be shared and improved in partnership
17
+ with others.
18
+
19
+ The OFL allows the licensed fonts to be used, studied, modified and
20
+ redistributed freely as long as they are not sold by themselves. The
21
+ fonts, including any derivative works, can be bundled, embedded,
22
+ redistributed and/or sold with any software provided that any reserved
23
+ names are not used by derivative works. The fonts and derivatives,
24
+ however, cannot be released under any other type of license. The
25
+ requirement for fonts to remain under this license does not apply
26
+ to any document created using the fonts or their derivatives.
27
+
28
+ DEFINITIONS
29
+ "Font Software" refers to the set of files released by the Copyright
30
+ Holder(s) under this license and clearly marked as such. This may
31
+ include source files, build scripts and documentation.
32
+
33
+ "Reserved Font Name" refers to any names specified as such after the
34
+ copyright statement(s).
35
+
36
+ "Original Version" refers to the collection of Font Software components as
37
+ distributed by the Copyright Holder(s).
38
+
39
+ "Modified Version" refers to any derivative made by adding to, deleting,
40
+ or substituting -- in part or in whole -- any of the components of the
41
+ Original Version, by changing formats or by porting the Font Software to a
42
+ new environment.
43
+
44
+ "Author" refers to any designer, engineer, programmer, technical
45
+ writer or other person who contributed to the Font Software.
46
+
47
+ PERMISSION & CONDITIONS
48
+ Permission is hereby granted, free of charge, to any person obtaining
49
+ a copy of the Font Software, to use, study, copy, merge, embed, modify,
50
+ redistribute, and sell modified and unmodified copies of the Font
51
+ Software, subject to the following conditions:
52
+
53
+ 1) Neither the Font Software nor any of its individual components,
54
+ in Original or Modified Versions, may be sold by itself.
55
+
56
+ 2) Original or Modified Versions of the Font Software may be bundled,
57
+ redistributed and/or sold with any software, provided that each copy
58
+ contains the above copyright notice and this license. These can be
59
+ included either as stand-alone text files, human-readable headers or
60
+ in the appropriate machine-readable metadata fields within text or
61
+ binary files as long as those fields can be easily viewed by the user.
62
+
63
+ 3) No Modified Version of the Font Software may use the Reserved Font
64
+ Name(s) unless explicit written permission is granted by the corresponding
65
+ Copyright Holder. This restriction only applies to the primary font name as
66
+ presented to the users.
67
+
68
+ 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
69
+ Software shall not be used to promote, endorse or advertise any
70
+ Modified Version, except to acknowledge the contribution(s) of the
71
+ Copyright Holder(s) and the Author(s) or with their explicit written
72
+ permission.
73
+
74
+ 5) The Font Software, modified or unmodified, in part or in whole,
75
+ must be distributed entirely under this license, and must not be
76
+ distributed under any other license. The requirement for fonts to
77
+ remain under this license does not apply to any document created
78
+ using the Font Software.
79
+
80
+ TERMINATION
81
+ This license becomes null and void if any of the above conditions are
82
+ not met.
83
+
84
+ DISCLAIMER
85
+ THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
86
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
87
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
88
+ OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
89
+ COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
90
+ INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
91
+ DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
92
+ FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
93
+ OTHER DEALINGS IN THE FONT SOFTWARE.
@@ -0,0 +1,244 @@
1
+ /* IBM Plex Mono and Playfair Display, SIL Open Font License 1.1 (see the OFL-*.txt files). Mirrored from Google Fonts. */
2
+ /* cyrillic-ext */
3
+ @font-face {
4
+ font-family: 'IBM Plex Mono';
5
+ font-style: normal;
6
+ font-weight: 400;
7
+ font-display: swap;
8
+ src: url(ibm-plex-mono-cyrillic-ext-400.woff2) format('woff2');
9
+ unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
10
+ }
11
+ /* cyrillic */
12
+ @font-face {
13
+ font-family: 'IBM Plex Mono';
14
+ font-style: normal;
15
+ font-weight: 400;
16
+ font-display: swap;
17
+ src: url(ibm-plex-mono-cyrillic-400.woff2) format('woff2');
18
+ unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
19
+ }
20
+ /* vietnamese */
21
+ @font-face {
22
+ font-family: 'IBM Plex Mono';
23
+ font-style: normal;
24
+ font-weight: 400;
25
+ font-display: swap;
26
+ src: url(ibm-plex-mono-vietnamese-400.woff2) format('woff2');
27
+ unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
28
+ }
29
+ /* latin-ext */
30
+ @font-face {
31
+ font-family: 'IBM Plex Mono';
32
+ font-style: normal;
33
+ font-weight: 400;
34
+ font-display: swap;
35
+ src: url(ibm-plex-mono-latin-ext-400.woff2) format('woff2');
36
+ unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
37
+ }
38
+ /* latin */
39
+ @font-face {
40
+ font-family: 'IBM Plex Mono';
41
+ font-style: normal;
42
+ font-weight: 400;
43
+ font-display: swap;
44
+ src: url(ibm-plex-mono-latin-400.woff2) format('woff2');
45
+ unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
46
+ }
47
+ /* cyrillic-ext */
48
+ @font-face {
49
+ font-family: 'IBM Plex Mono';
50
+ font-style: normal;
51
+ font-weight: 500;
52
+ font-display: swap;
53
+ src: url(ibm-plex-mono-cyrillic-ext-500.woff2) format('woff2');
54
+ unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
55
+ }
56
+ /* cyrillic */
57
+ @font-face {
58
+ font-family: 'IBM Plex Mono';
59
+ font-style: normal;
60
+ font-weight: 500;
61
+ font-display: swap;
62
+ src: url(ibm-plex-mono-cyrillic-500.woff2) format('woff2');
63
+ unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
64
+ }
65
+ /* vietnamese */
66
+ @font-face {
67
+ font-family: 'IBM Plex Mono';
68
+ font-style: normal;
69
+ font-weight: 500;
70
+ font-display: swap;
71
+ src: url(ibm-plex-mono-vietnamese-500.woff2) format('woff2');
72
+ unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
73
+ }
74
+ /* latin-ext */
75
+ @font-face {
76
+ font-family: 'IBM Plex Mono';
77
+ font-style: normal;
78
+ font-weight: 500;
79
+ font-display: swap;
80
+ src: url(ibm-plex-mono-latin-ext-500.woff2) format('woff2');
81
+ unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
82
+ }
83
+ /* latin */
84
+ @font-face {
85
+ font-family: 'IBM Plex Mono';
86
+ font-style: normal;
87
+ font-weight: 500;
88
+ font-display: swap;
89
+ src: url(ibm-plex-mono-latin-500.woff2) format('woff2');
90
+ unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
91
+ }
92
+ /* cyrillic-ext */
93
+ @font-face {
94
+ font-family: 'IBM Plex Mono';
95
+ font-style: normal;
96
+ font-weight: 600;
97
+ font-display: swap;
98
+ src: url(ibm-plex-mono-cyrillic-ext-600.woff2) format('woff2');
99
+ unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
100
+ }
101
+ /* cyrillic */
102
+ @font-face {
103
+ font-family: 'IBM Plex Mono';
104
+ font-style: normal;
105
+ font-weight: 600;
106
+ font-display: swap;
107
+ src: url(ibm-plex-mono-cyrillic-600.woff2) format('woff2');
108
+ unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
109
+ }
110
+ /* vietnamese */
111
+ @font-face {
112
+ font-family: 'IBM Plex Mono';
113
+ font-style: normal;
114
+ font-weight: 600;
115
+ font-display: swap;
116
+ src: url(ibm-plex-mono-vietnamese-600.woff2) format('woff2');
117
+ unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
118
+ }
119
+ /* latin-ext */
120
+ @font-face {
121
+ font-family: 'IBM Plex Mono';
122
+ font-style: normal;
123
+ font-weight: 600;
124
+ font-display: swap;
125
+ src: url(ibm-plex-mono-latin-ext-600.woff2) format('woff2');
126
+ unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
127
+ }
128
+ /* latin */
129
+ @font-face {
130
+ font-family: 'IBM Plex Mono';
131
+ font-style: normal;
132
+ font-weight: 600;
133
+ font-display: swap;
134
+ src: url(ibm-plex-mono-latin-600.woff2) format('woff2');
135
+ unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
136
+ }
137
+ /* cyrillic */
138
+ @font-face {
139
+ font-family: 'Playfair Display';
140
+ font-style: normal;
141
+ font-weight: 400;
142
+ font-display: swap;
143
+ src: url(playfair-display-cyrillic-400.woff2) format('woff2');
144
+ unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
145
+ }
146
+ /* vietnamese */
147
+ @font-face {
148
+ font-family: 'Playfair Display';
149
+ font-style: normal;
150
+ font-weight: 400;
151
+ font-display: swap;
152
+ src: url(playfair-display-vietnamese-400.woff2) format('woff2');
153
+ unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
154
+ }
155
+ /* latin-ext */
156
+ @font-face {
157
+ font-family: 'Playfair Display';
158
+ font-style: normal;
159
+ font-weight: 400;
160
+ font-display: swap;
161
+ src: url(playfair-display-latin-ext-400.woff2) format('woff2');
162
+ unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
163
+ }
164
+ /* latin */
165
+ @font-face {
166
+ font-family: 'Playfair Display';
167
+ font-style: normal;
168
+ font-weight: 400;
169
+ font-display: swap;
170
+ src: url(playfair-display-latin-400.woff2) format('woff2');
171
+ unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
172
+ }
173
+ /* cyrillic */
174
+ @font-face {
175
+ font-family: 'Playfair Display';
176
+ font-style: normal;
177
+ font-weight: 500;
178
+ font-display: swap;
179
+ src: url(playfair-display-cyrillic-400.woff2) format('woff2');
180
+ unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
181
+ }
182
+ /* vietnamese */
183
+ @font-face {
184
+ font-family: 'Playfair Display';
185
+ font-style: normal;
186
+ font-weight: 500;
187
+ font-display: swap;
188
+ src: url(playfair-display-vietnamese-400.woff2) format('woff2');
189
+ unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
190
+ }
191
+ /* latin-ext */
192
+ @font-face {
193
+ font-family: 'Playfair Display';
194
+ font-style: normal;
195
+ font-weight: 500;
196
+ font-display: swap;
197
+ src: url(playfair-display-latin-ext-400.woff2) format('woff2');
198
+ unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
199
+ }
200
+ /* latin */
201
+ @font-face {
202
+ font-family: 'Playfair Display';
203
+ font-style: normal;
204
+ font-weight: 500;
205
+ font-display: swap;
206
+ src: url(playfair-display-latin-400.woff2) format('woff2');
207
+ unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
208
+ }
209
+ /* cyrillic */
210
+ @font-face {
211
+ font-family: 'Playfair Display';
212
+ font-style: normal;
213
+ font-weight: 600;
214
+ font-display: swap;
215
+ src: url(playfair-display-cyrillic-400.woff2) format('woff2');
216
+ unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
217
+ }
218
+ /* vietnamese */
219
+ @font-face {
220
+ font-family: 'Playfair Display';
221
+ font-style: normal;
222
+ font-weight: 600;
223
+ font-display: swap;
224
+ src: url(playfair-display-vietnamese-400.woff2) format('woff2');
225
+ unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
226
+ }
227
+ /* latin-ext */
228
+ @font-face {
229
+ font-family: 'Playfair Display';
230
+ font-style: normal;
231
+ font-weight: 600;
232
+ font-display: swap;
233
+ src: url(playfair-display-latin-ext-400.woff2) format('woff2');
234
+ unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
235
+ }
236
+ /* latin */
237
+ @font-face {
238
+ font-family: 'Playfair Display';
239
+ font-style: normal;
240
+ font-weight: 600;
241
+ font-display: swap;
242
+ src: url(playfair-display-latin-400.woff2) format('woff2');
243
+ unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
244
+ }
package/public/index.html CHANGED
@@ -4,9 +4,7 @@
4
4
  <meta charset="UTF-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
6
  <title>Memory Diagnoser</title>
7
- <link rel="preconnect" href="https://fonts.googleapis.com">
8
- <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
9
- <link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500;600&family=Playfair+Display:wght@400;500;600&display=swap" rel="stylesheet">
7
+ <link rel="stylesheet" href="fonts/fonts.css">
10
8
  <link rel="stylesheet" href="style.css">
11
9
  <link rel="stylesheet" href="themes.css">
12
10
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/styles/github-dark.min.css" id="hljsDark">
@@ -27,6 +25,7 @@
27
25
  <script defer src="/app.js"></script>
28
26
  </head>
29
27
  <body>
28
+ <script src="/vendor/claude-hub-sdk.js"></script>
30
29
  <div class="topbar">
31
30
  <div class="topbar-logo">
32
31
  <div class="logo-mark"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><ellipse cx="12" cy="5" rx="9" ry="3"/><path d="M3 5v4c0 1.66 4.03 3 9 3s9-1.34 9-3V5"/><path d="M3 9v4c0 1.66 4.03 3 9 3s9-1.34 9-3V9"/><path d="M3 13v4c0 1.66 4.03 3 9 3s9-1.34 9-3v-4"/></svg></div>
@@ -166,6 +165,7 @@
166
165
  <div class="modal modal-help">
167
166
  <div class="modal-header">
168
167
  <h3>Memory Diagnoser</h3>
168
+ <a class="help-docs" id="helpDocs" href="https://nikiforovall.blog/claude-code-memory/" target="_blank" rel="noopener">Docs ↗</a>
169
169
  <button class="modal-close" onclick="toggleHelpModal()">&#10005;</button>
170
170
  </div>
171
171
  <div class="modal-body">
package/public/style.css CHANGED
@@ -43,6 +43,13 @@
43
43
  padding: 0;
44
44
  box-sizing: border-box;
45
45
  }
46
+ button,
47
+ input,
48
+ select,
49
+ textarea {
50
+ font-family: inherit;
51
+ }
52
+
46
53
  body {
47
54
  /* native widgets (select popups, form controls) follow color-scheme, not CSS vars */
48
55
  color-scheme: dark;
@@ -964,6 +971,23 @@ body.light .tree-conditional .file-name {
964
971
  margin-bottom: 6px;
965
972
  text-transform: uppercase;
966
973
  }
974
+ .help-docs {
975
+ margin: 0 12px 0 auto;
976
+ padding: 4px 10px;
977
+ border: 1px solid var(--border);
978
+ border-radius: 6px;
979
+ color: var(--text-secondary);
980
+ font-size: 12px;
981
+ text-decoration: none;
982
+ white-space: nowrap;
983
+ transition:
984
+ color 0.15s ease,
985
+ border-color 0.15s ease;
986
+ }
987
+ .help-docs:hover {
988
+ color: var(--accent);
989
+ border-color: var(--accent);
990
+ }
967
991
  /* One shared grid for both halves (key | label | spacer | key | label): each key
968
992
  gutter is sized by its column's widest combo, and left/right rows share row
969
993
  tracks, so paired groups line up — a shorter group just leaves empty rows.
@@ -0,0 +1,50 @@
1
+ // claude-hub-sdk 1.1.0 (sha256 0fed14faa670). Copied by npm run sdk:sync in claude-code-hub. Do not edit.
2
+ // Claude Code Hub SDK stub: what an app serves when no hub runs it. Under a hub, the hub hands the
3
+ // app its real SDK (client.js) in place of this file. The API must match client.js; a test checks it.
4
+ ((root) => {
5
+ 'use strict';
6
+
7
+ function createClaudeHub(win) {
8
+ let hub = null;
9
+ const off = () => {};
10
+
11
+ function connect({ standalone = {} } = {}) {
12
+ if (hub) return hub;
13
+ const standaloneFn = (action) => {
14
+ const fn = (typeof standalone === 'function' ? standalone() : standalone)[action];
15
+ return typeof fn === 'function' ? fn : null;
16
+ };
17
+ hub = {
18
+ status: 'standalone',
19
+ themes: [],
20
+ inHub: false,
21
+ onStatus: () => off,
22
+ onActive: () => off,
23
+ onThemes: () => off,
24
+ subscribe: () => off,
25
+ bindTheme: () => off,
26
+ handle() {},
27
+ publish() {},
28
+ invoke(action, params = {}) {
29
+ const target = standaloneFn(action)?.(params);
30
+ if (!target) return Promise.resolve({ ok: false, reason: 'unhandled' });
31
+ win.open(target, '_blank', 'noopener');
32
+ return Promise.resolve({ ok: true, handledBy: 'standalone' });
33
+ },
34
+ can: (action) => !!standaloneFn(action),
35
+ forwards: () => false,
36
+ closeGuard() {},
37
+ openExternal(url) {
38
+ win.open(url, '_blank', 'noopener');
39
+ },
40
+ terminalToken: () => Promise.resolve(null),
41
+ };
42
+ return hub;
43
+ }
44
+
45
+ return { connect };
46
+ }
47
+
48
+ if (typeof module === 'object' && module.exports) module.exports = { createClaudeHub };
49
+ else root.ClaudeHub = createClaudeHub(root);
50
+ })(typeof window !== 'undefined' ? window : globalThis);
package/server.js CHANGED
@@ -608,18 +608,10 @@ app.use(net.frameGuard);
608
608
  app.use(net.originGuard);
609
609
 
610
610
  app.use(express.json());
611
+ // Under a hub, the hub hands over its SDK. Alone, public/vendor/claude-hub-sdk.js is a stub.
612
+ if (process.env.HUB_SDK_SERVER) require(process.env.HUB_SDK_SERVER).mount(app);
611
613
  app.use(express.static(path.join(__dirname, 'public')));
612
614
 
613
- app.get('/hub-config', (_req, res) => {
614
- res.json({
615
- name: 'Memory Diagnoser',
616
- icon: 'brain',
617
- description: 'Explore Claude Code memory sources',
618
- enabled: !!process.env.CLAUDE_HUB,
619
- url: process.env.HUB_URL || null,
620
- });
621
- });
622
-
623
615
  app.get('/api/project', (_req, res) => {
624
616
  res.json({ path: currentProjectPath, name: path.basename(currentProjectPath), configDir: CLAUDE_DIR });
625
617
  });