claude-code-memory-explorer 0.12.0 → 0.14.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 +1 -1
- package/lib/open-editor.js +4 -1
- package/package.json +1 -1
- package/public/app.js +171 -45
- package/public/index.html +16 -33
- package/public/style.css +184 -36
- package/server.js +15 -8
package/README.md
CHANGED
|
@@ -35,7 +35,7 @@ PORT=8080 Custom port (default: 3544, falls back if busy)
|
|
|
35
35
|
--open Open browser on start
|
|
36
36
|
```
|
|
37
37
|
|
|
38
|
-
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`.
|
|
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`.
|
|
39
39
|
|
|
40
40
|
## License
|
|
41
41
|
|
package/lib/open-editor.js
CHANGED
|
@@ -221,6 +221,7 @@ function execNoShell(cmd, args, opts = {}) {
|
|
|
221
221
|
let argv = args;
|
|
222
222
|
const spawnOpts = {
|
|
223
223
|
cwd: opts.cwd || undefined,
|
|
224
|
+
env: opts.env,
|
|
224
225
|
timeout: opts.timeout,
|
|
225
226
|
windowsHide: true,
|
|
226
227
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
@@ -258,7 +259,9 @@ function execNoShell(cmd, args, opts = {}) {
|
|
|
258
259
|
child.on('error', reject);
|
|
259
260
|
child.on('close', (code, signal) => {
|
|
260
261
|
if (signal) return reject(new Error(`Command terminated by ${signal}: ${stderr.trim()}`));
|
|
261
|
-
|
|
262
|
+
// The claude CLI reports its own failures on stdout, so stderr alone is
|
|
263
|
+
// often empty and the caller would surface only the exit code.
|
|
264
|
+
if (code !== 0) return reject(new Error(stderr.trim() || stdout.trim() || `Command exited with code ${code}`));
|
|
262
265
|
resolve({ stdout, stderr });
|
|
263
266
|
});
|
|
264
267
|
});
|
package/package.json
CHANGED
package/public/app.js
CHANGED
|
@@ -221,11 +221,10 @@ async function putProject(dirPath) {
|
|
|
221
221
|
}
|
|
222
222
|
|
|
223
223
|
function changeProject() {
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
renderRecentProjects();
|
|
224
|
+
document.getElementById('projectPickerInput').value = '';
|
|
225
|
+
renderProjectPicker();
|
|
227
226
|
document.getElementById('projectPickerModal').classList.add('open');
|
|
228
|
-
|
|
227
|
+
setPickerMode(false);
|
|
229
228
|
}
|
|
230
229
|
|
|
231
230
|
async function submitProjectPicker() {
|
|
@@ -248,6 +247,8 @@ async function submitProjectPicker() {
|
|
|
248
247
|
}
|
|
249
248
|
}
|
|
250
249
|
|
|
250
|
+
const RECENT_PROJECTS_MAX = 20;
|
|
251
|
+
|
|
251
252
|
function getRecentProjects() {
|
|
252
253
|
try {
|
|
253
254
|
return JSON.parse(localStorage.getItem('recentProjects') || '[]');
|
|
@@ -259,38 +260,102 @@ function getRecentProjects() {
|
|
|
259
260
|
function addRecentProject(p) {
|
|
260
261
|
const recent = getRecentProjects().filter((r) => r !== p);
|
|
261
262
|
recent.unshift(p);
|
|
262
|
-
localStorage.setItem('recentProjects', JSON.stringify(recent.slice(0,
|
|
263
|
+
localStorage.setItem('recentProjects', JSON.stringify(recent.slice(0, RECENT_PROJECTS_MAX)));
|
|
263
264
|
}
|
|
264
265
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
266
|
+
// The recents list is the switcher; typing a path is the secondary act behind "+ Add path".
|
|
267
|
+
let pickerRows = [];
|
|
268
|
+
let pickerIdx = -1;
|
|
269
|
+
|
|
270
|
+
function setPickerMode(add) {
|
|
271
|
+
document.getElementById('projectPickerFilter').hidden = add;
|
|
272
|
+
document.getElementById('projectPickerAddForm').hidden = !add;
|
|
273
|
+
const input = document.getElementById(add ? 'projectPathInput' : 'projectPickerInput');
|
|
274
|
+
if (add) input.value = document.getElementById('projectBtn').title;
|
|
275
|
+
// The overlay fades in, so a focus() on a field that is still hidden is dropped.
|
|
276
|
+
setTimeout(() => input.focus(), 100);
|
|
270
277
|
}
|
|
271
278
|
|
|
272
|
-
function
|
|
273
|
-
|
|
279
|
+
function splitProjectPath(p) {
|
|
280
|
+
const trimmed = p.replace(/[/\\]+$/, '');
|
|
281
|
+
const cut = Math.max(trimmed.lastIndexOf('/'), trimmed.lastIndexOf('\\'));
|
|
282
|
+
return { name: trimmed.slice(cut + 1), parent: trimmed.slice(0, Math.max(cut, 0)) };
|
|
274
283
|
}
|
|
275
284
|
|
|
276
|
-
function
|
|
277
|
-
const
|
|
285
|
+
function renderProjectPicker() {
|
|
286
|
+
const list = document.getElementById('projectPickerList');
|
|
287
|
+
const q = document.getElementById('projectPickerInput').value.trim().toLowerCase();
|
|
288
|
+
const current = document.getElementById('projectBtn').title;
|
|
278
289
|
const recent = getRecentProjects();
|
|
279
|
-
|
|
280
|
-
|
|
290
|
+
pickerRows = q ? recent.filter((p) => p.toLowerCase().includes(q)) : recent;
|
|
291
|
+
if (!pickerRows.length) {
|
|
292
|
+
pickerIdx = -1;
|
|
293
|
+
const msg = recent.length ? 'No projects match' : 'No recent projects — add one below';
|
|
294
|
+
list.innerHTML = `<div class="picker-empty">${msg}</div>`;
|
|
281
295
|
return;
|
|
282
296
|
}
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
297
|
+
list.innerHTML = pickerRows
|
|
298
|
+
.map((p, i) => {
|
|
299
|
+
const { name, parent } = splitProjectPath(p);
|
|
300
|
+
const cur = p === current;
|
|
301
|
+
return `<div class="picker-row${cur ? ' current' : ''}" onclick="_pickProject(${i})" title="${esc(p)}">
|
|
302
|
+
<span class="picker-name">${esc(name)}</span>
|
|
303
|
+
<span class="picker-parent">${esc(parent)}</span>
|
|
304
|
+
${cur ? '<span class="picker-tag">current</span>' : ''}
|
|
305
|
+
<button class="picker-remove" onclick="_removeRecentProject(${i}, event)" title="Remove">✕</button>
|
|
306
|
+
</div>`;
|
|
307
|
+
})
|
|
308
|
+
.join('');
|
|
309
|
+
// Opens on the first row that would actually change something.
|
|
310
|
+
setPickerCursor(pickerRows.findIndex((p) => p !== current));
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function setPickerCursor(idx) {
|
|
314
|
+
const list = document.getElementById('projectPickerList');
|
|
315
|
+
list.children[pickerIdx]?.classList.remove('selected');
|
|
316
|
+
pickerIdx = Math.min(Math.max(idx, 0), pickerRows.length - 1);
|
|
317
|
+
const row = list.children[pickerIdx];
|
|
318
|
+
row?.classList.add('selected');
|
|
319
|
+
row?.scrollIntoView({ block: 'nearest' });
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function _pickProject(idx) {
|
|
323
|
+
const p = pickerRows[idx];
|
|
324
|
+
if (!p) return;
|
|
325
|
+
document.getElementById('projectPathInput').value = p;
|
|
326
|
+
submitProjectPicker();
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function _removeRecentProject(idx, e) {
|
|
330
|
+
e.stopPropagation();
|
|
331
|
+
const target = pickerRows[idx];
|
|
332
|
+
if (!target) return;
|
|
333
|
+
localStorage.setItem('recentProjects', JSON.stringify(getRecentProjects().filter((p) => p !== target)));
|
|
334
|
+
renderProjectPicker();
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function initProjectPicker() {
|
|
338
|
+
document.getElementById('projectPickerAdd').addEventListener('click', () => setPickerMode(true));
|
|
339
|
+
document.getElementById('projectPickerBack').addEventListener('click', () => setPickerMode(false));
|
|
340
|
+
const filter = document.getElementById('projectPickerInput');
|
|
341
|
+
filter.addEventListener('input', renderProjectPicker);
|
|
342
|
+
filter.addEventListener('keydown', (e) => {
|
|
343
|
+
if (e.key === 'Escape') closeModal('projectPickerModal');
|
|
344
|
+
else if (e.key === 'ArrowDown' || (e.key === 'n' && e.ctrlKey)) setPickerCursor(pickerIdx + 1);
|
|
345
|
+
else if (e.key === 'ArrowUp' || (e.key === 'p' && e.ctrlKey)) setPickerCursor(pickerIdx - 1);
|
|
346
|
+
else if (e.key === 'Enter') _pickProject(pickerIdx);
|
|
347
|
+
else return;
|
|
348
|
+
e.preventDefault();
|
|
349
|
+
e.stopPropagation();
|
|
350
|
+
});
|
|
351
|
+
document.getElementById('projectPathInput').addEventListener('keydown', (e) => {
|
|
352
|
+
if (e.key === 'Enter') submitProjectPicker();
|
|
353
|
+
// Escape steps back to the list rather than closing — the list is where you came from.
|
|
354
|
+
else if (e.key === 'Escape') setPickerMode(false);
|
|
355
|
+
else return;
|
|
356
|
+
e.preventDefault();
|
|
357
|
+
e.stopPropagation();
|
|
358
|
+
});
|
|
294
359
|
}
|
|
295
360
|
|
|
296
361
|
// #endregion PROJECT
|
|
@@ -301,7 +366,80 @@ function closeModal(id) {
|
|
|
301
366
|
document.getElementById(id).classList.remove('open');
|
|
302
367
|
}
|
|
303
368
|
|
|
369
|
+
// Each entry pairs a left and a right group onto the same grid rows, so their
|
|
370
|
+
// headings sit level and the shorter group just leaves empty rows.
|
|
371
|
+
// `combo` joins the keys with a plus (a chord) instead of listing them as
|
|
372
|
+
// alternatives; `hub` marks rows that only work inside Claude Code Hub.
|
|
373
|
+
const SHORTCUT_PAIRS = [
|
|
374
|
+
[
|
|
375
|
+
{
|
|
376
|
+
title: 'Navigate',
|
|
377
|
+
rows: [
|
|
378
|
+
{ keys: ['j', '↓'], label: 'Next item' },
|
|
379
|
+
{ keys: ['k', '↑'], label: 'Previous item' },
|
|
380
|
+
{ keys: ['l', '→'], label: 'Expand / enter child' },
|
|
381
|
+
{ keys: ['h', '←'], label: 'Collapse / parent' },
|
|
382
|
+
{ keys: ['Enter'], label: 'Select item' },
|
|
383
|
+
{ keys: ['Esc'], label: 'Close modal' },
|
|
384
|
+
],
|
|
385
|
+
},
|
|
386
|
+
{
|
|
387
|
+
title: 'Actions',
|
|
388
|
+
rows: [
|
|
389
|
+
{ keys: ['e'], label: 'Open in editor' },
|
|
390
|
+
{ keys: ['r'], label: 'Refresh data' },
|
|
391
|
+
{ keys: ['t'], label: 'Toggle theme' },
|
|
392
|
+
{ keys: ['Shift', 'P'], combo: true, label: 'Switch project' },
|
|
393
|
+
{ keys: ['?'], label: 'Show this help' },
|
|
394
|
+
],
|
|
395
|
+
},
|
|
396
|
+
],
|
|
397
|
+
[
|
|
398
|
+
{
|
|
399
|
+
title: 'Hub',
|
|
400
|
+
hub: true,
|
|
401
|
+
rows: [
|
|
402
|
+
{ keys: ['Ctrl', 'Alt', '←/→'], combo: true, label: 'Previous / next hub app' },
|
|
403
|
+
{ keys: ['Alt', '1…9'], combo: true, label: 'Jump to hub app by number' },
|
|
404
|
+
{ keys: ['Ctrl', 'Alt', 'P'], combo: true, label: 'Project picker' },
|
|
405
|
+
],
|
|
406
|
+
},
|
|
407
|
+
],
|
|
408
|
+
];
|
|
409
|
+
|
|
410
|
+
const EMPTY_GROUP = { title: '', rows: [] };
|
|
411
|
+
|
|
412
|
+
// Interleaves each pair's rows left-then-right so CSS grid auto-placement lands
|
|
413
|
+
// them on shared row tracks (see .shortcuts in style.css).
|
|
414
|
+
function buildHelpShortcuts() {
|
|
415
|
+
const cells = [];
|
|
416
|
+
SHORTCUT_PAIRS.forEach(([left, right = EMPTY_GROUP], pair) => {
|
|
417
|
+
const first = pair === 0 ? ' sc-first' : '';
|
|
418
|
+
const sides = [
|
|
419
|
+
[left, 'sc-l'],
|
|
420
|
+
[right, 'sc-r'],
|
|
421
|
+
];
|
|
422
|
+
const head = (g, side) =>
|
|
423
|
+
g.title ? `<div class="${esc(`sc-group ${side}${first}${g.hub ? ' sc-hub' : ''}`)}">${g.title}</div>` : '';
|
|
424
|
+
cells.push(sides.map(([g, side]) => head(g, side)).join(''));
|
|
425
|
+
for (let i = 0; i < Math.max(left.rows.length, right.rows.length); i++) {
|
|
426
|
+
for (const [group, side] of sides) {
|
|
427
|
+
const row = group.rows[i];
|
|
428
|
+
if (!row) continue;
|
|
429
|
+
const sep = row.combo ? '<span class="sc-plus">+</span>' : '<span class="sc-or">/</span>';
|
|
430
|
+
const keys = row.keys.map((k) => `<kbd>${esc(k)}</kbd>`).join(sep);
|
|
431
|
+
const cls = side + (group.hub ? ' sc-hub' : '');
|
|
432
|
+
cells.push(`<dt class="${esc(cls)}">${keys}</dt><dd class="${esc(cls)}">${esc(row.label)}</dd>`);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
});
|
|
436
|
+
return cells.join('');
|
|
437
|
+
}
|
|
438
|
+
|
|
304
439
|
function toggleHelpModal() {
|
|
440
|
+
const list = document.getElementById('helpShortcuts');
|
|
441
|
+
if (!list.childElementCount) list.innerHTML = buildHelpShortcuts();
|
|
442
|
+
list.classList.toggle('sc-standalone', !window.__HUB__?.enabled);
|
|
305
443
|
document.getElementById('helpModal').classList.toggle('open');
|
|
306
444
|
}
|
|
307
445
|
|
|
@@ -312,19 +450,6 @@ function openHealthView() {
|
|
|
312
450
|
if (healthViewOpen) refreshAnalysis();
|
|
313
451
|
}
|
|
314
452
|
|
|
315
|
-
function bindModalKeys(inputId, modalId, submitFn) {
|
|
316
|
-
document.getElementById(inputId).addEventListener('keydown', (e) => {
|
|
317
|
-
if (e.key === 'Enter') {
|
|
318
|
-
e.preventDefault();
|
|
319
|
-
submitFn();
|
|
320
|
-
}
|
|
321
|
-
if (e.key === 'Escape') {
|
|
322
|
-
e.preventDefault();
|
|
323
|
-
closeModal(modalId);
|
|
324
|
-
}
|
|
325
|
-
});
|
|
326
|
-
}
|
|
327
|
-
|
|
328
453
|
// #endregion MODAL
|
|
329
454
|
|
|
330
455
|
// #region RENDER_TREE
|
|
@@ -1236,7 +1361,7 @@ function findingPrompt(f) {
|
|
|
1236
1361
|
} else if (paths.length > 1) {
|
|
1237
1362
|
header = 'In the Claude Code instruction files listed below, apply this fix:';
|
|
1238
1363
|
} else if (f.scope === 'user') {
|
|
1239
|
-
const path = stackData.find((s) => s.id === 'user-claude-md')?.path ||
|
|
1364
|
+
const path = stackData.find((s) => s.id === 'user-claude-md')?.path || summaryData.userClaudeMd;
|
|
1240
1365
|
header = `In the user-level Claude Code instructions at ${path}, apply this fix:`;
|
|
1241
1366
|
} else {
|
|
1242
1367
|
const dir = memoryDirPath();
|
|
@@ -1911,7 +2036,7 @@ document.addEventListener('keydown', (e) => {
|
|
|
1911
2036
|
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
|
|
1912
2037
|
const modal = document.querySelector('.modal-overlay.open');
|
|
1913
2038
|
if (modal) {
|
|
1914
|
-
if (e.key === 'Escape') {
|
|
2039
|
+
if (e.key === 'Escape' || (e.key === '?' && modal.id === 'helpModal')) {
|
|
1915
2040
|
modal.classList.remove('open');
|
|
1916
2041
|
e.preventDefault();
|
|
1917
2042
|
}
|
|
@@ -1973,8 +2098,9 @@ document.addEventListener('keydown', (e) => {
|
|
|
1973
2098
|
e.preventDefault();
|
|
1974
2099
|
fwd(e);
|
|
1975
2100
|
}
|
|
1976
|
-
//
|
|
1977
|
-
|
|
2101
|
+
// Own branch: the Alt+digit case below requires !ctrlKey. The hub owns the Ctrl+Alt+letter
|
|
2102
|
+
// keymap and ignores unbound letters.
|
|
2103
|
+
if (e.ctrlKey && e.altKey && !e.shiftKey && !e.metaKey && /^[a-z]$/i.test(e.key)) {
|
|
1978
2104
|
e.preventDefault();
|
|
1979
2105
|
fwd(e);
|
|
1980
2106
|
}
|
|
@@ -2116,7 +2242,7 @@ window.addEventListener('popstate', (e) => {
|
|
|
2116
2242
|
document.addEventListener('DOMContentLoaded', async () => {
|
|
2117
2243
|
loadTheme();
|
|
2118
2244
|
initResize();
|
|
2119
|
-
|
|
2245
|
+
initProjectPicker();
|
|
2120
2246
|
// Handle ?project= query param, else restore last-used project from localStorage
|
|
2121
2247
|
const params = new URLSearchParams(location.search);
|
|
2122
2248
|
let desiredProject = params.get('project');
|
package/public/index.html
CHANGED
|
@@ -100,15 +100,21 @@
|
|
|
100
100
|
<button class="modal-close" onclick="closeModal('projectPickerModal')">✕</button>
|
|
101
101
|
</div>
|
|
102
102
|
<div class="modal-body">
|
|
103
|
-
<div class="
|
|
104
|
-
<
|
|
105
|
-
<
|
|
106
|
-
<
|
|
103
|
+
<div class="picker-filter" id="projectPickerFilter">
|
|
104
|
+
<input type="text" id="projectPickerInput" class="picker-input" placeholder="Filter projects..." autocomplete="off">
|
|
105
|
+
<div class="picker-list" id="projectPickerList"></div>
|
|
106
|
+
<button class="picker-add" id="projectPickerAdd">+ Add path...</button>
|
|
107
107
|
</div>
|
|
108
|
-
<div id="
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
108
|
+
<div class="picker-add-form" id="projectPickerAddForm" hidden>
|
|
109
|
+
<div class="modal-field">
|
|
110
|
+
<label>Project directory</label>
|
|
111
|
+
<input type="text" id="projectPathInput" placeholder="/path/to/project" autocomplete="off">
|
|
112
|
+
<span class="modal-hint">Full path to the project directory</span>
|
|
113
|
+
</div>
|
|
114
|
+
<div class="modal-actions">
|
|
115
|
+
<button class="action-btn" id="projectPickerBack">Back</button>
|
|
116
|
+
<button class="action-btn primary" id="projectPickerSubmit" onclick="submitProjectPicker()">Switch</button>
|
|
117
|
+
</div>
|
|
112
118
|
</div>
|
|
113
119
|
</div>
|
|
114
120
|
</div>
|
|
@@ -152,7 +158,7 @@
|
|
|
152
158
|
|
|
153
159
|
<!-- Help modal -->
|
|
154
160
|
<div class="modal-overlay" id="helpModal" onclick="if(event.target===this)toggleHelpModal()">
|
|
155
|
-
<div class="modal">
|
|
161
|
+
<div class="modal modal-help">
|
|
156
162
|
<div class="modal-header">
|
|
157
163
|
<h3>Memory Diagnoser</h3>
|
|
158
164
|
<button class="modal-close" onclick="toggleHelpModal()">✕</button>
|
|
@@ -161,31 +167,8 @@
|
|
|
161
167
|
<p class="modal-desc">Browse and understand all memory sources that influence Claude Code behavior for your project.</p>
|
|
162
168
|
<hr class="modal-divider">
|
|
163
169
|
<h4 class="shortcut-section">Keyboard Shortcuts</h4>
|
|
164
|
-
<
|
|
165
|
-
<div>
|
|
166
|
-
<h4 class="shortcut-section">Navigation</h4>
|
|
167
|
-
<table class="shortcut-table">
|
|
168
|
-
<tr><td><kbd>j</kbd> / <kbd>↓</kbd></td><td>Next item</td></tr>
|
|
169
|
-
<tr><td><kbd>k</kbd> / <kbd>↑</kbd></td><td>Previous item</td></tr>
|
|
170
|
-
<tr><td><kbd>l</kbd> / <kbd>→</kbd></td><td>Expand / enter child</td></tr>
|
|
171
|
-
<tr><td><kbd>h</kbd> / <kbd>←</kbd></td><td>Collapse / parent</td></tr>
|
|
172
|
-
<tr><td><kbd>Enter</kbd></td><td>Select item</td></tr>
|
|
173
|
-
<tr><td><kbd>Esc</kbd></td><td>Close modal</td></tr>
|
|
174
|
-
</table>
|
|
175
|
-
</div>
|
|
176
|
-
<div>
|
|
177
|
-
<h4 class="shortcut-section">Actions</h4>
|
|
178
|
-
<table class="shortcut-table">
|
|
179
|
-
<tr><td><kbd>r</kbd></td><td>Refresh data</td></tr>
|
|
180
|
-
<tr><td><kbd>t</kbd></td><td>Toggle theme</td></tr>
|
|
181
|
-
<tr><td><kbd>e</kbd></td><td>Open in editor</td></tr>
|
|
182
|
-
<tr><td><kbd>Shift+P</kbd></td><td>Switch project</td></tr>
|
|
183
|
-
<tr><td><kbd>?</kbd></td><td>Show this help</td></tr>
|
|
184
|
-
</table>
|
|
185
|
-
</div>
|
|
186
|
-
</div>
|
|
170
|
+
<dl class="shortcuts" id="helpShortcuts"></dl>
|
|
187
171
|
</div>
|
|
188
|
-
<button class="modal-close-btn" onclick="toggleHelpModal()">Close</button>
|
|
189
172
|
</div>
|
|
190
173
|
</div>
|
|
191
174
|
|
package/public/style.css
CHANGED
|
@@ -805,6 +805,24 @@ body.light .tree-conditional .file-name {
|
|
|
805
805
|
max-height: 80vh;
|
|
806
806
|
overflow-y: auto;
|
|
807
807
|
}
|
|
808
|
+
/* The paired shortcut columns need room the 440px default cannot give. */
|
|
809
|
+
.modal.modal-help {
|
|
810
|
+
width: 960px;
|
|
811
|
+
}
|
|
812
|
+
/* Matches the roomier help modals in the other hub apps: larger title, 24px gutters. */
|
|
813
|
+
.modal-help .modal-header {
|
|
814
|
+
padding: 20px 24px;
|
|
815
|
+
}
|
|
816
|
+
.modal-help .modal-header h3 {
|
|
817
|
+
font-family: var(--font-mono);
|
|
818
|
+
font-size: 18px;
|
|
819
|
+
}
|
|
820
|
+
.modal-help .modal-body {
|
|
821
|
+
padding: 24px;
|
|
822
|
+
}
|
|
823
|
+
.modal-help .modal-desc {
|
|
824
|
+
font-size: 13px;
|
|
825
|
+
}
|
|
808
826
|
.modal.modal-wide {
|
|
809
827
|
width: 1140px;
|
|
810
828
|
}
|
|
@@ -878,21 +896,6 @@ body.light .tree-conditional .file-name {
|
|
|
878
896
|
border-top: 1px solid var(--border);
|
|
879
897
|
margin: 12px 0;
|
|
880
898
|
}
|
|
881
|
-
.modal-close-btn {
|
|
882
|
-
display: block;
|
|
883
|
-
width: calc(100% - 32px);
|
|
884
|
-
margin: 0 16px 16px;
|
|
885
|
-
padding: 8px;
|
|
886
|
-
background: var(--bg-elevated);
|
|
887
|
-
border: 1px solid var(--border);
|
|
888
|
-
border-radius: 4px;
|
|
889
|
-
color: var(--text-secondary);
|
|
890
|
-
cursor: pointer;
|
|
891
|
-
font-size: 12px;
|
|
892
|
-
}
|
|
893
|
-
.modal-close-btn:hover {
|
|
894
|
-
background: var(--bg-hover);
|
|
895
|
-
}
|
|
896
899
|
/* matches the marketplace app's action-btn look */
|
|
897
900
|
.action-btn {
|
|
898
901
|
padding: 4px 10px;
|
|
@@ -961,19 +964,90 @@ body.light .tree-conditional .file-name {
|
|
|
961
964
|
margin-bottom: 6px;
|
|
962
965
|
text-transform: uppercase;
|
|
963
966
|
}
|
|
964
|
-
|
|
967
|
+
/* One shared grid for both halves (key | label | spacer | key | label): each key
|
|
968
|
+
gutter is sized by its column's widest combo, and left/right rows share row
|
|
969
|
+
tracks, so paired groups line up — a shorter group just leaves empty rows.
|
|
970
|
+
Cells opt into a half via sc-l / sc-r; the markup is generated from
|
|
971
|
+
SHORTCUT_PAIRS in app.js. */
|
|
972
|
+
.shortcuts {
|
|
973
|
+
margin: 0;
|
|
965
974
|
display: grid;
|
|
966
|
-
grid-template-columns: 1fr 1fr;
|
|
967
|
-
gap:
|
|
975
|
+
grid-template-columns: max-content 1fr 28px max-content 1fr;
|
|
976
|
+
column-gap: 14px;
|
|
968
977
|
}
|
|
969
|
-
.
|
|
970
|
-
|
|
978
|
+
.shortcuts dt.sc-l {
|
|
979
|
+
grid-column: 1;
|
|
980
|
+
}
|
|
981
|
+
.shortcuts dd.sc-l {
|
|
982
|
+
grid-column: 2;
|
|
983
|
+
}
|
|
984
|
+
.shortcuts dt.sc-r {
|
|
985
|
+
grid-column: 4;
|
|
986
|
+
}
|
|
987
|
+
.shortcuts dd.sc-r {
|
|
988
|
+
grid-column: 5;
|
|
989
|
+
}
|
|
990
|
+
.shortcuts .sc-group.sc-l {
|
|
991
|
+
grid-column: 1 / 3;
|
|
992
|
+
}
|
|
993
|
+
.shortcuts .sc-group.sc-r {
|
|
994
|
+
grid-column: 4 / 6;
|
|
995
|
+
}
|
|
996
|
+
.shortcuts dt {
|
|
997
|
+
margin: 0;
|
|
998
|
+
display: flex;
|
|
999
|
+
align-items: center;
|
|
1000
|
+
align-self: center;
|
|
1001
|
+
gap: 4px;
|
|
1002
|
+
padding: 3px 0;
|
|
1003
|
+
}
|
|
1004
|
+
.shortcuts dd {
|
|
1005
|
+
margin: 0;
|
|
1006
|
+
/* The other hub apps set a mono body font; memory is sans, so the help rows
|
|
1007
|
+
opt into mono to keep the shortcut tables reading the same across apps. */
|
|
1008
|
+
font-family: var(--font-mono);
|
|
1009
|
+
font-size: 13px;
|
|
1010
|
+
color: var(--text-secondary);
|
|
1011
|
+
align-self: center;
|
|
1012
|
+
padding: 3px 0;
|
|
1013
|
+
}
|
|
1014
|
+
/* Section headings carry the column: full-width rule + primary ink, so the eye
|
|
1015
|
+
finds the group before it reads any single row. */
|
|
1016
|
+
.shortcuts .sc-group {
|
|
1017
|
+
padding: 18px 0 6px;
|
|
1018
|
+
margin-bottom: 6px;
|
|
1019
|
+
font-size: 11px;
|
|
1020
|
+
font-weight: 700;
|
|
1021
|
+
letter-spacing: 0.16em;
|
|
1022
|
+
text-transform: uppercase;
|
|
1023
|
+
color: var(--text-primary);
|
|
1024
|
+
border-bottom: 1px solid var(--border);
|
|
971
1025
|
}
|
|
972
|
-
.
|
|
973
|
-
padding: 2px
|
|
1026
|
+
.shortcuts .sc-group.sc-first {
|
|
1027
|
+
padding-top: 2px;
|
|
974
1028
|
}
|
|
975
|
-
|
|
976
|
-
|
|
1029
|
+
/* keycap chips: thicker bottom border reads as a key */
|
|
1030
|
+
.shortcuts kbd {
|
|
1031
|
+
display: inline-flex;
|
|
1032
|
+
align-items: center;
|
|
1033
|
+
justify-content: center;
|
|
1034
|
+
min-width: 22px;
|
|
1035
|
+
height: 22px;
|
|
1036
|
+
padding: 0 6px;
|
|
1037
|
+
box-sizing: border-box;
|
|
1038
|
+
color: var(--text-primary);
|
|
1039
|
+
border-bottom-width: 2px;
|
|
1040
|
+
border-radius: 5px;
|
|
1041
|
+
}
|
|
1042
|
+
.shortcuts .sc-plus,
|
|
1043
|
+
.shortcuts .sc-or {
|
|
1044
|
+
color: var(--text-muted);
|
|
1045
|
+
font-size: 11px;
|
|
1046
|
+
}
|
|
1047
|
+
/* Hub keys do nothing outside Claude Code Hub, so the standalone app hides that
|
|
1048
|
+
whole column rather than listing keys that no-op. */
|
|
1049
|
+
.shortcuts.sc-standalone .sc-hub {
|
|
1050
|
+
display: none;
|
|
977
1051
|
}
|
|
978
1052
|
kbd {
|
|
979
1053
|
background: var(--bg-hover);
|
|
@@ -989,31 +1063,77 @@ kbd {
|
|
|
989
1063
|
font-size: 10px;
|
|
990
1064
|
line-height: 1;
|
|
991
1065
|
}
|
|
992
|
-
.
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
1066
|
+
.picker-filter,
|
|
1067
|
+
.picker-add-form {
|
|
1068
|
+
display: flex;
|
|
1069
|
+
flex-direction: column;
|
|
1070
|
+
gap: 10px;
|
|
1071
|
+
}
|
|
1072
|
+
.picker-filter[hidden],
|
|
1073
|
+
.picker-add-form[hidden] {
|
|
1074
|
+
display: none;
|
|
1075
|
+
}
|
|
1076
|
+
.picker-input {
|
|
1077
|
+
width: 100%;
|
|
1078
|
+
padding: 8px 10px;
|
|
1079
|
+
background: var(--bg-deep);
|
|
1080
|
+
border: 1px solid var(--border);
|
|
1081
|
+
border-radius: 4px;
|
|
1082
|
+
color: var(--text-primary);
|
|
1083
|
+
font-family: var(--font-mono);
|
|
1084
|
+
font-size: 13px;
|
|
1085
|
+
}
|
|
1086
|
+
.picker-input:focus {
|
|
1087
|
+
outline: none;
|
|
1088
|
+
border-color: var(--accent);
|
|
996
1089
|
}
|
|
997
|
-
.
|
|
1090
|
+
.picker-list {
|
|
1091
|
+
max-height: 50vh;
|
|
1092
|
+
overflow-y: auto;
|
|
998
1093
|
display: flex;
|
|
999
|
-
|
|
1094
|
+
flex-direction: column;
|
|
1095
|
+
gap: 2px;
|
|
1096
|
+
}
|
|
1097
|
+
.picker-row {
|
|
1098
|
+
display: flex;
|
|
1099
|
+
align-items: baseline;
|
|
1100
|
+
gap: 8px;
|
|
1000
1101
|
padding: 5px 8px;
|
|
1102
|
+
border: 1px solid transparent;
|
|
1001
1103
|
border-radius: 4px;
|
|
1002
1104
|
cursor: pointer;
|
|
1003
1105
|
font-size: 12px;
|
|
1004
1106
|
font-family: var(--font-mono);
|
|
1005
1107
|
color: var(--text-secondary);
|
|
1006
1108
|
}
|
|
1007
|
-
.
|
|
1109
|
+
.picker-row:hover {
|
|
1008
1110
|
background: var(--bg-hover);
|
|
1111
|
+
color: var(--text-primary);
|
|
1112
|
+
}
|
|
1113
|
+
.picker-row.selected {
|
|
1114
|
+
background: var(--bg-hover);
|
|
1115
|
+
border-color: var(--accent);
|
|
1116
|
+
color: var(--text-primary);
|
|
1009
1117
|
}
|
|
1010
|
-
.
|
|
1118
|
+
.picker-name {
|
|
1119
|
+
white-space: nowrap;
|
|
1120
|
+
}
|
|
1121
|
+
.picker-parent {
|
|
1011
1122
|
flex: 1;
|
|
1123
|
+
min-width: 0;
|
|
1012
1124
|
overflow: hidden;
|
|
1013
1125
|
text-overflow: ellipsis;
|
|
1014
1126
|
white-space: nowrap;
|
|
1127
|
+
font-size: 11px;
|
|
1128
|
+
color: var(--text-muted);
|
|
1129
|
+
}
|
|
1130
|
+
.picker-tag {
|
|
1131
|
+
font-size: 10px;
|
|
1132
|
+
text-transform: uppercase;
|
|
1133
|
+
letter-spacing: 0.5px;
|
|
1134
|
+
color: var(--accent-text);
|
|
1015
1135
|
}
|
|
1016
|
-
.
|
|
1136
|
+
.picker-remove {
|
|
1017
1137
|
background: none;
|
|
1018
1138
|
border: none;
|
|
1019
1139
|
color: var(--text-muted);
|
|
@@ -1022,9 +1142,33 @@ kbd {
|
|
|
1022
1142
|
padding: 0 4px;
|
|
1023
1143
|
opacity: 0;
|
|
1024
1144
|
}
|
|
1025
|
-
.
|
|
1145
|
+
.picker-row:hover .picker-remove,
|
|
1146
|
+
.picker-row.selected .picker-remove {
|
|
1026
1147
|
opacity: 1;
|
|
1027
1148
|
}
|
|
1149
|
+
.picker-remove:hover {
|
|
1150
|
+
color: var(--error);
|
|
1151
|
+
}
|
|
1152
|
+
.picker-empty {
|
|
1153
|
+
padding: 12px 10px;
|
|
1154
|
+
color: var(--text-muted);
|
|
1155
|
+
font-size: 12px;
|
|
1156
|
+
}
|
|
1157
|
+
.picker-add {
|
|
1158
|
+
width: 100%;
|
|
1159
|
+
text-align: left;
|
|
1160
|
+
background: none;
|
|
1161
|
+
border: none;
|
|
1162
|
+
border-top: 1px solid var(--border);
|
|
1163
|
+
padding: 8px 2px 2px;
|
|
1164
|
+
color: var(--text-muted);
|
|
1165
|
+
cursor: pointer;
|
|
1166
|
+
font-family: inherit;
|
|
1167
|
+
font-size: 12px;
|
|
1168
|
+
}
|
|
1169
|
+
.picker-add:hover {
|
|
1170
|
+
color: var(--accent-text);
|
|
1171
|
+
}
|
|
1028
1172
|
/* #endregion MODAL */
|
|
1029
1173
|
|
|
1030
1174
|
/* #region LOADING */
|
|
@@ -1131,8 +1275,12 @@ body.light {
|
|
|
1131
1275
|
.summary-bar {
|
|
1132
1276
|
flex-wrap: wrap;
|
|
1133
1277
|
}
|
|
1134
|
-
|
|
1135
|
-
|
|
1278
|
+
/* Rows are interleaved left-then-right, so a one-column stack would read out
|
|
1279
|
+
of order. Keep both halves and just tighten the gutters. */
|
|
1280
|
+
.shortcuts {
|
|
1281
|
+
grid-template-columns: max-content 1fr 12px max-content 1fr;
|
|
1282
|
+
column-gap: 8px;
|
|
1283
|
+
font-size: 11px;
|
|
1136
1284
|
}
|
|
1137
1285
|
}
|
|
1138
1286
|
/* #endregion RESPONSIVE */
|
package/server.js
CHANGED
|
@@ -25,7 +25,13 @@ const expandHome = (p) => (typeof p === 'string' ? p : '').replace(/^~/, os.home
|
|
|
25
25
|
const PORT = getArg('port') || process.env.PORT || 3544;
|
|
26
26
|
const AUTO_OPEN = process.argv.includes('--open');
|
|
27
27
|
const claudeDirArg = getArg('dir') || process.env.CLAUDE_CONFIG_DIR || process.env.CLAUDE_DIR;
|
|
28
|
-
const
|
|
28
|
+
const DEFAULT_CLAUDE_DIR = path.join(os.homedir(), '.claude');
|
|
29
|
+
const CLAUDE_DIR = claudeDirArg ? expandHome(claudeDirArg) : DEFAULT_CLAUDE_DIR;
|
|
30
|
+
const USER_CLAUDE_MD = path.join(CLAUDE_DIR, 'CLAUDE.md');
|
|
31
|
+
// Claude Code keeps .claude.json beside ~/.claude by default but inside CLAUDE_CONFIG_DIR
|
|
32
|
+
// when that is set, so the default dir must stay unset for the CLI rather than be spelled out.
|
|
33
|
+
const IS_DEFAULT_CLAUDE_DIR = path.resolve(CLAUDE_DIR) === path.resolve(DEFAULT_CLAUDE_DIR);
|
|
34
|
+
const CLI_ENV = IS_DEFAULT_CLAUDE_DIR ? process.env : { ...process.env, CLAUDE_CONFIG_DIR: CLAUDE_DIR };
|
|
29
35
|
const projectDirArg = getArg('project');
|
|
30
36
|
|
|
31
37
|
// #endregion CLI_ARGS
|
|
@@ -233,8 +239,7 @@ function discoverMemorySources(projectPath) {
|
|
|
233
239
|
}
|
|
234
240
|
|
|
235
241
|
// 2. User CLAUDE.md
|
|
236
|
-
const
|
|
237
|
-
const userInfo = fileInfo(userClaudeMd);
|
|
242
|
+
const userInfo = fileInfo(USER_CLAUDE_MD);
|
|
238
243
|
if (userInfo) {
|
|
239
244
|
sources.push({
|
|
240
245
|
id: 'user-claude-md',
|
|
@@ -843,7 +848,7 @@ const RUBRIC_CLAUDE_MD = [
|
|
|
843
848
|
const RUBRIC_CLAUDE_MD_USER = [
|
|
844
849
|
'User-level CLAUDE.md criteria (global instructions, loaded in every session of every project):',
|
|
845
850
|
'- Universality: every rule must hold on any project. Flag rules referencing one specific repo, stack, tool chain, or project path — they belong in that project\'s CLAUDE.md (kind: demote).',
|
|
846
|
-
|
|
851
|
+
`- Machine verification only: check that tools the file names exist on PATH (command -v / which / where) and that files, skills, or agents it references exist under ${CLAUDE_DIR}. NEVER verify against the working directory's repository — it is an arbitrary project and proves nothing about a global rule.`,
|
|
847
852
|
'- Self-consistency: flag sections, tags, or mnemonics that contradict or duplicate each other within the file and its imports.',
|
|
848
853
|
'- Executability: flag standards name-dropped without concrete behaviors ("follow style guide X") and rules the model cannot act on — require the 2-3 specific behaviors the user actually wants.',
|
|
849
854
|
'- Flag emphasis inflation (all-caps, NEVER/ALWAYS without stakes) and vague or no-op rules the model cannot act on.',
|
|
@@ -970,7 +975,7 @@ const REVIEWER_COMMON = [
|
|
|
970
975
|
// directory is an arbitrary project and must not influence a global-file audit.
|
|
971
976
|
const REVIEWER_COMMON_USER = [
|
|
972
977
|
'You review the user-level (global) CLAUDE.md that loads in every session of every project; the parent passes its name and full content.',
|
|
973
|
-
|
|
978
|
+
`Verify before judging, against the machine only: check tools the file names with \`command -v\` / \`which\` / \`where\` (the only Bash commands you may run), and check files, skills, or agents it references under ${CLAUDE_DIR} with your read tools. NEVER read or judge against the repository in your working directory — it is one arbitrary project and proves nothing about a global rule.`,
|
|
974
979
|
'Also judge whether the content is still needed at all: if the tool it references is gone from the machine or the file it points to no longer exists, say so explicitly.',
|
|
975
980
|
'A check you could not run is not evidence of absence. If a probe is denied by permissions or errors out, report that claim as UNCHECKED and say why — never turn a failed check into a stale or invalidate finding.',
|
|
976
981
|
];
|
|
@@ -1019,7 +1024,7 @@ const ANALYSIS_AGENTS = {
|
|
|
1019
1024
|
[RUBRIC_CLAUDE_MD, RUBRIC_IMPORTS],
|
|
1020
1025
|
),
|
|
1021
1026
|
'claude-md-user-reviewer': reviewerAgent(
|
|
1022
|
-
|
|
1027
|
+
`Reviews and verifies the user-level (global) CLAUDE.md against the user rubric, PATH, and ${CLAUDE_DIR} — never against the working repository.`,
|
|
1023
1028
|
[RUBRIC_CLAUDE_MD_USER, RUBRIC_IMPORTS],
|
|
1024
1029
|
REVIEWER_COMMON_USER,
|
|
1025
1030
|
),
|
|
@@ -1039,7 +1044,7 @@ function runClaudeAnalysis(prompt, model, cwd) {
|
|
|
1039
1044
|
try {
|
|
1040
1045
|
// cwd = the project root so the agent can verify claims against the actual
|
|
1041
1046
|
// files with its read-only tools (Read/Glob/Grep need no permission grants).
|
|
1042
|
-
child = spawn('claude', args, { cwd: cwd || os.tmpdir(), windowsHide: true });
|
|
1047
|
+
child = spawn('claude', args, { cwd: cwd || os.tmpdir(), env: CLI_ENV, windowsHide: true });
|
|
1043
1048
|
} catch (e) {
|
|
1044
1049
|
return resolve({ ok: false, error: `Failed to spawn claude: ${e.message}` });
|
|
1045
1050
|
}
|
|
@@ -1313,7 +1318,9 @@ app.get('/api/summary', (_req, res) => {
|
|
|
1313
1318
|
for (const f of sources) scopeChars[f.scope] = (scopeChars[f.scope] || 0) + (f.chars || 0);
|
|
1314
1319
|
const totalChars = sources.reduce((s, f) => s + (f.chars || 0), 0) + skillDesc.chars;
|
|
1315
1320
|
// ids: which sources make up the footprint, so the client can highlight them
|
|
1316
|
-
|
|
1321
|
+
// userClaudeMd: where the user-level file lives even when it does not exist yet, so the
|
|
1322
|
+
// client can name it in fix prompts without assuming ~/.claude
|
|
1323
|
+
res.json({ totalFiles, totalLines, totalBytes, totalChars, scopeChars, skillDesc, alwaysLoaded, conditional, onDemand, ids: sources.map(s => s.id), userClaudeMd: USER_CLAUDE_MD });
|
|
1317
1324
|
});
|
|
1318
1325
|
|
|
1319
1326
|
app.get('/api/stack', (_req, res) => {
|