@cmodjs/core 0.2.3 → 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 (273) hide show
  1. package/docs/commands.md +58 -10
  2. package/docs/dependencies.md +12 -12
  3. package/docs/hooks.md +41 -28
  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 +66 -23
  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 +18 -6
  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 -3
  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 -33
  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 +317 -186
  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 +18 -6
  177. package/src/runtime/announce.ts +74 -0
  178. package/src/runtime/bound.ts +12 -0
  179. package/src/runtime/claude.ts +5 -3
  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 -31
  185. package/src/runtime/hooks.ts +19 -10
  186. package/src/runtime/installer.ts +10 -9
  187. package/src/runtime/lifecycle.ts +313 -181
  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 +23 -18
  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 +24 -16
  226. package/src/utils/paths.ts +15 -1
  227. package/src/utils/text.ts +4 -0
  228. package/testing/fake-claude.d.ts +4 -2
  229. package/testing/fake-claude.d.ts.map +1 -1
  230. package/testing/fake-claude.js +18 -12
  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.d.ts +1 -0
  262. package/utils/parse-shell.d.ts.map +1 -1
  263. package/utils/parse-shell.js +22 -13
  264. package/utils/paths.d.ts +2 -0
  265. package/utils/paths.d.ts.map +1 -1
  266. package/utils/paths.js +13 -1
  267. package/utils/text.d.ts +1 -0
  268. package/utils/text.d.ts.map +1 -1
  269. package/utils/text.js +3 -0
  270. package/src/ui/slots.ts +0 -36
  271. package/ui/slots.d.ts +0 -208
  272. package/ui/slots.d.ts.map +0 -1
  273. package/ui/slots.js +0 -19
package/docs/mod.md CHANGED
@@ -15,7 +15,7 @@ type ModDefinition<State, Name, Options> = {
15
15
  readonly options?: Options
16
16
  readonly api?: { … }
17
17
  readonly installer?: readonly Step<State>[]
18
- setup(mod: Mod<State, Options>): void | Promise<void>
18
+ setup(mod: Mod<State, Options>): unknown
19
19
  }
20
20
  ```
21
21
 
@@ -24,17 +24,17 @@ type ModDefinition<State, Name, Options> = {
24
24
  - `options` declares what a person sets, such as a token, and types `mod.options`. [options.md](options.md) explains them.
25
25
  - `api` holds the methods other mods call. [dependencies.md](dependencies.md) explains it.
26
26
  - `installer` lists the steps the person finishes in Claude Code after the permissions and the options, such as signing in. [install-steps.md](install-steps.md#steps-in-claude-code) explains them.
27
- - `setup` runs once when the mod starts. It adds hooks, panes, slot renders, and jobs. It may be `async`.
27
+ - `setup` runs once when the mod starts. It adds hooks, panes, renders, and jobs. It may be `async`.
28
28
 
29
29
  ```tsx
30
- import { defineMod } from '../node_modules/@cmodjs/core/mod.js'
30
+ import { defineMod, UserPromptSubmit } from '../node_modules/@cmodjs/core/mod.js'
31
31
 
32
32
  export const greeter = defineMod({
33
33
  name: 'greeter',
34
34
  state: { session: { isGreeted: false } },
35
35
 
36
36
  setup(mod) {
37
- mod.on('UserPromptSubmit', () => {
37
+ mod.on(UserPromptSubmit, () => {
38
38
  if (mod.state.session.isGreeted) return
39
39
  mod.state.session.isGreeted = true
40
40
  mod.ui.toast(`Hello from ${mod.name}`)
@@ -47,19 +47,45 @@ export const greeter = defineMod({
47
47
 
48
48
  Claude Code loads the mod at the start of each session. cmod then checks that the mod is set up. A mod the person has not accepted yet opens the installer pane, `Install <name>`, which lists its permissions and the changes it makes to the computer, and waits for Accept ([install-steps.md](install-steps.md)). An option with no default and no value is asked on the next page of the same pane. Then cmod loads the saved state, runs `setup`, marks as open each of the mod's panes that Claude Code already shows, and opens the pane again for each `installer` step the mod declares that is not done.
49
49
 
50
- The first time a version starts, cmod shows the toast `<name> is ready` and logs `<name> added <what setup added>`, such as `the my-mod pane, a render of AbovePrompt and a hook on UserPromptSubmit`.
50
+ The first time a mod starts on a machine, cmod shows the toast `Installed` under its name. Opened, it lists the mod's `plugin.json` `description`, what setup added, such as `Adds the my-mod pane, a render of AbovePrompt and a hook on UserPromptSubmit.`, what its install step set up, and each permission the person allowed, with a Settings button for `/mods <name>`. The first time a new version starts, the toast is `Updated to <version>`: the first three changes of that version's `CHANGELOG.md` section, the version the machine ran before, and a Changelog button ([Release notes](#release-notes)). A mod with no changelog lists what it adds now and no longer adds instead. A new session, `/reload-plugins`, and a version the machine already ran show no toast.
51
51
 
52
- When a step fails, the mod does not start. Its progress line above the prompt names the failure and the fix:
52
+ When a step fails, or the person declines the install, the mod stays stopped. Once `cmod install <name>` sets it up in a terminal, the mod starts at the next prompt of every session it stopped in, with no `/reload-plugins`.
53
+
54
+ When a step fails, the mod does not start. An error toast above the prompt names the failure, with the fix under it:
53
55
 
54
56
  - `<mod>: state.<group> is not a lifetime. …` or `<mod>: state.<group> is not an object of values. …` when the declared state is not valid ([state.md](state.md)).
55
57
  - `its state did not load: <error>` when reading the saved values fails.
56
58
  - `it needs <titles>` when the person chose Not now on an option with no default, or no one can answer, as in `claude -p` ([options.md](options.md)). This one says `Set it in /config.` instead, and Claude Code reloads the mod once the value is set.
57
59
  - `its setup function threw: <error>` when `setup` throws or rejects.
58
60
  - `its open panes did not load: <error>` when Claude Code cannot list the open panes.
61
+ - `the details of its session did not load: <error>` when Claude Code cannot name the session's id, model, or start time.
59
62
  - `it decides permissions on <events>, so hooks/register.ts must call registerPermissionCheck(addHook) after registerMod` when `setup` adds a hook on `PermissionRequest`, or a job that decides permissions, and `register.ts` does not register the permission check.
60
63
 
61
64
  Each of these says `Fix it, then run /reload-plugins.`
62
65
 
66
+ ## Release notes
67
+
68
+ A mod's `CHANGELOG.md`, at its root, holds what changed in each version, written for the people who use it. It follows the Keep a Changelog format: an `## [Unreleased]` section for what is not released yet, then one `## [<version>] - <date>` section per release, newest first, with its entries under `### Added`, `### Changed`, `### Removed`, and `### Fixed`. A breaking change goes under Changed or Removed, and raises the major version.
69
+
70
+ ```markdown
71
+ # Changelog
72
+
73
+ ## [Unreleased]
74
+
75
+ ## [1.4.0] - 2026-10-13
76
+
77
+ ### Added
78
+ - /rollback undoes the last deploy, including its database migration.
79
+
80
+ ### Removed
81
+ - /deploy-old. Use /deploy --legacy instead.
82
+ ```
83
+
84
+ - `cmod publish` releases the version's section as the GitHub release's notes, and refuses a version with no section ([commands.md](commands.md#cmod-publish)). Every release ships the file.
85
+ - The Updated toast shows the first three changes of the new version's section, and its Changelog button opens `/mods <name> changelog`, the whole file with the installed version at the top of the view.
86
+ - A linked checkout whose version has no section yet shows its `[Unreleased]` section, so the notes show as soon as the version moves.
87
+ - `cmod new` writes the file with an `[Unreleased]` and a `[0.1.0]` section.
88
+
63
89
  ## Register the mod with Claude Code
64
90
 
65
91
  `hooks/hooks.json` names `hooks/register.ts` as the plugin's hooks module. `register.ts` calls `registerMod` and does nothing else:
@@ -91,89 +117,172 @@ export function register(addHook: On, options: PluginOptions): void {
91
117
  }
92
118
  ```
93
119
 
94
- Such a mod also declares `"approve": true` in its `package.json` `"cmod".permissions`, so the person grants it at install. `cmod check` adds the line when it is missing, and without the grant cmod drops every allow or ask the mod answers, so Claude Code decides those calls itself ([permissions.md](permissions.md)). A mod without `registerPermissionCheck` has no part in Claude Code's permission check, so the plugin directory reads it as one that never answers a permission. A mod that decides permissions without it does not start, and its progress line names the line to add.
120
+ Such a mod also declares `"approve": true` in its `package.json` `"cmod".permissions`, so the person grants it at install. `cmod check` adds the line when it is missing, and without the grant cmod drops every allow or ask the mod answers, so Claude Code decides those calls itself ([permissions.md](permissions.md)). A mod without `registerPermissionCheck` has no part in Claude Code's permission check, so the plugin directory reads it as one that never answers a permission. A mod that decides permissions without it does not start, and its error toast names the line to add.
95
121
 
96
122
  Keep `register.ts` this small. Claude Code checks a hooks module before it loads it, and refuses some shapes, such as a `$` passed to a function in another module. The plugin directory reads the bundled module too, and flags it when the name of `register`'s first parameter is declared anywhere else in the bundle. `@cmodjs/core` declares `on` as the method `mod.on`, so name that parameter `addHook`. Declare `register` as a function, as here: `cmod publish` bundles the hooks module into one file, and Claude Code refuses a bundled `register` that is not one.
97
123
 
98
- ## What `mod` can call
124
+ ## mod: this mod
99
125
 
100
- `setup` gets `mod`, of type `Mod<State, Options>`. Every hook, render, and job gets the same `mod`.
126
+ `setup` gets `mod`, of type `Mod<State, Options>`. Every hook, render, and job gets the same `mod`. `mod` holds what belongs to this mod: its name, its folders, its values, its grants, and what it adds to Claude Code.
101
127
 
102
128
  ```ts
103
129
  type Mod<State, Options> = {
104
130
  readonly name: string
131
+ readonly folder: string
132
+ readonly dataFolder: string
105
133
  readonly state: Readonly<State>
106
134
  readonly options: OptionValues<Options>
107
- readonly dataFolder: string
108
- readonly projectRoot: string
109
- readonly cwd: string
110
- on(event, hook, options?): void
135
+ readonly permissions: Permissions
136
+ on(hook, listener, options?): Disposable
137
+ on(hook, matcher, listener, options?): Disposable
138
+ every(ms, listener): Disposable
139
+ after(ms, listener): Disposable
111
140
  use(job): Handle
112
- every(ms, hook): Timer
113
- readonly ui: { pane, render, toast, progress, ask, scroll }
114
- readonly process: { run, spawn }
115
- readonly fs: { read, write, list, exists, stat, find }
116
- readonly metadata: { read, update }
117
- readonly http: { fetch }
141
+ log(text): void
142
+ readonly ui: { pane, render, toast, ask, copy, scroll, notice }
118
143
  readonly settings: { page, open }
119
- readonly session: { messages, append, submit }
120
- readonly agent: { spawn }
121
- readonly model: { complete }
122
- readonly permissions: { has }
123
144
  readonly dependencies: CmodDependencies
124
- readonly claude: ModClaude
125
145
  }
126
146
  ```
127
147
 
128
148
  | Member | What it is | Docs |
129
149
  | --- | --- | --- |
130
150
  | `name` | The mod's name from `defineMod`. | |
151
+ | `folder` | The plugin's own folder, where the files the mod ships live. | |
152
+ | `dataFolder` | The mod's own folder in the cmod store: `$XDG_DATA_HOME/cmod/data/<plugin name>`, or `~/.local/share/cmod/data/<plugin name>` when `XDG_DATA_HOME` is not set. The install step gets the same folder as `CMOD_DATA`. cmod deletes it when the mod is removed. | [install-steps.md](install-steps.md) |
131
153
  | `state` | The mod's values. Assign to a key to change it. | [state.md](state.md) |
132
154
  | `options` | The values a person set for the mod's options, typed from their declaration. Read-only. | [options.md](options.md) |
133
- | `dataFolder` | The mod's own folder in the cmod store: `$XDG_DATA_HOME/cmod/data/<plugin name>`, or `~/.local/share/cmod/data/<plugin name>` when `XDG_DATA_HOME` is not set. The install step gets the same folder as `CMOD_DATA`. cmod deletes it when the mod is removed. | [install-steps.md](install-steps.md) |
134
- | `projectRoot` | The project the session works in. It follows `/cd`. | |
135
- | `cwd` | The session's working folder. It follows a `cd` in a Bash or PowerShell call, and `/cd`. | |
136
- | `on` | Adds a hook on a Claude Code event. | [hooks.md](hooks.md) |
155
+ | `permissions` | Says whether the person granted each permission. | [permissions.md](permissions.md) |
156
+ | `on` | Listens to a Claude Code hook, such as `PreToolUse`. | [hooks.md](hooks.md) |
157
+ | `every` and `after` | Run a listener on a timer while the session runs. | below |
137
158
  | `use` | Adds a job, such as a slash command or a tool, and returns its handle. | [jobs.md](jobs.md) |
138
- | `every` | Runs a function every so many milliseconds while the session runs. | below |
139
- | `ui` | Panes, slot renders, toasts, progress lines, questions, and scrolling. | [ui.md](ui.md) |
140
- | `process` | Runs a program. | below |
141
- | `fs` | Reads, writes, looks up, and finds files. | below |
142
- | `metadata` | Reads and changes the mod's keys in one file's metadata. | below |
143
- | `http` | Fetches a URL. | below |
159
+ | `log` | Writes a line, under the mod's name, to Claude Code's debug log. | below |
160
+ | `ui` | Panes, renders at a place Claude Code draws, toasts, questions, the clipboard, scrolling, and a line under a permission dialog. | [ui.md](ui.md) |
144
161
  | `settings` | Adds the mod's own pages to `/mods`, and opens them. | below |
145
- | `session` | Reads the conversation or a subagent's, adds a note Claude reads, and asks Claude for a turn. | below |
146
- | `agent` | Starts a subagent. | below |
147
- | `model` | Asks a model one question, outside the conversation. | below |
148
- | `permissions` | Says whether the person has a permission turned on. `process`, `fs.write`, `metadata.update`, `http`, `session`, `agent`, `model`, and `claude` each check theirs. | [permissions.md](permissions.md) |
149
162
  | `dependencies` | Calls the methods of other mods. | [dependencies.md](dependencies.md) |
150
- | `claude` | Claude Code's own calls and events, for what `mod` has no member for. | [jobs.md](jobs.md#the-claude-members) |
151
163
 
152
- ### mod.every
164
+ Everything that is Claude Code's or the computer's rather than the mod's is an import from `mod.js`, so a function in any file calls it without being handed `mod`:
153
165
 
154
166
  ```ts
155
- mod.every(ms: number, hook: () => unknown): Timer
167
+ import { fs, http, metadata, model, process, sessions, settings } from '../node_modules/@cmodjs/core/mod.js'
156
168
  ```
157
169
 
158
- `every` runs `hook` every `ms` milliseconds until the session ends or the mod calls `cancel()` on the `Timer` it returns. A tick that comes while the last run is still going is skipped, so a slow run never stacks up. A run that throws logs `<mod>: the every <ms> ms hook failed: <error>`, and the next tick runs again. `ms` is a whole number above 0 and at most `2147483647`, and any other `ms` stops `setup` with the fix.
170
+ | Import | What it is | Docs |
171
+ | --- | --- | --- |
172
+ | `sessions` | The conversations: the one the mod runs in, every other one, and search across them. | [session.md](session.md) |
173
+ | `settings` | Claude Code's `settings.json` files: the person's, the project's, and the project's local one. | below |
174
+ | `model` | Asks a model one question, outside the conversation. | below |
175
+ | `fs` | Reads, writes, lists, and finds files. | below |
176
+ | `metadata` | Reads and changes the mod's keys in one file's metadata. | below |
177
+ | `process` | Runs a program. | below |
178
+ | `http` | Fetches a URL. | below |
179
+
180
+ Each import works once the mod has started, in `setup` and anything it adds. Each one checks the permission it needs, and rejects with the line to add or the grant to turn on when the person has not granted it ([permissions.md](permissions.md)).
181
+
182
+ ### Disposable
183
+
184
+ `mod.on`, `mod.every`, `mod.after`, `mod.ui.render`, and a session's `on` each return a `Disposable`. `dispose()` takes back what the call added, so a mod turns a feature off and on while it runs. Everything a mod adds is taken back when the mod stops, so a mod that keeps a feature for the whole session ignores the `Disposable`.
185
+
186
+ ```ts
187
+ type Disposable = { dispose(): void }
188
+ ```
189
+
190
+ ```ts
191
+ const watching = mod.on(PostToolUse, { tool_name: 'Edit' }, () => void (mod.state.session.edits += 1))
192
+ mod.use(slashCommand({ name: 'stop-counting', description: 'Stop counting edits', reply: () => watching.dispose() }))
193
+ ```
194
+
195
+ ### mod.every and mod.after
196
+
197
+ ```ts
198
+ mod.every(ms: number, listener: () => unknown): Disposable
199
+ mod.after(ms: number, listener: () => unknown): Disposable
200
+ ```
201
+
202
+ `every` runs `listener` every `ms` milliseconds until the session ends or the mod disposes it. A tick that comes while the last run is still going is skipped, so a slow run never stacks up. `after` runs `listener` once, `ms` milliseconds from now, unless the mod disposes it first. A run that throws shows the error toast `The every <ms> ms timer failed`, or `The after <ms> ms timer failed`, and the next tick of an `every` runs again. `ms` is a whole number above 0 and at most `2147483647`, and any other `ms` stops `setup` with `<mod>: mod.every is <ms>. Give a whole number of milliseconds above 0 and at most 2147483647.`
159
203
 
160
204
  A watch that reacts on its own is `every` plus `mod.state` plus a `prompt` with no trigger: `every` checks the outside world, writes what it found to `mod.state`, and the panes and the prompt follow the state ([jobs.md](jobs.md#prompt)).
161
205
 
162
206
  ```ts
163
207
  mod.every(60_000, async () => {
164
- const { stdout } = await mod.process.run(['gh', 'run', 'list', '--limit', '1', '--json', 'conclusion'])
208
+ const { stdout } = await process.run(['gh', 'run', 'list', '--limit', '1', '--json', 'conclusion'])
165
209
  mod.state.session.ci = JSON.parse(stdout)[0]?.conclusion ?? 'running'
166
210
  })
167
211
  ```
168
212
 
169
- ### mod.process
213
+ ### mod.log
214
+
215
+ ```ts
216
+ mod.log(text: string): void
217
+ ```
218
+
219
+ `log` writes `<mod>: <text>` to Claude Code's debug log, which `claude --debug` shows. It is for what helps the mod's author find a fault and the person never needs to see. Tell the person with a toast instead ([ui.md](ui.md#toasts)).
220
+
221
+ ### mod.settings
222
+
223
+ ```ts
224
+ mod.settings.page(page: Pane<State>): void
225
+ mod.settings.open(pageId?: string): Promise<void>
226
+ ```
227
+
228
+ `/mods` shows each mod's description, options, permissions, keys, and changelog, and turns a mod off or removes it. `page` adds a page of the mod's own to its entry there, drawn as any pane is ([ui.md](ui.md)). The id `changelog` is taken by the Changelog page, so `page` refuses it. `open` opens that page, or with no id opens the mod's entry in `/mods`. `open` rejects for an id `page` did not add.
229
+
230
+ ```tsx
231
+ mod.settings.page(definePane({ id: 'runs', title: 'Runs', render: (current) => <Text>{current.state.session.lastRun}</Text> }))
232
+ mod.use(slashCommand({ name: 'ci-runs', description: 'Show the CI runs', reply: () => void mod.settings.open('runs') }))
233
+ ```
234
+
235
+ ## settings: Claude Code's settings files
236
+
237
+ ```ts
238
+ settings.user.read(): Promise<Settings>
239
+ settings.user.update(values: Readonly<Record<string, unknown>>): Promise<void>
240
+ settings.project.read(): Promise<Settings>
241
+ settings.project.update(values: Readonly<Record<string, unknown>>): Promise<void>
242
+ settings.local.read(): Promise<Settings>
243
+ settings.local.update(values: Readonly<Record<string, unknown>>): Promise<void>
244
+ ```
245
+
246
+ `settings.user` is the person's own `~/.claude/settings.json`, `settings.project` is the project's `.claude/settings.json`, and `settings.local` is the project's `.claude/settings.local.json`.
247
+
248
+ - `read` answers the values in that one file, such as `(await settings.user.read())['enabledPlugins']`.
249
+ - `update` writes each top-level key of `values` into that file. `mcpServers` in `settings.project` goes to the project's `.mcp.json`, where Claude Code reads a project's servers.
250
+ - A key replaces its whole value, and `null` removes the key. Every other key, and the file's formatting, stays as it was.
251
+ - Each key needs its own `config` permission, such as `"config": ["cleanupPeriodDays"]` ([permissions.md](permissions.md)), and `update` rejects with the line to add for a key without one.
252
+ - Removing the mod takes out each value it set that still equals what it set, so a value the person changed since stays.
253
+ - `fs.write` to a Claude Code settings file rejects and names `update`, because a whole-file write loses the person's own edits and leaves the mod's values behind when it is removed.
254
+
255
+ ```ts
256
+ await settings.user.update({ cleanupPeriodDays: 365 })
257
+ ```
258
+
259
+ ## model: one question outside the conversation
260
+
261
+ ```ts
262
+ model.complete(request: ModelCompleteRequest, options?: { signal?: AbortSignal }): Promise<ModelCompleteResult>
263
+ ```
264
+
265
+ `complete` asks a model one question through Claude Code's own login, outside the conversation, so the answer costs no context and the person sees no turn. The request names the `model`, such as `'haiku'`, and the `prompt`, and takes `system`, `maxTokens`, `effort`, and `timeoutMs`. `timeoutMs` stops a model that is slow or down, so the listener that waits on it still answers in time. `options.signal` stops the call when its `AbortController` aborts. It needs the `model` permission.
266
+
267
+ The call never rejects for what the model did. The result's `isAnswered` is true with the reply's `text`, or false with a `reason`, such as `'api-error'` or `'aborted'`.
268
+
269
+ Put a limit on the listener too, with `mod.on`'s `timeoutMs` ([hooks.md](hooks.md#mod-on)), which also covers the work around the call:
270
+
271
+ ```ts
272
+ mod.on(Stop, async ({ last_assistant_message }) => {
273
+ const verdict = await model.complete({ model: 'haiku', prompt: `Does this reply ask the person to do work Claude could do itself? Answer yes or no.\n\n${last_assistant_message}`, timeoutMs: 8000 })
274
+ if (verdict.isAnswered && verdict.text.trim().toLowerCase().startsWith('yes')) return { decision: 'block', reason: 'Do that work yourself, then reply.' }
275
+ }, { timeoutMs: 9000 })
276
+ ```
277
+
278
+ ## process: run a program
170
279
 
171
280
  ```ts
172
- mod.process.run(argv: readonly string[], init?: ProcessRunInit): Promise<ProcessRunResult>
173
- mod.process.spawn(argv: readonly string[], init?: { cwd?, env?, input? }): HookStream<ProcessSpawnChunk, ProcessSpawnResult>
281
+ process.run(argv: readonly string[], init?: ProcessRunInit): Promise<ProcessRunResult>
282
+ process.spawn(argv: readonly string[], init?: { cwd?, env?, input? }): HookStream<ProcessSpawnChunk, ProcessSpawnResult>
174
283
  ```
175
284
 
176
- Both run a program without a shell. `argv[0]` is the program and the rest are its arguments. A bare name that cmod installed, such as `trace`, runs from cmod's programs folder ([install-steps.md](install-steps.md#run-a-program-by-its-name)), and any other name runs through Claude Code's `PATH`.
285
+ Both run a program without a shell. `argv[0]` is the program and the rest are its arguments. A bare name that cmod installed, such as `trace`, runs from cmod's programs folder ([install-steps.md](install-steps.md#run-a-program-by-its-name)), and any other name runs through Claude Code's `PATH`. Each program needs its `run` permission.
177
286
 
178
287
  - `init.cwd` is the program's folder, absolute or relative to the session's working folder. Without it the program runs in the session's working folder.
179
288
  - `init.env` sets variables over Claude Code's own environment.
@@ -182,44 +291,49 @@ Both run a program without a shell. `argv[0]` is the program and the rest are it
182
291
  - `run` resolves `{ exitCode, stdout, stderr, isStdoutTruncated, isStderrTruncated }` once the program exits, whatever its exit code. It keeps the first 4 MiB of each stream. A program ended by a signal has `exitCode` 1.
183
292
  - `run` rejects when the program cannot start, and when it is still running at `timeoutMs`. It reads the whole output, so a background process the program leaves writing holds the call until the timeout.
184
293
  - `spawn` streams `{ stream: 'stdout' | 'stderr', text }` pieces as the program writes them. Its `result` resolves `{ code, signal }` once the stream is read to its end. Its first pull rejects when the program cannot start.
185
- - The `for await` loop over a `spawn` is the program's life. Leaving the loop, or calling `return()` on the stream, kills the program, and so does unloading the mod. Returning from the hook does not.
294
+ - The `for await` loop over a `spawn` is the program's life. Leaving the loop, or calling `return()` on the stream, kills the program, and so does unloading the mod. Returning from the listener does not.
186
295
 
187
- Inside a job, cmod adds the job's deadline to each call ([jobs.md](jobs.md)).
296
+ A job's deadline bounds the whole callback that makes these calls ([jobs.md](jobs.md#deadlines)).
188
297
 
189
298
  ```ts
190
- import { defineMod } from '../node_modules/@cmodjs/core/mod.js'
299
+ import { defineMod, process, SessionStart, sessions } from '../node_modules/@cmodjs/core/mod.js'
191
300
 
192
301
  export const branchNote = defineMod({
193
302
  name: 'branch-note',
194
303
  setup(mod) {
195
- mod.on('SessionStart', async () => {
196
- const { exitCode, stdout } = await mod.process.run(['git', 'branch', '--show-current'], { cwd: mod.projectRoot })
304
+ mod.on(SessionStart, async () => {
305
+ const { exitCode, stdout } = await process.run(['git', 'branch', '--show-current'], { cwd: sessions.current.project })
197
306
  if (exitCode !== 0) return
198
- return { hookSpecificOutput: { hookEventName: 'SessionStart', additionalContext: `The git branch is ${stdout.trim()}.` } }
307
+ return { hookSpecificOutput: { hookEventName: SessionStart, additionalContext: `The git branch is ${stdout.trim()}.` } }
199
308
  })
200
309
  },
201
310
  })
202
311
  ```
203
312
 
204
- ### mod.fs
313
+ ## fs: files
205
314
 
206
315
  ```ts
207
- mod.fs.read(path: string): Promise<string>
208
- mod.fs.write(path: string, text: string): Promise<void>
209
- mod.fs.list(path?: string): Promise<FsEntry[]>
210
- mod.fs.exists(path: string): Promise<boolean>
211
- mod.fs.stat(path: string, options?: { resolve: boolean }): Promise<FsStat>
212
- mod.fs.find(glob: string | readonly string[]): Promise<readonly FileMatch[]>
316
+ fs.read(path: string): Promise<string>
317
+ fs.write(path: string, text: string): Promise<void>
318
+ fs.list(path?: string): Promise<Entry[]>
319
+ fs.exists(path: string): Promise<boolean>
320
+ fs.stat(path: string, options?: { resolve: boolean }): Promise<Stat>
321
+ fs.find(glob: string | readonly string[]): Promise<readonly FileMatch[]>
213
322
  ```
214
323
 
215
- - A relative path is under the session's working folder. An absolute path is used as given. Text is UTF-8.
324
+ ```ts
325
+ type Entry = { name: string; isFile: boolean; isFolder: boolean; isLink: boolean; size: number; modifiedAt: number }
326
+ type Stat = { isFile: boolean; isFolder: boolean; isLink: boolean; size: number; modifiedAt: number; realPath?: string }
327
+ ```
328
+
329
+ - A relative path is under the session's working folder. An absolute path is used as given. `~/.claude` is Claude Code's config folder, and `~` is the home folder. Text is UTF-8.
216
330
  - `read` rejects a missing file and a file over 4 MiB.
217
- - `write` writes the whole file and creates its folders.
218
- - `list` lists a folder, the working folder without a path. Each entry is `{ name, kind, size, mtimeMs, isLink }`, and `kind` is `'file'`, `'dir'`, or `'other'`. A symbolic link is `'other'` with `isLink` true.
331
+ - `write` writes the whole file and creates its folders. Writing outside the project and the mod's data folder needs the `files` permission.
332
+ - `list` lists a folder, the working folder without a path. A symbolic link is neither a file nor a folder, with `isLink` true. `modifiedAt` is the time it last changed, in milliseconds since 1970.
219
333
  - `exists` answers whether the path leads to a file or a folder.
220
- - `stat` answers `{ kind, size, mtimeMs, isLink }` of what the path leads to, following a symbolic link, and rejects a missing path. With `{ resolve: true }` it also answers `realPath`: the absolute path, every symbolic link followed.
334
+ - `stat` answers what the path leads to, following a symbolic link, and rejects a missing path. With `{ resolve: true }` it also answers `realPath`: the absolute path, every symbolic link followed.
221
335
 
222
- A file the mod ships is under the plugin's folder, `mod.claude.plugin.root` ([jobs.md](jobs.md#the-claude-members)). A file the mod keeps for itself belongs in `mod.dataFolder`.
336
+ A file the mod ships is under `mod.folder`. A file the mod keeps for itself belongs in `mod.dataFolder`.
223
337
 
224
338
  `find` answers every file a glob matches, each with its metadata, so a mod finds the Skills, agents, scripts, or notes it acts on in one call:
225
339
 
@@ -227,27 +341,28 @@ A file the mod ships is under the plugin's folder, `mod.claude.plugin.root` ([jo
227
341
  type FileMatch = { readonly path: string; readonly name?: string; readonly metadata: Readonly<Record<string, string>>; readonly error?: string }
228
342
  ```
229
343
 
230
- - A glob starting with `~/.claude` starts at Claude Code's config folder, `CLAUDE_CONFIG_DIR` when it is set. One starting with `~/` starts at home, one starting with `/` is absolute, and any other starts at the project root. Several globs answer each file once.
231
- - `path` is absolute. `name` is a Markdown file's frontmatter `name`. `metadata` holds the mod's own keys, bare, as `mod.metadata.read` answers them.
344
+ - A glob starting with `~/.claude` starts at Claude Code's config folder, `CLAUDE_CONFIG_DIR` when it is set. One starting with `~/` starts at home, one starting with `/` is absolute, and any other starts at the project. Several globs answer each file once.
345
+ - `path` is absolute. `name` is a Markdown file's frontmatter `name`. `metadata` holds the mod's own keys, bare, as `metadata.read` answers them.
232
346
  - `find` follows symbolic links into the folders they point at, once each. It skips `.git` and `node_modules` unless the glob names them.
233
- - A file whose metadata cmod cannot read comes back with empty `metadata` and an `error` naming the line, and cmod logs it once.
347
+ - A file whose metadata cmod cannot read comes back with empty `metadata` and an `error` naming the line, and cmod logs it once. A file cmod cannot open comes back the same way, with an `error` naming the reason.
348
+ - A folder cmod cannot list, such as one it has no permission to read, is left out, and cmod writes `<mod>: fs.find skipped <folder>: <reason>` to Claude Code's debug log once.
234
349
  - cmod keeps what it read of each file until the file's modified time or size changes.
235
350
 
236
351
  ```ts
237
- const skills = await mod.fs.find(['~/.claude/skills/*/SKILL.md', '.claude/skills/*/SKILL.md'])
352
+ const skills = await fs.find(['~/.claude/skills/*/SKILL.md', '.claude/skills/*/SKILL.md'])
238
353
  const planning = skills.filter((skill) => skill.metadata['modes']?.split(' ').includes('plan'))
239
354
  ```
240
355
 
241
- ### mod.metadata
356
+ ## metadata: a file's own keys
242
357
 
243
358
  ```ts
244
- mod.metadata.read(path: string): Promise<Readonly<Record<string, string>>>
245
- mod.metadata.update(path: string, change: (metadata: Record<string, string>) => void): Promise<void>
359
+ metadata.read(path: string): Promise<Readonly<Record<string, string>>>
360
+ metadata.update(path: string, change: (metadata: Record<string, string>) => void): Promise<void>
246
361
  ```
247
362
 
248
363
  Metadata is a file's own set of string keys, kept in the file, so it moves and is committed with the file. Each mod reads and writes its own keys only: the file stores them as `<mod>.<key>`, and the mod sees them bare. A value is one line of text, and a list is one space-separated string.
249
364
 
250
- Where a file keeps its metadata depends on its kind:
365
+ Where a file keeps its metadata depends on its type:
251
366
 
252
367
  ```markdown
253
368
  ---
@@ -268,116 +383,27 @@ metadata:
268
383
  - A `.sh`, `.py`, `.ts`, `.js`, `.yaml`, or `.toml` file keeps it in a `/// metadata` comment block after any shebang, in the file's own comment, `#` or `//`, the way PEP 723 keeps a Python script's dependencies.
269
384
  - Any other file keeps it in a sidecar, `<file>.meta`.
270
385
 
271
- `update` hands `change` the mod's current keys, and writes what `change` leaves. It changes only the metadata block, keeps other mods' keys, writes nothing when nothing changed, and removes an empty block. It writes through a symbolic link to the file it points at. Writing a file outside the project and the mod's data folder needs the `files` permission, as `mod.fs.write` does ([permissions.md](permissions.md)). A key is letters, digits, `_`, `.`, and `-`, and `update` rejects any other key and a value with a line break.
386
+ `update` hands `change` the mod's current keys, and writes what `change` leaves. It changes only the metadata block, keeps other mods' keys, writes nothing when nothing changed, and removes an empty block. It writes through a symbolic link to the file it points at. Writing a file outside the project and the mod's data folder needs the `files` permission, as `fs.write` does ([permissions.md](permissions.md)). A key is letters, digits, `_`, `.`, and `-`, and `update` rejects any other key and a value with a line break.
387
+
388
+ `read` rejects when cmod cannot open the file or read its metadata, with the reason or the line. `update` rejects the same way and writes nothing, so it never overwrites metadata it could not read.
272
389
 
273
390
  ```ts
274
- await mod.metadata.update('.claude/skills/plan/SKILL.md', (metadata) => {
275
- metadata['modes'] = 'plan build'
391
+ await metadata.update('.claude/skills/plan/SKILL.md', (keys) => {
392
+ keys['modes'] = 'plan build'
276
393
  })
277
394
  ```
278
395
 
279
396
  `cmod check` reads the metadata of every file the mod ships, and fails one cmod cannot read.
280
397
 
281
- ### mod.http
398
+ ## http: fetch a URL
282
399
 
283
400
  ```ts
284
- mod.http.fetch(url: string, init?: HttpInit): Promise<HttpResponse>
401
+ http.fetch(url: string, init?: HttpInit): Promise<HttpResponse>
285
402
  ```
286
403
 
287
404
  - `init` takes `method` (`GET` by default), `headers`, `body` as text, and `socketPath`, the absolute path of a Unix socket the request goes over.
288
405
  - The response is `{ status, ok, headers, text }`. `ok` is true for a 2xx status. Header names are lower case.
289
-
290
- ### mod.settings
291
-
292
- ```ts
293
- mod.settings.page(page: Pane<State>): void
294
- mod.settings.open(pageId?: string): Promise<void>
295
- ```
296
-
297
- `/mods` shows each mod's description, options, permissions, and keys, and turns a mod off or removes it. `page` adds a page of the mod's own to its entry there, drawn as any pane is ([ui.md](ui.md)). `open` opens that page, or with no id opens the mod's entry in `/mods`. `open` rejects for an id `page` did not add.
298
-
299
- ```tsx
300
- mod.settings.page(definePane({ id: 'runs', title: 'Runs', render: (current) => <Text>{current.state.session.lastRun}</Text> }))
301
- mod.use(slashCommand({ name: 'ci-runs', description: 'Show the CI runs', reply: () => void mod.settings.open('runs') }))
302
- ```
303
-
304
- Claude Code's own `settings.json` files are read with `mod.claude.settings.read`, such as `(await mod.claude.settings.read({ source: 'user' }))['enabledPlugins']` ([jobs.md](jobs.md#the-claude-members)).
305
-
306
- ### mod.session
307
-
308
- ```ts
309
- mod.session.messages(): Promise<SessionMessage[]>
310
- mod.session.messages(args: { agentId: string }): Promise<SessionMessage[] | { deny: string }>
311
- mod.session.append(text: string): Promise<void>
312
- mod.session.submit(text: string): Promise<void>
313
- ```
314
-
315
- `messages` reads the main conversation, one `{ role, text, toolUses, toolResults }` entry per message. A message has no id of its own, and each tool use carries its `tool_use_id`. `{ agentId }` reads that subagent's conversation instead, the id a `SubagentStart` or `SubagentStop` hook gets as `agent_id`, and answers `{ deny }` when the session cannot read it, so `Array.isArray` tells the two apart. The Messages API form, `{ role, content }` with the content blocks whole, is `mod.claude.session.messages({ as: 'api' })`.
316
-
317
- ```ts
318
- mod.on('SubagentStop', async ({ agent_id }) => {
319
- const read = await mod.session.messages({ agentId: agent_id })
320
- if (Array.isArray(read)) mod.ui.toast(read.at(-1)?.text ?? '')
321
- })
322
- ```
323
-
324
- `append` adds `text` to the conversation as a user message the person does not see, so Claude reads it on its next call to the model. It starts no turn. It rejects with `<mod>: Claude Code refused the note: <reason>` when a plugin's `session.append` handler refuses it.
325
-
326
- `submit` sends `text` as the person's next prompt, so Claude answers it as a turn of its own. Claude Code holds it until the session is idle, so it never cuts into a turn, and shows it under `The <mod> plugin sent a message:`. It rejects with `<mod>: Claude Code dropped the prompt: <reason>` when a plugin drops it. A prompt that comes on a clock, such as every 10 minutes, belongs in a `schedule` job instead, which Claude Code shows as one `Running scheduled task` row ([jobs.md](jobs.md#schedule)).
327
-
328
- ```ts
329
- mod.every(60_000, async () => {
330
- const { stdout } = await mod.process.run(['gh', 'run', 'list', '--limit', '1', '--json', 'conclusion'])
331
- if (JSON.parse(stdout)[0]?.conclusion !== 'failure') return
332
- await mod.session.append('CI failed on the last push.')
333
- await mod.session.submit('Find the cause of the CI failure and fix it.')
334
- })
335
- ```
336
-
337
- ### mod.agent
338
-
339
- ```ts
340
- mod.agent.spawn(args: { prompt: string; description?; subagentType?; model?; name?; cwd? }): Promise<AgentSpawnResult>
341
- ```
342
-
343
- `spawn` starts a subagent in the background, the way the Agent tool starts one, and resolves `{ agentId, model }` once it started, or `{ deny }` when a hook refused it. It does not wait for the subagent to finish. `subagentType` names an agent type, such as `explorer`. `mod.session.messages({ agentId })` reads the subagent's conversation so far.
344
-
345
- The subagent's answer arrives on the mod's `SubagentStop` hook once it finishes: the input's `agent_id` is the `agentId` that `spawn` resolved, and its `last_assistant_message` is the subagent's final reply. A hook may wait for it, as this one does:
346
-
347
- ```ts
348
- const answers = new Map<string, (answer: string) => void>()
349
-
350
- mod.on('SubagentStop', (input) => {
351
- answers.get(input.agent_id)?.(input.last_assistant_message ?? '')
352
- })
353
-
354
- mod.on('UserPromptSubmit', async (input) => {
355
- if (!input.prompt.startsWith('/summarize')) return
356
- const spawned = await mod.agent.spawn({ prompt: 'Summarize README.md in one line.', subagentType: 'Explore' })
357
- if (spawned.agentId === undefined) return
358
- const summary = await new Promise<string>((resolve) => answers.set(spawned.agentId as string, resolve))
359
- return { hookSpecificOutput: { hookEventName: 'UserPromptSubmit', additionalContext: summary } }
360
- })
361
- ```
362
-
363
- ### mod.model
364
-
365
- ```ts
366
- mod.model.complete(request: ModelCompleteRequest, options?: { signal?: AbortSignal }): Promise<ModelCompleteResult>
367
- ```
368
-
369
- `complete` asks a model one question through Claude Code's own login, outside the conversation, so the answer costs no context and the person sees no turn. The request names the `model`, such as `'haiku'`, and the `prompt`, and takes `system`, `maxTokens`, `effort`, and `timeoutMs`. `timeoutMs` stops a model that is slow or down, so the hook that waits on it still answers in time. `options.signal` stops the call when its `AbortController` aborts.
370
-
371
- The call never rejects for what the model did. The result's `isAnswered` is true with the reply's `text`, or false with a `reason`, such as `'api-error'` or `'aborted'`.
372
-
373
- Put a limit on the hook too, with `mod.on`'s `timeoutMs` ([hooks.md](hooks.md#mod-on)), which also covers the work around the call:
374
-
375
- ```ts
376
- mod.on('Stop', async ({ last_assistant_message }) => {
377
- const verdict = await mod.model.complete({ model: 'haiku', prompt: `Does this reply ask the person to do work Claude could do itself? Answer yes or no.\n\n${last_assistant_message}`, timeoutMs: 8000 })
378
- if (verdict.isAnswered && verdict.text.trim().toLowerCase().startsWith('yes')) return { decision: 'block', reason: 'Do that work yourself, then reply.' }
379
- }, { timeoutMs: 9000 })
380
- ```
406
+ - Each host needs its `network` permission. A fetch to the address the mod's own `program` job answers on needs none, while that program runs ([jobs.md](jobs.md#program)).
381
407
 
382
408
  ## messageOf
383
409
 
@@ -388,14 +414,14 @@ messageOf(error: unknown): string
388
414
  Returns an `Error`'s `message`, or the value as text. Use it in a `catch`, where the error is `unknown`.
389
415
 
390
416
  ```ts
391
- import { defineMod, messageOf } from '../node_modules/@cmodjs/core/mod.js'
417
+ import { defineMod, fs, messageOf, Stop } from '../node_modules/@cmodjs/core/mod.js'
392
418
 
393
419
  export const notes = defineMod({
394
420
  name: 'notes',
395
421
  setup(mod) {
396
- mod.on('Stop', async () => {
397
- await mod.fs.write(`${mod.dataFolder}/last-stop.txt`, new Date().toISOString()).catch((error: unknown) => {
398
- mod.ui.toast(`notes could not save: ${messageOf(error)}`)
422
+ mod.on(Stop, async () => {
423
+ await fs.write(`${mod.dataFolder}/last-stop.txt`, new Date().toISOString()).catch((error: unknown) => {
424
+ mod.ui.toast.error('Could not save the last stop', { description: messageOf(error) })
399
425
  })
400
426
  })
401
427
  },
package/docs/options.md CHANGED
@@ -49,7 +49,7 @@ Each value is, in order of precedence:
49
49
  3. The value the person set in `/config`.
50
50
  4. The `default` in the declaration.
51
51
 
52
- An option with no default and no value is asked before `setup` runs, on a page of the installer pane: one field per option, each saved to `/config` as the person submits it. A secret is set in `/config` itself, where Claude Code keeps it in the keychain, and the page says so. When the person chooses Not now, or no one can answer, as in `claude -p`, the mod does not start: its progress line says `it needs <title>` and `Set it in /config.`, and Claude Code reloads the mod once the value is set.
52
+ An option with no default and no value is asked before `setup` runs, on a page of the installer pane: one field per option, each saved to `/config` through `cmod option` as the person submits it. A secret is set in `/config` itself, where Claude Code keeps it in the keychain, and the page says so. When the person chooses Not now, or no one can answer, as in `claude -p`, the mod does not start: its error toast says `it needs <title>` and `Set it in /config.`, and Claude Code reloads the mod once the value is set.
53
53
 
54
54
  `cmod install`, `cmod link`, and `cmod try` set options from the terminal with `--option key=value`, once per option ([commands.md](commands.md)).
55
55
 
@@ -89,5 +89,5 @@ Claude Code reads a plugin's options from `userConfig` in `.claude-plugin/plugin
89
89
  ```ts
90
90
  const tested = testMod(ciWatch, { options: { githubToken: 'ghp_test', branch: 'develop' } })
91
91
  await tested.start()
92
- expect(tested.shown.toasts).toContain('Watching develop every 10 minutes')
92
+ expect(tested.shown.toasts).toContainEqual({ type: 'default', message: 'Watching develop every 10 minutes' })
93
93
  ```