claude-code-memory-explorer 0.1.0 → 0.2.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/package.json +1 -1
- package/public/app.js +69 -29
- package/public/index.html +3 -0
- package/public/style.css +27 -12
- package/server.js +34 -0
package/package.json
CHANGED
package/public/app.js
CHANGED
|
@@ -59,11 +59,13 @@ function highlightSource(text, fileName) {
|
|
|
59
59
|
|
|
60
60
|
function linkifyImports(text, sourceId) {
|
|
61
61
|
if (!stackData.length) return { text, placeholders: [] };
|
|
62
|
-
const byName = Object.fromEntries(stackData.map(c => [c.name, c]));
|
|
62
|
+
const byName = Object.fromEntries(stackData.map((c) => [c.name, c]));
|
|
63
63
|
const placeholders = [];
|
|
64
64
|
const ph = (child, display) => {
|
|
65
65
|
const token = `\x00LINK${placeholders.length}\x00`;
|
|
66
|
-
placeholders.push(
|
|
66
|
+
placeholders.push(
|
|
67
|
+
`<a class="inline-import" href="#" onclick="selectFile('${escJs(child.id)}');return false" title="${esc(child.path)}">${esc(display)}</a>`,
|
|
68
|
+
);
|
|
67
69
|
return token;
|
|
68
70
|
};
|
|
69
71
|
// Replace @path refs with placeholders
|
|
@@ -293,7 +295,9 @@ function getTreeIndex() {
|
|
|
293
295
|
return treeIndex;
|
|
294
296
|
}
|
|
295
297
|
|
|
296
|
-
function invalidateTreeIndex() {
|
|
298
|
+
function invalidateTreeIndex() {
|
|
299
|
+
treeIndex = null;
|
|
300
|
+
}
|
|
297
301
|
|
|
298
302
|
function renderTree() {
|
|
299
303
|
const container = document.getElementById('treeContent');
|
|
@@ -376,9 +380,7 @@ function navigateGroup(direction) {
|
|
|
376
380
|
if (!activeScopes.length) return;
|
|
377
381
|
|
|
378
382
|
const current = stackData.find((s) => s.id === selectedFileId);
|
|
379
|
-
const currentScope = current?.parentId
|
|
380
|
-
? stackData.find((s) => s.id === current.parentId)?.scope
|
|
381
|
-
: current?.scope;
|
|
383
|
+
const currentScope = current?.parentId ? stackData.find((s) => s.id === current.parentId)?.scope : current?.scope;
|
|
382
384
|
let scopeIdx = activeScopes.indexOf(currentScope);
|
|
383
385
|
if (scopeIdx === -1) {
|
|
384
386
|
scopeIdx = direction > 0 ? 0 : activeScopes.length - 1;
|
|
@@ -435,7 +437,7 @@ async function renderPreview() {
|
|
|
435
437
|
html += '</div>';
|
|
436
438
|
|
|
437
439
|
// Imports — only show children (files that have this source as parent)
|
|
438
|
-
const children = stackData.filter(s => s.parentId === source.id);
|
|
440
|
+
const children = stackData.filter((s) => s.parentId === source.id);
|
|
439
441
|
const unresolved = source.unresolvedImports || [];
|
|
440
442
|
if (children.length || unresolved.length) {
|
|
441
443
|
html += '<div class="preview-imports">';
|
|
@@ -516,11 +518,15 @@ function renderBudget() {
|
|
|
516
518
|
document.getElementById('statAlways').textContent = summaryData.alwaysLoaded;
|
|
517
519
|
|
|
518
520
|
// Budget text
|
|
519
|
-
document.getElementById('budgetText').textContent =
|
|
521
|
+
document.getElementById('budgetText').textContent =
|
|
522
|
+
`${summaryData.totalLines.toLocaleString()} lines / ${formatBytes(summaryData.totalBytes)}`;
|
|
520
523
|
|
|
521
524
|
// Budget segments — proportional by lines per scope
|
|
522
525
|
const segContainer = document.getElementById('budgetSegments');
|
|
523
|
-
if (!stackData.length) {
|
|
526
|
+
if (!stackData.length) {
|
|
527
|
+
segContainer.innerHTML = '';
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
524
530
|
|
|
525
531
|
const scopeTotals = {};
|
|
526
532
|
for (const s of stackData) {
|
|
@@ -543,10 +549,7 @@ function renderBudget() {
|
|
|
543
549
|
|
|
544
550
|
async function loadData() {
|
|
545
551
|
try {
|
|
546
|
-
[stackData, summaryData] = await Promise.all([
|
|
547
|
-
fetchJSON('/api/stack'),
|
|
548
|
-
fetchJSON('/api/summary'),
|
|
549
|
-
]);
|
|
552
|
+
[stackData, summaryData] = await Promise.all([fetchJSON('/api/stack'), fetchJSON('/api/summary')]);
|
|
550
553
|
invalidateTreeIndex();
|
|
551
554
|
renderTree();
|
|
552
555
|
renderBudget();
|
|
@@ -556,7 +559,10 @@ async function loadData() {
|
|
|
556
559
|
const auto = proj || user;
|
|
557
560
|
if (auto) selectedFileId = auto.id;
|
|
558
561
|
}
|
|
559
|
-
if (selectedFileId) {
|
|
562
|
+
if (selectedFileId) {
|
|
563
|
+
renderTree();
|
|
564
|
+
renderPreview();
|
|
565
|
+
}
|
|
560
566
|
} catch (err) {
|
|
561
567
|
showToast('Failed to load: ' + err.message, 'error');
|
|
562
568
|
}
|
|
@@ -609,10 +615,22 @@ document.addEventListener('keydown', (e) => {
|
|
|
609
615
|
e.preventDefault();
|
|
610
616
|
changeProject();
|
|
611
617
|
}
|
|
612
|
-
if (e.key === 'j' || e.key === 'ArrowDown') {
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
618
|
+
if (e.key === 'j' || e.key === 'ArrowDown') {
|
|
619
|
+
e.preventDefault();
|
|
620
|
+
navigateTree(1);
|
|
621
|
+
}
|
|
622
|
+
if (e.key === 'k' || e.key === 'ArrowUp') {
|
|
623
|
+
e.preventDefault();
|
|
624
|
+
navigateTree(-1);
|
|
625
|
+
}
|
|
626
|
+
if (e.key === 'h' || e.key === 'ArrowLeft') {
|
|
627
|
+
e.preventDefault();
|
|
628
|
+
navigateGroup(-1);
|
|
629
|
+
}
|
|
630
|
+
if (e.key === 'l' || e.key === 'ArrowRight') {
|
|
631
|
+
e.preventDefault();
|
|
632
|
+
navigateGroup(1);
|
|
633
|
+
}
|
|
616
634
|
if (e.key === 'Enter' && selectedFileId) renderPreview();
|
|
617
635
|
if (e.key === 'e' && selectedFileId) {
|
|
618
636
|
const s = stackData.find((x) => x.id === selectedFileId);
|
|
@@ -624,13 +642,27 @@ document.addEventListener('keydown', (e) => {
|
|
|
624
642
|
|
|
625
643
|
// #region HUB_INTEGRATION
|
|
626
644
|
|
|
627
|
-
async function
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
645
|
+
(async function initHub() {
|
|
646
|
+
const cfg = await fetch('/hub-config')
|
|
647
|
+
.then((r) => r.json())
|
|
648
|
+
.catch(() => ({}));
|
|
649
|
+
if (!cfg.enabled) return;
|
|
650
|
+
window.__HUB__ = cfg;
|
|
651
|
+
document.addEventListener('keydown', (e) => {
|
|
652
|
+
if (e.ctrlKey && e.altKey && (e.key === 'ArrowLeft' || e.key === 'ArrowRight')) {
|
|
653
|
+
e.preventDefault();
|
|
654
|
+
window.parent?.postMessage({ type: 'hub:keydown', key: e.key }, '*');
|
|
655
|
+
}
|
|
656
|
+
if (e.altKey && !e.ctrlKey && !e.shiftKey && !e.metaKey && /^[1-9]$/.test(e.key)) {
|
|
657
|
+
e.preventDefault();
|
|
658
|
+
window.parent?.postMessage({ type: 'hub:keydown', key: e.key }, '*');
|
|
659
|
+
}
|
|
660
|
+
});
|
|
661
|
+
})();
|
|
662
|
+
|
|
663
|
+
function hubNavigate(app, url) {
|
|
664
|
+
if (!window.__HUB__?.enabled) return;
|
|
665
|
+
window.parent?.postMessage({ type: 'hub:navigate', app, url }, '*');
|
|
634
666
|
}
|
|
635
667
|
|
|
636
668
|
// #endregion HUB_INTEGRATION
|
|
@@ -688,7 +720,6 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|
|
688
720
|
loadTheme();
|
|
689
721
|
initResize();
|
|
690
722
|
bindModalKeys('projectPathInput', 'projectPickerModal', submitProjectPicker);
|
|
691
|
-
await detectHub();
|
|
692
723
|
// Handle ?project= query param
|
|
693
724
|
const params = new URLSearchParams(location.search);
|
|
694
725
|
if (params.has('project')) {
|
|
@@ -705,12 +736,21 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|
|
705
736
|
const qs = params.toString();
|
|
706
737
|
history.replaceState(null, '', qs ? `?${qs}` : location.pathname + location.hash);
|
|
707
738
|
}
|
|
708
|
-
|
|
709
|
-
|
|
739
|
+
// Retry initial load — server may not be ready yet (e.g. Hub iframe race)
|
|
740
|
+
for (let attempt = 0; attempt < 5; attempt++) {
|
|
741
|
+
try {
|
|
742
|
+
await loadProject();
|
|
743
|
+
break;
|
|
744
|
+
} catch {
|
|
745
|
+
if (attempt < 4) await new Promise((r) => setTimeout(r, 500));
|
|
746
|
+
else showToast('Failed to connect to server', 'error');
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
if (projectData) addRecentProject(projectData.path);
|
|
710
750
|
await loadData();
|
|
711
751
|
// Restore file selection from hash
|
|
712
752
|
const hash = decodeURIComponent(location.hash.slice(1));
|
|
713
|
-
if (hash && stackData.find(s => s.id === hash)) {
|
|
753
|
+
if (hash && stackData.find((s) => s.id === hash)) {
|
|
714
754
|
selectedFileId = hash;
|
|
715
755
|
renderTree();
|
|
716
756
|
renderPreview();
|
package/public/index.html
CHANGED
|
@@ -40,6 +40,9 @@
|
|
|
40
40
|
<button class="topbar-btn" id="themeBtn" title="Toggle theme (t)" onclick="toggleTheme()">
|
|
41
41
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/><line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/></svg>
|
|
42
42
|
</button>
|
|
43
|
+
<a class="topbar-btn" href="https://github.com/NikiforovAll/claude-code-memory" target="_blank" rel="noopener" title="GitHub">
|
|
44
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor" stroke="none"><path d="M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385.6.105.825-.255.825-.57 0-.285-.015-1.23-.015-2.235-3.015.555-3.795-.735-4.035-1.41-.135-.345-.72-1.41-1.23-1.695-.42-.225-1.02-.78-.015-.795.945-.015 1.62.87 1.845 1.23 1.08 1.815 2.805 1.305 3.495.99.105-.78.42-1.305.765-1.605-2.67-.3-5.46-1.335-5.46-5.925 0-1.305.465-2.385 1.23-3.225-.12-.3-.54-1.53.12-3.18 0 0 1.005-.315 3.3 1.23.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23.66 1.65.24 2.88.12 3.18.765.84 1.23 1.905 1.23 3.225 0 4.605-2.805 5.625-5.475 5.925.435.375.81 1.095.81 2.22 0 1.605-.015 2.895-.015 3.3 0 .315.225.69.825.57A12.02 12.02 0 0024 12c0-6.63-5.37-12-12-12z"/></svg>
|
|
45
|
+
</a>
|
|
43
46
|
</div>
|
|
44
47
|
|
|
45
48
|
<!-- Size bar -->
|
package/public/style.css
CHANGED
|
@@ -130,25 +130,34 @@ body {
|
|
|
130
130
|
flex: 1;
|
|
131
131
|
}
|
|
132
132
|
.topbar-btn {
|
|
133
|
+
display: flex;
|
|
134
|
+
align-items: center;
|
|
135
|
+
gap: 5px;
|
|
133
136
|
padding: 6px 10px;
|
|
134
137
|
border-radius: 6px;
|
|
135
138
|
font-size: 11px;
|
|
136
139
|
font-weight: 500;
|
|
140
|
+
cursor: pointer;
|
|
137
141
|
border: 1px solid var(--border);
|
|
138
142
|
background: var(--bg-elevated);
|
|
139
143
|
color: var(--text-tertiary);
|
|
140
144
|
font-family: var(--font-mono);
|
|
145
|
+
transition: all 0.15s ease;
|
|
146
|
+
white-space: nowrap;
|
|
141
147
|
text-transform: uppercase;
|
|
142
148
|
letter-spacing: 0.03em;
|
|
143
|
-
|
|
144
|
-
display: flex;
|
|
145
|
-
align-items: center;
|
|
146
|
-
gap: 6px;
|
|
149
|
+
text-decoration: none;
|
|
147
150
|
}
|
|
148
151
|
.topbar-btn:hover {
|
|
149
152
|
border-color: var(--accent);
|
|
150
153
|
color: var(--text-secondary);
|
|
151
154
|
}
|
|
155
|
+
.topbar-btn svg {
|
|
156
|
+
color: var(--text-muted);
|
|
157
|
+
}
|
|
158
|
+
.topbar-btn.loading svg {
|
|
159
|
+
animation: spin 1s linear infinite;
|
|
160
|
+
}
|
|
152
161
|
/* #endregion TOPBAR */
|
|
153
162
|
|
|
154
163
|
/* #region BUDGET_BAR */
|
|
@@ -288,9 +297,15 @@ body {
|
|
|
288
297
|
font-family: var(--font-mono);
|
|
289
298
|
flex-shrink: 0;
|
|
290
299
|
}
|
|
291
|
-
.tree-child .file-name {
|
|
292
|
-
|
|
293
|
-
|
|
300
|
+
.tree-child .file-name {
|
|
301
|
+
opacity: 0.75;
|
|
302
|
+
}
|
|
303
|
+
.tree-conditional .file-name {
|
|
304
|
+
opacity: 0.55;
|
|
305
|
+
}
|
|
306
|
+
body.light .tree-conditional .file-name {
|
|
307
|
+
opacity: 0.75;
|
|
308
|
+
}
|
|
294
309
|
/* #endregion TREE */
|
|
295
310
|
|
|
296
311
|
/* #region PREVIEW */
|
|
@@ -752,13 +767,13 @@ kbd {
|
|
|
752
767
|
body.light {
|
|
753
768
|
--bg-deep: #e8e6e3;
|
|
754
769
|
--bg-surface: #f4f3f1;
|
|
755
|
-
--bg-elevated: #
|
|
756
|
-
--bg-hover: #
|
|
757
|
-
--border: #
|
|
770
|
+
--bg-elevated: #dddbd8;
|
|
771
|
+
--bg-hover: #d2d0cc;
|
|
772
|
+
--border: #a09b94;
|
|
758
773
|
--text-primary: #0a0a0a;
|
|
759
|
-
--text-secondary: #
|
|
774
|
+
--text-secondary: #444444;
|
|
760
775
|
--text-tertiary: #666666;
|
|
761
|
-
--text-muted: #
|
|
776
|
+
--text-muted: #888888;
|
|
762
777
|
--accent-text: #b85a20;
|
|
763
778
|
}
|
|
764
779
|
/* #endregion LIGHT_THEME */
|
package/server.js
CHANGED
|
@@ -250,6 +250,38 @@ function discoverMemorySources(projectPath) {
|
|
|
250
250
|
}
|
|
251
251
|
}
|
|
252
252
|
|
|
253
|
+
// 4b. Scan subdirectories of projectPath for CLAUDE.md (tree-scoped)
|
|
254
|
+
const SKIP_DIRS = new Set(['node_modules', '.git', '.hg', '.svn', 'dist', 'build', 'out', '.next', '.nuxt', 'vendor', '__pycache__', '.venv', 'venv']);
|
|
255
|
+
function walkForClaudeMd(dir, depth) {
|
|
256
|
+
if (depth > 5) return;
|
|
257
|
+
let entries;
|
|
258
|
+
try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return; }
|
|
259
|
+
for (const entry of entries) {
|
|
260
|
+
if (!entry.isDirectory() || SKIP_DIRS.has(entry.name) || entry.name.startsWith('.')) continue;
|
|
261
|
+
const subdir = path.join(dir, entry.name);
|
|
262
|
+
for (const name of ['CLAUDE.md', '.claude/CLAUDE.md']) {
|
|
263
|
+
const filePath = path.join(subdir, name);
|
|
264
|
+
if (seenPaths.has(filePath)) continue;
|
|
265
|
+
const info = fileInfo(filePath);
|
|
266
|
+
if (!info) continue;
|
|
267
|
+
seenPaths.add(filePath);
|
|
268
|
+
const rel = path.relative(projectPath, subdir).replace(/\\/g, '/');
|
|
269
|
+
sources.push({
|
|
270
|
+
id: `project-claude-md-${subdir.replace(/[^a-zA-Z0-9]/g, '-')}`,
|
|
271
|
+
name: `${rel}/${name.includes('/') ? '.claude/CLAUDE.md' : 'CLAUDE.md'}`,
|
|
272
|
+
scope: 'project',
|
|
273
|
+
load: 'tree',
|
|
274
|
+
...info,
|
|
275
|
+
dir: subdir,
|
|
276
|
+
isProjectRoot: false,
|
|
277
|
+
...spreadImports(info.path, info.content),
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
walkForClaudeMd(subdir, depth + 1);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
walkForClaudeMd(projectPath, 0);
|
|
284
|
+
|
|
253
285
|
// 5. Project rules (.claude/rules/*.md)
|
|
254
286
|
const projectRulesDir = path.join(projectPath, '.claude', 'rules');
|
|
255
287
|
if (fs.existsSync(projectRulesDir)) {
|
|
@@ -439,6 +471,8 @@ app.get('/hub-config', (_req, res) => {
|
|
|
439
471
|
name: 'Claude Code Memory',
|
|
440
472
|
icon: 'brain',
|
|
441
473
|
description: 'Explore Claude Code memory sources',
|
|
474
|
+
enabled: !!process.env.CLAUDE_HUB,
|
|
475
|
+
url: process.env.HUB_URL || null,
|
|
442
476
|
});
|
|
443
477
|
});
|
|
444
478
|
|