claude-code-memory-explorer 0.10.0 → 0.11.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 +474 -25
- package/public/index.html +20 -2
- package/public/style.css +607 -32
- package/server.js +524 -23
package/package.json
CHANGED
package/public/app.js
CHANGED
|
@@ -58,7 +58,7 @@ function highlightSource(text, fileName) {
|
|
|
58
58
|
if (fm) {
|
|
59
59
|
const fmHtml = hljs.highlight(fm[1], { language: 'yaml' }).value;
|
|
60
60
|
const bodyHtml = hljs.highlight(fm[2], { language: 'markdown' }).value;
|
|
61
|
-
return `<span class="hl-frontmatter">---</span>\n${fmHtml}\n<span class="hl-frontmatter">---</span>\n${bodyHtml}`;
|
|
61
|
+
return `<span class="hl-fm-block"><span class="hl-frontmatter">---</span>\n${fmHtml}\n<span class="hl-frontmatter">---</span></span>\n${bodyHtml}`;
|
|
62
62
|
}
|
|
63
63
|
}
|
|
64
64
|
return hljs.highlight(text, { language: lang }).value;
|
|
@@ -304,6 +304,12 @@ function toggleHelpModal() {
|
|
|
304
304
|
document.getElementById('helpModal').classList.toggle('open');
|
|
305
305
|
}
|
|
306
306
|
|
|
307
|
+
// biome-ignore lint/correctness/noUnusedVariables: called from topbar markup
|
|
308
|
+
function openAnalysisModal() {
|
|
309
|
+
document.getElementById('analysisModal').classList.add('open');
|
|
310
|
+
refreshAnalysis();
|
|
311
|
+
}
|
|
312
|
+
|
|
307
313
|
function bindModalKeys(inputId, modalId, submitFn) {
|
|
308
314
|
document.getElementById(inputId).addEventListener('keydown', (e) => {
|
|
309
315
|
if (e.key === 'Enter') {
|
|
@@ -321,12 +327,13 @@ function bindModalKeys(inputId, modalId, submitFn) {
|
|
|
321
327
|
|
|
322
328
|
// #region RENDER_TREE
|
|
323
329
|
|
|
324
|
-
const SCOPE_ORDER = ['policy', 'user', 'project', 'rule', 'memory', 'agent-memory'];
|
|
330
|
+
const SCOPE_ORDER = ['policy', 'user', 'project', 'rule', 'memory', 'skill', 'agent-memory'];
|
|
325
331
|
const SCOPE_LABELS = {
|
|
326
332
|
policy: 'Managed Policy',
|
|
327
333
|
user: 'User',
|
|
328
334
|
project: 'Project',
|
|
329
335
|
rule: 'Rules',
|
|
336
|
+
skill: 'Skills',
|
|
330
337
|
memory: 'Auto Memory',
|
|
331
338
|
'agent-memory': 'Agent Memory',
|
|
332
339
|
};
|
|
@@ -667,14 +674,30 @@ async function renderPreview() {
|
|
|
667
674
|
html += `<button class="action-btn small" onclick="_confirmDeleteFile('${escAttrJs(source.path)}','${escAttrJs(source.name)}')" title="Delete file"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/><path d="M10 11v6M14 11v6"/><path d="M9 6V4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2"/></svg></button>`;
|
|
668
675
|
html += '</div></div>';
|
|
669
676
|
|
|
677
|
+
// Description gets its own full-width line; the rest stay as key:value chips
|
|
678
|
+
const fmDesc =
|
|
679
|
+
fileData.frontmatter && typeof fileData.frontmatter.description === 'string'
|
|
680
|
+
? fileData.frontmatter.description.replace(/\s+/g, ' ')
|
|
681
|
+
: '';
|
|
682
|
+
if (fmDesc)
|
|
683
|
+
html += `<div class="preview-desc" title="Click to expand" onclick="this.classList.toggle('expanded')">${esc(fmDesc)}</div>`;
|
|
684
|
+
|
|
670
685
|
// Badges row
|
|
671
686
|
html += '<div class="preview-badges">';
|
|
672
687
|
html += `<span class="load-badge load-${source.load}">${esc(source.load)}</span>`;
|
|
673
688
|
html += `<span class="tag-badge">${source.lines}L / ${formatBytes(source.bytes)}</span>`;
|
|
674
689
|
if (fileData.frontmatter) {
|
|
675
690
|
for (const [k, v] of Object.entries(fileData.frontmatter)) {
|
|
676
|
-
|
|
677
|
-
|
|
691
|
+
if (k === 'description' && fmDesc) continue;
|
|
692
|
+
let val;
|
|
693
|
+
if (Array.isArray(v)) val = v.join(', ');
|
|
694
|
+
else if (v && typeof v === 'object')
|
|
695
|
+
val = Object.entries(v)
|
|
696
|
+
.map(([ck, cv]) => `${ck}=${cv}`)
|
|
697
|
+
.join(', ');
|
|
698
|
+
else val = String(v).replace(/\s+/g, ' ');
|
|
699
|
+
const shown = val.length > 72 ? `${val.slice(0, 69)}…` : val;
|
|
700
|
+
html += `<span class="tag-badge" title="${esc(`${k}: ${val}`)}"><span class="tag-k">${esc(k)}</span>${esc(shown)}</span>`;
|
|
678
701
|
}
|
|
679
702
|
}
|
|
680
703
|
html += '</div>';
|
|
@@ -696,14 +719,15 @@ async function renderPreview() {
|
|
|
696
719
|
html += '</div>';
|
|
697
720
|
}
|
|
698
721
|
|
|
722
|
+
// File path lives inside the header as a muted line instead of its own band
|
|
723
|
+
html += `<div class="preview-filepath" title="${esc(source.path)}">${esc(source.path)}</div>`;
|
|
699
724
|
html += '</div>';
|
|
700
725
|
|
|
701
|
-
// File path
|
|
702
|
-
html += `<div class="preview-filepath">${esc(source.path)}</div>`;
|
|
703
|
-
|
|
704
726
|
const hl = (text) => {
|
|
705
727
|
const { text: processed, placeholders } = linkifyImports(text, source.id);
|
|
706
|
-
|
|
728
|
+
// Skill names aren't filenames (e.g. "aspire") — derive the language from the real file
|
|
729
|
+
const fileName = source.path ? source.path.split(/[\\/]/).pop() : source.name;
|
|
730
|
+
return restorePlaceholders(highlightSource(processed, fileName), placeholders);
|
|
707
731
|
};
|
|
708
732
|
|
|
709
733
|
if (showMemoryTable) {
|
|
@@ -848,6 +872,433 @@ async function openInEditor(filePath) {
|
|
|
848
872
|
|
|
849
873
|
// #endregion RENDER_PREVIEW
|
|
850
874
|
|
|
875
|
+
// #region ANALYZER
|
|
876
|
+
|
|
877
|
+
let analysisPollTimer = null;
|
|
878
|
+
let lastAnalysisSt = null;
|
|
879
|
+
let analysisFindings = []; // findings of the rendered result, addressed by index from handlers
|
|
880
|
+
let analysisFilter = 'all';
|
|
881
|
+
let analysisDismissed = new Set(); // session-only; keys are `${kind}|${title}`
|
|
882
|
+
let analysisTs = null; // reset dismissals + filter when a new result lands
|
|
883
|
+
let analysisRunIdx = 0; // which run from the per-project history is shown (0 = latest)
|
|
884
|
+
|
|
885
|
+
// Session-only identity of a finding, used by dismiss/copy/render alike
|
|
886
|
+
function findingKey(f) {
|
|
887
|
+
return `${f.kind}|${f.title}`;
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
function resetAnalysisView(runIdx = 0) {
|
|
891
|
+
analysisRunIdx = runIdx;
|
|
892
|
+
analysisFilter = 'all';
|
|
893
|
+
analysisDismissed = new Set();
|
|
894
|
+
}
|
|
895
|
+
let scopePanelOpen = false;
|
|
896
|
+
let scopeChecked = null; // Set of source ids; seeded with the default scope on first open
|
|
897
|
+
const scopeCollapsed = new Set();
|
|
898
|
+
const ANALYSIS_MODELS = ['sonnet', 'opus', 'fable'];
|
|
899
|
+
let analysisModel = ANALYSIS_MODELS.includes(localStorage.getItem('analysisModel'))
|
|
900
|
+
? localStorage.getItem('analysisModel')
|
|
901
|
+
: 'sonnet';
|
|
902
|
+
|
|
903
|
+
function _setAnalysisModel(m) {
|
|
904
|
+
if (!ANALYSIS_MODELS.includes(m)) return;
|
|
905
|
+
analysisModel = m;
|
|
906
|
+
try {
|
|
907
|
+
localStorage.setItem('analysisModel', m);
|
|
908
|
+
} catch {}
|
|
909
|
+
rerenderAnalysis();
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
async function _runAnalysis(force, ids) {
|
|
913
|
+
try {
|
|
914
|
+
const body = { force: !!force, model: analysisModel };
|
|
915
|
+
if (Array.isArray(ids) && ids.length) body.ids = ids;
|
|
916
|
+
const res = await fetch('/api/memory/analyze', {
|
|
917
|
+
method: 'POST',
|
|
918
|
+
headers: { 'Content-Type': 'application/json' },
|
|
919
|
+
body: JSON.stringify(body),
|
|
920
|
+
});
|
|
921
|
+
if (!res.ok && res.status !== 409) {
|
|
922
|
+
const msg = await res
|
|
923
|
+
.json()
|
|
924
|
+
.then((e) => e.error)
|
|
925
|
+
.catch(() => `Analyze failed (${res.status})`);
|
|
926
|
+
showToast(msg, 'error');
|
|
927
|
+
return;
|
|
928
|
+
}
|
|
929
|
+
const data = await res.json().catch(() => ({}));
|
|
930
|
+
if (data.cached) showToast('Memory unchanged — showing cached analysis. Use Re-run to force.', 'info');
|
|
931
|
+
} catch (err) {
|
|
932
|
+
showToast(err.message, 'error');
|
|
933
|
+
return;
|
|
934
|
+
}
|
|
935
|
+
refreshAnalysis();
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
async function refreshAnalysis() {
|
|
939
|
+
const el = document.getElementById('analysisSection');
|
|
940
|
+
if (!el) return;
|
|
941
|
+
let st;
|
|
942
|
+
try {
|
|
943
|
+
st = await fetchJSON('/api/memory/analysis');
|
|
944
|
+
} catch {
|
|
945
|
+
return;
|
|
946
|
+
}
|
|
947
|
+
if (st.ts !== analysisTs) {
|
|
948
|
+
analysisTs = st.ts;
|
|
949
|
+
resetAnalysisView();
|
|
950
|
+
}
|
|
951
|
+
lastAnalysisSt = st;
|
|
952
|
+
el.innerHTML = renderAnalysis(st);
|
|
953
|
+
applyScopeIndeterminate(el);
|
|
954
|
+
clearTimeout(analysisPollTimer);
|
|
955
|
+
if (st.running) analysisPollTimer = setTimeout(refreshAnalysis, 2500);
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
// Re-render from the cached state — used by scope/filter/dismiss handlers so a
|
|
959
|
+
// checkbox click never triggers a server round-trip.
|
|
960
|
+
function rerenderAnalysis() {
|
|
961
|
+
const el = document.getElementById('analysisSection');
|
|
962
|
+
if (!el || !lastAnalysisSt) return;
|
|
963
|
+
el.innerHTML = renderAnalysis(lastAnalysisSt);
|
|
964
|
+
applyScopeIndeterminate(el);
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
// `indeterminate` is a DOM property, not an attribute — set it after every innerHTML render.
|
|
968
|
+
function applyScopeIndeterminate(el) {
|
|
969
|
+
for (const box of el.querySelectorAll('input[data-ind]')) box.indeterminate = true;
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
// #region ANALYZER_SCOPE
|
|
973
|
+
|
|
974
|
+
const CLAUDE_MD_SCOPES = new Set(['project', 'local']);
|
|
975
|
+
|
|
976
|
+
function analysisScopeGroups() {
|
|
977
|
+
return [
|
|
978
|
+
{
|
|
979
|
+
key: 'memory',
|
|
980
|
+
label: 'Auto memory',
|
|
981
|
+
cls: 't-memory',
|
|
982
|
+
tag: 'memory',
|
|
983
|
+
items: stackData.filter((s) => s.scope === 'memory'),
|
|
984
|
+
},
|
|
985
|
+
{
|
|
986
|
+
key: 'skill',
|
|
987
|
+
label: 'Skills',
|
|
988
|
+
cls: 't-skill',
|
|
989
|
+
tag: 'skill',
|
|
990
|
+
items: stackData.filter((s) => s.scope === 'skill' && !s.parentId),
|
|
991
|
+
},
|
|
992
|
+
{
|
|
993
|
+
key: 'claudemd',
|
|
994
|
+
label: 'CLAUDE.md',
|
|
995
|
+
cls: 't-claudemd',
|
|
996
|
+
tag: 'md',
|
|
997
|
+
items: stackData.filter((s) => CLAUDE_MD_SCOPES.has(s.scope) && !s.parentId),
|
|
998
|
+
},
|
|
999
|
+
{
|
|
1000
|
+
key: 'agent',
|
|
1001
|
+
label: 'Agent memory',
|
|
1002
|
+
cls: 't-agent',
|
|
1003
|
+
tag: 'agent',
|
|
1004
|
+
items: stackData.filter((s) => s.scope === 'agent-memory'),
|
|
1005
|
+
},
|
|
1006
|
+
].filter((g) => g.items.length);
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
function _toggleScopePanel() {
|
|
1010
|
+
scopePanelOpen = !scopePanelOpen;
|
|
1011
|
+
if (scopePanelOpen && !scopeChecked) {
|
|
1012
|
+
// default scope = auto memory only (skills, agent memory, CLAUDE.md are opt-in)
|
|
1013
|
+
scopeChecked = new Set(stackData.filter((s) => s.scope === 'memory').map((s) => s.id));
|
|
1014
|
+
}
|
|
1015
|
+
rerenderAnalysis();
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
function _toggleScopeGroup(key) {
|
|
1019
|
+
const g = analysisScopeGroups().find((x) => x.key === key);
|
|
1020
|
+
if (!g) return;
|
|
1021
|
+
const allOn = g.items.every((s) => scopeChecked.has(s.id));
|
|
1022
|
+
for (const s of g.items) {
|
|
1023
|
+
if (allOn) scopeChecked.delete(s.id);
|
|
1024
|
+
else scopeChecked.add(s.id);
|
|
1025
|
+
}
|
|
1026
|
+
rerenderAnalysis();
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
function _toggleScopeLeaf(id) {
|
|
1030
|
+
if (scopeChecked.has(id)) scopeChecked.delete(id);
|
|
1031
|
+
else scopeChecked.add(id);
|
|
1032
|
+
rerenderAnalysis();
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
function _toggleScopeCollapse(key, e) {
|
|
1036
|
+
e.stopPropagation();
|
|
1037
|
+
if (scopeCollapsed.has(key)) scopeCollapsed.delete(key);
|
|
1038
|
+
else scopeCollapsed.add(key);
|
|
1039
|
+
rerenderAnalysis();
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
function _runScopedAnalysis() {
|
|
1043
|
+
if (!scopeChecked?.size) return;
|
|
1044
|
+
scopePanelOpen = false;
|
|
1045
|
+
_runAnalysis(true, [...scopeChecked]);
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
function renderScopePanel() {
|
|
1049
|
+
if (!scopePanelOpen || !scopeChecked) return '';
|
|
1050
|
+
let html = '<div class="scope-panel open">';
|
|
1051
|
+
html += '<div class="scope-panel-title">Select what to analyze</div>';
|
|
1052
|
+
html += '<div class="scope-tree">';
|
|
1053
|
+
for (const g of analysisScopeGroups()) {
|
|
1054
|
+
const on = g.items.filter((s) => scopeChecked.has(s.id)).length;
|
|
1055
|
+
const collapsed = scopeCollapsed.has(g.key);
|
|
1056
|
+
html += `<div class="scope-group${collapsed ? ' collapsed' : ''}">`;
|
|
1057
|
+
html += `<div class="scope-row" onclick="_toggleScopeGroup('${escAttrJs(g.key)}')">`;
|
|
1058
|
+
html += `<span class="scope-caret" onclick="_toggleScopeCollapse('${escAttrJs(g.key)}', event)">${collapsed ? '▸' : '▾'}</span>`;
|
|
1059
|
+
html += `<input type="checkbox" ${on === g.items.length ? 'checked' : ''} ${on > 0 && on < g.items.length ? 'data-ind="1"' : ''} onclick="event.preventDefault()">`;
|
|
1060
|
+
html += `<span class="scope-tag ${esc(g.cls)}">${g.tag}</span>`;
|
|
1061
|
+
html += `<span>${esc(g.label)}</span>`;
|
|
1062
|
+
html += `<span class="scope-meta">${on}/${g.items.length}</span>`;
|
|
1063
|
+
html += '</div>';
|
|
1064
|
+
html += '<div class="scope-children">';
|
|
1065
|
+
for (const s of g.items) {
|
|
1066
|
+
html += `<div class="scope-row" onclick="_toggleScopeLeaf('${escAttrJs(s.id)}')">`;
|
|
1067
|
+
html += `<input type="checkbox" ${scopeChecked.has(s.id) ? 'checked' : ''} onclick="event.preventDefault()">`;
|
|
1068
|
+
html += `<span>${esc(s.name)}</span>`;
|
|
1069
|
+
html += `<span class="scope-meta">${s.lines}L</span>`;
|
|
1070
|
+
html += '</div>';
|
|
1071
|
+
}
|
|
1072
|
+
html += '</div></div>';
|
|
1073
|
+
}
|
|
1074
|
+
html += '</div>';
|
|
1075
|
+
const n = scopeChecked.size;
|
|
1076
|
+
html += '<div class="scope-footer">';
|
|
1077
|
+
html += `<span class="scope-hint">${n} file${n === 1 ? '' : 's'} selected</span>`;
|
|
1078
|
+
html += '<div class="scope-model" title="Model for this analysis">';
|
|
1079
|
+
for (const m of ANALYSIS_MODELS)
|
|
1080
|
+
html += `<button class="scope-model-opt${m === analysisModel ? ' active' : ''}" onclick="_setAnalysisModel('${escAttrJs(m)}')">${m}</button>`;
|
|
1081
|
+
html += '</div>';
|
|
1082
|
+
html += '<button class="action-btn small" onclick="_toggleScopePanel()">Cancel</button>';
|
|
1083
|
+
html += `<button class="action-btn small primary" ${n ? '' : 'disabled'} onclick="_runScopedAnalysis()">Run analysis (${n})</button>`;
|
|
1084
|
+
html += '</div></div>';
|
|
1085
|
+
return html;
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
// #endregion ANALYZER_SCOPE
|
|
1089
|
+
|
|
1090
|
+
// #region ANALYZER_ACTIONS
|
|
1091
|
+
|
|
1092
|
+
function memoryDirPath() {
|
|
1093
|
+
const idx = stackData.find((s) => s.scope === 'memory' && s.name === 'MEMORY.md');
|
|
1094
|
+
return idx ? idx.path.replace(/[\\/][^\\/]+$/, '') : null;
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
function findingPrompt(f) {
|
|
1098
|
+
const dir = memoryDirPath();
|
|
1099
|
+
const lines = [
|
|
1100
|
+
dir
|
|
1101
|
+
? `In the Claude Code auto-memory at ${dir}, apply this fix:`
|
|
1102
|
+
: "In this project's Claude Code auto-memory, apply this fix:",
|
|
1103
|
+
'',
|
|
1104
|
+
`Finding (${f.kind}, ${f.severity}): ${f.title}`,
|
|
1105
|
+
f.detail,
|
|
1106
|
+
'',
|
|
1107
|
+
`Action: ${f.suggestion}`,
|
|
1108
|
+
];
|
|
1109
|
+
if ((f.files || []).length) lines.push(`Files: ${f.files.join(', ')}`);
|
|
1110
|
+
lines.push('', 'Update MEMORY.md index lines if files are added, renamed, or removed.');
|
|
1111
|
+
return lines.join('\n');
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
function copyText(text, okMsg) {
|
|
1115
|
+
navigator.clipboard
|
|
1116
|
+
.writeText(text)
|
|
1117
|
+
.then(() => showToast(okMsg, 'info'))
|
|
1118
|
+
.catch(() => showToast('Clipboard unavailable', 'error'));
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
function _copyFindingPrompt(idx) {
|
|
1122
|
+
const f = analysisFindings[idx];
|
|
1123
|
+
if (f) copyText(findingPrompt(f), 'Prompt copied — paste into any Claude Code session');
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
function _copyAllFindings() {
|
|
1127
|
+
const live = analysisFindings.filter((f) => !analysisDismissed.has(findingKey(f)));
|
|
1128
|
+
if (!live.length) return;
|
|
1129
|
+
copyText(live.map(findingPrompt).join('\n\n---\n\n'), `${live.length} prompts copied`);
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
function _dismissFinding(btn, idx) {
|
|
1133
|
+
const f = analysisFindings[idx];
|
|
1134
|
+
if (!f) return;
|
|
1135
|
+
const card = btn.closest('.analysis-card');
|
|
1136
|
+
card.style.maxHeight = `${card.offsetHeight}px`;
|
|
1137
|
+
requestAnimationFrame(() => card.classList.add('leaving'));
|
|
1138
|
+
setTimeout(() => {
|
|
1139
|
+
analysisDismissed.add(findingKey(f));
|
|
1140
|
+
rerenderAnalysis();
|
|
1141
|
+
}, 280);
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
function _restoreDismissed() {
|
|
1145
|
+
analysisDismissed = new Set();
|
|
1146
|
+
rerenderAnalysis();
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
function _setAnalysisFilter(sev) {
|
|
1150
|
+
analysisFilter = sev;
|
|
1151
|
+
rerenderAnalysis();
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
// #endregion ANALYZER_ACTIONS
|
|
1155
|
+
|
|
1156
|
+
function analysisTimeAgo(ts) {
|
|
1157
|
+
const min = Math.round((Date.now() - ts) / 60000);
|
|
1158
|
+
if (min < 1) return 'just now';
|
|
1159
|
+
if (min < 60) return `${min}m ago`;
|
|
1160
|
+
const h = Math.round(min / 60);
|
|
1161
|
+
return h < 24 ? `${h}h ago` : `${Math.round(h / 24)}d ago`;
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
const COPY_ICON =
|
|
1165
|
+
'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>';
|
|
1166
|
+
const DISMISS_ICON =
|
|
1167
|
+
'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 6L6 18M6 6l12 12"/></svg>';
|
|
1168
|
+
const FILE_ICON =
|
|
1169
|
+
'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><path d="M14 2v6h6"/></svg>';
|
|
1170
|
+
|
|
1171
|
+
function fileChipScope(source) {
|
|
1172
|
+
if (!source) return '';
|
|
1173
|
+
return source.scope === 'agent-memory' ? 'agent' : source.scope;
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
function renderAnalysisHead(st, r, ts) {
|
|
1177
|
+
const meta = [];
|
|
1178
|
+
if (ts) meta.push(analysisTimeAgo(ts));
|
|
1179
|
+
if (r?.costUsd != null) meta.push(`$${r.costUsd.toFixed(2)}`);
|
|
1180
|
+
if (r?.durationMs != null) meta.push(`${Math.round(r.durationMs / 1000)}s`);
|
|
1181
|
+
if (r?.model) meta.push(r.model);
|
|
1182
|
+
if (r?.scopeDesc) meta.push(`scope: ${r.scopeDesc}`);
|
|
1183
|
+
if (st.stale && analysisRunIdx === 0) meta.push('memory changed since');
|
|
1184
|
+
let html = '<div class="analysis-head">';
|
|
1185
|
+
html += '<span class="analysis-title">Claude analysis</span>';
|
|
1186
|
+
html += `<span class="analysis-meta">${esc(meta.join(' · '))}</span>`;
|
|
1187
|
+
if (r && (r.findings || []).length)
|
|
1188
|
+
html += '<button class="action-btn small" onclick="_copyAllFindings()">Copy all</button>';
|
|
1189
|
+
html += `<button class="action-btn small primary" onclick="_toggleScopePanel()">New analysis…</button>`;
|
|
1190
|
+
html += '</div>';
|
|
1191
|
+
html += renderRunsStrip(st);
|
|
1192
|
+
html += renderScopePanel();
|
|
1193
|
+
return html;
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
function _setAnalysisRun(i) {
|
|
1197
|
+
resetAnalysisView(i);
|
|
1198
|
+
rerenderAnalysis();
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
async function _deleteAnalysisRun(ev, ts) {
|
|
1202
|
+
ev.stopPropagation();
|
|
1203
|
+
await fetch('/api/memory/analysis/delete-run', {
|
|
1204
|
+
method: 'POST',
|
|
1205
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1206
|
+
body: JSON.stringify({ ts }),
|
|
1207
|
+
});
|
|
1208
|
+
resetAnalysisView();
|
|
1209
|
+
analysisTs = null; // force refreshAnalysis to re-adopt the new latest run
|
|
1210
|
+
refreshAnalysis();
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
// Past runs for this project (newest first).
|
|
1214
|
+
function renderRunsStrip(st) {
|
|
1215
|
+
const runs = st.runs || [];
|
|
1216
|
+
if (!runs.length) return '';
|
|
1217
|
+
let html = '<div class="runs-strip"><span class="runs-label">runs</span>';
|
|
1218
|
+
runs.forEach((run, i) => {
|
|
1219
|
+
const r = run.result || {};
|
|
1220
|
+
const bits = [analysisTimeAgo(run.ts)];
|
|
1221
|
+
if (r.model) bits.push(r.model);
|
|
1222
|
+
if (r.costUsd != null) bits.push(`$${r.costUsd.toFixed(2)}`);
|
|
1223
|
+
html += `<span class="run-chip${i === analysisRunIdx ? ' active' : ''}" onclick="_setAnalysisRun(${i})" title="${esc(r.scopeDesc || '')}">${esc(bits.join(' · '))}<button class="run-chip-x" onclick="_deleteAnalysisRun(event, ${escAttrJs(run.ts)})" title="Delete this run">×</button></span>`;
|
|
1224
|
+
});
|
|
1225
|
+
html += '</div>';
|
|
1226
|
+
return html;
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
function renderAnalysis(st) {
|
|
1230
|
+
if (st.running) {
|
|
1231
|
+
return '<div class="analysis-box"><div class="analysis-status"><span class="analysis-spinner"></span>Analyzing memory with Claude Code — usually 30–90s…</div></div>';
|
|
1232
|
+
}
|
|
1233
|
+
const runs = st.runs || [];
|
|
1234
|
+
const shown = runs[analysisRunIdx];
|
|
1235
|
+
let html = '<div class="analysis-box">';
|
|
1236
|
+
html += renderAnalysisHead(st, shown?.result, shown?.ts);
|
|
1237
|
+
if (st.error) {
|
|
1238
|
+
html += `<div class="analysis-status analysis-status-error">${esc(st.error)} <button class="action-btn small" onclick="_runAnalysis(true)">Retry</button></div></div>`;
|
|
1239
|
+
return html;
|
|
1240
|
+
}
|
|
1241
|
+
if (!shown) {
|
|
1242
|
+
html += '<div class="analysis-empty">No analysis yet — pick a scope and run one.</div></div>';
|
|
1243
|
+
return html;
|
|
1244
|
+
}
|
|
1245
|
+
const r = shown.result;
|
|
1246
|
+
analysisFindings = r.findings || [];
|
|
1247
|
+
const byName = Object.fromEntries(stackData.map((s) => [s.name, s]));
|
|
1248
|
+
if (r.summary) html += `<div class="analysis-summary">${esc(r.summary)}</div>`;
|
|
1249
|
+
|
|
1250
|
+
const live = analysisFindings.filter((f) => !analysisDismissed.has(findingKey(f)));
|
|
1251
|
+
const count = (sev) => live.filter((f) => f.severity === sev).length;
|
|
1252
|
+
if (analysisFindings.length) {
|
|
1253
|
+
html += '<div class="sev-strip">';
|
|
1254
|
+
for (const sev of ['all', 'high', 'med', 'low']) {
|
|
1255
|
+
const n = sev === 'all' ? live.length : count(sev);
|
|
1256
|
+
html += `<button class="sev-pill${analysisFilter === sev ? ' active' : ''}" data-sev="${esc(sev)}" onclick="_setAnalysisFilter('${escAttrJs(sev)}')">`;
|
|
1257
|
+
if (sev !== 'all') html += '<span class="dot"></span>';
|
|
1258
|
+
html += `${sev} ${n}</button>`;
|
|
1259
|
+
}
|
|
1260
|
+
if (analysisDismissed.size) {
|
|
1261
|
+
html += `<span class="dismissed-note">${analysisDismissed.size} dismissed · <a onclick="_restoreDismissed()">restore</a></span>`;
|
|
1262
|
+
}
|
|
1263
|
+
html += '</div>';
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
const visible = live.filter((f) => analysisFilter === 'all' || f.severity === analysisFilter);
|
|
1267
|
+
if (!analysisFindings.length) html += '<div class="analysis-empty">No issues found.</div>';
|
|
1268
|
+
else if (!visible.length) html += '<div class="analysis-empty">Nothing here — filtered out or dismissed.</div>';
|
|
1269
|
+
for (const f of visible) {
|
|
1270
|
+
const idx = analysisFindings.indexOf(f);
|
|
1271
|
+
html += `<div class="analysis-card ${esc(`sev-${f.severity}`)}">`;
|
|
1272
|
+
html += '<div class="analysis-card-head">';
|
|
1273
|
+
html += `<span class="kind-badge ${esc(`kind-${f.kind}`)}">${esc(f.kind)}</span>`;
|
|
1274
|
+
html += `<span class="analysis-card-title">${esc(f.title)}</span>`;
|
|
1275
|
+
html += '<span class="card-actions">';
|
|
1276
|
+
html += `<button class="card-btn" onclick="_copyFindingPrompt(${idx})" title="Copy an agent-ready instruction for this finding">${COPY_ICON}Copy prompt</button>`;
|
|
1277
|
+
html += `<button class="card-btn" onclick="_dismissFinding(this, ${idx})" title="Drop this finding from the list">${DISMISS_ICON}Dismiss</button>`;
|
|
1278
|
+
html += '</span></div>';
|
|
1279
|
+
const files = f.files || [];
|
|
1280
|
+
html += '<div class="analysis-card-files">';
|
|
1281
|
+
if (!files.length) html += '<span class="no-files">no file — new memory suggested</span>';
|
|
1282
|
+
for (const fn of files) {
|
|
1283
|
+
const child = byName[fn];
|
|
1284
|
+
const scopeTag = fileChipScope(child);
|
|
1285
|
+
if (child) {
|
|
1286
|
+
html += `<a class="file-chip" href="#" onclick="closeModal('analysisModal');selectFile('${escAttrJs(child.id)}');return false" title="${esc(child.path)}">${FILE_ICON}${esc(fn)}<span class="fscope fscope-${esc(scopeTag)}">${esc(scopeTag)}</span></a>`;
|
|
1287
|
+
} else {
|
|
1288
|
+
html += `<span class="file-chip">${FILE_ICON}${esc(fn)}</span>`;
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
html += '</div>';
|
|
1292
|
+
html += `<div class="analysis-card-detail">${esc(f.detail)}</div>`;
|
|
1293
|
+
html += `<div class="analysis-card-suggestion">${esc(f.suggestion)}</div>`;
|
|
1294
|
+
html += '</div>';
|
|
1295
|
+
}
|
|
1296
|
+
html += '</div>';
|
|
1297
|
+
return html;
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
// #endregion ANALYZER
|
|
1301
|
+
|
|
851
1302
|
// #region RENDER_BUDGET
|
|
852
1303
|
|
|
853
1304
|
function renderBudget() {
|
|
@@ -855,32 +1306,30 @@ function renderBudget() {
|
|
|
855
1306
|
|
|
856
1307
|
// Summary stat cards
|
|
857
1308
|
document.getElementById('statFiles').textContent = summaryData.totalFiles;
|
|
858
|
-
document.getElementById('
|
|
1309
|
+
document.getElementById('statChars').textContent = summaryData.totalChars.toLocaleString();
|
|
859
1310
|
document.getElementById('statBytes').textContent = formatBytes(summaryData.totalBytes);
|
|
860
1311
|
document.getElementById('statAlways').textContent = summaryData.alwaysLoaded;
|
|
861
1312
|
|
|
862
1313
|
// Budget text
|
|
863
1314
|
document.getElementById('budgetText').textContent =
|
|
864
|
-
`${summaryData.
|
|
1315
|
+
`${summaryData.totalChars.toLocaleString()} chars / ${formatBytes(summaryData.totalBytes)}`;
|
|
865
1316
|
|
|
866
|
-
// Budget segments — proportional by
|
|
1317
|
+
// Budget segments — proportional by chars per scope; the server's summary owns
|
|
1318
|
+
// the footprint filter, the client only renders its per-scope totals.
|
|
867
1319
|
const segContainer = document.getElementById('budgetSegments');
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
}
|
|
872
|
-
|
|
873
|
-
const scopeTotals = {};
|
|
874
|
-
for (const s of stackData) {
|
|
875
|
-
scopeTotals[s.scope] = (scopeTotals[s.scope] || 0) + (s.lines || 0);
|
|
876
|
-
}
|
|
877
|
-
const totalLines = summaryData.totalLines || 1;
|
|
1320
|
+
const scopeTotals = summaryData.scopeChars || {};
|
|
1321
|
+
const skillDescChars = summaryData.skillDesc?.chars || 0;
|
|
1322
|
+
const totalChars = summaryData.totalChars || 1;
|
|
878
1323
|
let html = '';
|
|
879
1324
|
for (const scope of SCOPE_ORDER) {
|
|
880
|
-
const
|
|
881
|
-
if (!
|
|
882
|
-
const pct = (
|
|
883
|
-
html += `<div class="budget-segment" style="width:${pct}%;background:var(--scope-${scope})" title="${SCOPE_LABELS[scope] || scope}: ${
|
|
1325
|
+
const chars = scopeTotals[scope];
|
|
1326
|
+
if (!chars) continue;
|
|
1327
|
+
const pct = (chars / totalChars) * 100;
|
|
1328
|
+
html += `<div class="budget-segment" style="width:${pct}%;background:var(--scope-${scope})" title="${SCOPE_LABELS[scope] || scope}: ${chars.toLocaleString()} chars (${pct.toFixed(1)}%)"></div>`;
|
|
1329
|
+
}
|
|
1330
|
+
if (skillDescChars) {
|
|
1331
|
+
const pct = (skillDescChars / totalChars) * 100;
|
|
1332
|
+
html += `<div class="budget-segment" style="width:${pct}%;background:var(--scope-skill)" title="Skill descriptions (${esc(summaryData.skillDesc.count)} enabled): ${skillDescChars.toLocaleString()} chars (${pct.toFixed(1)}%)"></div>`;
|
|
884
1333
|
}
|
|
885
1334
|
segContainer.innerHTML = html;
|
|
886
1335
|
}
|
package/public/index.html
CHANGED
|
@@ -32,6 +32,10 @@
|
|
|
32
32
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 19a2 2 0 01-2 2H4a2 2 0 01-2-2V5a2 2 0 012-2h5l2 3h9a2 2 0 012 2z"/></svg>
|
|
33
33
|
<span id="projectName">Loading...</span>
|
|
34
34
|
</div>
|
|
35
|
+
<button class="topbar-btn" id="analyzeBtn" title="Analyze memory with Claude" onclick="openAnalysisModal()">
|
|
36
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3l1.9 5.7L19.6 10l-5.7 1.9L12 17.6l-1.9-5.7L4.4 10l5.7-1.9z"/><path d="M19 15l.8 2.2L22 18l-2.2.8L19 21l-.8-2.2L16 18l2.2-.8z"/></svg>
|
|
37
|
+
<span>Analyze</span>
|
|
38
|
+
</button>
|
|
35
39
|
<button class="topbar-btn" id="refreshBtn" title="Refresh (r)" onclick="refreshData()">
|
|
36
40
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="23 4 23 10 17 10"/><path d="M20.49 15a9 9 0 11-2.12-9.36L23 10"/></svg>
|
|
37
41
|
</button>
|
|
@@ -44,6 +48,7 @@
|
|
|
44
48
|
</button>
|
|
45
49
|
<button class="topbar-btn" id="helpBtn" title="Keyboard shortcuts (?)" onclick="toggleHelpModal()">
|
|
46
50
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><circle cx="12" cy="17" r="0.5" fill="currentColor"/></svg>
|
|
51
|
+
<kbd>?</kbd>
|
|
47
52
|
</button>
|
|
48
53
|
<a class="topbar-btn" href="https://github.com/NikiforovAll/claude-code-memory" target="_blank" rel="noopener" title="GitHub">
|
|
49
54
|
<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>
|
|
@@ -54,7 +59,7 @@
|
|
|
54
59
|
<div class="budget-bar" id="budgetBar">
|
|
55
60
|
<div class="budget-label">
|
|
56
61
|
<span>Memory sources</span>
|
|
57
|
-
<span class="mono" id="budgetText">0
|
|
62
|
+
<span class="mono" id="budgetText">0 chars</span>
|
|
58
63
|
</div>
|
|
59
64
|
<div class="budget-track">
|
|
60
65
|
<div class="budget-segments" id="budgetSegments"></div>
|
|
@@ -82,7 +87,7 @@
|
|
|
82
87
|
<!-- Summary stats -->
|
|
83
88
|
<div class="summary-bar" id="summaryBar">
|
|
84
89
|
<div class="summary-card"><span class="sc-value" id="statFiles">-</span><span class="sc-label">Files</span></div>
|
|
85
|
-
<div class="summary-card"><span class="sc-value" id="
|
|
90
|
+
<div class="summary-card"><span class="sc-value" id="statChars">-</span><span class="sc-label">Chars</span></div>
|
|
86
91
|
<div class="summary-card"><span class="sc-value" id="statBytes">-</span><span class="sc-label">Bytes</span></div>
|
|
87
92
|
<div class="summary-card"><span class="sc-value" id="statAlways">-</span><span class="sc-label">Always loaded</span></div>
|
|
88
93
|
</div>
|
|
@@ -145,6 +150,19 @@
|
|
|
145
150
|
</div>
|
|
146
151
|
</div>
|
|
147
152
|
|
|
153
|
+
<!-- Analysis modal -->
|
|
154
|
+
<div class="modal-overlay" id="analysisModal" onclick="if(event.target===this)closeModal('analysisModal')">
|
|
155
|
+
<div class="modal modal-wide">
|
|
156
|
+
<div class="modal-header">
|
|
157
|
+
<h3>Claude analysis</h3>
|
|
158
|
+
<button class="modal-close" onclick="closeModal('analysisModal')">✕</button>
|
|
159
|
+
</div>
|
|
160
|
+
<div class="modal-body">
|
|
161
|
+
<div id="analysisSection"></div>
|
|
162
|
+
</div>
|
|
163
|
+
</div>
|
|
164
|
+
</div>
|
|
165
|
+
|
|
148
166
|
<!-- Help modal -->
|
|
149
167
|
<div class="modal-overlay" id="helpModal" onclick="if(event.target===this)toggleHelpModal()">
|
|
150
168
|
<div class="modal">
|