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
package/CHANGELOG.md CHANGED
@@ -1,5 +1,51 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.2.4](https://github.com/ayagmar/ccdesk/compare/v0.2.3...v0.2.4) (2026-10-13)
4
+
5
+ ### Features
6
+
7
+ * **core:** warn when a higher managed source than the files is present ([cb04a30](https://github.com/ayagmar/ccdesk/commit/cb04a30e72293270bdf4e3529a938ef13188bb41))
8
+
9
+ ### Bug Fixes
10
+
11
+ * **core:** count Explorer as having opened the file on Windows ([eb1a120](https://github.com/ayagmar/ccdesk/commit/eb1a12090507be426765c38d2e71a5f5500622ef))
12
+ * **core:** hide header values and more secret flags in MCP server args ([3a9ee02](https://github.com/ayagmar/ccdesk/commit/3a9ee024a124602f486b3e8f41e23fffa7c84565))
13
+ * **core:** look up ~/.claude.json folders as Claude Code keys them on Windows ([67a445d](https://github.com/ayagmar/ccdesk/commit/67a445d2a569bccd8f1615e798af099ece5dbe38))
14
+ * **core:** name long project stores as Claude Code does ([98f4dd8](https://github.com/ayagmar/ccdesk/commit/98f4dd8f4efbe1ce0c2154e58b554c9a05baed95))
15
+ * **core:** read JSON files that start with a byte order mark ([6bb2fd3](https://github.com/ayagmar/ccdesk/commit/6bb2fd302d468f1603d2230270ce5c9999027e93))
16
+ * **core:** report a backup folder ccdesk cannot read instead of listing none ([1209b9c](https://github.com/ayagmar/ccdesk/commit/1209b9c0a360939a3247fd3e92ad9b0d41b579f6))
17
+ * **core:** say why claude did not start on Windows instead of "not on PATH" ([9c0d3ad](https://github.com/ayagmar/ccdesk/commit/9c0d3adcc0ad0cb1ff4ed6b66377b603c1b1f4da))
18
+
19
+ ### Performance Improvements
20
+
21
+ * **core:** group hand-approved calls without copying each group ([9ab7219](https://github.com/ayagmar/ccdesk/commit/9ab7219986690e08669f3aceacb93f3f48777e11))
22
+ * **ui:** refresh the overview on focus and saves, not on watched changes ([9010576](https://github.com/ayagmar/ccdesk/commit/901057643dfcded5f2463ccb09353a5d264f109d))
23
+
24
+ ## [0.2.3](https://github.com/ayagmar/ccdesk/compare/v0.2.2...v0.2.3) (2026-10-13)
25
+
26
+ ### Features
27
+
28
+ * **cli:** add read-only scan --json and explain subcommands ([fc59aed](https://github.com/ayagmar/ccdesk/commit/fc59aed4935371e2a979991a03a94cf01c0e2e68))
29
+ * **core:** add Watcher.subscribe to push changes ([3eb360e](https://github.com/ayagmar/ccdesk/commit/3eb360e286fa34eede2c23aa1b3fe69f2fb6b129))
30
+ * **core:** make the core importable as the package entry point ([82a4107](https://github.com/ayagmar/ccdesk/commit/82a41075f88c6b0ab475eb559afe1cf13aacc25a))
31
+ * **view:** export the view logic as ccdesk/view ([b0e4365](https://github.com/ayagmar/ccdesk/commit/b0e436513da85184e9e141d2502fba1d297f39ee))
32
+
33
+ ### Bug Fixes
34
+
35
+ * **core:** refuse to open a file when no editor starts, instead of failing ([a359842](https://github.com/ayagmar/ccdesk/commit/a359842ecd33db582b565878e56a63ac00ad664e))
36
+ * **core:** refuse to open credentials or missing paths in createCore ([9562aab](https://github.com/ayagmar/ccdesk/commit/9562aab75961097c97b193f9edac8f5a34f8c701))
37
+ * **core:** report broken ~/.claude.json, managed-mcp.json and plugin files ([03710a6](https://github.com/ayagmar/ccdesk/commit/03710a6ff76557bde8e3b935244b700c8b1daf37))
38
+ * **core:** report memory, agent and skill files the scan cannot read ([3b91804](https://github.com/ayagmar/ccdesk/commit/3b9180401fd0479aa584c86f56dead7aa12b6dbf))
39
+ * **core:** resolve Watcher.subscribe and wait once moved watchers start ([039bb73](https://github.com/ayagmar/ccdesk/commit/039bb73282c014f2389ad37c37b5b80f6a91511e))
40
+ * **ui:** disable Undo while it runs and clear stale errors in Recent changes ([2c0c1e5](https://github.com/ayagmar/ccdesk/commit/2c0c1e5e381b49b40c1d99a5b9b3cfd1ce2d25a7))
41
+ * **ui:** say how to recover from an expired token wherever a call fails ([f9864a2](https://github.com/ayagmar/ccdesk/commit/f9864a24cf105691e8396de369c4167aa92f9750))
42
+ * **ui:** stop warning that the file changed after saving a setting ([e807947](https://github.com/ayagmar/ccdesk/commit/e80794746c9eed8503baa5a6cfa694568ab37740))
43
+ * **watch:** rewatch before calling subscribers, so one that throws cannot skip it ([d63fa49](https://github.com/ayagmar/ccdesk/commit/d63fa49cdf947795c8cafabc8e76458f91689601))
44
+
45
+ ### Performance Improvements
46
+
47
+ * **ui:** load the settings schema and catalog from the server ([34ba654](https://github.com/ayagmar/ccdesk/commit/34ba654ca9d5513a792ed5c44a50490a9d79dd54))
48
+
3
49
  ## [0.2.2](https://github.com/ayagmar/ccdesk/compare/v0.2.1...v0.2.2) (2026-10-12)
4
50
 
5
51
  ### Bug Fixes
package/README.md CHANGED
@@ -96,21 +96,24 @@ More in [Reading the interface](https://ayagmar.github.io/ccdesk/interface/).
96
96
  ## Options
97
97
 
98
98
  ```
99
- npx ccdesk [options]
99
+ npx ccdesk [options] Open the page in your browser
100
+ npx ccdesk scan [dir] --json Print what Claude Code loads in dir, as JSON
101
+ npx ccdesk explain <file> [--dir <dir>] Print what loads once Claude reads or edits file
100
102
 
101
103
  --port <n> Port to listen on (default: a free port)
102
104
  --no-open Print the URL instead of opening a browser
103
105
  --config-dir <path> Claude folder to read, as CLAUDE_CONFIG_DIR sets it for Claude Code
104
106
  (wins over $CLAUDE_CONFIG_DIR)
107
+ --dir <dir> For explain: the folder the session starts in (default: the current one)
105
108
  -h, --help Show this help
106
109
  ```
107
110
 
108
- ccdesk reads `~/.claude` and `~/.claude.json`, or `$CLAUDE_CONFIG_DIR` when it is set, as Claude Code does. To look at another profile without exporting the variable, pass `--config-dir <path>`. Edits made in another editor or by Claude show up within a second. Changes to `~/.claude.json`, such as trust decisions and MCP approvals, show up when you return to the tab. Requires Node.js 22.19 or newer. See [Platform notes](https://ayagmar.github.io/ccdesk/platforms/) for Windows, macOS and WSL.
111
+ ccdesk reads `~/.claude` and `~/.claude.json`, or `$CLAUDE_CONFIG_DIR` when it is set, as Claude Code does. To look at another profile without exporting the variable, pass `--config-dir <path>`. `ccdesk scan` prints the same view the page shows for a directory, for scripts; `ccdesk explain` lists the path rules, skills and folder instructions that load once Claude reads or edits a file. Both only read, and hidden values stay hidden in what they print. Edits made in another editor or by Claude show up within a second. Changes to `~/.claude.json`, such as trust decisions and MCP approvals, show up when you return to the tab. Requires Node.js 22.19 or newer. See [Platform notes](https://ayagmar.github.io/ccdesk/platforms/) for Windows, macOS and WSL.
109
112
 
110
113
  ## Privacy and safety
111
114
 
112
115
  - The server listens on `127.0.0.1` only, and every API call needs a token generated for that run. Nothing leaves your machine.
113
- - Values under `env` and `headers`, and keys in an MCP server's `args` (after flags such as `--token` or `--api-key`, or starting like `sk-` or `ghp_`), are hidden everywhere in the UI, diffs included, and ccdesk refuses any edit that would change them.
116
+ - Values under `env` and `headers`, and keys in an MCP server's `args` (after flags such as `--token` or `--api-key`, in a `--header` such as `Authorization: Bearer …`, or starting like `sk-` or `ghp_`), are hidden everywhere in the UI, diffs included, and ccdesk refuses any edit that would change them.
114
117
  - ccdesk never reads `~/.claude/.credentials.json`.
115
118
  - From your transcripts, only counts and rule patterns (a command's program and subcommand, a fetch's domain) reach the page, never commands or file contents.
116
119
  - Backups are readable only by you on Linux and macOS.
@@ -126,7 +129,7 @@ pnpm dev:ui # UI with hot reload on localhost:4320
126
129
  pnpm check # typecheck, lint, format, tests, build
127
130
  ```
128
131
 
129
- Tests never touch your real `~/.claude`; each one builds a throwaway home directory. The settings schema, scopes and built-in tools are generated from [`@anthropic-ai/claude-agent-sdk`](https://www.npmjs.com/package/@anthropic-ai/claude-agent-sdk) and Claude Code's docs, and a daily workflow opens a pull request when they change. [Contributing](https://ayagmar.github.io/ccdesk/contributing/) covers the generators, the docs site and releases.
132
+ Tests never touch your real `~/.claude`; each one builds a throwaway home directory. The settings schema, scopes and built-in tools are generated from [`@anthropic-ai/claude-agent-sdk`](https://www.npmjs.com/package/@anthropic-ai/claude-agent-sdk) and Claude Code's docs, and a daily workflow opens a pull request when they change. [Contributing](https://ayagmar.github.io/ccdesk/contributing/) covers the generators, the docs site and releases. [ARCHITECTURE.md](ARCHITECTURE.md) explains how the code fits together, and [`docs/`](docs/) holds the plan for splitting the core from the web server and feature ideas.
130
133
 
131
134
  ## License
132
135
 
@@ -0,0 +1,6 @@
1
+ import { type AgentEdit, type Change, type SettingsEditPreview } from "../shared/types.ts";
2
+ import { type ScanEnv } from "./scan/env.ts";
3
+ import { type Serially } from "./write.ts";
4
+ export declare function previewAgentEdit(env: ScanEnv, edit: AgentEdit): Promise<SettingsEditPreview>;
5
+ /** Writes or deletes an agent file, backed up first, only while it is as reviewed. */
6
+ export declare function saveAgentEdit(env: ScanEnv, edit: AgentEdit, expectedRevision: string, queue: Serially): Promise<Change>;
@@ -1,10 +1,12 @@
1
1
  import { join, resolve } from "node:path";
2
2
  import {} from "../shared/types.js";
3
3
  import {} from "./scan/env.js";
4
- import { gitRoot, revisionOf, shownText } from "./scan/files.js";
4
+ import { revisionOf } from "./scan/files.js";
5
+ import { gitRoot } from "./scan/git.js";
6
+ import { shownText } from "./secrets.js";
5
7
  import { scanDirectory } from "./scan/index.js";
6
8
  import { agentProblems } from "./scan/inventory.js";
7
- import { RefusedError, readIfExists, recordChange, removeExactly, serially, StaleError, writeAtomic, } from "./write.js";
9
+ import { RefusedError, readIfExists, recordChange, removeExactly, StaleError, saveReviewed, writeAtomic, } from "./write.js";
8
10
  /** The file an edit changes: an agent the scan shows here, or a new one in your or the project's folder. */
9
11
  async function targetOf(env, edit) {
10
12
  const dir = resolve(edit.dir);
@@ -46,37 +48,29 @@ async function plan(env, edit) {
46
48
  errors = [
47
49
  "ccdesk does not write env, header or URL values it would then hide; add them in your editor instead.",
48
50
  ];
51
+ const { text } = edit;
49
52
  return {
50
- path,
51
- raw,
52
53
  preview: {
53
54
  path,
54
55
  before,
55
- after: errors.length > 0 ? (before ?? "") : (shownText(edit.text) ?? ""),
56
+ after: errors.length > 0 ? (before ?? "") : (shownText(text) ?? ""),
56
57
  revision: revisionOf(raw),
57
58
  reformats: false,
58
59
  errors,
59
60
  },
61
+ save: () => recordChange(env, { kind: "agent", target: path, before: raw }, async () => {
62
+ if (text !== null)
63
+ await writeAtomic(env, path, text, revisionOf(raw));
64
+ else if (!(await removeExactly(path, revisionOf(raw))))
65
+ throw new StaleError(`${path} changed while ccdesk deleted it; nothing was deleted.`);
66
+ return { target: path, revision: revisionOf(text) };
67
+ }),
60
68
  };
61
69
  }
62
70
  export async function previewAgentEdit(env, edit) {
63
71
  return (await plan(env, edit)).preview;
64
72
  }
65
73
  /** Writes or deletes an agent file, backed up first, only while it is as reviewed. */
66
- export function saveAgentEdit(env, edit, expectedRevision) {
67
- return serially(async () => {
68
- const { path, raw, preview } = await plan(env, edit);
69
- if (preview.errors.length > 0)
70
- throw new RefusedError(preview.errors.join(" "));
71
- if (preview.revision !== expectedRevision)
72
- throw new RefusedError("The agent changed since you reviewed this; review it again.");
73
- const { text } = edit;
74
- return recordChange(env, { kind: "agent", target: path, before: raw }, async () => {
75
- if (text !== null)
76
- await writeAtomic(env, path, text, expectedRevision);
77
- else if (!(await removeExactly(path, expectedRevision)))
78
- throw new StaleError(`${path} changed while ccdesk deleted it; nothing was deleted.`);
79
- return { target: path, revision: revisionOf(text) };
80
- });
81
- });
74
+ export function saveAgentEdit(env, edit, expectedRevision, queue) {
75
+ return saveReviewed(queue, () => plan(env, edit), expectedRevision, "The agent changed since you reviewed this; review it again.");
82
76
  }
@@ -0,0 +1,19 @@
1
+ import { type BackupSummary, type RestorePreview } from "../shared/types.ts";
2
+ import { type ScanEnv } from "./scan/env.ts";
3
+ import { type Serially } from "./write.ts";
4
+ /** The changes ccdesk made, newest first, a page at a time. */
5
+ export declare function listBackups(env: ScanEnv, { before, limit }: {
6
+ before?: string;
7
+ limit: number;
8
+ }): Promise<{
9
+ changes: BackupSummary[];
10
+ more: boolean;
11
+ }>;
12
+ export declare function previewRestore(env: ScanEnv, id: string): Promise<RestorePreview>;
13
+ /**
14
+ * Puts a backed-up file back, or removes a file ccdesk created, if the file is still as it was
15
+ * when the restore was reviewed. A removed file is backed up first, so it can be put back too.
16
+ */
17
+ export declare function restoreBackup(env: ScanEnv, id: string, expectedRevision: string, queue: Serially): Promise<{
18
+ target: string;
19
+ }>;
@@ -1,29 +1,29 @@
1
1
  import { lstat, readdir, readFile, realpath } from "node:fs/promises";
2
2
  import { basename, dirname, join } from "node:path";
3
+ import { Compile } from "typebox/compile";
3
4
  import {} from "../shared/types.js";
4
5
  import { assertMemoryDir, indexPathIn, revisionOfBoth } from "./mutations.js";
6
+ import { parseJson } from "./scan/data.js";
5
7
  import {} from "./scan/env.js";
6
- import { isRecord, isWithin, revisionOf, shownText } from "./scan/files.js";
7
- import { hiddenValues, shown } from "./secrets.js";
8
- import { backupRoot, forgetChange, MANIFEST, RefusedError, readIfExists, recordChange, removeExactly, SHOWN_AFTER, SNAPSHOT, StaleError, serially, writeAtomic, } from "./write.js";
9
- const KINDS = new Set([
10
- "settings",
11
- "mcp",
12
- "agent",
13
- "memory-index",
14
- "memory-delete",
15
- "memory-restore",
16
- "mcp-claude",
17
- ]);
18
- function strings(value) {
19
- return Array.isArray(value) && value.every((v) => typeof v === "string");
20
- }
8
+ import { isWithin, revisionOf } from "./scan/files.js";
9
+ import { hiddenValues, shown, shownText } from "./secrets.js";
10
+ import { BackupManifestSchema, backupRoot, forgetChange, MANIFEST, RefusedError, readIfExists, recordChange, removeExactly, SHOWN_AFTER, SNAPSHOT, StaleError, writeAtomic, } from "./write.js";
11
+ const backupManifest = Compile(BackupManifestSchema);
21
12
  /** A backup folder's manifest; anything that does not look like one ccdesk wrote is skipped. */
22
13
  async function readManifest(env, id) {
23
14
  // ccdesk's ids are letters, digits and dashes, so no id can name a folder outside the root.
24
15
  if (!/^[\w-]+$/.test(id))
25
16
  return null;
26
- const text = await readIfExists(env, join(backupRoot(env), id, MANIFEST)).catch(() => null);
17
+ let text;
18
+ try {
19
+ text = await readIfExists(env, join(backupRoot(env), id, MANIFEST));
20
+ }
21
+ catch (error) {
22
+ // A stray file in the backup folder is not a backup; a folder ccdesk cannot read is an error.
23
+ if (error.code === "ENOTDIR")
24
+ return null;
25
+ throw error;
26
+ }
27
27
  if (text === null)
28
28
  return null;
29
29
  let data;
@@ -33,21 +33,25 @@ async function readManifest(env, id) {
33
33
  catch {
34
34
  return null;
35
35
  }
36
- if (!isRecord(data) ||
37
- typeof data.kind !== "string" ||
38
- !KINDS.has(data.kind) ||
39
- typeof data.target !== "string" ||
40
- typeof data.createdAt !== "string" ||
41
- typeof data.before !== "string" ||
42
- typeof data.after !== "string" ||
43
- (data.removedIndexLines !== undefined && !strings(data.removedIndexLines)) ||
44
- (data.undoCommands !== undefined && !strings(data.undoCommands)))
45
- return null;
46
- return data;
36
+ return backupManifest.Check(data) ? data : null;
37
+ }
38
+ /**
39
+ * Backup folder names, newest first. No folder yet means no changes; one ccdesk cannot read, such
40
+ * as one a `sudo ccdesk` left owned by root, is an error rather than an empty list.
41
+ */
42
+ async function backupIds(env) {
43
+ try {
44
+ return (await readdir(backupRoot(env))).sort().reverse();
45
+ }
46
+ catch (error) {
47
+ if (error.code === "ENOENT")
48
+ return [];
49
+ throw error;
50
+ }
47
51
  }
48
52
  /** The changes ccdesk made, newest first, a page at a time. */
49
53
  export async function listBackups(env, { before, limit }) {
50
- const ids = (await readdir(backupRoot(env)).catch(() => [])).sort().reverse();
54
+ const ids = await backupIds(env);
51
55
  const changes = [];
52
56
  for (const id of ids) {
53
57
  if (before !== undefined && id >= before)
@@ -89,7 +93,7 @@ function shownSettings(text) {
89
93
  if (text === null)
90
94
  return null;
91
95
  try {
92
- return shown(JSON.parse(text));
96
+ return shown(parseJson(text));
93
97
  }
94
98
  catch {
95
99
  return "(not valid JSON; open the file to see it)\n";
@@ -131,26 +135,38 @@ async function claudeMcpPreview(id, manifest, dir) {
131
135
  }
132
136
  async function planRestore(env, id) {
133
137
  const { manifest, dir } = await backupAt(env, id);
134
- if (manifest.kind === "mcp-claude")
135
- throw new RefusedError(MADE_BY_CLAUDE);
136
138
  let target = manifest.target;
137
- if (manifest.kind === "settings" && !isSettingsFile(env, target))
138
- throw new RefusedError("This backup does not point at a settings file.");
139
- if (manifest.kind === "mcp" && basename(target) !== ".mcp.json")
140
- throw new RefusedError("This backup does not point at a .mcp.json.");
141
- if (manifest.kind === "agent" && !isAgentFile(env, target))
142
- throw new RefusedError("This backup does not point at a subagent file.");
143
- if (manifest.kind === "memory-index" && basename(target) !== "MEMORY.md")
144
- throw new RefusedError("This backup does not point at a MEMORY.md.");
145
- if (manifest.kind !== "settings" && manifest.kind !== "mcp" && manifest.kind !== "agent") {
146
- const store = await assertMemoryDir(env, dirname(target));
139
+ switch (manifest.kind) {
140
+ case "mcp-claude":
141
+ throw new RefusedError(MADE_BY_CLAUDE);
142
+ case "settings":
143
+ if (!isSettingsFile(env, target))
144
+ throw new RefusedError("This backup does not point at a settings file.");
145
+ break;
146
+ case "mcp":
147
+ if (basename(target) !== ".mcp.json")
148
+ throw new RefusedError("This backup does not point at a .mcp.json.");
149
+ break;
150
+ case "agent":
151
+ if (!isAgentFile(env, target))
152
+ throw new RefusedError("This backup does not point at a subagent file.");
153
+ break;
147
154
  // The same confinement as any other MEMORY.md change: a link out of the folder is refused.
148
- target =
149
- manifest.kind === "memory-index" ? await indexPathIn(store) : join(store, basename(target));
155
+ case "memory-index": {
156
+ if (basename(target) !== "MEMORY.md")
157
+ throw new RefusedError("This backup does not point at a MEMORY.md.");
158
+ target = await indexPathIn(await assertMemoryDir(env, dirname(target)));
159
+ break;
160
+ }
150
161
  // A deleted memory comes back only as a topic file, as ccdesk deletes only those.
151
- if (manifest.kind !== "memory-index" &&
152
- (!target.endsWith(".md") || basename(target) === "MEMORY.md"))
153
- throw new RefusedError("This backup does not point at a memory file.");
162
+ case "memory-delete":
163
+ case "memory-restore": {
164
+ const store = await assertMemoryDir(env, dirname(target));
165
+ target = join(store, basename(target));
166
+ if (!target.endsWith(".md") || basename(target) === "MEMORY.md")
167
+ throw new RefusedError("This backup does not point at a memory file.");
168
+ break;
169
+ }
154
170
  }
155
171
  let saved = null;
156
172
  if (manifest.before !== revisionOf(null)) {
@@ -247,8 +263,8 @@ export async function previewRestore(env, id) {
247
263
  * Puts a backed-up file back, or removes a file ccdesk created, if the file is still as it was
248
264
  * when the restore was reviewed. A removed file is backed up first, so it can be put back too.
249
265
  */
250
- export function restoreBackup(env, id, expectedRevision) {
251
- return serially(async () => {
266
+ export function restoreBackup(env, id, expectedRevision, queue) {
267
+ return queue(async () => {
252
268
  const plan = await planRestore(env, id);
253
269
  const { manifest, current, saved, index } = plan;
254
270
  const blocked = blockedReason(plan);
@@ -0,0 +1,96 @@
1
+ import { type RunClaude } from "./mcp-edit.ts";
2
+ import { type ScanEnv } from "./scan/env.ts";
3
+ import { type AgentEdit, type HookEdit, type McpEdit, type MemoryAction, type PermissionEdit, type SettingsEdit } from "../shared/types.ts";
4
+ export interface CoreOptions {
5
+ /** Runs `claude` for user and local MCP servers. By default, the claude on `env`'s PATH. */
6
+ runClaude?: RunClaude;
7
+ /**
8
+ * Opens a file or folder and resolves with the command used. By default, the editor `env` names
9
+ * or has. The core refuses a missing path and the credentials file before calling it.
10
+ */
11
+ openFile?: (path: string) => Promise<string>;
12
+ }
13
+ /**
14
+ * The core for one machine: every read and change, with `env` already passed. Changes made through
15
+ * one core run one at a time, and its usage counts share one cache. Two cores share neither, so
16
+ * make one per Claude folder: two on the same folder could both pass a revision check.
17
+ */
18
+ export declare function createCore(env: ScanEnv, options?: CoreOptions): {
19
+ env: ScanEnv;
20
+ openFile: (path: string) => Promise<string>;
21
+ scanOverview: (launchDir: string) => Promise<import("../shared/types.ts").Overview>;
22
+ scanDirectory: (dir: string, profile?: import("../shared/types.ts").SessionProfile | undefined) => Promise<import("../shared/types.ts").DirectoryView>;
23
+ compareSettings: (a: string, b: string) => Promise<import("./index.ts").SettingsComparison>;
24
+ testPath: (dir: string, file: string, viewOf?: ((cwd: string) => Promise<import("../shared/types.ts").DirectoryView>) | undefined) => Promise<import("../shared/types.ts").PathTest>;
25
+ readUsage: (dir: string, rules: import("./scan/usage.ts").RuleInEffect[], args_2?: {
26
+ signal?: AbortSignal;
27
+ timeLimitMs?: number;
28
+ maxCachedCalls?: number;
29
+ } | undefined) => Promise<import("../shared/types.ts").Usage>;
30
+ previewSettingsEdit: (edit: {
31
+ dir: string;
32
+ scope: import("../shared/types.ts").Scope;
33
+ key: string;
34
+ value?: unknown;
35
+ }) => Promise<import("../shared/types.ts").SettingsEditPreview>;
36
+ saveSettingsEdit: (edit: SettingsEdit, revision: string) => Promise<import("../shared/types.ts").Change>;
37
+ previewPermissionEdit: (edit: {
38
+ dir: string;
39
+ file: string;
40
+ remove?: {
41
+ list: import("../shared/types.ts").PermissionList;
42
+ rule: string;
43
+ };
44
+ add?: {
45
+ list: import("../shared/types.ts").PermissionList;
46
+ rule: string;
47
+ };
48
+ }) => Promise<import("../shared/types.ts").SettingsEditPreview>;
49
+ savePermissionEdit: (edit: PermissionEdit, revision: string) => Promise<import("../shared/types.ts").Change>;
50
+ previewHookEdit: (edit: {
51
+ dir: string;
52
+ file: string;
53
+ remove?: {
54
+ event: string;
55
+ at: {
56
+ group: number;
57
+ index: number;
58
+ };
59
+ definition: unknown;
60
+ };
61
+ add?: {
62
+ event: string;
63
+ matcher?: string;
64
+ definition: unknown;
65
+ };
66
+ }) => Promise<import("../shared/types.ts").SettingsEditPreview>;
67
+ saveHookEdit: (edit: HookEdit, revision: string) => Promise<import("../shared/types.ts").Change>;
68
+ previewAgentEdit: (edit: {
69
+ dir: string;
70
+ path?: string;
71
+ create?: {
72
+ scope: "project" | "user";
73
+ file: string;
74
+ };
75
+ text: string | null;
76
+ }) => Promise<import("../shared/types.ts").SettingsEditPreview>;
77
+ saveAgentEdit: (edit: AgentEdit, revision: string) => Promise<import("../shared/types.ts").Change>;
78
+ previewMcpEdit: (edit: McpEdit) => Promise<import("../shared/types.ts").SettingsEditPreview>;
79
+ saveMcpEdit: (edit: McpEdit, revision: string) => Promise<import("../shared/types.ts").Change | {
80
+ target: string;
81
+ }>;
82
+ previewMemoryChange: (action: MemoryAction, path: string) => Promise<import("../shared/types.ts").MemoryChangePreview>;
83
+ changeMemory: (action: MemoryAction, path: string, revision: string) => Promise<import("../shared/types.ts").Change>;
84
+ listBackups: (args_0: {
85
+ before?: string;
86
+ limit: number;
87
+ }) => Promise<{
88
+ changes: import("../shared/types.ts").BackupSummary[];
89
+ more: boolean;
90
+ }>;
91
+ previewRestore: (id: string) => Promise<import("../shared/types.ts").RestorePreview>;
92
+ restoreBackup: (id: string, revision: string) => Promise<{
93
+ target: string;
94
+ }>;
95
+ };
96
+ export type Core = ReturnType<typeof createCore>;
@@ -0,0 +1,57 @@
1
+ import { stat } from "node:fs/promises";
2
+ import { previewAgentEdit, saveAgentEdit } from "./agent-edit.js";
3
+ import { listBackups, previewRestore, restoreBackup } from "./backups.js";
4
+ import { claudeRunner, previewMcpEdit, saveMcpEdit } from "./mcp-edit.js";
5
+ import { changeMemory, previewMemoryChange } from "./mutations.js";
6
+ import { openInEditor } from "./open.js";
7
+ import { compareSettings } from "./scan/compare.js";
8
+ import {} from "./scan/env.js";
9
+ import { isCredentials } from "./scan/files.js";
10
+ import { scanDirectory, scanOverview } from "./scan/index.js";
11
+ import { testPath } from "./scan/path-test.js";
12
+ import { usageCounter } from "./scan/usage.js";
13
+ import { previewHookEdit, previewPermissionEdit, previewSettingsEdit, saveHookEdit, savePermissionEdit, saveSettingsEdit, } from "./settings-edit.js";
14
+ import { changeQueue, RefusedError } from "./write.js";
15
+ import {} from "../shared/types.js";
16
+ /**
17
+ * The core for one machine: every read and change, with `env` already passed. Changes made through
18
+ * one core run one at a time, and its usage counts share one cache. Two cores share neither, so
19
+ * make one per Claude folder: two on the same folder could both pass a revision check.
20
+ */
21
+ export function createCore(env, options = {}) {
22
+ const run = options.runClaude ?? claudeRunner(env);
23
+ const open = options.openFile ?? ((path) => openInEditor(path, env));
24
+ const queue = changeQueue();
25
+ const bound = (read) => (...args) => read(env, ...args);
26
+ return {
27
+ env,
28
+ openFile: async (path) => {
29
+ const info = await stat(path).catch(() => null);
30
+ if (!info?.isFile() && !info?.isDirectory())
31
+ throw new RefusedError("That file or folder no longer exists.");
32
+ if (await isCredentials(env, path))
33
+ throw new RefusedError("ccdesk does not open credentials.");
34
+ return open(path);
35
+ },
36
+ scanOverview: bound(scanOverview),
37
+ scanDirectory: bound(scanDirectory),
38
+ compareSettings: bound(compareSettings),
39
+ testPath: bound(testPath),
40
+ readUsage: bound(usageCounter()),
41
+ previewSettingsEdit: bound(previewSettingsEdit),
42
+ saveSettingsEdit: (edit, revision) => saveSettingsEdit(env, edit, revision, queue),
43
+ previewPermissionEdit: bound(previewPermissionEdit),
44
+ savePermissionEdit: (edit, revision) => savePermissionEdit(env, edit, revision, queue),
45
+ previewHookEdit: bound(previewHookEdit),
46
+ saveHookEdit: (edit, revision) => saveHookEdit(env, edit, revision, queue),
47
+ previewAgentEdit: bound(previewAgentEdit),
48
+ saveAgentEdit: (edit, revision) => saveAgentEdit(env, edit, revision, queue),
49
+ previewMcpEdit: (edit) => previewMcpEdit(env, edit, run),
50
+ saveMcpEdit: (edit, revision) => saveMcpEdit(env, edit, revision, run, queue),
51
+ previewMemoryChange: bound(previewMemoryChange),
52
+ changeMemory: (action, path, revision) => changeMemory(env, action, path, revision, queue),
53
+ listBackups: bound(listBackups),
54
+ previewRestore: bound(previewRestore),
55
+ restoreBackup: (id, revision) => restoreBackup(env, id, revision, queue),
56
+ };
57
+ }
@@ -0,0 +1,22 @@
1
+ export { type Core, type CoreOptions, createCore } from "./core.ts";
2
+ export { isConfigHome, type ScanEnv, scanEnvFromProcess } from "./scan/env.ts";
3
+ export { compareSettings } from "./scan/compare.ts";
4
+ export { revisionOf } from "./scan/files.ts";
5
+ export { scanDirectory, scanOverview } from "./scan/index.ts";
6
+ export { testPath } from "./scan/path-test.ts";
7
+ export { CountStopped } from "./scan/usage.ts";
8
+ export { hasHiddenValues, redactSecrets, shown, shownText } from "./secrets.ts";
9
+ export { Watcher } from "./watch.ts";
10
+ export { previewAgentEdit } from "./agent-edit.ts";
11
+ export { claudeProgram, claudeRunner, previewMcpEdit, type RunClaude } from "./mcp-edit.ts";
12
+ export { isMemoryAction, previewMemoryChange } from "./mutations.ts";
13
+ export { InvalidEditError, parseAgentEdit, parseHookEdit, parseMcpEdit, parsePermissionEdit, parseSettingsEdit, } from "./parse-edit.ts";
14
+ export { previewHookEdit, previewPermissionEdit, previewSettingsEdit } from "./settings-edit.ts";
15
+ export { RefusedError, StaleError } from "./write.ts";
16
+ export { listBackups, previewRestore } from "./backups.ts";
17
+ export { openInEditor } from "./open.ts";
18
+ export { REFERENCE } from "./reference.ts";
19
+ export * from "../shared/compare.ts";
20
+ export * from "../shared/memory.ts";
21
+ export * from "../shared/scopes.ts";
22
+ export * from "../shared/types.ts";
@@ -0,0 +1,38 @@
1
+ // The core: everything a front end needs to read and change Claude Code's configuration, without
2
+ // HTTP. It re-exports the modules in src/core and src/shared; see ARCHITECTURE.md for how they
3
+ // fit together and docs/split-plan.md for where they are going.
4
+ // The core for one machine: createCore(env) returns every read and change with the env already
5
+ // passed, its own change queue and usage cache, and the editor and claude its env names. Changes,
6
+ // usage counts and undo are made only through a core, so they run on its queue and cache.
7
+ export { createCore } from "./core.js";
8
+ // Environment: every function below takes the ScanEnv first.
9
+ export { isConfigHome, scanEnvFromProcess } from "./scan/env.js";
10
+ // Read.
11
+ export { compareSettings } from "./scan/compare.js";
12
+ export { revisionOf } from "./scan/files.js";
13
+ export { scanDirectory, scanOverview } from "./scan/index.js";
14
+ export { testPath } from "./scan/path-test.js";
15
+ export { CountStopped } from "./scan/usage.js";
16
+ export { hasHiddenValues, redactSecrets, shown, shownText } from "./secrets.js";
17
+ export { Watcher } from "./watch.js";
18
+ // Edits: the parse functions turn input from outside (JSON, arguments) into an edit, or throw
19
+ // InvalidEditError. Preview returns a revision, and a core's save takes it back. Both refuse with
20
+ // RefusedError, also when the file changed since the preview; StaleError, a RefusedError, means it
21
+ // changed while ccdesk was writing it.
22
+ export { previewAgentEdit } from "./agent-edit.js";
23
+ export { claudeProgram, claudeRunner, previewMcpEdit } from "./mcp-edit.js";
24
+ export { isMemoryAction, previewMemoryChange } from "./mutations.js";
25
+ export { InvalidEditError, parseAgentEdit, parseHookEdit, parseMcpEdit, parsePermissionEdit, parseSettingsEdit, } from "./parse-edit.js";
26
+ export { previewHookEdit, previewPermissionEdit, previewSettingsEdit } from "./settings-edit.js";
27
+ export { RefusedError, StaleError } from "./write.js";
28
+ // History: every change ccdesk saves is a backup that a core can restore by its id.
29
+ export { listBackups, previewRestore } from "./backups.js";
30
+ // Host.
31
+ export { openInEditor } from "./open.js";
32
+ // Claude Code's settings schema, settings catalog and hook events.
33
+ export { REFERENCE } from "./reference.js";
34
+ // Types and constants shared with the web UI.
35
+ export * from "../shared/compare.js";
36
+ export * from "../shared/memory.js";
37
+ export * from "../shared/scopes.js";
38
+ export * from "../shared/types.js";
@@ -0,0 +1,23 @@
1
+ import { type Change, type McpEdit, type SettingsEditPreview } from "../shared/types.ts";
2
+ import { type ScanEnv } from "./scan/env.ts";
3
+ import { type Serially } from "./write.ts";
4
+ /**
5
+ * Runs `claude` with these arguments in `cwd`, with the profile ccdesk reads so its edits land
6
+ * where the page shows them, and resolves with its output. Replaced in tests, so nothing outside
7
+ * the test changes.
8
+ */
9
+ export type RunClaude = (args: string[], cwd: string, configDir: string | undefined) => Promise<string>;
10
+ /**
11
+ * The program to run for `claude`. On Windows, npm installs Claude Code as a claude.cmd that starts
12
+ * the package's claude.exe; Node cannot run a .cmd without a shell, which would read the server's
13
+ * JSON as shell syntax, so ccdesk runs that claude.exe itself. A claude.exe earlier on PATH, as the
14
+ * native installer puts there, comes first, as it would in a terminal.
15
+ */
16
+ export declare function claudeProgram(path: string, platform: NodeJS.Platform): Promise<string>;
17
+ /** Runs the claude on `host`'s PATH, with `host`'s environment. */
18
+ export declare function claudeRunner(host: Pick<ScanEnv, "platform" | "processEnv">): RunClaude;
19
+ export declare function previewMcpEdit(env: ScanEnv, edit: McpEdit, run: RunClaude): Promise<SettingsEditPreview>;
20
+ /** Makes a reviewed MCP edit, only while the servers are still as reviewed. */
21
+ export declare function saveMcpEdit(env: ScanEnv, edit: McpEdit, expectedRevision: string, run: RunClaude, queue: Serially): Promise<Change | {
22
+ target: string;
23
+ }>;