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.
Files changed (131) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/README.md +7 -4
  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 +64 -48
  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 +23 -0
  12. package/dist/{server → core}/mcp-edit.js +121 -100
  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 +28 -0
  16. package/dist/{server → core}/open.js +38 -13
  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 +14 -0
  26. package/dist/core/scan/data.js +26 -0
  27. package/dist/core/scan/env.d.ts +28 -0
  28. package/dist/{server → core}/scan/env.js +15 -4
  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 +20 -0
  32. package/dist/core/scan/git.js +106 -0
  33. package/dist/core/scan/index.d.ts +5 -0
  34. package/dist/{server → core}/scan/index.js +119 -82
  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 +153 -95
  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 +7 -5
  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 +37 -0
  54. package/dist/{server → core}/scan/settings.js +98 -93
  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 +19 -0
  58. package/dist/{server → core}/scan/usage.js +122 -97
  59. package/dist/core/schema.d.ts +8 -0
  60. package/dist/core/schema.js +67 -0
  61. package/dist/core/secrets.d.ts +45 -0
  62. package/dist/core/secrets.js +207 -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 +117 -0
  68. package/dist/{server → core}/write.js +61 -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 +6 -0
  81. package/dist/shared/memory.js +18 -2
  82. package/dist/shared/requests.d.ts +104 -0
  83. package/dist/shared/requests.js +73 -0
  84. package/dist/shared/scopes.d.ts +10 -0
  85. package/dist/shared/settings-catalog.json +2199 -0
  86. package/dist/shared/types.d.ts +535 -0
  87. package/dist/shared/types.js +11 -0
  88. package/dist/ui/assets/index-C9qqEI5Z.js +60 -0
  89. package/dist/ui/index.html +1 -1
  90. package/dist/view/diff.d.ts +16 -0
  91. package/dist/view/diff.js +85 -0
  92. package/dist/view/format.d.ts +17 -0
  93. package/dist/view/format.js +47 -0
  94. package/dist/view/index.d.ts +11 -0
  95. package/dist/view/index.js +13 -0
  96. package/dist/view/lines.d.ts +9 -0
  97. package/dist/view/lines.js +12 -0
  98. package/dist/view/path-test.d.ts +3 -0
  99. package/dist/view/path-test.js +7 -0
  100. package/dist/view/sections/catalog.d.ts +9 -0
  101. package/dist/view/sections/catalog.js +47 -0
  102. package/dist/view/sections/extensions.d.ts +1 -0
  103. package/dist/view/sections/extensions.js +48 -0
  104. package/dist/view/sections/hooks.d.ts +1 -0
  105. package/dist/view/sections/hooks.js +32 -0
  106. package/dist/view/sections/index.d.ts +6 -0
  107. package/dist/view/sections/index.js +27 -0
  108. package/dist/view/sections/instructions.d.ts +6 -0
  109. package/dist/view/sections/instructions.js +38 -0
  110. package/dist/view/sections/mcp.d.ts +3 -0
  111. package/dist/view/sections/mcp.js +38 -0
  112. package/dist/view/sections/memory.d.ts +1 -0
  113. package/dist/view/sections/memory.js +31 -0
  114. package/dist/view/sections/permissions.d.ts +17 -0
  115. package/dist/view/sections/permissions.js +87 -0
  116. package/dist/view/sections/settings.d.ts +1 -0
  117. package/dist/view/sections/settings.js +33 -0
  118. package/dist/view/sections/tools.d.ts +6 -0
  119. package/dist/view/sections/tools.js +57 -0
  120. package/dist/view/sections/types.d.ts +123 -0
  121. package/dist/view/sections/types.js +10 -0
  122. package/dist/view/settings.d.ts +12 -0
  123. package/dist/view/settings.js +31 -0
  124. package/dist/view/usage.d.ts +8 -0
  125. package/dist/view/usage.js +18 -0
  126. package/package.json +13 -2
  127. package/dist/server/scan/files.js +0 -480
  128. package/dist/server/scan/path-test.js +0 -136
  129. package/dist/server/schema.js +0 -15
  130. package/dist/server/secrets.js +0 -63
  131. package/dist/ui/assets/index-Dg9dxngR.js +0 -60
@@ -1,12 +1,13 @@
1
1
  import { basename, dirname, join, relative } from "node:path";
2
2
  import { HIGHEST_FIRST } from "../../shared/scopes.js";
3
3
  import {} from "../../shared/types.js";
4
+ import { redactSecrets, shownUrl } from "../secrets.js";
4
5
  import {} from "./env.js";
5
- import { isDir, isFile, isRecord, isWithin, listDir, parseFrontmatter, readJsonObject, readTextFile, redactSecrets, resolvedPath, shownUrl, stringField, toStringList, } from "./files.js";
6
+ import { isDir, isFile, isWithin, listDir, readJsonOrReport, readTextFile, resolvedPath, stringField, toStringList, } from "./files.js";
7
+ import { isRecord, parseFrontmatter } from "./data.js";
6
8
  import { ancestorsRootFirst } from "./instructions.js";
7
9
  import { readMcpServers } from "./mcp.js";
8
10
  import { manifestCommandMap, manifestConfigs, manifestPaths, pluginNamespace, readPlugins, } from "./plugins.js";
9
- import { effectiveSettings } from "./settings.js";
10
11
  function isTrue(value) {
11
12
  return value === true || (typeof value === "string" && /^(true|yes|on|1)$/i.test(value));
12
13
  }
@@ -14,8 +15,8 @@ function isTrue(value) {
14
15
  * A `.md` file as an agent, command or output style. `fileName` is its name from its path, which a
15
16
  * frontmatter `name` replaces, except for a command (docs: skills#how-a-skill-gets-its-command-name).
16
17
  */
17
- async function readEntry(env, path, fileName, scope, { plugin, isCommand = false } = {}) {
18
- const file = await readTextFile(env, path);
18
+ async function readEntry(env, path, fileName, scope, { plugin, isCommand = false, problems } = {}) {
19
+ const file = await readTextFile(env, path, problems);
19
20
  if (!file)
20
21
  return null;
21
22
  const { data } = parseFrontmatter(file.text);
@@ -40,7 +41,7 @@ async function readEntry(env, path, fileName, scope, { plugin, isCommand = false
40
41
  * `*.md` files in a folder and its subfolders. A file in a subfolder is named by its path, joined
41
42
  * with `separator`: commands use ":" (`frontend/component.md` is `/frontend:component`).
42
43
  */
43
- async function readFileEntries(env, dir, scope, { plugin, separator = "/" } = {}, base = dir, depth = 0) {
44
+ async function readFileEntries(env, dir, scope, options = {}, base = dir, depth = 0) {
44
45
  const out = [];
45
46
  for (const name of await listDir(dir)) {
46
47
  if (name.startsWith("."))
@@ -48,16 +49,18 @@ async function readFileEntries(env, dir, scope, { plugin, separator = "/" } = {}
48
49
  const path = join(dir, name);
49
50
  if (await isDir(path)) {
50
51
  if (depth < 4)
51
- out.push(...(await readFileEntries(env, path, scope, { plugin, separator }, base, depth + 1)));
52
+ out.push(...(await readFileEntries(env, path, scope, options, base, depth + 1)));
52
53
  continue;
53
54
  }
54
55
  if (!name.endsWith(".md"))
55
56
  continue;
57
+ const separator = options.separator ?? "/";
56
58
  const fileName = relative(base, path).replace(/\.md$/, "").split(/[/\\]/).join(separator);
57
59
  // Commands are the entries joined with ":".
58
60
  const entry = await readEntry(env, path, fileName, scope, {
59
- plugin,
61
+ plugin: options.plugin,
60
62
  isCommand: separator === ":",
63
+ problems: options.problems,
61
64
  });
62
65
  if (entry)
63
66
  out.push(entry);
@@ -74,6 +77,7 @@ async function readPaths(env, paths, scope, options) {
74
77
  const entry = await readEntry(env, path, basename(path, ".md"), scope, {
75
78
  plugin: options.plugin,
76
79
  isCommand: options.separator === ":",
80
+ problems: options.problems,
77
81
  });
78
82
  if (entry)
79
83
  out.push(entry);
@@ -81,9 +85,9 @@ async function readPaths(env, paths, scope, options) {
81
85
  }
82
86
  return out;
83
87
  }
84
- export async function readSkill(env, folder, scope, plugin) {
88
+ export async function readSkill(env, folder, scope, { plugin, problems } = {}) {
85
89
  const path = join(folder, "SKILL.md");
86
- const file = await readTextFile(env, path);
90
+ const file = await readTextFile(env, path, problems);
87
91
  if (!file)
88
92
  return null;
89
93
  const { data, body } = parseFrontmatter(file.text);
@@ -115,87 +119,104 @@ export async function readSkill(env, folder, scope, plugin) {
115
119
  return entry;
116
120
  }
117
121
  /** `<name>/SKILL.md` folders, or one folder holding SKILL.md itself. */
118
- async function readSkills(env, dir, scope, plugin) {
119
- if (plugin && (await isFile(join(dir, "SKILL.md")))) {
120
- const skill = await readSkill(env, dir, scope, plugin);
122
+ async function readSkills(env, dir, scope, options) {
123
+ if (options.plugin && (await isFile(join(dir, "SKILL.md")))) {
124
+ const skill = await readSkill(env, dir, scope, options);
121
125
  return skill ? [skill] : [];
122
126
  }
123
127
  const out = [];
124
128
  for (const name of await listDir(dir)) {
125
129
  if (name.startsWith(".") || (scope === "user" && name === "synced"))
126
130
  continue;
127
- const skill = await readSkill(env, join(dir, name), scope, plugin);
131
+ const skill = await readSkill(env, join(dir, name), scope, options);
128
132
  if (skill)
129
133
  out.push(skill);
130
134
  }
131
135
  return out;
132
136
  }
133
137
  /** A plugin's skills, agents, commands and output styles, from its folders and its manifest. */
134
- async function pluginEntries(env, plugin) {
135
- const root = plugin.installPath;
136
- const options = { plugin: plugin.id };
138
+ async function pluginEntries(env, loaded, problems) {
139
+ const root = loaded.plugin.installPath;
140
+ const options = { plugin: loaded.plugin.id, problems };
141
+ const namespace = pluginNamespace(loaded);
142
+ const listed = async (key, folder, separator = "/") => readPaths(env, (await manifestPaths(loaded, key)) ?? [join(root, folder)], "plugin", {
143
+ ...options,
144
+ separator,
145
+ });
146
+ const skills = await pluginSkills(env, loaded, problems);
147
+ const commands = [
148
+ ...(await listed("commands", "commands", ":")),
149
+ ...(await manifestCommands(env, loaded, problems)),
150
+ ];
151
+ // A plugin's skills and commands are called as plugin-name:name; a name that already has the
152
+ // prefix keeps it once (docs: skills#how-a-skill-gets-its-command-name).
153
+ for (const entry of [...skills, ...commands])
154
+ entry.id = entry.name.startsWith(`${namespace}:`) ? entry.name : `${namespace}:${entry.name}`;
155
+ return {
156
+ skills,
157
+ agents: await pluginAgents(env, loaded, problems),
158
+ commands,
159
+ outputStyles: await listed("outputStyles", "output-styles"),
160
+ };
161
+ }
162
+ /** A plugin's skills: its skills folder, the manifest's extra ones, or a SKILL.md at its root. */
163
+ async function pluginSkills(env, loaded, problems) {
164
+ const root = loaded.plugin.installPath;
165
+ const options = { plugin: loaded.plugin.id, problems };
137
166
  // `skills` adds to the default folder; the other keys replace theirs (docs: plugins-reference).
138
- const extraSkills = await manifestPaths(env, plugin, "skills");
167
+ const extraSkills = await manifestPaths(loaded, "skills");
139
168
  const skillDirs = [join(root, "skills"), ...(extraSkills ?? [])];
140
169
  const rootSkill = extraSkills === null && !(await isDir(join(root, "skills")));
141
- const namespace = await pluginNamespace(env, plugin);
142
170
  const skills = [];
143
171
  if (rootSkill)
144
- for (const skill of await readSkills(env, root, "plugin", plugin.id)) {
172
+ for (const skill of await readSkills(env, root, "plugin", options)) {
145
173
  // Without a name, a root SKILL.md is named after the plugin (docs: skills#how-a-skill-gets-its-command-name).
146
174
  if (skill.folder === root && !stringField(parseFrontmatter(skill.text).data, "name"))
147
- skill.name = namespace;
175
+ skill.name = pluginNamespace(loaded);
148
176
  skills.push(skill);
149
177
  }
150
178
  for (const dir of skillDirs)
151
- skills.push(...(await readSkills(env, dir, "plugin", plugin.id)));
152
- const listed = async (key, folder, separator = "/") => readPaths(env, (await manifestPaths(env, plugin, key)) ?? [join(root, folder)], "plugin", {
153
- ...options,
154
- separator,
155
- });
156
- const commands = await listed("commands", "commands", ":");
157
- for (const c of await manifestCommandMap(env, plugin))
158
- commands.push(c.text === null
159
- ? ((await readEntry(env, c.path, c.name, "plugin", {
160
- plugin: plugin.id,
161
- isCommand: true,
162
- })) ?? {
163
- name: c.name,
164
- description: c.description,
165
- path: c.path,
166
- scope: "plugin",
167
- plugin: plugin.id,
168
- text: "",
169
- })
170
- : {
171
- name: c.name,
172
- description: c.description,
173
- path: c.path,
174
- scope: "plugin",
175
- plugin: plugin.id,
176
- text: c.text,
177
- });
178
- // A plugin's skills and commands are called as plugin-name:name; a name that already has the
179
- // prefix keeps it once (docs: skills#how-a-skill-gets-its-command-name).
180
- for (const entry of [...skills, ...commands])
181
- entry.id = entry.name.startsWith(`${namespace}:`) ? entry.name : `${namespace}:${entry.name}`;
182
- // A plugin subagent is named with the plugin and its subfolders (docs: sub-agents#choose-the-subagent-scope).
179
+ skills.push(...(await readSkills(env, dir, "plugin", options)));
180
+ return skills;
181
+ }
182
+ /** The commands a plugin's manifest names with their own file or text. */
183
+ async function manifestCommands(env, loaded, problems) {
184
+ const plugin = loaded.plugin.id;
185
+ const commands = [];
186
+ for (const c of await manifestCommandMap(loaded)) {
187
+ const listed = {
188
+ name: c.name,
189
+ description: c.description,
190
+ path: c.path,
191
+ scope: "plugin",
192
+ plugin,
193
+ text: c.text ?? "",
194
+ };
195
+ const read = c.text === null
196
+ ? await readEntry(env, c.path, c.name, "plugin", { plugin, problems, isCommand: true })
197
+ : null;
198
+ commands.push(read ?? listed);
199
+ }
200
+ return commands;
201
+ }
202
+ /** A plugin's subagents, named with the plugin and its subfolders (docs: sub-agents#choose-the-subagent-scope). */
203
+ async function pluginAgents(env, loaded, problems) {
204
+ const root = loaded.plugin.installPath;
205
+ const namespace = pluginNamespace(loaded);
183
206
  const agents = [];
184
- for (const path of (await manifestPaths(env, plugin, "agents")) ?? [join(root, "agents")]) {
207
+ for (const path of (await manifestPaths(loaded, "agents")) ?? [join(root, "agents")]) {
185
208
  const folder = (await isDir(path)) ? path : null;
186
- for (const agent of await readPaths(env, [path], "plugin", options)) {
209
+ for (const agent of await readPaths(env, [path], "plugin", {
210
+ plugin: loaded.plugin.id,
211
+ problems,
212
+ })) {
187
213
  const subfolders = folder ? relative(folder, dirname(agent.path)).split(/[/\\]/) : [];
188
214
  const own = agent.name.split("/").at(-1) ?? agent.name;
189
215
  agent.id = [namespace, ...subfolders.filter(Boolean), own].join(":");
190
216
  agents.push(agent);
191
217
  }
192
218
  }
193
- return {
194
- skills,
195
- agents,
196
- commands,
197
- outputStyles: await listed("outputStyles", "output-styles"),
198
- };
219
+ return agents;
199
220
  }
200
221
  /**
201
222
  * The `.claude/<sub>` folders of the start directory and every parent up to the repository root
@@ -229,7 +250,7 @@ export function agentProblems(text) {
229
250
  * yours. A name defined again lower down is marked as shadowed (docs: sub-agents#choose-the-subagent-scope).
230
251
  * A file Claude Code skips takes no part in that.
231
252
  */
232
- async function readAgents(env, projectDir, gitRoot) {
253
+ async function readAgents(env, projectDir, gitRoot, problems) {
233
254
  const sources = [
234
255
  [join(env.managedDir, ".claude", "agents"), "managed"],
235
256
  ...projectFolders(env, projectDir, gitRoot, "agents")
@@ -240,7 +261,7 @@ async function readAgents(env, projectDir, gitRoot) {
240
261
  const agents = [];
241
262
  const winners = new Map();
242
263
  for (const [dir, scope] of sources)
243
- for (const agent of await readFileEntries(env, dir, scope)) {
264
+ for (const agent of await readFileEntries(env, dir, scope, { problems })) {
244
265
  const [problem] = agentProblems(agent.text);
245
266
  if (problem) {
246
267
  agent.skipped = problem;
@@ -261,7 +282,7 @@ async function readAgents(env, projectDir, gitRoot) {
261
282
  * repository root (the closest wins) and yours (docs: output-styles#create-a-custom-output-style).
262
283
  * The docs order only the project folders; managed, project, then yours follows subagents.
263
284
  */
264
- async function readOutputStyles(env, projectDir, gitRoot) {
285
+ async function readOutputStyles(env, projectDir, gitRoot, problems) {
265
286
  const sources = [
266
287
  [join(env.managedDir, ".claude", "output-styles"), "managed"],
267
288
  ...projectFolders(env, projectDir, gitRoot, "output-styles")
@@ -271,7 +292,7 @@ async function readOutputStyles(env, projectDir, gitRoot) {
271
292
  ];
272
293
  const styles = [];
273
294
  for (const [dir, scope] of sources)
274
- styles.push(...(await readFileEntries(env, dir, scope)));
295
+ styles.push(...(await readFileEntries(env, dir, scope, { problems })));
275
296
  return styles;
276
297
  }
277
298
  /**
@@ -293,16 +314,16 @@ export function applySkillOverrides(entries, overrides) {
293
314
  }
294
315
  }
295
316
  export async function readInventory(input) {
296
- const { env, projectDir, gitRoot, layers } = input;
297
- const plugins = await readPlugins(env, layers, projectDir, gitRoot);
298
- const active = plugins.filter((p) => p.enabled);
299
- const fromPlugins = await Promise.all(active.map((plugin) => pluginEntries(env, plugin)));
317
+ const { env, projectDir, gitRoot, layers, problems } = input;
318
+ const plugins = await readPlugins(env, layers, projectDir, gitRoot, problems);
319
+ const active = plugins.filter((p) => p.plugin.enabled);
320
+ const fromPlugins = await Promise.all(active.map((plugin) => pluginEntries(env, plugin, problems)));
300
321
  const projectClaude = join(projectDir, ".claude");
301
322
  const hasProjectDir = projectClaude !== env.claudeDir;
302
323
  const files = async (sub, separator = "/") => [
303
- ...(await readFileEntries(env, join(env.claudeDir, sub), "user", { separator })),
324
+ ...(await readFileEntries(env, join(env.claudeDir, sub), "user", { separator, problems })),
304
325
  ...(hasProjectDir
305
- ? await readFileEntries(env, join(projectClaude, sub), "project", { separator })
326
+ ? await readFileEntries(env, join(projectClaude, sub), "project", { separator, problems })
306
327
  : []),
307
328
  ];
308
329
  // Project skills come from the start directory and every parent up to the repository root.
@@ -323,7 +344,7 @@ export async function readInventory(input) {
323
344
  // (docs: skills#resolve-skills-that-share-a-name).
324
345
  const winners = new Map();
325
346
  for (const [dir, scope] of skillDirs) {
326
- for (const skill of await readSkills(env, dir, scope)) {
347
+ for (const skill of await readSkills(env, dir, scope, { problems })) {
327
348
  // A folder that several locations link to loads once.
328
349
  const real = skill.realPath ?? skill.folder ?? skill.path;
329
350
  if (seenFolders.has(real))
@@ -347,36 +368,38 @@ export async function readInventory(input) {
347
368
  if (skill)
348
369
  command.shadowedBy = skill.path;
349
370
  }
350
- const overrides = effectiveSettings(layers).find((s) => s.key === "skillOverrides")?.value;
371
+ const overrides = input.effective.find((s) => s.key === "skillOverrides")?.value;
351
372
  applySkillOverrides([...skills, ...commands], overrides);
352
373
  return {
353
374
  agents: [
354
- ...(await readAgents(env, projectDir, gitRoot)),
375
+ ...(await readAgents(env, projectDir, gitRoot, problems)),
355
376
  ...fromPlugins.flatMap((p) => p.agents),
356
377
  ],
357
378
  skills: [...skills, ...fromPlugins.flatMap((p) => p.skills)],
358
379
  commands: [...commands, ...fromPlugins.flatMap((p) => p.commands)],
359
380
  outputStyles: [
360
- ...(await readOutputStyles(env, projectDir, gitRoot)),
381
+ ...(await readOutputStyles(env, projectDir, gitRoot, problems)),
361
382
  ...fromPlugins.flatMap((p) => p.outputStyles),
362
383
  ],
363
- hooks: await readHooks(env, layers, active, input.trust, input.profile),
384
+ hooks: await readHooks(input, active),
364
385
  mcpServers: await readMcpServers(input, active),
365
- plugins,
386
+ plugins: plugins.map((p) => p.plugin),
366
387
  };
367
388
  }
368
- async function readHooks(env, layers, plugins, trust, profile) {
389
+ async function readHooks(input, plugins) {
390
+ const { env, layers, trust, profile, problems } = input;
369
391
  const out = [];
370
392
  for (const layer of layers) {
371
393
  if (layer.data)
372
394
  out.push(...hooksFrom(layer.data.hooks, layer.scope, layer.path));
373
395
  }
374
- for (const plugin of plugins) {
396
+ for (const loaded of plugins) {
397
+ const { plugin } = loaded;
375
398
  const path = join(plugin.installPath, "hooks", "hooks.json");
376
- const file = await readJsonObject(env, path);
377
- if (file.ok)
378
- out.push(...hooksFrom(file.data.hooks, "plugin", path, plugin));
379
- for (const part of await manifestConfigs(env, plugin, "hooks"))
399
+ const file = await readJsonOrReport(env, path, problems);
400
+ if (file)
401
+ out.push(...hooksFrom(file.hooks, "plugin", path, plugin));
402
+ for (const part of await manifestConfigs(env, loaded, "hooks", problems))
380
403
  out.push(...hooksFrom(part.value, "plugin", part.path, plugin));
381
404
  }
382
405
  // docs: hooks#disable-or-remove-hooks, settings-reference#allowmanagedhooksonly
@@ -387,7 +410,7 @@ async function readHooks(env, layers, plugins, trust, profile) {
387
410
  .flatMap((scope) => layers.filter((l) => l.scope === scope))
388
411
  .find((l) => typeof l.data?.disableAllHooks === "boolean")?.data?.disableAllHooks === true;
389
412
  const managedOnly = managed?.allowManagedHooksOnly === true;
390
- const forcedByManaged = new Set(plugins.filter((p) => p.enabledIn === "managed").map((p) => p.id));
413
+ const forcedByManaged = new Set(plugins.filter((p) => p.plugin.enabledIn === "managed").map((p) => p.plugin.id));
391
414
  // An interactive session holds back hooks from every settings file, managed and yours included,
392
415
  // until the folder or a parent covering it is trusted; -p and the SDK do not (docs:
393
416
  // hooks#workspace-trust, permissions#what-runs-before-you-trust-a-folder). The docs say nothing
@@ -417,36 +440,32 @@ async function readHooks(env, layers, plugins, trust, profile) {
417
440
  function hooksFrom(config, source, sourcePath, plugin) {
418
441
  if (!isRecord(config))
419
442
  return [];
420
- const out = [];
421
- for (const [event, groups] of Object.entries(config)) {
422
- if (!Array.isArray(groups))
423
- continue;
424
- for (const [g, group] of groups.entries()) {
425
- if (!isRecord(group) || !Array.isArray(group.hooks))
426
- continue;
427
- for (const [index, h] of group.hooks.entries()) {
428
- if (!isRecord(h))
429
- continue;
430
- const entry = {
431
- event,
432
- type: stringField(h, "type") || "command",
433
- command: stringField(h, "command") ||
434
- stringField(h, "prompt") ||
435
- shownUrl(stringField(h, "url")),
436
- source,
437
- sourcePath,
438
- active: true,
439
- at: { group: g, index },
440
- definition: redactSecrets(h),
441
- };
442
- const matcher = stringField(group, "matcher");
443
- if (matcher)
444
- entry.matcher = matcher;
445
- if (plugin)
446
- entry.plugin = plugin.id;
447
- out.push(entry);
448
- }
449
- }
450
- }
451
- return out;
443
+ return Object.entries(config).flatMap(([event, groups]) => Array.isArray(groups)
444
+ ? groups.flatMap((group, g) => groupHooks(event, g, group, source, sourcePath, plugin))
445
+ : []);
446
+ }
447
+ /** The hooks of one matcher group, each with where it sits so an edit can find it again. */
448
+ function groupHooks(event, g, group, source, sourcePath, plugin) {
449
+ if (!isRecord(group) || !Array.isArray(group.hooks))
450
+ return [];
451
+ const matcher = stringField(group, "matcher");
452
+ return group.hooks.flatMap((h, index) => {
453
+ if (!isRecord(h))
454
+ return [];
455
+ const entry = {
456
+ event,
457
+ type: stringField(h, "type") || "command",
458
+ command: stringField(h, "command") || stringField(h, "prompt") || shownUrl(stringField(h, "url")),
459
+ source,
460
+ sourcePath,
461
+ active: true,
462
+ at: { group: g, index },
463
+ definition: redactSecrets(h),
464
+ };
465
+ if (matcher)
466
+ entry.matcher = matcher;
467
+ if (plugin)
468
+ entry.plugin = plugin.id;
469
+ return [entry];
470
+ });
452
471
  }
@@ -0,0 +1,28 @@
1
+ import { type McpServer, type Problem, type SessionProfile, type SettingsLayer, type Trust } from "../../shared/types.ts";
2
+ import { type ScanEnv } from "./env.ts";
3
+ import { type LoadedPlugin } from "./plugins.ts";
4
+ /**
5
+ * What makes a server definition one Claude Code cannot start: a missing command or url, a type it
6
+ * does not know, or a field the docs define holding the wrong kind of value (docs: mcp). It checks
7
+ * the rest when it starts the server.
8
+ */
9
+ export declare function serverProblems(definition: unknown): string[];
10
+ interface Input {
11
+ env: ScanEnv;
12
+ projectDir: string;
13
+ layers: SettingsLayer[];
14
+ /** Managed settings as written, nothing hidden. */
15
+ managed: Record<string, unknown> | null;
16
+ claudeJson: Record<string, unknown> | null;
17
+ trust: Trust;
18
+ profile: SessionProfile;
19
+ /** Receives MCP files that are there but do not parse. */
20
+ problems: Problem[];
21
+ }
22
+ /**
23
+ * Every MCP server definition, each marked with whether Claude Code would start it here
24
+ * (docs: mcp#scope-hierarchy-and-precedence, managed-mcp). ccdesk never connects, so an active
25
+ * server is one that is configured and allowed, not one that is known to work.
26
+ */
27
+ export declare function readMcpServers(input: Input, plugins: LoadedPlugin[]): Promise<McpServer[]>;
28
+ export {};