claude-code-memory-explorer 0.11.1 → 0.12.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 (3) hide show
  1. package/package.json +1 -1
  2. package/public/app.js +32 -11
  3. package/server.js +57 -10
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-code-memory-explorer",
3
- "version": "0.11.1",
3
+ "version": "0.12.0",
4
4
  "description": "Memory explorer dashboard for Claude Code",
5
5
  "main": "server.js",
6
6
  "bin": {
package/public/app.js CHANGED
@@ -1055,11 +1055,7 @@ async function refreshAnalysis() {
1055
1055
  computeTreeHealth(st);
1056
1056
  renderTree();
1057
1057
  if (isNew && !healthViewOpen && selectedFileId) renderPreview();
1058
- const el = document.getElementById('analysisSection');
1059
- if (el) {
1060
- el.innerHTML = renderAnalysis(st);
1061
- applyScopeIndeterminate(el);
1062
- }
1058
+ rerenderAnalysis(st);
1063
1059
  }
1064
1060
  lastPollKey = pollKey;
1065
1061
  clearTimeout(analysisPollTimer);
@@ -1072,11 +1068,16 @@ async function refreshAnalysis() {
1072
1068
 
1073
1069
  // Re-render from the cached state — used by scope/filter/dismiss handlers so a
1074
1070
  // checkbox click never triggers a server round-trip.
1075
- function rerenderAnalysis() {
1071
+ function rerenderAnalysis(st = lastAnalysisSt) {
1076
1072
  const el = document.getElementById('analysisSection');
1077
- if (!el || !lastAnalysisSt) return;
1078
- el.innerHTML = renderAnalysis(lastAnalysisSt);
1073
+ if (!el || !st) return;
1074
+ // The scope tree is the scroll container; innerHTML replaces it, which would
1075
+ // snap a group toggle deep in the list back to the top of the panel.
1076
+ const scroll = el.querySelector('.scope-tree')?.scrollTop || 0;
1077
+ el.innerHTML = renderAnalysis(st);
1079
1078
  applyScopeIndeterminate(el);
1079
+ const tree = el.querySelector('.scope-tree');
1080
+ if (tree) tree.scrollTop = scroll;
1080
1081
  }
1081
1082
 
1082
1083
  // `indeterminate` is a DOM property, not an attribute — set it after every innerHTML render.
@@ -1216,9 +1217,25 @@ function memoryDirPath() {
1216
1217
  return idx ? idx.path.replace(/[\\/][^\\/]+$/, '') : null;
1217
1218
  }
1218
1219
 
1220
+ // Resolve a finding's bare file names to the live stack sources they name, so a
1221
+ // prompt can cite real paths instead of guessing a location from `scope`.
1222
+ function findingSources(f) {
1223
+ const ids = new Set();
1224
+ for (const name of f.files || []) {
1225
+ for (const id of findingFileIds(name, f.scope)) ids.add(id);
1226
+ }
1227
+ return [...ids].map((id) => stackData.find((s) => s.id === id)).filter((s) => s?.path);
1228
+ }
1229
+
1219
1230
  function findingPrompt(f) {
1231
+ const sources = findingSources(f);
1232
+ const paths = sources.map((s) => s.path);
1220
1233
  let header;
1221
- if (f.scope === 'user') {
1234
+ if (paths.length === 1) {
1235
+ header = `In the Claude Code instruction file at ${paths[0]}, apply this fix:`;
1236
+ } else if (paths.length > 1) {
1237
+ header = 'In the Claude Code instruction files listed below, apply this fix:';
1238
+ } else if (f.scope === 'user') {
1222
1239
  const path = stackData.find((s) => s.id === 'user-claude-md')?.path || '~/.claude/CLAUDE.md';
1223
1240
  header = `In the user-level Claude Code instructions at ${path}, apply this fix:`;
1224
1241
  } else {
@@ -1228,8 +1245,12 @@ function findingPrompt(f) {
1228
1245
  : "In this project's Claude Code auto-memory, apply this fix:";
1229
1246
  }
1230
1247
  const lines = [header, '', `Finding (${f.kind}, ${f.severity}): ${f.title}`, f.detail, '', `Action: ${f.suggestion}`];
1231
- if ((f.files || []).length) lines.push(`Files: ${f.files.join(', ')}`);
1232
- if (f.scope !== 'user') lines.push('', 'Update MEMORY.md index lines if files are added, renamed, or removed.');
1248
+ const cited = paths.length ? paths : f.files || [];
1249
+ if (cited.length) lines.push(`Files: ${cited.join(', ')}`);
1250
+ // The index note only applies to auto-memory files; a CLAUDE.md or SKILL.md
1251
+ // fix has no MEMORY.md to keep in sync.
1252
+ const touchesMemory = paths.length ? sources.some((s) => s.scope === 'memory') : f.scope !== 'user';
1253
+ if (touchesMemory) lines.push('', 'Update MEMORY.md index lines if files are added, renamed, or removed.');
1233
1254
  return lines.join('\n');
1234
1255
  }
1235
1256
 
package/server.js CHANGED
@@ -850,6 +850,25 @@ const RUBRIC_CLAUDE_MD_USER = [
850
850
  '- Preferences should carry their why: a bare prohibition gets overridden when inconvenient; a one-clause reason makes it generalize.',
851
851
  ].join('\n');
852
852
 
853
+ // The server already knows every `@import` a file makes: parseImports() carries the
854
+ // grammar (npm scopes excluded, extension required) and resolveImport() the resolution
855
+ // (~ = home, else relative to the importing file). Handing the reviewer that answer as
856
+ // fact beats shipping it a prose reimplementation it would run differently each time.
857
+ // Hard `@` imports only — markdown links are references, not inlined content.
858
+ function importLines(src) {
859
+ const lines = [`PATH: ${src.path}`];
860
+ const resolved = [];
861
+ const missing = [];
862
+ for (const imp of parseImports(src.content).imports) {
863
+ const abs = resolveImport(imp, src.path);
864
+ if (fs.existsSync(abs)) resolved.push(abs);
865
+ else missing.push(`${imp} (tried ${abs})`);
866
+ }
867
+ if (resolved.length) lines.push(`IMPORTS: ${resolved.join(', ')}`);
868
+ if (missing.length) lines.push(`MISSING IMPORTS: ${missing.join(', ')}`);
869
+ return lines;
870
+ }
871
+
853
872
  function buildAnalyzePrompt(memSources, skillSources, agentSources, claudeMdSources, userSrc, userAudited) {
854
873
  const fullSkills = skillSources || [];
855
874
  const fullAgent = agentSources || [];
@@ -885,8 +904,9 @@ function buildAnalyzePrompt(memSources, skillSources, agentSources, claudeMdSour
885
904
  parts.push(
886
905
  '',
887
906
  'Rules:',
888
- `- You are the orchestrator; do not review files yourself. Your working directory is the project root. For every audited file, launch the type-matched reviewer subagent via your Agent tool — "memory-reviewer" for memory and agent-memory files, "skill-reviewer" for skills, "claude-md-reviewer" for project CLAUDE.md files${userAudited ? ', "claude-md-user-reviewer" for the user-level CLAUDE.md' : ''} — all in a single parallel batch of blocking calls (never background/async agents — you must hold your final report until every reviewer has returned). Pass each one only the file name and its full content; the reviewers carry their own criteria and verify claims against the repo${userAudited ? ' (the user-level reviewer verifies against the machine, never the repo)' : ''}.`,
907
+ `- You are the orchestrator; do not review files yourself. Your working directory is the project root. For every audited file, launch the type-matched reviewer subagent via your Agent tool — "memory-reviewer" for memory and agent-memory files, "skill-reviewer" for skills, "claude-md-reviewer" for project CLAUDE.md files${userAudited ? ', "claude-md-user-reviewer" for the user-level CLAUDE.md' : ''} — all in a single parallel batch of blocking calls (never background/async agents — you must hold your final report until every reviewer has returned). Pass each one only the file name, its full content, and — for skills and CLAUDE.md files — the PATH:, IMPORTS:, and MISSING IMPORTS: lines from its block below; the reviewers carry their own criteria and verify claims against the repo${userAudited ? ' (the user-level reviewer verifies against the machine, never the repo)' : ''}.`,
889
908
  `- Your own job: merge the reviewers' per-file findings and add the cross-file kinds (duplicate, contradiction, merge, promote${crossScope ? ', override, shadow' : ''}) by comparing the files side by side.`,
909
+ '- @imports are inlined content: the files on a block\'s IMPORTS: line load as part of that file. Reviewers read them, so treat what they report from an import as belonging to the importing file when you compare files side by side — a rule imported by one file and written out in another is still a duplicate. Imported files are never audited on their own; attribute every finding to the importer.',
890
910
  '- Verification is a filter: drop any finding a reviewer\'s claim checks disproved, and never report "likely outdated" for a claim a reviewer confirmed still HOLDS. Record what was checked in the finding\'s "evidence" field (one sentence, internal bookkeeping — it is not shown to the user). Omit "evidence" for judgment-only findings.',
891
911
  '- files: the exact file names involved as listed below (e.g. feedback_foo.md, MEMORY.md, or a skill name like my-skill). Bare names only — never append scope markers like "(user)"; scope is carried by the scope field.',
892
912
  '- scope: "user" if the finding only cites the user-level CLAUDE.md (demote findings are always "user"); "cross" if it cites both the user-level file and any project-side file (override, shadow, and promote findings are "cross"); "project" for everything else.',
@@ -900,9 +920,12 @@ function buildAnalyzePrompt(memSources, skillSources, agentSources, claudeMdSour
900
920
  '',
901
921
  );
902
922
  if (userAudited) {
903
- parts.push('USER CLAUDE.MD (audit this with claude-md-user-reviewer):', `=== FILE (user): ${userSrc.name} ===`, userSrc.content, '');
923
+ parts.push('USER CLAUDE.MD (audit this with claude-md-user-reviewer):', `=== FILE (user): ${userSrc.name} ===`, ...importLines(userSrc), userSrc.content, '');
904
924
  } else {
905
- parts.push('GLOBAL CLAUDE.md (reference only, do not audit it):', '<<<', userSrc?.content || '(none)', '>>>', '');
925
+ parts.push(
926
+ 'GLOBAL CLAUDE.md (reference only, do not audit it):',
927
+ ...(userSrc ? importLines(userSrc).slice(1).map((l) => `${l} — inlined into the global file; read these before judging a rule missing from it`) : []),
928
+ '<<<', userSrc?.content || '(none)', '>>>', '');
906
929
  }
907
930
  if (memSources.length) {
908
931
  parts.push('MEMORY FILES:');
@@ -913,7 +936,7 @@ function buildAnalyzePrompt(memSources, skillSources, agentSources, claudeMdSour
913
936
  if (fullSkills.length) {
914
937
  parts.push('PROJECT SKILLS (audit these):');
915
938
  for (const s of fullSkills) {
916
- parts.push(`=== SKILL: ${s.name} ===`, s.content, '');
939
+ parts.push(`=== SKILL: ${s.name} ===`, ...importLines(s), s.content, '');
917
940
  }
918
941
  }
919
942
  if (fullAgent.length) {
@@ -925,7 +948,7 @@ function buildAnalyzePrompt(memSources, skillSources, agentSources, claudeMdSour
925
948
  if (fullClaudeMd.length) {
926
949
  parts.push('CLAUDE.MD FILES (audit these):');
927
950
  for (const s of fullClaudeMd) {
928
- parts.push(`=== FILE (${s.scope}): ${s.name} ===`, s.content, '');
951
+ parts.push(`=== FILE (${s.scope}): ${s.name} ===`, ...importLines(s), s.content, '');
929
952
  }
930
953
  }
931
954
  return parts.join('\n');
@@ -940,6 +963,7 @@ const REVIEWER_COMMON = [
940
963
  'You review exactly one file that shapes Claude Code behavior; the parent passes its name and full content.',
941
964
  'Verify before judging: for every claim that references the repository in your working directory — a file, directory, command, script, flag, or path — check whether it still holds with your read-only tools. For tools the file expects on the machine, check existence with `command -v` / `which` / `where` (the only Bash commands you may run). Never call something "likely stale" when you can check it.',
942
965
  'Also judge whether the content is still needed at all: if the thing it guards against is gone (the tool was removed, the convention is enforced elsewhere, the referenced file no longer exists), say so explicitly.',
966
+ '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.',
943
967
  ];
944
968
 
945
969
  // The user reviewer swaps repo verification for machine verification — the working
@@ -948,6 +972,7 @@ const REVIEWER_COMMON_USER = [
948
972
  'You review the user-level (global) CLAUDE.md that loads in every session of every project; the parent passes its name and full content.',
949
973
  '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 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.',
950
974
  '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
+ '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.',
951
976
  ];
952
977
 
953
978
  const REVIEWER_REPORT = [
@@ -956,10 +981,24 @@ const REVIEWER_REPORT = [
956
981
  '2. FINDINGS: candidate findings for this file only (kind: stale, invalidate, or quality; severity high/med/low; title; detail; one-sentence fix). An empty list is a valid answer — do not pad.',
957
982
  ];
958
983
 
984
+ // `@path/to/file.md` in a CLAUDE.md or a skill is an import: Claude Code inlines the
985
+ // target's content where the token sits. A reviewer that judges only the text in front
986
+ // of it reads an import as a dangling reference and never sees the rules the file
987
+ // actually loads. `importLines()` resolves the graph server-side and states it as fact,
988
+ // so the rubric only has to say what an import means. Memory files are excluded: the
989
+ // imports they make are audited as sources in their own right.
990
+ const RUBRIC_IMPORTS = [
991
+ 'Import notation (@path):',
992
+ '- The IMPORTS: line of your file block lists absolute paths Claude Code inlines into this file at load time. Read each one and judge its content against every criterion above as part of this file — including imports of imports. Duplication between the file and its own import, contradictions across its imports, and bloat pulled in through an import all count against the importing file.',
993
+ '- An @path token is an import, never a broken link or an unexplained reference: do not flag the pointer itself as vague or undefined. Anything on the MISSING IMPORTS: line resolves to nothing, and is the only missing-import finding to report (kind: stale).',
994
+ ].join('\n');
995
+
996
+ // `rubric` takes one rubric constant or a list of them, so a reviewer can stack the
997
+ // shared import rubric on top of its per-type one without a second parameter.
959
998
  function reviewerAgent(description, rubric, common = REVIEWER_COMMON) {
960
999
  return {
961
1000
  description,
962
- prompt: [common.join('\n'), RUBRIC_CORE, rubric, REVIEWER_REPORT.join('\n')].join('\n\n'),
1001
+ prompt: [common.join('\n'), RUBRIC_CORE, ...[].concat(rubric), REVIEWER_REPORT.join('\n')].join('\n\n'),
963
1002
  // Bash is permission-scoped to command -v / which / where by the spawn's
964
1003
  // --allowedTools — anything else the reviewer tries is auto-denied in -p mode.
965
1004
  tools: ['Read', 'Grep', 'Glob', 'Bash'],
@@ -973,15 +1012,15 @@ const ANALYSIS_AGENTS = {
973
1012
  ),
974
1013
  'skill-reviewer': reviewerAgent(
975
1014
  'Reviews and verifies one SKILL.md file against the skill rubric and the repository. One per skill, in parallel.',
976
- RUBRIC_SKILL,
1015
+ [RUBRIC_SKILL, RUBRIC_IMPORTS],
977
1016
  ),
978
1017
  'claude-md-reviewer': reviewerAgent(
979
1018
  'Reviews and verifies one CLAUDE.md instruction file against the CLAUDE.md rubric and the repository. One per file, in parallel.',
980
- RUBRIC_CLAUDE_MD,
1019
+ [RUBRIC_CLAUDE_MD, RUBRIC_IMPORTS],
981
1020
  ),
982
1021
  'claude-md-user-reviewer': reviewerAgent(
983
1022
  'Reviews and verifies the user-level (global) CLAUDE.md against the user rubric, PATH, and ~/.claude — never against the working repository.',
984
- RUBRIC_CLAUDE_MD_USER,
1023
+ [RUBRIC_CLAUDE_MD_USER, RUBRIC_IMPORTS],
985
1024
  REVIEWER_COMMON_USER,
986
1025
  ),
987
1026
  };
@@ -1017,7 +1056,15 @@ function runClaudeAnalysis(prompt, model, cwd) {
1017
1056
  try {
1018
1057
  const envelope = JSON.parse(out);
1019
1058
  if (envelope.is_error) return finish({ ok: false, error: String(envelope.result || 'Claude Code reported an error') });
1020
- finish({ ok: true, data: envelope.structured_output, costUsd: envelope.total_cost_usd, durationMs: envelope.duration_ms });
1059
+ // A run can exit clean without structured_output (schema retries exhausted,
1060
+ // truncated stdout). Spreading that absence downstream yields an empty
1061
+ // findings list, which the UI cannot tell apart from a genuine all-clear —
1062
+ // so treat a missing payload as a failure rather than as "no problems".
1063
+ const data = envelope.structured_output;
1064
+ if (!data || !Array.isArray(data.findings)) {
1065
+ return finish({ ok: false, error: 'Claude Code returned no structured output — the audit did not complete, so no result is being shown.' });
1066
+ }
1067
+ finish({ ok: true, data, costUsd: envelope.total_cost_usd, durationMs: envelope.duration_ms });
1021
1068
  } catch {
1022
1069
  finish({ ok: false, error: `Claude Code returned an invalid JSON envelope${err ? `: ${err.slice(0, 300)}` : ''}` });
1023
1070
  }