ccdesk 0.2.1 → 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 +54 -0
- package/README.md +9 -5
- package/dist/core/agent-edit.d.ts +6 -0
- package/dist/{server → core}/agent-edit.js +21 -22
- 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 +116 -95
- 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 +5 -3
- package/dist/core/scan/context.d.ts +13 -0
- package/dist/{server → core}/scan/context.js +58 -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 +10 -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 +6 -9
- 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 -92
- 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/core/settings-edit.js +216 -0
- package/dist/core/watch.d.ts +45 -0
- package/dist/{server → core}/watch.js +129 -20
- 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 -478
- package/dist/server/scan/path-test.js +0 -121
- package/dist/server/schema.js +0 -15
- package/dist/server/secrets.js +0 -63
- package/dist/server/settings-edit.js +0 -221
- package/dist/ui/assets/index-XqJwI3k9.js +0 -60
|
@@ -1,221 +0,0 @@
|
|
|
1
|
-
import { stat } from "node:fs/promises";
|
|
2
|
-
import {} from "ajv";
|
|
3
|
-
import { allowedScopes, ignoredReason } from "../shared/scopes.js";
|
|
4
|
-
import schema from "../shared/settings.schema.json" with { type: "json" };
|
|
5
|
-
import {} from "../shared/types.js";
|
|
6
|
-
import {} from "./scan/env.js";
|
|
7
|
-
import { isRecord, jsonSyntaxError, redactSecrets, revisionOf } from "./scan/files.js";
|
|
8
|
-
import { isSkippedMcpRule } from "./scan/permissions.js";
|
|
9
|
-
import { settingsFiles } from "./scan/settings.js";
|
|
10
|
-
import { ajv, schemaErrors } from "./schema.js";
|
|
11
|
-
import { hasHiddenValues, shown } from "./secrets.js";
|
|
12
|
-
import { RefusedError, readIfExists, recordChange, serially, writeAtomic } from "./write.js";
|
|
13
|
-
const properties = schema.definitions.ClaudeCodeSettings.properties;
|
|
14
|
-
const validators = new Map();
|
|
15
|
-
function validatorFor(key) {
|
|
16
|
-
if (!Object.hasOwn(properties, key))
|
|
17
|
-
return null;
|
|
18
|
-
let validate = validators.get(key);
|
|
19
|
-
if (!validate) {
|
|
20
|
-
validate = ajv.compile(properties[key]);
|
|
21
|
-
validators.set(key, validate);
|
|
22
|
-
}
|
|
23
|
-
return validate;
|
|
24
|
-
}
|
|
25
|
-
async function editableFile(env, dir, pick, missing = "That is not a settings file Claude Code reads here.") {
|
|
26
|
-
const file = (await settingsFiles(env, dir)).find(pick);
|
|
27
|
-
if (!file)
|
|
28
|
-
throw new RefusedError(missing);
|
|
29
|
-
if (file.scope === "managed")
|
|
30
|
-
throw new RefusedError("Managed settings are set by your organisation.");
|
|
31
|
-
if (file.scope !== "user" && !(await stat(dir).catch(() => null))?.isDirectory())
|
|
32
|
-
throw new RefusedError(`${dir} is not a directory on this machine.`);
|
|
33
|
-
return file;
|
|
34
|
-
}
|
|
35
|
-
async function readSettings(env, path) {
|
|
36
|
-
const raw = await readIfExists(env, path);
|
|
37
|
-
if (raw === null || raw.trim() === "")
|
|
38
|
-
return { raw, data: {} };
|
|
39
|
-
let parsed;
|
|
40
|
-
try {
|
|
41
|
-
parsed = JSON.parse(raw);
|
|
42
|
-
}
|
|
43
|
-
catch (error) {
|
|
44
|
-
throw new RefusedError(`${path} is ${jsonSyntaxError(error)}; fix it by hand first.`);
|
|
45
|
-
}
|
|
46
|
-
if (!isRecord(parsed))
|
|
47
|
-
throw new RefusedError(`${path} does not hold a JSON object.`);
|
|
48
|
-
return { raw, data: parsed };
|
|
49
|
-
}
|
|
50
|
-
/**
|
|
51
|
-
* Works out the file change that sets or removes one key, refusing what ccdesk cannot do safely.
|
|
52
|
-
* `checksHiddenValues` is for a key whose edit checks the part it changes itself, so values it
|
|
53
|
-
* leaves alone are written back exactly as they were.
|
|
54
|
-
*/
|
|
55
|
-
async function planKey(env, file, key, value, checksHiddenValues = false) {
|
|
56
|
-
if (!allowedScopes(key).includes(file.scope))
|
|
57
|
-
throw new RefusedError(`${ignoredReason(key)}, so a ${file.scope} value would do nothing.`);
|
|
58
|
-
const validate = validatorFor(key);
|
|
59
|
-
if (!validate)
|
|
60
|
-
throw new RefusedError(`${key} is not in Claude Code's settings schema (${schema.$comment}).`);
|
|
61
|
-
const { path } = file;
|
|
62
|
-
const { raw, data } = await readSettings(env, path);
|
|
63
|
-
const nextValue = value(data);
|
|
64
|
-
// ccdesk never shows hidden values, so it must not write them either.
|
|
65
|
-
if (!checksHiddenValues &&
|
|
66
|
-
(hasHiddenValues({ [key]: data[key] }) || hasHiddenValues({ [key]: nextValue })))
|
|
67
|
-
throw new RefusedError(`${key} holds hidden values; edit it in the file instead.`);
|
|
68
|
-
if (raw !== null && JSON.stringify(nextValue) === JSON.stringify(data[key]))
|
|
69
|
-
throw new RefusedError("This file already says that; there is nothing to change.");
|
|
70
|
-
const before = raw === null ? null : shown(data);
|
|
71
|
-
// The diff is rendered from the parsed file, so a file in another layout is reported as
|
|
72
|
-
// reformatted instead of diffing its raw text.
|
|
73
|
-
const reformats = raw !== null && raw.trim() !== "" && raw !== `${JSON.stringify(data, null, 2)}\n`;
|
|
74
|
-
const base = { path, before, revision: revisionOf(raw), reformats };
|
|
75
|
-
const errors = nextValue === undefined ? [] : schemaErrors(validate, nextValue, key);
|
|
76
|
-
if (errors.length > 0)
|
|
77
|
-
return {
|
|
78
|
-
path,
|
|
79
|
-
scope: file.scope,
|
|
80
|
-
raw,
|
|
81
|
-
next: null,
|
|
82
|
-
preview: { ...base, after: before ?? "", errors },
|
|
83
|
-
};
|
|
84
|
-
const next = { ...data };
|
|
85
|
-
if (nextValue === undefined)
|
|
86
|
-
delete next[key];
|
|
87
|
-
else
|
|
88
|
-
next[key] = nextValue;
|
|
89
|
-
return {
|
|
90
|
-
path,
|
|
91
|
-
scope: file.scope,
|
|
92
|
-
raw,
|
|
93
|
-
next: `${JSON.stringify(next, null, 2)}\n`,
|
|
94
|
-
preview: { ...base, after: shown(next), errors: [] },
|
|
95
|
-
};
|
|
96
|
-
}
|
|
97
|
-
async function planSettingsEdit(env, edit) {
|
|
98
|
-
const file = await editableFile(env, edit.dir, (f) => f.scope === edit.scope && !f.legacy, edit.scope === "project"
|
|
99
|
-
? "In your home directory this file is your user settings; edit it as User."
|
|
100
|
-
: undefined);
|
|
101
|
-
return planKey(env, file, edit.key, () => edit.value);
|
|
102
|
-
}
|
|
103
|
-
/** One rule taken out of its list and/or added to another, applied to the file as it is on disk. */
|
|
104
|
-
async function planPermissionEdit(env, edit) {
|
|
105
|
-
const file = await editableFile(env, edit.dir, (f) => f.path === edit.file);
|
|
106
|
-
return planKey(env, file, "permissions", (data) => {
|
|
107
|
-
const permissions = isRecord(data.permissions) ? { ...data.permissions } : {};
|
|
108
|
-
const { remove, add } = edit;
|
|
109
|
-
if (remove) {
|
|
110
|
-
const list = Array.isArray(permissions[remove.list])
|
|
111
|
-
? permissions[remove.list]
|
|
112
|
-
: [];
|
|
113
|
-
if (!list.includes(remove.rule))
|
|
114
|
-
throw new RefusedError(`${remove.rule} is no longer in ${remove.list} in this file.`);
|
|
115
|
-
const kept = list.filter((r) => r !== remove.rule);
|
|
116
|
-
if (kept.length > 0)
|
|
117
|
-
permissions[remove.list] = kept;
|
|
118
|
-
else
|
|
119
|
-
delete permissions[remove.list];
|
|
120
|
-
}
|
|
121
|
-
if (add) {
|
|
122
|
-
if (isSkippedMcpRule(add.rule))
|
|
123
|
-
throw new RefusedError("Claude Code skips an MCP rule with parentheses in a settings file. To match a parameter of an MCP tool, pass a deny rule with --disallowedTools.");
|
|
124
|
-
const list = Array.isArray(permissions[add.list]) ? permissions[add.list] : [];
|
|
125
|
-
if (!list.includes(add.rule))
|
|
126
|
-
permissions[add.list] = [...list, add.rule];
|
|
127
|
-
}
|
|
128
|
-
return Object.keys(permissions).length > 0 ? permissions : undefined;
|
|
129
|
-
});
|
|
130
|
-
}
|
|
131
|
-
/** The matcher a group is written with; a missing or empty one matches everything. */
|
|
132
|
-
function matcherOf(group) {
|
|
133
|
-
return isRecord(group) && typeof group.matcher === "string" ? group.matcher : "";
|
|
134
|
-
}
|
|
135
|
-
/** One hook taken out, replaced or added, applied to the file as it is on disk. */
|
|
136
|
-
async function planHookEdit(env, edit) {
|
|
137
|
-
const file = await editableFile(env, edit.dir, (f) => f.path === edit.file);
|
|
138
|
-
return planKey(env, file, "hooks", (data) => {
|
|
139
|
-
const hooks = isRecord(data.hooks)
|
|
140
|
-
? structuredClone(data.hooks)
|
|
141
|
-
: {};
|
|
142
|
-
const groupsOf = (event) => {
|
|
143
|
-
const groups = hooks[event];
|
|
144
|
-
return Array.isArray(groups) ? groups : [];
|
|
145
|
-
};
|
|
146
|
-
const { remove, add } = edit;
|
|
147
|
-
if (remove) {
|
|
148
|
-
const groups = groupsOf(remove.event);
|
|
149
|
-
const group = groups[remove.at.group];
|
|
150
|
-
const list = isRecord(group) && Array.isArray(group.hooks) ? group.hooks : null;
|
|
151
|
-
const hook = list?.[remove.at.index];
|
|
152
|
-
if (!list || JSON.stringify(redactSecrets(hook)) !== JSON.stringify(remove.definition))
|
|
153
|
-
throw new RefusedError("This hook changed in the file since ccdesk read it; review it again.");
|
|
154
|
-
// The page saw this hook with its header values hidden, so a changed copy would lose them.
|
|
155
|
-
if (add && hasHiddenValues(hook))
|
|
156
|
-
throw new RefusedError("This hook sends header values ccdesk does not show; edit it in the file, or remove it and add it again.");
|
|
157
|
-
const matcher = add?.matcher ?? "";
|
|
158
|
-
// A hook alone in its group keeps the group when only its matcher changes, unless another
|
|
159
|
-
// group already has that matcher.
|
|
160
|
-
const inPlace = add?.event === remove.event &&
|
|
161
|
-
(matcher === matcherOf(group) ||
|
|
162
|
-
(list.length === 1 && !groups.some((g) => g !== group && matcherOf(g) === matcher)));
|
|
163
|
-
if (add && inPlace && isRecord(group)) {
|
|
164
|
-
list[remove.at.index] = add.definition;
|
|
165
|
-
if (matcher)
|
|
166
|
-
group.matcher = matcher;
|
|
167
|
-
else
|
|
168
|
-
delete group.matcher;
|
|
169
|
-
return hooks;
|
|
170
|
-
}
|
|
171
|
-
list.splice(remove.at.index, 1);
|
|
172
|
-
if (list.length === 0)
|
|
173
|
-
groups.splice(remove.at.group, 1);
|
|
174
|
-
if (groups.length === 0 && add?.event !== remove.event)
|
|
175
|
-
delete hooks[remove.event];
|
|
176
|
-
}
|
|
177
|
-
if (add) {
|
|
178
|
-
if (hasHiddenValues(add.definition))
|
|
179
|
-
throw new RefusedError("ccdesk does not write header values it would then hide; add this hook in the file instead.");
|
|
180
|
-
const groups = groupsOf(add.event);
|
|
181
|
-
const matcher = add.matcher ?? "";
|
|
182
|
-
const group = groups.find((g) => matcherOf(g) === matcher && isRecord(g) && Array.isArray(g.hooks));
|
|
183
|
-
if (isRecord(group) && Array.isArray(group.hooks))
|
|
184
|
-
group.hooks.push(add.definition);
|
|
185
|
-
else
|
|
186
|
-
groups.push({ ...(matcher ? { matcher } : {}), hooks: [add.definition] });
|
|
187
|
-
hooks[add.event] = groups;
|
|
188
|
-
}
|
|
189
|
-
return Object.keys(hooks).length > 0 ? hooks : undefined;
|
|
190
|
-
}, true);
|
|
191
|
-
}
|
|
192
|
-
/** Writes a planned edit, but only if the file is still the revision the preview was made from. */
|
|
193
|
-
async function commit(env, plan, expectedRevision) {
|
|
194
|
-
if (plan.next === null)
|
|
195
|
-
throw new RefusedError(plan.preview.errors.join("; "));
|
|
196
|
-
if (plan.preview.revision !== expectedRevision)
|
|
197
|
-
throw new RefusedError("The file changed since you reviewed this edit; review it again.");
|
|
198
|
-
const next = plan.next;
|
|
199
|
-
return recordChange(env, { kind: "settings", target: plan.path, before: plan.raw }, async () => {
|
|
200
|
-
await writeAtomic(env, plan.path, next, expectedRevision);
|
|
201
|
-
return { target: plan.path, revision: revisionOf(next) };
|
|
202
|
-
});
|
|
203
|
-
}
|
|
204
|
-
export async function previewSettingsEdit(env, edit) {
|
|
205
|
-
return (await planSettingsEdit(env, edit)).preview;
|
|
206
|
-
}
|
|
207
|
-
export function saveSettingsEdit(env, edit, expectedRevision) {
|
|
208
|
-
return serially(async () => commit(env, await planSettingsEdit(env, edit), expectedRevision));
|
|
209
|
-
}
|
|
210
|
-
export async function previewPermissionEdit(env, edit) {
|
|
211
|
-
return (await planPermissionEdit(env, edit)).preview;
|
|
212
|
-
}
|
|
213
|
-
export function savePermissionEdit(env, edit, expectedRevision) {
|
|
214
|
-
return serially(async () => commit(env, await planPermissionEdit(env, edit), expectedRevision));
|
|
215
|
-
}
|
|
216
|
-
export async function previewHookEdit(env, edit) {
|
|
217
|
-
return (await planHookEdit(env, edit)).preview;
|
|
218
|
-
}
|
|
219
|
-
export function saveHookEdit(env, edit, expectedRevision) {
|
|
220
|
-
return serially(async () => commit(env, await planHookEdit(env, edit), expectedRevision));
|
|
221
|
-
}
|