@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/ui.md CHANGED
@@ -1,14 +1,15 @@
1
1
  # UI
2
2
 
3
- A mod draws in four places: its own panes, the rows Claude Code draws (slots), the blocks of Claude's replies (markdown slots), and short messages: toasts, progress lines, and questions. All of it lives on `mod.ui`.
3
+ A mod draws in four ways: its own panes, the rows Claude Code draws (places), the blocks of Claude's replies (markdown places), and short messages: toasts, questions, and notices. All of it lives on `mod.ui`.
4
4
 
5
5
  ```ts
6
6
  mod.ui.pane(pane: Pane<State>): PaneHandle
7
- mod.ui.render<S extends Slot>(slot: S, Component: (props: SlotProps<S>) => RenderElement): void
8
- mod.ui.toast(text: string, options?: { timeoutMs?: number }): void
9
- mod.ui.progress<T>(title: string, task: (report: (step: ProgressStep) => void) => Promise<T>): Promise<T>
7
+ mod.ui.render<P extends Place>(place: P, Component: (props: PlaceProps<P>) => RenderElement): Disposable
8
+ mod.ui.toast: Toast
9
+ mod.ui.copy(text: string): Promise<void>
10
10
  mod.ui.ask(question: string, options?: readonly string[] | AskOptions): Promise<string>
11
11
  mod.ui.scroll(args: UiScrollArgs): Promise<UiScrollResult>
12
+ mod.ui.notice(callId: string, text: string | undefined): void
12
13
  ```
13
14
 
14
15
  Claude Mod Manager (cmod) draws the mod again whenever a value in `mod.state` changes. A render reads `mod.state` and returns what to show. It never draws by hand.
@@ -45,13 +46,13 @@ export function Count({ label, count }: { readonly label: string; readonly count
45
46
  }
46
47
  ```
47
48
 
48
- An element called outside a render throws `<Element> was called outside a render. Use it inside a pane's render or a slot's component.` So build elements inside a pane's `render` or a slot's component, never at module load or in a hook.
49
+ An element built outside a render, at module load or in a hook, holds its props until a render draws it. So a hook can build a toast's `description` and the band draws it later.
49
50
 
50
51
  `drawWith(table, draw, markdown?)` is the function cmod draws a render with. A mod never calls it.
51
52
 
52
53
  ## Components
53
54
 
54
- `ui/components.js` exports controls built from the elements and Claude Code's theme colors, so they follow the person's theme and look the same in every mod. Each is a component like the ones a mod writes: a function of props, used in a pane's `render` or a slot's component.
55
+ `ui/components.js` exports controls built from the elements and Claude Code's theme colors, so they follow the person's theme and look the same in every mod. Each is a component like the ones a mod writes: a function of props, used in a pane's `render` or a place's component.
55
56
 
56
57
  ```ts
57
58
  Tabs(props: { tabs: readonly { key: string; label: string }[]; selected: string; onSelect(key: string): void; children? })
@@ -62,7 +63,7 @@ Tooltip(props: { text: string; children? })
62
63
  Help(props: { text: string; isOpen: boolean; onOpenChange(isOpen: boolean): void })
63
64
  Dialog(props: { title: string; children?; actions })
64
65
  Pagination(props: { page: number; pages: number; onPage(page: number): void })
65
- ProgressBar(props: { done: number; total: number; width?: number })
66
+ Progress(props: { value: number; width?: number })
66
67
  ```
67
68
 
68
69
  - `Tabs` draws a row of tabs over `children`. The selected tab's number is in the accent color and its label bold and underlined. Each other tab is a `Button` whose hotkey is its number, so a pane that holds the keyboard switches tabs by number, Tab, or a click. A hotkey is one digit, so only the first nine tabs have a number.
@@ -72,7 +73,7 @@ ProgressBar(props: { done: number; total: number; width?: number })
72
73
  - `Help` draws a `?` that shows `text` the same way. A press of `?` keeps the card open until the next press, so the keyboard reaches it too. Keep `isOpen` in `mod.state`.
73
74
  - `Dialog` lays out a dialog's title, body, and `actions`, the buttons in a row. Put the safe action first, as the `primary` Button with `autoFocus`.
74
75
  - `Pagination` draws `p: Previous 2 of 5 n: Next`. `p` and `n` are its hotkeys, and an end it cannot pass is dim text.
75
- - `ProgressBar` fills `width` columns, 24 unless given, in proportion to `done` of `total`, and shows the count. The progress lines `mod.ui.progress` draws use it too.
76
+ - `Progress` fills `value` percent of `width` columns, 24 unless given. Put the count or a label beside it in a `Text`, or pass it as a loading toast's `description`.
76
77
  - `Tabs`, `Toggle`, `Help`, and `Pagination` hold no state of their own. Keep their value in `mod.state` and set it in the callback, as in React's controlled components.
77
78
 
78
79
  A pane opened with `focus`, `closeOnEscape`, and `holdToasts` is a dialog. Claude Code shows it as a tab of the dock, beside the mod's other panes, and Escape closes it:
@@ -114,7 +115,7 @@ Open it with `mod.ui.pane(confirmPane).open({ focus: true })`. It draws as:
114
115
  [ Keep it ] [ Delete ]
115
116
  ```
116
117
 
117
- A pane with `Tabs`, a `Tooltip`, a `ProgressBar`, `Pagination`, and a `Toggle` with `Help` draws as:
118
+ A pane with `Tabs`, a `Tooltip`, a `Progress` beside its count, `Pagination`, and a `Toggle` with `Help` draws as:
118
119
 
119
120
  ```text
120
121
  1: Controls 2: Split 3: Files
@@ -248,141 +249,166 @@ export const notes = defineMod({
248
249
  })
249
250
  ```
250
251
 
251
- ## Slots: change a row Claude Code draws
252
+ ## Places: change a row Claude Code draws
252
253
 
253
- `mod.ui.render(slot, Component)` draws in place of one of Claude Code's own rows. `ui/slots.js` exports `slots`:
254
+ `mod.ui.render(place, Component)` draws in place of one of Claude Code's own rows. `mod.js` exports each place as a constant. A message type is a place too, so `Reply` is where Claude Code draws a reply:
254
255
 
255
- | Slot | What Claude Code draws there |
256
+ | Place | What Claude Code draws there |
256
257
  | --- | --- |
257
- | `slots.AssistantMessage` | One text block of Claude's reply |
258
- | `slots.UserMessage` | A user row: the prompt, a task notification, or another agent's message |
259
- | `slots.ToolUse` | A tool call's row |
260
- | `slots.ToolResult` | A tool call's result |
261
- | `slots.ToolGroup` | The condensed row of a group of tool calls |
262
- | `slots.ToolProgress` | A tool's progress line |
263
- | `slots.CommandOutput` | A slash command's output |
264
- | `slots.AskUserQuestion` | The dialog the AskUserQuestion tool opens |
265
- | `slots.InfoNotice` | A dim status line under the logo |
266
- | `slots.Spinner` | The spinner line while Claude works |
267
- | `slots.TurnDuration` | The line that closes a turn |
268
- | `slots.SessionMode` | The session's modes |
269
- | `slots.PromptHint` | The prompt's hint line, such as `? for shortcuts` |
270
- | `slots.AbovePrompt` | The band above the prompt, where surveys draw |
271
-
272
- `Component` gets `SlotProps<S>`: the props Claude Code types as `RenderPropsOf['<Slot>']`, plus `Default`.
258
+ | `Reply` | One text block of Claude's reply |
259
+ | `Prompt` | A prompt row: the person's prompt, a task notification, or another session's message |
260
+ | `ToolCall` | A tool call's row |
261
+ | `ToolResult` | A tool call's result |
262
+ | `Notice` | A dim status line under the logo |
263
+ | `ToolGroup` | The condensed row of a group of tool calls |
264
+ | `ToolProgress` | A tool's progress line |
265
+ | `CommandOutput` | A slash command's output |
266
+ | `AskUserQuestion` | The dialog the AskUserQuestion tool opens |
267
+ | `Spinner` | The spinner line while Claude works |
268
+ | `TurnDuration` | The line that closes a turn |
269
+ | `SessionMode` | The session's modes |
270
+ | `PromptHint` | The prompt's hint line, such as `? for shortcuts` |
271
+ | `AbovePrompt` | The band above the prompt, where surveys draw |
272
+
273
+ `Component` gets `PlaceProps<P>`: the props Claude Code draws that row with, plus `Default`.
273
274
 
274
275
  - `Default` draws what Claude Code, and the renders of other mods, would draw. Pass it props to change what it draws, such as `<Default hint="…" />`. Return `<Default />` to change nothing.
275
276
  - A render adds to what it wraps. In `AbovePrompt`, drawing `<Default />` and a line under it keeps the lines of Claude Code and other mods.
276
277
  - A render must draw the same for the same props. One that calls `Default` a different number of times on a second draw falls back to Claude Code's drawing.
277
- - A render that throws falls back to Claude Code's drawing. cmod logs `<mod>: the <Slot> render threw, so Claude Code draws its own: <error>` once.
278
- - A mod renders each slot once. A second `mod.ui.render` of one slot throws `<mod>: a render of <Slot> is already added. Render each slot once.` For a markdown slot the error names it as `a render of markdown <Kind>`, such as `a render of markdown CodeBlock`.
279
- - `slots.ToolUse` changes a call's own row. A group of calls shows the `slots.ToolGroup` row, which Claude Code builds from the stored message, so a mod that hides a tool's input also sets `isExpanded` on `slots.ToolGroup` to unfold the group into `ToolUse` rows.
280
- - No slot covers the permission dialog.
278
+ - A render that throws, or draws differently on a second pass, falls back to Claude Code's drawing, and cmod shows the error toast `The <Place> render failed` once, with the reason under it.
279
+ - A mod renders each place once. A second `mod.ui.render` of one place throws `<mod>: a render of <Place> is already added. Render each place once.`
280
+ - `render` returns a `Disposable`. `dispose()` takes the render back, and Claude Code draws the row as before ([mod.md](mod.md#disposable)).
281
+ - `ToolCall` changes a call's own row. A group of calls shows the `ToolGroup` row, which Claude Code builds from the stored message, so a mod that hides a tool's input also sets `isExpanded` on `ToolGroup` to unfold the group into `ToolCall` rows.
282
+ - No place covers the permission dialog. `mod.ui.notice` adds a line under it instead ([Notices](#notices)).
281
283
 
282
284
  ```tsx
283
- import { defineMod } from '../node_modules/@cmodjs/core/mod.js'
285
+ import { AbovePrompt, defineMod, PromptHint, UserPromptSubmit } from '../node_modules/@cmodjs/core/mod.js'
284
286
  import { Box, Text } from '../node_modules/@cmodjs/core/ui/elements.js'
285
- import { slots } from '../node_modules/@cmodjs/core/ui/slots.js'
286
287
 
287
288
  export const hints = defineMod({
288
289
  name: 'hints',
289
290
  state: { session: { prompts: 0 } },
290
291
  setup(mod) {
291
- mod.ui.render(slots.PromptHint, ({ hint, Default }) => <Default hint={hint.toUpperCase()} />)
292
- mod.ui.render(slots.AbovePrompt, ({ hasSurvey, Default }) =>
292
+ mod.ui.render(PromptHint, ({ hint, Default }) => <Default hint={hint.toUpperCase()} />)
293
+ mod.ui.render(AbovePrompt, ({ hasSurvey, Default }) =>
293
294
  hasSurvey ? <Default /> : (
294
295
  <Box flexDirection="column">
295
296
  <Default />
296
297
  <Text dimColor>{`Prompts: ${mod.state.session.prompts}`}</Text>
297
298
  </Box>
298
299
  ))
299
- mod.on('UserPromptSubmit', () => {
300
+ mod.on(UserPromptSubmit, () => {
300
301
  mod.state.session.prompts += 1
301
302
  })
302
303
  },
303
304
  })
304
305
  ```
305
306
 
306
- `Slot` and `SlotProps` are the types behind `slots`. Type a component's props as `SlotProps<typeof slots.PromptHint>`.
307
+ `Place` is the type of every place, and `PlaceProps` the props of one. Type a component's props as `PlaceProps<typeof PromptHint>`.
307
308
 
308
- ## Markdown slots: change the blocks of a reply
309
+ ## Markdown places: change the blocks of a reply
309
310
 
310
- `ui/markdown.js` exports `markdownSlots`, one per markdown block:
311
+ Each markdown block of a reply is a place of its own. `ui/markdown.js` exports these places, apart from the rest, because it brings the markdown reader, about 230 KB that takes some 18 ms to load. A mod that imports none of them starts without it.
311
312
 
312
- | Slot | Block | Props beyond `text` and `source` |
313
+ | Place | Block | Props beyond `text` and `source` |
313
314
  | --- | --- | --- |
314
- | `markdownSlots.Heading` | A heading | `depth`, 1 to 6 |
315
- | `markdownSlots.Paragraph` | A paragraph | none |
316
- | `markdownSlots.CodeBlock` | A code block | `lang`, `meta`, `value` |
317
- | `markdownSlots.BlockQuote` | A block quote | none |
318
- | `markdownSlots.List` | A list | `ordered`, `start`, `spread` |
319
- | `markdownSlots.Table` | A table | `align` |
320
- | `markdownSlots.ThematicBreak` | A `---` line | none |
321
- | `markdownSlots.HtmlBlock` | An HTML block | `value` |
315
+ | `Heading` | A heading | `depth`, 1 to 6 |
316
+ | `Paragraph` | A paragraph | none |
317
+ | `CodeBlock` | A code block | `lang`, `meta`, `value` |
318
+ | `BlockQuote` | A block quote | none |
319
+ | `List` | A list | `ordered`, `start`, `spread` |
320
+ | `Table` | A table | `align` |
321
+ | `ThematicBreak` | A `---` line | none |
322
+ | `HtmlBlock` | An HTML block | `value` |
322
323
 
323
324
  - `text` is the block's plain text, one line per list item, table row, or quoted line. `source` is the block's markdown.
324
325
  - `Default` draws the block as Claude Code would. `<Default source="…" />` draws other markdown in its place.
325
- - A markdown slot render draws in Claude's replies and in every `Markdown` element of the mod's panes.
326
+ - A markdown render draws in Claude's replies and in every `Markdown` element of the mod's panes.
326
327
  - While a reply streams in, Claude Code draws a block that is not finished yet. The mod draws it once it is.
327
- - A render that throws falls back to Claude Code's drawing of the block and logs once.
328
+ - A render that throws falls back to Claude Code's drawing of the block, and shows the error toast once.
328
329
 
329
330
  ```tsx
330
331
  import type { RenderElement } from 'claude-code'
331
- import { defineMod } from '../node_modules/@cmodjs/core/mod.js'
332
+ import { defineMod, type PlaceProps } from '../node_modules/@cmodjs/core/mod.js'
333
+ import { CodeBlock } from '../node_modules/@cmodjs/core/ui/markdown.js'
332
334
  import { Text } from '../node_modules/@cmodjs/core/ui/elements.js'
333
- import { markdownSlots } from '../node_modules/@cmodjs/core/ui/markdown.js'
334
- import type { SlotProps } from '../node_modules/@cmodjs/core/ui/slots.js'
335
335
 
336
- function MermaidBlock({ lang, value, Default }: SlotProps<typeof markdownSlots.CodeBlock>): RenderElement {
336
+ function MermaidBlock({ lang, value, Default }: PlaceProps<typeof CodeBlock>): RenderElement {
337
337
  return lang === 'mermaid' ? <Text>{`diagram: ${value}`}</Text> : <Default />
338
338
  }
339
339
 
340
340
  export const diagrams = defineMod({
341
341
  name: 'diagrams',
342
342
  setup(mod) {
343
- mod.ui.render(markdownSlots.CodeBlock, MermaidBlock)
343
+ mod.ui.render(CodeBlock, MermaidBlock)
344
344
  },
345
345
  })
346
346
  ```
347
347
 
348
348
  ```ts
349
- markdownBlocks<S>(text: string, slot: S): Omit<SlotProps<S>, 'Default'>[]
349
+ markdownBlocks<P extends MarkdownPlace>(text: string, place: P): Omit<PlaceProps<P>, 'Default'>[]
350
350
  ```
351
351
 
352
- `markdownBlocks` returns the props of every block of one kind in a markdown text. A test uses it to call a component with real props.
353
-
354
- `MarkdownKind`, `MarkdownReader`, and `MarkdownPiece` are the types behind `markdownSlots`. A mod never builds them.
352
+ `markdownBlocks`, exported from `ui/markdown.js`, returns the props of every block at one markdown place in a markdown text, such as every code block of a reply. A mod reads the blocks of a reply it got in a hook with it, and a test calls a component with real props.
355
353
 
356
- ## Toasts
354
+ ```ts
355
+ import { CodeBlock, markdownBlocks } from '../node_modules/@cmodjs/core/ui/markdown.js'
357
356
 
358
- `mod.ui.toast(text, options?)` shows the person a short message on Claude Code's stack of plugin toasts, over the transcript's top right corner and under the mod's name. It stays 4 seconds, or `timeoutMs`. A click takes it off, and the pointer resting on it holds it. While a pane opened with `holdToasts` is shown, it waits.
357
+ const diagrams = markdownBlocks(reply, CodeBlock).filter((block) => block.lang === 'mermaid')
358
+ ```
359
359
 
360
- ## Progress lines
360
+ ## Notices
361
361
 
362
362
  ```ts
363
- mod.ui.progress<T>(title: string, task: (report: (step: ProgressStep) => void) => Promise<T>): Promise<T>
363
+ mod.ui.notice(callId: string, text: string | undefined): void
364
+ ```
365
+
366
+ `notice` shows `text` as one line under the permission dialog that is open for the tool call `callId`, the call's `tool_use_id`, such as `checked by safe-delete`. `undefined` removes the line, and Claude Code removes it on its own once the call is decided. A call with no open dialog takes no line.
367
+
368
+ ## Toasts
369
+
370
+ `mod.ui.toast` shows the person a short message in the band above the prompt, one row per toast, under the mod's name. Its shape follows [Sonner](https://sonner.emilkowal.ski/toast):
364
371
 
365
- type ProgressStep = { readonly done: number; readonly total: number; readonly label?: string }
372
+ ```ts
373
+ mod.ui.toast(message: string, options?: ToastOptions): ToastId
374
+ mod.ui.toast.success(message, options?) / .info / .warning / .error / .loading
375
+ mod.ui.toast.promise<T>(task: Promise<T> | (() => Promise<T>), messages: { loading: string; success: string | ((value: T) => string); error: string | ((error: unknown) => string) }): { unwrap(): Promise<T> }
376
+ mod.ui.toast.dismiss(id?: ToastId): void
377
+
378
+ type ToastOptions = {
379
+ id?: ToastId
380
+ description?: string | RenderElement
381
+ action?: { label: string; onClick(): unknown }
382
+ duration?: number
383
+ }
366
384
  ```
367
385
 
368
- `progress` draws a line in the band above the prompt while `task` runs, and removes it when `task` settles. The line shows a spinner and the title. After the first `report` it also shows a bar, `done/total`, and the label. `progress` resolves or rejects as `task` does.
386
+ - Each type draws its icon: `✓` success, `ℹ` info, `⚠` warning, `✗` error, and a spinner for loading.
387
+ - A toast stays 4 seconds once the band draws it, or `duration` ms. `Infinity` keeps it until the person closes it, and a loading toast stays until it changes. The time waits while Claude Code shows a survey over the band.
388
+ - A toast with the `id` of a shown toast replaces it in place, so a loading toast turns into its success or error.
389
+ - A text `description` shows its first line dim under the message. A press on the message unfolds the whole text and stops the time, and a second press folds it. An element `description`, such as a `Progress`, draws as it is.
390
+ - `action` draws a button after the message. A press that throws shows an error toast with the cause.
391
+ - `✕` closes the toast. The band shows the newest 3, and `+N more` shows every toast.
392
+ - `promise` shows `loading` while `task` runs, then `success` or `error` in the same row. `unwrap()` resolves or rejects as `task` does.
369
393
 
370
394
  ```ts
371
- import { defineMod } from '../node_modules/@cmodjs/core/mod.js'
395
+ import { defineMod, fs, SessionStart, sessions } from '../node_modules/@cmodjs/core/mod.js'
372
396
 
373
397
  export const indexer = defineMod({
374
398
  name: 'indexer',
375
399
  setup(mod) {
376
- mod.on('SessionStart', async () => {
377
- const entries = await mod.fs.list(mod.projectRoot)
378
- await mod.ui.progress('Indexing', async (report) => {
379
- for (const [index, entry] of entries.entries()) report({ done: index + 1, total: entries.length, label: entry.name })
380
- })
400
+ mod.on(SessionStart, async () => {
401
+ const indexed = fs.list(sessions.current.project).then((entries) => entries.length)
402
+ mod.ui.toast.promise(indexed, { loading: 'Indexing files', success: (count) => `Indexed ${count} files`, error: 'Indexing failed' })
381
403
  })
382
404
  },
383
405
  })
384
406
  ```
385
407
 
408
+ An error cmod catches for a mod, such as a hook that throws, shows as an error toast that stays. Its `Copy` button copies the mod's name and version, the `@cmodjs/core` version, the message, the whole cause, and the stack, for a bug report.
409
+
410
+ `mod.ui.copy(text)` puts `text` on the person's clipboard. It rejects with Claude Code's reason when the terminal cannot take it.
411
+
386
412
  ## Questions
387
413
 
388
414
  ```ts
@@ -394,14 +420,14 @@ type AskOptions = { options?: readonly string[]; header?: string; multiSelect?:
394
420
  `ask` shows a question with 2 to 4 options and resolves the answer. Fewer than two options are padded with Yes and No, and the person may type an answer of their own. `header` is a short chip beside the question, 12 characters at most. `multiSelect` lets the person pick several, and the answer comes back joined by commas.
395
421
 
396
422
  ```ts
397
- import { defineMod } from '../node_modules/@cmodjs/core/mod.js'
423
+ import { defineMod, process, sessions, Stop } from '../node_modules/@cmodjs/core/mod.js'
398
424
 
399
425
  export const committer = defineMod({
400
426
  name: 'committer',
401
427
  setup(mod) {
402
- mod.on('Stop', async () => {
428
+ mod.on(Stop, async () => {
403
429
  const answer = await mod.ui.ask('Commit the changes?', { options: ['Commit', 'Wait'], header: 'Commit' })
404
- if (answer === 'Commit') await mod.process.run(['git', 'commit', '-am', 'wip'], { cwd: mod.projectRoot })
430
+ if (answer === 'Commit') await process.run(['git', 'commit', '-am', 'wip'], { cwd: sessions.current.project })
405
431
  })
406
432
  },
407
433
  })
@@ -419,6 +445,12 @@ mod.ui.scroll(args: { to: 'start' | 'end' | { key: string } | { requestId: strin
419
445
  <Button label="Latest" onPress={() => mod.ui.scroll({ to: 'end', in: 'log' })} />
420
446
  ```
421
447
 
448
+ A [message](session.md#message)'s `id` names the row Claude Code draws it under, so a history pane brings the conversation to the message a person presses. When one row holds several messages, such as a reply with its tool calls, each `id` ends in `:<n>`, and the row is the `id` before it:
449
+
450
+ ```tsx
451
+ <Button label={message.text} onPress={() => mod.ui.scroll({ to: { requestId: message.id.split(':')[0] } })} />
452
+ ```
453
+
422
454
  ## Test the UI
423
455
 
424
- `tested.lines(paneId)` reads an open pane as rows of text. `tested.lines(slot, props)` draws a slot render. `tested.press(paneId, key)` presses a button. `tested.shown.toasts` lists the toasts. [testing.md](testing.md) covers each.
456
+ `tested.lines(paneId)` reads an open pane as rows of text. `tested.lines(place, props)` draws a place render. `tested.press(paneId, key)` presses a button. `tested.shown.toasts` lists the toasts shown now, each as `{ type, message, description? }`. [testing.md](testing.md) covers each.
@@ -1 +1 @@
1
- {"version":3,"file":"check.d.ts","sourceRoot":"","sources":["../src/jobs/check.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,WAAW,CAAA;AAOpC,OAAO,EAAyB,KAAK,MAAM,EAAE,MAAM,+BAA+B,CAAA;AAIlF,wBAAgB,KAAK,CAAC,KAAK,SAAS,MAAM,GAAG,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,EAAE,OAAO,EAAE;IAAE,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IAAC,QAAQ,CAAC,GAAG,EAAE,SAAS,MAAM,EAAE,CAAC;IAAC,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,GAAG,CAAC,IAAI,EAAE,KAAK,CAAC,CA4BjM"}
1
+ {"version":3,"file":"check.d.ts","sourceRoot":"","sources":["../src/jobs/check.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,WAAW,CAAA;AAQpC,OAAO,EAAyB,KAAK,MAAM,EAAE,MAAM,+BAA+B,CAAA;AAIlF,wBAAgB,KAAK,CAAC,KAAK,SAAS,MAAM,GAAG,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,EAAE,OAAO,EAAE;IAAE,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IAAC,QAAQ,CAAC,GAAG,EAAE,SAAS,MAAM,EAAE,CAAC;IAAC,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,GAAG,CAAC,IAAI,EAAE,KAAK,CAAC,CA8BjM"}
package/jobs/check.js CHANGED
@@ -2,7 +2,8 @@ import { longestMs } from '../runtime/deadline.js';
2
2
  import { callEffects } from '../utils/call-effects.js';
3
3
  import { dirname } from '../vendor.js';
4
4
  import { listed, messageOf } from '../utils/text.js';
5
- import { afterCall, modOf, targetWords } from './context.js';
5
+ import { engineOf } from '../runtime/engine.js';
6
+ import { afterCall, targetWords, withinDeadline } from './context.js';
6
7
  import { matchTarget, targetOf } from './permissions/match-target.js';
7
8
  const defaultMs = 60_000;
8
9
  export function check(options) {
@@ -25,7 +26,9 @@ export function check(options) {
25
26
  return [];
26
27
  try {
27
28
  const { command, folder } = triggered;
28
- const { exitCode, stdout, stderr } = await modOf(job, { folder }, workspace, deadline).process.run(command);
29
+ const workTree = await workspace.scope?.workTreeOf(folder);
30
+ const { claude } = engineOf(job.mod);
31
+ const { exitCode, stdout, stderr } = await withinDeadline(job.mod, deadline, command.join(' '), () => claude.process.run(command, { ...(workTree === undefined ? {} : { cwd: workTree }), timeoutMs }));
29
32
  return exitCode === 0 ? [] : [`${command.join(' ')} exited with ${exitCode}:\n${`${stdout}${stderr}`.trim()}`];
30
33
  }
31
34
  catch (error) {
package/jobs/context.d.ts CHANGED
@@ -1,14 +1,13 @@
1
- import type { JobContext, Mod } from '../mod.js';
1
+ import type { JobContext } from '../mod.js';
2
2
  import { type Deadline } from '../runtime/deadline.js';
3
- import type { ToolCall } from '../utils/call-effects.js';
3
+ import type { Call } from '../utils/call-effects.js';
4
4
  import { type Workspace } from './permissions/find-project-scope.js';
5
5
  import { type Target } from './permissions/match-target.js';
6
- export declare function workspaceReader({ mod: { claude } }: JobContext): () => Promise<Workspace>;
7
- export declare function afterCall<State extends object>(job: JobContext<State>, contextAfter: (call: ToolCall, workspace: Workspace) => Promise<readonly string[]>, failed: (error: unknown) => readonly string[]): void;
8
- export declare function modOf<State extends object>(job: JobContext<State>, { call, folder }: {
9
- readonly call?: ToolCall;
6
+ export declare function workspaceReader({ mod }: JobContext): () => Promise<Workspace>;
7
+ export declare function afterCall<State extends object>(job: JobContext<State>, contextAfter: (call: Call, workspace: Workspace) => Promise<readonly string[]>, failed: (error: unknown) => readonly string[]): void;
8
+ export declare function inFolder(call: Call, folder: string, workspace: Workspace): Promise<Call & {
10
9
  readonly folder: string;
11
- }, workspace: Workspace, deadline: Deadline): Mod<State>;
12
- export declare function modWithin<State extends object>({ mod }: JobContext<State>, deadline: Deadline, workTree?: () => Promise<string | undefined>): Mod<State>;
10
+ }>;
11
+ export declare function withinDeadline<Value>(mod: object, deadline: Deadline, subject: string, run: () => Value | Promise<Value>): Promise<Value>;
13
12
  export declare function targetWords(target: Target): string;
14
13
  //# sourceMappingURL=context.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../src/jobs/context.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,GAAG,EAAE,MAAM,WAAW,CAAA;AAEhD,OAAO,EAAkB,KAAK,QAAQ,EAAE,MAAM,wBAAwB,CAAA;AAItE,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,0BAA0B,CAAA;AACxD,OAAO,EAAoB,KAAK,SAAS,EAAE,MAAM,qCAAqC,CAAA;AACtF,OAAO,EAAY,KAAK,MAAM,EAAE,MAAM,+BAA+B,CAAA;AAOrE,wBAAgB,eAAe,CAAC,EAAE,GAAG,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,UAAU,GAAG,MAAM,OAAO,CAAC,SAAS,CAAC,CAOzF;AAaD,wBAAgB,SAAS,CAAC,KAAK,SAAS,MAAM,EAC5C,GAAG,EAAE,UAAU,CAAC,KAAK,CAAC,EACtB,YAAY,EAAE,CAAC,IAAI,EAAE,QAAQ,EAAE,SAAS,EAAE,SAAS,KAAK,OAAO,CAAC,SAAS,MAAM,EAAE,CAAC,EAClF,MAAM,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,SAAS,MAAM,EAAE,GAC5C,IAAI,CAUN;AAMD,wBAAgB,KAAK,CAAC,KAAK,SAAS,MAAM,EAAE,GAAG,EAAE,UAAU,CAAC,KAAK,CAAC,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE;IAAE,QAAQ,CAAC,IAAI,CAAC,EAAE,QAAQ,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CAAE,EAAE,SAAS,EAAE,SAAS,EAAE,QAAQ,EAAE,QAAQ,GAAG,GAAG,CAAC,KAAK,CAAC,CAEjM;AAED,wBAAgB,SAAS,CAAC,KAAK,SAAS,MAAM,EAAE,EAAE,GAAG,EAAE,EAAE,UAAU,CAAC,KAAK,CAAC,EAAE,QAAQ,EAAE,QAAQ,EAAE,QAAQ,GAAE,MAAM,OAAO,CAAC,MAAM,GAAG,SAAS,CAAyB,GAAG,GAAG,CAAC,KAAK,CAAC,CA4B/K;AAwBD,wBAAgB,WAAW,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CASlD"}
1
+ {"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../src/jobs/context.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,WAAW,CAAA;AAC3C,OAAO,EAAkB,KAAK,QAAQ,EAAE,MAAM,wBAAwB,CAAA;AAItE,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,0BAA0B,CAAA;AACpD,OAAO,EAAoB,KAAK,SAAS,EAAE,MAAM,qCAAqC,CAAA;AACtF,OAAO,EAAY,KAAK,MAAM,EAAE,MAAM,+BAA+B,CAAA;AAKrE,wBAAgB,eAAe,CAAC,EAAE,GAAG,EAAE,EAAE,UAAU,GAAG,MAAM,OAAO,CAAC,SAAS,CAAC,CAmB7E;AAED,wBAAgB,SAAS,CAAC,KAAK,SAAS,MAAM,EAAE,GAAG,EAAE,UAAU,CAAC,KAAK,CAAC,EAAE,YAAY,EAAE,CAAC,IAAI,EAAE,IAAI,EAAE,SAAS,EAAE,SAAS,KAAK,OAAO,CAAC,SAAS,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,SAAS,MAAM,EAAE,GAAG,IAAI,CAW3M;AAED,wBAAsB,QAAQ,CAAC,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,GAAG;IAAE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC,CAE5H;AAED,wBAAgB,cAAc,CAAC,KAAK,EAAE,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC,GAAG,OAAO,CAAC,KAAK,CAAC,CAEzI;AAED,wBAAgB,WAAW,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CASlD"}
package/jobs/context.js CHANGED
@@ -1,33 +1,35 @@
1
1
  import { beforeDeadline } from '../runtime/deadline.js';
2
- import { dependencyCalls } from '../runtime/dependencies.js';
2
+ import { engineOf } from '../runtime/engine.js';
3
3
  import { toolInputOf } from '../runtime/tool-calls.js';
4
4
  import { listed } from '../utils/text.js';
5
5
  import { findProjectScope } from './permissions/find-project-scope.js';
6
6
  import { targetOf } from './permissions/match-target.js';
7
- export function workspaceReader({ mod: { claude } }) {
7
+ export function workspaceReader({ mod }) {
8
+ const { claude } = engineOf(mod);
8
9
  let session;
9
10
  return async () => {
10
- session ??= readSession(claude);
11
+ session ??= readSession();
11
12
  const [{ home, scope, fs }, projectRoot, cwd] = await Promise.all([session, claude.session.root(), claude.session.cwd()]);
12
13
  return { projectRoot, cwd, home, fs, scope };
13
14
  };
14
- }
15
- async function readSession(claude) {
16
- const fs = {
17
- read: (path) => claude.fs.read(path),
18
- stat: (path) => claude.fs.stat(path, { resolve: true }),
19
- exists: (path) => claude.fs.exists(path),
20
- };
21
- const home = await claude.env.home();
22
- if (home === undefined)
23
- throw new Error('HOME is not set, so ~ in a path has no meaning. Start Claude Code with HOME set.');
24
- return { home, fs, scope: await findProjectScope(claude.plugin.root, home, fs) };
15
+ async function readSession() {
16
+ const fs = {
17
+ read: (path) => claude.fs.read(path),
18
+ stat: (path) => claude.fs.stat(path, { resolve: true }),
19
+ exists: (path) => claude.fs.exists(path),
20
+ };
21
+ const home = await claude.env.home();
22
+ if (home === undefined)
23
+ throw new Error('HOME is not set, so ~ in a path has no meaning. Start Claude Code with HOME set.');
24
+ return { home, fs, scope: await findProjectScope(claude.plugin.root, home, fs) };
25
+ }
25
26
  }
26
27
  export function afterCall(job, contextAfter, failed) {
28
+ const { router, toolCalls } = engineOf(job.mod);
27
29
  const readWorkspace = workspaceReader(job);
28
- job.mod.claude.on('tool.call', async (e, next) => {
29
- const before = Promise.all([useOf(job, e), readWorkspace()]);
30
- await before.catch(() => undefined);
30
+ router.add('tool.call', async (e, next) => {
31
+ const before = Promise.all([toolCalls.agentOf(e.tool_use_id).then((agent) => ({ tool: e.tool, input: toolInputOf(e), ...agent })), readWorkspace()]);
32
+ await Promise.allSettled([before]);
31
33
  const result = await next(e);
32
34
  if (result.deny !== undefined || result.isError === true)
33
35
  return result;
@@ -35,63 +37,11 @@ export function afterCall(job, contextAfter, failed) {
35
37
  return added.length === 0 ? result : { ...result, context: [...(result.context ?? []), ...added] };
36
38
  });
37
39
  }
38
- async function useOf({ toolCalls }, e) {
39
- return { tool: e.tool, input: toolInputOf(e), ...(await toolCalls.agentOf(e.tool_use_id)) };
40
- }
41
- export function modOf(job, { call, folder }, workspace, deadline) {
42
- return modWithin(job, deadline, async () => workspace.scope?.workTreeOf(call !== undefined && 'path' in call ? call.path : folder));
40
+ export async function inFolder(call, folder, workspace) {
41
+ return { ...call, folder: (await workspace.scope?.workTreeOf('path' in call ? call.path : folder)) ?? folder };
43
42
  }
44
- export function modWithin({ mod }, deadline, workTree = async () => undefined) {
45
- const { claude } = mod;
46
- const within = (call, task) => beforeDeadline(claude, deadline, call, task);
47
- const folderOf = async (cwd) => {
48
- const folder = cwd ?? (await workTree());
49
- return folder === undefined ? {} : { cwd: folder };
50
- };
51
- const overrides = {
52
- process: {
53
- async run(argv, init) {
54
- const folder = await folderOf(init?.cwd);
55
- const timeoutMs = Math.min(init?.timeoutMs ?? deadline.ms, deadline.longestMs ?? deadline.ms);
56
- return beforeDeadline(claude, { ...deadline, ms: timeoutMs }, 'mod.process.run', mod.process.run(argv, { ...init, ...folder, timeoutMs }));
57
- },
58
- spawn: (argv, init) => spawnWithin(claude, deadline, folderOf(init?.cwd).then((folder) => mod.process.spawn(argv, { ...init, ...folder }))),
59
- },
60
- fs: {
61
- read: (path) => within('mod.fs.read', mod.fs.read(path)),
62
- write: (path, text) => within('mod.fs.write', mod.fs.write(path, text)),
63
- list: (path) => within('mod.fs.list', mod.fs.list(path)),
64
- exists: (path) => within('mod.fs.exists', mod.fs.exists(path)),
65
- stat: (path, options) => within('mod.fs.stat', mod.fs.stat(path, options)),
66
- find: (glob) => within('mod.fs.find', mod.fs.find(glob)),
67
- },
68
- http: { fetch: (url, init) => within('mod.http.fetch', mod.http.fetch(url, init)) },
69
- dependencies: dependencyCalls(claude, within),
70
- };
71
- return Object.assign(Object.create(mod), overrides);
72
- }
73
- function spawnWithin(claude, deadline, started) {
74
- async function* pieces() {
75
- const stream = await started;
76
- let finish = () => undefined;
77
- const running = beforeDeadline(claude, deadline, 'mod.process.spawn', new Promise((resolve) => (finish = resolve)));
78
- const passed = running.then(() => new Promise(() => undefined));
79
- try {
80
- for (;;) {
81
- const piece = await Promise.race([stream.next(), passed]);
82
- if (piece.done === true)
83
- return piece.value;
84
- yield piece.value;
85
- }
86
- }
87
- finally {
88
- finish();
89
- void stream.return?.();
90
- }
91
- }
92
- const result = started.then((stream) => stream.result);
93
- result.catch(() => undefined);
94
- return Object.assign(pieces(), { result });
43
+ export function withinDeadline(mod, deadline, subject, run) {
44
+ return beforeDeadline(engineOf(mod).claude, deadline, subject, Promise.resolve().then(run));
95
45
  }
96
46
  export function targetWords(target) {
97
47
  const { key, patterns, exclusions } = targetOf(target);
@@ -1,9 +1,11 @@
1
- import { type ToolCall } from '../../utils/call-effects.js';
1
+ import { type Call } from '../../utils/call-effects.js';
2
2
  import type { Workspace } from './find-project-scope.js';
3
3
  import { type TargetCalls, type TargetKey, type TargetOf } from './match-target.js';
4
4
  export type Rule<Mod> = {
5
5
  [Key in TargetKey]: TargetOf<Key> & {
6
- when?: (call: TargetCalls[Key], mod: Mod) => boolean | Promise<boolean>;
6
+ when?: (call: TargetCalls[Key] & {
7
+ readonly folder: string;
8
+ }, mod: Mod) => boolean | Promise<boolean>;
7
9
  reason?: string;
8
10
  };
9
11
  }[TargetKey];
@@ -17,5 +19,5 @@ export type Verdict = {
17
19
  reason?: string;
18
20
  };
19
21
  export declare function stricterVerdict<Result extends Verdict>(first: Result, second: Result): Result;
20
- export declare function decidePermission<Mod>(rules: PermissionRules<Mod>, use: ToolCall, workspace: Workspace, modOf: (call: ToolCall, folder: string) => Mod): Promise<Verdict | undefined>;
22
+ export declare function decidePermission<Mod>(rules: PermissionRules<Mod>, use: Call, workspace: Workspace, mod: Mod, within: (run: () => boolean | Promise<boolean>) => Promise<boolean>): Promise<Verdict | undefined>;
21
23
  //# sourceMappingURL=decide-permission.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"decide-permission.d.ts","sourceRoot":"","sources":["../../src/jobs/permissions/decide-permission.ts"],"names":[],"mappings":"AAEA,OAAO,EAA6C,KAAK,QAAQ,EAAE,MAAM,6BAA6B,CAAA;AACtG,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAA;AACxD,OAAO,EAAuE,KAAK,WAAW,EAAE,KAAK,SAAS,EAAE,KAAK,QAAQ,EAAE,MAAM,mBAAmB,CAAA;AAGxJ,MAAM,MAAM,IAAI,CAAC,GAAG,IAAI;KACrB,GAAG,IAAI,SAAS,GAAG,QAAQ,CAAC,GAAG,CAAC,GAAG;QAClC,IAAI,CAAC,EAAE,CAAC,IAAI,EAAE,WAAW,CAAC,GAAG,CAAC,EAAE,GAAG,EAAE,GAAG,KAAK,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAA;QACvE,MAAM,CAAC,EAAE,MAAM,CAAA;KAChB;CACF,CAAC,SAAS,CAAC,CAAA;AAEZ,MAAM,MAAM,eAAe,CAAC,GAAG,IAAI;IAAE,IAAI,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;IAAC,GAAG,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,EAAE,CAAA;CAAE,CAAA;AAC5E,MAAM,MAAM,QAAQ,GAAG,OAAO,GAAG,KAAK,GAAG,MAAM,CAAA;AAC/C,MAAM,MAAM,OAAO,GAAG;IAAE,QAAQ,EAAE,QAAQ,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,CAAA;AAI7D,wBAAgB,eAAe,CAAC,MAAM,SAAS,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAE7F;AAED,wBAAsB,gBAAgB,CAAC,GAAG,EAAE,KAAK,EAAE,eAAe,CAAC,GAAG,CAAC,EAAE,GAAG,EAAE,QAAQ,EAAE,SAAS,EAAE,SAAS,EAAE,KAAK,EAAE,CAAC,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,KAAK,GAAG,GAAG,OAAO,CAAC,OAAO,GAAG,SAAS,CAAC,CAe1L"}
1
+ {"version":3,"file":"decide-permission.d.ts","sourceRoot":"","sources":["../../src/jobs/permissions/decide-permission.ts"],"names":[],"mappings":"AAEA,OAAO,EAA6C,KAAK,IAAI,EAAE,MAAM,6BAA6B,CAAA;AAClG,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAA;AACxD,OAAO,EAAuE,KAAK,WAAW,EAAE,KAAK,SAAS,EAAE,KAAK,QAAQ,EAAE,MAAM,mBAAmB,CAAA;AAIxJ,MAAM,MAAM,IAAI,CAAC,GAAG,IAAI;KACrB,GAAG,IAAI,SAAS,GAAG,QAAQ,CAAC,GAAG,CAAC,GAAG;QAClC,IAAI,CAAC,EAAE,CAAC,IAAI,EAAE,WAAW,CAAC,GAAG,CAAC,GAAG;YAAE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;SAAE,EAAE,GAAG,EAAE,GAAG,KAAK,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAA;QACrG,MAAM,CAAC,EAAE,MAAM,CAAA;KAChB;CACF,CAAC,SAAS,CAAC,CAAA;AAEZ,MAAM,MAAM,eAAe,CAAC,GAAG,IAAI;IAAE,IAAI,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;IAAC,GAAG,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,EAAE,CAAA;CAAE,CAAA;AAC5E,MAAM,MAAM,QAAQ,GAAG,OAAO,GAAG,KAAK,GAAG,MAAM,CAAA;AAC/C,MAAM,MAAM,OAAO,GAAG;IAAE,QAAQ,EAAE,QAAQ,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,CAAA;AAI7D,wBAAgB,eAAe,CAAC,MAAM,SAAS,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAE7F;AAED,wBAAsB,gBAAgB,CAAC,GAAG,EAAE,KAAK,EAAE,eAAe,CAAC,GAAG,CAAC,EAAE,GAAG,EAAE,IAAI,EAAE,SAAS,EAAE,SAAS,EAAE,GAAG,EAAE,GAAG,EAAE,MAAM,EAAE,CAAC,GAAG,EAAE,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,KAAK,OAAO,CAAC,OAAO,CAAC,GAAG,OAAO,CAAC,OAAO,GAAG,SAAS,CAAC,CAerN"}
@@ -3,16 +3,17 @@ import { messageOf } from '../../utils/text.js';
3
3
  import { callBaseOf, callEffects } from '../../utils/call-effects.js';
4
4
  import { folderInScope, matchTarget, targetOf } from './match-target.js';
5
5
  import { resolve } from '../../vendor.js';
6
+ import { inFolder } from '../context.js';
6
7
  const wordBoundary = '\\s;&|()<>\'"`=';
7
8
  export function stricterVerdict(first, second) {
8
9
  return strictness.indexOf(second.decision) > strictness.indexOf(first.decision) ? second : first;
9
10
  }
10
- export async function decidePermission(rules, use, workspace, modOf) {
11
+ export async function decidePermission(rules, use, workspace, mod, within) {
11
12
  try {
12
13
  const effects = callEffects(use, workspace);
13
14
  for (const decision of ['deny', 'ask']) {
14
15
  for (const rule of rules[decision] ?? []) {
15
- const outcome = await ruleOutcome(rule, use, effects, workspace, modOf);
16
+ const outcome = await ruleOutcome(rule, use, effects, workspace, mod, within);
16
17
  if (outcome === undefined)
17
18
  continue;
18
19
  const reason = [rule.reason, outcome.failure === undefined ? undefined : `Its when check failed: ${outcome.failure}`].filter((part) => part !== undefined).join(' ');
@@ -25,7 +26,7 @@ export async function decidePermission(rules, use, workspace, modOf) {
25
26
  return { decision: 'deny', reason: `The permissions job failed, so it denies the call: ${messageOf(error)}` };
26
27
  }
27
28
  }
28
- async function ruleOutcome(rule, use, effects, workspace, modOf) {
29
+ async function ruleOutcome(rule, use, effects, workspace, mod, within) {
29
30
  const matched = await matchTarget(rule, use, effects, workspace);
30
31
  const calls = matched.length > 0 ? matched : await unparsedMatches(rule, use, effects, workspace);
31
32
  if (calls.length === 0)
@@ -36,7 +37,8 @@ async function ruleOutcome(rule, use, effects, workspace, modOf) {
36
37
  for (const { call, access, folder } of calls) {
37
38
  const fullCall = access?.contents === undefined ? call : { ...call, ...(await access.contents()) };
38
39
  try {
39
- if (await when(fullCall, modOf(fullCall, folder)))
40
+ const placed = await inFolder(fullCall, folder, workspace);
41
+ if (await within(() => when(placed, mod)))
40
42
  return { failure: undefined };
41
43
  }
42
44
  catch (error) {
@@ -1,4 +1,4 @@
1
- import type { FileSystem } from '../../utils/paths.js';
1
+ import { type FileSystem } from '../../utils/paths.js';
2
2
  export type ProjectScope = {
3
3
  workTreeOf: (path: string) => Promise<string | undefined>;
4
4
  };
@@ -1 +1 @@
1
- {"version":3,"file":"find-project-scope.d.ts","sourceRoot":"","sources":["../../src/jobs/permissions/find-project-scope.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAA;AAGtD,MAAM,MAAM,YAAY,GAAG;IAAE,UAAU,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAAA;CAAE,CAAA;AAExF,MAAM,MAAM,SAAS,GAAG;IAAE,WAAW,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,UAAU,CAAC;IAAC,KAAK,EAAE,YAAY,GAAG,SAAS,CAAA;CAAE,CAAA;AAE3H,wBAAsB,gBAAgB,CAAC,UAAU,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,UAAU,GAAG,OAAO,CAAC,YAAY,GAAG,SAAS,CAAC,CAwB1H"}
1
+ {"version":3,"file":"find-project-scope.d.ts","sourceRoot":"","sources":["../../src/jobs/permissions/find-project-scope.ts"],"names":[],"mappings":"AAAA,OAAO,EAAa,KAAK,UAAU,EAAE,MAAM,sBAAsB,CAAA;AAGjE,MAAM,MAAM,YAAY,GAAG;IAAE,UAAU,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAAA;CAAE,CAAA;AAExF,MAAM,MAAM,SAAS,GAAG;IAAE,WAAW,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,UAAU,CAAC;IAAC,KAAK,EAAE,YAAY,GAAG,SAAS,CAAA;CAAE,CAAA;AAE3H,wBAAsB,gBAAgB,CAAC,UAAU,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,UAAU,GAAG,OAAO,CAAC,YAAY,GAAG,SAAS,CAAC,CAwB1H"}