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.
- package/CHANGELOG.md +25 -0
- package/README.md +6 -3
- package/dist/core/agent-edit.d.ts +6 -0
- package/dist/{server → core}/agent-edit.js +15 -21
- package/dist/core/backups.d.ts +19 -0
- package/dist/{server → core}/backups.js +37 -45
- package/dist/core/core.d.ts +96 -0
- package/dist/core/core.js +57 -0
- package/dist/core/index.d.ts +22 -0
- package/dist/core/index.js +38 -0
- package/dist/core/mcp-edit.d.ts +25 -0
- package/dist/{server → core}/mcp-edit.js +115 -98
- package/dist/core/mutations.d.ts +20 -0
- package/dist/{server → core}/mutations.js +8 -15
- package/dist/core/open.d.ts +31 -0
- package/dist/{server → core}/open.js +39 -12
- package/dist/core/parse-edit.d.ts +12 -0
- package/dist/core/parse-edit.js +64 -0
- package/dist/core/reference.d.ts +3 -0
- package/dist/core/reference.js +11 -0
- package/dist/core/scan/compare.d.ts +7 -0
- package/dist/{server → core}/scan/compare.js +2 -2
- package/dist/core/scan/context.d.ts +13 -0
- package/dist/{server → core}/scan/context.js +55 -32
- package/dist/core/scan/data.d.ts +8 -0
- package/dist/core/scan/data.js +18 -0
- package/dist/core/scan/env.d.ts +26 -0
- package/dist/{server → core}/scan/env.js +7 -3
- package/dist/core/scan/files.d.ts +68 -0
- package/dist/core/scan/files.js +270 -0
- package/dist/core/scan/git.d.ts +14 -0
- package/dist/core/scan/git.js +98 -0
- package/dist/core/scan/index.d.ts +5 -0
- package/dist/{server → core}/scan/index.js +115 -81
- package/dist/core/scan/instructions.d.ts +54 -0
- package/dist/{server → core}/scan/instructions.js +141 -73
- package/dist/core/scan/inventory.d.ts +38 -0
- package/dist/{server → core}/scan/inventory.js +134 -115
- package/dist/core/scan/mcp.d.ts +28 -0
- package/dist/{server → core}/scan/mcp.js +151 -94
- package/dist/core/scan/memory.d.ts +8 -0
- package/dist/{server → core}/scan/memory.js +8 -8
- package/dist/core/scan/path-test.d.ts +9 -0
- package/dist/core/scan/path-test.js +156 -0
- package/dist/core/scan/permissions.d.ts +31 -0
- package/dist/{server → core}/scan/permissions.js +5 -8
- package/dist/core/scan/plugins.d.ts +39 -0
- package/dist/{server → core}/scan/plugins.js +36 -27
- package/dist/core/scan/projects.d.ts +9 -0
- package/dist/{server → core}/scan/projects.js +5 -4
- package/dist/core/scan/sandbox.d.ts +14 -0
- package/dist/{server → core}/scan/sandbox.js +87 -59
- package/dist/core/scan/settings.d.ts +35 -0
- package/dist/{server → core}/scan/settings.js +88 -92
- package/dist/core/scan/tools.d.ts +10 -0
- package/dist/{server → core}/scan/tools.js +28 -20
- package/dist/core/scan/usage.d.ts +25 -0
- package/dist/{server → core}/scan/usage.js +117 -95
- package/dist/core/schema.d.ts +8 -0
- package/dist/core/schema.js +67 -0
- package/dist/core/secrets.d.ts +44 -0
- package/dist/core/secrets.js +187 -0
- package/dist/core/settings-edit.d.ts +9 -0
- package/dist/{server → core}/settings-edit.js +91 -112
- package/dist/core/watch.d.ts +45 -0
- package/dist/{server → core}/watch.js +54 -17
- package/dist/core/write.d.ts +119 -0
- package/dist/{server → core}/write.js +63 -8
- package/dist/server/args.d.ts +32 -0
- package/dist/server/args.js +106 -0
- package/dist/server/cli.d.ts +2 -0
- package/dist/server/cli.js +58 -83
- package/dist/server/commands.d.ts +8 -0
- package/dist/server/commands.js +39 -0
- package/dist/server/server.d.ts +12 -0
- package/dist/server/server.js +80 -195
- package/dist/server/start.d.ts +13 -0
- package/dist/server/start.js +21 -0
- package/dist/shared/compare.d.ts +25 -0
- package/dist/shared/memory.d.ts +2 -0
- package/dist/shared/requests.d.ts +104 -0
- package/dist/shared/requests.js +73 -0
- package/dist/shared/scopes.d.ts +10 -0
- package/dist/shared/settings-catalog.json +2199 -0
- package/dist/shared/types.d.ts +530 -0
- package/dist/shared/types.js +11 -0
- package/dist/ui/assets/index-CH1HoC38.js +60 -0
- package/dist/ui/index.html +1 -1
- package/dist/view/diff.d.ts +16 -0
- package/dist/view/diff.js +85 -0
- package/dist/view/format.d.ts +17 -0
- package/dist/view/format.js +47 -0
- package/dist/view/index.d.ts +11 -0
- package/dist/view/index.js +13 -0
- package/dist/view/lines.d.ts +9 -0
- package/dist/view/lines.js +12 -0
- package/dist/view/path-test.d.ts +3 -0
- package/dist/view/path-test.js +7 -0
- package/dist/view/sections/catalog.d.ts +9 -0
- package/dist/view/sections/catalog.js +47 -0
- package/dist/view/sections/extensions.d.ts +1 -0
- package/dist/view/sections/extensions.js +48 -0
- package/dist/view/sections/hooks.d.ts +1 -0
- package/dist/view/sections/hooks.js +32 -0
- package/dist/view/sections/index.d.ts +6 -0
- package/dist/view/sections/index.js +27 -0
- package/dist/view/sections/instructions.d.ts +6 -0
- package/dist/view/sections/instructions.js +38 -0
- package/dist/view/sections/mcp.d.ts +3 -0
- package/dist/view/sections/mcp.js +38 -0
- package/dist/view/sections/memory.d.ts +1 -0
- package/dist/view/sections/memory.js +31 -0
- package/dist/view/sections/permissions.d.ts +17 -0
- package/dist/view/sections/permissions.js +87 -0
- package/dist/view/sections/settings.d.ts +1 -0
- package/dist/view/sections/settings.js +33 -0
- package/dist/view/sections/tools.d.ts +6 -0
- package/dist/view/sections/tools.js +57 -0
- package/dist/view/sections/types.d.ts +123 -0
- package/dist/view/sections/types.js +10 -0
- package/dist/view/settings.d.ts +12 -0
- package/dist/view/settings.js +31 -0
- package/dist/view/usage.d.ts +8 -0
- package/dist/view/usage.js +18 -0
- package/package.json +13 -2
- package/dist/server/scan/files.js +0 -480
- package/dist/server/scan/path-test.js +0 -136
- package/dist/server/schema.js +0 -15
- package/dist/server/secrets.js +0 -63
- 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 {
|
|
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
|
|
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
|
|
55
|
+
const problems = [];
|
|
56
|
+
const claudeJson = await readClaudeJson(env, problems);
|
|
57
57
|
const root = exists ? await gitRoot(path) : null;
|
|
58
|
-
const
|
|
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
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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 =
|
|
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
|
-
|
|
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 && !
|
|
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
|
-
|
|
276
|
-
|
|
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,
|
|
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
|
|
50
|
-
const
|
|
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
|
-
|
|
77
|
-
|
|
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
|
-
|
|
82
|
-
const
|
|
83
|
-
|
|
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
|
-
|
|
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.
|
|
110
|
-
file.
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
|
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
|
-
|
|
130
|
-
|
|
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 (
|
|
139
|
-
skip(file, skippedByMode);
|
|
140
|
-
// Managed files cannot be excluded
|
|
141
|
-
|
|
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
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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
|
-
|
|
159
|
-
|
|
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
|
|
217
|
-
if (
|
|
218
|
-
entry.realPath =
|
|
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,
|