@cmodjs/core 3.1.0 → 4.0.1

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 (200) hide show
  1. package/docs/commands.md +38 -21
  2. package/docs/dependencies.md +27 -20
  3. package/docs/index.md +21 -17
  4. package/docs/install-steps.md +37 -29
  5. package/docs/jobs.md +25 -52
  6. package/docs/mod.md +93 -65
  7. package/docs/options.md +2 -2
  8. package/docs/permissions.md +4 -4
  9. package/docs/session.md +14 -12
  10. package/docs/testing.md +55 -32
  11. package/docs/ui.md +255 -263
  12. package/hotkeys.d.ts +22 -0
  13. package/hotkeys.d.ts.map +1 -0
  14. package/hotkeys.js +26 -0
  15. package/installer.d.ts +1 -1
  16. package/installer.d.ts.map +1 -1
  17. package/installer.js +11 -6
  18. package/jobs/check.d.ts +2 -1
  19. package/jobs/check.d.ts.map +1 -1
  20. package/jobs/check.js +2 -2
  21. package/jobs/context.d.ts +2 -1
  22. package/jobs/context.d.ts.map +1 -1
  23. package/jobs/context.js +1 -1
  24. package/jobs/permissions.d.ts +2 -1
  25. package/jobs/permissions.d.ts.map +1 -1
  26. package/jobs/permissions.js +2 -2
  27. package/jobs/program.js +1 -1
  28. package/jobs/prompt.d.ts +2 -1
  29. package/jobs/prompt.d.ts.map +1 -1
  30. package/jobs/prompt.js +20 -13
  31. package/jobs/schedule.d.ts +2 -1
  32. package/jobs/schedule.d.ts.map +1 -1
  33. package/jobs/schedule.js +15 -1
  34. package/jobs/slash-command.d.ts +6 -2
  35. package/jobs/slash-command.d.ts.map +1 -1
  36. package/jobs/slash-command.js +6 -5
  37. package/jobs/tool.d.ts +6 -4
  38. package/jobs/tool.d.ts.map +1 -1
  39. package/jobs/tool.js +3 -3
  40. package/mod.d.ts +63 -42
  41. package/mod.d.ts.map +1 -1
  42. package/mod.js +11 -1
  43. package/package.json +1 -1
  44. package/records.d.ts +13 -8
  45. package/records.d.ts.map +1 -1
  46. package/records.js +41 -31
  47. package/register.d.ts.map +1 -1
  48. package/register.js +1 -2
  49. package/runtime/announce.d.ts +1 -1
  50. package/runtime/announce.d.ts.map +1 -1
  51. package/runtime/announce.js +6 -7
  52. package/runtime/before.d.ts +2 -2
  53. package/runtime/before.d.ts.map +1 -1
  54. package/runtime/before.js +1 -1
  55. package/runtime/claude.d.ts +0 -1
  56. package/runtime/claude.d.ts.map +1 -1
  57. package/runtime/conversations.d.ts +4 -1
  58. package/runtime/conversations.d.ts.map +1 -1
  59. package/runtime/conversations.js +42 -14
  60. package/runtime/dependencies.d.ts.map +1 -1
  61. package/runtime/dependencies.js +2 -1
  62. package/runtime/engine.d.ts +5 -0
  63. package/runtime/engine.d.ts.map +1 -1
  64. package/runtime/grants.js +2 -2
  65. package/runtime/hooks.d.ts +1 -1
  66. package/runtime/hooks.d.ts.map +1 -1
  67. package/runtime/hotkeys.d.ts +15 -0
  68. package/runtime/hotkeys.d.ts.map +1 -0
  69. package/runtime/hotkeys.js +58 -0
  70. package/runtime/installer.d.ts +5 -2
  71. package/runtime/installer.d.ts.map +1 -1
  72. package/runtime/installer.js +23 -12
  73. package/runtime/lifecycle.d.ts.map +1 -1
  74. package/runtime/lifecycle.js +190 -141
  75. package/runtime/options.js +2 -2
  76. package/runtime/pages.d.ts +28 -0
  77. package/runtime/pages.d.ts.map +1 -0
  78. package/runtime/pages.js +98 -0
  79. package/runtime/parts.d.ts +14 -0
  80. package/runtime/parts.d.ts.map +1 -0
  81. package/runtime/parts.js +59 -0
  82. package/runtime/permission-check.d.ts +2 -2
  83. package/runtime/permission-check.d.ts.map +1 -1
  84. package/runtime/programs.js +1 -1
  85. package/runtime/router.d.ts +2 -2
  86. package/runtime/router.d.ts.map +1 -1
  87. package/runtime/router.js +1 -1
  88. package/runtime/sessions.d.ts +10 -9
  89. package/runtime/sessions.d.ts.map +1 -1
  90. package/runtime/sessions.js +14 -5
  91. package/runtime/state.d.ts +2 -1
  92. package/runtime/state.d.ts.map +1 -1
  93. package/runtime/state.js +1 -0
  94. package/runtime/toaster.d.ts +1 -1
  95. package/runtime/toaster.d.ts.map +1 -1
  96. package/runtime/toaster.js +10 -4
  97. package/runtime/ui.d.ts +87 -5
  98. package/runtime/ui.d.ts.map +1 -1
  99. package/runtime/ui.js +374 -109
  100. package/runtime/watches.d.ts +9 -0
  101. package/runtime/watches.d.ts.map +1 -0
  102. package/runtime/watches.js +73 -0
  103. package/runtime/widgets.d.ts +48 -0
  104. package/runtime/widgets.d.ts.map +1 -0
  105. package/runtime/widgets.js +219 -0
  106. package/session/context/turn/message/message.d.ts.map +1 -1
  107. package/session/context/turn/message/message.js +7 -2
  108. package/session/session.d.ts +11 -5
  109. package/session/session.d.ts.map +1 -1
  110. package/src/hotkeys.ts +51 -0
  111. package/src/installer.ts +8 -4
  112. package/src/jobs/check.ts +4 -3
  113. package/src/jobs/context.ts +3 -2
  114. package/src/jobs/permissions.ts +4 -3
  115. package/src/jobs/program.ts +1 -1
  116. package/src/jobs/prompt.ts +23 -14
  117. package/src/jobs/schedule.ts +16 -2
  118. package/src/jobs/slash-command.ts +10 -7
  119. package/src/jobs/tool.ts +7 -5
  120. package/src/mod.ts +59 -47
  121. package/src/records.ts +46 -31
  122. package/src/register.ts +2 -3
  123. package/src/runtime/announce.ts +6 -7
  124. package/src/runtime/before.ts +4 -4
  125. package/src/runtime/claude.ts +0 -1
  126. package/src/runtime/conversations.ts +66 -20
  127. package/src/runtime/dependencies.ts +2 -1
  128. package/src/runtime/engine.ts +5 -0
  129. package/src/runtime/grants.ts +2 -2
  130. package/src/runtime/hooks.ts +1 -0
  131. package/src/runtime/hotkeys.ts +80 -0
  132. package/src/runtime/installer.ts +24 -12
  133. package/src/runtime/lifecycle.ts +189 -136
  134. package/src/runtime/options.ts +2 -2
  135. package/src/runtime/pages.ts +127 -0
  136. package/src/runtime/parts.ts +70 -0
  137. package/src/runtime/permission-check.ts +2 -2
  138. package/src/runtime/programs.ts +1 -1
  139. package/src/runtime/router.ts +3 -3
  140. package/src/runtime/sessions.ts +24 -15
  141. package/src/runtime/state.ts +3 -1
  142. package/src/runtime/toaster.ts +10 -4
  143. package/src/runtime/ui.ts +407 -115
  144. package/src/runtime/watches.ts +75 -0
  145. package/src/runtime/widgets.ts +287 -0
  146. package/src/session/context/turn/message/message.ts +7 -2
  147. package/src/session/session.ts +10 -6
  148. package/src/testing/fake-claude.ts +9 -6
  149. package/src/testing/fake-elements.ts +7 -4
  150. package/src/testing/fake-sessions.ts +6 -4
  151. package/src/testing.ts +203 -46
  152. package/src/ui/areas.ts +27 -0
  153. package/src/ui/elements.ts +35 -0
  154. package/src/ui/markdown.ts +18 -18
  155. package/src/ui/panes.ts +73 -0
  156. package/src/ui/styles.ts +53 -0
  157. package/src/utils/subscription.ts +1 -0
  158. package/testing/fake-claude.d.ts +0 -1
  159. package/testing/fake-claude.d.ts.map +1 -1
  160. package/testing/fake-claude.js +9 -5
  161. package/testing/fake-elements.js +7 -4
  162. package/testing/fake-sessions.d.ts +2 -1
  163. package/testing/fake-sessions.d.ts.map +1 -1
  164. package/testing/fake-sessions.js +1 -0
  165. package/testing.d.ts +21 -6
  166. package/testing.d.ts.map +1 -1
  167. package/testing.js +206 -45
  168. package/ui/areas.d.ts +29 -0
  169. package/ui/areas.d.ts.map +1 -0
  170. package/ui/areas.js +6 -0
  171. package/ui/elements.d.ts +3 -0
  172. package/ui/elements.d.ts.map +1 -1
  173. package/ui/elements.js +36 -0
  174. package/ui/markdown.d.ts +14 -14
  175. package/ui/markdown.d.ts.map +1 -1
  176. package/ui/markdown.js +11 -11
  177. package/ui/panes.d.ts +71 -0
  178. package/ui/panes.d.ts.map +1 -0
  179. package/ui/panes.js +16 -0
  180. package/ui/styles.d.ts +44 -0
  181. package/ui/styles.d.ts.map +1 -0
  182. package/ui/styles.js +24 -0
  183. package/utils/subscription.d.ts +4 -0
  184. package/utils/subscription.d.ts.map +1 -0
  185. package/jobs/status-line.d.ts +0 -6
  186. package/jobs/status-line.d.ts.map +0 -1
  187. package/jobs/status-line.js +0 -39
  188. package/src/jobs/status-line.ts +0 -44
  189. package/src/ui/define-pane.ts +0 -27
  190. package/src/ui/places.ts +0 -49
  191. package/src/utils/disposable.ts +0 -3
  192. package/ui/define-pane.d.ts +0 -15
  193. package/ui/define-pane.d.ts.map +0 -1
  194. package/ui/define-pane.js +0 -12
  195. package/ui/places.d.ts +0 -43
  196. package/ui/places.d.ts.map +0 -1
  197. package/ui/places.js +0 -31
  198. package/utils/disposable.d.ts +0 -4
  199. package/utils/disposable.d.ts.map +0 -1
  200. /package/utils/{disposable.js → subscription.js} +0 -0
package/docs/commands.md CHANGED
@@ -12,7 +12,7 @@ Each command below restates what `cmod <command> --help` prints, for a mod autho
12
12
  cmod new <name> [--project]
13
13
  ```
14
14
 
15
- Creates a mod in `./<name>`: a `defineMod` with one listener, one pane, and one render of the band above the prompt, a Skill, a test, and a `CHANGELOG.md` ([mod.md](mod.md#release-notes)). Then installs its packages.
15
+ Creates a mod in `./<name>`: a `defineMod` with one listener, a Sidebar page, a widget, and a hotkey that shows the page, a Skill, a test, and a `CHANGELOG.md` ([mod.md](mod.md#release-notes)). Then installs its packages.
16
16
 
17
17
  - `<name>` is lowercase letters, digits, and `-`, starting with a letter.
18
18
  - `--project` creates a project-scope plugin in `./.claude/skills/<name>/` of the repository in the current folder, which loads for everyone who trusts the repository. Run it in the repository's root.
@@ -25,7 +25,7 @@ Creates a mod in `./<name>`: a `defineMod` with one listener, one pane, and one
25
25
  cmod link [path] [--yes] [--option key=value]...
26
26
  ```
27
27
 
28
- Loads the checkout at `path` (default: the current folder) in every new Claude Code session, in place of the installed mod. Installs the cmod plugin when Claude Code lacks it, unless the checkout is cmod itself. Then installs the checkout's packages, builds the program `cli/` declares and links it into cmod's programs folder and `~/.local/bin`, writes the folder into `CLAUDE_CODE_PLUGIN_DIRS` in the `env` block of Claude Code's `settings.json`, and runs the checkout's install step. A session that is already running loads it after `/reload-plugins`.
28
+ Loads the checkout at `path` (default: the current folder) in every new Claude Code session, in place of the installed mod. Installs the cmod plugin when Claude Code lacks it, unless the checkout is cmod itself. Then installs the checkout's packages, builds the program `cli/` declares and links it into cmod's programs folder and `~/.local/bin`, writes the folder into `CLAUDE_CODE_PLUGIN_DIRS` in the `env` block of Claude Code's `settings.json`, and runs the checkout's install step. Claude Code reads that list only when a session starts, so a session that is already running loads the checkout once the person starts a new one. From then on, Claude Code reloads the checkout in each session when a file in it is saved.
29
29
 
30
30
  - `--yes` approves the mod's install and uninstall commands without asking.
31
31
  - `--option key=value` sets one of the mod's options through `claude plugin configure`, once per option, under the id `claude plugin list` shows for the linked checkout, such as `my-mod@inline`. It refuses a key the mod does not declare, and names the keys it does ([options.md](options.md)).
@@ -48,17 +48,21 @@ Runs every check this machine can run on the mod at `path` (default: the current
48
48
 
49
49
  1. Installs its packages.
50
50
  2. Checks its layout.
51
- 3. Checks that each step runs a script, and prints the permissions the person grants at install and can grant later, as the person reads them ([permissions.md](permissions.md)). It adds `"approve": true` when the mod answers the permission check, with a `beforePermissionCheck` or `beforePermissionRequest` listener or the `permissions` job, and `hooks/register.ts` calls `registerPermissionCheck`. It fails a mod whose `src/` hears permissions while `register.ts` does not call `registerPermissionCheck`.
52
- 4. Writes the options `defineMod` declares into `.claude-plugin/plugin.json` `userConfig`, keeping the file's own formatting, so Claude Code shows them in `/config` ([options.md](options.md)). It loads `hooks/register.ts` to read them, without starting the mod.
53
- 5. Checks its imports.
54
- 6. Looks for prebuilt binaries outside `cli/`.
55
- 7. Bundles the hooks module as `cmod publish` does, and names each reason a bundle fails.
56
- 8. Validates it with Claude Code, `claude plugin validate --strict`.
57
- 9. Compiles it against Claude Code's API with `tsc`, with `tsconfig.json`, and then with `tests/tsconfig.json` when it exists. `cmod new` writes both: the root config leaves Bun out of `src/`, which runs inside Claude Code, and the tests config adds `bun-types`, so a test can run a real program with `Bun.spawn` or read files with `node:fs`. When `.claude-plugin/types/` is missing, as in a fresh clone or in CI, Claude Code writes it first: cmod loads the mod in one `claude -p` run with a config folder of its own and no model to reach, so the run sends nothing.
58
- 10. Lints it with `oxlint`.
59
- 11. Runs, with `bun test`, every test file that does not import `claude-code/testing`. When `bunfig.toml` names a `[test]` `root`, it runs only the test files under that folder, as `bun test` does.
60
- 12. Runs, with `claude plugin test`, the test files that import `claude-code/testing`. It skips this when none does.
61
- 13. Checks its name against other plugins. Only a project plugin, in `.claude/skills/<name>/` of a repository, runs this check: it fails when an installed or linked plugin has the same name and hides the project plugin. Any other mod passes it at once.
51
+ 3. Checks that each step runs a script.
52
+ 4. Checks that `package.json` `"os"` and `"cpu"` list only machines cmod runs mods on, darwin and linux on arm64 and x64, and that the install step downloads a program for each machine they list.
53
+ 5. Prints the permissions the person grants at install and can grant later, as the person reads them ([permissions.md](permissions.md)). It adds `"approve": true` when the mod allows or asks on the permission check and `hooks/register.ts` calls `registerPermissionCheck`. It fails a mod whose `src/` hears permissions while `register.ts` does not call `registerPermissionCheck`.
54
+ 6. Reads the metadata of every file in the mod, and names each file whose metadata cmod cannot read.
55
+ 7. Writes the options `defineMod` declares into `.claude-plugin/plugin.json` `userConfig`, and its hotkeys into `package.json` `"cmod"` `hotkeys`, keeping each file's own formatting, so Claude Code shows the options in `/config` ([options.md](options.md)). It loads `hooks/register.ts` to read them, without starting the mod.
56
+ 8. Writes the `cmod` dependency in `.claude-plugin/plugin.json` as `>=` the version of the `@cmodjs/core` the mod installed, such as `>=4.0.0`, so every newer cmod runs the mod. A mod still on `@cmodjs/core` 3 keeps its own range ([dependencies.md](dependencies.md#call-the-methods-mod-dependencies)).
57
+ 9. Checks its imports.
58
+ 10. Looks for prebuilt binaries outside `cli/`.
59
+ 11. Bundles the hooks module as `cmod publish` does, and names each reason a bundle fails.
60
+ 12. Validates it with Claude Code, `claude plugin validate --strict`.
61
+ 13. Compiles it against Claude Code's API with `tsc`, with `tsconfig.json`, and then with `tests/tsconfig.json` when it exists. `cmod new` writes both: the root config leaves Bun out of `src/`, which runs inside Claude Code, and the tests config adds `bun-types`, so a test can run a real program with `Bun.spawn` or read files with `node:fs`. When `.claude-plugin/types/` is missing, as in a fresh clone or in CI, Claude Code writes it first: cmod loads the mod in one `claude -p` run with a config folder of its own and no model to reach, so the run sends nothing.
62
+ 14. Lints it with `oxlint`.
63
+ 15. Runs, with `bun test`, every test file that does not import `claude-code/testing`. When `bunfig.toml` names a `[test]` `root`, it runs only the test files under that folder, as `bun test` does.
64
+ 16. Runs, with `claude plugin test`, the test files that import `claude-code/testing`. It skips this when none does.
65
+ 17. Checks its name against other plugins. Only a project plugin, in `.claude/skills/<name>/` of a repository, runs this check: it fails when an installed or linked plugin has the same name and hides the project plugin. Any other mod passes it at once.
62
66
 
63
67
  `cmod check` fetches `tsc` 7.0.2 and `oxlint` 1.86.0 into the cmod store the first time. It ends with a count of passed, failed, and skipped checks, and exits 1 when one failed.
64
68
 
@@ -114,12 +118,15 @@ It needs, and refuses to start without:
114
118
  - a `package.json` with no dependency on a `file:` or `link:` path, which no user has
115
119
  - a `"files"` list, when `package.json` has one, whose every path matches a committed file
116
120
  - a `"program"` in the `cmod` key of `package.json` that names the program `cli/` declares, when `cli/` declares one
121
+ - a `cmod` dependency in `.claude-plugin/plugin.json` of `>=` the `@cmodjs/core` the mod bundles, which `cmod check` writes
117
122
  - for a real publish, a working tree with no uncommitted changes, no `v<version>` or `<name>--v<version>` tag yet, a `git push` to `origin` that succeeds, and the `gh` command, which creates the GitHub release
118
123
 
119
124
  `--dry-run` builds everything into a temporary folder, and pushes nothing. It writes `marketplace.json` there too, so the working tree stays as it was. It allows uncommitted changes and an existing tag, and builds from the last commit. It prints the folder that holds the release, so you can read what the directory will read.
120
125
 
121
126
  ## Install mods
122
127
 
128
+ `cmod install`, `cmod update`, `cmod remove`, `cmod enable`, `cmod disable`, `cmod link`, `cmod unlink`, and `cmod try` write the time to `plugins-changed` in the cmod store when they finish, also when they fail partway, such as an update that updated some mods. The cmod plugin of every running session watches that file and runs `/reload-plugins`, so each session runs the mods the person has now. A reload clears each mod's `memory` state. A machine where cmod set up no mod has no store, and these commands write nothing there. `cmod permission` needs no reload, because a mod sees its grants change before its next call.
129
+
123
130
  ### cmod install
124
131
 
125
132
  ```text
@@ -142,7 +149,7 @@ Adds the plugin's marketplace to Claude Code and installs the plugin through Cla
142
149
  cmod update [name] [--yes]
143
150
  ```
144
151
 
145
- Updates the named plugin, or every plugin Claude Code has installed, through Claude Code, then runs the install step of each mod whose version or scripts changed. A linked plugin is skipped, because saving its files updates it. When Claude Code fails to update a plugin, `cmod update` names it, updates the rest, and exits 1. A running session loads the new version after `/reload-plugins`.
152
+ Updates the named plugin, or every plugin Claude Code has installed, through Claude Code, then runs the install step of each mod whose version or scripts changed. A linked plugin is skipped, because saving its files updates it. When Claude Code fails to update a plugin, `cmod update` names it, updates the rest, and exits 1. Every running session then loads the new version.
146
153
 
147
154
  - `--yes` approves changed install and uninstall commands without asking.
148
155
 
@@ -161,15 +168,17 @@ cmod enable <name>
161
168
  cmod disable <name>
162
169
  ```
163
170
 
164
- Turns the plugin on or off through Claude Code, as `/plugin` does. A running session follows after `/reload-plugins`. A mod keeps its setup, its data, and its program while it is off, and runs no install or uninstall step either way. They refuse a plugin linked with `cmod link`, which `cmod unlink` stops loading. The `Turn off` and `Turn on` buttons in `/mods` run them.
171
+ Turns the plugin on or off through Claude Code, as `/plugin` does, and every running session follows. A mod keeps its setup, its data, and its program while it is off, and runs no install or uninstall step either way. They refuse a plugin linked with `cmod link`, which `cmod unlink` stops loading. The `Turn off` and `Turn on` buttons in Settings run them.
165
172
 
166
173
  ### cmod list
167
174
 
168
175
  ```text
169
- cmod list
176
+ cmod list [--json]
170
177
  ```
171
178
 
172
- Lists every plugin Claude Code has installed or linked, with its version. A mod shows whether cmod set it up, and any other plugin shows whether Claude Code enabled it. It also lists every mod Claude Code removed whose uninstall step has not run yet.
179
+ Lists every plugin Claude Code has installed or linked, with its version. A mod shows whether cmod set it up, and any other plugin shows whether Claude Code enabled it. A plugin Claude Code turned off shows Claude Code's reason, such as `Requires "cmod@market" >=4.2.0, installed 4.0.0`. It also lists every mod Claude Code removed whose uninstall step has not run yet.
180
+
181
+ - `--json` prints the list as JSON: one object per row with its `name`, `version`, `state`, and `unsatisfied`, the dependencies Claude Code turned the plugin off for because they are older than the plugin needs.
173
182
 
174
183
  - The `cmod` commands count a plugin as a mod only when its `package.json` has a `cmod` step key or cmod holds an install record for it. A mod made with `cmod new` has neither until its first Claude Code session sets it up, so until then `cmod list` shows it as `enabled`, like any other plugin.
175
184
 
@@ -205,7 +214,7 @@ Reads every conversation Claude Code saved, as the sessions, contexts, turns, ta
205
214
  - `state` prints what a running conversation resumes from.
206
215
  - `follow` prints each message, turn, context, task, and goal change of the sessions named as Claude Code saves it, until stopped with Ctrl+C. `--running` follows every running session of the project too, and each one that starts later. The cmod plugin runs it to hear other running sessions.
207
216
  - `--json` prints JSON instead of one line each.
208
- - `index` reads every saved conversation into the index at `~/.local/share/cmod/history-7.sqlite`, which holds where each message is in its transcript, not a copy of it, and each conversation's turns, contexts, and tasks. While a mod other than cmod holds the `conversation` permission, the cmod plugin runs it in the background at each session's start, and a second run while one is running exits at once. The index forgets a conversation whose transcript is deleted.
217
+ - `index` reads every saved conversation into the index at `~/.local/share/cmod/history-8.sqlite`, which holds where each message is in its transcript, not a copy of it, and each conversation's turns, contexts, and tasks. While a mod other than cmod holds the `conversation` permission, the cmod plugin runs it in the background at each session's start, and a second run while one is running exits at once. The index forgets a conversation whose transcript is deleted.
209
218
 
210
219
  ## Run in an install script
211
220
 
@@ -227,7 +236,7 @@ cmod runs these itself. A person runs them to see a step's whole log, to finish
227
236
  cmod setup <plugin-root> [--events] [--consent <sha256>] [--yes]
228
237
  ```
229
238
 
230
- Refuses a machine the `os` and `cpu` keys of the mod's `package.json` leave out. Downloads the program its `cmod.program` names, or builds it from the checkout's `cli/` when `cmod link` loaded the mod, so a linked checkout whose version has no release yet still sets up, and links it into cmod's programs folder, which Claude Code runs it from by its name, and into `~/.local/bin` unless a file there is not cmod's. Runs the mod's install step, saves its uninstall step, grants the permissions its `cmod.permissions` lists, and records the mod as set up. A mod installed from a marketplace whose automatic updates the person has not turned on or off gets them turned on, through `"autoUpdate": true` on the marketplace's `extraKnownMarketplaces` entry in Claude Code's `settings.json`, and the consent question says so. The consent covers the scripts, the program, the keys, and the permissions together, and an update asks again only for what changed. An unchanged mod runs nothing. `--events` prints one event per line for a program to read, and the mod's install toast in Claude Code reads them.
239
+ Refuses a machine the `os` and `cpu` keys of the mod's `package.json` leave out. Downloads the program its `cmod.program` names, or builds it from the checkout's `cli/` when `cmod link` loaded the mod, so a linked checkout whose version has no release yet still sets up, and links it into cmod's programs folder, which Claude Code runs it from by its name, and into `~/.local/bin` unless a file there is not cmod's. Runs the mod's install step, saves its uninstall step, grants the permissions its `cmod.permissions` lists, and records the mod as set up. A mod installed from a marketplace whose automatic updates the person has not turned on or off gets them turned on, through `"autoUpdate": true` on the marketplace's `extraKnownMarketplaces` entry in Claude Code's `settings.json`, and the consent question says so. The consent covers the scripts, the program, the hotkeys, and the permissions together, and an update asks again only for what changed. An unchanged mod runs nothing. `--events` prints one event per line for a program to read, and the mod's install toast in Claude Code reads them.
231
240
 
232
241
  ### cmod teardown
233
242
 
@@ -243,7 +252,15 @@ Runs a removed mod's saved uninstall step, then deletes its install record, the
243
252
  cmod permission <mod> <name> [value] on|off
244
253
  ```
245
254
 
246
- Turns one permission of a set-up mod on or off, such as `cmod permission ci-watch model off` or `cmod permission ci-watch network api.github.com on`. A toggle in `/mods` runs it. It refuses a permission the mod's `package.json` `cmod.permissions` does not list, and the mod sees the change before its next call, with no reload ([permissions.md](permissions.md)).
255
+ Turns one permission of a set-up mod on or off, such as `cmod permission ci-watch model off` or `cmod permission ci-watch network api.github.com on`. A toggle in Settings runs it. It refuses a permission the mod's `package.json` `cmod.permissions` does not list, and the mod sees the change before its next call, with no reload ([permissions.md](permissions.md)).
256
+
257
+ ### cmod hotkey
258
+
259
+ ```text
260
+ cmod hotkey <mod> <hotkey> <combo>
261
+ ```
262
+
263
+ Binds one hotkey of a mod to a new combo in Claude Code's `keybindings.json`, such as `cmod hotkey notes toggle ctrl+shift+n`, and removes the combo it had. A combo in Settings, under the mod, Hotkeys, runs it, and so does the cmod plugin for its own two hotkeys at its first start. It refuses a combo that is not one, and a combo the person already bound to something else, with `<combo> already runs something else in ~/.claude/keybindings.json. Pick another combo.` The mod's next update keeps the combo ([mod.md](mod.md#hotkeys)).
247
264
 
248
265
  ### cmod settings
249
266
 
@@ -267,4 +284,4 @@ Links the `rules/` folder of each mod that is on and has the `prompt` permission
267
284
  cmod option <name> <key> <value>
268
285
  ```
269
286
 
270
- Sets one option of an installed plugin through `claude plugin configure`, the way `/config` sets it, such as `cmod option ci-watch branch main`. A list option takes its items separated by commas. It refuses a key the plugin's `plugin.json` `userConfig` does not declare. A field in `/mods` and the option questions a mod asks at its first start run it, and the mod reads the new value at its next start ([options.md](options.md)).
287
+ Sets one option of an installed plugin through `claude plugin configure`, the way `/config` sets it, such as `cmod option ci-watch branch main`. A list option takes its items separated by commas. It refuses a key the plugin's `plugin.json` `userConfig` does not declare. A field in Settings and the option questions a mod asks at its first start run it, and the mod reads the new value at its next start ([options.md](options.md)).
@@ -1,30 +1,33 @@
1
1
  # Call another mod
2
2
 
3
- A mod offers methods to other mods in `api`. Another mod lists it as a dependency and calls those methods through `mod.dependencies`, typed from the provider's own types file.
3
+ A mod offers methods to other mods by returning them from `setup`, as a VS Code extension returns its API from `activate`. Another mod lists it as a dependency and calls those methods through `mod.dependencies`, typed from the provider's own types file.
4
4
 
5
- ## Offer methods: api
5
+ ## Offer methods: return them from setup
6
6
 
7
- Each method of `api` gets the caller's input and the provider's own `mod` as its second argument, and returns its result or a promise of it. Inputs and results cross from one plugin to another, so keep them plain JSON data.
7
+ What `setup` returns is the mod's API: an object whose every value is a method. Each method gets the caller's input and returns its result or a promise of it. It reaches the provider's `mod` and state through `setup`'s scope. Inputs and results cross from one plugin to another, so keep them plain JSON data. A `setup` that returns anything else stops the mod with `<mod>: setup returns its API, an object of methods other mods call, or nothing.` A one-line `setup` returns what its call returns, so `setup: () => sessions.current.onPrompt(…)` offers that `Subscription`'s `remove` to other mods. Give such a `setup` braces.
8
8
 
9
9
  ```ts
10
10
  import { defineMod, fs } from '../node_modules/@cmodjs/core/mod.js'
11
11
 
12
12
  export const tracer = defineMod({
13
13
  name: 'tracer',
14
- api: {
15
- signatures: async ({ path }: { path: string }) =>
16
- (await fs.read(path)).split('\n').flatMap((text, index) => {
17
- const name = /^export function (\w+)/.exec(text)?.[1]
18
- return name === undefined ? [] : [{ name, line: index + 1 }]
19
- }),
14
+ setup() {
15
+ return {
16
+ signatures: async ({ path }: { path: string }) =>
17
+ (await fs.read(path)).split('\n').flatMap((text, index) => {
18
+ const name = /^export function (\w+)/.exec(text)?.[1]
19
+ return name === undefined ? [] : [{ name, line: index + 1 }]
20
+ }),
21
+ }
20
22
  },
21
- setup() {},
22
23
  })
23
24
  ```
24
25
 
26
+ A call that reaches the mod while it is still starting waits for `setup` to return.
27
+
25
28
  ## Type the methods: CmodDependencies
26
29
 
27
- The provider types its `api` in `types/index.d.ts`, and its `.claude-plugin/plugin.json` names that file as `"types": "./types/index.d.ts"`. The file adds the mod to `CmodDependencies`, which the Claude Mod Manager (cmod) plugin declares on the `claude-code` module:
30
+ The provider types the methods its `setup` returns in `types/index.d.ts`, and its `.claude-plugin/plugin.json` names that file as `"types": "./types/index.d.ts"`. The file adds the mod to `CmodDependencies`, which the Claude Mod Manager (cmod) plugin declares on the `claude-code` module:
28
31
 
29
32
  ```ts
30
33
  export type TracerSignature = { name: string; line: number }
@@ -41,7 +44,7 @@ declare module 'claude-code' {
41
44
  ```
42
45
 
43
46
  - Each method takes one input and returns a promise.
44
- - Once the name is in `CmodDependencies`, `tsc` checks the provider's own `api` against it, so a method that returns another shape fails `cmod check`.
47
+ - Once the name is in `CmodDependencies`, `tsc` checks what the provider's own `setup` returns against it, so a method that returns another shape fails `cmod check`.
45
48
  - A types file augments only `'claude-code'`. Claude Code refuses a types file that augments any other module.
46
49
 
47
50
  ## Call the methods: mod.dependencies
@@ -49,16 +52,20 @@ declare module 'claude-code' {
49
52
  A mod that calls tracer lists it in its own `.claude-plugin/plugin.json`, and Claude Code installs it with the mod:
50
53
 
51
54
  ```json
52
- "dependencies": [{ "name": "cmod", "version": "^1.0.0" }, "tracer"]
55
+ "dependencies": [{ "name": "cmod", "version": ">=4.0.0" }, "tracer"]
53
56
  ```
54
57
 
55
58
  A provider that changes or removes a method breaks every mod that calls it, and with automatic updates the provider can update first. Name the versions you tested with a range, and Claude Code turns your mod off with a clear error instead of letting its calls fail one by one:
56
59
 
57
60
  ```json
58
- "dependencies": [{ "name": "cmod", "version": "^1.0.0" }, { "name": "tracer", "version": "^1.0" }]
61
+ "dependencies": [{ "name": "cmod", "version": ">=4.0.0" }, { "name": "tracer", "version": "^1.0" }]
59
62
  ```
60
63
 
61
- Claude Code checks the range against the provider's `version` when it loads your mod. Where a marketplace lists the provider from its git repository, as every mod's marketplace lists cmod, Claude Code also installs it at the highest `<name>--v<version>` tag inside the range, which `cmod publish` pushes for every release ([Claude Code's plugin dependencies](https://code.claude.com/docs/en/plugins/dependencies)). cmod is a provider too: a mod built on `@cmodjs/core` 1.0 calls methods only the 1.0 cmod plugin has, so `cmod new` writes the `^1.0.0` range.
64
+ Claude Code checks the range against the provider's `version` when it loads your mod. Where a marketplace lists the provider from its git repository, as every mod's marketplace lists cmod, Claude Code also installs it at the highest `<name>--v<version>` tag inside the range, which `cmod publish` pushes for every release ([Claude Code's plugin dependencies](https://code.claude.com/docs/en/plugins/dependencies)).
65
+
66
+ cmod is a provider too, with one difference: every newer cmod runs every mod built on an older `@cmodjs/core`. So a mod names only the oldest cmod it runs on, the version of the `@cmodjs/core` it bundles, as `>=4.0.0`. `cmod check` writes that range whenever the mod's `@cmodjs/core` changes, and `cmod publish` refuses a release whose range differs. A caret range such as `^4.0.0` stops Claude Code from updating cmod past 4.x for every mod on the computer, so leave it to `cmod check`.
67
+
68
+ Each mod's marketplace lists its own copy of cmod, and Claude Code loads only one of them. When a mod needs a newer cmod than a copy Claude Code installed, or two copies differ, the cmod plugin updates every copy and reloads the mods, so a mod never waits for the person to update cmod.
62
69
 
63
70
  Claude Code then lays tracer's types file beside the mod's own types, so the call is typed:
64
71
 
@@ -88,14 +95,14 @@ export const outline = defineMod({
88
95
  A call that cannot answer rejects with one of these reasons:
89
96
 
90
97
  ```text
91
- tracer is not installed. Add it with /plugin.
98
+ tracer is not installed. Install it in Plugins.
92
99
  tracer is not set up. Its toast says why.
93
100
  tracer is installing. Try again when it's ready.
94
101
  tracer has no method signatures.
95
102
  tracer: <the message of the error the method threw>
96
103
  ```
97
104
 
98
- - `tracer is not installed` covers a provider whose code did not load.
105
+ - `tracer is not installed` covers a provider whose code did not load. The caller's error toast shows a `Plugins` button, which opens Claude Code's `/plugin`.
99
106
  - `tracer is not set up` covers a provider the person declined to install and one whose install failed. Its own toast shows why, with `Retry` or `Install`.
100
107
  - `tracer is installing` covers a provider that is still running its install step. A provider that is only starting makes the call wait for it, up to 9 seconds.
101
108
  - When tracer is disabled, Claude Code unloads the mod that lists it, with its own error: `Dependency "tracer" is disabled — enable it or remove the dependency`.
@@ -105,13 +112,13 @@ tracer: <the message of the error the method threw>
105
112
  notInstalled(name: string): string
106
113
  ```
107
114
 
108
- `notInstalled`, exported from `mod.js`, returns the first reason for a mod name: `<name> is not installed. Add it with /plugin.` A test uses it to expect that message.
115
+ `notInstalled`, exported from `mod.js`, returns the first reason for a mod name: `<name> is not installed. Install it in Plugins.` A test uses it to expect that message.
109
116
 
110
117
  ## Test the calls
111
118
 
112
- `testMod(outline, { dependencies: { tracer: { signatures: async () => [] } } })` answers tracer's methods with the functions given. A call to a mod the test does not fake fails with `tracer is not installed. Add it with /plugin.`, as in Claude Code. [testing.md](testing.md) covers it.
119
+ `testMod(outline, { dependencies: { tracer: { signatures: async () => [] } } })` answers tracer's methods with the functions given. A call to a mod the test does not fake fails with `tracer is not installed. Install it in Plugins.`, as in Claude Code. [testing.md](testing.md) covers it.
113
120
 
114
- A test of the provider's own `api` fires `cmod.call`, as a caller's `mod.dependencies` does. The answer is `{ value }` with what the method returned, or `{ deny }` with the reason it failed:
121
+ A test of the provider's own methods fires `cmod.call`, as a caller's `mod.dependencies` does. The answer is `{ value }` with what the method returned, or `{ deny }` with the reason it failed:
115
122
 
116
123
  ```ts
117
124
  const tested = testMod(tracer, { files: { '/work/a.ts': 'export function greet() {}\n' } })
package/docs/index.md CHANGED
@@ -10,25 +10,27 @@ A mod is a Claude Code plugin built with `@cmodjs/core`. Claude Mod Manager (cmo
10
10
 
11
11
  ```text
12
12
  my-mod/
13
- ├── .claude-plugin/plugin.json name, version, and "dependencies": [{ "name": "cmod", "version": "^1.0.0" }]
13
+ ├── .claude-plugin/plugin.json name, version, and "dependencies": [{ "name": "cmod", "version": ">=4.0.0" }]
14
14
  ├── .claude/CLAUDE.md facts for coding agents, starting with these docs
15
15
  ├── .gitignore node_modules/ and .claude-plugin/types/
16
16
  ├── .oxlintrc.json leaves .claude-plugin/types/ out of the lint
17
17
  ├── hooks/hooks.json names hooks/register.ts as the hooks module
18
18
  ├── hooks/register.ts registerMod(addHook, myMod, options), and nothing else
19
- ├── src/mod.tsx defineMod: the state, the setup, the listeners, the panes
19
+ ├── src/mod.tsx defineMod: the state, the hotkeys, the setup, the listeners
20
20
  ├── src/state.ts the starting state
21
- ├── src/panes/prompts.tsx one definePane per file
22
- ├── src/components/prompt-count.tsx what the panes and renders draw with
21
+ ├── src/pages/prompts.tsx one Sidebar page per file
22
+ ├── src/widgets/prompts.tsx one widget per file
23
+ ├── src/components/prompt-count.tsx what the page and the widget draw with
23
24
  ├── skills/my-mod/SKILL.md a Skill the mod ships
25
+ ├── rules/prompt-count.md a rule Claude reads
24
26
  ├── tests/mod.test.ts testMod tests, run with bun test
25
- ├── package.json depends on @cmodjs/core
27
+ ├── package.json depends on @cmodjs/core, and lists the permissions and hotkeys
26
28
  └── tsconfig.json extends .claude-plugin/types/tsconfig.json, with the JSX options
27
29
  ```
28
30
 
29
31
  `cmod new --project` writes the same files, less `.claude/CLAUDE.md`, into `.claude/skills/<name>/` of the repository.
30
32
 
31
- The template is a working example to replace. It counts the prompts of the session in `state.session.prompts`, shows the count in a pane titled with the mod's name and above the prompt, and tests both. Replace the counter, the pane, the component, the Skill, and the tests with the mod's own.
33
+ The template is a working example to replace. It counts the prompts of the session in `state.session.prompts`, shows the count below the prompt and in a Sidebar page that the `alt+shift+p` hotkey shows or hides, and tests all three. Replace the counter, the page, the widget, the component, the Skill, the rule, and the tests with the mod's own.
32
34
 
33
35
  A mod imports `@cmodjs/core` by path, such as `../node_modules/@cmodjs/core/mod.js`, because Claude Code loads a plugin's own files only. Types such as `RenderElement` and `On` come from the `claude-code` module.
34
36
 
@@ -64,14 +66,16 @@ A program the mod needs lives as source in `cli/`. `cmod publish` builds it and
64
66
  | Which hosts, programs, and files may my mod reach, and how does the person grant them? | [permissions.md](permissions.md) |
65
67
  | How does my mod find files and keep its own keys on each one, such as on a Skill? | [mod.md](mod.md#metadata-a-file-s-own-keys) |
66
68
  | How does the person finish a setup step only they can do, such as signing in? | [install-steps.md](install-steps.md#steps-in-claude-code) |
67
- | How do I add my mod's own page to `/mods`, or change Claude Code's settings? | [mod.md](mod.md#mod-settings), [mod.md](mod.md#settings-claude-code-s-settings-files) |
69
+ | How do I add my mod's own page to Settings, or change Claude Code's settings? | [ui.md](ui.md#settings-pages), [mod.md](mod.md#settings-claude-code-s-settings-files) |
70
+ | How do I give my mod a hotkey the person can change? | [mod.md](mod.md#hotkeys) |
68
71
  | How does my mod read and follow this conversation, its tasks, or earlier ones, search them, or start a task? | [session.md](session.md) |
69
72
  | How does my mod hear a prompt, a tool call, a permission dialog, or a model switch, and change or refuse it? | [session.md](session.md#listen-to-a-record), [session.md](session.md#the-session-s-own-events) |
70
73
  | How does my mod hear, change, or run any slash command, a built-in such as `/compact` included? | [mod.md](mod.md#commands-slash-commands) |
71
- | How do I draw a pane, change a row Claude Code draws, or ask the person something? | [ui.md](ui.md) |
72
- | How do I add a slash command, a tool, a permission rule, a check, a prompt, a schedule, a status line, or a background program? | [jobs.md](jobs.md) |
74
+ | How do I draw a page in the Sidebar, a pane, change how Claude Code draws a row, or ask the person something? | [ui.md](ui.md) |
75
+ | How do I show a status, such as the branch or a CI result, by the prompt, while Claude works, after each turn, or on the Dashboard? | [ui.md](ui.md#widgets) |
76
+ | How do I add a slash command, a tool, a permission rule, a check, a prompt, a schedule, or a background program? | [jobs.md](jobs.md) |
73
77
  | How does one mod call another? | [dependencies.md](dependencies.md) |
74
- | How does a mod change the machine, bind a key, ship a program, or clean up after itself? | [install-steps.md](install-steps.md) |
78
+ | How does a mod change the machine, ship a program, or clean up after itself? | [install-steps.md](install-steps.md) |
75
79
  | How do I test a mod without Claude Code? | [testing.md](testing.md) |
76
80
  | Which `cmod` command links, checks, tries in a clean Claude Code, or publishes a mod? | [commands.md](commands.md) |
77
81
 
@@ -81,32 +85,32 @@ A mod imports from the files below, and every other file in this package is inte
81
85
 
82
86
  | Import | Exports | Docs |
83
87
  | --- | --- | --- |
84
- | `mod.js` | `defineMod`, `ModDefinition`, `Mod`, `Disposable`, `Permissions`, `Allowed`, `JobContext`, `Job`, `PaneHandle`, `longestMs`, `messageOf` | [mod.md](mod.md), [jobs.md](jobs.md) |
85
- | `mod.js` | `settings`, `SettingsFile`, `SettingsFileChange`, `SettingsFileChangeListener`, `config`, `ConfigRow`, `commands`, `CommandInfo`, `CommandRun`, `CommandRunAnswer`, `CommandRunResult`, `model`, `fs`, `Entry`, `Stat`, `FileMatch`, `FileChange`, `metadata`, `Metadata`, `process`, `http`, `Imports` | [mod.md](mod.md) |
88
+ | `mod.js` | `defineMod`, `ModDefinition`, `Mod`, `Subscription`, `Timer`, `Reservation`, `Permissions`, `Allowed`, `JobContext`, `Job`, `SlashCommand`, `Tool`, `Step`, `longestMs`, `messageOf` | [mod.md](mod.md), [jobs.md](jobs.md), [install-steps.md](install-steps.md#steps-in-claude-code) |
89
+ | `mod.js` | `Hotkey`, `HotkeyInit`, `Combo`, `Key` | [mod.md](mod.md#hotkeys) |
90
+ | `mod.js` | `settings`, `SettingsFile`, `SettingsFileChange`, `SettingsFileChangeListener`, `config`, `ConfigRow`, `commands`, `CommandInfo`, `CommandRun`, `CommandRunAnswer`, `CommandRunResult`, `model`, `fs`, `Entry`, `Stat`, `FileMatch`, `FileChange`, `metadata`, `Metadata`, `process`, `http`, `clipboard`, `Imports` | [mod.md](mod.md) |
86
91
  | `mod.js` | `sessions`, `Sessions`, `Session`, `CurrentSession`, `TaskOwner`, `RecordPart`, `OnMethods`, `BeforeMethods`, `Listener`, `ListenerTime`, `ReadOptions`, `AnswerOptions`, `Refusal`, `SearchOptions`, `SessionScope`, `SessionSend`, `TaskStartArgs`, `ToolCallArgs`, `Usage`, `Surface`, `Matcher` | [session.md](session.md) |
87
92
  | `mod.js` | `Placement`, `ProjectSetup`, `SessionStartAsked`, `SessionStarted`, `SessionEnded`, `ModelChange`, `CwdChange`, `DirectoryAdd`, `Worktree`, `Notification`, `Elicitation`, `ElicitationResponse`, `McpServer`, `PermissionUpdate`, `PermissionCheck`, `PermissionRequest`, `PermissionDenial`, `BatchedCall`, `ToolBatch`, `Todo`, `ReplyDisplay` | [session.md](session.md#the-session-s-own-events) |
88
93
  | `mod.js` | `Message`, `MessageType`, `Prompt`, `Note`, `Reply`, `ToolCall`, `ToolResult`, `Skill`, `InstructionFile`, `Notice`, `Sender`, `SenderType`, `Person`, `Mod`, `Schedule`, `Channel`, `Sdk`, `Engine` | [session.md](session.md#message) |
89
94
  | `mod.js` | `Context`, `Turn`, `Goal`, `Task` | [session.md](session.md#turn-context-goal-and-task) |
90
95
  | `mod.js` | `Toast`, `ToastId`, `ToastType`, `ToastOptions` | [ui.md](ui.md#toasts) |
91
- | `mod.js` | `Place`, `PlaceProps`, `ToolGroup`, `ToolProgress`, `CommandOutput`, `AskUserQuestion`, `Spinner`, `TurnDuration`, `SessionMode`, `PromptHint`, `AbovePrompt` | [ui.md](ui.md#places-change-a-row-claude-code-draws) |
96
+ | `mod.js` | `PageInit`, `Page`, `PageProps`, `PaneInit`, `Pane`, `PaneProps`, `OpenOptions`, `ScrollTarget`, `ScrollOptions` | [ui.md](ui.md#the-sidebar-and-its-pages) |
97
+ | `mod.js` | `WidgetInit`, `Widget`, `WidgetContext`, `Area`, `AbovePrompt`, `BelowPrompt`, `Working`, `TurnEnd`, `Dashboard` | [ui.md](ui.md#widgets) |
98
+ | `mod.js` | `StyleInit`, `Style`, `StyleProps`, `Kind`, `Row`, `Block`, `ToolGroup`, `ToolProgress`, `CommandOutput`, `AskUserQuestion` | [ui.md](ui.md#styles-change-how-claude-code-draws) |
92
99
  | `mod.js` | `notInstalled` | [dependencies.md](dependencies.md) |
93
- | `installer.js` | `defineStep`, `Step` | [install-steps.md](install-steps.md#steps-in-claude-code) |
94
100
  | `register.js` | `registerMod`, `registerPermissionCheck`, `registeredMod` | [mod.md](mod.md) |
95
101
  | `options.js` | `option`, `Option`, `Options`, `OptionValues`, `OptionValue`, `OptionType`, `UserConfigField`, `userConfigOf` | [options.md](options.md) |
96
102
  | `shell.js` | `parseShell`, `ParsedShell`, `ShellCommand` | [mod.md](mod.md#shell-js-and-path-js-read-a-command-line) |
97
103
  | `path.js` | `resolve`, `join`, `dirname`, `basename`, `extname`, `relative`, `isAbsolute`, `normalize` | [mod.md](mod.md#shell-js-and-path-js-read-a-command-line) |
98
104
  | `testing.js` | `testMod`, `TestOptions`, `TestedMod`, `Raise`, `Fakes`, `Shown`, `ShownToast`, `TestCall`, `FakeSession`, `MessageDraft`, `RoutedEvent` | [testing.md](testing.md) |
99
- | `ui/define-pane.js` | `definePane`, `Pane` | [ui.md](ui.md) |
100
105
  | `ui/elements.js` | `Box`, `Text`, `Button`, `Link`, `Code`, `Markdown`, `Input`, `Select`, `Image`, `drawWith` | [ui.md](ui.md) |
101
106
  | `ui/components.js` | `Tabs`, `Tab`, `Split`, `Panel`, `Toggle`, `Tooltip`, `Help`, `Dialog`, `Pagination`, `Progress` | [ui.md](ui.md) |
102
- | `ui/markdown.js` | `Heading`, `Paragraph`, `CodeBlock`, `BlockQuote`, `List`, `Table`, `ThematicBreak`, `HtmlBlock`, `MarkdownPlace`, `markdownBlocks`, `BlockProps`, `MarkdownReader`, `MarkdownPiece` | [ui.md](ui.md#markdown-places-change-the-blocks-of-a-reply) |
107
+ | `ui/markdown.js` | `Heading`, `Paragraph`, `CodeBlock`, `BlockQuote`, `List`, `Table`, `ThematicBreak`, `HtmlBlock`, `Block`, `markdownBlocks`, `BlockProps`, `MarkdownReader`, `MarkdownPiece` | [ui.md](ui.md#markdown-blocks) |
103
108
  | `jobs/slash-command.js` | `slashCommand`, `CommandReply` | [jobs.md](jobs.md) |
104
109
  | `jobs/tool.js` | `tool` | [jobs.md](jobs.md) |
105
110
  | `jobs/permissions.js` | `permissions`, `Rule`, `PermissionRules`, `Target`, `CommandCall`, `FileCall`, `FetchCall`, `Call` | [jobs.md](jobs.md) |
106
111
  | `jobs/check.js` | `check` | [jobs.md](jobs.md) |
107
112
  | `jobs/prompt.js` | `prompt` | [jobs.md](jobs.md) |
108
113
  | `jobs/schedule.js` | `schedule`, `ScheduledPrompt` | [jobs.md](jobs.md) |
109
- | `jobs/status-line.js` | `statusLine` | [jobs.md](jobs.md) |
110
114
  | `jobs/program.js` | `program`, `Program` | [jobs.md](jobs.md) |
111
115
  | `jobs/context.js` | `afterCall`, `inFolder`, `withinDeadline`, `workspaceReader` | [jobs.md](jobs.md#the-building-blocks) |
112
116
  | `jobs/permissions/decide-permission.js` | `decidePermission`, `stricterVerdict`, `Rule`, `PermissionRules`, `Decision`, `Verdict` | [jobs.md](jobs.md) |
@@ -10,7 +10,7 @@ A mod that changes the machine, such as by adding a shell alias or downloading a
10
10
  "install": "./setup/install.sh",
11
11
  "uninstall": "./setup/uninstall.sh",
12
12
  "program": "hello",
13
- "keys": { "shift+tab": "/mode" }
13
+ "hotkeys": { "cycle": { "title": "Cycle the mode", "default": "shift+tab" } }
14
14
  }
15
15
  }
16
16
  ```
@@ -20,7 +20,7 @@ Each key is optional.
20
20
  - `install` is one shell command that sets the mod up.
21
21
  - `uninstall` is one shell command that undoes it.
22
22
  - `program` is the name of a program cmod downloads from the mod's GitHub release, and Claude Code then runs by its name. [Ship a program](#ship-a-program) explains it.
23
- - `keys` binds keys in Claude Code to the mod's slash commands. [Key bindings](#key-bindings) explains it.
23
+ - `hotkeys` is the hotkeys `defineMod` declares, which `cmod check` writes here so cmod binds them at install without loading the mod. [Hotkeys](#hotkeys) explains it.
24
24
 
25
25
  cmod refuses a `cmod` key that breaks these rules, with the fix in the message:
26
26
 
@@ -29,16 +29,16 @@ cmod refuses a `cmod` key that breaks these rules, with the fix in the message:
29
29
  - Each names at least one script file inside the mod, and no script at the mod's root. cmod asks consent for the whole folder of each script, so put the scripts in a folder, such as `setup/`.
30
30
  - A symbolic link in a script folder points at a file.
31
31
  - `program` is a command name: letters, digits, `.`, `_`, and `-`, starting with a letter or digit.
32
- - `keys` binds at least one key, and each binds one of the mod's commands as `/<name>`.
32
+ - Each hotkey has a title and a default combo, as `defineMod` takes them ([mod.md](mod.md#hotkeys)).
33
33
  - `.claude-plugin/plugin.json` has a `version`.
34
34
 
35
35
  ## Consent
36
36
 
37
- Before a step runs for the first time, cmod shows the person the commands, the keys, and the permissions, and asks to run them. cmod hashes the commands, the keys, the permissions, and every file in each script folder, and remembers the hash the person approved with each permission. A change to any of those asks again, and an added permission is the only one it asks about ([permissions.md](permissions.md)).
37
+ Before a step runs for the first time, cmod shows the person the commands, the hotkeys, and the permissions, and asks to run them. cmod hashes the commands, the hotkeys, the permissions, and every file in each script folder, and remembers the hash the person approved with each permission. A change to any of those asks again, and an added permission is the only one it asks about ([permissions.md](permissions.md)).
38
38
 
39
- - In Claude Code, the mod opens the installer pane, `Install <mod>`, focused. It lists `<mod> wants to:`, each permission as one sentence, then `and change your computer:` with `Run <install> now, and <uninstall> when you remove it` and `Bind shift+tab to /mode`, naming only what the mod has. `Accept` (a) runs the setup, and a loading toast above the prompt shows the install with a progress bar. `Not now` (n) and Escape decline.
40
- - In a session with no screen, such as `claude -p`, the mod waits and logs `<mod> waits for consent to run <install>. Run cmod install <mod> in a terminal.` That person is already in a terminal.
41
- - In a terminal, `cmod install`, `cmod link`, `cmod try`, and `cmod update` print the commands, the program, the keys, and the permissions, and ask `Set up <mod>? [y/N]`. `--yes` approves without asking.
39
+ - In Claude Code, the mod opens the installer pane, `Install <mod>`, focused. It lists `<mod> wants to:`, each permission as one sentence, then `and change your computer:` with `Run <install> now, and <uninstall> when you remove it` and `Bind shift+tab to Cycle the mode`, naming only what the mod has. `Accept` (a) runs the setup, and a loading toast above the prompt shows the install with a progress bar. `Not now` (n) and Escape decline.
40
+ - In a session with no screen, such as `claude -p`, the mod waits and logs `<mod> waits for consent to run <install>. Run cmod install <mod> in a terminal.` That person is already in a terminal, and no screen exists to draw a button on.
41
+ - In a terminal, `cmod install`, `cmod link`, `cmod try`, and `cmod update` print the commands, the program, the hotkeys, and the permissions, and ask `Set up <mod>? [y/N]`. `--yes` approves without asking.
42
42
  - `Not now` leaves the mod off with the toast `Not installed` and an `Install` button that asks again.
43
43
 
44
44
  The mod starts only once its install step has run ([mod.md](mod.md)).
@@ -47,50 +47,57 @@ The mod starts only once its install step has run ([mod.md](mod.md)).
47
47
 
48
48
  Claude Code updates a plugin by itself only when its marketplace has automatic updates on, and they are off for every marketplace that is not Anthropic's. So the setup of a mod installed from a marketplace turns them on, through `"autoUpdate": true` on that marketplace's `extraKnownMarketplaces` entry in `settings.json`. The consent lists it as `Update <mod> automatically from the <marketplace> marketplace`, and a mod with nothing to consent to logs it. The setup never changes a choice the person made, in `settings.json` or with the toggle under `/plugin` Marketplaces, and leaves a linked mod alone.
49
49
 
50
- Claude Code downloads an update in the background and loads it at the next launch or `/reload-plugins`. An update whose scripts, keys, or permissions changed waits for consent again, so an update never runs a new install step unasked.
50
+ Claude Code downloads an update in the background and loads it at the next launch or `/reload-plugins`. An update whose scripts, hotkeys, or permissions changed waits for consent again, so an update never runs a new install step unasked.
51
51
 
52
- In Claude Code, the mod sets itself up through the `cmod` in cmod's programs folder, or else the one PATH finds, and only through one that knows every step it has. A mod with `keys` waits for cmod 0.1.12 or later, because an older cmod skips the keys, and a mod with `permissions` waits for cmod 0.2.0 or later, because an older cmod grants none of them. While PATH finds an older one, the line above the prompt says `PATH finds cmod <version>, and <mod> needs cmod 0.1.12 or later. Run npm i -g @cmodjs/cli, or put ~/.local/bin ahead of the old cmod on PATH.` The mod sets up as soon as a new enough cmod answers, such as the one the cmod plugin installs into `~/.local/bin` at its first start.
52
+ In Claude Code, the mod sets itself up through the `cmod` program the cmod plugin installed, never one elsewhere on PATH, and only once that program is at least the version of the `@cmodjs/core` the mod bundles. Until then its toast says `Waiting for cmod`. After a minute it says `<mod> needs cmod <version> or later, and this computer has cmod <version>, so cmod is updating itself.` and runs `cmod update cmod` once. The mod sets up as soon as a new enough cmod is installed. When the update leaves cmod older, the next wait shows `<mod> needs cmod <version> or later, and the newest cmod is <version>.` with a Retry button instead of updating again.
53
53
 
54
54
  ## Steps in Claude Code
55
55
 
56
56
  Some setup only the person can do, such as signing in to a service. A mod lists those steps in `installer`, and the installer pane shows each one after the permissions and the options, once the mod has started:
57
57
 
58
- ```ts
59
- import { process } from '../node_modules/@cmodjs/core/mod.js'
60
- import { defineStep } from '../node_modules/@cmodjs/core/installer.js'
58
+ ```tsx
59
+ import { defineMod, type Step } from '../node_modules/@cmodjs/core/mod.js'
60
+ import { Button, Text } from '../node_modules/@cmodjs/core/ui/elements.js'
61
+ import { isSignedIn, openSignIn } from './github.js'
61
62
 
62
- const signIn = defineStep({
63
+ const signIn: Step = {
63
64
  id: 'sign-in',
64
65
  title: 'Sign in to GitHub',
65
- render: () => <Text>Run gh auth login in a terminal, then press Next.</Text>,
66
- isDone: async () => (await process.run(['gh', 'auth', 'status'])).exitCode === 0,
67
- })
66
+ render: () => <Button label="Open the sign-in page" onPress={() => void openSignIn()} />,
67
+ isDone: () => isSignedIn(),
68
+ }
68
69
 
69
70
  export const ciWatch = defineMod({ name: 'ci-watch', installer: [signIn], setup(mod) { … } })
70
71
  ```
71
72
 
72
73
  ```ts
73
- defineStep<State>(step: { id: string; title: string; render(mod, props): RenderElement; isDone(mod): boolean | Promise<boolean> }): Step<State>
74
+ type Step<State> = {
75
+ readonly id: string
76
+ readonly title: string
77
+ render(mod: Mod<State>, props: PaneProps): RenderElement
78
+ isDone(mod: Mod<State>): boolean | Promise<boolean>
79
+ }
74
80
  ```
75
81
 
76
- - `id` is 1 to 64 letters, digits, `_`, or `-`. `title` is the line above the step, with `(1 of 2)`. `defineStep` throws for either one missing.
77
- - `render` draws the step's body, the way a pane's `render` does ([ui.md](ui.md)).
82
+ - `id` is 1 to 64 letters, digits, `_`, or `-`, unique among the steps. `title` is the line above the step, with `(1 of 2)`. `defineMod` throws for an id that is not one, a blank title, or two steps with one id.
83
+ - `render` draws the step's body, the way a pane's `render` does ([ui.md](ui.md)). Give the step a `Button` that does the work or opens the place to do it, never a command to type.
78
84
  - `isDone` says whether the step is finished. A step that is done is skipped, so the pane opens only for what is left. `Next` checks `isDone`, and shows `Finish <title> first.` while it is false. The last step's button says `Finish`.
79
- - An `isDone` that throws or rejects counts as not done. At start cmod logs `<mod>: the <title> step's isDone failed: <error>`, and `Next` shows `The <title> step's isDone failed: <error>`.
80
- - `Not now` closes the pane and logs `<mod> needs one more step, starting with <title>. Run /mods <mod> to finish.` The mod keeps running. `/mods <mod>` shows `Needs setup: <title>` with a `Finish setup` button, which opens the pane on the first step left.
85
+ - An `isDone` that throws or rejects counts as not done, and `Next` shows `The <title> step's isDone failed: <error>`.
86
+ - `Not now` closes the pane, and the mod keeps running. A toast stays above the prompt, `Needs one more step` with `Starting with <title>.` under it, and its `Finish` button opens the pane on the first step left. Settings shows `Needs setup: <title>` with a `Finish setup` button too.
81
87
 
82
- ## Key bindings
88
+ ## Hotkeys
83
89
 
84
- A plugin cannot bind a key in Claude Code: Claude Code reads key bindings only from the person's `keybindings.json`. So cmod writes the mod's `keys` there, with consent, as the setup's last step:
90
+ A plugin cannot bind a key in Claude Code: Claude Code reads key bindings only from the person's `keybindings.json`. So cmod writes each hotkey there, with consent, as the setup's last step. A hotkey runs a hidden command of the mod, `<mod>--<hotkey>`, which the mod hears as `onPress` ([mod.md](mod.md#hotkeys)):
85
91
 
86
92
  ```json
87
- { "bindings": [{ "context": "Chat", "bindings": { "shift+tab": "command:mode" } }] }
93
+ { "bindings": [{ "context": "Chat", "bindings": { "shift+tab": "command:modes--cycle" } }] }
88
94
  ```
89
95
 
90
- - cmod writes each key into the `Chat` bindings of `~/.claude/keybindings.json`, or of `$CLAUDE_CONFIG_DIR/keybindings.json` when that variable is set. When the file is a symbolic link, cmod writes the file it points to. Claude Code reads the change at once, with no restart.
91
- - A key the person already bound to something else stays theirs. cmod logs `~/.claude/keybindings.json binds <key> to "<action>", so it stays. To use /<name> on <key>, put "<key>": "command:<name>" in its Chat bindings.`
92
- - An upgrade that drops a key removes its binding. Removing the mod removes each binding it added that still runs its command, and a `Chat` block that leaves empty, and leaves a binding the person changed.
93
- - A key's command takes `immediate: true` and a `reply` that returns `undefined`, so it works while Claude works and adds no row to the conversation ([jobs.md](jobs.md#slashcommand)). A key bound to `shift+tab` replaces Claude Code's own mode switch, and Claude Code then drops its `shift+tab` hint.
96
+ - cmod writes each combo into the `Chat` bindings of `~/.claude/keybindings.json`, or of `$CLAUDE_CONFIG_DIR/keybindings.json` when that variable is set. When the file is a symbolic link, cmod writes the file it points to. Claude Code reads the change at once, with no restart.
97
+ - A combo the person already bound to something else stays theirs, and the hotkey has no combo. The person picks one in Settings, under the mod, Hotkeys.
98
+ - The combo the person picks in Settings runs `cmod hotkey <mod> <hotkey> <combo>`. An update keeps the combo each hotkey has in `keybindings.json`, whether Settings or the person's own edit bound it ([commands.md](commands.md#cmod-hotkey)).
99
+ - An upgrade that drops a hotkey removes its binding. Removing the mod removes every binding that runs one of its hotkeys, and a `Chat` block that leaves empty. A binding the person moved to another action stays.
100
+ - A hotkey bound to `shift+tab` replaces Claude Code's own mode switch, and Claude Code then drops its `shift+tab` hint.
94
101
 
95
102
  ## How a step runs
96
103
 
@@ -118,7 +125,8 @@ The install step runs again on every new version of the mod and whenever its scr
118
125
  - When the first install of a mod fails or is stopped, cmod runs the uninstall step to undo it. So the uninstall step must work on a partial install.
119
126
  - When the install step of an upgrade fails or is stopped, cmod keeps the version it set up before.
120
127
  - The failure shows at once as an error toast with `Retry`, `Report`, and `Hide` ([ui.md](ui.md#toasts)).
121
- - When waiting can fix the failure, cmod also retries in the background after 2, 4, 8, 16, 32, 64, and 128 seconds, then every 128 seconds, and the toast says `Trying again in <n> s.` A try that works replaces the toast with `Installed`. Waiting can fix two failures: a linked checkout whose build or install step failed, which cmod tries again once the checkout changed, and a release download that got 404 or no answer, as when a release was published seconds ago. Any other failure waits for `Retry`.
128
+ - When waiting can fix the failure, cmod also retries in the background after 2, 4, 8, 16, 32, 64, and 128 seconds, then every 128 seconds, and the toast says `Trying again in <n> s.` A try that works replaces the toast with `Installed`. Waiting can fix two failures: a linked checkout whose build or install step failed, and a release download that got 404 or no answer, as when a release was published seconds ago. Any other failure waits for `Retry`. A linked checkout's failed build runs again only once a file in its program's folder changes, so a retry with no change shows the same error without building.
129
+ - A new version of a linked checkout whose install steps did not change keeps running on its last install while cmod builds the new version in the background, and other sessions wait for that one build. A failed build shows the build's own error.
122
130
  - Before each try, cmod reads the mod's install record, so a mod another session installed starts without running its install again.
123
131
 
124
132
  ## The uninstall step