ccdesk 0.2.2 → 0.2.4
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 +46 -0
- package/README.md +7 -4
- 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 +64 -48
- 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 +23 -0
- package/dist/{server → core}/mcp-edit.js +121 -100
- package/dist/core/mutations.d.ts +20 -0
- package/dist/{server → core}/mutations.js +8 -15
- package/dist/core/open.d.ts +28 -0
- package/dist/{server → core}/open.js +38 -13
- 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 +14 -0
- package/dist/core/scan/data.js +26 -0
- package/dist/core/scan/env.d.ts +28 -0
- package/dist/{server → core}/scan/env.js +15 -4
- package/dist/core/scan/files.d.ts +68 -0
- package/dist/core/scan/files.js +270 -0
- package/dist/core/scan/git.d.ts +20 -0
- package/dist/core/scan/git.js +106 -0
- package/dist/core/scan/index.d.ts +5 -0
- package/dist/{server → core}/scan/index.js +119 -82
- 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 +153 -95
- 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 +7 -5
- 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 +37 -0
- package/dist/{server → core}/scan/settings.js +98 -93
- 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 +19 -0
- package/dist/{server → core}/scan/usage.js +122 -97
- package/dist/core/schema.d.ts +8 -0
- package/dist/core/schema.js +67 -0
- package/dist/core/secrets.d.ts +45 -0
- package/dist/core/secrets.js +207 -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 +117 -0
- package/dist/{server → core}/write.js +61 -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 +6 -0
- package/dist/shared/memory.js +18 -2
- 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 +535 -0
- package/dist/shared/types.js +11 -0
- package/dist/ui/assets/index-C9qqEI5Z.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
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { dirname, normalize, relative } from "node:path";
|
|
3
|
+
import { promisify } from "node:util";
|
|
4
|
+
import {} from "../../shared/types.js";
|
|
5
|
+
import {} from "./env.js";
|
|
6
|
+
import { isWithin } from "./files.js";
|
|
7
|
+
const run = promisify(execFile);
|
|
8
|
+
export async function gitRoot(dir) {
|
|
9
|
+
try {
|
|
10
|
+
const { stdout } = await run("git", ["-C", dir, "rev-parse", "--show-toplevel"], {
|
|
11
|
+
timeout: 5000,
|
|
12
|
+
});
|
|
13
|
+
// Git prints forward slashes on Windows too; normalise so path comparisons hold.
|
|
14
|
+
return stdout.trim() ? normalize(stdout.trim()) : null;
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
/** Every checkout of the repository `dir` is in, the main one first; empty outside git. */
|
|
21
|
+
export async function worktrees(dir) {
|
|
22
|
+
try {
|
|
23
|
+
const { stdout } = await run("git", ["-C", dir, "worktree", "list", "--porcelain"], {
|
|
24
|
+
timeout: 5000,
|
|
25
|
+
});
|
|
26
|
+
return stdout
|
|
27
|
+
.split("\n")
|
|
28
|
+
.filter((line) => line.startsWith("worktree "))
|
|
29
|
+
.map((line) => normalize(line.slice("worktree ".length)));
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return [];
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/** Main worktree root: worktrees of one repository share Claude Code's auto memory. */
|
|
36
|
+
export async function mainWorktreeRoot(dir) {
|
|
37
|
+
try {
|
|
38
|
+
const { stdout } = await run("git", ["-C", dir, "rev-parse", "--path-format=absolute", "--git-common-dir"], { timeout: 5000 });
|
|
39
|
+
const common = stdout.trim();
|
|
40
|
+
if (!common)
|
|
41
|
+
return null;
|
|
42
|
+
return common.endsWith("/.git") || common.endsWith("\\.git")
|
|
43
|
+
? normalize(dirname(common))
|
|
44
|
+
: null;
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* The key ~/.claude.json keeps a folder's trust answers and local MCP servers under: the main
|
|
52
|
+
* checkout's root, else the repository root, else the folder itself outside git. The scan reads
|
|
53
|
+
* under it and `claude mcp add --scope local` writes under it, so both take it from here.
|
|
54
|
+
*/
|
|
55
|
+
export function trustKey(dir, mainRoot, root) {
|
|
56
|
+
return mainRoot ?? root ?? dir;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* A folder as ~/.claude.json names it under `projects`: Claude Code writes Windows paths with
|
|
60
|
+
* forward slashes (observed in v2.1.296; the docs do not say), so lookups go through this.
|
|
61
|
+
*/
|
|
62
|
+
export function claudeJsonKey(env, path) {
|
|
63
|
+
return env.platform === "win32" ? path.replaceAll("\\", "/") : path;
|
|
64
|
+
}
|
|
65
|
+
/** Git visibility of each path, batched into two git calls per repository. */
|
|
66
|
+
export async function visibilityOf(root, paths) {
|
|
67
|
+
const result = new Map();
|
|
68
|
+
if (paths.length === 0)
|
|
69
|
+
return result;
|
|
70
|
+
if (!root) {
|
|
71
|
+
for (const p of paths)
|
|
72
|
+
result.set(p, "no-git");
|
|
73
|
+
return result;
|
|
74
|
+
}
|
|
75
|
+
const inside = paths.filter((p) => p !== root && isWithin(root, p));
|
|
76
|
+
for (const p of paths)
|
|
77
|
+
if (!inside.includes(p))
|
|
78
|
+
result.set(p, "no-git");
|
|
79
|
+
if (inside.length === 0)
|
|
80
|
+
return result;
|
|
81
|
+
const rels = new Map(inside.map((p) => [p, relative(root, p).replaceAll("\\", "/")]));
|
|
82
|
+
const tracked = await gitLines(root, ["ls-files", "--full-name", "-z", "--", ...inside], "\0");
|
|
83
|
+
// check-ignore echoes the paths it matched, and on Windows an absolute path does not come back
|
|
84
|
+
// as written, so it gets relative ones. It accepts -z only with --stdin, so its output is split
|
|
85
|
+
// by line.
|
|
86
|
+
const ignored = await gitLines(root, ["check-ignore", "--no-index", "--", ...rels.values()], "\n");
|
|
87
|
+
for (const [p, rel] of rels) {
|
|
88
|
+
if (tracked.has(rel))
|
|
89
|
+
result.set(p, "tracked");
|
|
90
|
+
else if (ignored.has(rel))
|
|
91
|
+
result.set(p, "ignored");
|
|
92
|
+
else
|
|
93
|
+
result.set(p, "untracked");
|
|
94
|
+
}
|
|
95
|
+
return result;
|
|
96
|
+
}
|
|
97
|
+
// Failures (including check-ignore's exit 1 for "nothing ignored") read as an empty set.
|
|
98
|
+
async function gitLines(root, args, separator) {
|
|
99
|
+
try {
|
|
100
|
+
const { stdout } = await run("git", ["-C", root, ...args], { timeout: 5000 });
|
|
101
|
+
return new Set(stdout.split(separator).filter(Boolean));
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
return new Set();
|
|
105
|
+
}
|
|
106
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { type DirectoryView, type Overview, type SessionProfile } from "../../shared/types.ts";
|
|
2
|
+
import { type ScanEnv } from "./env.ts";
|
|
3
|
+
export declare function scanOverview(env: ScanEnv, launchDir: string): Promise<Overview>;
|
|
4
|
+
/** Everything Claude Code reads when started in `dir`. */
|
|
5
|
+
export declare function scanDirectory(env: ScanEnv, dir: string, profile?: SessionProfile): Promise<DirectoryView>;
|
|
@@ -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 { claudeJsonKey, 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,98 @@ 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
|
-
const entry = (
|
|
178
|
-
|
|
204
|
+
const entry = (dir) => {
|
|
205
|
+
const found = projects[claudeJsonKey(env, dir)];
|
|
206
|
+
return isRecord(found) ? found : {};
|
|
207
|
+
};
|
|
208
|
+
const root = trustKey(path, mainRoot, repoRoot);
|
|
179
209
|
const own = entry(root);
|
|
180
210
|
const trust = {
|
|
181
211
|
root,
|
|
182
212
|
trusted: own.hasTrustDialogAccepted === true,
|
|
183
213
|
sessionOnly: path === env.home,
|
|
184
|
-
|
|
185
|
-
externalImports: own.hasClaudeMdExternalIncludesApproved === true
|
|
186
|
-
? "approved"
|
|
187
|
-
: own.hasClaudeMdExternalIncludesWarningShown === true
|
|
188
|
-
? "declined"
|
|
189
|
-
: "not-asked",
|
|
214
|
+
externalImports: importAnswer(own),
|
|
190
215
|
};
|
|
191
|
-
if (!trust.trusted && !
|
|
216
|
+
if (!trust.trusted && !mainRoot && !repoRoot)
|
|
192
217
|
for (let d = dirname(path); d !== dirname(d); d = dirname(d))
|
|
193
218
|
if (entry(d).hasTrustDialogAccepted === true) {
|
|
194
219
|
trust.inheritedFrom = d;
|
|
@@ -196,12 +221,23 @@ function trustOf(env, claudeJson, path, repositoryRoot) {
|
|
|
196
221
|
}
|
|
197
222
|
return trust;
|
|
198
223
|
}
|
|
224
|
+
/**
|
|
225
|
+
* Your answer to the dialog about imports from outside the project. These two keys are not
|
|
226
|
+
* documented; they are what Claude Code writes for that dialog.
|
|
227
|
+
*/
|
|
228
|
+
function importAnswer(project) {
|
|
229
|
+
if (project.hasClaudeMdExternalIncludesApproved === true)
|
|
230
|
+
return "approved";
|
|
231
|
+
if (project.hasClaudeMdExternalIncludesWarningShown === true)
|
|
232
|
+
return "declined";
|
|
233
|
+
return "not-asked";
|
|
234
|
+
}
|
|
199
235
|
/**
|
|
200
236
|
* The auto memory Claude Code uses here (docs: memory#storage-location, settings-reference#automemoryenabled).
|
|
201
237
|
* Worktrees and subdirectories of one repository share one store. The store is returned even
|
|
202
238
|
* when its folder does not exist yet, so a note about memory being off still shows.
|
|
203
239
|
*/
|
|
204
|
-
async function readMemory(env, mainRoot, path, effective, trustOptions) {
|
|
240
|
+
async function readMemory(env, mainRoot, path, effective, trustOptions, problems) {
|
|
205
241
|
const key = mainRoot ?? path;
|
|
206
242
|
const setting = effective.find((s) => s.key === "autoMemoryDirectory");
|
|
207
243
|
// A value from a repository file counts once the folder is trusted, like hooks (docs:
|
|
@@ -216,7 +252,7 @@ async function readMemory(env, mainRoot, path, effective, trustOptions) {
|
|
|
216
252
|
const customDir = typeof custom?.value === "string" ? expandHome(env, custom.value) : null;
|
|
217
253
|
const storeName = env.projectDirName ?? storeDirName(key);
|
|
218
254
|
const dir = customDir ?? join(env.claudeDir, "projects", storeName, "memory");
|
|
219
|
-
const store = (await readMemoryStore(env, dir, key)) ?? {
|
|
255
|
+
const store = (await readMemoryStore(env, dir, key, problems)) ?? {
|
|
220
256
|
dir,
|
|
221
257
|
loaded: true,
|
|
222
258
|
projectPath: key,
|
|
@@ -272,8 +308,9 @@ function agentsMdMode(layers) {
|
|
|
272
308
|
for (const id of ["cc-plugin-agents-md@builtin", "agents-md@builtin"]) {
|
|
273
309
|
const entry = configs[id];
|
|
274
310
|
const value = isRecord(entry) && isRecord(entry.options) ? entry.options.instructionFiles : undefined;
|
|
275
|
-
|
|
276
|
-
|
|
311
|
+
const mode = AGENTS_MD_MODES.find((m) => m === value);
|
|
312
|
+
if (mode)
|
|
313
|
+
return mode;
|
|
277
314
|
}
|
|
278
315
|
}
|
|
279
316
|
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 {};
|