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.
Files changed (131) hide show
  1. package/CHANGELOG.md +54 -0
  2. package/README.md +9 -5
  3. package/dist/core/agent-edit.d.ts +6 -0
  4. package/dist/{server → core}/agent-edit.js +21 -22
  5. package/dist/core/backups.d.ts +19 -0
  6. package/dist/{server → core}/backups.js +37 -45
  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 +25 -0
  12. package/dist/{server → core}/mcp-edit.js +116 -95
  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 +31 -0
  16. package/dist/{server → core}/open.js +39 -12
  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 +5 -3
  23. package/dist/core/scan/context.d.ts +13 -0
  24. package/dist/{server → core}/scan/context.js +58 -32
  25. package/dist/core/scan/data.d.ts +8 -0
  26. package/dist/core/scan/data.js +18 -0
  27. package/dist/core/scan/env.d.ts +26 -0
  28. package/dist/{server → core}/scan/env.js +10 -3
  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 +14 -0
  32. package/dist/core/scan/git.js +98 -0
  33. package/dist/core/scan/index.d.ts +5 -0
  34. package/dist/{server → core}/scan/index.js +115 -81
  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 +151 -94
  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 +6 -9
  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 +5 -4
  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 +35 -0
  54. package/dist/{server → core}/scan/settings.js +88 -92
  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 +25 -0
  58. package/dist/{server → core}/scan/usage.js +117 -92
  59. package/dist/core/schema.d.ts +8 -0
  60. package/dist/core/schema.js +67 -0
  61. package/dist/core/secrets.d.ts +44 -0
  62. package/dist/core/secrets.js +187 -0
  63. package/dist/core/settings-edit.d.ts +9 -0
  64. package/dist/core/settings-edit.js +216 -0
  65. package/dist/core/watch.d.ts +45 -0
  66. package/dist/{server → core}/watch.js +129 -20
  67. package/dist/core/write.d.ts +119 -0
  68. package/dist/{server → core}/write.js +63 -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 +2 -0
  81. package/dist/shared/requests.d.ts +104 -0
  82. package/dist/shared/requests.js +73 -0
  83. package/dist/shared/scopes.d.ts +10 -0
  84. package/dist/shared/settings-catalog.json +2199 -0
  85. package/dist/shared/types.d.ts +530 -0
  86. package/dist/shared/types.js +11 -0
  87. package/dist/ui/assets/index-CH1HoC38.js +60 -0
  88. package/dist/ui/index.html +1 -1
  89. package/dist/view/diff.d.ts +16 -0
  90. package/dist/view/diff.js +85 -0
  91. package/dist/view/format.d.ts +17 -0
  92. package/dist/view/format.js +47 -0
  93. package/dist/view/index.d.ts +11 -0
  94. package/dist/view/index.js +13 -0
  95. package/dist/view/lines.d.ts +9 -0
  96. package/dist/view/lines.js +12 -0
  97. package/dist/view/path-test.d.ts +3 -0
  98. package/dist/view/path-test.js +7 -0
  99. package/dist/view/sections/catalog.d.ts +9 -0
  100. package/dist/view/sections/catalog.js +47 -0
  101. package/dist/view/sections/extensions.d.ts +1 -0
  102. package/dist/view/sections/extensions.js +48 -0
  103. package/dist/view/sections/hooks.d.ts +1 -0
  104. package/dist/view/sections/hooks.js +32 -0
  105. package/dist/view/sections/index.d.ts +6 -0
  106. package/dist/view/sections/index.js +27 -0
  107. package/dist/view/sections/instructions.d.ts +6 -0
  108. package/dist/view/sections/instructions.js +38 -0
  109. package/dist/view/sections/mcp.d.ts +3 -0
  110. package/dist/view/sections/mcp.js +38 -0
  111. package/dist/view/sections/memory.d.ts +1 -0
  112. package/dist/view/sections/memory.js +31 -0
  113. package/dist/view/sections/permissions.d.ts +17 -0
  114. package/dist/view/sections/permissions.js +87 -0
  115. package/dist/view/sections/settings.d.ts +1 -0
  116. package/dist/view/sections/settings.js +33 -0
  117. package/dist/view/sections/tools.d.ts +6 -0
  118. package/dist/view/sections/tools.js +57 -0
  119. package/dist/view/sections/types.d.ts +123 -0
  120. package/dist/view/sections/types.js +10 -0
  121. package/dist/view/settings.d.ts +12 -0
  122. package/dist/view/settings.js +31 -0
  123. package/dist/view/usage.d.ts +8 -0
  124. package/dist/view/usage.js +18 -0
  125. package/package.json +13 -2
  126. package/dist/server/scan/files.js +0 -478
  127. package/dist/server/scan/path-test.js +0 -121
  128. package/dist/server/schema.js +0 -15
  129. package/dist/server/secrets.js +0 -63
  130. package/dist/server/settings-edit.js +0 -221
  131. package/dist/ui/assets/index-XqJwI3k9.js +0 -60
package/CHANGELOG.md CHANGED
@@ -1,5 +1,59 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.2.3](https://github.com/ayagmar/ccdesk/compare/v0.2.2...v0.2.3) (2026-10-13)
4
+
5
+ ### Features
6
+
7
+ * **cli:** add read-only scan --json and explain subcommands ([fc59aed](https://github.com/ayagmar/ccdesk/commit/fc59aed4935371e2a979991a03a94cf01c0e2e68))
8
+ * **core:** add Watcher.subscribe to push changes ([3eb360e](https://github.com/ayagmar/ccdesk/commit/3eb360e286fa34eede2c23aa1b3fe69f2fb6b129))
9
+ * **core:** make the core importable as the package entry point ([82a4107](https://github.com/ayagmar/ccdesk/commit/82a41075f88c6b0ab475eb559afe1cf13aacc25a))
10
+ * **view:** export the view logic as ccdesk/view ([b0e4365](https://github.com/ayagmar/ccdesk/commit/b0e436513da85184e9e141d2502fba1d297f39ee))
11
+
12
+ ### Bug Fixes
13
+
14
+ * **core:** refuse to open a file when no editor starts, instead of failing ([a359842](https://github.com/ayagmar/ccdesk/commit/a359842ecd33db582b565878e56a63ac00ad664e))
15
+ * **core:** refuse to open credentials or missing paths in createCore ([9562aab](https://github.com/ayagmar/ccdesk/commit/9562aab75961097c97b193f9edac8f5a34f8c701))
16
+ * **core:** report broken ~/.claude.json, managed-mcp.json and plugin files ([03710a6](https://github.com/ayagmar/ccdesk/commit/03710a6ff76557bde8e3b935244b700c8b1daf37))
17
+ * **core:** report memory, agent and skill files the scan cannot read ([3b91804](https://github.com/ayagmar/ccdesk/commit/3b9180401fd0479aa584c86f56dead7aa12b6dbf))
18
+ * **core:** resolve Watcher.subscribe and wait once moved watchers start ([039bb73](https://github.com/ayagmar/ccdesk/commit/039bb73282c014f2389ad37c37b5b80f6a91511e))
19
+ * **ui:** disable Undo while it runs and clear stale errors in Recent changes ([2c0c1e5](https://github.com/ayagmar/ccdesk/commit/2c0c1e5e381b49b40c1d99a5b9b3cfd1ce2d25a7))
20
+ * **ui:** say how to recover from an expired token wherever a call fails ([f9864a2](https://github.com/ayagmar/ccdesk/commit/f9864a24cf105691e8396de369c4167aa92f9750))
21
+ * **ui:** stop warning that the file changed after saving a setting ([e807947](https://github.com/ayagmar/ccdesk/commit/e80794746c9eed8503baa5a6cfa694568ab37740))
22
+ * **watch:** rewatch before calling subscribers, so one that throws cannot skip it ([d63fa49](https://github.com/ayagmar/ccdesk/commit/d63fa49cdf947795c8cafabc8e76458f91689601))
23
+
24
+ ### Performance Improvements
25
+
26
+ * **ui:** load the settings schema and catalog from the server ([34ba654](https://github.com/ayagmar/ccdesk/commit/34ba654ca9d5513a792ed5c44a50490a9d79dd54))
27
+
28
+ ## [0.2.2](https://github.com/ayagmar/ccdesk/compare/v0.2.1...v0.2.2) (2026-10-12)
29
+
30
+ ### Bug Fixes
31
+
32
+ * **agents:** refuse agent text with values its review would hide ([3a9b39f](https://github.com/ayagmar/ccdesk/commit/3a9b39f2634cf4a10a9fc9a7ba7c627dddebd9da))
33
+ * **compare:** ignore key order when comparing settings ([db71cde](https://github.com/ayagmar/ccdesk/commit/db71cde8c19c4bae292e5f41cd8d8c0cf7c578e4))
34
+ * **context:** count a linked rule imported by its target's path once ([63830ef](https://github.com/ayagmar/ccdesk/commit/63830ef9096c2a86005cb60bad3d1c872921cdc1))
35
+ * **context:** count a rule in a linked folder once when it is also imported ([168039b](https://github.com/ayagmar/ccdesk/commit/168039b738524b1d72f5e48edfcb51bea069a506))
36
+ * **context:** prefer the real path the scan recorded when deduplicating ([d5a3e75](https://github.com/ayagmar/ccdesk/commit/d5a3e7566668156de9a3739b9fa51a39301cf697))
37
+ * **env:** make a relative CLAUDE_CONFIG_DIR absolute ([2939488](https://github.com/ayagmar/ccdesk/commit/2939488c5ded1869367ca769218f17be8880fcd4))
38
+ * **hooks:** refuse a replacement hook with hidden header values ([c40e7b4](https://github.com/ayagmar/ccdesk/commit/c40e7b4dfeda44fe104ad24cbd790a846da31266))
39
+ * **mcp:** name the chosen profile in the claude commands ccdesk shows ([4d99fbd](https://github.com/ayagmar/ccdesk/commit/4d99fbd37087fa0a6332bc997975efb510d123b5))
40
+ * **mcp:** run claude with the profile chosen by --config-dir ([02c6af0](https://github.com/ayagmar/ccdesk/commit/02c6af0bd56235874dd1a46dd1a3aaf124f299b8))
41
+ * **path-test:** apply startup's rules to a subfolder's linked rules ([49d426c](https://github.com/ayagmar/ccdesk/commit/49d426c07beb5ce8e6538c2091b5e3c989d356e1))
42
+ * **path-test:** keep AGENTS.md out when a subfolder's CLAUDE.md is skipped ([c05ed89](https://github.com/ayagmar/ccdesk/commit/c05ed89f04db3a83b1dad2e35b803cbb4af2e4a3))
43
+ * **path-test:** match a linked rule's target against excludes only as startup does ([dfb6a79](https://github.com/ayagmar/ccdesk/commit/dfb6a79ae70c7eb4c94f4905c76b78f081c8afce))
44
+ * **path-test:** skip subfolder instructions and rules over 4 MiB ([3696d74](https://github.com/ayagmar/ccdesk/commit/3696d74ec070dda64184b100b9eecef181e52884))
45
+ * **scan:** cite the docs section on MCP rules with parentheses ([901c372](https://github.com/ayagmar/ccdesk/commit/901c3729ed4089bdfd5dd19a6265a5995aae1458))
46
+ * **scan:** treat a name starting with two dots as inside its folder ([af6b025](https://github.com/ayagmar/ccdesk/commit/af6b02549e55a24394197da08326412bb1890059))
47
+ * **settings:** refuse hook events Claude Code does not know ([b6561e6](https://github.com/ayagmar/ccdesk/commit/b6561e6f9b5a86a9fa27b3138cb570f493edb0d5))
48
+ * **usage:** do not start a count for a page that already left ([4984a58](https://github.com/ayagmar/ccdesk/commit/4984a585d6a6ccd3914f2e06b9f274bd4b66eb21))
49
+ * **watch:** answer a wait whose watcher closed while it was starting ([5c63129](https://github.com/ayagmar/ccdesk/commit/5c63129bdb60d9bc5f6d13a7665e4f273dcfb406))
50
+ * **watch:** answer waiting pages when the watcher closes ([ad1c5ea](https://github.com/ayagmar/ccdesk/commit/ad1c5ea1080362858755e5f2c09e4bf19a3682e2))
51
+ * **watch:** refresh when a file inside an installed plugin changes ([a09a99e](https://github.com/ayagmar/ccdesk/commit/a09a99e38c37c765c5117138324d50174ad7b142))
52
+ * **watch:** refresh when a missing import is created ([279b033](https://github.com/ayagmar/ccdesk/commit/279b03353f414ed9faeaf0a86450c49da56aea1a))
53
+ * **watch:** refresh when a missing import's folder appears later ([91f6444](https://github.com/ayagmar/ccdesk/commit/91f644452c3975dd09b2f1fbb86b2c96cf5da561))
54
+ * **watch:** watch ~/.claude/plugins only for the install list ([382b6b0](https://github.com/ayagmar/ccdesk/commit/382b6b0990c82f71dab37cfd8840bd1aec42c47e))
55
+ * **watch:** watch only the plugin files the scan reads ([8d12adc](https://github.com/ayagmar/ccdesk/commit/8d12adcfa0cdf74097273516d04130a99cffe578))
56
+
3
57
  ## [0.2.1](https://github.com/ayagmar/ccdesk/compare/v0.2.0...v0.2.1) (2026-10-12)
4
58
 
5
59
  ### Features
package/README.md CHANGED
@@ -8,6 +8,7 @@
8
8
  [![npm](https://img.shields.io/npm/v/ccdesk?style=flat-square&color=141414)](https://www.npmjs.com/package/ccdesk)
9
9
  [![CI](https://img.shields.io/github/actions/workflow/status/ayagmar/ccdesk/ci.yml?branch=master&style=flat-square&label=ci)](https://github.com/ayagmar/ccdesk/actions/workflows/ci.yml)
10
10
  [![node](https://img.shields.io/node/v/ccdesk?style=flat-square&color=141414)](https://github.com/ayagmar/ccdesk/blob/master/package.json)
11
+ [![Ask DeepWiki](https://img.shields.io/badge/Ask-DeepWiki-blue)](https://deepwiki.com/ayagmar/ccdesk)
11
12
  [![license](https://img.shields.io/github/license/ayagmar/ccdesk?style=flat-square&color=141414)](https://github.com/ayagmar/ccdesk/blob/master/LICENSE)
12
13
 
13
14
  See and change everything Claude Code loads on your machine: instruction files, settings, permissions, hooks, MCP servers, subagents, skills, plugins and auto memory.
@@ -16,7 +17,7 @@ See and change everything Claude Code loads on your machine: instruction files,
16
17
  npx ccdesk
17
18
  ```
18
19
 
19
- Each piece lives in its own file, at one of six scopes: managed, user, project, local, auto memory or a plugin. ccdesk opens in your browser on the directory you ran it from, lists everything that applies there in the order Claude Code loads it, and names the file behind each piece. When something is wrong, change it on the same page: you review the diff first, and every edit has a backup and an undo. It is not a session viewer or a usage dashboard.
20
+ Each piece lives in its own file, at one of six scopes: managed, user, project, local, auto memory or a plugin. ccdesk opens in your browser on the directory you ran it from, lists everything that applies there in the order Claude Code loads it, and names the file behind each piece. When something is wrong, change it on the same page: you review the diff first, and every file ccdesk writes is backed up and can be undone. It is not a session viewer or a usage dashboard.
20
21
 
21
22
  **Docs:** [ayagmar.github.io/ccdesk](https://ayagmar.github.io/ccdesk/)
22
23
 
@@ -74,7 +75,7 @@ Each piece lives in its own file, at one of six scopes: managed, user, project,
74
75
  - The old file is backed up to `~/.claude/backups/ccdesk/` first. Undo is one click, and **Recent changes** can restore any earlier edit.
75
76
  - A settings file that is a symlink, such as one in a dotfiles repository, is written through and stays a link.
76
77
  - A file with other hard links is not saved: replacing it would split it from its other names. Edit it in your editor.
77
- - ccdesk never writes `~/.claude.json`. User and local MCP server changes go through Claude Code's own `claude mcp` command, so they need `claude` on your PATH.
78
+ - ccdesk never writes `~/.claude.json`. User and local MCP server changes go through Claude Code's own `claude mcp` command, so they need `claude` on your PATH. ccdesk keeps no backup of these and cannot undo them; **Recent changes** lists each one with the commands that reverse it.
78
79
 
79
80
  Details: [Recent changes, backups and undo](https://ayagmar.github.io/ccdesk/recent-changes/)
80
81
 
@@ -95,16 +96,19 @@ More in [Reading the interface](https://ayagmar.github.io/ccdesk/interface/).
95
96
  ## Options
96
97
 
97
98
  ```
98
- 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
99
102
 
100
103
  --port <n> Port to listen on (default: a free port)
101
104
  --no-open Print the URL instead of opening a browser
102
105
  --config-dir <path> Claude folder to read, as CLAUDE_CONFIG_DIR sets it for Claude Code
103
106
  (wins over $CLAUDE_CONFIG_DIR)
107
+ --dir <dir> For explain: the folder the session starts in (default: the current one)
104
108
  -h, --help Show this help
105
109
  ```
106
110
 
107
- 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. 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.
108
112
 
109
113
  ## Privacy and safety
110
114
 
@@ -125,7 +129,7 @@ pnpm dev:ui # UI with hot reload on localhost:4320
125
129
  pnpm check # typecheck, lint, format, tests, build
126
130
  ```
127
131
 
128
- 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.
129
133
 
130
134
  ## License
131
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, serially, 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);
@@ -40,38 +42,35 @@ async function plan(env, edit) {
40
42
  // from it would lose them; it can still be deleted, as a whole.
41
43
  if (edit.text !== null && before !== raw)
42
44
  throw new RefusedError(`${path} has env, header or URL values ccdesk does not show; edit it in your editor instead.`);
43
- const errors = edit.text === null ? [] : agentProblems(edit.text);
45
+ let errors = edit.text === null ? [] : agentProblems(edit.text);
46
+ // Nor does it write values its review would hide.
47
+ if (edit.text !== null && errors.length === 0 && shownText(edit.text) !== edit.text)
48
+ errors = [
49
+ "ccdesk does not write env, header or URL values it would then hide; add them in your editor instead.",
50
+ ];
51
+ const { text } = edit;
44
52
  return {
45
- path,
46
- raw,
47
53
  preview: {
48
54
  path,
49
55
  before,
50
- after: errors.length > 0 ? (before ?? "") : (shownText(edit.text) ?? ""),
56
+ after: errors.length > 0 ? (before ?? "") : (shownText(text) ?? ""),
51
57
  revision: revisionOf(raw),
52
58
  reformats: false,
53
59
  errors,
54
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
+ }),
55
68
  };
56
69
  }
57
70
  export async function previewAgentEdit(env, edit) {
58
71
  return (await plan(env, edit)).preview;
59
72
  }
60
73
  /** Writes or deletes an agent file, backed up first, only while it is as reviewed. */
61
- export function saveAgentEdit(env, edit, expectedRevision) {
62
- return serially(async () => {
63
- const { path, raw, preview } = await plan(env, edit);
64
- if (preview.errors.length > 0)
65
- throw new RefusedError(preview.errors.join(" "));
66
- if (preview.revision !== expectedRevision)
67
- throw new RefusedError("The agent changed since you reviewed this; review it again.");
68
- const { text } = edit;
69
- return recordChange(env, { kind: "agent", target: path, before: raw }, async () => {
70
- if (text !== null)
71
- await writeAtomic(env, path, text, expectedRevision);
72
- else if (!(await removeExactly(path, expectedRevision)))
73
- throw new StaleError(`${path} changed while ccdesk deleted it; nothing was deleted.`);
74
- return { target: path, revision: revisionOf(text) };
75
- });
76
- });
74
+ export function saveAgentEdit(env, edit, expectedRevision, queue = serially) {
75
+ return saveReviewed(queue, () => plan(env, edit), expectedRevision, "The agent changed since you reviewed this; review it again.");
77
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,23 +1,13 @@
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";
5
6
  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
- }
7
+ import { isWithin, revisionOf } from "./scan/files.js";
8
+ import { hiddenValues, shown, shownText } from "./secrets.js";
9
+ import { BackupManifestSchema, backupRoot, forgetChange, MANIFEST, RefusedError, readIfExists, recordChange, removeExactly, SHOWN_AFTER, SNAPSHOT, StaleError, serially, writeAtomic, } from "./write.js";
10
+ const backupManifest = Compile(BackupManifestSchema);
21
11
  /** A backup folder's manifest; anything that does not look like one ccdesk wrote is skipped. */
22
12
  async function readManifest(env, id) {
23
13
  // ccdesk's ids are letters, digits and dashes, so no id can name a folder outside the root.
@@ -33,17 +23,7 @@ async function readManifest(env, id) {
33
23
  catch {
34
24
  return null;
35
25
  }
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;
26
+ return backupManifest.Check(data) ? data : null;
47
27
  }
48
28
  /** The changes ccdesk made, newest first, a page at a time. */
49
29
  export async function listBackups(env, { before, limit }) {
@@ -131,26 +111,38 @@ async function claudeMcpPreview(id, manifest, dir) {
131
111
  }
132
112
  async function planRestore(env, id) {
133
113
  const { manifest, dir } = await backupAt(env, id);
134
- if (manifest.kind === "mcp-claude")
135
- throw new RefusedError(MADE_BY_CLAUDE);
136
114
  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));
115
+ switch (manifest.kind) {
116
+ case "mcp-claude":
117
+ throw new RefusedError(MADE_BY_CLAUDE);
118
+ case "settings":
119
+ if (!isSettingsFile(env, target))
120
+ throw new RefusedError("This backup does not point at a settings file.");
121
+ break;
122
+ case "mcp":
123
+ if (basename(target) !== ".mcp.json")
124
+ throw new RefusedError("This backup does not point at a .mcp.json.");
125
+ break;
126
+ case "agent":
127
+ if (!isAgentFile(env, target))
128
+ throw new RefusedError("This backup does not point at a subagent file.");
129
+ break;
147
130
  // 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));
131
+ case "memory-index": {
132
+ if (basename(target) !== "MEMORY.md")
133
+ throw new RefusedError("This backup does not point at a MEMORY.md.");
134
+ target = await indexPathIn(await assertMemoryDir(env, dirname(target)));
135
+ break;
136
+ }
150
137
  // 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.");
138
+ case "memory-delete":
139
+ case "memory-restore": {
140
+ const store = await assertMemoryDir(env, dirname(target));
141
+ target = join(store, basename(target));
142
+ if (!target.endsWith(".md") || basename(target) === "MEMORY.md")
143
+ throw new RefusedError("This backup does not point at a memory file.");
144
+ break;
145
+ }
154
146
  }
155
147
  let saved = null;
156
148
  if (manifest.before !== revisionOf(null)) {
@@ -247,8 +239,8 @@ export async function previewRestore(env, id) {
247
239
  * Puts a backed-up file back, or removes a file ccdesk created, if the file is still as it was
248
240
  * when the restore was reviewed. A removed file is backed up first, so it can be put back too.
249
241
  */
250
- export function restoreBackup(env, id, expectedRevision) {
251
- return serially(async () => {
242
+ export function restoreBackup(env, id, expectedRevision, queue = serially) {
243
+ return queue(async () => {
252
244
  const plan = await planRestore(env, id);
253
245
  const { manifest, current, saved, index } = plan;
254
246
  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, readUsage } from "./scan/usage.ts";
8
+ export { hasHiddenValues, redactSecrets, shown, shownText } from "./secrets.ts";
9
+ export { Watcher } from "./watch.ts";
10
+ export { previewAgentEdit, saveAgentEdit } from "./agent-edit.ts";
11
+ export { claudeProgram, claudeRunner, previewMcpEdit, type RunClaude, runClaude, saveMcpEdit, } from "./mcp-edit.ts";
12
+ export { changeMemory, isMemoryAction, previewMemoryChange } from "./mutations.ts";
13
+ export { InvalidEditError, parseAgentEdit, parseHookEdit, parseMcpEdit, parsePermissionEdit, parseSettingsEdit, } from "./parse-edit.ts";
14
+ export { previewHookEdit, previewPermissionEdit, previewSettingsEdit, saveHookEdit, savePermissionEdit, saveSettingsEdit, } from "./settings-edit.ts";
15
+ export { RefusedError, StaleError } from "./write.ts";
16
+ export { listBackups, previewRestore, restoreBackup } 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 the reads and changes below with the env
5
+ // already passed, its own change queue and usage cache, and the editor and claude its env names.
6
+ export { createCore } from "./core.js";
7
+ // Environment: every function below takes the ScanEnv first. Called directly, they share one
8
+ // change queue and one usage cache across the process; they stay for one release beside createCore.
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, readUsage } 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, save takes it back. Both refuse with RefusedError,
20
+ // also when the file changed since the preview; StaleError, a RefusedError, means it changed while
21
+ // ccdesk was writing it.
22
+ export { previewAgentEdit, saveAgentEdit } from "./agent-edit.js";
23
+ export { claudeProgram, claudeRunner, previewMcpEdit, runClaude, saveMcpEdit, } from "./mcp-edit.js";
24
+ export { changeMemory, isMemoryAction, previewMemoryChange } from "./mutations.js";
25
+ export { InvalidEditError, parseAgentEdit, parseHookEdit, parseMcpEdit, parsePermissionEdit, parseSettingsEdit, } from "./parse-edit.js";
26
+ export { previewHookEdit, previewPermissionEdit, previewSettingsEdit, saveHookEdit, savePermissionEdit, saveSettingsEdit, } from "./settings-edit.js";
27
+ export { RefusedError, StaleError } from "./write.js";
28
+ // History: every change ccdesk saves is a backup that can be restored by its id.
29
+ export { listBackups, previewRestore, restoreBackup } 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,25 @@
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
+ /** claudeRunner for this process's PATH and environment, read on each run; use a core's instead. */
20
+ export declare const runClaude: RunClaude;
21
+ export declare function previewMcpEdit(env: ScanEnv, edit: McpEdit, run: RunClaude): Promise<SettingsEditPreview>;
22
+ /** Makes a reviewed MCP edit, only while the servers are still as reviewed. */
23
+ export declare function saveMcpEdit(env: ScanEnv, edit: McpEdit, expectedRevision: string, run: RunClaude, queue?: Serially): Promise<Change | {
24
+ target: string;
25
+ }>;