ccdesk 0.2.2 → 0.2.3

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 (130) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/README.md +6 -3
  3. package/dist/core/agent-edit.d.ts +6 -0
  4. package/dist/{server → core}/agent-edit.js +15 -21
  5. package/dist/core/backups.d.ts +19 -0
  6. package/dist/{server → core}/backups.js +37 -45
  7. package/dist/core/core.d.ts +96 -0
  8. package/dist/core/core.js +57 -0
  9. package/dist/core/index.d.ts +22 -0
  10. package/dist/core/index.js +38 -0
  11. package/dist/core/mcp-edit.d.ts +25 -0
  12. package/dist/{server → core}/mcp-edit.js +115 -98
  13. package/dist/core/mutations.d.ts +20 -0
  14. package/dist/{server → core}/mutations.js +8 -15
  15. package/dist/core/open.d.ts +31 -0
  16. package/dist/{server → core}/open.js +39 -12
  17. package/dist/core/parse-edit.d.ts +12 -0
  18. package/dist/core/parse-edit.js +64 -0
  19. package/dist/core/reference.d.ts +3 -0
  20. package/dist/core/reference.js +11 -0
  21. package/dist/core/scan/compare.d.ts +7 -0
  22. package/dist/{server → core}/scan/compare.js +2 -2
  23. package/dist/core/scan/context.d.ts +13 -0
  24. package/dist/{server → core}/scan/context.js +55 -32
  25. package/dist/core/scan/data.d.ts +8 -0
  26. package/dist/core/scan/data.js +18 -0
  27. package/dist/core/scan/env.d.ts +26 -0
  28. package/dist/{server → core}/scan/env.js +7 -3
  29. package/dist/core/scan/files.d.ts +68 -0
  30. package/dist/core/scan/files.js +270 -0
  31. package/dist/core/scan/git.d.ts +14 -0
  32. package/dist/core/scan/git.js +98 -0
  33. package/dist/core/scan/index.d.ts +5 -0
  34. package/dist/{server → core}/scan/index.js +115 -81
  35. package/dist/core/scan/instructions.d.ts +54 -0
  36. package/dist/{server → core}/scan/instructions.js +141 -73
  37. package/dist/core/scan/inventory.d.ts +38 -0
  38. package/dist/{server → core}/scan/inventory.js +134 -115
  39. package/dist/core/scan/mcp.d.ts +28 -0
  40. package/dist/{server → core}/scan/mcp.js +151 -94
  41. package/dist/core/scan/memory.d.ts +8 -0
  42. package/dist/{server → core}/scan/memory.js +8 -8
  43. package/dist/core/scan/path-test.d.ts +9 -0
  44. package/dist/core/scan/path-test.js +156 -0
  45. package/dist/core/scan/permissions.d.ts +31 -0
  46. package/dist/{server → core}/scan/permissions.js +5 -8
  47. package/dist/core/scan/plugins.d.ts +39 -0
  48. package/dist/{server → core}/scan/plugins.js +36 -27
  49. package/dist/core/scan/projects.d.ts +9 -0
  50. package/dist/{server → core}/scan/projects.js +5 -4
  51. package/dist/core/scan/sandbox.d.ts +14 -0
  52. package/dist/{server → core}/scan/sandbox.js +87 -59
  53. package/dist/core/scan/settings.d.ts +35 -0
  54. package/dist/{server → core}/scan/settings.js +88 -92
  55. package/dist/core/scan/tools.d.ts +10 -0
  56. package/dist/{server → core}/scan/tools.js +28 -20
  57. package/dist/core/scan/usage.d.ts +25 -0
  58. package/dist/{server → core}/scan/usage.js +117 -95
  59. package/dist/core/schema.d.ts +8 -0
  60. package/dist/core/schema.js +67 -0
  61. package/dist/core/secrets.d.ts +44 -0
  62. package/dist/core/secrets.js +187 -0
  63. package/dist/core/settings-edit.d.ts +9 -0
  64. package/dist/{server → core}/settings-edit.js +91 -112
  65. package/dist/core/watch.d.ts +45 -0
  66. package/dist/{server → core}/watch.js +54 -17
  67. package/dist/core/write.d.ts +119 -0
  68. package/dist/{server → core}/write.js +63 -8
  69. package/dist/server/args.d.ts +32 -0
  70. package/dist/server/args.js +106 -0
  71. package/dist/server/cli.d.ts +2 -0
  72. package/dist/server/cli.js +58 -83
  73. package/dist/server/commands.d.ts +8 -0
  74. package/dist/server/commands.js +39 -0
  75. package/dist/server/server.d.ts +12 -0
  76. package/dist/server/server.js +80 -195
  77. package/dist/server/start.d.ts +13 -0
  78. package/dist/server/start.js +21 -0
  79. package/dist/shared/compare.d.ts +25 -0
  80. package/dist/shared/memory.d.ts +2 -0
  81. package/dist/shared/requests.d.ts +104 -0
  82. package/dist/shared/requests.js +73 -0
  83. package/dist/shared/scopes.d.ts +10 -0
  84. package/dist/shared/settings-catalog.json +2199 -0
  85. package/dist/shared/types.d.ts +530 -0
  86. package/dist/shared/types.js +11 -0
  87. package/dist/ui/assets/index-CH1HoC38.js +60 -0
  88. package/dist/ui/index.html +1 -1
  89. package/dist/view/diff.d.ts +16 -0
  90. package/dist/view/diff.js +85 -0
  91. package/dist/view/format.d.ts +17 -0
  92. package/dist/view/format.js +47 -0
  93. package/dist/view/index.d.ts +11 -0
  94. package/dist/view/index.js +13 -0
  95. package/dist/view/lines.d.ts +9 -0
  96. package/dist/view/lines.js +12 -0
  97. package/dist/view/path-test.d.ts +3 -0
  98. package/dist/view/path-test.js +7 -0
  99. package/dist/view/sections/catalog.d.ts +9 -0
  100. package/dist/view/sections/catalog.js +47 -0
  101. package/dist/view/sections/extensions.d.ts +1 -0
  102. package/dist/view/sections/extensions.js +48 -0
  103. package/dist/view/sections/hooks.d.ts +1 -0
  104. package/dist/view/sections/hooks.js +32 -0
  105. package/dist/view/sections/index.d.ts +6 -0
  106. package/dist/view/sections/index.js +27 -0
  107. package/dist/view/sections/instructions.d.ts +6 -0
  108. package/dist/view/sections/instructions.js +38 -0
  109. package/dist/view/sections/mcp.d.ts +3 -0
  110. package/dist/view/sections/mcp.js +38 -0
  111. package/dist/view/sections/memory.d.ts +1 -0
  112. package/dist/view/sections/memory.js +31 -0
  113. package/dist/view/sections/permissions.d.ts +17 -0
  114. package/dist/view/sections/permissions.js +87 -0
  115. package/dist/view/sections/settings.d.ts +1 -0
  116. package/dist/view/sections/settings.js +33 -0
  117. package/dist/view/sections/tools.d.ts +6 -0
  118. package/dist/view/sections/tools.js +57 -0
  119. package/dist/view/sections/types.d.ts +123 -0
  120. package/dist/view/sections/types.js +10 -0
  121. package/dist/view/settings.d.ts +12 -0
  122. package/dist/view/settings.js +31 -0
  123. package/dist/view/usage.d.ts +8 -0
  124. package/dist/view/usage.js +18 -0
  125. package/package.json +13 -2
  126. package/dist/server/scan/files.js +0 -480
  127. package/dist/server/scan/path-test.js +0 -136
  128. package/dist/server/schema.js +0 -15
  129. package/dist/server/secrets.js +0 -63
  130. package/dist/ui/assets/index-Dg9dxngR.js +0 -60
@@ -4,20 +4,19 @@ import { storeDirName } from "../../shared/memory.js";
4
4
  import {} from "../../shared/types.js";
5
5
  import { buildContext } from "./context.js";
6
6
  import { isConfigHome } from "./env.js";
7
- import { gitRoot, isDir, isFile, isRecord, mainWorktreeRoot, readJsonObject, stringsIn, visibilityOf, } from "./files.js";
7
+ import { isDir, isFile, readJsonObject, stringsIn } from "./files.js";
8
+ import { isRecord } from "./data.js";
9
+ import { gitRoot, mainWorktreeRoot, trustKey, visibilityOf } from "./git.js";
8
10
  import { AGENTS_MD_MODES, resolveInstructions } from "./instructions.js";
9
11
  import { readInventory } from "./inventory.js";
10
12
  import { readMemoryStore } from "./memory.js";
11
13
  import { permissionsOf, trustWaits } from "./permissions.js";
12
14
  import { listProjects } from "./projects.js";
13
- import { effectiveSettings, readSettingsLayers } from "./settings.js";
15
+ import { effectiveSettings, readManagedSettings, readSettingsLayers } from "./settings.js";
14
16
  import { builtinTools } from "./tools.js";
15
- async function readClaudeJson(env) {
16
- const result = await readJsonObject(env, env.claudeJson);
17
- return result.ok ? result.data : null;
18
- }
19
17
  export async function scanOverview(env, launchDir) {
20
- const claudeJson = await readClaudeJson(env);
18
+ const result = await readJsonObject(env, env.claudeJson);
19
+ const claudeJson = result.ok ? result.data : null;
21
20
  const projects = await listProjects(env, claudeJson, launchDir);
22
21
  const hashesByName = new Map();
23
22
  const memoryCatalog = [];
@@ -53,26 +52,30 @@ export async function scanOverview(env, launchDir) {
53
52
  export async function scanDirectory(env, dir, profile = "interactive") {
54
53
  const path = resolve(dir);
55
54
  const exists = await isDir(path);
56
- const claudeJson = await readClaudeJson(env);
55
+ const problems = [];
56
+ const claudeJson = await readClaudeJson(env, problems);
57
57
  const root = exists ? await gitRoot(path) : null;
58
- const settings = await readSettingsLayers(env, path);
58
+ const managed = await readManagedSettings(env);
59
+ const settings = await readSettingsLayers(env, path, managed);
60
+ for (const l of settings)
61
+ if (l.error)
62
+ problems.push({
63
+ path: l.path,
64
+ error: l.error,
65
+ ...(l.blocksStart ? { blocksStart: true } : {}),
66
+ });
59
67
  const effective = effectiveSettings(settings);
60
68
  const mode = agentsMdMode(settings);
61
69
  const excludes = effective.find((s) => s.key === "claudeMdExcludes")?.value;
62
70
  const mainRoot = exists ? await mainWorktreeRoot(path) : null;
63
- const trust = trustOf(env, claudeJson, path, mainRoot ?? root);
64
- const managedLayer = settings.find((l) => l.scope === "managed");
65
- const managedClaudeMd = managedLayer?.data?.claudeMd;
66
- const problems = [];
71
+ const trust = trustOf(env, claudeJson, path, mainRoot, root);
67
72
  const instructions = await resolveInstructions({
68
73
  env,
69
74
  cwd: path,
70
75
  mode,
71
76
  excludes: stringsIn(excludes),
72
77
  externalImports: trust.externalImports,
73
- managedClaudeMd: managedLayer && typeof managedClaudeMd === "string"
74
- ? { path: `${managedLayer.path}#claudeMd`, text: managedClaudeMd }
75
- : null,
78
+ managedClaudeMd: managedClaudeMd(settings),
76
79
  problems,
77
80
  });
78
81
  const inventory = await readInventory({
@@ -80,67 +83,23 @@ export async function scanDirectory(env, dir, profile = "interactive") {
80
83
  projectDir: path,
81
84
  gitRoot: root,
82
85
  layers: settings,
86
+ effective,
87
+ managed: managed.data,
83
88
  claudeJson,
84
89
  trust,
85
90
  profile,
91
+ problems,
86
92
  });
87
- // Every project file that shapes Claude here, so the UI can say who sees each one.
88
- const mcpJson = join(path, ".mcp.json");
89
- const projectFiles = [
90
- ...instructions.filter((i) => i.scope === "project" || i.scope === "local").map((i) => i.path),
91
- ...settings
92
- .filter((s) => s.exists && (s.scope === "project" || s.scope === "local"))
93
- .map((s) => s.path),
94
- ...((await isFile(mcpJson)) ? [mcpJson] : []),
95
- ...[...inventory.agents, ...inventory.skills, ...inventory.commands, ...inventory.outputStyles]
96
- .filter((e) => e.scope === "project" || e.scope === "local")
97
- .map((e) => e.path),
98
- ];
99
- const visibility = await visibilityOf(root, projectFiles);
100
- for (const item of [...instructions, ...settings]) {
101
- const v = visibility.get(item.path);
102
- if (v)
103
- item.visibility = v;
104
- }
105
- let sharing = null;
106
- if (root) {
107
- sharing = { tracked: [], untracked: [], ignored: [] };
108
- for (const file of new Set(projectFiles)) {
109
- const v = visibility.get(file);
110
- // Shown as repository paths, with forward slashes on every platform.
111
- const rel = relative(root, file).replaceAll("\\", "/");
112
- if (v === "tracked")
113
- sharing.tracked.push(rel);
114
- else if (v === "untracked")
115
- sharing.untracked.push(rel);
116
- else if (v === "ignored")
117
- sharing.ignored.push(rel);
118
- }
119
- }
120
- // A local file tracked in git, or in a .claude that is a link, counts as the repository's
121
- // (docs: permissions#when-your-local-settings-file-needs-trust).
122
- const localFromRepository = (await Promise.all(settings
123
- .filter((l) => l.scope === "local" && l.exists)
124
- .map(async (l) => l.visibility === "tracked" ||
125
- ((await lstat(dirname(l.path)).catch(() => null))?.isSymbolicLink() ?? false)))).some(Boolean);
93
+ const sharing = await shareProjectFiles(path, root, instructions, settings, inventory);
126
94
  const trustOptions = {
127
95
  trust,
128
96
  inConfigHome: isConfigHome(env, path),
129
- localFromRepository,
97
+ // Reads the visibility shareProjectFiles gave each layer.
98
+ localFromRepository: await localFromRepository(settings),
130
99
  profile,
131
100
  };
132
- const memory = await readMemory(env, mainRoot, path, effective, trustOptions);
101
+ const memory = await readMemory(env, mainRoot, path, effective, trustOptions, problems);
133
102
  const permissions = permissionsOf(settings, trustOptions);
134
- for (const l of settings)
135
- if (l.error)
136
- problems.push({
137
- path: l.path,
138
- error: l.error,
139
- ...(l.blocksStart ? { blocksStart: true } : {}),
140
- });
141
- const mcp = await readJsonObject(env, mcpJson);
142
- if (!mcp.ok && !mcp.missing)
143
- problems.push({ path: mcpJson, error: mcp.error });
144
103
  return {
145
104
  path,
146
105
  profile,
@@ -163,32 +122,95 @@ export async function scanDirectory(env, dir, profile = "interactive") {
163
122
  permissions,
164
123
  tools: builtinTools(permissions, inventory.hooks),
165
124
  trust,
166
- problems,
125
+ // One line per file: a file read twice, such as a rule that a CLAUDE.md also imports, is one problem.
126
+ problems: problems.filter((p, i) => problems.findIndex((q) => q.path === p.path) === i),
167
127
  sharing,
168
128
  };
169
129
  }
130
+ /** ~/.claude.json, or null when it is missing or broken; a broken one is a problem. */
131
+ async function readClaudeJson(env, problems) {
132
+ const file = await readJsonObject(env, env.claudeJson);
133
+ if (file.ok)
134
+ return file.data;
135
+ // Without it the folder looks untrusted and your servers missing, so the problem says why.
136
+ if (!file.missing)
137
+ problems.push({
138
+ path: env.claudeJson,
139
+ error: `${file.error}, so ccdesk cannot show your trust answers or your user and local MCP servers`,
140
+ });
141
+ return null;
142
+ }
143
+ /** The CLAUDE.md text managed settings give in their claudeMd key. */
144
+ function managedClaudeMd(settings) {
145
+ const managed = settings.find((l) => l.scope === "managed");
146
+ const text = managed?.data?.claudeMd;
147
+ return managed && typeof text === "string" ? { path: `${managed.path}#claudeMd`, text } : null;
148
+ }
149
+ /**
150
+ * Says who sees each project file that shapes Claude here: marks the instructions and settings
151
+ * with their git visibility, and returns the repository's files by visibility, or null outside one.
152
+ */
153
+ async function shareProjectFiles(path, root, instructions, settings, inventory) {
154
+ const inProject = (scope) => scope === "project" || scope === "local";
155
+ const mcpJson = join(path, ".mcp.json");
156
+ const projectFiles = [
157
+ ...instructions.filter((i) => inProject(i.scope)).map((i) => i.path),
158
+ ...settings.filter((s) => s.exists && inProject(s.scope)).map((s) => s.path),
159
+ ...((await isFile(mcpJson)) ? [mcpJson] : []),
160
+ ...[...inventory.agents, ...inventory.skills, ...inventory.commands, ...inventory.outputStyles]
161
+ .filter((e) => inProject(e.scope))
162
+ .map((e) => e.path),
163
+ ];
164
+ const visibility = await visibilityOf(root, projectFiles);
165
+ for (const item of [...instructions, ...settings]) {
166
+ const v = visibility.get(item.path);
167
+ if (v)
168
+ item.visibility = v;
169
+ }
170
+ if (!root)
171
+ return null;
172
+ const sharing = { tracked: [], untracked: [], ignored: [] };
173
+ for (const file of new Set(projectFiles)) {
174
+ const v = visibility.get(file);
175
+ // Shown as repository paths, with forward slashes on every platform.
176
+ const rel = relative(root, file).replaceAll("\\", "/");
177
+ if (v === "tracked")
178
+ sharing.tracked.push(rel);
179
+ else if (v === "untracked")
180
+ sharing.untracked.push(rel);
181
+ else if (v === "ignored")
182
+ sharing.ignored.push(rel);
183
+ }
184
+ return sharing;
185
+ }
186
+ /**
187
+ * Whether the local settings file counts as the repository's: tracked in git, or in a .claude
188
+ * that is a link (docs: permissions#when-your-local-settings-file-needs-trust).
189
+ */
190
+ async function localFromRepository(settings) {
191
+ const fromRepository = await Promise.all(settings
192
+ .filter((l) => l.scope === "local" && l.exists)
193
+ .map(async (l) => l.visibility === "tracked" ||
194
+ ((await lstat(dirname(l.path)).catch(() => null))?.isSymbolicLink() ?? false)));
195
+ return fromRepository.some(Boolean);
196
+ }
170
197
  /**
171
198
  * Your answers to Claude Code's trust dialogs, from ~/.claude.json (docs: permissions#project-allow-rules-and-workspace-trust).
172
199
  * In a repository the key is the main checkout's root; outside one it is the folder, and a trusted
173
200
  * parent folder covers it. In your home directory trust lasts one session and is never saved.
174
201
  */
175
- function trustOf(env, claudeJson, path, repositoryRoot) {
202
+ function trustOf(env, claudeJson, path, mainRoot, repoRoot) {
176
203
  const projects = isRecord(claudeJson?.projects) ? claudeJson.projects : {};
177
204
  const entry = (key) => (isRecord(projects[key]) ? projects[key] : {});
178
- const root = repositoryRoot ?? path;
205
+ const root = trustKey(path, mainRoot, repoRoot);
179
206
  const own = entry(root);
180
207
  const trust = {
181
208
  root,
182
209
  trusted: own.hasTrustDialogAccepted === true,
183
210
  sessionOnly: path === env.home,
184
- // These two keys are not documented; they are what Claude Code writes for the import dialog.
185
- externalImports: own.hasClaudeMdExternalIncludesApproved === true
186
- ? "approved"
187
- : own.hasClaudeMdExternalIncludesWarningShown === true
188
- ? "declined"
189
- : "not-asked",
211
+ externalImports: importAnswer(own),
190
212
  };
191
- if (!trust.trusted && !repositoryRoot)
213
+ if (!trust.trusted && !mainRoot && !repoRoot)
192
214
  for (let d = dirname(path); d !== dirname(d); d = dirname(d))
193
215
  if (entry(d).hasTrustDialogAccepted === true) {
194
216
  trust.inheritedFrom = d;
@@ -196,12 +218,23 @@ function trustOf(env, claudeJson, path, repositoryRoot) {
196
218
  }
197
219
  return trust;
198
220
  }
221
+ /**
222
+ * Your answer to the dialog about imports from outside the project. These two keys are not
223
+ * documented; they are what Claude Code writes for that dialog.
224
+ */
225
+ function importAnswer(project) {
226
+ if (project.hasClaudeMdExternalIncludesApproved === true)
227
+ return "approved";
228
+ if (project.hasClaudeMdExternalIncludesWarningShown === true)
229
+ return "declined";
230
+ return "not-asked";
231
+ }
199
232
  /**
200
233
  * The auto memory Claude Code uses here (docs: memory#storage-location, settings-reference#automemoryenabled).
201
234
  * Worktrees and subdirectories of one repository share one store. The store is returned even
202
235
  * when its folder does not exist yet, so a note about memory being off still shows.
203
236
  */
204
- async function readMemory(env, mainRoot, path, effective, trustOptions) {
237
+ async function readMemory(env, mainRoot, path, effective, trustOptions, problems) {
205
238
  const key = mainRoot ?? path;
206
239
  const setting = effective.find((s) => s.key === "autoMemoryDirectory");
207
240
  // A value from a repository file counts once the folder is trusted, like hooks (docs:
@@ -216,7 +249,7 @@ async function readMemory(env, mainRoot, path, effective, trustOptions) {
216
249
  const customDir = typeof custom?.value === "string" ? expandHome(env, custom.value) : null;
217
250
  const storeName = env.projectDirName ?? storeDirName(key);
218
251
  const dir = customDir ?? join(env.claudeDir, "projects", storeName, "memory");
219
- const store = (await readMemoryStore(env, dir, key)) ?? {
252
+ const store = (await readMemoryStore(env, dir, key, problems)) ?? {
220
253
  dir,
221
254
  loaded: true,
222
255
  projectPath: key,
@@ -272,8 +305,9 @@ function agentsMdMode(layers) {
272
305
  for (const id of ["cc-plugin-agents-md@builtin", "agents-md@builtin"]) {
273
306
  const entry = configs[id];
274
307
  const value = isRecord(entry) && isRecord(entry.options) ? entry.options.instructionFiles : undefined;
275
- if (typeof value === "string" && AGENTS_MD_MODES.includes(value))
276
- return value;
308
+ const mode = AGENTS_MD_MODES.find((m) => m === value);
309
+ if (mode)
310
+ return mode;
277
311
  }
278
312
  }
279
313
  return "claude-md-or-agents-md";
@@ -0,0 +1,54 @@
1
+ import { type AgentsMdMode, type Instruction, type Problem, type TextFile, type Trust } from "../../shared/types.ts";
2
+ import { type ScanEnv } from "./env.ts";
3
+ export declare const AGENTS_MD_MODES: AgentsMdMode[];
4
+ /** The CLAUDE.md and AGENTS.md files each folder can hold, in the order Claude Code reads them. */
5
+ export declare const CLAUDE_MD_NAMES: string[];
6
+ export declare const AGENTS_MD_NAMES: string[];
7
+ /**
8
+ * Whether AGENTS.md loads, given the instruction mode and whether a CLAUDE.md or CLAUDE.local.md
9
+ * is there (docs: memory#choose-which-instruction-files-load).
10
+ */
11
+ export declare function agentsMdLoads(mode: AgentsMdMode, hasClaudeMd: boolean): boolean;
12
+ /**
13
+ * Whether a claudeMdExcludes glob matches a file by its own path or, for a link, its target's
14
+ * (docs: memory#exclude-specific-claude-md-files).
15
+ */
16
+ export declare function isExcluded(excludes: string[], path: string, realPath?: string): boolean;
17
+ /**
18
+ * Where a rule in `dir` really is when that is outside both the project and `dir`: a link's
19
+ * target, or the file a linked folder leads to. Undefined for a rule that stays inside.
20
+ */
21
+ export declare function ruleTarget(file: TextFile, dir: string, realCwd: string): Promise<string | undefined>;
22
+ /**
23
+ * Why a project rule linked from outside the project does not load, or null when it does: it waits
24
+ * for your approval of outside imports, and once approved loads only without a paths field
25
+ * (docs: memory#share-rules-across-projects-with-symlinks).
26
+ */
27
+ export declare function linkedRuleBlock(target: string | undefined, realCwd: string, approved: boolean, hasPaths: boolean): "needs-approval" | "has-paths" | null;
28
+ /** Directories from the filesystem root down to `cwd`, the order Claude Code concatenates them in. */
29
+ export declare function ancestorsRootFirst(cwd: string): string[];
30
+ interface ResolveOptions {
31
+ env: ScanEnv;
32
+ cwd: string;
33
+ mode: AgentsMdMode;
34
+ /** Merged `claudeMdExcludes` globs from every settings layer. */
35
+ excludes: string[];
36
+ /** Your answer to Claude Code's dialog about imports from outside the project. */
37
+ externalImports: Trust["externalImports"];
38
+ /** The managed settings' `claudeMd` text, which loads like a managed CLAUDE.md. */
39
+ managedClaudeMd: {
40
+ path: string;
41
+ text: string;
42
+ } | null;
43
+ /** Receives instruction files and imports that exist but cannot be read. */
44
+ problems: Problem[];
45
+ }
46
+ /**
47
+ * The instruction files Claude Code reads in `cwd`, in load order, each with whether it loads at
48
+ * session start, when Claude works with matching files, or not at all, and why.
49
+ * Follows code.claude.com/docs/en/memory.
50
+ */
51
+ export declare function resolveInstructions(options: ResolveOptions): Promise<Instruction[]>;
52
+ /** An invalid pattern matches nothing. */
53
+ export declare function safeMatch(path: string, glob: string): boolean;
54
+ export {};
@@ -1,9 +1,8 @@
1
- import { constants } from "node:fs";
2
- import { access, stat } from "node:fs/promises";
3
1
  import { dirname, isAbsolute, join, matchesGlob, parse, resolve } from "node:path";
4
2
  import {} from "../../shared/types.js";
5
3
  import {} from "./env.js";
6
- import { isFile, isNetworkPath, isWithin, MAX_TEXT_BYTES, parseFrontmatter, readTextFile, resolvedPath, toStringList, walkMarkdown, } from "./files.js";
4
+ import { isFile, isNetworkPath, isWithin, MAX_TEXT_BYTES, readTextFile, resolvedPath, toStringList, walkMarkdown, } from "./files.js";
5
+ import { parseFrontmatter } from "./data.js";
7
6
  const MAX_IMPORT_HOPS = 4;
8
7
  export const AGENTS_MD_MODES = [
9
8
  "claude-md-or-agents-md",
@@ -11,6 +10,45 @@ export const AGENTS_MD_MODES = [
11
10
  "claude-md",
12
11
  "managed-only",
13
12
  ];
13
+ /** The CLAUDE.md and AGENTS.md files each folder can hold, in the order Claude Code reads them. */
14
+ export const CLAUDE_MD_NAMES = ["CLAUDE.md", ".claude/CLAUDE.md"];
15
+ export const AGENTS_MD_NAMES = ["AGENTS.md", ".claude/AGENTS.md"];
16
+ /**
17
+ * Whether AGENTS.md loads, given the instruction mode and whether a CLAUDE.md or CLAUDE.local.md
18
+ * is there (docs: memory#choose-which-instruction-files-load).
19
+ */
20
+ export function agentsMdLoads(mode, hasClaudeMd) {
21
+ return mode === "claude-md-and-agents-md" || (mode === "claude-md-or-agents-md" && !hasClaudeMd);
22
+ }
23
+ /**
24
+ * Whether a claudeMdExcludes glob matches a file by its own path or, for a link, its target's
25
+ * (docs: memory#exclude-specific-claude-md-files).
26
+ */
27
+ export function isExcluded(excludes, path, realPath) {
28
+ return excludes.some((glob) => [path, realPath].some((p) => p !== undefined && safeMatch(p, glob)));
29
+ }
30
+ /**
31
+ * Where a rule in `dir` really is when that is outside both the project and `dir`: a link's
32
+ * target, or the file a linked folder leads to. Undefined for a rule that stays inside.
33
+ */
34
+ export async function ruleTarget(file, dir, realCwd) {
35
+ if (file.realPath)
36
+ return file.realPath;
37
+ const real = await resolvedPath(file.path);
38
+ return isWithin(realCwd, real) || isWithin(await resolvedPath(dir), real) ? undefined : real;
39
+ }
40
+ /**
41
+ * Why a project rule linked from outside the project does not load, or null when it does: it waits
42
+ * for your approval of outside imports, and once approved loads only without a paths field
43
+ * (docs: memory#share-rules-across-projects-with-symlinks).
44
+ */
45
+ export function linkedRuleBlock(target, realCwd, approved, hasPaths) {
46
+ if (target === undefined || isWithin(realCwd, target))
47
+ return null;
48
+ if (!approved)
49
+ return "needs-approval";
50
+ return hasPaths ? "has-paths" : null;
51
+ }
14
52
  /** Directories from the filesystem root down to `cwd`, the order Claude Code concatenates them in. */
15
53
  export function ancestorsRootFirst(cwd) {
16
54
  const dirs = [];
@@ -31,25 +69,31 @@ export function ancestorsRootFirst(cwd) {
31
69
  */
32
70
  export async function resolveInstructions(options) {
33
71
  const { env, cwd, mode, excludes, externalImports, managedClaudeMd, problems } = options;
34
- /** A file that exists but cannot be read is reported, not taken for a missing one. */
35
- const readInstruction = async (path) => {
36
- const file = await readTextFile(env, path);
37
- if (file || !(await stat(path).catch(() => null))?.isFile())
38
- return file;
39
- const denied = await access(path, constants.R_OK).then(() => null, (error) => error.code ?? String(error));
40
- if (denied)
41
- problems.push({ path, error: `cannot be read (${denied})` });
42
- return null;
43
- };
44
72
  const ctx = {
45
73
  realCwd: await resolvedPath(resolve(cwd)),
46
74
  env,
47
75
  approved: externalImports === "approved",
76
+ problems,
48
77
  };
49
- const managedOnly = mode === "managed-only";
50
- const skippedByMode = `Skipped: instruction mode is ${mode}`;
78
+ const out = await readOwnFiles(ctx, managedClaudeMd);
79
+ const { claudeFiles, agentsFiles } = await readDirectoryFiles(ctx, cwd);
80
+ explainClaudeFiles(claudeFiles, cwd);
81
+ explainAgentsFiles(agentsFiles, claudeFiles, mode);
82
+ for (const dir of ancestorsRootFirst(cwd))
83
+ out.push(...claudeFiles.filter((f) => f.dir === dir), ...agentsFiles.filter((f) => f.dir === dir));
84
+ applyLoadRules(out, ctx, mode, excludes, externalImports);
85
+ addChecks(out);
86
+ return out;
87
+ }
88
+ /** A file that exists but cannot be read is reported, not taken for a missing one. */
89
+ function readInstruction(ctx, path) {
90
+ return readTextFile(ctx.env, path, ctx.problems);
91
+ }
92
+ /** The managed and user files, which come first and do not depend on the folder. */
93
+ async function readOwnFiles(ctx, managedClaudeMd) {
94
+ const { env } = ctx;
51
95
  const out = [];
52
- const managed = await readInstruction(join(env.managedDir, "CLAUDE.md"));
96
+ const managed = await readInstruction(ctx, join(env.managedDir, "CLAUDE.md"));
53
97
  if (managed)
54
98
  out.push(await instruction(managed, "claude-md", "managed", env.managedDir, ctx, "Organisation policy, always loaded"));
55
99
  if (managedClaudeMd) {
@@ -65,69 +109,94 @@ export async function resolveInstructions(options) {
65
109
  entry.imports = [];
66
110
  out.push(entry);
67
111
  }
68
- const user = await readInstruction(join(env.claudeDir, "CLAUDE.md"));
112
+ const user = await readInstruction(ctx, join(env.claudeDir, "CLAUDE.md"));
69
113
  if (user)
70
114
  out.push(await instruction(user, "claude-md", "user", env.claudeDir, ctx, "Your instructions for every project"));
71
115
  for (const path of await walkMarkdown(join(env.claudeDir, "rules"))) {
72
- const file = await readInstruction(path);
116
+ const file = await readInstruction(ctx, path);
73
117
  if (file)
74
118
  out.push(await rule(file, "user", env.claudeDir, ctx));
75
119
  }
76
- // Per directory, root first: CLAUDE.md, .claude/CLAUDE.md, rules, then CLAUDE.local.md last
77
- // ("your personal notes are the last thing Claude reads at that level"), then AGENTS.md.
120
+ return out;
121
+ }
122
+ /**
123
+ * The project and local files of `cwd` and every folder above it. Per directory, root first:
124
+ * CLAUDE.md, .claude/CLAUDE.md, rules, then CLAUDE.local.md last ("your personal notes are the last
125
+ * thing Claude reads at that level"), then AGENTS.md.
126
+ */
127
+ async function readDirectoryFiles(ctx, cwd) {
78
128
  const claudeFiles = [];
79
129
  const agentsFiles = [];
80
130
  for (const dir of ancestorsRootFirst(cwd)) {
81
- // ~/.claude/CLAUDE.md and ~/.claude/rules are the user files above, not the home's project files.
82
- const isConfigHome = join(dir, ".claude") === env.claudeDir;
83
- for (const name of ["CLAUDE.md", ".claude/CLAUDE.md"]) {
84
- if (isConfigHome && name === ".claude/CLAUDE.md")
85
- continue;
86
- const file = await readInstruction(join(dir, name));
87
- if (file)
88
- claudeFiles.push(await instruction(file, "claude-md", "project", dir, ctx, ""));
89
- }
90
- if (!isConfigHome && dir !== env.claudeDir)
91
- for (const path of await walkMarkdown(join(dir, ".claude", "rules"))) {
92
- const file = await readInstruction(path);
93
- if (file)
94
- claudeFiles.push(await rule(file, "project", dir, ctx));
95
- }
96
- const local = await readInstruction(join(dir, "CLAUDE.local.md"));
97
- if (local)
98
- claudeFiles.push(await instruction(local, "claude-local-md", "local", dir, ctx, ""));
99
- for (const name of ["AGENTS.md", ".claude/AGENTS.md"]) {
100
- const file = await readInstruction(join(dir, name));
131
+ claudeFiles.push(...(await readClaudeFiles(ctx, dir)));
132
+ for (const name of AGENTS_MD_NAMES) {
133
+ const file = await readInstruction(ctx, join(dir, name));
101
134
  if (file)
102
135
  agentsFiles.push(await instruction(file, "agents-md", "project", dir, ctx, ""));
103
136
  }
104
137
  }
105
- const hasClaudeMd = claudeFiles.some((f) => f.kind !== "rule");
138
+ return { claudeFiles, agentsFiles };
139
+ }
140
+ /** One folder's CLAUDE.md files, rules and CLAUDE.local.md, in the order Claude Code reads them. */
141
+ async function readClaudeFiles(ctx, dir) {
142
+ const { env } = ctx;
143
+ const out = [];
144
+ // ~/.claude/CLAUDE.md and ~/.claude/rules are the user files above, not the home's project files.
145
+ const holdsClaudeDir = join(dir, ".claude") === env.claudeDir;
146
+ for (const name of CLAUDE_MD_NAMES) {
147
+ if (holdsClaudeDir && name === ".claude/CLAUDE.md")
148
+ continue;
149
+ const file = await readInstruction(ctx, join(dir, name));
150
+ if (file)
151
+ out.push(await instruction(file, "claude-md", "project", dir, ctx, ""));
152
+ }
153
+ if (!holdsClaudeDir && dir !== env.claudeDir)
154
+ for (const path of await walkMarkdown(join(dir, ".claude", "rules"))) {
155
+ const file = await readInstruction(ctx, path);
156
+ if (file)
157
+ out.push(await rule(file, "project", dir, ctx));
158
+ }
159
+ const local = await readInstruction(ctx, join(dir, "CLAUDE.local.md"));
160
+ if (local)
161
+ out.push(await instruction(local, "claude-local-md", "local", dir, ctx, ""));
162
+ return out;
163
+ }
164
+ /** Why each CLAUDE.md and CLAUDE.local.md loads; rules have their reason already. */
165
+ function explainClaudeFiles(claudeFiles, cwd) {
106
166
  for (const file of claudeFiles) {
107
167
  if (file.kind === "rule")
108
168
  continue;
109
- file.reason =
110
- file.kind === "claude-local-md"
111
- ? "Your private notes for this directory"
112
- : file.dir === resolve(cwd)
113
- ? "Instructions for this directory"
114
- : "Inherited from a parent directory";
169
+ if (file.kind === "claude-local-md")
170
+ file.reason = "Your private notes for this directory";
171
+ else if (file.dir === resolve(cwd))
172
+ file.reason = "Instructions for this directory";
173
+ else
174
+ file.reason = "Inherited from a parent directory";
115
175
  }
176
+ }
177
+ /** Whether each AGENTS.md loads next to the CLAUDE.md files found, and why. */
178
+ function explainAgentsFiles(agentsFiles, claudeFiles, mode) {
179
+ const hasClaudeMd = claudeFiles.some((f) => f.kind !== "rule");
116
180
  const includedElsewhere = new Set(claudeFiles.flatMap((f) => [f.realPath ?? f.path, ...f.imports.map((i) => i.resolved)]));
117
181
  for (const file of agentsFiles) {
118
- if (mode === "managed-only" || mode === "claude-md")
119
- skip(file, skippedByMode);
182
+ if (!agentsMdLoads(mode, false))
183
+ skip(file, skippedByMode(mode));
120
184
  else if (includedElsewhere.has(file.realPath ?? file.path))
121
185
  skip(file, "Already included through a CLAUDE.md import or symlink");
186
+ else if (!agentsMdLoads(mode, hasClaudeMd))
187
+ skip(file, "Ignored: a CLAUDE.md or CLAUDE.local.md exists here or above, so Claude Code reads that instead");
122
188
  else if (mode === "claude-md-and-agents-md")
123
189
  file.reason = "Loaded alongside CLAUDE.md (instruction mode claude-md-and-agents-md)";
124
- else if (hasClaudeMd)
125
- skip(file, "Ignored: a CLAUDE.md or CLAUDE.local.md exists here or above, so Claude Code reads that instead");
126
190
  else
127
191
  file.reason = "Loaded because there is no CLAUDE.md here or above";
128
192
  }
129
- for (const dir of ancestorsRootFirst(cwd))
130
- out.push(...claudeFiles.filter((f) => f.dir === dir), ...agentsFiles.filter((f) => f.dir === dir));
193
+ }
194
+ /**
195
+ * Skips what Claude Code leaves out of every file but managed ones: all of them in managed-only
196
+ * mode, ones matched by claudeMdExcludes, ones over 4 MiB, and project rules linked from outside
197
+ * the project that do not qualify.
198
+ */
199
+ function applyLoadRules(out, ctx, mode, excludes, externalImports) {
131
200
  const waiting = externalImports === "declined"
132
201
  ? "You declined imports from outside this project"
133
202
  : "Waits for you to approve imports from outside this project";
@@ -135,28 +204,27 @@ export async function resolveInstructions(options) {
135
204
  if (file.scope === "managed")
136
205
  continue;
137
206
  // Path-scoped rules still load on demand in managed-only mode (docs: memory#choose-which-instruction-files-load).
138
- if (managedOnly && file.status === "loaded")
139
- skip(file, skippedByMode);
140
- // Managed files cannot be excluded; a linked file is excluded by its own path or its target's
141
- // (docs: memory#exclude-specific-claude-md-files).
142
- const excluded = excludes.some((glob) => [file.path, file.realPath].some((p) => p !== undefined && safeMatch(p, glob)));
143
- if (excluded)
207
+ if (mode === "managed-only" && file.status === "loaded")
208
+ skip(file, skippedByMode(mode));
209
+ // Managed files cannot be excluded.
210
+ if (isExcluded(excludes, file.path, file.realPath))
144
211
  skip(file, "Skipped by claudeMdExcludes");
145
212
  if (file.status !== "skipped" && file.bytes > MAX_TEXT_BYTES)
146
213
  skip(file, "Skipped: Claude Code does not load an instruction file over 4 MiB");
147
214
  // A project's links outside it need your approval, like its imports; your own files do not.
148
215
  if (file.scope !== "project" && file.scope !== "local")
149
216
  continue;
150
- if (file.kind === "rule" && file.realPath && !isWithin(ctx.realCwd, file.realPath)) {
151
- if (!ctx.approved)
152
- skip(file, `${waiting}; this rule links outside it`);
153
- // docs: memory#share-rules-across-projects-with-symlinks
154
- else if (file.paths)
155
- skip(file, "Links outside this project, and such a rule loads only without a paths field");
156
- }
217
+ if (file.kind !== "rule")
218
+ continue;
219
+ const block = linkedRuleBlock(file.realPath, ctx.realCwd, ctx.approved, file.paths !== undefined);
220
+ if (block === "needs-approval")
221
+ skip(file, `${waiting}; this rule links outside it`);
222
+ else if (block === "has-paths")
223
+ skip(file, "Links outside this project, and such a rule loads only without a paths field");
157
224
  }
158
- addChecks(out);
159
- return out;
225
+ }
226
+ function skippedByMode(mode) {
227
+ return `Skipped: instruction mode is ${mode}`;
160
228
  }
161
229
  function skip(file, reason) {
162
230
  if (file.status === "skipped")
@@ -165,7 +233,7 @@ function skip(file, reason) {
165
233
  file.reason = reason;
166
234
  }
167
235
  /** An invalid pattern matches nothing. */
168
- function safeMatch(path, glob) {
236
+ export function safeMatch(path, glob) {
169
237
  try {
170
238
  return matchesGlob(path, glob);
171
239
  }
@@ -213,9 +281,9 @@ async function rule(file, scope, dir, ctx) {
213
281
  entry.status = "on-match";
214
282
  }
215
283
  // A rule reached through a linked folder is not a link itself, but its target can be outside.
216
- const real = await resolvedPath(file.path);
217
- if (!entry.realPath && !isWithin(ctx.realCwd, real) && !isWithin(await resolvedPath(dir), real))
218
- entry.realPath = real;
284
+ const target = await ruleTarget(file, dir, ctx.realCwd);
285
+ if (target)
286
+ entry.realPath = target;
219
287
  return entry;
220
288
  }
221
289
  async function instruction(file, kind, scope, dir, ctx, reason) {
@@ -248,7 +316,7 @@ async function collectImports(file, ctx, readExternal) {
248
316
  continue;
249
317
  seen.add(real);
250
318
  const external = !isWithin(ctx.realCwd, real);
251
- const imported = !external || readExternal ? await readTextFile(ctx.env, target) : null;
319
+ const imported = !external || readExternal ? await readTextFile(ctx.env, target, ctx.problems) : null;
252
320
  const ref = {
253
321
  raw,
254
322
  resolved: real,