ccdesk 0.2.3 → 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 +21 -0
- package/README.md +1 -1
- package/dist/core/agent-edit.d.ts +1 -1
- package/dist/core/agent-edit.js +2 -2
- package/dist/core/backups.d.ts +1 -1
- package/dist/core/backups.js +29 -5
- package/dist/core/index.d.ts +6 -6
- package/dist/core/index.js +14 -14
- package/dist/core/mcp-edit.d.ts +1 -3
- package/dist/core/mcp-edit.js +15 -11
- package/dist/core/mutations.d.ts +1 -1
- package/dist/core/mutations.js +2 -2
- package/dist/core/open.d.ts +2 -5
- package/dist/core/open.js +6 -8
- package/dist/core/scan/data.d.ts +6 -0
- package/dist/core/scan/data.js +8 -0
- package/dist/core/scan/env.d.ts +2 -0
- package/dist/core/scan/env.js +8 -1
- package/dist/core/scan/files.js +2 -2
- package/dist/core/scan/git.d.ts +6 -0
- package/dist/core/scan/git.js +8 -0
- package/dist/core/scan/index.js +5 -2
- package/dist/core/scan/mcp.js +2 -1
- package/dist/core/scan/projects.js +2 -1
- package/dist/core/scan/settings.d.ts +2 -0
- package/dist/core/scan/settings.js +12 -3
- package/dist/core/scan/usage.d.ts +0 -6
- package/dist/core/scan/usage.js +7 -4
- package/dist/core/secrets.d.ts +3 -2
- package/dist/core/secrets.js +49 -29
- package/dist/core/settings-edit.d.ts +3 -3
- package/dist/core/settings-edit.js +4 -4
- package/dist/core/write.d.ts +0 -2
- package/dist/core/write.js +2 -4
- package/dist/shared/memory.d.ts +5 -1
- package/dist/shared/memory.js +18 -2
- package/dist/shared/types.d.ts +5 -0
- package/dist/ui/assets/{index-CH1HoC38.js → index-C9qqEI5Z.js} +2 -2
- package/dist/ui/index.html +1 -1
- package/package.json +1 -1
package/dist/core/scan/usage.js
CHANGED
|
@@ -75,8 +75,6 @@ export function usageCounter() {
|
|
|
75
75
|
return current.promise;
|
|
76
76
|
};
|
|
77
77
|
}
|
|
78
|
-
/** Counts with a cache and queue shared by every caller in the process; use a core's instead. */
|
|
79
|
-
export const readUsage = usageCounter();
|
|
80
78
|
function stoppedBecause(signal) {
|
|
81
79
|
return new CountStopped(signal.reason instanceof DOMException && signal.reason.name === "TimeoutError"
|
|
82
80
|
? "Counting tool calls took too long and stopped; what was read is kept, so the next count continues."
|
|
@@ -334,8 +332,13 @@ function approvals(calls, rules) {
|
|
|
334
332
|
for (const c of calls) {
|
|
335
333
|
if (c.byYou !== true)
|
|
336
334
|
continue;
|
|
337
|
-
if (c.rule)
|
|
338
|
-
|
|
335
|
+
if (!c.rule)
|
|
336
|
+
continue;
|
|
337
|
+
const group = groups.get(c.rule);
|
|
338
|
+
if (group)
|
|
339
|
+
group.push(c);
|
|
340
|
+
else
|
|
341
|
+
groups.set(c.rule, [c]);
|
|
339
342
|
}
|
|
340
343
|
const refused = calls.filter((c) => c.byYou === false);
|
|
341
344
|
const out = [];
|
package/dist/core/secrets.d.ts
CHANGED
|
@@ -38,7 +38,8 @@ export declare function hasHiddenValues(value: unknown): boolean;
|
|
|
38
38
|
export declare function hiddenValues(text: string | null): string | null;
|
|
39
39
|
/**
|
|
40
40
|
* The secret text in a value, to take out of a message that may quote it: an env or header value
|
|
41
|
-
* or a hidden argument whole, the
|
|
42
|
-
* password and query. Longest first, so a
|
|
41
|
+
* or a hidden argument whole, the hidden end of one whose start is shown (`--token=value`,
|
|
42
|
+
* `Authorization: Bearer value`), and a URL's user name, password and query. Longest first, so a
|
|
43
|
+
* secret that contains another is taken out whole.
|
|
43
44
|
*/
|
|
44
45
|
export declare function secretTexts(value: unknown): string[];
|
package/dist/core/secrets.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
|
|
2
|
-
import { FRONTMATTER, isRecord } from "./scan/data.js";
|
|
2
|
+
import { FRONTMATTER, isRecord, parseJson } from "./scan/data.js";
|
|
3
3
|
/**
|
|
4
4
|
* A server URL as ccdesk shows it: a user name, password or query string can carry a key, so they
|
|
5
5
|
* are hidden, as Claude Code hides the URL in its own status output (docs: mcp).
|
|
@@ -21,34 +21,52 @@ export function shownUrl(url) {
|
|
|
21
21
|
return url.replace(/\/\/[^/@]*@/, "//•••@").replace(/\?.*$/, "?•••");
|
|
22
22
|
}
|
|
23
23
|
}
|
|
24
|
-
/** A flag whose value is a secret, such as `--token` or `--
|
|
25
|
-
const SECRET_FLAG = /^--(token|api[-_]?key|apikey|password|secret|auth
|
|
24
|
+
/** A flag whose value is a secret, such as `--token`, `--api-key`, `--client-secret` or `--auth-token`. */
|
|
25
|
+
const SECRET_FLAG = /^--([\w-]*[-_])?(token|api[-_]?key|apikey|password|secret|auth\w*|key)$/i;
|
|
26
|
+
/** A flag whose value is an HTTP header, as mcp-remote takes `--header "Authorization: Bearer …"`. */
|
|
27
|
+
const HEADER_FLAG = /^(--header|-H)$/;
|
|
28
|
+
/** An argument that is itself a credential header or an Authorization value. */
|
|
29
|
+
const SECRET_HEADER = /^([\w-]*(authorization|api-key|token|cookie)\s*:|(bearer|basic)\s+\S)/i;
|
|
26
30
|
/** A value that starts like a common API key or token. */
|
|
27
31
|
const KEY_PREFIX = /^(sk-|ghp_|gho_|ghu_|ghs_|ghr_|github_pat_|xox[bp]-|glpat-|AKIA[0-9A-Z]{16})/;
|
|
32
|
+
/** A header as ccdesk shows it: the name and the `Bearer` or `Basic` scheme stay, the rest is hidden. */
|
|
33
|
+
function shownHeader(header) {
|
|
34
|
+
const kept = /^([^:]*:\s*)?((bearer|basic)\s+)?/i.exec(header)?.[0] ?? "";
|
|
35
|
+
return `${kept}•••`;
|
|
36
|
+
}
|
|
28
37
|
/**
|
|
29
38
|
* A server's command-line arguments as ccdesk shows them: the value after a secret flag, given as
|
|
30
|
-
* `--token value` or `--token=value`, a value
|
|
31
|
-
* a
|
|
39
|
+
* `--token value` or `--token=value`, a header's value after `--header` or `-H`, a credential
|
|
40
|
+
* header or Authorization value on its own, a value that starts like a known key, and the
|
|
41
|
+
* credentials in a URL are hidden.
|
|
32
42
|
*/
|
|
33
43
|
function shownArgs(args) {
|
|
34
|
-
return args.map((arg, i) =>
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
const equals = arg.indexOf("=");
|
|
41
|
-
if (equals > 0) {
|
|
42
|
-
const name = arg.slice(0, equals);
|
|
43
|
-
if (SECRET_FLAG.test(name) || KEY_PREFIX.test(arg.slice(equals + 1)))
|
|
44
|
-
return `${name}=•••`;
|
|
45
|
-
}
|
|
46
|
-
if (KEY_PREFIX.test(arg))
|
|
44
|
+
return args.map((arg, i) => (typeof arg === "string" ? shownArg(arg, args[i - 1]) : arg));
|
|
45
|
+
}
|
|
46
|
+
/** One argument as ccdesk shows it, given the one before it. */
|
|
47
|
+
function shownArg(arg, previous) {
|
|
48
|
+
if (typeof previous === "string" && !arg.startsWith("-")) {
|
|
49
|
+
if (SECRET_FLAG.test(previous))
|
|
47
50
|
return "•••";
|
|
48
|
-
if (
|
|
49
|
-
return
|
|
50
|
-
|
|
51
|
-
|
|
51
|
+
if (HEADER_FLAG.test(previous))
|
|
52
|
+
return shownHeader(arg);
|
|
53
|
+
}
|
|
54
|
+
const equals = arg.indexOf("=");
|
|
55
|
+
if (equals > 0) {
|
|
56
|
+
const name = arg.slice(0, equals);
|
|
57
|
+
const value = arg.slice(equals + 1);
|
|
58
|
+
if (HEADER_FLAG.test(name))
|
|
59
|
+
return `${name}=${shownHeader(value)}`;
|
|
60
|
+
if (SECRET_FLAG.test(name) || KEY_PREFIX.test(value))
|
|
61
|
+
return `${name}=•••`;
|
|
62
|
+
}
|
|
63
|
+
if (SECRET_HEADER.test(arg))
|
|
64
|
+
return shownHeader(arg);
|
|
65
|
+
if (KEY_PREFIX.test(arg))
|
|
66
|
+
return "•••";
|
|
67
|
+
if (/^https?:\/\//i.test(arg))
|
|
68
|
+
return shownUrl(arg);
|
|
69
|
+
return arg;
|
|
52
70
|
}
|
|
53
71
|
export function redactSecrets(value) {
|
|
54
72
|
if (Array.isArray(value))
|
|
@@ -86,7 +104,7 @@ export function shownText(text) {
|
|
|
86
104
|
return null;
|
|
87
105
|
let data;
|
|
88
106
|
try {
|
|
89
|
-
data =
|
|
107
|
+
data = parseJson(text);
|
|
90
108
|
}
|
|
91
109
|
catch {
|
|
92
110
|
return shownFrontmatter(text);
|
|
@@ -114,7 +132,7 @@ function shownFrontmatter(text) {
|
|
|
114
132
|
/** A file named .json that does not parse is not shown: its text could still hold a secret. */
|
|
115
133
|
export function shownJsonFile(text) {
|
|
116
134
|
try {
|
|
117
|
-
|
|
135
|
+
parseJson(text);
|
|
118
136
|
}
|
|
119
137
|
catch {
|
|
120
138
|
return "(a JSON file ccdesk could not parse, so its text is not shown; open it to see it)\n";
|
|
@@ -156,7 +174,7 @@ export function hasHiddenValues(value) {
|
|
|
156
174
|
*/
|
|
157
175
|
export function hiddenValues(text) {
|
|
158
176
|
try {
|
|
159
|
-
const data = text === null ? null :
|
|
177
|
+
const data = text === null ? null : parseJson(text);
|
|
160
178
|
return JSON.stringify(hiddenIn(data).map(({ path, raw }) => [path, raw]));
|
|
161
179
|
}
|
|
162
180
|
catch {
|
|
@@ -165,8 +183,9 @@ export function hiddenValues(text) {
|
|
|
165
183
|
}
|
|
166
184
|
/**
|
|
167
185
|
* The secret text in a value, to take out of a message that may quote it: an env or header value
|
|
168
|
-
* or a hidden argument whole, the
|
|
169
|
-
* password and query. Longest first, so a
|
|
186
|
+
* or a hidden argument whole, the hidden end of one whose start is shown (`--token=value`,
|
|
187
|
+
* `Authorization: Bearer value`), and a URL's user name, password and query. Longest first, so a
|
|
188
|
+
* secret that contains another is taken out whole.
|
|
170
189
|
*/
|
|
171
190
|
export function secretTexts(value) {
|
|
172
191
|
const found = hiddenIn(value).flatMap(({ raw, shown: hidden }) => {
|
|
@@ -174,8 +193,9 @@ export function secretTexts(value) {
|
|
|
174
193
|
return [];
|
|
175
194
|
if (hidden === "•••")
|
|
176
195
|
return [raw];
|
|
177
|
-
|
|
178
|
-
|
|
196
|
+
// The start was kept, as in `--token=•••` or `Authorization: Bearer •••`: the rest is the secret.
|
|
197
|
+
if (typeof hidden === "string" && hidden.endsWith("•••") && raw.startsWith(hidden.slice(0, -3)))
|
|
198
|
+
return [raw.slice(hidden.length - 3)];
|
|
179
199
|
if (shownUrl(raw) === raw)
|
|
180
200
|
return [];
|
|
181
201
|
const url = URL.parse(raw);
|
|
@@ -2,8 +2,8 @@ import { type Change, type HookEdit, type PermissionEdit, type SettingsEdit, typ
|
|
|
2
2
|
import { type ScanEnv } from "./scan/env.ts";
|
|
3
3
|
import { type Serially } from "./write.ts";
|
|
4
4
|
export declare function previewSettingsEdit(env: ScanEnv, edit: SettingsEdit): Promise<SettingsEditPreview>;
|
|
5
|
-
export declare function saveSettingsEdit(env: ScanEnv, edit: SettingsEdit, expectedRevision: string, queue
|
|
5
|
+
export declare function saveSettingsEdit(env: ScanEnv, edit: SettingsEdit, expectedRevision: string, queue: Serially): Promise<Change>;
|
|
6
6
|
export declare function previewPermissionEdit(env: ScanEnv, edit: PermissionEdit): Promise<SettingsEditPreview>;
|
|
7
|
-
export declare function savePermissionEdit(env: ScanEnv, edit: PermissionEdit, expectedRevision: string, queue
|
|
7
|
+
export declare function savePermissionEdit(env: ScanEnv, edit: PermissionEdit, expectedRevision: string, queue: Serially): Promise<Change>;
|
|
8
8
|
export declare function previewHookEdit(env: ScanEnv, edit: HookEdit): Promise<SettingsEditPreview>;
|
|
9
|
-
export declare function saveHookEdit(env: ScanEnv, edit: HookEdit, expectedRevision: string, queue
|
|
9
|
+
export declare function saveHookEdit(env: ScanEnv, edit: HookEdit, expectedRevision: string, queue: Serially): Promise<Change>;
|
|
@@ -11,7 +11,7 @@ import { isSkippedMcpRule } from "./scan/permissions.js";
|
|
|
11
11
|
import { settingsFiles } from "./scan/settings.js";
|
|
12
12
|
import { schemaErrors } from "./schema.js";
|
|
13
13
|
import { hasHiddenValues, redactSecrets, shown } from "./secrets.js";
|
|
14
|
-
import { RefusedError, readJsonObject, recordChange, saveReviewed,
|
|
14
|
+
import { RefusedError, readJsonObject, recordChange, saveReviewed, writeAtomic, } from "./write.js";
|
|
15
15
|
const properties = schema.definitions.ClaudeCodeSettings.properties;
|
|
16
16
|
const validators = new Map();
|
|
17
17
|
function validatorFor(key) {
|
|
@@ -199,18 +199,18 @@ const CHANGED = "The file changed since you reviewed this edit; review it again.
|
|
|
199
199
|
export async function previewSettingsEdit(env, edit) {
|
|
200
200
|
return (await planSettingsEdit(env, edit)).preview;
|
|
201
201
|
}
|
|
202
|
-
export function saveSettingsEdit(env, edit, expectedRevision, queue
|
|
202
|
+
export function saveSettingsEdit(env, edit, expectedRevision, queue) {
|
|
203
203
|
return saveReviewed(queue, () => planSettingsEdit(env, edit), expectedRevision, CHANGED);
|
|
204
204
|
}
|
|
205
205
|
export async function previewPermissionEdit(env, edit) {
|
|
206
206
|
return (await planPermissionEdit(env, edit)).preview;
|
|
207
207
|
}
|
|
208
|
-
export function savePermissionEdit(env, edit, expectedRevision, queue
|
|
208
|
+
export function savePermissionEdit(env, edit, expectedRevision, queue) {
|
|
209
209
|
return saveReviewed(queue, () => planPermissionEdit(env, edit), expectedRevision, CHANGED);
|
|
210
210
|
}
|
|
211
211
|
export async function previewHookEdit(env, edit) {
|
|
212
212
|
return (await planHookEdit(env, edit)).preview;
|
|
213
213
|
}
|
|
214
|
-
export function saveHookEdit(env, edit, expectedRevision, queue
|
|
214
|
+
export function saveHookEdit(env, edit, expectedRevision, queue) {
|
|
215
215
|
return saveReviewed(queue, () => planHookEdit(env, edit), expectedRevision, CHANGED);
|
|
216
216
|
}
|
package/dist/core/write.d.ts
CHANGED
|
@@ -14,8 +14,6 @@ export declare class StaleError extends RefusedError {
|
|
|
14
14
|
export type Serially = <T>(change: () => Promise<T>) => Promise<T>;
|
|
15
15
|
/** A new, empty queue of changes. Each core has its own. */
|
|
16
16
|
export declare function changeQueue(): Serially;
|
|
17
|
-
/** The queue the save functions use when they are called without a core's. */
|
|
18
|
-
export declare const serially: Serially;
|
|
19
17
|
/** What every change's review has: the revision it was worked out from and why it cannot be saved. */
|
|
20
18
|
interface Review {
|
|
21
19
|
/** Fingerprint of everything the change was worked out from. */
|
package/dist/core/write.js
CHANGED
|
@@ -3,7 +3,7 @@ import { chmod, link, lstat, mkdir, open, readFile, realpath, rename, rm, writeF
|
|
|
3
3
|
import { basename, dirname, join } from "node:path";
|
|
4
4
|
import { Type } from "typebox";
|
|
5
5
|
import { BACKUP_KINDS } from "../shared/types.js";
|
|
6
|
-
import { isRecord } from "./scan/data.js";
|
|
6
|
+
import { isRecord, parseJson } from "./scan/data.js";
|
|
7
7
|
import {} from "./scan/env.js";
|
|
8
8
|
import { isCredentials, isSecretPath, revisionOf } from "./scan/files.js";
|
|
9
9
|
import { jsonSyntaxError } from "./secrets.js";
|
|
@@ -24,8 +24,6 @@ export function changeQueue() {
|
|
|
24
24
|
return run;
|
|
25
25
|
};
|
|
26
26
|
}
|
|
27
|
-
/** The queue the save functions use when they are called without a core's. */
|
|
28
|
-
export const serially = changeQueue();
|
|
29
27
|
/**
|
|
30
28
|
* Plans a change again and makes it, but only when it has no errors and everything it was worked
|
|
31
29
|
* out from is as reviewed; `changed` is the refusal otherwise. Planning and saving share one turn
|
|
@@ -91,7 +89,7 @@ export async function readJsonObject(env, path) {
|
|
|
91
89
|
return { raw, data: {}, reformats: false };
|
|
92
90
|
let parsed;
|
|
93
91
|
try {
|
|
94
|
-
parsed =
|
|
92
|
+
parsed = parseJson(raw);
|
|
95
93
|
}
|
|
96
94
|
catch (error) {
|
|
97
95
|
throw new RefusedError(`${path} is ${jsonSyntaxError(error)}; fix it by hand first.`);
|
package/dist/shared/memory.d.ts
CHANGED
|
@@ -1,2 +1,6 @@
|
|
|
1
|
-
/**
|
|
1
|
+
/**
|
|
2
|
+
* Claude Code names a project's store after its path with every non-alphanumeric character as "-".
|
|
3
|
+
* A name over 200 characters is cut there and ends with "-" and a hash of the whole path, so deep
|
|
4
|
+
* paths that share a start get different stores (observed in the SDK 0.3.296; the docs do not say).
|
|
5
|
+
*/
|
|
2
6
|
export declare function storeDirName(projectPath: string): string;
|
package/dist/shared/memory.js
CHANGED
|
@@ -1,4 +1,20 @@
|
|
|
1
|
-
/** Claude Code
|
|
1
|
+
/** Longest store name Claude Code keeps as it is; a longer one is cut and given a hash of the path. */
|
|
2
|
+
const MAX_STORE_NAME = 200;
|
|
3
|
+
/**
|
|
4
|
+
* Claude Code names a project's store after its path with every non-alphanumeric character as "-".
|
|
5
|
+
* A name over 200 characters is cut there and ends with "-" and a hash of the whole path, so deep
|
|
6
|
+
* paths that share a start get different stores (observed in the SDK 0.3.296; the docs do not say).
|
|
7
|
+
*/
|
|
2
8
|
export function storeDirName(projectPath) {
|
|
3
|
-
|
|
9
|
+
const name = projectPath.replace(/[^a-zA-Z0-9]/g, "-");
|
|
10
|
+
if (name.length <= MAX_STORE_NAME)
|
|
11
|
+
return name;
|
|
12
|
+
return `${name.slice(0, MAX_STORE_NAME)}-${Math.abs(pathHash(projectPath)).toString(36)}`;
|
|
13
|
+
}
|
|
14
|
+
/** Java's 32-bit string hash, over UTF-16 code units, as Claude Code computes it. */
|
|
15
|
+
function pathHash(text) {
|
|
16
|
+
let hash = 0;
|
|
17
|
+
for (let i = 0; i < text.length; i++)
|
|
18
|
+
hash = ((hash << 5) - hash + text.charCodeAt(i)) | 0;
|
|
19
|
+
return hash;
|
|
4
20
|
}
|
package/dist/shared/types.d.ts
CHANGED
|
@@ -100,6 +100,11 @@ export interface SettingsLayer {
|
|
|
100
100
|
error?: string;
|
|
101
101
|
/** For managed settings: a file that is not a JSON object, so Claude Code refuses to start. */
|
|
102
102
|
blocksStart?: boolean;
|
|
103
|
+
/**
|
|
104
|
+
* For managed settings: managed sources that rank above these files and that ccdesk does not
|
|
105
|
+
* read, such as the server-managed settings cache or a macOS configuration profile.
|
|
106
|
+
*/
|
|
107
|
+
higherSources?: string[];
|
|
103
108
|
}
|
|
104
109
|
export interface EffectiveSetting {
|
|
105
110
|
key: string;
|