claude-code-memory-explorer 1.2.0 → 1.3.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/server.js CHANGED
@@ -38,6 +38,7 @@ const projectDirArg = getArg('project');
38
38
 
39
39
  // #region STATE
40
40
 
41
+ // null is the user scope: only the sources every session loads, whatever the project.
41
42
  let currentProjectPath = projectDirArg ? path.resolve(expandHome(projectDirArg)) : process.cwd();
42
43
 
43
44
  const cache = {};
@@ -214,6 +215,19 @@ function spreadImports(filePath, content) {
214
215
  return { imports: resolved, softImports: resolvedSoft, unresolvedImports: unresolved };
215
216
  }
216
217
 
218
+ function fmName(info) {
219
+ const n = info.frontmatter?.name;
220
+ return typeof n === 'string' ? n : '';
221
+ }
222
+
223
+ // A skill's name + description ride the system prompt every session (unless model
224
+ // invocation is disabled) — that is its standing cost, not the on-demand body.
225
+ // A subagent's name + description ride the Agent tool's listing the same way.
226
+ function descChars(name, info) {
227
+ const desc = info.frontmatter?.description;
228
+ return name.length + (typeof desc === 'string' ? desc.trim().length : 0);
229
+ }
230
+
217
231
  function hasPathsFilter(frontmatter) {
218
232
  if (!frontmatter?.paths) return false;
219
233
  return Array.isArray(frontmatter.paths) ? frontmatter.paths.length > 0 : true;
@@ -269,7 +283,7 @@ function discoverMemorySources(projectPath) {
269
283
  }
270
284
 
271
285
  // 4. Walk up from projectPath to find CLAUDE.md and CLAUDE.local.md
272
- const ancestors = getAncestorDirs(projectPath);
286
+ const ancestors = projectPath ? getAncestorDirs(projectPath) : [];
273
287
  const seenPaths = new Set(sources.map(s => s.path));
274
288
  for (const dir of ancestors) {
275
289
  const isProjectRoot = path.resolve(dir) === path.resolve(projectPath);
@@ -327,11 +341,11 @@ function discoverMemorySources(projectPath) {
327
341
  walkForClaudeMd(subdir, depth + 1);
328
342
  }
329
343
  }
330
- walkForClaudeMd(projectPath, 0);
344
+ if (projectPath) walkForClaudeMd(projectPath, 0);
331
345
 
332
346
  // 5. Project rules (.claude/rules/*.md)
333
- const projectRulesDir = path.join(projectPath, '.claude', 'rules');
334
- if (fs.existsSync(projectRulesDir)) {
347
+ const projectRulesDir = projectPath && path.join(projectPath, '.claude', 'rules');
348
+ if (projectRulesDir && fs.existsSync(projectRulesDir)) {
335
349
  for (const file of findMdFiles(projectRulesDir)) {
336
350
  const info = fileInfo(file);
337
351
  if (!info) continue;
@@ -352,7 +366,7 @@ function discoverMemorySources(projectPath) {
352
366
  const info = fileInfo(file);
353
367
  if (!info) return;
354
368
  const dirName = path.basename(path.dirname(file));
355
- const skillName = (info.frontmatter && typeof info.frontmatter.name === 'string' && info.frontmatter.name) || dirName;
369
+ const skillName = fmName(info) || dirName;
356
370
  const display = extra.nestedRel ? `${extra.nestedRel}:${skillName}` : skillName;
357
371
  sources.push({
358
372
  id: `skill-${skillSource}-${slug(file)}`,
@@ -361,16 +375,40 @@ function discoverMemorySources(projectPath) {
361
375
  load: 'ondemand',
362
376
  skillSource,
363
377
  skillName,
378
+ descChars: info.frontmatter?.['disable-model-invocation'] === 'true' ? null : descChars(skillName, info),
364
379
  ...extra,
365
380
  ...info,
366
381
  ...spreadImports(info.path, info.content),
367
382
  });
368
383
  }
369
- for (const file of findSkillFiles(path.join(projectPath, '.claude', 'skills'))) pushSkill(file, 'project');
384
+ for (const file of findSkillFiles(path.join(CLAUDE_DIR, 'skills'))) pushSkill(file, 'user');
385
+ if (projectPath) {
386
+ for (const file of findSkillFiles(path.join(projectPath, '.claude', 'skills'))) pushSkill(file, 'project');
387
+ }
370
388
  for (const { dir, rel } of nestedSkillDirs) {
371
389
  for (const file of findSkillFiles(dir)) pushSkill(file, 'project', { nestedRel: rel });
372
390
  }
373
391
 
392
+ // 5c. Subagent definitions: the description is listed in every session, the body loads when the agent runs
393
+ const agentRoots = [{ root: path.join(CLAUDE_DIR, 'agents'), agentSource: 'user' }];
394
+ if (projectPath) agentRoots.push({ root: path.join(projectPath, '.claude', 'agents'), agentSource: 'project' });
395
+ for (const { root, agentSource } of agentRoots) {
396
+ for (const file of findMdFiles(root)) {
397
+ const info = fileInfo(file);
398
+ if (!info) continue;
399
+ const name = fmName(info) || path.basename(file, '.md');
400
+ sources.push({
401
+ id: `agent-${agentSource}-${slug(file)}`,
402
+ name,
403
+ scope: 'agent',
404
+ load: 'ondemand',
405
+ agentSource,
406
+ descChars: descChars(name, info),
407
+ ...info,
408
+ });
409
+ }
410
+ }
411
+
374
412
  function pushMemoryDir(dir, scope, idPrefix, extraFields = {}) {
375
413
  const memoryMd = path.join(dir, 'MEMORY.md');
376
414
  const memInfo = fileInfo(memoryMd);
@@ -404,7 +442,7 @@ function discoverMemorySources(projectPath) {
404
442
  }
405
443
 
406
444
  // 6. Auto memory (projects base honors `autoMemoryDirectory` user setting)
407
- const memoryDir = findMemoryDir(projectPath);
445
+ const memoryDir = projectPath && findMemoryDir(projectPath);
408
446
  if (memoryDir && fs.existsSync(memoryDir)) {
409
447
  pushMemoryDir(memoryDir, 'memory', 'memory');
410
448
  }
@@ -413,11 +451,13 @@ function discoverMemorySources(projectPath) {
413
451
  // user: ~/.claude/agent-memory/<agent>/
414
452
  // project: <project>/.claude/agent-memory/<agent>/
415
453
  // local: <project>/.claude/agent-memory-local/<agent>/
416
- const agentMemoryRoots = [
417
- { root: path.join(CLAUDE_DIR, 'agent-memory'), agentScope: 'user' },
418
- { root: path.join(projectPath, '.claude', 'agent-memory'), agentScope: 'project' },
419
- { root: path.join(projectPath, '.claude', 'agent-memory-local'), agentScope: 'local' },
420
- ];
454
+ const agentMemoryRoots = [{ root: path.join(CLAUDE_DIR, 'agent-memory'), agentScope: 'user' }];
455
+ if (projectPath) {
456
+ agentMemoryRoots.push(
457
+ { root: path.join(projectPath, '.claude', 'agent-memory'), agentScope: 'project' },
458
+ { root: path.join(projectPath, '.claude', 'agent-memory-local'), agentScope: 'local' },
459
+ );
460
+ }
421
461
  for (const { root, agentScope } of agentMemoryRoots) {
422
462
  let agentDirs;
423
463
  try { agentDirs = fs.readdirSync(root, { withFileTypes: true }); } catch { continue; }
@@ -437,8 +477,9 @@ function discoverMemorySources(projectPath) {
437
477
  ...(s.imports || []).map(imp => ({ imp, parent: s, hard: true })),
438
478
  ...(s.softImports || []).map(imp => ({ imp, parent: s, hard: false })),
439
479
  ]);
480
+ // https://code.claude.com/docs/en/memory: imports nest up to four hops.
440
481
  let depth = 0;
441
- while (queue.length && depth < 5) {
482
+ while (queue.length && depth < 4) {
442
483
  const batch = queue.splice(0, queue.length);
443
484
  for (const { imp, parent, hard } of batch) {
444
485
  if (seen.has(imp)) {
@@ -476,8 +517,16 @@ function discoverMemorySources(projectPath) {
476
517
  return sources;
477
518
  }
478
519
 
479
- function findSkillFiles(root) {
480
- return findMdFiles(root, { name: 'SKILL.md', maxDepth: 4 });
520
+ // A SKILL.md inside a skill's folder (templates, examples) is not a skill of its own.
521
+ function findSkillFiles(dir, depth = 0) {
522
+ let entries;
523
+ try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return []; }
524
+ if (entries.some((e) => e.isFile() && e.name === 'SKILL.md')) return [path.join(dir, 'SKILL.md')];
525
+ if (depth >= 4) return [];
526
+ // Claude Code parks removed synced skills in skills/.trash/ and does not load them.
527
+ return entries
528
+ .filter((e) => e.isDirectory() && !e.name.startsWith('.'))
529
+ .flatMap((e) => findSkillFiles(path.join(dir, e.name), depth + 1));
481
530
  }
482
531
 
483
532
  function findMdFiles(dir, opts = {}, depth = 0) {
@@ -553,11 +602,9 @@ function encodeProjectPath(projectPath) {
553
602
  .replace(/\//g, '-');
554
603
  }
555
604
 
556
- // User-scope `autoMemoryDirectory` only — Claude Code rejects this key from project/local settings for security.
605
+ // Managed, then user `autoMemoryDirectory` — Claude Code rejects this key from project/local settings for security.
557
606
  function getProjectsBaseDir() {
558
- let settings = {};
559
- try { settings = JSON.parse(fs.readFileSync(path.join(CLAUDE_DIR, 'settings.json'), 'utf-8')); } catch { /* no settings */ }
560
- const raw = settings.autoMemoryDirectory;
607
+ const raw = readJson(managedSettingsPath())?.autoMemoryDirectory || readJson(USER_SETTINGS_PATH)?.autoMemoryDirectory;
561
608
  if (typeof raw === 'string' && raw.trim()) return path.resolve(expandHome(raw));
562
609
  return path.join(CLAUDE_DIR, 'projects');
563
610
  }
@@ -612,20 +659,33 @@ app.use(express.json());
612
659
  if (process.env.HUB_SDK_SERVER) require(process.env.HUB_SDK_SERVER).mount(app);
613
660
  app.use(express.static(path.join(__dirname, 'public')));
614
661
 
662
+ function projectInfo() {
663
+ const p = currentProjectPath;
664
+ return { path: p, name: p ? path.basename(p) : 'User', user: !p, configDir: CLAUDE_DIR };
665
+ }
666
+
615
667
  app.get('/api/project', (_req, res) => {
616
- res.json({ path: currentProjectPath, name: path.basename(currentProjectPath), configDir: CLAUDE_DIR });
668
+ res.json(projectInfo());
617
669
  });
618
670
 
671
+ // Body: {path} opens a project, {user: true} the user scope.
619
672
  app.put('/api/project', (req, res) => {
620
- const { path: dirPath } = req.body;
673
+ const { path: dirPath, user } = req.body || {};
674
+ if (user === true) {
675
+ if (currentProjectPath) {
676
+ currentProjectPath = null;
677
+ clearCache();
678
+ }
679
+ return res.json(projectInfo());
680
+ }
621
681
  if (!dirPath) return res.status(400).json({ error: 'path required' });
622
682
  const resolved = path.resolve(expandHome(dirPath));
623
683
  if (!fs.existsSync(resolved)) return res.status(404).json({ error: 'directory not found' });
624
- if (resolved !== path.resolve(currentProjectPath)) {
684
+ if (!currentProjectPath || resolved !== path.resolve(currentProjectPath)) {
625
685
  currentProjectPath = resolved;
626
686
  clearCache();
627
687
  }
628
- res.json({ path: currentProjectPath, name: path.basename(currentProjectPath) });
688
+ res.json(projectInfo());
629
689
  });
630
690
 
631
691
  app.post('/api/refresh', (_req, res) => {
@@ -636,11 +696,8 @@ app.post('/api/refresh', (_req, res) => {
636
696
  function resolveAllowedPath(filePath) {
637
697
  if (!filePath) return { error: { status: 400, message: 'path required' } };
638
698
  const resolved = path.resolve(expandHome(filePath));
639
- const roots = [
640
- path.resolve(CLAUDE_DIR),
641
- path.resolve(currentProjectPath),
642
- getProjectsBaseDir(),
643
- ];
699
+ const roots = [path.resolve(CLAUDE_DIR), getProjectsBaseDir()];
700
+ if (currentProjectPath) roots.push(path.resolve(currentProjectPath));
644
701
  const inRoot = roots.some((r) => resolved === r || resolved.startsWith(r + path.sep));
645
702
  if (!inRoot) return { error: { status: 403, message: 'path outside allowed roots' } };
646
703
  return { resolved };
@@ -1089,7 +1146,8 @@ function selectAnalysisSources(stack, ids) {
1089
1146
  const wanted = new Set(ids);
1090
1147
  return all.filter((s) => wanted.has(s.id));
1091
1148
  }
1092
- return all.filter((s) => s.scope === 'memory');
1149
+ // The user scope has no auto memory, so its default is the user CLAUDE.md.
1150
+ return all.filter((s) => s.scope === (currentProjectPath ? 'memory' : 'user'));
1093
1151
  }
1094
1152
 
1095
1153
  function scopeDescription(selected) {
@@ -1108,12 +1166,17 @@ function scopeDescription(selected) {
1108
1166
  return parts.join(' + ');
1109
1167
  }
1110
1168
 
1169
+ // The user scope has no project entry: its runs are the shared user-scope entry's.
1170
+ const analysisProject = () => currentProjectPath || USER_SCOPE;
1171
+
1111
1172
  app.get('/api/memory/analysis', (_req, res) => {
1112
- const st = getAnalysisState(currentProjectPath);
1113
- const running = st.pending.some((p) => !p.stalled);
1114
1173
  // The shared user-scope entry rides along on every project's payload; its
1115
1174
  // staleness tracks the user CLAUDE.md itself, independent of the project.
1116
1175
  const us = getAnalysisState(USER_SCOPE);
1176
+ const st = currentProjectPath
1177
+ ? getAnalysisState(currentProjectPath)
1178
+ : { ...EMPTY_ANALYSIS, pending: us.pending, error: us.error };
1179
+ const running = st.pending.some((p) => !p.stalled);
1117
1180
  // Staleness needs a full stack scan + content hash — skip it while a run is in
1118
1181
  // flight (the client polls every 2.5s and stale is meaningless mid-run anyway).
1119
1182
  let stale = false;
@@ -1126,8 +1189,8 @@ app.get('/api/memory/analysis', (_req, res) => {
1126
1189
  stale = st.hash !== hash;
1127
1190
  }
1128
1191
  if (us.result) {
1129
- const userSrc = stack.find((s) => s.id === 'user-claude-md');
1130
- userStale = us.hash !== (userSrc ? memoryContentHash([userSrc]) : null);
1192
+ const selected = us.ids ? selectAnalysisSources(stack, us.ids) : [];
1193
+ userStale = us.hash !== (selected.length ? memoryContentHash(selected) : null);
1131
1194
  }
1132
1195
  }
1133
1196
  res.json({
@@ -1146,7 +1209,7 @@ app.get('/api/memory/analysis', (_req, res) => {
1146
1209
  // scope:'user' targets the shared user-scope entry, so the action holds across
1147
1210
  // projects; anything else targets the current project's entry.
1148
1211
  function analysisTarget(req) {
1149
- return req.body?.scope === 'user' ? USER_SCOPE : currentProjectPath;
1212
+ return req.body?.scope === 'user' ? USER_SCOPE : analysisProject();
1150
1213
  }
1151
1214
 
1152
1215
  app.post('/api/memory/analysis/delete-run', (req, res) => {
@@ -1176,7 +1239,7 @@ app.post('/api/memory/analysis/dismiss', (req, res) => {
1176
1239
  });
1177
1240
 
1178
1241
  app.post('/api/memory/analyze', (req, res) => {
1179
- const prev = getAnalysisState(currentProjectPath);
1242
+ const prev = getAnalysisState(analysisProject());
1180
1243
  const stack = getStack();
1181
1244
  const ids = req.body?.ids;
1182
1245
  const selected = selectAnalysisSources(stack, ids);
@@ -1198,13 +1261,16 @@ app.post('/api/memory/analyze', (req, res) => {
1198
1261
  const claudeMdSources = selected.filter((s) => CLAUDE_MD_SCOPES.has(s.scope));
1199
1262
  const userSrc = userSelected || stack.find((s) => s.id === 'user-claude-md');
1200
1263
  const prompt = buildAnalyzePrompt(memSources, skills, agentSources, claudeMdSources, userSrc, !!userSelected);
1201
- const stateIds = Array.isArray(ids) && ids.length ? ids : null;
1264
+ // The user entry is also read from projects, where the default selection differs, so it keeps its ids.
1265
+ const stateIds = Array.isArray(ids) && ids.length ? ids : currentProjectPath ? null : selected.map((s) => s.id);
1202
1266
  const model = ANALYSIS_MODELS.has(req.body?.model) ? req.body.model : null;
1203
1267
  // Capture the project now — the user may switch projects while the run is in flight,
1204
1268
  // and the result must land under the project it was computed for.
1205
- const project = currentProjectPath;
1269
+ const project = analysisProject();
1206
1270
  const runId = crypto.randomUUID();
1207
1271
  const scopeDesc = scopeDescription(selected);
1272
+ // In the user scope the run already lands in the user entry, so there is nothing to split off.
1273
+ const splitUser = !!userSelected && project !== USER_SCOPE;
1208
1274
  saveAnalysisState(project, {
1209
1275
  ...prev,
1210
1276
  // Starting a fresh run retires stalled leftovers — the user has moved on.
@@ -1216,12 +1282,12 @@ app.post('/api/memory/analyze', (req, res) => {
1216
1282
  // file is owned by the user-scope entry's snapshot, not the project's.
1217
1283
  // Snapshotting up front also keeps `selected` (full file contents) out of the
1218
1284
  // completion closure, which can live for hours on a run that never returns.
1219
- const audited = selected.filter((s) => s.scope !== 'user').map((s) => ({ id: s.id, name: s.name, scope: s.scope, lines: s.lines }));
1285
+ const audited = selected.filter((s) => !splitUser || s.scope !== 'user').map((s) => ({ id: s.id, name: s.name, scope: s.scope, lines: s.lines }));
1220
1286
  const userAudited = userSelected ? [{ id: userSelected.id, name: userSelected.name, scope: 'user', lines: userSelected.lines }] : null;
1221
1287
  const userId = userSelected?.id || null;
1222
1288
  // Prepend a run to an entry's history, newest first, capped.
1223
1289
  const withRun = (cur, run) => [run, ...(cur.runs || [])].slice(0, MAX_ANALYSIS_RUNS);
1224
- runClaudeAnalysis(prompt, model, userOnly ? os.homedir() : project).then((r) => {
1290
+ runClaudeAnalysis(prompt, model, userOnly || project === USER_SCOPE ? os.homedir() : project).then((r) => {
1225
1291
  const ts = Date.now();
1226
1292
  let result = r.ok ? { ...r.data, costUsd: r.costUsd, durationMs: r.durationMs, scopeDesc, model } : null;
1227
1293
  if (result) {
@@ -1232,7 +1298,7 @@ app.post('/api/memory/analyze', (req, res) => {
1232
1298
  }
1233
1299
  // Findings about the user CLAUDE.md alone are shared across projects: split them
1234
1300
  // into the user-scope entry; project and cross findings stay with the project.
1235
- if (result && userId) {
1301
+ if (result && splitUser) {
1236
1302
  const all = result.findings;
1237
1303
  const uCur = getAnalysisState(USER_SCOPE);
1238
1304
  const uRun = {
@@ -1247,7 +1313,7 @@ app.post('/api/memory/analyze', (req, res) => {
1247
1313
  }
1248
1314
  // Re-read from disk — parallel runs or another instance may have finished meanwhile.
1249
1315
  const cur = getAnalysisState(project);
1250
- const runs = result && !userOnly ? withRun(cur, { result, ts, hash, ids: stateIds, audited }) : cur.runs || [];
1316
+ const runs = result && !(userOnly && splitUser) ? withRun(cur, { result, ts, hash, ids: stateIds, audited }) : cur.runs || [];
1251
1317
  saveAnalysisState(project, {
1252
1318
  ...cur,
1253
1319
  pending: cur.pending.filter((p) => p.id !== runId),
@@ -1269,13 +1335,15 @@ app.post('/api/open-in-editor', (req, res) => {
1269
1335
  }
1270
1336
  });
1271
1337
 
1272
- // A skill's name + description ride the system prompt every session (unless model
1273
- // invocation is disabled) — that is its standing cost, not the on-demand body.
1274
- function skillDescMeta(source) {
1275
- const fm = source.frontmatter || {};
1276
- if (fm['disable-model-invocation'] === 'true') return null;
1277
- const desc = typeof fm.description === 'string' ? fm.description : '';
1278
- return { chars: (source.skillName || source.name || '').length + desc.trim().length };
1338
+ // Claude Code lists one definition per name: a user skill wins over a project skill, a project
1339
+ // agent over a user agent. The stack holds user definitions before project ones.
1340
+ function descTotal(stack, scope) {
1341
+ const byName = new Map();
1342
+ for (const s of stack) {
1343
+ if (s.scope !== scope || s.descChars == null) continue;
1344
+ if (scope === 'agent' || !byName.has(s.name)) byName.set(s.name, s.descChars);
1345
+ }
1346
+ return { count: byName.size, chars: [...byName.values()].reduce((n, c) => n + c, 0) };
1279
1347
  }
1280
1348
 
1281
1349
  app.get('/api/summary', (_req, res) => {
@@ -1294,7 +1362,7 @@ app.get('/api/summary', (_req, res) => {
1294
1362
  if (s.parentId && treeIds.has(s.parentId) && !treeIds.has(s.id)) { treeIds.add(s.id); grew = true; }
1295
1363
  }
1296
1364
  }
1297
- const sources = stack.filter(s => s.scope !== 'skill' && s.load !== 'link' && !treeIds.has(s.id)
1365
+ const sources = stack.filter(s => s.scope !== 'skill' && s.scope !== 'agent' && s.load !== 'link' && !treeIds.has(s.id)
1298
1366
  && !((s.scope === 'memory' || s.scope === 'agent-memory') && s.load === 'ondemand'));
1299
1367
  const totalFiles = sources.length;
1300
1368
  const totalLines = sources.reduce((s, f) => s + (f.lines || 0), 0);
@@ -1302,16 +1370,94 @@ app.get('/api/summary', (_req, res) => {
1302
1370
  const alwaysLoaded = sources.filter(s => s.load === 'always' || s.load === 'startup').length;
1303
1371
  const conditional = sources.filter(s => s.load === 'conditional').length;
1304
1372
  const onDemand = sources.filter(s => s.load === 'ondemand').length;
1305
- const skillDescs = stack.filter(s => s.scope === 'skill').map(skillDescMeta).filter(Boolean);
1306
- const skillDesc = { count: skillDescs.length, chars: skillDescs.reduce((s, d) => s + d.chars, 0) };
1373
+ const skillDesc = descTotal(stack, 'skill');
1374
+ const agentDesc = descTotal(stack, 'agent');
1307
1375
  // Per-scope char totals so the client budget bar shares this exact footprint filter
1308
1376
  const scopeChars = {};
1309
1377
  for (const f of sources) scopeChars[f.scope] = (scopeChars[f.scope] || 0) + (f.chars || 0);
1310
- const totalChars = sources.reduce((s, f) => s + (f.chars || 0), 0) + skillDesc.chars;
1378
+ const totalChars = sources.reduce((s, f) => s + (f.chars || 0), 0) + skillDesc.chars + agentDesc.chars;
1311
1379
  // ids: which sources make up the footprint, so the client can highlight them
1312
1380
  // userClaudeMd: where the user-level file lives even when it does not exist yet, so the
1313
1381
  // client can name it in fix prompts without assuming ~/.claude
1314
- res.json({ totalFiles, totalLines, totalBytes, totalChars, scopeChars, skillDesc, alwaysLoaded, conditional, onDemand, ids: sources.map(s => s.id), userClaudeMd: USER_CLAUDE_MD });
1382
+ res.json({ totalFiles, totalLines, totalBytes, totalChars, scopeChars, skillDesc, agentDesc, alwaysLoaded, conditional, onDemand, ids: sources.map(s => s.id), userClaudeMd: USER_CLAUDE_MD });
1383
+ });
1384
+
1385
+ const MANAGED_SETTINGS_PATH = path.join(path.dirname(MANAGED_POLICY_PATHS[0]), 'managed-settings.json');
1386
+ // Claude Code caches server-managed settings (claude.ai admin console) in the config dir. Within the
1387
+ // managed tier they win over the system file, and the sources do not merge.
1388
+ const REMOTE_SETTINGS_PATH = path.join(CLAUDE_DIR, 'remote-settings.json');
1389
+ const USER_SETTINGS_PATH = path.join(CLAUDE_DIR, 'settings.json');
1390
+ const managedSettingsPath = () => (fs.existsSync(REMOTE_SETTINGS_PATH) ? REMOTE_SETTINGS_PATH : MANAGED_SETTINGS_PATH);
1391
+
1392
+ function readJson(file) {
1393
+ try {
1394
+ return JSON.parse(fs.readFileSync(file, 'utf-8'));
1395
+ } catch {
1396
+ return null;
1397
+ }
1398
+ }
1399
+
1400
+ // Managed settings override user settings; project and local settings are out of the user scope.
1401
+ function effectiveSetting(layers, key, fallback) {
1402
+ for (const { from, data } of layers) {
1403
+ if (data && data[key] !== undefined) return { value: data[key], from };
1404
+ }
1405
+ return { value: fallback, from: 'default' };
1406
+ }
1407
+
1408
+ function findOutputStyleFile(name) {
1409
+ const want = String(name).toLowerCase();
1410
+ const files = findMdFiles(path.join(CLAUDE_DIR, 'output-styles'));
1411
+ const byBase = files.find((f) => path.basename(f, '.md').toLowerCase() === want);
1412
+ if (byBase) return fileInfo(byBase);
1413
+ for (const file of files) {
1414
+ const info = fileInfo(file);
1415
+ if (info && fmName(info).toLowerCase() === want) return info;
1416
+ }
1417
+ return null;
1418
+ }
1419
+
1420
+ function discoverHome() {
1421
+ const managedPath = managedSettingsPath();
1422
+ const managed = readJson(managedPath);
1423
+ const user = readJson(USER_SETTINGS_PATH);
1424
+ const layers = [
1425
+ { from: 'managed', data: managed },
1426
+ { from: 'user', data: user },
1427
+ ];
1428
+ const style = effectiveSetting(layers, 'outputStyle', 'default');
1429
+ const styleFile = style.from === 'default' ? null : findOutputStyleFile(style.value);
1430
+ const envLayers = [
1431
+ { from: 'managed', data: managed?.env },
1432
+ { from: 'user', data: user?.env },
1433
+ { from: 'env', data: process.env },
1434
+ ];
1435
+ return {
1436
+ settingsPaths: { user: USER_SETTINGS_PATH, managed: managedPath },
1437
+ managed: {
1438
+ source: managedPath === REMOTE_SETTINGS_PATH ? 'server' : 'file',
1439
+ keys: managed && typeof managed === 'object' ? Object.keys(managed) : null,
1440
+ claudeMdChars: typeof managed?.claudeMd === 'string' ? managed.claudeMd.length : null,
1441
+ },
1442
+ outputStyle: {
1443
+ name: style.value,
1444
+ from: style.from,
1445
+ path: styleFile?.path || null,
1446
+ chars: styleFile?.chars ?? null,
1447
+ keepCodingInstructions: styleFile?.frontmatter?.['keep-coding-instructions'] ?? null,
1448
+ },
1449
+ settings: {
1450
+ autoMemoryEnabled: effectiveSetting(layers, 'autoMemoryEnabled', true),
1451
+ autoMemoryDirectory: effectiveSetting(layers, 'autoMemoryDirectory', null),
1452
+ claudeMdExcludes: effectiveSetting(layers, 'claudeMdExcludes', []),
1453
+ cleanupPeriodDays: effectiveSetting(layers, 'cleanupPeriodDays', 30),
1454
+ CLAUDE_CODE_DISABLE_AUTO_MEMORY: effectiveSetting(envLayers, 'CLAUDE_CODE_DISABLE_AUTO_MEMORY', null),
1455
+ },
1456
+ };
1457
+ }
1458
+
1459
+ app.get('/api/home', (_req, res) => {
1460
+ res.json(cached('home', discoverHome));
1315
1461
  });
1316
1462
 
1317
1463
  app.get('/api/stack', (_req, res) => {
@@ -1355,7 +1501,7 @@ app.get('/api/file', (req, res) => {
1355
1501
  app.get('/api/imports', (req, res) => {
1356
1502
  const filePath = req.query.path;
1357
1503
  if (!filePath) return res.status(400).json({ error: 'path query param required' });
1358
- const maxDepth = 5;
1504
+ const maxDepth = 4;
1359
1505
  const chain = [];
1360
1506
  const visited = new Set();
1361
1507
 
@@ -1364,7 +1510,7 @@ app.get('/api/imports', (req, res) => {
1364
1510
  visited.add(fp);
1365
1511
  const info = fileInfo(fp);
1366
1512
  if (!info) { chain.push({ path: fp, error: 'not found' }); return; }
1367
- const imports = parseImports(info.content);
1513
+ const { imports } = parseImports(info.content);
1368
1514
  const node = { path: fp, lines: info.lines, bytes: info.bytes, imports: [] };
1369
1515
  chain.push(node);
1370
1516
  for (const imp of imports) {