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.
Files changed (40) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/README.md +1 -1
  3. package/dist/core/agent-edit.d.ts +1 -1
  4. package/dist/core/agent-edit.js +2 -2
  5. package/dist/core/backups.d.ts +1 -1
  6. package/dist/core/backups.js +29 -5
  7. package/dist/core/index.d.ts +6 -6
  8. package/dist/core/index.js +14 -14
  9. package/dist/core/mcp-edit.d.ts +1 -3
  10. package/dist/core/mcp-edit.js +15 -11
  11. package/dist/core/mutations.d.ts +1 -1
  12. package/dist/core/mutations.js +2 -2
  13. package/dist/core/open.d.ts +2 -5
  14. package/dist/core/open.js +6 -8
  15. package/dist/core/scan/data.d.ts +6 -0
  16. package/dist/core/scan/data.js +8 -0
  17. package/dist/core/scan/env.d.ts +2 -0
  18. package/dist/core/scan/env.js +8 -1
  19. package/dist/core/scan/files.js +2 -2
  20. package/dist/core/scan/git.d.ts +6 -0
  21. package/dist/core/scan/git.js +8 -0
  22. package/dist/core/scan/index.js +5 -2
  23. package/dist/core/scan/mcp.js +2 -1
  24. package/dist/core/scan/projects.js +2 -1
  25. package/dist/core/scan/settings.d.ts +2 -0
  26. package/dist/core/scan/settings.js +12 -3
  27. package/dist/core/scan/usage.d.ts +0 -6
  28. package/dist/core/scan/usage.js +7 -4
  29. package/dist/core/secrets.d.ts +3 -2
  30. package/dist/core/secrets.js +49 -29
  31. package/dist/core/settings-edit.d.ts +3 -3
  32. package/dist/core/settings-edit.js +4 -4
  33. package/dist/core/write.d.ts +0 -2
  34. package/dist/core/write.js +2 -4
  35. package/dist/shared/memory.d.ts +5 -1
  36. package/dist/shared/memory.js +18 -2
  37. package/dist/shared/types.d.ts +5 -0
  38. package/dist/ui/assets/{index-CH1HoC38.js → index-C9qqEI5Z.js} +2 -2
  39. package/dist/ui/index.html +1 -1
  40. package/package.json +1 -1
@@ -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
- groups.set(c.rule, [...(groups.get(c.rule) ?? []), c]);
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 = [];
@@ -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 part after `=` of `--token=value`, and a URL's user name,
42
- * password and query. Longest first, so a secret that contains another is taken out whole.
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[];
@@ -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 `--api-key`. */
25
- const SECRET_FLAG = /^--(token|api[-_]?key|apikey|password|secret|auth|key)$/i;
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 that starts like a known key, and the credentials in
31
- * a URL are hidden.
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
- if (typeof arg !== "string")
36
- return arg;
37
- const previous = args[i - 1];
38
- if (typeof previous === "string" && SECRET_FLAG.test(previous) && !arg.startsWith("-"))
39
- return "•••";
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 (/^https?:\/\//i.test(arg))
49
- return shownUrl(arg);
50
- return arg;
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 = JSON.parse(text);
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
- JSON.parse(text);
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 : JSON.parse(text);
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 part after `=` of `--token=value`, and a URL's user name,
169
- * password and query. Longest first, so a secret that contains another is taken out whole.
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
- if (typeof hidden === "string" && hidden.endsWith("=•••"))
178
- return [raw.slice(raw.indexOf("=") + 1)];
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?: Serially): Promise<Change>;
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?: Serially): Promise<Change>;
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?: Serially): Promise<Change>;
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, serially, writeAtomic, } from "./write.js";
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 = serially) {
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 = serially) {
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 = serially) {
214
+ export function saveHookEdit(env, edit, expectedRevision, queue) {
215
215
  return saveReviewed(queue, () => planHookEdit(env, edit), expectedRevision, CHANGED);
216
216
  }
@@ -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. */
@@ -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 = JSON.parse(raw);
92
+ parsed = parseJson(raw);
95
93
  }
96
94
  catch (error) {
97
95
  throw new RefusedError(`${path} is ${jsonSyntaxError(error)}; fix it by hand first.`);
@@ -1,2 +1,6 @@
1
- /** Claude Code names a project's store after its path with every non-alphanumeric character as "-". */
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;
@@ -1,4 +1,20 @@
1
- /** Claude Code names a project's store after its path with every non-alphanumeric character as "-". */
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
- return projectPath.replace(/[^a-zA-Z0-9]/g, "-");
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
  }
@@ -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;