@cmodjs/core 0.3.0 → 1.5.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 (280) hide show
  1. package/docs/commands.md +61 -10
  2. package/docs/dependencies.md +16 -14
  3. package/docs/hooks.md +64 -29
  4. package/docs/index.md +29 -25
  5. package/docs/install-steps.md +13 -9
  6. package/docs/jobs.md +59 -111
  7. package/docs/mod.md +227 -175
  8. package/docs/options.md +2 -2
  9. package/docs/permissions.md +27 -16
  10. package/docs/session.md +311 -0
  11. package/docs/state.md +11 -7
  12. package/docs/testing.md +68 -23
  13. package/docs/ui.md +116 -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 +14 -4
  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 +178 -75
  48. package/mod.d.ts.map +1 -1
  49. package/mod.js +127 -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 +13 -8
  54. package/records.d.ts.map +1 -1
  55. package/records.js +33 -17
  56. package/register.d.ts.map +1 -1
  57. package/register.js +34 -5
  58. package/runtime/announce.d.ts +18 -0
  59. package/runtime/announce.d.ts.map +1 -0
  60. package/runtime/announce.js +68 -0
  61. package/runtime/before.d.ts +32 -0
  62. package/runtime/before.d.ts.map +1 -0
  63. package/runtime/before.js +214 -0
  64. package/runtime/bound.d.ts +4 -0
  65. package/runtime/bound.d.ts.map +1 -0
  66. package/runtime/bound.js +9 -0
  67. package/runtime/claude.d.ts +4 -2
  68. package/runtime/claude.d.ts.map +1 -1
  69. package/runtime/conversations.d.ts +17 -0
  70. package/runtime/conversations.d.ts.map +1 -0
  71. package/runtime/conversations.js +563 -0
  72. package/runtime/deadline.d.ts +0 -1
  73. package/runtime/deadline.d.ts.map +1 -1
  74. package/runtime/deadline.js +1 -1
  75. package/runtime/dependencies.d.ts +2 -0
  76. package/runtime/dependencies.d.ts.map +1 -1
  77. package/runtime/dependencies.js +10 -1
  78. package/runtime/engine.d.ts +13 -0
  79. package/runtime/engine.d.ts.map +1 -0
  80. package/runtime/engine.js +10 -0
  81. package/runtime/grants.d.ts +4 -0
  82. package/runtime/grants.d.ts.map +1 -1
  83. package/runtime/grants.js +68 -31
  84. package/runtime/hooks.d.ts +5 -4
  85. package/runtime/hooks.d.ts.map +1 -1
  86. package/runtime/hooks.js +29 -9
  87. package/runtime/installer.d.ts +1 -1
  88. package/runtime/installer.d.ts.map +1 -1
  89. package/runtime/installer.js +17 -14
  90. package/runtime/lifecycle.d.ts +3 -1
  91. package/runtime/lifecycle.d.ts.map +1 -1
  92. package/runtime/lifecycle.js +414 -212
  93. package/runtime/metadata.d.ts +3 -1
  94. package/runtime/metadata.d.ts.map +1 -1
  95. package/runtime/metadata.js +25 -14
  96. package/runtime/options.d.ts +3 -1
  97. package/runtime/options.d.ts.map +1 -1
  98. package/runtime/options.js +4 -4
  99. package/runtime/programs.d.ts +8 -0
  100. package/runtime/programs.d.ts.map +1 -1
  101. package/runtime/programs.js +39 -1
  102. package/runtime/router.d.ts +2 -1
  103. package/runtime/router.d.ts.map +1 -1
  104. package/runtime/router.js +11 -3
  105. package/runtime/sessions.d.ts +58 -0
  106. package/runtime/sessions.d.ts.map +1 -0
  107. package/runtime/sessions.js +433 -0
  108. package/runtime/state.d.ts +6 -2
  109. package/runtime/state.d.ts.map +1 -1
  110. package/runtime/state.js +46 -13
  111. package/runtime/toaster.d.ts +24 -0
  112. package/runtime/toaster.d.ts.map +1 -0
  113. package/runtime/toaster.js +286 -0
  114. package/runtime/ui.d.ts +6 -17
  115. package/runtime/ui.d.ts.map +1 -1
  116. package/runtime/ui.js +62 -118
  117. package/session/context/context.d.ts +12 -0
  118. package/session/context/context.d.ts.map +1 -0
  119. package/session/context/context.js +2 -0
  120. package/session/context/turn/message/document.d.ts +7 -0
  121. package/session/context/turn/message/document.d.ts.map +1 -0
  122. package/session/context/turn/message/document.js +1 -0
  123. package/session/context/turn/message/message.d.ts +55 -0
  124. package/session/context/turn/message/message.d.ts.map +1 -0
  125. package/session/context/turn/message/message.js +178 -0
  126. package/session/context/turn/message/note.d.ts +8 -0
  127. package/session/context/turn/message/note.d.ts.map +1 -0
  128. package/session/context/turn/message/note.js +1 -0
  129. package/session/context/turn/message/notice.d.ts +6 -0
  130. package/session/context/turn/message/notice.d.ts.map +1 -0
  131. package/session/context/turn/message/notice.js +1 -0
  132. package/session/context/turn/message/prompt.d.ts +8 -0
  133. package/session/context/turn/message/prompt.d.ts.map +1 -0
  134. package/session/context/turn/message/prompt.js +1 -0
  135. package/session/context/turn/message/reply.d.ts +6 -0
  136. package/session/context/turn/message/reply.d.ts.map +1 -0
  137. package/session/context/turn/message/reply.js +1 -0
  138. package/session/context/turn/message/sender.d.ts +14 -0
  139. package/session/context/turn/message/sender.d.ts.map +1 -0
  140. package/session/context/turn/message/sender.js +30 -0
  141. package/session/context/turn/message/skill.d.ts +7 -0
  142. package/session/context/turn/message/skill.d.ts.map +1 -0
  143. package/session/context/turn/message/skill.js +1 -0
  144. package/session/context/turn/message/tool-call.d.ts +9 -0
  145. package/session/context/turn/message/tool-call.d.ts.map +1 -0
  146. package/session/context/turn/message/tool-call.js +1 -0
  147. package/session/context/turn/message/tool-result.d.ts +8 -0
  148. package/session/context/turn/message/tool-result.d.ts.map +1 -0
  149. package/session/context/turn/message/tool-result.js +1 -0
  150. package/session/context/turn/turn.d.ts +13 -0
  151. package/session/context/turn/turn.d.ts.map +1 -0
  152. package/session/context/turn/turn.js +2 -0
  153. package/session/conversation.d.ts +73 -0
  154. package/session/conversation.d.ts.map +1 -0
  155. package/session/conversation.js +146 -0
  156. package/session/goal/goal.d.ts +11 -0
  157. package/session/goal/goal.d.ts.map +1 -0
  158. package/session/goal/goal.js +1 -0
  159. package/session/goal/task.d.ts +16 -0
  160. package/session/goal/task.d.ts.map +1 -0
  161. package/session/goal/task.js +3 -0
  162. package/session/session.d.ts +158 -0
  163. package/session/session.d.ts.map +1 -0
  164. package/session/session.js +2 -0
  165. package/src/jobs/check.ts +7 -4
  166. package/src/jobs/context.ts +24 -82
  167. package/src/jobs/permissions/decide-permission.ts +10 -8
  168. package/src/jobs/permissions/find-project-scope.ts +8 -14
  169. package/src/jobs/permissions/match-target.ts +3 -3
  170. package/src/jobs/permissions.ts +8 -6
  171. package/src/jobs/program.ts +13 -4
  172. package/src/jobs/prompt.ts +21 -24
  173. package/src/jobs/schedule.ts +14 -14
  174. package/src/jobs/slash-command.ts +20 -14
  175. package/src/jobs/status-line.ts +9 -21
  176. package/src/jobs/tool.ts +17 -11
  177. package/src/mod.ts +252 -97
  178. package/src/options.ts +22 -22
  179. package/src/records.ts +40 -23
  180. package/src/register.ts +34 -5
  181. package/src/runtime/announce.ts +76 -0
  182. package/src/runtime/before.ts +200 -0
  183. package/src/runtime/bound.ts +12 -0
  184. package/src/runtime/claude.ts +7 -2
  185. package/src/runtime/conversations.ts +579 -0
  186. package/src/runtime/deadline.ts +2 -2
  187. package/src/runtime/dependencies.ts +13 -1
  188. package/src/runtime/engine.ts +23 -0
  189. package/src/runtime/grants.ts +69 -29
  190. package/src/runtime/hooks.ts +39 -14
  191. package/src/runtime/installer.ts +16 -14
  192. package/src/runtime/lifecycle.ts +413 -201
  193. package/src/runtime/metadata.ts +37 -14
  194. package/src/runtime/options.ts +19 -4
  195. package/src/runtime/programs.ts +40 -1
  196. package/src/runtime/router.ts +11 -4
  197. package/src/runtime/sessions.ts +447 -0
  198. package/src/runtime/state.ts +50 -14
  199. package/src/runtime/toaster.ts +334 -0
  200. package/src/runtime/ui.ts +65 -143
  201. package/src/session/context/context.ts +14 -0
  202. package/src/session/context/turn/message/document.ts +8 -0
  203. package/src/session/context/turn/message/message.ts +203 -0
  204. package/src/session/context/turn/message/note.ts +9 -0
  205. package/src/session/context/turn/message/notice.ts +7 -0
  206. package/src/session/context/turn/message/prompt.ts +9 -0
  207. package/src/session/context/turn/message/reply.ts +7 -0
  208. package/src/session/context/turn/message/sender.ts +38 -0
  209. package/src/session/context/turn/message/skill.ts +8 -0
  210. package/src/session/context/turn/message/tool-call.ts +10 -0
  211. package/src/session/context/turn/message/tool-result.ts +9 -0
  212. package/src/session/context/turn/turn.ts +15 -0
  213. package/src/session/conversation.ts +194 -0
  214. package/src/session/goal/goal.ts +10 -0
  215. package/src/session/goal/task.ts +19 -0
  216. package/src/session/session.ts +141 -0
  217. package/src/testing/fake-claude.ts +25 -17
  218. package/src/testing/fake-elements.ts +3 -2
  219. package/src/testing/fake-files.ts +1 -1
  220. package/src/testing/fake-sessions.ts +277 -0
  221. package/src/testing.ts +138 -35
  222. package/src/ui/components.ts +3 -3
  223. package/src/ui/elements.ts +22 -8
  224. package/src/ui/markdown.ts +22 -26
  225. package/src/ui/places.ts +49 -0
  226. package/src/utils/call-effects.ts +5 -5
  227. package/src/utils/changelog.ts +32 -0
  228. package/src/utils/disposable.ts +3 -0
  229. package/src/utils/matcher.ts +37 -0
  230. package/src/utils/metadata.ts +17 -15
  231. package/src/utils/parse-shell.ts +9 -8
  232. package/src/utils/paths.ts +21 -4
  233. package/src/utils/text.ts +4 -0
  234. package/testing/fake-claude.d.ts +5 -1
  235. package/testing/fake-claude.d.ts.map +1 -1
  236. package/testing/fake-claude.js +21 -12
  237. package/testing/fake-elements.js +4 -2
  238. package/testing/fake-files.js +1 -1
  239. package/testing/fake-sessions.d.ts +81 -0
  240. package/testing/fake-sessions.d.ts.map +1 -0
  241. package/testing/fake-sessions.js +249 -0
  242. package/testing.d.ts +34 -13
  243. package/testing.d.ts.map +1 -1
  244. package/testing.js +115 -23
  245. package/ui/components.d.ts +2 -3
  246. package/ui/components.d.ts.map +1 -1
  247. package/ui/components.js +3 -3
  248. package/ui/elements.d.ts.map +1 -1
  249. package/ui/elements.js +19 -9
  250. package/ui/markdown.d.ts +35 -33
  251. package/ui/markdown.d.ts.map +1 -1
  252. package/ui/markdown.js +14 -16
  253. package/ui/places.d.ts +43 -0
  254. package/ui/places.d.ts.map +1 -0
  255. package/ui/places.js +31 -0
  256. package/utils/call-effects.d.ts +2 -1
  257. package/utils/call-effects.d.ts.map +1 -1
  258. package/utils/call-effects.js +3 -3
  259. package/utils/changelog.d.ts +9 -0
  260. package/utils/changelog.d.ts.map +1 -0
  261. package/utils/changelog.js +29 -0
  262. package/utils/disposable.d.ts +4 -0
  263. package/utils/disposable.d.ts.map +1 -0
  264. package/utils/disposable.js +1 -0
  265. package/utils/matcher.d.ts +13 -0
  266. package/utils/matcher.d.ts.map +1 -0
  267. package/utils/matcher.js +32 -0
  268. package/utils/metadata.d.ts +3 -3
  269. package/utils/metadata.js +18 -14
  270. package/utils/parse-shell.js +9 -7
  271. package/utils/paths.d.ts +4 -1
  272. package/utils/paths.d.ts.map +1 -1
  273. package/utils/paths.js +19 -4
  274. package/utils/text.d.ts +1 -0
  275. package/utils/text.d.ts.map +1 -1
  276. package/utils/text.js +3 -0
  277. package/src/ui/slots.ts +0 -36
  278. package/ui/slots.d.ts +0 -208
  279. package/ui/slots.d.ts.map +0 -1
  280. package/ui/slots.js +0 -19
@@ -36,10 +36,10 @@ 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.
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.`
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
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
- - `Not now` leaves the mod off and logs `<mod> is not installed. Run cmod install <mod> to install it.`
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)).
45
45
 
@@ -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,9 @@ 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 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`.
122
+ - Before each try, cmod reads the mod's install record, so a mod another session installed starts without running its install again.
119
123
 
120
124
  ## The uninstall step
121
125
 
@@ -162,7 +166,7 @@ cmod download <program> <machine> <url> <sha256> [<machine> <url> <sha256> …]
162
166
 
163
167
  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
168
 
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.
169
+ `$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
170
 
167
171
  ```sh
168
172
  cmod download mermaid-ascii \
@@ -192,7 +196,7 @@ A mod that builds its own command-line program keeps its source in `cli/`, and c
192
196
 
193
197
  Then:
194
198
 
195
- - `cmod link` builds the program and links this machine's build into cmod's programs folder and `~/.local/bin`.
199
+ - `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
200
  - `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
201
  - 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
202
  - `cmod unlink` and the uninstall remove the program and both links.
@@ -202,7 +206,7 @@ Then:
202
206
  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
207
 
204
208
  - 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.
209
+ - `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
210
  - Install and uninstall steps find the programs folder on `PATH` after cmod's own `bun`.
207
211
 
208
212
  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 +219,7 @@ A mod runs on macOS and Linux, on arm64 and x64, unless the `os` and `cpu` keys
215
219
  { "name": "safe-delete", "os": ["darwin"], "cpu": ["arm64"] }
216
220
  ```
217
221
 
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.
222
+ - 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
223
  - `cmod publish` builds the program for each machine the mod runs on.
220
224
  - `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
225
 
package/docs/jobs.md CHANGED
@@ -27,18 +27,20 @@ Each job announces itself. The first time a version starts, Claude Mod Manager (
27
27
 
28
28
  ## Deadlines
29
29
 
30
- Inside a job, the `mod` a function receives fails each call that runs past the job's deadline, with `<call> passed the <n> s deadline of <job>`, such as `mod.fs.read passed the 30 s deadline of tool`.
30
+ A job gives each function it calls a deadline. The whole function must answer within it, every call it makes included, or the job goes on without its answer and names what passed the deadline, as in `the open_ticket tool passed the 30 s deadline of tool`.
31
31
 
32
- | Job and function | Deadline of each `mod.fs`, `mod.http`, `mod.dependencies`, and `mod.process.spawn` call | `mod.process.run` |
32
+ | Job and function | Deadline | Past the deadline |
33
33
  | --- | --- | --- |
34
- | `slashCommand`'s `reply` | 30 s | 30 s, or `timeoutMs` up to 10 minutes |
35
- | `tool`'s `execute` | 30 s | 30 s, or `timeoutMs` up to 10 minutes |
36
- | `prompt`'s `prompt` | 5 s | 5 s, or `timeoutMs` up to 10 minutes |
37
- | `statusLine`'s `text` | 2 s | 2 s at most |
38
- | `permissions`' `when` | 2 s | 2 s at most |
39
- | `check`'s `run` | none: `run` is the check's only call | `timeoutMs`, 60 s by default, 10 minutes at most |
34
+ | `slashCommand`'s `reply` | 30 s | The command shows `/<name> failed: /<name> passed the 30 s deadline of slashCommand`. |
35
+ | `tool`'s `execute` | 30 s | The call is denied with `The <name> tool failed: the <name> tool passed the 30 s deadline of tool`. |
36
+ | `prompt`'s `prompt` | 5 s | The prompt adds nothing, and the reason goes to Claude Code's debug log. |
37
+ | `statusLine`'s `text` | 2 s | The status line keeps its last text, and the reason goes to Claude Code's debug log. |
38
+ | `permissions`' `when` | 2 s | The rule matches, and its reason adds `Its when check failed: it passed the 2 s deadline of permissions`. |
39
+ | `check`'s `run` | `timeoutMs`, 60 s by default, 10 minutes at most | The call's result says the command passed the deadline. |
40
40
 
41
- Outside a job, in a hook or a render, a `mod.dependencies` call fails after 30 s, and the other calls have only Claude Code's own limits ([mod.md](mod.md)).
41
+ A deadline stops waiting for the function, not the work it started: a program the function spawned runs on until it exits or the mod stops. A function that waits on something slow keeps it short, such as a `process.run` with its own `timeoutMs`.
42
+
43
+ Outside a job, in a listener or a render, a `mod.dependencies` call fails after 30 s, and the other calls have only Claude Code's own limits ([mod.md](mod.md)).
42
44
 
43
45
  ## slashCommand
44
46
 
@@ -48,17 +50,17 @@ slashCommand<State>(options: {
48
50
  readonly description: string
49
51
  readonly argumentHint?: string
50
52
  readonly immediate?: true
51
- readonly reply: (input: { args: string; positionals: string[] }, mod: Mod<State>) => Reply | void | Promise<Reply | void>
52
- }): Job<void, State>
53
+ readonly reply: (input: { args: string; positionals: string[] }, mod: Mod<State>) => CommandReply | void | Promise<CommandReply | void>
54
+ }): Job<Disposable, State>
53
55
 
54
- type Reply = string | { text?: string; context?: string } | undefined
56
+ type CommandReply = string | { text?: string; context?: string } | undefined
55
57
  ```
56
58
 
57
59
  - `name` is the command without the slash: 1 to 64 letters, digits, `_`, or `-`. The person types `/<name>`. Another name throws `<mod>: "<name>" is not a slash command name.`
58
60
  - `description` is the line the typeahead and `/help` show.
59
61
  - `argumentHint` is drawn dim after the name, such as `[path]`.
60
62
  - `immediate: true` runs the command at once while Claude is still working. Without it, the command waits for the turn to end.
61
- - A command lasts for the session. Adding a command with a name the session already has replaces it, and Claude Code has no call that removes one, so a command whose source is gone answers that it no longer applies.
63
+ - A command lasts for the session. Adding a command with a name the session already has replaces it, and Claude Code has no call that removes one, so a command whose source is gone answers that it no longer applies. `dispose()` on the handle stops the mod answering the command.
62
64
  - A key the mod binds in its `package.json` `"cmod": { "keys": { "shift+tab": "/mode" } }` runs the command ([install-steps.md](install-steps.md#key-bindings)). The command of a key takes `immediate: true`, so the key works while Claude works, and a `reply` that returns `undefined`, so a press adds no row to the conversation. It changes `mod.state`, and a pane or band that shows the state is how the person sees the press. cmod logs `<mod>: <key> runs /<name>, which waits for Claude's turn to end and adds a row to the conversation. Give /<name> immediate: true.` for a key whose command lacks it.
63
65
 
64
66
  ```ts
@@ -101,10 +103,10 @@ tool<Schema, State>(options: {
101
103
  readonly description: string
102
104
  readonly inputSchema?: Schema
103
105
  readonly execute: (input: FromSchema<Schema>, mod: Mod<State>) => unknown
104
- }): Job<{ readonly name: string }, State>
106
+ }): Job<Disposable & { readonly name: string }, State>
105
107
  ```
106
108
 
107
- - `name` is 1 to 64 letters, digits, `_`, or `-`. Claude sees the tool as `mcp__<plugin name>__<name>`, and the handle's `name` is that full name.
109
+ - `name` is 1 to 64 letters, digits, `_`, or `-`. Claude sees the tool as `mcp__<plugin name>__<name>`, and the handle's `name` is that full name. `dispose()` on the handle stops the mod answering the tool.
108
110
  - `description` tells Claude what the tool does.
109
111
  - `inputSchema` is a JSON Schema with `type: 'object'`. It types `execute`'s `input`, so write it inline. cmod checks each call's input against it, draft 2020-12, and denies an input that does not fit with the list of its errors. Its properties cannot be named `tool`, `tool_use_id`, `consent`, or `agentId`, which Claude Code keeps for itself.
110
112
  - `execute` returns the result Claude reads. A `string` and an array of content blocks go to Claude as they are. Any other value goes as JSON. `undefined` returns nothing.
@@ -175,7 +177,7 @@ permissions<State>(rules: PermissionRules<State> | ((state: State) => Permission
175
177
  type PermissionRules<State> = { deny?: Rule<State>[]; ask?: Rule<State>[] }
176
178
 
177
179
  type Rule<State> = Target & {
178
- when?: (call: <the call of the target's key>, mod: Mod<State>) => boolean | Promise<boolean>
180
+ when?: (call: <the call of the target's key> & { folder: string }, mod: Mod<State>) => boolean | Promise<boolean>
179
181
  reason?: string
180
182
  }
181
183
  ```
@@ -189,16 +191,16 @@ type Rule<State> = Target & {
189
191
  - `permissions({})` throws `permissions: give it a deny or an ask rule, or remove it from setup.`
190
192
  - A mod that uses `permissions` calls `registerPermissionCheck(addHook)` after `registerMod` in `hooks/register.ts` ([mod.md](mod.md)). Without it the mod does not start.
191
193
 
192
- `when` gets the call, typed by the target's key. `jobs/permissions.js` exports each type, and `ToolCall` is any of them:
194
+ `when` gets the call, typed by the target's key. `jobs/permissions.js` exports each type, and `Call` is any of them:
193
195
 
194
- | Key | Call type | Fields beyond `tool`, `input`, `agentId`, and `agentType` |
196
+ | Key | Call type | Fields beyond `tool`, `input`, `agentId`, `agentType`, and `folder` |
195
197
  | --- | --- | --- |
196
198
  | `command` | `CommandCall` | `commands`, each `[program, ...arguments]`, and `isFullyParsed` |
197
199
  | `read`, `write` | `FileCall` | `path`, and for an `Edit` or a `Write`, `content`, the new text, and `previousContent`, the text before |
198
200
  | `fetch` | `FetchCall` | `url` |
199
- | `subagent`, `tool` | `ToolCall` | none |
201
+ | `subagent`, `tool` | `Call` | none |
200
202
 
201
- `tool` is the tool's name, such as `Bash`, and `input` is its input as Claude Code sent it, such as `{ command, description }` for `Bash`, so a rule reads any field cmod does not parse. `agentId` and `agentType` name the subagent that makes the call. In the main conversation `agentId` is absent, and `agentType` is the agent the session started as, when it started as one. `jobs/permissions.js` also exports `Target`, `Rule`, and `PermissionRules`.
203
+ `tool` is the tool's name, such as `Bash`, and `input` is its input as Claude Code sent it, such as `{ command, description }` for `Bash`, so a rule reads any field cmod does not parse. `agentId` and `agentType` name the subagent that makes the call. In the main conversation `agentId` is absent, and `agentType` is the agent the session started as, when it started as one. `folder` is the root of the git work tree that holds the call's file or folder, or that folder itself outside one, so `process.run(['git', 'status'], { cwd: call.folder })` asks the repository the call works in. `jobs/permissions.js` also exports `Target`, `Rule`, and `PermissionRules`.
202
204
 
203
205
  ```ts
204
206
  import { defineMod } from '../node_modules/@cmodjs/core/mod.js'
@@ -256,7 +258,7 @@ It needs `"prompt": true`. Without the grant it adds nothing and logs once which
256
258
  ```ts
257
259
  prompt<State>(options: {
258
260
  name: string
259
- prompt: string | ((input: { userPrompt?: string; call?: ToolCall }, mod: Mod<State>) => string | undefined | Promise<string | undefined>)
261
+ prompt: string | ((input: { userPrompt?: string; call?: Call & { folder: string } }, mod: Mod<State>) => string | undefined | Promise<string | undefined>)
260
262
  when?: RegExp | ((userPrompt: string) => boolean)
261
263
  after?: Target | Target[]
262
264
  }): Job<void, State>
@@ -268,16 +270,16 @@ prompt<State>(options: {
268
270
  | --- | --- |
269
271
  | neither | At the start of each conversation, as a block of context named `name`. A block of that name already in the context wins. Then again whenever `mod.state` changes and the text differs from what Claude last got. |
270
272
  | `when` | With each prompt the person sends whose text matches the `RegExp`, or for which the function returns true. `input.userPrompt` is the prompt. |
271
- | `after` | After each call that matches a target and succeeds, with the call's result. `input.call` is the matched call. |
273
+ | `after` | After each call that matches a target and succeeds, with the call's result. `input.call` is the matched call, with its `folder` as `permissions` gives it. |
272
274
 
273
275
  - With `when` or `after`, the text goes to Claude under the heading `# <name>`.
274
- - With neither, the text follows the mod's state. After a change to `mod.state`, cmod calls `prompt` again, and when the text differs from what Claude last got, it adds `# <name>` and the new text to the conversation as a note Claude reads on its next call to the model, through `mod.session.append`. Several changes in a row give one note with the last text. A note waits until the conversation has started, so a state change before the first prompt changes only the first block.
276
+ - With neither, the text follows the mod's state. After a change to `mod.state`, cmod calls `prompt` again, and when the text differs from what Claude last got, it adds `# <name>` and the new text to the conversation as a `Note` Claude reads on its next call to the model, as `sessions.current.send` does ([session.md](session.md#send-to-a-conversation)). Several changes in a row give one note with the last text. A note waits until the conversation has started, so a state change before the first prompt changes only the first block.
275
277
  - A `prompt` function that returns `undefined` or `''` adds nothing. One that throws adds nothing, and the error goes to Claude Code's debug log.
276
278
  - `name` cannot be blank. `when` and `after` together throw: add a second `prompt` for the other trigger. Two prompts with one name in a mod throw.
277
279
  - A `when` prompt runs on `prompt.submit` and adds its text to the event it passes down. In a test, `fire('prompt.submit', …)` returns that text in the answer's `context`. [testing.md](testing.md#test-a-prompt-with-when) shows how.
278
280
 
279
281
  ```ts
280
- import { defineMod } from '../node_modules/@cmodjs/core/mod.js'
282
+ import { defineMod, fs, sessions } from '../node_modules/@cmodjs/core/mod.js'
281
283
  import { prompt } from '../node_modules/@cmodjs/core/jobs/prompt.js'
282
284
 
283
285
  export const conventions = defineMod({
@@ -287,7 +289,7 @@ export const conventions = defineMod({
287
289
  mod.use(prompt({ name: 'Commit messages', prompt: 'Write commit messages in the imperative mood.' }))
288
290
  mod.use(prompt({ name: 'Mode', prompt: (_input, mod) => `You are in ${mod.state.session.mode} mode.` }))
289
291
  mod.use(prompt({ name: 'Migrations', when: /\bmigrat/i, prompt: 'Every migration has a down step.' }))
290
- mod.use(prompt({ name: 'Schema', after: { write: 'db/schema.sql' }, prompt: async (_input, mod) => `The schema is now:\n${await mod.fs.read(`${mod.projectRoot}/db/schema.sql`)}` }))
292
+ mod.use(prompt({ name: 'Schema', after: { write: 'db/schema.sql' }, prompt: async () => `The schema is now:\n${await fs.read(`${sessions.current.project}/db/schema.sql`)}` }))
291
293
  },
292
294
  })
293
295
  ```
@@ -297,9 +299,9 @@ export const conventions = defineMod({
297
299
  It needs `"prompt": true`, because each cron starts a turn ([permissions.md](permissions.md)).
298
300
 
299
301
  ```ts
300
- schedule<State>(entry: Schedule | ((state: State) => Schedule | undefined)): Job<void, State>
302
+ schedule<State>(entry: ScheduledPrompt | ((state: State) => ScheduledPrompt | undefined)): Job<void, State>
301
303
 
302
- type Schedule = { readonly cron: string; readonly prompt: string }
304
+ type ScheduledPrompt = { readonly cron: string; readonly prompt: string }
303
305
  ```
304
306
 
305
307
  `schedule` gives Claude a prompt on a schedule, through the cron jobs Claude Code keeps for `/loop`. `cron` is a standard 5-field cron in local time, such as `*/10 * * * *` for every 10 minutes. At each fire Claude Code shows one `Running scheduled task` row and Claude answers the prompt.
@@ -368,13 +370,13 @@ type Program = {
368
370
  `program` runs a background program, such as a preview server, and hands the mod its address.
369
371
 
370
372
  - `command` is the program on `PATH` and its arguments. `environment` sets variables for it. An empty `command` throws.
371
- - The program's first line on standard output must be its address: `127.0.0.1:<port>`, or `unix:<absolute socket path>`. `ready()` then resolves `{ url: 'http://127.0.0.1:<port>' }`, or `{ url: 'http://localhost', socketPath }`. Pass both to `mod.http.fetch`.
373
+ - The program's first line on standard output must be its address: `127.0.0.1:<port>`, or `unix:<absolute socket path>`. `ready()` then resolves `{ url: 'http://127.0.0.1:<port>' }`, or `{ url: 'http://localhost', socketPath }`. Pass both to `http.fetch`, which reaches that address with no `network` permission while the program runs.
372
374
  - In an interactive session the program starts with the mod. Otherwise it starts at the first `ready()`.
373
375
  - When the program exits, cmod starts it again after 1, 2, 4, 8, and 16 seconds. After 5 failed tries, or a first line that is not an address, the state is `fatal`, `ready()` rejects, and cmod logs `<mod>: the <program> program stopped: <reason>`.
374
376
  - A program that ships with the mod gets onto `PATH` through the `program` key of the mod's `package.json` ([install-steps.md](install-steps.md)). A program an install step fetched with `cmod download` is not on `PATH`, so `command` names it by its full path, `${mod.dataFolder}/bin/<program>`.
375
377
 
376
378
  ```ts
377
- import { defineMod } from '../node_modules/@cmodjs/core/mod.js'
379
+ import { defineMod, http } from '../node_modules/@cmodjs/core/mod.js'
378
380
  import { program } from '../node_modules/@cmodjs/core/jobs/program.js'
379
381
  import { slashCommand } from '../node_modules/@cmodjs/core/jobs/slash-command.js'
380
382
 
@@ -386,9 +388,9 @@ export const preview = defineMod({
386
388
  slashCommand({
387
389
  name: 'preview-status',
388
390
  description: 'Ask the preview server for its status',
389
- reply: async (_input, mod) => {
391
+ reply: async () => {
390
392
  const { url, socketPath } = await server.ready()
391
- const response = await mod.http.fetch(`${url}/status`, socketPath === undefined ? {} : { socketPath })
393
+ const response = await http.fetch(`${url}/status`, socketPath === undefined ? {} : { socketPath })
392
394
  return response.text
393
395
  },
394
396
  }),
@@ -405,96 +407,42 @@ A `Job` is a function. `mod.use` calls it with a `JobContext` and returns what i
405
407
  type JobContext<State> = {
406
408
  readonly mod: Mod<State>
407
409
  announce(feature: string): void
408
- reserveName(kind: string, name: string, taken: string): void
409
- readonly toolCalls: ToolCalls
410
+ reserveName(type: string, name: string, taken: string): Disposable
410
411
  }
411
412
  ```
412
413
 
413
- A job reaches Claude Code through `mod.claude`, the same member every hook has:
414
-
415
- - `mod.claude` holds Claude Code's own calls. [The claude members](#the-claude-members) lists each one. Each call checks the permission it needs, as `mod`'s own members do ([permissions.md](permissions.md)).
416
- - `mod.claude.store` is where cmod saves `mod.state`, under keys that start with `<mod>.`, such as `<mod>.<key>`. A job of your own never writes those keys, or it changes the mod's state behind cmod's back.
417
- - `mod.claude.on` adds a Claude Code hook-module handler, `(e, next) => …`, on a `classic.<ModEvent>` event or on `tool.check`, `tool.call`, `prompt.submit`, `prompt.context`, `command.run`, `session.measure`, `skill.prompt`, `ui.render`, `ui.press`, `ui.scroll`, `ui.close`, or `cmod.call`. The handler calls `next(e)` to pass the event on. cmod checks what it answers against the person's grants, part by part ([permissions.md](permissions.md#when-a-hook-answer-has-no-grant)). A job with a handler on `tool.check` or `classic.PermissionRequest` needs `registerPermissionCheck` in `hooks/register.ts` ([mod.md](mod.md)).
418
-
419
- The context adds what only a job needs:
420
-
414
+ - `mod` is the mod that uses the job. A job builds on `mod.on`, `mod.every`, `mod.ui`, and the imports from `mod.js`, as `setup` does ([mod.md](mod.md)).
421
415
  - `announce` adds a phrase to the list cmod logs when the mod first starts.
422
- - `reserveName` throws `taken` when the mod already reserved that `kind` and `name`. A job calls it to refuse a duplicate.
423
- - `toolCalls.agentOf(toolUseId)` resolves `{ agentId?, agentType? }`, the subagent behind a call.
424
- - `toolCalls.cwdOf(toolUseId)` returns the working folder when the call ran, or `undefined` once its `PostToolUse` or `PostToolUseFailure` has run. cmod keeps the folders of the last 100 calls.
416
+ - `reserveName` throws `taken` when the mod already reserved that `type` and `name`, so a job refuses a duplicate. `dispose()` on what it returns frees the name again.
425
417
 
426
- ```ts
427
- import { defineMod, type Job } from '../node_modules/@cmodjs/core/mod.js'
418
+ A job that hands back a feature the mod turns off returns a `Disposable`, as `slashCommand` and `tool` do:
428
419
 
429
- const clock: Job<{ now(): Promise<number> }> = ({ mod, announce }) => {
430
- announce('a clock')
431
- return { now: () => mod.claude.clock.now() }
420
+ ```ts
421
+ import { defineMod, sessions, Stop, ToolCall, type Disposable, type Job } from '../node_modules/@cmodjs/core/mod.js'
422
+
423
+ const editCounter = (tool: string): Job<Disposable & { readonly count: () => number }> => ({ mod, announce, reserveName }) => {
424
+ const reserved = reserveName('editCounter', tool, `${mod.name}: edits of ${tool} are already counted.`)
425
+ let edits = 0
426
+ const listening = sessions.current.on(ToolCall, { name: tool }, () => void (edits += 1))
427
+ announce(`a count of ${tool} calls`)
428
+ return {
429
+ count: () => edits,
430
+ dispose() {
431
+ listening.dispose()
432
+ reserved.dispose()
433
+ },
434
+ }
432
435
  }
433
436
 
434
- export const timer = defineMod({
435
- name: 'timer',
437
+ export const counter = defineMod({
438
+ name: 'counter',
436
439
  setup(mod) {
437
- const started = mod.use(clock)
438
- mod.on('Stop', async () => {
439
- mod.ui.toast(`Turn ended at ${new Date(await started.now()).toISOString()}`)
440
- })
440
+ const writes = mod.use(editCounter('Write'))
441
+ mod.on(Stop, () => mod.ui.toast(`${writes.count()} files written so far`))
441
442
  },
442
443
  })
443
444
  ```
444
445
 
445
- ### The claude members
446
-
447
- `mod.js` exports the types `Claude`, `RoutedEvent`, `RoutedHook`, and `ToolCalls`, so a job of your own names them in its own helpers, such as `(claude: Claude, calls: ToolCalls): RoutedHook<'command.run'> => …`.
448
-
449
- Each member calls the member of the same name in Claude Code's hooks API, `$`, with the same arguments. The `env` members read `$.env.get` instead. `mod.claude.on` adds a handler, as [Write a job of your own](#write-a-job-of-your-own) shows.
450
-
451
- A member that does what a permission covers checks it first and rejects with the fix: `process` needs `run`, `http.fetch` needs `network`, `fs.write` outside the project and the data folder needs `files` or, for Claude Code's own settings files, `config`, `fs.read` of a transcript needs `conversation`, `session.messages` needs `conversation`, `session.append` and `prompt.submit` need `prompt`, `model.complete` needs `model`, `agent.spawn` needs `agents`, and `tool.call` needs `tools`, except `CronCreate`, which needs `prompt`, and `CronList` and `CronDelete`, which need nothing ([permissions.md](permissions.md)).
452
-
453
- | Member | What it does |
454
- | --- | --- |
455
- | `plugin.name` | The plugin's name. |
456
- | `plugin.root` | The plugin's own folder, absolute, where the files the mod ships live. |
457
- | `ui.toast(text, options?)` | Shows `text` for a few seconds, 4000 ms unless `options.timeoutMs` says otherwise. |
458
- | `ui.status(text)` | Pins `text` as the plugin's one status line under the prompt. `undefined` removes it. |
459
- | `ui.log(text, options?)` | Adds a dim line to the transcript that Claude does not read, or with `{ to: 'debug' }` a line to the debug log only. |
460
- | `ui.notice(toolUseId, text)` | Shows `text` as a line under the open permission dialog of that call. `undefined` removes it. |
461
- | `ui.invalidate(event)` | Draws `ui.render` again, or drops the cached answers of `prompt.context` and the other cached events. |
462
- | `ui.ask(question, options?)` | Asks the person in Claude Code's question dialog, and resolves the label they chose. It rejects when they dismiss it, and in a `-p` run. |
463
- | `ui.open(pane)` | Opens a pane, `{ id, title?, focus?, closeOnEscape?, holdToasts?, rows?, columns? }`, and resolves `{ isPlaced }`. |
464
- | `ui.close(pane)` | Closes the open pane with that `{ id }`. |
465
- | `ui.panes()` | The plugin's open panes: each one's id, title, and whether it is shown, holds the keyboard, and is placed. |
466
- | `ui.scroll(args)` | What `mod.ui.scroll` calls. |
467
- | `ui.resolve(e)` | The element table of the surface a `ui.render` event draws on. |
468
- | `process.run(argv, init?)` | Runs a program to its end. `mod.process.run` is built on it. |
469
- | `process.spawn(request)` | Starts a program and streams its output. `mod.process.spawn` is built on it. |
470
- | `fs.read`, `fs.write`, `fs.list`, `fs.exists`, `fs.stat` | What `mod.fs` calls. A relative path is under the session's working folder. |
471
- | `http.fetch(url, init?)` | What `mod.http.fetch` calls. |
472
- | `settings.read(args?)` | Reads Claude Code's own `settings.json` files. Without `{ source }` it answers every source merged. `user`, `project`, `local`, `flag`, and `policy` each answer one. |
473
- | `config.list()` | Lists the rows `/config` shows, such as `<mod>.<option>`. A mod sets an option through `cmod option <mod> <key> <value>`, as `/mods` does. |
474
- | `store.get`, `store.set`, `store.delete`, `store.keys` | The plugin's key-value store, where cmod also keeps `mod.state`. |
475
- | `clock.now()` | Resolves milliseconds since the epoch. |
476
- | `clock.after(ms, fn)` | Calls `fn` once after `ms` milliseconds, and returns `{ cancel() }`. |
477
- | `clock.every(ms, fn)` | Calls `fn` every `ms` milliseconds, at least 1, and returns `{ cancel() }`. |
478
- | `session.id()` | The session's id, the transcript file's name. |
479
- | `session.root()` | The session's project root. A shell `cd` does not move it. |
480
- | `session.cwd()` | The folder the session runs in. |
481
- | `session.model()` | The main conversation's model, as `/model` shows it. |
482
- | `session.usage()` | When the session began, the context window's fill, the rate-limit windows, and the cost, as the status line has them. |
483
- | `session.surfaces()` | Every surface the session draws on, `terminal` first. Empty in a plain `-p` run. |
484
- | `session.messages(args?)` | What `mod.session.messages` calls. |
485
- | `session.append({ message, agentId? })` | Adds a message Claude reads to the conversation, or to a subagent's, and resolves `{ message, uuid }` or `{ deny }`. `mod.session.append` is built on it. |
486
- | `prompt.submit({ text, asUser? })` | Sends a prompt Claude answers as a turn once the session is idle. `mod.session.submit` is built on it. |
487
- | `model.complete(request, options?)` | What `mod.model.complete` calls. |
488
- | `command.register(spec)` | Adds a slash command. `slashCommand` is built on it. |
489
- | `tool.register(spec)` | Adds a tool. `tool` is built on it. |
490
- | `tool.call({ tool, ...input })` | Calls a tool the way Claude does, through the permission check, and resolves `{ result, text, isError? }` or `{ deny }`. `schedule` is built on it, calling `CronCreate` and `CronDelete`. |
491
- | `agent.list()` | The session's subagents and teammates so far. |
492
- | `agent.spawn(args)` | What `mod.agent.spawn` calls. |
493
- | `env.home()` | The `HOME` variable, or `undefined` when it is unset. |
494
- | `env.dataHome()` | The `XDG_DATA_HOME` variable, or `undefined`. |
495
- | `env.configHome()` | The `CLAUDE_CONFIG_DIR` variable, or `undefined`. |
496
- | `cmod.call({ to, method, input })` | Calls `method` of the mod named `to`. `mod.dependencies` is built on it. |
497
-
498
446
  ### The building blocks
499
447
 
500
448
  The built-in jobs are written from these building blocks, which a job of your own may import:
@@ -502,9 +450,9 @@ The built-in jobs are written from these building blocks, which a job of your ow
502
450
  | Import | Export | What it does |
503
451
  | --- | --- | --- |
504
452
  | `jobs/context.js` | `afterCall(job, contextAfter, failed)` | Runs `contextAfter` after each call that succeeds, and adds the lines it returns to the call's result. |
505
- | `jobs/context.js` | `modWithin(job, deadline, workTree?)` | A `mod` whose calls fail at `deadline`, `{ ms, job?, longestMs? }`. |
506
- | `jobs/context.js` | `modOf(job, { call?, folder }, workspace, deadline)` | `modWithin`, with `mod.process` calls run in the call's work tree. |
507
- | `jobs/context.js` | `workspaceReader(job)` | A function that reads the session's `projectRoot`, `cwd`, `home`, file access, and project scope. |
453
+ | `jobs/context.js` | `inFolder(call, folder, workspace)` | Adds the `folder` a rule's `when` gets: the work tree that holds the call's path or folder, or the folder itself. |
454
+ | `jobs/context.js` | `withinDeadline(mod, deadline, subject, run)` | Runs `run`, and rejects with `<subject> passed the <n> s deadline of <job>` when it has not answered by `deadline`, `{ ms, job? }`. |
455
+ | `jobs/context.js` | `workspaceReader(job)` | A function that reads the session's project, working folder, home, file access, and project scope. |
508
456
  | `jobs/permissions/decide-permission.js` | `decidePermission`, `stricterVerdict`, and the types `Rule`, `PermissionRules`, `Decision`, `Verdict` | Decides a call against permission rules. |
509
457
  | `jobs/permissions/find-project-scope.js` | `findProjectScope`, and the types `ProjectScope`, `Workspace` | Finds the repository a project plugin belongs to. |
510
458