@cmodjs/core 0.3.0 → 1.0.0

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 (271) hide show
  1. package/docs/commands.md +50 -10
  2. package/docs/dependencies.md +12 -12
  3. package/docs/hooks.md +39 -27
  4. package/docs/index.md +29 -25
  5. package/docs/install-steps.md +9 -7
  6. package/docs/jobs.md +59 -111
  7. package/docs/mod.md +200 -174
  8. package/docs/options.md +2 -2
  9. package/docs/permissions.md +27 -16
  10. package/docs/session.md +248 -0
  11. package/docs/state.md +11 -7
  12. package/docs/testing.md +65 -22
  13. package/docs/ui.md +108 -76
  14. package/jobs/check.d.ts.map +1 -1
  15. package/jobs/check.js +5 -2
  16. package/jobs/context.d.ts +7 -8
  17. package/jobs/context.d.ts.map +1 -1
  18. package/jobs/context.js +23 -73
  19. package/jobs/permissions/decide-permission.d.ts +5 -3
  20. package/jobs/permissions/decide-permission.d.ts.map +1 -1
  21. package/jobs/permissions/decide-permission.js +6 -4
  22. package/jobs/permissions/find-project-scope.d.ts +1 -1
  23. package/jobs/permissions/find-project-scope.d.ts.map +1 -1
  24. package/jobs/permissions/find-project-scope.js +8 -11
  25. package/jobs/permissions/match-target.d.ts +2 -2
  26. package/jobs/permissions/match-target.d.ts.map +1 -1
  27. package/jobs/permissions.d.ts +1 -1
  28. package/jobs/permissions.d.ts.map +1 -1
  29. package/jobs/permissions.js +6 -4
  30. package/jobs/program.d.ts.map +1 -1
  31. package/jobs/program.js +8 -3
  32. package/jobs/prompt.d.ts +4 -2
  33. package/jobs/prompt.d.ts.map +1 -1
  34. package/jobs/prompt.js +16 -15
  35. package/jobs/schedule.d.ts +2 -2
  36. package/jobs/schedule.d.ts.map +1 -1
  37. package/jobs/schedule.js +9 -9
  38. package/jobs/slash-command.d.ts +4 -4
  39. package/jobs/slash-command.d.ts.map +1 -1
  40. package/jobs/slash-command.js +14 -9
  41. package/jobs/status-line.d.ts +1 -11
  42. package/jobs/status-line.d.ts.map +1 -1
  43. package/jobs/status-line.js +8 -12
  44. package/jobs/tool.d.ts +2 -2
  45. package/jobs/tool.d.ts.map +1 -1
  46. package/jobs/tool.js +15 -9
  47. package/mod.d.ts +154 -75
  48. package/mod.d.ts.map +1 -1
  49. package/mod.js +94 -3
  50. package/options.d.ts +2 -2
  51. package/options.js +19 -19
  52. package/package.json +3 -3
  53. package/records.d.ts +10 -8
  54. package/records.d.ts.map +1 -1
  55. package/records.js +28 -17
  56. package/register.d.ts.map +1 -1
  57. package/register.js +17 -5
  58. package/runtime/announce.d.ts +18 -0
  59. package/runtime/announce.d.ts.map +1 -0
  60. package/runtime/announce.js +66 -0
  61. package/runtime/bound.d.ts +4 -0
  62. package/runtime/bound.d.ts.map +1 -0
  63. package/runtime/bound.js +9 -0
  64. package/runtime/claude.d.ts +3 -2
  65. package/runtime/claude.d.ts.map +1 -1
  66. package/runtime/conversations.d.ts +16 -0
  67. package/runtime/conversations.d.ts.map +1 -0
  68. package/runtime/conversations.js +291 -0
  69. package/runtime/deadline.d.ts +0 -1
  70. package/runtime/deadline.d.ts.map +1 -1
  71. package/runtime/deadline.js +1 -1
  72. package/runtime/dependencies.d.ts +2 -0
  73. package/runtime/dependencies.d.ts.map +1 -1
  74. package/runtime/dependencies.js +9 -0
  75. package/runtime/engine.d.ts +17 -0
  76. package/runtime/engine.d.ts.map +1 -0
  77. package/runtime/engine.js +10 -0
  78. package/runtime/grants.d.ts +4 -0
  79. package/runtime/grants.d.ts.map +1 -1
  80. package/runtime/grants.js +63 -26
  81. package/runtime/hooks.d.ts +5 -4
  82. package/runtime/hooks.d.ts.map +1 -1
  83. package/runtime/hooks.js +10 -5
  84. package/runtime/installer.d.ts.map +1 -1
  85. package/runtime/installer.js +11 -9
  86. package/runtime/lifecycle.d.ts +3 -1
  87. package/runtime/lifecycle.d.ts.map +1 -1
  88. package/runtime/lifecycle.js +316 -185
  89. package/runtime/metadata.d.ts +3 -1
  90. package/runtime/metadata.d.ts.map +1 -1
  91. package/runtime/metadata.js +21 -11
  92. package/runtime/options.d.ts +3 -1
  93. package/runtime/options.d.ts.map +1 -1
  94. package/runtime/options.js +4 -4
  95. package/runtime/programs.d.ts +8 -0
  96. package/runtime/programs.d.ts.map +1 -1
  97. package/runtime/programs.js +39 -1
  98. package/runtime/router.d.ts +2 -1
  99. package/runtime/router.d.ts.map +1 -1
  100. package/runtime/router.js +11 -3
  101. package/runtime/sessions.d.ts +36 -0
  102. package/runtime/sessions.d.ts.map +1 -0
  103. package/runtime/sessions.js +235 -0
  104. package/runtime/state.d.ts +6 -2
  105. package/runtime/state.d.ts.map +1 -1
  106. package/runtime/state.js +46 -13
  107. package/runtime/toaster.d.ts +31 -0
  108. package/runtime/toaster.d.ts.map +1 -0
  109. package/runtime/toaster.js +176 -0
  110. package/runtime/ui.d.ts +6 -17
  111. package/runtime/ui.d.ts.map +1 -1
  112. package/runtime/ui.js +36 -112
  113. package/session/context/context.d.ts +10 -0
  114. package/session/context/context.d.ts.map +1 -0
  115. package/session/context/context.js +2 -0
  116. package/session/context/turn/message/document.d.ts +7 -0
  117. package/session/context/turn/message/document.d.ts.map +1 -0
  118. package/session/context/turn/message/document.js +1 -0
  119. package/session/context/turn/message/message.d.ts +54 -0
  120. package/session/context/turn/message/message.d.ts.map +1 -0
  121. package/session/context/turn/message/message.js +137 -0
  122. package/session/context/turn/message/note.d.ts +8 -0
  123. package/session/context/turn/message/note.d.ts.map +1 -0
  124. package/session/context/turn/message/note.js +1 -0
  125. package/session/context/turn/message/notice.d.ts +6 -0
  126. package/session/context/turn/message/notice.d.ts.map +1 -0
  127. package/session/context/turn/message/notice.js +1 -0
  128. package/session/context/turn/message/prompt.d.ts +8 -0
  129. package/session/context/turn/message/prompt.d.ts.map +1 -0
  130. package/session/context/turn/message/prompt.js +1 -0
  131. package/session/context/turn/message/reply.d.ts +6 -0
  132. package/session/context/turn/message/reply.d.ts.map +1 -0
  133. package/session/context/turn/message/reply.js +1 -0
  134. package/session/context/turn/message/sender.d.ts +14 -0
  135. package/session/context/turn/message/sender.d.ts.map +1 -0
  136. package/session/context/turn/message/sender.js +30 -0
  137. package/session/context/turn/message/skill.d.ts +7 -0
  138. package/session/context/turn/message/skill.d.ts.map +1 -0
  139. package/session/context/turn/message/skill.js +1 -0
  140. package/session/context/turn/message/tool-call.d.ts +9 -0
  141. package/session/context/turn/message/tool-call.d.ts.map +1 -0
  142. package/session/context/turn/message/tool-call.js +1 -0
  143. package/session/context/turn/message/tool-result.d.ts +8 -0
  144. package/session/context/turn/message/tool-result.d.ts.map +1 -0
  145. package/session/context/turn/message/tool-result.js +1 -0
  146. package/session/context/turn/turn.d.ts +12 -0
  147. package/session/context/turn/turn.d.ts.map +1 -0
  148. package/session/context/turn/turn.js +2 -0
  149. package/session/conversation.d.ts +64 -0
  150. package/session/conversation.d.ts.map +1 -0
  151. package/session/conversation.js +123 -0
  152. package/session/goal/goal.d.ts +10 -0
  153. package/session/goal/goal.d.ts.map +1 -0
  154. package/session/goal/goal.js +1 -0
  155. package/session/goal/task.d.ts +16 -0
  156. package/session/goal/task.d.ts.map +1 -0
  157. package/session/goal/task.js +3 -0
  158. package/session/session.d.ts +99 -0
  159. package/session/session.d.ts.map +1 -0
  160. package/session/session.js +1 -0
  161. package/src/jobs/check.ts +7 -4
  162. package/src/jobs/context.ts +24 -82
  163. package/src/jobs/permissions/decide-permission.ts +10 -8
  164. package/src/jobs/permissions/find-project-scope.ts +8 -14
  165. package/src/jobs/permissions/match-target.ts +3 -3
  166. package/src/jobs/permissions.ts +8 -6
  167. package/src/jobs/program.ts +7 -3
  168. package/src/jobs/prompt.ts +21 -24
  169. package/src/jobs/schedule.ts +14 -14
  170. package/src/jobs/slash-command.ts +20 -14
  171. package/src/jobs/status-line.ts +9 -21
  172. package/src/jobs/tool.ts +17 -11
  173. package/src/mod.ts +209 -97
  174. package/src/options.ts +22 -22
  175. package/src/records.ts +35 -23
  176. package/src/register.ts +17 -5
  177. package/src/runtime/announce.ts +74 -0
  178. package/src/runtime/bound.ts +12 -0
  179. package/src/runtime/claude.ts +4 -2
  180. package/src/runtime/conversations.ts +302 -0
  181. package/src/runtime/deadline.ts +2 -2
  182. package/src/runtime/dependencies.ts +12 -0
  183. package/src/runtime/engine.ts +22 -0
  184. package/src/runtime/grants.ts +66 -24
  185. package/src/runtime/hooks.ts +19 -10
  186. package/src/runtime/installer.ts +10 -9
  187. package/src/runtime/lifecycle.ts +312 -180
  188. package/src/runtime/metadata.ts +33 -11
  189. package/src/runtime/options.ts +19 -4
  190. package/src/runtime/programs.ts +40 -1
  191. package/src/runtime/router.ts +11 -4
  192. package/src/runtime/sessions.ts +242 -0
  193. package/src/runtime/state.ts +50 -14
  194. package/src/runtime/toaster.ts +218 -0
  195. package/src/runtime/ui.ts +42 -139
  196. package/src/session/context/context.ts +12 -0
  197. package/src/session/context/turn/message/document.ts +8 -0
  198. package/src/session/context/turn/message/message.ts +165 -0
  199. package/src/session/context/turn/message/note.ts +9 -0
  200. package/src/session/context/turn/message/notice.ts +7 -0
  201. package/src/session/context/turn/message/prompt.ts +9 -0
  202. package/src/session/context/turn/message/reply.ts +7 -0
  203. package/src/session/context/turn/message/sender.ts +38 -0
  204. package/src/session/context/turn/message/skill.ts +8 -0
  205. package/src/session/context/turn/message/tool-call.ts +10 -0
  206. package/src/session/context/turn/message/tool-result.ts +9 -0
  207. package/src/session/context/turn/turn.ts +14 -0
  208. package/src/session/conversation.ts +169 -0
  209. package/src/session/goal/goal.ts +9 -0
  210. package/src/session/goal/task.ts +19 -0
  211. package/src/session/session.ts +94 -0
  212. package/src/testing/fake-claude.ts +22 -16
  213. package/src/testing/fake-elements.ts +3 -2
  214. package/src/testing/fake-files.ts +1 -1
  215. package/src/testing/fake-sessions.ts +203 -0
  216. package/src/testing.ts +112 -33
  217. package/src/ui/components.ts +3 -3
  218. package/src/ui/elements.ts +22 -8
  219. package/src/ui/markdown.ts +22 -26
  220. package/src/ui/places.ts +49 -0
  221. package/src/utils/call-effects.ts +3 -3
  222. package/src/utils/changelog.ts +32 -0
  223. package/src/utils/matcher.ts +37 -0
  224. package/src/utils/metadata.ts +17 -15
  225. package/src/utils/parse-shell.ts +9 -8
  226. package/src/utils/paths.ts +15 -1
  227. package/src/utils/text.ts +4 -0
  228. package/testing/fake-claude.d.ts +4 -1
  229. package/testing/fake-claude.d.ts.map +1 -1
  230. package/testing/fake-claude.js +18 -11
  231. package/testing/fake-elements.js +4 -2
  232. package/testing/fake-files.js +1 -1
  233. package/testing/fake-sessions.d.ts +74 -0
  234. package/testing/fake-sessions.d.ts.map +1 -0
  235. package/testing/fake-sessions.js +170 -0
  236. package/testing.d.ts +33 -13
  237. package/testing.d.ts.map +1 -1
  238. package/testing.js +92 -21
  239. package/ui/components.d.ts +2 -3
  240. package/ui/components.d.ts.map +1 -1
  241. package/ui/components.js +3 -3
  242. package/ui/elements.d.ts.map +1 -1
  243. package/ui/elements.js +19 -9
  244. package/ui/markdown.d.ts +35 -33
  245. package/ui/markdown.d.ts.map +1 -1
  246. package/ui/markdown.js +14 -16
  247. package/ui/places.d.ts +43 -0
  248. package/ui/places.d.ts.map +1 -0
  249. package/ui/places.js +31 -0
  250. package/utils/call-effects.d.ts +1 -1
  251. package/utils/call-effects.d.ts.map +1 -1
  252. package/utils/call-effects.js +2 -2
  253. package/utils/changelog.d.ts +9 -0
  254. package/utils/changelog.d.ts.map +1 -0
  255. package/utils/changelog.js +29 -0
  256. package/utils/matcher.d.ts +13 -0
  257. package/utils/matcher.d.ts.map +1 -0
  258. package/utils/matcher.js +32 -0
  259. package/utils/metadata.d.ts +3 -3
  260. package/utils/metadata.js +18 -14
  261. package/utils/parse-shell.js +9 -7
  262. package/utils/paths.d.ts +2 -0
  263. package/utils/paths.d.ts.map +1 -1
  264. package/utils/paths.js +13 -1
  265. package/utils/text.d.ts +1 -0
  266. package/utils/text.d.ts.map +1 -1
  267. package/utils/text.js +3 -0
  268. package/src/ui/slots.ts +0 -36
  269. package/ui/slots.d.ts +0 -208
  270. package/ui/slots.d.ts.map +0 -1
  271. package/ui/slots.js +0 -19
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 hook, one pane, and one render of the band above the prompt, a Skill, and a test. Then installs its packages.
15
+ Creates a mod in `./<name>`: a `defineMod` with one hook, 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.
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.
@@ -28,7 +28,7 @@ cmod link [path] [--yes] [--option key=value]...
28
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`.
29
29
 
30
30
  - `--yes` approves the mod's install and uninstall commands without asking.
31
- - `--option key=value` sets one of the mod's options through `claude plugin configure`, once per option. It refuses a key the mod does not declare, and names the keys it does ([options.md](options.md)).
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)).
32
32
 
33
33
  ### cmod unlink
34
34
 
@@ -54,9 +54,9 @@ Runs every check this machine can run on the mod at `path` (default: the current
54
54
  6. Looks for prebuilt binaries outside `cli/`.
55
55
  7. Bundles the hooks module as `cmod publish` does, and names each reason a bundle fails.
56
56
  8. Validates it with Claude Code, `claude plugin validate --strict`.
57
- 9. Type-checks it 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.
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
58
  10. Lints it with `oxlint`.
59
- 11. Runs, with `bun test`, every test file that does not import `claude-code/testing`.
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
60
  12. Runs, with `claude plugin test`, the test files that import `claude-code/testing`. It skips this when none does.
61
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.
62
62
 
@@ -68,7 +68,7 @@ Runs every check this machine can run on the mod at `path` (default: the current
68
68
  cmod try <owner/repo | path>... [--yes] [--option key=value]... [--home <folder>] [-- claude arguments]
69
69
  ```
70
70
 
71
- Starts one throwaway Claude Code session with mods set up, for trying a mod by hand or testing it with an agent. Installs each mod's packages, installs the cmod plugin when Claude Code lacks it, and runs each mod's install step after asking consent. Then starts one Claude Code session with every mod loaded through `--plugin-dir`.
71
+ Starts one throwaway Claude Code session with mods set up, for trying a mod by hand or testing it with an agent. Installs each mod's packages, installs the cmod plugin when Claude Code lacks it, and runs each mod's install step after asking consent. Then links every mod into the home's `settings.json`, as `cmod link` does, starts one Claude Code session, and unlinks each folder it linked when the session ends. With `--home ~`, a session you start meanwhile loads the tried mods too.
72
72
 
73
73
  - `--option key=value` sets one option of the mod. With several mods, write `--option <mod>.key=value`.
74
74
  - The session runs with a new home folder, so it sees only the mods named here and changes none of your Claude Code settings, mods, key bindings, or history. The folder is deleted when the session ends.
@@ -78,7 +78,7 @@ Starts one throwaway Claude Code session with mods set up, for trying a mod by h
78
78
  - `ANTHROPIC_CONFIG_DIR` points at your `~/.config/anthropic` login profiles, and `CLAUDE_CODE_OAUTH_TOKEN` and `ANTHROPIC_API_KEY` pass through.
79
79
  - The session keeps how Claude Code reaches the model and what it must never do, so a try is as safe as your own session. From your `settings.json` it keeps `env` (a proxy or `ANTHROPIC_BASE_URL`, without `cmod link`'s `CLAUDE_CODE_PLUGIN_DIRS`), the credential helpers (`apiKeyHelper`, `awsAuthRefresh`, `awsCredentialExport`, `gcpAuthRefresh`, `otelHeadersHelper`, `proxyAuthHelper`), the login pins (`forceLoginMethod`, `forceLoginOrgUUID`, `forceLoginGatewayUrl`, `gatewayInternalNetworks`), `sandbox`, and `permissions.deny`. From `~/.claude.json` it keeps your account and the API keys you approved. A `~/` in a helper still means your own home. Hooks, plugins, allow rules, the model, and every other preference stay behind. Managed settings apply as always.
80
80
  - `--home <folder>` runs in that folder and keeps it, so the next `cmod try --home <folder>` starts from it. `--home ~` runs in your own home, with your settings and mods.
81
- - Unlike `cmod link`, it builds no program from `cli/`. A mod whose `package.json` `cmod.program` names a program downloads it from the GitHub release of the mod's version, so it fails until `cmod publish` has released that version.
81
+ - A checkout's program is built from its `cli/` the way `cmod link` builds it, with your own home and environment, so the build finds your toolchains and caches. A GitHub mod's program downloads from the GitHub release of the mod's version, so it fails until `cmod publish` has released that version.
82
82
  - When the session ends, even when its terminal closes, it runs each mod's uninstall step and deletes its record, data folder, approval, key bindings, and program, so the mod stays uninstalled. A checkout already set up from the same folder keeps its setup.
83
83
  - A GitHub mod is cloned into a temporary folder, which is deleted too.
84
84
  - A mod another folder has set up in that home is refused, so `cmod try` never replaces an installed copy.
@@ -92,13 +92,13 @@ cmod publish [path] [--dry-run]
92
92
 
93
93
  Releases the mod at `path` (default: the current folder) at the version in its `plugin.json`. It refuses a `plugin.json` whose `userConfig` differs from the options `defineMod` declares, and says to run `cmod check` and commit `.claude-plugin/plugin.json`.
94
94
 
95
- 1. Builds the release from the committed files, leaving out `cli/`, `.github/`, and `.claude/`. A `"files"` list in `package.json` limits the release to the paths it lists, plus the folders of the install and uninstall steps, `.claude-plugin/`, `package.json`, `tsconfig.json`, the README, and the license, the way `npm publish` reads it. `tsconfig.json` holds the JSX settings the bundle in step 2 needs. List every folder the hooks module imports from, such as `"files": ["hooks", "src", "skills"]`, because the bundle in step 2 is built from the release.
95
+ 1. Builds the release from the committed files, leaving out `cli/`, `.github/`, and `.claude/`. A `"files"` list in `package.json` limits the release to the paths it lists, plus the folders of the install and uninstall steps, `.claude-plugin/`, `package.json`, `tsconfig.json`, the README, the license, and `CHANGELOG.md`, the way `npm publish` reads it. `tsconfig.json` holds the JSX settings the bundle in step 2 needs. List every folder the hooks module imports from, such as `"files": ["hooks", "src", "skills"]`, because the bundle in step 2 is built from the release.
96
96
  2. Bundles the hooks module that `hooks/hooks.json` names, with the mod's source and packages, into one readable `.js` file, writes each control character in it as a `\u` escape, and points `hooks/hooks.json` at it. Anthropic's plugin directory reads a repository without installing its packages, so it can follow a mod only when all its code is in one file.
97
97
  3. Checks the release with `claude plugin validate --strict`, and stops with the validator's message when it fails.
98
98
  4. Commits the release as the `release` branch, and builds the release archive from that commit.
99
99
  5. Builds the program `cli/` declares, and writes `SHA256SUMS` for every file of the release.
100
100
  6. Writes `.claude-plugin/marketplace.json`, listing the archive and the cmod plugin. The cmod entry points at the `release` branch of `heyJordanParker/cmod`, so the mod's `"cmod"` dependency resolves in this marketplace and follows each cmod release with no new publish of the mod.
101
- 7. Commits that file, tags `v<version>`, pushes the tag and the `release` branch, and creates the GitHub release. Its notes open with `## Breaking changes`, listing each `BREAKING CHANGE:` footer of a commit since the last `v` tag and the subject of each commit typed with `!`, such as `feat!: mod.claude replaces job.claude`, so the people who use the mod know what to change before they update. `--dry-run` prints that list.
101
+ 7. Commits that file, tags `v<version>`, pushes the tag and the `release` branch, and creates the GitHub release. Its notes are the version's section of the committed `CHANGELOG.md`, above GitHub's compare link ([mod.md](mod.md#release-notes)). `--dry-run` prints them.
102
102
  8. Prints the link to paste as the Repository when you submit the mod at [claude.ai/directory/manage](https://claude.ai/directory/manage), such as `https://github.com/owner/greeter/tree/release`. The portal reads the branch from the link, so the directory follows `release`.
103
103
 
104
104
  The release keeps `package.json`, so Claude Code still installs the mod's packages for its install and uninstall steps. It keeps `bun.lock` too, unless a `"files"` list leaves it out. `main` keeps the source only.
@@ -106,6 +106,7 @@ The release keeps `package.json`, so Claude Code still installs the mod's packag
106
106
  It needs, and refuses to start without:
107
107
 
108
108
  - the mod's folder as the root of a git repository with at least one commit, because `git archive` builds from `HEAD`
109
+ - a `## [<version>]` section with notes in the committed `CHANGELOG.md`
109
110
  - `node_modules` in the mod's folder, from `bun install`, when the hooks module imports a package
110
111
  - a `register` declared as a function in the hooks module, as `cmod new` writes it, because the validator refuses a bundled `register` that is not one
111
112
  - the `claude` command, which validates the release
@@ -172,6 +173,37 @@ Lists every plugin Claude Code has installed or linked, with its version. A mod
172
173
 
173
174
  - 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.
174
175
 
176
+ ## Read your conversations
177
+
178
+ ### cmod history
179
+
180
+ ```text
181
+ cmod history sessions [--all] [--since 2d] [--running] [--project <folder>] [--json]
182
+ cmod history search <text> [--all] [--since 2d] [--running] [--project <folder>] [--limit <n>] [--before <session>:<id>] [--json]
183
+ cmod history messages [<session>] [--task <id>] [--turn <id>] [--after <id>] [--last <n>] [--json]
184
+ cmod history turns [<session>] [--context <n>] [--last <n>] [--json]
185
+ cmod history contexts [<session>] [--json]
186
+ cmod history tasks [<session>] [--task <id>] [--json]
187
+ cmod history goal [<session>] [--json]
188
+ cmod history state [<session>] [--json]
189
+ cmod history follow <session>... [--json]
190
+ cmod history index
191
+ ```
192
+
193
+ Reads every conversation Claude Code saved, as the sessions, contexts, turns, tasks, and messages `sessions` answers a mod ([session.md](session.md)), so a person or an agent reads them in a terminal without parsing a transcript.
194
+
195
+ - `sessions` lists the conversations of the current folder's project, or `--project`'s, newest first, each with its time, id, and title. `--all` lists every project's, `--since` keeps those active in the last `30m`, `6h`, `2d`, or `1w`, or since a date such as `2026-10-01`, and `--running` keeps those open in Claude Code now.
196
+ - `search` prints the newest 50 messages whose text or tool input contains the text, at least 3 characters, any case, each with its time and session. Each tool input value is searched in its first 1,024 characters, and tool results are not searched. `--all`, `--since`, and `--running` scope it as they scope `sessions`, `--limit` changes the 50, and `--before <session>:<id>` prints the page after the message it names, the last one of the page before.
197
+ - `messages` prints one conversation's messages in order: the session named, or the one the command runs in when Claude Code runs it. `--task` reads a task's messages instead, `--turn` keeps one turn's, `--after` starts after a message id, and `--last` keeps that many from the end.
198
+ - `turns` prints the conversation's turns, each opened by a prompt and closed when Claude finishes. `--context` keeps one context's, and `--last` keeps that many from the end.
199
+ - `contexts` prints the conversation's contexts, a new one after each compaction.
200
+ - `tasks` prints the tasks the conversation started, or with `--task` the tasks that task started.
201
+ - `goal` prints what the conversation is for: the goal a mod set, or its first prompt.
202
+ - `state` prints what a running conversation resumes from.
203
+ - `follow` prints each message, turn, context, task, and goal change of the sessions named as Claude Code saves it, until stopped with Ctrl+C. The cmod plugin runs it to hear other running sessions.
204
+ - `--json` prints JSON instead of one line each.
205
+ - `index` reads every saved conversation into the index at `~/.local/share/cmod/history-3.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.
206
+
175
207
  ## Run in an install script
176
208
 
177
209
  ### cmod download
@@ -192,7 +224,7 @@ cmod runs these itself. A person runs them to see a step's whole log, to finish
192
224
  cmod setup <plugin-root> [--events] [--consent <sha256>] [--yes]
193
225
  ```
194
226
 
195
- Refuses a machine the `os` and `cpu` keys of the mod's `package.json` leave out. Downloads the program its `cmod.program` names 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 progress line in Claude Code reads them.
227
+ 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.
196
228
 
197
229
  ### cmod teardown
198
230
 
@@ -200,7 +232,7 @@ Refuses a machine the `os` and `cpu` keys of the mod's `package.json` leave out.
200
232
  cmod teardown <plugin-name> [--events]
201
233
  ```
202
234
 
203
- Runs a removed mod's saved uninstall step, then deletes its install record, the saved step, every version of its program, and its data folder, and forgets the scripts and the permissions the person approved for it. It keeps the mod's config folder, `~/.claude/cmods/<plugin-name>`. A failed uninstall step keeps the record, so `cmod teardown` can run again.
235
+ Runs a removed mod's saved uninstall step, then deletes its install record, the saved step, every version of its program, and its data folder, takes out each Claude Code setting it changed that still has the value it set, and forgets the scripts and the permissions the person approved for it. It keeps the mod's config folder, `~/.claude/cmods/<plugin-name>`. A failed uninstall step keeps the record, so `cmod teardown` can run again.
204
236
 
205
237
  ### cmod permission
206
238
 
@@ -210,6 +242,14 @@ cmod permission <mod> <name> [value] on|off
210
242
 
211
243
  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)).
212
244
 
245
+ ### cmod settings
246
+
247
+ ```text
248
+ cmod settings <mod> <user|project|local> <json> [--project <folder>]
249
+ ```
250
+
251
+ Changes Claude Code settings for a set-up mod, as its `settings.user.update`, `settings.project.update`, and `settings.local.update` do ([mod.md](mod.md#settings-claude-code-s-settings-files)), such as `cmod settings keeper user '{"cleanupPeriodDays":365}'`. It writes each top-level key of the JSON object into the user, project, or local settings, and `null` removes the key. The project and local settings are those of `--project`, the current folder when it is left out, and `mcpServers` in the project settings goes to `.mcp.json`. Each key needs the mod's `config` permission for it. cmod keeps every other key and the file's formatting, and `cmod teardown` takes out each value the mod set that is still the same.
252
+
213
253
  ### cmod option
214
254
 
215
255
  ```text
@@ -4,16 +4,16 @@ A mod offers methods to other mods in `api`. Another mod lists it as a dependenc
4
4
 
5
5
  ## Offer methods: api
6
6
 
7
- Each method of `api` gets the caller's input and the provider's own `mod`, 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
+ 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.
8
8
 
9
9
  ```ts
10
- import { defineMod } from '../node_modules/@cmodjs/core/mod.js'
10
+ import { defineMod, fs } from '../node_modules/@cmodjs/core/mod.js'
11
11
 
12
12
  export const tracer = defineMod({
13
13
  name: 'tracer',
14
14
  api: {
15
- signatures: async ({ path }: { path: string }, mod) =>
16
- (await mod.fs.read(path)).split('\n').flatMap((text, index) => {
15
+ signatures: async ({ path }: { path: string }) =>
16
+ (await fs.read(path)).split('\n').flatMap((text, index) => {
17
17
  const name = /^export function (\w+)/.exec(text)?.[1]
18
18
  return name === undefined ? [] : [{ name, line: index + 1 }]
19
19
  }),
@@ -49,16 +49,16 @@ declare module 'claude-code' {
49
49
  A mod that calls tracer lists it in its own `.claude-plugin/plugin.json`, and Claude Code installs it with the mod:
50
50
 
51
51
  ```json
52
- "dependencies": ["cmod", "tracer"]
52
+ "dependencies": [{ "name": "cmod", "version": "^1.0.0" }, "tracer"]
53
53
  ```
54
54
 
55
55
  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
56
 
57
57
  ```json
58
- "dependencies": ["cmod", { "name": "tracer", "version": "^1.0" }]
58
+ "dependencies": [{ "name": "cmod", "version": "^1.0.0" }, { "name": "tracer", "version": "^1.0" }]
59
59
  ```
60
60
 
61
- Claude Code checks the range against the `version` in the provider's `plugin.json` when it loads your mod ([Claude Code's plugin dependencies](https://code.claude.com/docs/en/plugins/dependencies)).
61
+ Claude Code checks the range against the `version` in the provider's `plugin.json` when it loads your mod ([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.
62
62
 
63
63
  Claude Code then lays tracer's types file beside the mod's own types, so the call is typed:
64
64
 
@@ -85,7 +85,7 @@ export const outline = defineMod({
85
85
 
86
86
  ## When a call fails
87
87
 
88
- A call that cannot answer rejects with one of these messages:
88
+ A call that cannot answer rejects with one of these reasons:
89
89
 
90
90
  ```text
91
91
  tracer is not installed. Run cmod install tracer.
@@ -95,19 +95,19 @@ tracer: <the message of the error the method threw>
95
95
  ```
96
96
 
97
97
  - `tracer is not installed` covers a provider whose code did not load, one the person declined to install, and one whose install failed.
98
- - `tracer is installing` covers a provider that is still running its install step.
98
+ - `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.
99
99
  - 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`.
100
- - A call runs inside the deadline of the job that makes it ([jobs.md](jobs.md)), and 30 seconds anywhere else. Past the deadline of a job, the call fails with `mod.dependencies.tracer.signatures passed the 30 s deadline of slashCommand`, naming the job. Anywhere else it fails with `mod.dependencies.tracer.signatures passed its 30 s deadline`.
100
+ - A call fails with `mod.dependencies.tracer.signatures passed its 30 s deadline` when the other mod takes longer than 30 seconds. A job bounds the whole function that makes the call by its own deadline ([jobs.md](jobs.md)), so a slow call inside `slashCommand` can fail the command first, with `/outline passed the 30 s deadline of slashCommand`.
101
101
 
102
102
  ```ts
103
103
  notInstalled(name: string): string
104
104
  ```
105
105
 
106
- `notInstalled`, exported from `mod.js`, returns the first message for a mod name: `<name> is not installed. Run cmod install <name>.` A test uses it to expect that message.
106
+ `notInstalled`, exported from `mod.js`, returns the first reason for a mod name: `<name> is not installed. Run cmod install <name>.` A test uses it to expect that message.
107
107
 
108
108
  ## Test the calls
109
109
 
110
- `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. Run cmod install tracer.` [testing.md](testing.md) covers it.
110
+ `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. Run cmod install tracer.`, as in Claude Code. [testing.md](testing.md) covers it.
111
111
 
112
112
  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:
113
113
 
package/docs/hooks.md CHANGED
@@ -1,34 +1,44 @@
1
1
  # Hooks
2
2
 
3
- `mod.on` runs a function when Claude Code raises an event, such as a prompt or a tool call. The function reads the event's input and may answer to change what Claude Code does next.
3
+ A hook is a point where Claude Code asks its plugins, such as a prompt or a tool call. `mod.on` runs a listener at a hook. The listener reads the hook's input and may answer to change what Claude Code does next.
4
4
 
5
5
  ## mod.on
6
6
 
7
7
  ```ts
8
- mod.on<E extends ModEvent>(event: E, hook: ModHook<E>, options?: { timeoutMs?: number }): void
8
+ mod.on<H extends Hook>(hook: H, listener: HookListener<H>, options?: { timeoutMs?: number }): Disposable
9
+ mod.on<H extends Hook>(hook: H, matcher: Matcher<HookInput<H>>, listener: HookListener<H>, options?: { timeoutMs?: number }): Disposable
9
10
 
10
- type ModHook<E> = (input: HookInput<E>) => HookAnswer | void | Promise<HookAnswer | void>
11
+ type HookListener<H> = (input: HookInput<H>) => HookAnswer | void | Promise<HookAnswer | void>
11
12
  ```
12
13
 
13
- Add hooks in `setup`. Each `mod.on` adds one more hook. Several hooks on one event all run.
14
+ Add listeners in `setup`. Each `mod.on` adds one more listener. Several listeners on one hook all run. `dispose()` on the `Disposable` it returns removes the listener ([mod.md](mod.md#disposable)).
14
15
 
15
- A hook that waits on something slow, such as a model call or a server, takes `timeoutMs`. Past it, the event goes on as if the hook were absent, and cmod logs `<mod>: the <event> hook passed its <n> s timeout`. Claude Code also gives a hook's own code 10 seconds, and the time a hook waits on a `mod` call does not count toward those 10 seconds, so `timeoutMs` is the limit on a slow call. `timeoutMs` is a whole number above 0 and at most `2147483647`, and any other value stops `setup` with the fix.
16
+ `mod.js` exports each hook's name as a constant, such as `PreToolUse`, and `mod.on` takes it: `mod.on(PreToolUse, …)`.
17
+
18
+ A matcher runs the listener only for the inputs it matches. It is a partial copy of the input: a key it names must match, and a key it leaves out matches anything. A string matches the same string, a `RegExp` matches the text it tests true on, and a list matches any one of its items.
19
+
20
+ ```ts
21
+ mod.on(PostToolUse, { tool_name: /^(Edit|Write)$/ }, ({ files }) => void (mod.state.session.edited += files.changed.length))
22
+ mod.on(PreToolUse, { tool_name: 'Bash', tool_input: { command: /^rm / } }, () => ({ decision: 'block', reason: 'Use trash instead of rm.' }))
23
+ ```
24
+
25
+ A listener that waits on something slow, such as a model call or a server, takes `timeoutMs`. Past it, the hook goes on as if the listener were absent, and cmod shows the warning toast `The <hook> hook passed its <n> s timeout`. Claude Code also gives a listener's own code 10 seconds, and the time it waits on a call does not count toward those 10 seconds, so `timeoutMs` is the limit on a slow call. `timeoutMs` is a whole number above 0 and at most `2147483647`, and any other value stops `setup` with the fix.
16
26
 
17
27
  ```ts
18
- mod.on('Stop', async ({ last_assistant_message }) => {
19
- const verdict = await mod.model.complete({ model: 'haiku', prompt: `Is this reply complete? Answer yes or no.\n\n${last_assistant_message}` })
28
+ mod.on(Stop, async ({ last_assistant_message }) => {
29
+ const verdict = await model.complete({ model: 'haiku', prompt: `Is this reply complete? Answer yes or no.\n\n${last_assistant_message}` })
20
30
  if (verdict.isAnswered && verdict.text.trim().toLowerCase().startsWith('no')) return { decision: 'block', reason: 'Finish the reply.' }
21
31
  }, { timeoutMs: 8000 })
22
32
  ```
23
33
 
24
- `HookInput<E>`, exported from `mod.js`, is the input a hook on `E` gets, for a hook that lives in a function of its own:
34
+ `HookInput<H>`, exported from `mod.js`, is the input a listener on `H` gets, for a listener that lives in a function of its own:
25
35
 
26
36
  ```ts
27
- import type { HookAnswer, HookInput, Mod } from '../node_modules/@cmodjs/core/mod.js'
37
+ import { process, SessionStart, type HookAnswer, type HookInput } from '../node_modules/@cmodjs/core/mod.js'
28
38
 
29
- export async function addBranch(mod: Mod, input: HookInput<'SessionStart'>): Promise<HookAnswer | undefined> {
30
- const { stdout } = await mod.process.run(['git', 'branch', '--show-current'], { cwd: input.cwd })
31
- return { hookSpecificOutput: { hookEventName: 'SessionStart', additionalContext: `The git branch is ${stdout.trim()}.` } }
39
+ export async function addBranch(input: HookInput<typeof SessionStart>): Promise<HookAnswer | undefined> {
40
+ const { stdout } = await process.run(['git', 'branch', '--show-current'], { cwd: input.cwd })
41
+ return { hookSpecificOutput: { hookEventName: SessionStart, additionalContext: `The git branch is ${stdout.trim()}.` } }
32
42
  }
33
43
  ```
34
44
 
@@ -41,25 +51,25 @@ A `PreToolUse` hook runs on Claude Code's `tool.call`, before Claude Code checks
41
51
  A hook on `PermissionRequest` takes part in that check too, so a mod with one does not start without `registerPermissionCheck`.
42
52
 
43
53
  ```ts
44
- import { defineMod } from '../node_modules/@cmodjs/core/mod.js'
54
+ import { defineMod, PreToolUse } from '../node_modules/@cmodjs/core/mod.js'
45
55
 
46
56
  export const safeDelete = defineMod({
47
57
  name: 'safe-delete',
48
58
  setup(mod) {
49
- mod.on('PreToolUse', (input) => {
59
+ mod.on(PreToolUse, (input) => {
50
60
  const command = input.tool_input['command']
51
61
  if (input.tool_name !== 'Bash' || typeof command !== 'string' || !command.startsWith('rm ')) return
52
- return { hookSpecificOutput: { hookEventName: 'PreToolUse', updatedInput: { ...input.tool_input, command: command.replace(/^rm /, 'trash ') } } }
62
+ return { hookSpecificOutput: { hookEventName: PreToolUse, updatedInput: { ...input.tool_input, command: command.replace(/^rm /, 'trash ') } } }
53
63
  })
54
64
  },
55
65
  })
56
66
  ```
57
67
 
58
- ## Every ModEvent
68
+ ## Every hook
59
69
 
60
- `ModEvent` names the 18 events a mod hooks. The input of each is the hook input a `settings.json` hook reads for that event, typed as `ClassicHookInputs['<event>']` in the `claude-code` module, with the changes this table names.
70
+ `Hook` names the 18 hooks a mod listens to. The input of each is the input a `settings.json` hook reads, typed as `ClassicHookInputs['<hook>']` in the `claude-code` module, with the changes this table names.
61
71
 
62
- | Event | Input changes |
72
+ | Hook | Input changes |
63
73
  | --- | --- |
64
74
  | `SessionStart` | none |
65
75
  | `SessionEnd` | none |
@@ -80,13 +90,13 @@ export const safeDelete = defineMod({
80
90
  | `CwdChanged` | no `transcript_path` |
81
91
  | `FileChanged` | none |
82
92
 
83
- cmod raises `CwdChanged` itself, never Claude Code. It raises it when the working folder changes: after `/cd`, after a Bash or PowerShell call, and at a prompt. `mod.cwd` and `mod.projectRoot` already hold the new folder when the hook runs.
93
+ cmod raises `CwdChanged` itself, never Claude Code. It raises it when the working folder changes: after `/cd`, after a Bash or PowerShell call, and at a prompt. `sessions.current.cwd` and `sessions.current.project` already hold the new folder when the listener runs.
84
94
 
85
95
  Claude Code raises `FileChanged` when a file it watches changes, with the file's `file_path` and `event`: `'change'`, `'add'`, or `'unlink'`. It watches the paths a `SessionStart` hook answers in `watchPaths`:
86
96
 
87
97
  ```ts
88
- mod.on('SessionStart', () => ({ hookSpecificOutput: { hookEventName: 'SessionStart', watchPaths: [`${mod.dataFolder}/history`] } }))
89
- mod.on('FileChanged', ({ file_path, event }) => mod.ui.toast(`${file_path}: ${event}`))
98
+ mod.on(SessionStart, () => ({ hookSpecificOutput: { hookEventName: SessionStart, watchPaths: [`${mod.dataFolder}/history`] } }))
99
+ mod.on(FileChanged, ({ file_path, event }) => mod.ui.toast(`${file_path}: ${event}`))
90
100
  ```
91
101
 
92
102
  ## files: what a tool call reads and changes
@@ -97,6 +107,7 @@ The input of `PreToolUse`, `PostToolUse`, and `PostToolUseFailure` has `files: {
97
107
  - `Grep` counts its `path`, or the working folder when it names none, and `Glob` counts its `pattern` joined to its `path`. Since `read` lists only files, a `Grep` of a folder and every `Glob` usually leave `read` empty.
98
108
  - A `Bash` command reads and changes the files its commands name, such as `cat a.ts > b.ts`. A path that holds a shell expansion, such as `$HOME`, is left out.
99
109
  - `read` lists only paths that are files now. `changed` lists every path the call writes, whether it exists or not.
110
+ - A path cmod cannot check, such as one it has no permission to read, is left out of `read`, and cmod writes the path and the reason to Claude Code's debug log.
100
111
  - When cmod cannot work the files out, both lists are empty, and the reason goes to Claude Code's debug log.
101
112
 
102
113
  ## shell.js and path.js: read a command line
@@ -117,13 +128,14 @@ extname(path): string relative(from, to): string isAbsolute(path): bool
117
128
  - `folder` is where a command runs, as the line wrote it: `''` for the folder the line starts in, `src` after `cd src`, `/tmp` after `cd /tmp`, and `~/p` after `cd ~/p`. `resolve(input.cwd, folder)` makes it absolute.
118
129
  - `input` is the index in `commands` of the command whose output this one reads through `|` or `|&`, and is absent for a command that reads no pipe. `trace read a.ts | head -5` gives `head` the `input` `0`. A wrapper such as `timeout 5 trace read a.ts` writes the output of the command it runs, so `input` names `trace`, not `timeout`.
119
130
  - `reads`, `writes`, and `fetches` are the paths and URLs the commands name, as written. A path that holds a shell expansion makes `isFullyParsed` false, and so does a line the parser cannot read.
120
- - `resolve` has no working folder to start from in Claude Code's hooks, so pass an absolute folder first, such as `resolve(mod.cwd, path)`.
131
+ - `resolve` has no working folder to start from in Claude Code's hooks, so pass an absolute folder first, such as `resolve(sessions.current.cwd, path)`.
121
132
 
122
133
  ```ts
123
134
  import { basename, resolve } from '../node_modules/@cmodjs/core/path.js'
124
135
  import { parseShell } from '../node_modules/@cmodjs/core/shell.js'
136
+ import { PreToolUse } from '../node_modules/@cmodjs/core/mod.js'
125
137
 
126
- mod.on('PreToolUse', (input) => {
138
+ mod.on(PreToolUse, (input) => {
127
139
  if (input.tool_name !== 'Bash') return
128
140
  for (const { argv, folder } of parseShell(String(input.tool_input['command'])).commands) {
129
141
  if (basename(argv[0]) === 'trace') mod.ui.toast(`trace runs in ${resolve(input.cwd, folder)}`)
@@ -144,7 +156,7 @@ type HookAnswer = {
144
156
  decision?: 'block'
145
157
  reason?: string
146
158
  hookSpecificOutput?: {
147
- hookEventName?: ModEvent
159
+ hookEventName?: Hook
148
160
  additionalContext?: string
149
161
  permissionDecision?: 'allow' | 'deny' | 'ask'
150
162
  permissionDecisionReason?: string
@@ -192,13 +204,13 @@ Every mod's hooks and Claude Code's own `settings.json` hooks answer the same ev
192
204
  - For every other field, the mod's answer replaces the answer of the hooks beneath it.
193
205
 
194
206
  ```ts
195
- import { defineMod } from '../node_modules/@cmodjs/core/mod.js'
207
+ import { defineMod, Stop } from '../node_modules/@cmodjs/core/mod.js'
196
208
 
197
209
  export const keepGoing = defineMod({
198
210
  name: 'keep-going',
199
211
  state: { session: { hasCheckedTests: false } },
200
212
  setup(mod) {
201
- mod.on('Stop', () => {
213
+ mod.on(Stop, () => {
202
214
  if (mod.state.session.hasCheckedTests) return
203
215
  mod.state.session.hasCheckedTests = true
204
216
  return { decision: 'block', reason: 'Run the tests before you stop.' }
@@ -213,7 +225,7 @@ A hook that throws, rejects, or answers a field its event does not read fails:
213
225
 
214
226
  - On `PreToolUse` the call is denied with `<mod>: the PreToolUse hook failed: <error>`.
215
227
  - On `PermissionRequest` the request is denied with `<mod>: the PermissionRequest hook failed: <error>`.
216
- - On every other event cmod logs `<mod>: the <event> hook failed: <error>`, and the event goes on as if the hook answered nothing.
228
+ - On every other hook cmod shows the error toast `The <hook> hook failed` with the error under it, and the hook goes on as if the listener answered nothing.
217
229
 
218
230
  ## The first SessionStart
219
231
 
package/docs/index.md CHANGED
@@ -10,7 +10,7 @@ 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": ["cmod"]
13
+ ├── .claude-plugin/plugin.json name, version, and "dependencies": [{ "name": "cmod", "version": "^1.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
@@ -19,7 +19,7 @@ my-mod/
19
19
  ├── src/mod.tsx defineMod: the state, the setup, the hooks, the panes
20
20
  ├── src/state.ts the starting state
21
21
  ├── src/panes/prompts.tsx one definePane per file
22
- ├── src/components/prompt-count.tsx what the panes and slot renders draw with
22
+ ├── src/components/prompt-count.tsx what the panes and renders draw with
23
23
  ├── skills/my-mod/SKILL.md a Skill the mod ships
24
24
  ├── tests/mod.test.ts testMod tests, run with bun test
25
25
  ├── package.json depends on @cmodjs/core
@@ -32,15 +32,15 @@ The template is a working example to replace. It counts the prompts of the sessi
32
32
 
33
33
  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
34
 
35
- ## Claude Code's types
35
+ ## Claude Code's API description
36
36
 
37
- `.claude-plugin/types/` holds the `tsconfig.json` the mod's own extends, the `claude-code` module, and the `h` JSX global. Claude Code writes it when it loads the mod, and git ignores it. `cmod check` has Claude Code write it when it is missing, so a fresh clone and CI type-check too. Until it exists, `tsc` alone fails, so run `cmod check` once first.
37
+ `.claude-plugin/types/` describes Claude Code's API to TypeScript: the `tsconfig.json` the mod's own extends, the `claude-code` module that `@cmodjs/core` imports, and the `h` JSX global. Claude Code writes it when it loads the mod, and git ignores it. `cmod check` has Claude Code write it when it is missing, so a fresh clone and CI compile the mod against Claude Code's API too. Until it exists, `tsc` alone fails, so run `cmod check` once first.
38
38
 
39
39
  Claude Code links the types of each plugin the mod depends on, such as `cmod`, into the folder where that plugin is installed. `cmod try` installs the cmod plugin into a home it deletes when the session ends, which leaves those links leading nowhere, so `tsc` alone fails with `Cannot find type definition file for 'cmod'`. `cmod check` deletes a types folder with such a link and has Claude Code write it again.
40
40
 
41
41
  ## Where a mod runs
42
42
 
43
- Mod code runs inside Claude Code's hooks environment, not in Node or Bun. It has no Node or Bun modules, no DOM, and no `import()`. `mod.fs` reaches files, `mod.process` runs programs, and `mod.http` reaches the network ([mod.md](mod.md)). A job of your own also reaches the plugin's folder through `claude.plugin.root` and timers through `claude.clock` ([jobs.md](jobs.md)).
43
+ Mod code runs inside Claude Code's hooks environment, not in Node or Bun. It has no Node or Bun modules, no DOM, and no `import()`: Claude Code refuses a module that holds one. `fs` reaches files, `process` runs programs, `http` reaches the network, and `sessions` reads the conversations, each an import from `mod.js` ([mod.md](mod.md)). `mod.folder` is the plugin's own folder.
44
44
 
45
45
  `cmod check` fails a mod that breaks this:
46
46
 
@@ -57,15 +57,16 @@ A program the mod needs lives as source in `cli/`. `cmod publish` builds it and
57
57
  | Question | File |
58
58
  | --- | --- |
59
59
  | How do I define a mod, and what can `mod` call? | [mod.md](mod.md) |
60
- | How does a mod watch something outside Claude Code, tell Claude about it, or ask a model? | [mod.md](mod.md#mod-every) |
60
+ | How does a mod watch something outside Claude Code, tell Claude about it, or ask a model? | [mod.md](mod.md#mod-every-and-mod-after), [session.md](session.md#send-to-a-conversation) |
61
61
  | How does a mod give Claude a prompt on a schedule, like `/loop`? | [jobs.md](jobs.md#schedule) |
62
62
  | How do I keep values across prompts, sessions, and projects? | [state.md](state.md) |
63
63
  | How does a person set a token, a URL, or a setting for my mod, for themselves or their team? | [options.md](options.md) |
64
64
  | Which hosts, programs, and files may my mod reach, and how does the person grant them? | [permissions.md](permissions.md) |
65
- | How does my mod find files and keep its own keys on each one, such as on a Skill? | [mod.md](mod.md#mod-metadata) |
65
+ | 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
66
  | 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`? | [mod.md](mod.md#mod-settings) |
68
- | How do I react to a Claude Code event, and what can a hook answer? | [hooks.md](hooks.md) |
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) |
68
+ | 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
+ | How do I listen to a Claude Code hook, and what can a listener answer? | [hooks.md](hooks.md) |
69
70
  | How do I draw a pane, change a row Claude Code draws, or ask the person something? | [ui.md](ui.md) |
70
71
  | 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) |
71
72
  | How does one mod call another? | [dependencies.md](dependencies.md) |
@@ -79,31 +80,34 @@ A mod imports from the files below, and every other file in this package is inte
79
80
 
80
81
  | Import | Exports | Docs |
81
82
  | --- | --- | --- |
82
- | `mod.js` | `defineMod`, `ModDefinition`, `Mod`, `JobContext`, `Job`, `ProgressStep`, `PaneHandle`, `longestMs`, `messageOf` | [mod.md](mod.md), [jobs.md](jobs.md), [ui.md](ui.md) |
83
- | `mod.js` | `Claude`, `ModClaude`, `RoutedEvent`, `RoutedHook`, `ToolCalls` | [jobs.md](jobs.md) |
84
- | `mod.js` | `FileMatch`, `Metadata`, `ScrollArgs` | [mod.md](mod.md), [ui.md](ui.md) |
85
- | `installer.js` | `defineStep`, `Step` | [install-steps.md](install-steps.md#steps-in-claude-code) |
86
- | `mod.js` | `ModEvent`, `ModHook`, `HookOptions`, `HookInput`, `HookAnswer` | [hooks.md](hooks.md) |
83
+ | `mod.js` | `defineMod`, `ModDefinition`, `Mod`, `Disposable`, `Permissions`, `Allowed`, `JobContext`, `Job`, `PaneHandle`, `longestMs`, `messageOf` | [mod.md](mod.md), [jobs.md](jobs.md) |
84
+ | `mod.js` | `settings`, `SettingsFile`, `model`, `fs`, `Entry`, `Stat`, `FileMatch`, `metadata`, `Metadata`, `process`, `http`, `Imports` | [mod.md](mod.md) |
85
+ | `mod.js` | `sessions`, `Sessions`, `Session`, `SessionScope`, `SessionSend`, `SessionEvent`, `SessionPayload`, `TaskStartArgs`, `Usage`, `Matcher` | [session.md](session.md) |
86
+ | `mod.js` | `Message`, `MessageType`, `Prompt`, `Note`, `Reply`, `ToolCall`, `ToolResult`, `Skill`, `Document`, `Notice`, `Sender`, `SenderType`, `Person`, `Schedule`, `Channel`, `Sdk` | [session.md](session.md#message) |
87
+ | `mod.js` | `Context`, `ContextStart`, `ContextEnd`, `Turn`, `TurnStart`, `TurnEnd`, `Goal`, `GoalChange`, `Task`, `TaskStart`, `TaskEnd` | [session.md](session.md#turn-context-goal-and-task) |
88
+ | `mod.js` | `Hook`, `HookInput`, `HookListener`, `HookOptions`, `HookAnswer`, and one constant per hook, from `SessionStart` to `FileChanged` | [hooks.md](hooks.md) |
89
+ | `mod.js` | `Toast`, `ToastId`, `ToastType`, `ToastOptions` | [ui.md](ui.md#toasts) |
90
+ | `mod.js` | `Place`, `PlaceProps`, `ToolGroup`, `ToolProgress`, `CommandOutput`, `AskUserQuestion`, `Spinner`, `TurnDuration`, `SessionMode`, `PromptHint`, `AbovePrompt` | [ui.md](ui.md#places-change-a-row-claude-code-draws) |
87
91
  | `mod.js` | `notInstalled` | [dependencies.md](dependencies.md) |
92
+ | `installer.js` | `defineStep`, `Step` | [install-steps.md](install-steps.md#steps-in-claude-code) |
88
93
  | `register.js` | `registerMod`, `registerPermissionCheck`, `registeredMod` | [mod.md](mod.md) |
89
- | `options.js` | `option`, `Option`, `Options`, `OptionValues`, `OptionValue`, `OptionKind`, `UserConfigField`, `userConfigOf` | [options.md](options.md) |
94
+ | `options.js` | `option`, `Option`, `Options`, `OptionValues`, `OptionValue`, `OptionType`, `UserConfigField`, `userConfigOf` | [options.md](options.md) |
90
95
  | `shell.js` | `parseShell`, `ParsedShell`, `ShellCommand` | [hooks.md](hooks.md) |
91
96
  | `path.js` | `resolve`, `join`, `dirname`, `basename`, `extname`, `relative`, `isAbsolute`, `normalize` | [hooks.md](hooks.md) |
92
- | `testing.js` | `testMod`, `TestOptions`, `TestInput`, `TestedMod`, `Fakes`, `Shown`, `TestCall` | [testing.md](testing.md) |
97
+ | `testing.js` | `testMod`, `TestOptions`, `TestInput`, `TestedMod`, `Fakes`, `Shown`, `ShownToast`, `TestCall`, `FakeSession`, `MessageDraft` | [testing.md](testing.md) |
93
98
  | `ui/define-pane.js` | `definePane`, `Pane` | [ui.md](ui.md) |
94
99
  | `ui/elements.js` | `Box`, `Text`, `Button`, `Link`, `Code`, `Markdown`, `Input`, `Select`, `Image`, `drawWith` | [ui.md](ui.md) |
95
- | `ui/components.js` | `Tabs`, `Tab`, `Split`, `Panel`, `Toggle`, `Tooltip`, `Help`, `Dialog`, `Pagination`, `ProgressBar` | [ui.md](ui.md) |
96
- | `ui/slots.js` | `slots`, `Slot`, `SlotProps` | [ui.md](ui.md) |
97
- | `ui/markdown.js` | `markdownSlots`, `markdownBlocks`, `MarkdownKind`, `MarkdownReader`, `MarkdownPiece` | [ui.md](ui.md) |
98
- | `jobs/slash-command.js` | `slashCommand`, `Reply` | [jobs.md](jobs.md) |
100
+ | `ui/components.js` | `Tabs`, `Tab`, `Split`, `Panel`, `Toggle`, `Tooltip`, `Help`, `Dialog`, `Pagination`, `Progress` | [ui.md](ui.md) |
101
+ | `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) |
102
+ | `jobs/slash-command.js` | `slashCommand`, `CommandReply` | [jobs.md](jobs.md) |
99
103
  | `jobs/tool.js` | `tool` | [jobs.md](jobs.md) |
100
- | `jobs/permissions.js` | `permissions`, `Rule`, `PermissionRules`, `Target`, `CommandCall`, `FileCall`, `FetchCall`, `ToolCall` | [jobs.md](jobs.md) |
104
+ | `jobs/permissions.js` | `permissions`, `Rule`, `PermissionRules`, `Target`, `CommandCall`, `FileCall`, `FetchCall`, `Call` | [jobs.md](jobs.md) |
101
105
  | `jobs/check.js` | `check` | [jobs.md](jobs.md) |
102
106
  | `jobs/prompt.js` | `prompt` | [jobs.md](jobs.md) |
103
- | `jobs/schedule.js` | `schedule`, `Schedule` | [jobs.md](jobs.md) |
104
- | `jobs/status-line.js` | `statusLine`, `Usage` | [jobs.md](jobs.md) |
107
+ | `jobs/schedule.js` | `schedule`, `ScheduledPrompt` | [jobs.md](jobs.md) |
108
+ | `jobs/status-line.js` | `statusLine` | [jobs.md](jobs.md) |
105
109
  | `jobs/program.js` | `program`, `Program` | [jobs.md](jobs.md) |
106
- | `jobs/context.js` | `afterCall`, `modWithin`, `modOf`, `workspaceReader` | [jobs.md](jobs.md) |
110
+ | `jobs/context.js` | `afterCall`, `inFolder`, `withinDeadline`, `workspaceReader` | [jobs.md](jobs.md#the-building-blocks) |
107
111
  | `jobs/permissions/decide-permission.js` | `decidePermission`, `stricterVerdict`, `Rule`, `PermissionRules`, `Decision`, `Verdict` | [jobs.md](jobs.md) |
108
112
  | `jobs/permissions/find-project-scope.js` | `findProjectScope`, `ProjectScope`, `Workspace` | [jobs.md](jobs.md) |
109
113
 
@@ -114,4 +118,4 @@ A mod imports from the files below, and every other file in this package is inte
114
118
 
115
119
  ## Check your work
116
120
 
117
- Run `cmod check` in the mod's folder. It installs the packages, checks the layout and the imports, validates the mod with Claude Code, type-checks it, lints it, and runs its tests. Each failure names its fix. [commands.md](commands.md) lists every step.
121
+ Run `cmod check` in the mod's folder. It installs the packages, checks the layout and the imports, validates the mod with Claude Code, compiles it against Claude Code's API, lints it, and runs its tests. Each failure names its fix. [commands.md](commands.md) lists every step.
@@ -36,7 +36,7 @@ cmod refuses a `cmod` key that breaks these rules, with the fix in the message:
36
36
 
37
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)).
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 progress line above the prompt shows the install. `Not now` (n) and Escape decline.
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
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.`
41
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.
42
42
  - `Not now` leaves the mod off and logs `<mod> is not installed. Run cmod install <mod> to install it.`
@@ -56,13 +56,14 @@ In Claude Code, the mod sets itself up through the `cmod` in cmod's programs fol
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
58
  ```ts
59
+ import { process } from '../node_modules/@cmodjs/core/mod.js'
59
60
  import { defineStep } from '../node_modules/@cmodjs/core/installer.js'
60
61
 
61
62
  const signIn = defineStep({
62
63
  id: 'sign-in',
63
64
  title: 'Sign in to GitHub',
64
65
  render: () => <Text>Run gh auth login in a terminal, then press Next.</Text>,
65
- isDone: async (mod) => (await mod.process.run(['gh', 'auth', 'status'])).exitCode === 0,
66
+ isDone: async () => (await process.run(['gh', 'auth', 'status'])).exitCode === 0,
66
67
  })
67
68
 
68
69
  export const ciWatch = defineMod({ name: 'ci-watch', installer: [signIn], setup(mod) { … } })
@@ -75,6 +76,7 @@ defineStep<State>(step: { id: string; title: string; render(mod, props): RenderE
75
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.
76
77
  - `render` draws the step's body, the way a pane's `render` does ([ui.md](ui.md)).
77
78
  - `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>`.
78
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.
79
81
 
80
82
  ## Key bindings
@@ -115,7 +117,7 @@ The install step runs again on every new version of the mod and whenever its scr
115
117
 
116
118
  - 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.
117
119
  - When the install step of an upgrade fails or is stopped, cmod keeps the version it set up before.
118
- - The failure shows on the mod's progress line with `Fix the cause, then run: cmod install <mod>`.
120
+ - The failure shows as an error toast with `Fix the cause, then retry, or run cmod install <mod> in a terminal.` and a `Retry` button that runs the install again.
119
121
 
120
122
  ## The uninstall step
121
123
 
@@ -162,7 +164,7 @@ cmod download <program> <machine> <url> <sha256> [<machine> <url> <sha256> …]
162
164
 
163
165
  An install script runs `cmod download` to fetch a program built by someone else. It picks the download for this machine, checks its SHA-256, unpacks a `.tar.gz` or a `.zip` or takes the file as it is, and moves the file named `<program>` to `$CMOD_DATA/bin/<program>`, which Claude Code then runs by its name. A download whose SHA-256 differs installs nothing. Machines are `darwin-arm64`, `darwin-x64`, `linux-arm64`, and `linux-x64`.
164
166
 
165
- `$CMOD_DATA/bin/<program>` is `${mod.dataFolder}/bin/<program>` in mod code. That folder is not on `PATH`, so mod code names the program by that full path, such as `` mod.process.run([`${mod.dataFolder}/bin/mermaid-ascii`, '--help']) ``, and so does the `command` of a `program` job.
167
+ `$CMOD_DATA/bin/<program>` is `${mod.dataFolder}/bin/<program>` in mod code. That folder is not on `PATH`, so mod code names the program by that full path, such as `` process.run([`${mod.dataFolder}/bin/mermaid-ascii`, '--help']) ``, and so does the `command` of a `program` job.
166
168
 
167
169
  ```sh
168
170
  cmod download mermaid-ascii \
@@ -192,7 +194,7 @@ A mod that builds its own command-line program keeps its source in `cli/`, and c
192
194
 
193
195
  Then:
194
196
 
195
- - `cmod link` builds the program and links this machine's build into cmod's programs folder and `~/.local/bin`.
197
+ - `cmod link` builds the program and links this machine's build into cmod's programs folder and `~/.local/bin`. When the linked checkout's version changes, the next session's setup builds it again from the checkout, so a version with no release yet still sets up.
196
198
  - `cmod publish` builds every machine's file and attaches each to the GitHub release `v<version>`, with a `SHA256SUMS` file. It refuses a `cli/` program that the `package.json` `program` key does not name.
197
199
  - On install, cmod downloads `<repository>/releases/download/v<version>/<program>-<machine>`, checks it against `SHA256SUMS`, runs `<program> --version`, and links it into cmod's programs folder and `~/.local/bin`.
198
200
  - `cmod unlink` and the uninstall remove the program and both links.
@@ -202,7 +204,7 @@ Then:
202
204
  cmod links every program it installs into its programs folder, `~/.local/share/cmod/programs/<program>` (under `$XDG_DATA_HOME` when it is set): the program `cmod.program` names, each program `cmod download` installs, and `cmod` itself. Claude Code runs the one there by its name, whatever else `PATH` holds, so a mod's `trace` wins over `/usr/bin/trace`:
203
205
 
204
206
  - The cmod plugin's `SessionStart` hook puts the programs folder first on `PATH` for every Bash command Claude runs.
205
- - `mod.process.run` and `mod.process.spawn` run `[name, ...args]` from the programs folder when it holds `name`. A path such as `/usr/bin/trace` runs as given.
207
+ - `process.run` and `process.spawn` run `[name, ...args]` from the programs folder when it holds `name`. A path such as `/usr/bin/trace` runs as given.
206
208
  - Install and uninstall steps find the programs folder on `PATH` after cmod's own `bun`.
207
209
 
208
210
  cmod also links the program into `~/.local/bin/<program>` so a terminal runs it, unless a file there is not cmod's. Setup then logs which program a terminal runs, and changes nothing outside cmod's folders.
@@ -215,7 +217,7 @@ A mod runs on macOS and Linux, on arm64 and x64, unless the `os` and `cpu` keys
215
217
  { "name": "safe-delete", "os": ["darwin"], "cpu": ["arm64"] }
216
218
  ```
217
219
 
218
- - Setup refuses a machine the mod leaves out before any script runs, with `safe-delete runs on macOS on arm64, and this is Linux, so cmod set up nothing.`, in the terminal and in the mod's progress line in Claude Code.
220
+ - Setup refuses a machine the mod leaves out before any script runs, with `safe-delete runs on macOS on arm64, and this is Linux, so cmod set up nothing.`, in the terminal and in the mod's error toast in Claude Code.
219
221
  - `cmod publish` builds the program for each machine the mod runs on.
220
222
  - `cmod check` fails when a `cmod download` line in the install step has no download for one of those machines, and when `os` or `cpu` names a value cmod does not run mods on, such as `win32`.
221
223