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
@@ -2,8 +2,11 @@ import { lstat } from "node:fs/promises";
2
2
  import { join } from "node:path";
3
3
  import { allowedScopes, EVERY_FILE, ignoredReason } from "../../shared/scopes.js";
4
4
  import {} from "../../shared/types.js";
5
+ import { redactSecrets } from "../secrets.js";
5
6
  import {} from "./env.js";
6
- import { isRecord, listDir, mainWorktreeRoot, readJsonObject, redactSecrets } from "./files.js";
7
+ import { isFile, listDir, readJsonObject } from "./files.js";
8
+ import { isRecord } from "./data.js";
9
+ import { mainWorktreeRoot } from "./git.js";
7
10
  import { sandboxFilter } from "./sandbox.js";
8
11
  /** Lowest precedence first, so later layers override earlier ones. */
9
12
  const PRECEDENCE = ["user", "project", "local", "managed"];
@@ -132,12 +135,15 @@ export async function settingsFiles(env, dir) {
132
135
  files.push({ scope: "user", path: userFile });
133
136
  return files;
134
137
  }
135
- /** Settings files that apply in `dir`, highest precedence first. */
136
- export async function readSettingsLayers(env, dir) {
138
+ /**
139
+ * Settings files that apply in `dir`, highest precedence first. `managed` is what
140
+ * readManagedSettings read, so a scan reads the managed files once.
141
+ */
142
+ export async function readSettingsLayers(env, dir, managed) {
137
143
  const layers = [];
138
144
  for (const file of await settingsFiles(env, dir)) {
139
145
  if (file.scope === "managed") {
140
- layers.push(await readManagedLayer(env, file.path));
146
+ layers.push(managedLayer(file.path, managed));
141
147
  continue;
142
148
  }
143
149
  const result = await readJsonObject(env, file.path);
@@ -183,10 +189,17 @@ export async function readManagedSettings(env) {
183
189
  sources.push(source);
184
190
  data = data ? mergeSettings(data, result.data) : result.data;
185
191
  }
186
- return { data, sources, dropIns, errors, blocksStart };
192
+ // Server-managed settings, then MDM, outrank the files; by default Claude Code uses the first
193
+ // source that sets a policy key and skips the rest (docs: managed-settings#how-claude-code-combines-managed-sources).
194
+ const candidates = [join(env.claudeDir, "remote-settings.json"), ...(env.managedProfiles ?? [])];
195
+ const higher = [];
196
+ for (const source of candidates)
197
+ if (await isFile(source))
198
+ higher.push(source);
199
+ return { data, sources, dropIns, higher, errors, blocksStart };
187
200
  }
188
- async function readManagedLayer(env, path) {
189
- const { data, sources, dropIns, errors, blocksStart } = await readManagedSettings(env);
201
+ function managedLayer(path, managed) {
202
+ const { data, sources, dropIns, higher, errors, blocksStart } = managed;
190
203
  const layer = { scope: "managed", path, exists: false, data: null };
191
204
  if (data)
192
205
  layer.data = redactSecrets(data);
@@ -197,6 +210,8 @@ async function readManagedLayer(env, path) {
197
210
  layer.error = errors.join("; ");
198
211
  if (blocksStart)
199
212
  layer.blocksStart = true;
213
+ if (higher.length > 0)
214
+ layer.higherSources = higher;
200
215
  return layer;
201
216
  }
202
217
  /** The value Claude Code ends up with for each top-level key, and where it came from. */
@@ -211,90 +226,80 @@ export function effectiveSettings(layers) {
211
226
  }
212
227
  const keys = new Set([...byScope.values()].flatMap((data) => Object.keys(data)));
213
228
  const filterSandbox = sandboxFilter((key, scope) => allowedScopes(key).includes(scope) ? byScope.get(scope)?.[key] : undefined);
214
- const out = [];
215
- for (const key of [...keys].sort()) {
216
- const allowed = allowedScopes(key);
217
- const present = PRECEDENCE.filter((scope) => byScope.get(scope)?.[key] !== undefined);
218
- const valueIn = (scope) => byScope.get(scope)?.[key];
219
- const ignored = present
220
- .filter((scope) => !allowed.includes(scope))
221
- .map((scope) => ({ scope, value: valueIn(scope), reason: ignoredReason(key) }));
222
- let defined = present.filter((scope) => allowed.includes(scope));
223
- // A managed availableModels list is applied as-is; other files' entries are ignored.
224
- if (key === "availableModels" && defined.includes("managed")) {
225
- for (const scope of defined.filter((s) => s !== "managed"))
226
- ignored.push({
227
- scope,
228
- value: valueIn(scope),
229
- reason: "Managed settings set availableModels, so Claude Code uses that list as-is",
230
- });
231
- defined = ["managed"];
232
- }
233
- if (defined.length === 0) {
234
- out.push({ key, value: undefined, from: [], merged: false, overridden: [], ignored });
235
- continue;
236
- }
237
- const values = defined.map((scope) => {
238
- const { value, dropped } = withoutIgnoredFields(key, scope, valueIn(scope), filterSandbox);
239
- for (const d of dropped)
240
- ignored.push({ scope, ...d });
241
- return { scope, value };
242
- });
243
- const top = values[values.length - 1];
244
- if (!top)
245
- continue;
246
- const mergeable = !WHOLE_VALUE_KEYS.has(key) &&
247
- values.every((v) => Array.isArray(v.value) || isRecord(v.value));
248
- const strict = STRICTEST[key];
249
- const decided = strict && strictWinner(strict, values);
250
- if (strict && decided) {
251
- const { winner } = decided;
252
- // Only files that may just tighten it set the key, and none tightens it past the default.
253
- if (!winner)
254
- for (const v of values)
255
- ignored.push({ ...v, reason: "it is not stricter than Claude Code's default" });
256
- out.push({
257
- key,
258
- value: winner?.value,
259
- from: winner ? [winner.scope] : [],
260
- merged: false,
261
- overridden: winner ? values.filter((v) => v !== winner).reverse() : [],
262
- ignored,
263
- note: strictNote(strict),
264
- });
265
- }
266
- else if (mergeable && values.length > 1) {
267
- out.push({
268
- key,
269
- value: withStrictFields(key, values.slice(1).reduce((acc, v) => mergeKey(key, acc, v.value), values[0]?.value), values),
270
- from: defined,
271
- merged: true,
272
- overridden: [],
273
- ignored,
274
- });
275
- }
276
- else if (isRecord(top.value)) {
277
- out.push({
278
- key,
279
- value: withStrictFields(key, top.value, values),
280
- from: [top.scope],
281
- merged: false,
282
- overridden: values.slice(0, -1).reverse(),
283
- ignored,
284
- });
285
- }
286
- else {
287
- out.push({
288
- key,
289
- value: top.value,
290
- from: [top.scope],
291
- merged: false,
292
- overridden: values.slice(0, -1).reverse(),
293
- ignored,
294
- });
295
- }
229
+ return [...keys]
230
+ .sort()
231
+ .map((key) => effectiveSetting(key, (scope) => byScope.get(scope)?.[key], filterSandbox));
232
+ }
233
+ /** One key's value, from its value in each scope, lowest precedence first. */
234
+ function effectiveSetting(key, valueIn, filterSandbox) {
235
+ const { defined, ignored } = scopesThatCount(key, valueIn);
236
+ const values = defined.map((scope) => {
237
+ const { value, dropped } = withoutIgnoredFields(key, scope, valueIn(scope), filterSandbox);
238
+ for (const d of dropped)
239
+ ignored.push({ scope, ...d });
240
+ return { scope, value };
241
+ });
242
+ const top = values.at(-1);
243
+ if (!top)
244
+ return { key, value: undefined, from: [], merged: false, overridden: [], ignored };
245
+ const strict = STRICTEST[key];
246
+ const decided = strict && strictWinner(strict, values);
247
+ if (strict && decided) {
248
+ const { winner } = decided;
249
+ // Only files that may just tighten it set the key, and none tightens it past the default.
250
+ if (!winner)
251
+ for (const v of values)
252
+ ignored.push({ ...v, reason: "it is not stricter than Claude Code's default" });
253
+ return {
254
+ key,
255
+ value: winner?.value,
256
+ from: winner ? [winner.scope] : [],
257
+ merged: false,
258
+ overridden: winner ? values.filter((v) => v !== winner).reverse() : [],
259
+ ignored,
260
+ note: strictNote(strict),
261
+ };
296
262
  }
297
- return out;
263
+ const mergeable = !WHOLE_VALUE_KEYS.has(key) && values.every((v) => Array.isArray(v.value) || isRecord(v.value));
264
+ if (mergeable && values.length > 1)
265
+ return {
266
+ key,
267
+ value: withStrictFields(key, values.slice(1).reduce((acc, v) => mergeKey(key, acc, v.value), values[0]?.value), values),
268
+ from: defined,
269
+ merged: true,
270
+ overridden: [],
271
+ ignored,
272
+ };
273
+ return {
274
+ key,
275
+ value: withStrictFields(key, top.value, values),
276
+ from: [top.scope],
277
+ merged: false,
278
+ overridden: values.slice(0, -1).reverse(),
279
+ ignored,
280
+ };
281
+ }
282
+ /**
283
+ * The scopes whose value for `key` counts, lowest precedence first, and the values Claude Code
284
+ * ignores: ones from files the key is not read from, and, once managed settings set
285
+ * availableModels, every other file's list, since Claude Code uses the managed one as-is.
286
+ */
287
+ function scopesThatCount(key, valueIn) {
288
+ const allowed = allowedScopes(key);
289
+ const present = PRECEDENCE.filter((scope) => valueIn(scope) !== undefined);
290
+ const ignored = present
291
+ .filter((scope) => !allowed.includes(scope))
292
+ .map((scope) => ({ scope, value: valueIn(scope), reason: ignoredReason(key) }));
293
+ const defined = present.filter((scope) => allowed.includes(scope));
294
+ if (key !== "availableModels" || !defined.includes("managed"))
295
+ return { defined, ignored };
296
+ for (const scope of defined.filter((s) => s !== "managed"))
297
+ ignored.push({
298
+ scope,
299
+ value: valueIn(scope),
300
+ reason: "Managed settings set availableModels, so Claude Code uses that list as-is",
301
+ });
302
+ return { defined: ["managed"], ignored };
298
303
  }
299
304
  /** An object setting with each nested strictness exception (such as permissions.x) applied. */
300
305
  function withStrictFields(key, value, values) {
@@ -305,9 +310,9 @@ function withStrictFields(key, value, values) {
305
310
  const [head, field] = path.split(".");
306
311
  if (head !== key || !field)
307
312
  continue;
308
- const fieldValues = values
309
- .filter((v) => isRecord(v.value) && v.value[field] !== undefined)
310
- .map((v) => ({ scope: v.scope, value: v.value[field] }));
313
+ const fieldValues = values.flatMap((v) => isRecord(v.value) && v.value[field] !== undefined
314
+ ? [{ scope: v.scope, value: v.value[field] }]
315
+ : []);
311
316
  const decided = strictWinner(strict, fieldValues);
312
317
  if (decided)
313
318
  out = { ...out, [field]: decided.winner?.value };
@@ -0,0 +1,10 @@
1
+ import { type BuiltinTool, type Hook, type Permission, type PermissionList } from "../../shared/types.ts";
2
+ /** `Tool` or `Tool(specifier)`; `Tool(*)` names the whole tool too (docs: permissions#permission-rule-syntax). */
3
+ export declare function parseRule(rule: string): {
4
+ tool: string;
5
+ specifier: string | null;
6
+ };
7
+ /** Deny and ask rules may glob the tool name; allow rules name it exactly (docs: permissions#tool-name-wildcards). */
8
+ export declare function namesTool(list: PermissionList, tool: string, name: string): boolean;
9
+ /** Every built-in tool, with the rules and hooks that apply to it here. */
10
+ export declare function builtinTools(permissions: Permission[], hooks: Hook[]): BuiltinTool[];
@@ -50,31 +50,39 @@ function matcherCovers(matcher, name) {
50
50
  return false;
51
51
  }
52
52
  }
53
+ /**
54
+ * The rules about one tool: `whole` name the tool itself and decide it, `scoped` apply to some of
55
+ * its calls only. EndConversation ignores rules (see builtinTools).
56
+ */
57
+ function rulesFor(name, rules) {
58
+ const whole = [];
59
+ const scoped = [];
60
+ if (name === EXEMPT)
61
+ return { whole, scoped };
62
+ for (const r of rules) {
63
+ const { tool, specifier } = parseRule(r.rule);
64
+ if (specifier === null) {
65
+ if (namesTool(r.list, tool, name))
66
+ whole.push(r);
67
+ // Monitor's commands follow the Bash rules; its WebSocket watches do not
68
+ // (docs: tools-reference#monitor-tool).
69
+ else if (name === "Monitor" && namesTool(r.list, tool, "Bash"))
70
+ scoped.push(r);
71
+ }
72
+ else if (tool === name && UNCHECKED_PATH_RULE_TOOLS.has(tool)) {
73
+ scoped.push({ ...r, unchecked: true });
74
+ }
75
+ else if (tool === name || coveredTools(tool, r.list).includes(name))
76
+ scoped.push(r);
77
+ }
78
+ return { whole, scoped };
79
+ }
53
80
  /** Every built-in tool, with the rules and hooks that apply to it here. */
54
81
  export function builtinTools(permissions, hooks) {
55
82
  const rules = permissions.flatMap((p) => p.kind === "rule" && p.inEffect ? [{ list: p.list, rule: p.rule, scope: p.scope }] : []);
56
83
  const preToolUse = hooks.filter((h) => h.active && h.event === "PreToolUse");
57
84
  const tools = table.tools.map(({ name, description, asks }) => {
58
- const whole = [];
59
- const scoped = [];
60
- for (const r of rules) {
61
- if (name === EXEMPT)
62
- break;
63
- const { tool, specifier } = parseRule(r.rule);
64
- if (specifier === null) {
65
- if (namesTool(r.list, tool, name))
66
- whole.push(r);
67
- // Monitor's commands follow the Bash rules; its WebSocket watches do not
68
- // (docs: tools-reference#monitor-tool).
69
- else if (name === "Monitor" && namesTool(r.list, tool, "Bash"))
70
- scoped.push(r);
71
- }
72
- else if (tool === name && UNCHECKED_PATH_RULE_TOOLS.has(tool)) {
73
- scoped.push({ ...r, unchecked: true });
74
- }
75
- else if (tool === name || coveredTools(tool, r.list).includes(name))
76
- scoped.push(r);
77
- }
85
+ const { whole, scoped } = rulesFor(name, rules);
78
86
  const decidedBy = CHECK_ORDER.map((list) => whole.find((r) => r.list === list)).find(Boolean);
79
87
  const tool = {
80
88
  name,
@@ -0,0 +1,19 @@
1
+ import { type PermissionList, type Usage } from "../../shared/types.ts";
2
+ import { type ScanEnv } from "./env.ts";
3
+ /** A rule in effect here, as the scan found it. */
4
+ export interface RuleInEffect {
5
+ list: PermissionList;
6
+ rule: string;
7
+ }
8
+ /** A count that stopped before it finished, because it took too long or nobody wanted it. */
9
+ export declare class CountStopped extends Error {
10
+ }
11
+ /**
12
+ * A counter with its own cache of transcripts and its own queue: counts made through it run one at
13
+ * a time and reuse what earlier ones read. Each core has one.
14
+ */
15
+ export declare function usageCounter(): (env: ScanEnv, dir: string, rules: RuleInEffect[], { signal, timeLimitMs, maxCachedCalls, }?: {
16
+ signal?: AbortSignal;
17
+ timeLimitMs?: number;
18
+ maxCachedCalls?: number;
19
+ }) => Promise<Usage>;