@cmodjs/core 0.3.0 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (280) hide show
  1. package/docs/commands.md +61 -10
  2. package/docs/dependencies.md +16 -14
  3. package/docs/hooks.md +64 -29
  4. package/docs/index.md +29 -25
  5. package/docs/install-steps.md +13 -9
  6. package/docs/jobs.md +59 -111
  7. package/docs/mod.md +227 -175
  8. package/docs/options.md +2 -2
  9. package/docs/permissions.md +27 -16
  10. package/docs/session.md +311 -0
  11. package/docs/state.md +11 -7
  12. package/docs/testing.md +68 -23
  13. package/docs/ui.md +116 -76
  14. package/jobs/check.d.ts.map +1 -1
  15. package/jobs/check.js +5 -2
  16. package/jobs/context.d.ts +7 -8
  17. package/jobs/context.d.ts.map +1 -1
  18. package/jobs/context.js +23 -73
  19. package/jobs/permissions/decide-permission.d.ts +5 -3
  20. package/jobs/permissions/decide-permission.d.ts.map +1 -1
  21. package/jobs/permissions/decide-permission.js +6 -4
  22. package/jobs/permissions/find-project-scope.d.ts +1 -1
  23. package/jobs/permissions/find-project-scope.d.ts.map +1 -1
  24. package/jobs/permissions/find-project-scope.js +8 -11
  25. package/jobs/permissions/match-target.d.ts +2 -2
  26. package/jobs/permissions/match-target.d.ts.map +1 -1
  27. package/jobs/permissions.d.ts +1 -1
  28. package/jobs/permissions.d.ts.map +1 -1
  29. package/jobs/permissions.js +6 -4
  30. package/jobs/program.d.ts.map +1 -1
  31. package/jobs/program.js +14 -4
  32. package/jobs/prompt.d.ts +4 -2
  33. package/jobs/prompt.d.ts.map +1 -1
  34. package/jobs/prompt.js +16 -15
  35. package/jobs/schedule.d.ts +2 -2
  36. package/jobs/schedule.d.ts.map +1 -1
  37. package/jobs/schedule.js +9 -9
  38. package/jobs/slash-command.d.ts +4 -4
  39. package/jobs/slash-command.d.ts.map +1 -1
  40. package/jobs/slash-command.js +14 -9
  41. package/jobs/status-line.d.ts +1 -11
  42. package/jobs/status-line.d.ts.map +1 -1
  43. package/jobs/status-line.js +8 -12
  44. package/jobs/tool.d.ts +2 -2
  45. package/jobs/tool.d.ts.map +1 -1
  46. package/jobs/tool.js +15 -9
  47. package/mod.d.ts +178 -75
  48. package/mod.d.ts.map +1 -1
  49. package/mod.js +127 -3
  50. package/options.d.ts +2 -2
  51. package/options.js +19 -19
  52. package/package.json +3 -3
  53. package/records.d.ts +13 -8
  54. package/records.d.ts.map +1 -1
  55. package/records.js +33 -17
  56. package/register.d.ts.map +1 -1
  57. package/register.js +34 -5
  58. package/runtime/announce.d.ts +18 -0
  59. package/runtime/announce.d.ts.map +1 -0
  60. package/runtime/announce.js +68 -0
  61. package/runtime/before.d.ts +32 -0
  62. package/runtime/before.d.ts.map +1 -0
  63. package/runtime/before.js +214 -0
  64. package/runtime/bound.d.ts +4 -0
  65. package/runtime/bound.d.ts.map +1 -0
  66. package/runtime/bound.js +9 -0
  67. package/runtime/claude.d.ts +4 -2
  68. package/runtime/claude.d.ts.map +1 -1
  69. package/runtime/conversations.d.ts +17 -0
  70. package/runtime/conversations.d.ts.map +1 -0
  71. package/runtime/conversations.js +563 -0
  72. package/runtime/deadline.d.ts +0 -1
  73. package/runtime/deadline.d.ts.map +1 -1
  74. package/runtime/deadline.js +1 -1
  75. package/runtime/dependencies.d.ts +2 -0
  76. package/runtime/dependencies.d.ts.map +1 -1
  77. package/runtime/dependencies.js +10 -1
  78. package/runtime/engine.d.ts +13 -0
  79. package/runtime/engine.d.ts.map +1 -0
  80. package/runtime/engine.js +10 -0
  81. package/runtime/grants.d.ts +4 -0
  82. package/runtime/grants.d.ts.map +1 -1
  83. package/runtime/grants.js +68 -31
  84. package/runtime/hooks.d.ts +5 -4
  85. package/runtime/hooks.d.ts.map +1 -1
  86. package/runtime/hooks.js +29 -9
  87. package/runtime/installer.d.ts +1 -1
  88. package/runtime/installer.d.ts.map +1 -1
  89. package/runtime/installer.js +17 -14
  90. package/runtime/lifecycle.d.ts +3 -1
  91. package/runtime/lifecycle.d.ts.map +1 -1
  92. package/runtime/lifecycle.js +414 -212
  93. package/runtime/metadata.d.ts +3 -1
  94. package/runtime/metadata.d.ts.map +1 -1
  95. package/runtime/metadata.js +25 -14
  96. package/runtime/options.d.ts +3 -1
  97. package/runtime/options.d.ts.map +1 -1
  98. package/runtime/options.js +4 -4
  99. package/runtime/programs.d.ts +8 -0
  100. package/runtime/programs.d.ts.map +1 -1
  101. package/runtime/programs.js +39 -1
  102. package/runtime/router.d.ts +2 -1
  103. package/runtime/router.d.ts.map +1 -1
  104. package/runtime/router.js +11 -3
  105. package/runtime/sessions.d.ts +58 -0
  106. package/runtime/sessions.d.ts.map +1 -0
  107. package/runtime/sessions.js +433 -0
  108. package/runtime/state.d.ts +6 -2
  109. package/runtime/state.d.ts.map +1 -1
  110. package/runtime/state.js +46 -13
  111. package/runtime/toaster.d.ts +24 -0
  112. package/runtime/toaster.d.ts.map +1 -0
  113. package/runtime/toaster.js +286 -0
  114. package/runtime/ui.d.ts +6 -17
  115. package/runtime/ui.d.ts.map +1 -1
  116. package/runtime/ui.js +62 -118
  117. package/session/context/context.d.ts +12 -0
  118. package/session/context/context.d.ts.map +1 -0
  119. package/session/context/context.js +2 -0
  120. package/session/context/turn/message/document.d.ts +7 -0
  121. package/session/context/turn/message/document.d.ts.map +1 -0
  122. package/session/context/turn/message/document.js +1 -0
  123. package/session/context/turn/message/message.d.ts +55 -0
  124. package/session/context/turn/message/message.d.ts.map +1 -0
  125. package/session/context/turn/message/message.js +178 -0
  126. package/session/context/turn/message/note.d.ts +8 -0
  127. package/session/context/turn/message/note.d.ts.map +1 -0
  128. package/session/context/turn/message/note.js +1 -0
  129. package/session/context/turn/message/notice.d.ts +6 -0
  130. package/session/context/turn/message/notice.d.ts.map +1 -0
  131. package/session/context/turn/message/notice.js +1 -0
  132. package/session/context/turn/message/prompt.d.ts +8 -0
  133. package/session/context/turn/message/prompt.d.ts.map +1 -0
  134. package/session/context/turn/message/prompt.js +1 -0
  135. package/session/context/turn/message/reply.d.ts +6 -0
  136. package/session/context/turn/message/reply.d.ts.map +1 -0
  137. package/session/context/turn/message/reply.js +1 -0
  138. package/session/context/turn/message/sender.d.ts +14 -0
  139. package/session/context/turn/message/sender.d.ts.map +1 -0
  140. package/session/context/turn/message/sender.js +30 -0
  141. package/session/context/turn/message/skill.d.ts +7 -0
  142. package/session/context/turn/message/skill.d.ts.map +1 -0
  143. package/session/context/turn/message/skill.js +1 -0
  144. package/session/context/turn/message/tool-call.d.ts +9 -0
  145. package/session/context/turn/message/tool-call.d.ts.map +1 -0
  146. package/session/context/turn/message/tool-call.js +1 -0
  147. package/session/context/turn/message/tool-result.d.ts +8 -0
  148. package/session/context/turn/message/tool-result.d.ts.map +1 -0
  149. package/session/context/turn/message/tool-result.js +1 -0
  150. package/session/context/turn/turn.d.ts +13 -0
  151. package/session/context/turn/turn.d.ts.map +1 -0
  152. package/session/context/turn/turn.js +2 -0
  153. package/session/conversation.d.ts +73 -0
  154. package/session/conversation.d.ts.map +1 -0
  155. package/session/conversation.js +146 -0
  156. package/session/goal/goal.d.ts +11 -0
  157. package/session/goal/goal.d.ts.map +1 -0
  158. package/session/goal/goal.js +1 -0
  159. package/session/goal/task.d.ts +16 -0
  160. package/session/goal/task.d.ts.map +1 -0
  161. package/session/goal/task.js +3 -0
  162. package/session/session.d.ts +158 -0
  163. package/session/session.d.ts.map +1 -0
  164. package/session/session.js +2 -0
  165. package/src/jobs/check.ts +7 -4
  166. package/src/jobs/context.ts +24 -82
  167. package/src/jobs/permissions/decide-permission.ts +10 -8
  168. package/src/jobs/permissions/find-project-scope.ts +8 -14
  169. package/src/jobs/permissions/match-target.ts +3 -3
  170. package/src/jobs/permissions.ts +8 -6
  171. package/src/jobs/program.ts +13 -4
  172. package/src/jobs/prompt.ts +21 -24
  173. package/src/jobs/schedule.ts +14 -14
  174. package/src/jobs/slash-command.ts +20 -14
  175. package/src/jobs/status-line.ts +9 -21
  176. package/src/jobs/tool.ts +17 -11
  177. package/src/mod.ts +252 -97
  178. package/src/options.ts +22 -22
  179. package/src/records.ts +40 -23
  180. package/src/register.ts +34 -5
  181. package/src/runtime/announce.ts +76 -0
  182. package/src/runtime/before.ts +200 -0
  183. package/src/runtime/bound.ts +12 -0
  184. package/src/runtime/claude.ts +7 -2
  185. package/src/runtime/conversations.ts +579 -0
  186. package/src/runtime/deadline.ts +2 -2
  187. package/src/runtime/dependencies.ts +13 -1
  188. package/src/runtime/engine.ts +23 -0
  189. package/src/runtime/grants.ts +69 -29
  190. package/src/runtime/hooks.ts +39 -14
  191. package/src/runtime/installer.ts +16 -14
  192. package/src/runtime/lifecycle.ts +413 -201
  193. package/src/runtime/metadata.ts +37 -14
  194. package/src/runtime/options.ts +19 -4
  195. package/src/runtime/programs.ts +40 -1
  196. package/src/runtime/router.ts +11 -4
  197. package/src/runtime/sessions.ts +447 -0
  198. package/src/runtime/state.ts +50 -14
  199. package/src/runtime/toaster.ts +334 -0
  200. package/src/runtime/ui.ts +65 -143
  201. package/src/session/context/context.ts +14 -0
  202. package/src/session/context/turn/message/document.ts +8 -0
  203. package/src/session/context/turn/message/message.ts +203 -0
  204. package/src/session/context/turn/message/note.ts +9 -0
  205. package/src/session/context/turn/message/notice.ts +7 -0
  206. package/src/session/context/turn/message/prompt.ts +9 -0
  207. package/src/session/context/turn/message/reply.ts +7 -0
  208. package/src/session/context/turn/message/sender.ts +38 -0
  209. package/src/session/context/turn/message/skill.ts +8 -0
  210. package/src/session/context/turn/message/tool-call.ts +10 -0
  211. package/src/session/context/turn/message/tool-result.ts +9 -0
  212. package/src/session/context/turn/turn.ts +15 -0
  213. package/src/session/conversation.ts +194 -0
  214. package/src/session/goal/goal.ts +10 -0
  215. package/src/session/goal/task.ts +19 -0
  216. package/src/session/session.ts +141 -0
  217. package/src/testing/fake-claude.ts +25 -17
  218. package/src/testing/fake-elements.ts +3 -2
  219. package/src/testing/fake-files.ts +1 -1
  220. package/src/testing/fake-sessions.ts +277 -0
  221. package/src/testing.ts +138 -35
  222. package/src/ui/components.ts +3 -3
  223. package/src/ui/elements.ts +22 -8
  224. package/src/ui/markdown.ts +22 -26
  225. package/src/ui/places.ts +49 -0
  226. package/src/utils/call-effects.ts +5 -5
  227. package/src/utils/changelog.ts +32 -0
  228. package/src/utils/disposable.ts +3 -0
  229. package/src/utils/matcher.ts +37 -0
  230. package/src/utils/metadata.ts +17 -15
  231. package/src/utils/parse-shell.ts +9 -8
  232. package/src/utils/paths.ts +21 -4
  233. package/src/utils/text.ts +4 -0
  234. package/testing/fake-claude.d.ts +5 -1
  235. package/testing/fake-claude.d.ts.map +1 -1
  236. package/testing/fake-claude.js +21 -12
  237. package/testing/fake-elements.js +4 -2
  238. package/testing/fake-files.js +1 -1
  239. package/testing/fake-sessions.d.ts +81 -0
  240. package/testing/fake-sessions.d.ts.map +1 -0
  241. package/testing/fake-sessions.js +249 -0
  242. package/testing.d.ts +34 -13
  243. package/testing.d.ts.map +1 -1
  244. package/testing.js +115 -23
  245. package/ui/components.d.ts +2 -3
  246. package/ui/components.d.ts.map +1 -1
  247. package/ui/components.js +3 -3
  248. package/ui/elements.d.ts.map +1 -1
  249. package/ui/elements.js +19 -9
  250. package/ui/markdown.d.ts +35 -33
  251. package/ui/markdown.d.ts.map +1 -1
  252. package/ui/markdown.js +14 -16
  253. package/ui/places.d.ts +43 -0
  254. package/ui/places.d.ts.map +1 -0
  255. package/ui/places.js +31 -0
  256. package/utils/call-effects.d.ts +2 -1
  257. package/utils/call-effects.d.ts.map +1 -1
  258. package/utils/call-effects.js +3 -3
  259. package/utils/changelog.d.ts +9 -0
  260. package/utils/changelog.d.ts.map +1 -0
  261. package/utils/changelog.js +29 -0
  262. package/utils/disposable.d.ts +4 -0
  263. package/utils/disposable.d.ts.map +1 -0
  264. package/utils/disposable.js +1 -0
  265. package/utils/matcher.d.ts +13 -0
  266. package/utils/matcher.d.ts.map +1 -0
  267. package/utils/matcher.js +32 -0
  268. package/utils/metadata.d.ts +3 -3
  269. package/utils/metadata.js +18 -14
  270. package/utils/parse-shell.js +9 -7
  271. package/utils/paths.d.ts +4 -1
  272. package/utils/paths.d.ts.map +1 -1
  273. package/utils/paths.js +19 -4
  274. package/utils/text.d.ts +1 -0
  275. package/utils/text.d.ts.map +1 -1
  276. package/utils/text.js +3 -0
  277. package/src/ui/slots.ts +0 -36
  278. package/ui/slots.d.ts +0 -208
  279. package/ui/slots.d.ts.map +0 -1
  280. package/ui/slots.js +0 -19
package/src/records.ts CHANGED
@@ -19,23 +19,31 @@ export type Steps = {
19
19
  permissions?: readonly string[]
20
20
  }
21
21
 
22
- export const listPermissions = ['network', 'run', 'files'] as const
23
- export const flagPermissions = ['conversation', 'prompt', 'model', 'agents', 'tools', 'config', 'approve'] as const
22
+ export const listPermissions = ['network', 'run', 'files', 'config'] as const
23
+ export const flagPermissions = ['conversation', 'prompt', 'model', 'tasks', 'tools', 'approve'] as const
24
24
  export type ListPermission = (typeof listPermissions)[number]
25
25
  export type FlagPermission = (typeof flagPermissions)[number]
26
26
  const permissionNames: readonly string[] = [...listPermissions, ...flagPermissions]
27
27
  const hostName = /^[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?(\.[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?)*$/
28
28
  const programName = /^[A-Za-z0-9][A-Za-z0-9._+-]*$/
29
+ const settingName = /^[A-Za-z][A-Za-z0-9]*$/
30
+ const listItemHint: Readonly<Record<ListPermission, string>> = {
31
+ network: 'a host alone, such as "api.github.com", or a socket path, such as "/var/run/docker.sock"',
32
+ run: 'a program\'s command, such as "gh"',
33
+ files: 'a path that starts with ~/ or /, such as "~/.zshrc"',
34
+ config: 'a Claude Code setting\'s name, such as "cleanupPeriodDays"',
35
+ }
29
36
 
30
37
  export type ReadFile = (path: string) => Promise<string | undefined>
31
38
 
32
39
  export type RunnerEvent =
33
- | { kind: 'progress'; done: number; total: number; label: string }
34
- | { kind: 'log'; text: string }
35
- | { kind: 'needs-consent'; sha256: string; install: string; uninstall: string; keys: string; permissions: readonly string[] }
36
- | { kind: 'done'; name: string; version?: string }
37
- | { kind: 'missing'; name: string }
38
- | { kind: 'failed'; code: number; message: string }
40
+ | { type: 'progress'; done: number; total: number; label: string }
41
+ | { type: 'log'; text: string }
42
+ | { type: 'needs-consent'; sha256: string; install: string; uninstall: string; keys: string; permissions: readonly string[] }
43
+ | { type: 'done'; name: string; version?: string }
44
+ | { type: 'missing'; name: string }
45
+ | { type: 'retries'; when: 'changed' | 'later' }
46
+ | { type: 'failed'; code: number; message: string }
39
47
 
40
48
  export const pluginName = /^[A-Za-z0-9][A-Za-z0-9._-]*$/
41
49
 
@@ -153,12 +161,12 @@ function readPermissions(steps: Record<string, unknown>): string[] {
153
161
  items.push('run:*')
154
162
  continue
155
163
  }
156
- const example = name === 'network' ? '["api.github.com"]' : name === 'run' ? '["gh"], or "*" for any program' : '["~/.zshrc"]'
164
+ const example = { network: '["api.github.com"]', run: '["gh"], or "*" for any program', files: '["~/.zshrc"]', config: '["cleanupPeriodDays"]' }[name]
157
165
  if (!Array.isArray(value) || value.length === 0 || !value.every((each) => typeof each === 'string')) throw new Error(`${at} sets "${name}" to ${JSON.stringify(value)}. Write a list, such as "${name}": ${example}.`)
158
- for (const each of value as string[]) {
166
+ for (const each of value) {
159
167
  const isPath = /^(~\/|\/)/.test(each) && !each.split('/').includes('..')
160
- const isValid = name === 'network' ? hostName.test(each) || isPath : name === 'run' ? programName.test(each) : isPath
161
- if (!isValid) throw new Error(`${at} lists "${each}" under "${name}". Write ${name === 'network' ? 'a host alone, such as "api.github.com", or a socket path, such as "/var/run/docker.sock"' : name === 'run' ? 'a program\'s command, such as "gh"' : 'a path that starts with ~/ or /, such as "~/.zshrc"'}.`)
168
+ const isValid = { network: hostName.test(each) || isPath, run: programName.test(each), files: isPath, config: settingName.test(each) }[name]
169
+ if (isValid !== true) throw new Error(`${at} lists "${each}" under "${name}". Write ${listItemHint[name as ListPermission]}.`)
162
170
  items.push(`${name}:${each}`)
163
171
  }
164
172
  }
@@ -176,17 +184,17 @@ export function permissionWords(item: string): string {
176
184
  case 'files':
177
185
  return `Change ${target}`
178
186
  case 'conversation':
179
- return 'Read this conversation'
187
+ return 'Read your Claude Code conversations'
180
188
  case 'prompt':
181
189
  return 'Add text Claude reads and start turns'
182
190
  case 'model':
183
191
  return 'Ask a model, which uses your plan'
184
- case 'agents':
185
- return 'Start agents'
192
+ case 'tasks':
193
+ return 'Start tasks that work beside Claude'
186
194
  case 'tools':
187
195
  return "Use and change Claude's tool calls"
188
196
  case 'config':
189
- return 'Change your Claude Code settings'
197
+ return `Change ${target} in your Claude Code settings`
190
198
  case 'approve':
191
199
  return "Approve Claude's tool calls for you"
192
200
  default:
@@ -196,6 +204,8 @@ export function permissionWords(item: string): string {
196
204
 
197
205
  export const settingsPagesMethod = 'cmod:settingsPages'
198
206
 
207
+ export const changelogPage = 'changelog'
208
+
199
209
  export const openPageMethod = 'cmod:openPage'
200
210
 
201
211
  export const pendingStepsMethod = 'cmod:pendingSteps'
@@ -240,7 +250,10 @@ function readKeys(keys: unknown): Steps {
240
250
  return { keys: commands }
241
251
  }
242
252
 
253
+ export const coreVersion = '1.5.0'
254
+
243
255
  export function oldestCmodFor(steps: Steps): string {
256
+ if (steps.permissions?.some((item) => item === 'tasks' || item.startsWith('config:')) === true) return '1.0.0'
244
257
  if (steps.permissions !== undefined) return '0.2.0'
245
258
  return Object.keys(steps.keys ?? {}).length > 0 ? '0.1.12' : '0.0.0'
246
259
  }
@@ -303,7 +316,7 @@ export async function scriptsSha256(steps: Steps, files: { read: ReadFile; list:
303
316
  }
304
317
 
305
318
  export function formatEvent(event: RunnerEvent): string {
306
- switch (event.kind) {
319
+ switch (event.type) {
307
320
  case 'progress':
308
321
  return `progress ${event.done} ${event.total}${event.label === '' ? '' : ` ${event.label}`}`
309
322
  case 'log':
@@ -316,24 +329,28 @@ export function formatEvent(event: RunnerEvent): string {
316
329
  return `missing ${event.name}`
317
330
  case 'failed':
318
331
  return `failed ${event.code}\t${event.message.replace(/\s*\n\s*/g, ' ')}`
332
+ case 'retries':
333
+ return `retries ${event.when}`
319
334
  }
320
335
  }
321
336
 
322
337
  export function parseEvent(line: string): RunnerEvent {
323
338
  const progress = /^progress (\d+) (\d+)(?: (.*))?$/.exec(line)
324
- if (progress !== null) return { kind: 'progress', done: Number(progress[1]), total: Number(progress[2]), label: progress[3] ?? '' }
339
+ if (progress !== null) return { type: 'progress', done: Number(progress[1]), total: Number(progress[2]), label: progress[3] ?? '' }
325
340
  const consent = /^needs-consent (\S+)\t([^\t]*)\t([^\t]*)(?:\t(.*))?$/.exec(line)
326
341
  if (consent !== null) {
327
342
  const [keys = '', ...permissions] = (consent[4] ?? '').split('\t')
328
- return { kind: 'needs-consent', sha256: consent[1] as string, install: consent[2] as string, uninstall: consent[3] as string, keys, permissions: permissions.filter((item) => item !== '') }
343
+ return { type: 'needs-consent', sha256: consent[1] as string, install: consent[2] as string, uninstall: consent[3] as string, keys, permissions: permissions.filter((item) => item !== '') }
329
344
  }
330
345
  const done = /^done (\S+)(?: (\S+))?$/.exec(line)
331
- if (done !== null) return { kind: 'done', name: done[1] as string, ...(done[2] === undefined ? {} : { version: done[2] }) }
346
+ if (done !== null) return { type: 'done', name: done[1] as string, ...(done[2] === undefined ? {} : { version: done[2] }) }
332
347
  const missing = /^missing (\S+)$/.exec(line)
333
- if (missing !== null) return { kind: 'missing', name: missing[1] as string }
348
+ if (missing !== null) return { type: 'missing', name: missing[1] as string }
334
349
  const failed = /^failed (-?\d+)(?:\t(.*))?$/.exec(line)
335
- if (failed !== null) return { kind: 'failed', code: Number(failed[1]), message: failed[2] ?? '' }
336
- return { kind: 'log', text: line.replace(/^log /, '') }
350
+ if (failed !== null) return { type: 'failed', code: Number(failed[1]), message: failed[2] ?? '' }
351
+ const retries = /^retries (changed|later)$/.exec(line)
352
+ if (retries !== null) return { type: 'retries', when: retries[1] as 'changed' | 'later' }
353
+ return { type: 'log', text: line.replace(/^log /, '') }
337
354
  }
338
355
 
339
356
  export function isAtLeast(version: string, minimum: string): boolean {
package/src/register.ts CHANGED
@@ -3,6 +3,7 @@ import type { ModDefinition } from './mod.js'
3
3
  import type { Options } from './options.js'
4
4
  import type { Claude } from './runtime/claude.js'
5
5
  import type { RoutedEvent } from './runtime/hooks.js'
6
+ import { cmodPluginName } from './runtime/dependencies.js'
6
7
  import { createLifecycle, readPlugin, type Lifecycle } from './runtime/lifecycle.js'
7
8
 
8
9
  let lifecycle: Lifecycle<object>
@@ -19,7 +20,6 @@ async function startMod($: EngineInterface, eventInput: Frozen<Args<'session.sta
19
20
  const claude: Claude = {
20
21
  plugin: { name: $.plugin.name, root: $.plugin.root },
21
22
  ui: {
22
- toast: (text, options) => $.ui.toast(text, options),
23
23
  status: (text) => $.ui.status(text),
24
24
  log: (text, options) => $.ui.log(text, options),
25
25
  notice: (toolUseId, text) => $.ui.notice(toolUseId, text),
@@ -29,6 +29,8 @@ async function startMod($: EngineInterface, eventInput: Frozen<Args<'session.sta
29
29
  close: (pane) => $.ui.close(pane),
30
30
  panes: () => $.ui.panes(),
31
31
  scroll: (args) => $.ui.scroll(args),
32
+ copy: (args) => $.ui.copy(args),
33
+ focus: (args) => $.ui.focus(args),
32
34
  resolve: (render) => $.ui.resolve(render),
33
35
  },
34
36
  process: {
@@ -65,11 +67,12 @@ async function startMod($: EngineInterface, eventInput: Frozen<Args<'session.sta
65
67
  surfaces: () => $.session.surfaces(),
66
68
  messages: ((args?: Parameters<Claude['session']['messages']>[0]) => (args === undefined ? $.session.messages() : $.session.messages(args))) as Claude['session']['messages'],
67
69
  append: (args) => $.session.append(args),
70
+ send: (args) => $.session.send(args),
68
71
  },
69
72
  prompt: { submit: (args) => $.prompt.submit(args) },
70
73
  model: { complete: (request, options) => $.model.complete(request, options) },
71
74
  command: { register: (command) => $.command.register(command) },
72
- tool: { register: (tool) => $.tool.register(tool), call: ((input: ToolCallArgs) => $.tool.call(input)) as Claude['tool']['call'] },
75
+ tool: { register: (tool) => $.tool.register(tool), call: (input: ToolCallArgs) => $.tool.call(input) },
73
76
  agent: {
74
77
  list: () => $.agent.list(),
75
78
  spawn: (args) => $.agent.spawn(args),
@@ -81,34 +84,54 @@ async function startMod($: EngineInterface, eventInput: Frozen<Args<'session.sta
81
84
  },
82
85
  cmod: { call: (input) => $.cmod.call(input) },
83
86
  }
84
- await lifecycle.start(claude, readPlugin)
85
- return passOn(eventInput)
87
+ const [, answer] = await Promise.all([lifecycle.start(claude, readPlugin), passOn(eventInput)])
88
+ return answer
86
89
  }
87
90
 
88
91
  function routeToMod<N extends RoutedEvent>(_$: EngineInterface, eventInput: Frozen<Args<N>>, passOn: Next<N>): Promise<EventResult<N>> {
89
92
  return lifecycle.route(passOn.event, eventInput, (passed: unknown) => passOn(passed as Args<N>) as Promise<EventResult<N>>)
90
93
  }
91
94
 
95
+ function routeCallToMod(_$: EngineInterface, eventInput: Frozen<Args<'cmod.call'>>, passOn: Next<'cmod.call'>): Promise<EventResult<'cmod.call'>> {
96
+ const called = { ...eventInput, caller: passOn.origin.plugin } as Frozen<Args<'cmod.call'>>
97
+ return lifecycle.route('cmod.call', called, (passed) => passOn(Object.fromEntries(Object.entries(passed).filter(([key]) => key !== 'caller')) as Args<'cmod.call'>))
98
+ }
99
+
92
100
  export function registerMod<State extends object, Declared extends Options>(addHook: On, definition: ModDefinition<State, string, Declared>, options: PluginOptions): void {
93
101
  checksPermissions = false
94
102
  registered = definition as ModDefinition<object>
95
103
  lifecycle = createLifecycle(definition, () => checksPermissions, options)
96
104
  addHook('session.start', startMod)
105
+ addHook('classic.Setup', routeToMod)
97
106
  addHook('classic.SessionStart', routeToMod)
98
107
  addHook('classic.SessionEnd', routeToMod)
99
108
  addHook('classic.UserPromptSubmit', routeToMod)
109
+ addHook('classic.UserPromptExpansion', routeToMod)
100
110
  addHook('classic.InstructionsLoaded', routeToMod)
111
+ addHook('classic.ConfigChange', routeToMod)
112
+ addHook('classic.DirectoryAdded', routeToMod)
113
+ addHook('classic.PreModelSwitch', routeToMod)
114
+ addHook('classic.PostModelSwitch', routeToMod)
101
115
  addHook('classic.PermissionDenied', routeToMod)
102
116
  addHook('classic.PostToolUse', routeToMod)
103
117
  addHook('classic.PostToolUseFailure', routeToMod)
104
118
  addHook('classic.PostToolBatch', routeToMod)
119
+ addHook('classic.MessageDisplay', routeToMod)
105
120
  addHook('classic.SubagentStart', routeToMod)
106
121
  addHook('classic.SubagentStop', routeToMod)
122
+ addHook('classic.TaskCreated', routeToMod)
123
+ addHook('classic.TaskCompleted', routeToMod)
124
+ addHook('classic.TeammateIdle', routeToMod)
125
+ addHook('classic.Elicitation', routeToMod)
126
+ addHook('classic.ElicitationResult', routeToMod)
107
127
  addHook('classic.Notification', routeToMod)
108
128
  addHook('classic.PreCompact', routeToMod)
129
+ addHook('classic.PostCompact', routeToMod)
109
130
  addHook('classic.Stop', routeToMod)
110
131
  addHook('classic.StopFailure', routeToMod)
111
132
  addHook('classic.FileChanged', routeToMod)
133
+ addHook('classic.WorktreeCreate', routeToMod)
134
+ addHook('classic.WorktreeRemove', routeToMod)
112
135
  addHook('tool.call', routeToMod)
113
136
  addHook('prompt.submit', routeToMod)
114
137
  addHook('prompt.context', routeToMod)
@@ -119,7 +142,13 @@ export function registerMod<State extends object, Declared extends Options>(addH
119
142
  addHook('ui.press', routeToMod)
120
143
  addHook('ui.scroll', routeToMod)
121
144
  addHook('ui.close', routeToMod)
122
- addHook('cmod.call', routeToMod)
145
+ addHook('cmod.call', routeCallToMod)
146
+ if (definition.name !== cmodPluginName) return
147
+ addHook('session.append', routeToMod)
148
+ addHook('session.compact', routeToMod)
149
+ addHook('session.receive', routeToMod)
150
+ addHook('agent.spawn', routeToMod)
151
+ addHook('prompt.attachment', routeToMod)
123
152
  }
124
153
 
125
154
  export function registerPermissionCheck(addHook: On): void {
@@ -0,0 +1,76 @@
1
+ import { changelogPage, isObject, marketplaceOf, permissionWords } from '../records.js'
2
+ import { changeLines, changelogSections, unreleased } from '../utils/changelog.js'
3
+ import { ifMissing } from '../utils/paths.js'
4
+ import { listed } from '../utils/text.js'
5
+ import type { Claude } from './claude.js'
6
+ import { cmodPluginName } from './dependencies.js'
7
+ import type { Plugin } from './lifecycle.js'
8
+
9
+ export type Announced = { readonly version: string; readonly added: readonly string[] }
10
+
11
+ export type Announcement = { readonly message: string; readonly description?: string; readonly action?: { readonly label: string; readonly onClick: () => unknown } }
12
+
13
+ export const announcementMs = 10_000
14
+
15
+ const shownChanges = 3
16
+
17
+ export function announcedOf(saved: unknown): Announced | undefined {
18
+ if (typeof saved === 'string') return { version: saved, added: [] }
19
+ if (!isObject(saved) || typeof saved['version'] !== 'string') return undefined
20
+ return { version: saved['version'], added: Array.isArray(saved['added']) ? saved['added'].map(String) : [] }
21
+ }
22
+
23
+ export async function announcementOf(claude: Claude, plugin: Plugin, granted: ReadonlySet<string>, added: readonly string[], previous: Announced | undefined): Promise<Announcement> {
24
+ const read = (path: string) => claude.fs.read(`${plugin.root}/${path}`).catch(ifMissing(undefined))
25
+ const manifest = jsonOf(await read('.claude-plugin/plugin.json'))
26
+ const field = (key: string) => (isObject(manifest) && typeof manifest[key] === 'string' ? manifest[key] : undefined)
27
+ const open = (page?: string) => () => claude.cmod.call({ to: cmodPluginName, method: 'openSettings', input: { mod: plugin.name, ...(page === undefined ? {} : { page }) } })
28
+ if (previous === undefined) {
29
+ const { program, keys } = plugin.steps
30
+ const rules = (await claude.fs.list(`${plugin.root}/rules`).catch(ifMissing([]))).filter((entry) => entry.kind === 'file' && entry.name.endsWith('.md')).map((entry) => entry.name.slice(0, -'.md'.length)).toSorted()
31
+ const lines = [
32
+ field('description'),
33
+ added.length > 0 ? `Adds ${listed(added)}.` : undefined,
34
+ rules.length > 0 ? `Adds ${rules.length} ${rules.length === 1 ? 'rule' : 'rules'}: ${listed(rules)}.` : undefined,
35
+ program === undefined ? undefined : `Installed the ${program} program.`,
36
+ ...Object.entries(keys ?? {}).map(([key, command]) => `Binds ${key} to /${command}.`),
37
+ ...[...granted].map((item) => `${permissionWords(item)}.`),
38
+ ]
39
+ return { message: 'Installed', ...described(lines), action: { label: 'Settings', onClick: open() } }
40
+ }
41
+ const version = plugin.version ?? ''
42
+ const isCheckout = marketplaceOf(plugin.root, plugin.name) === undefined
43
+ const sections = changelogSections((await read('CHANGELOG.md')) ?? '')
44
+ const section = sections.find((each) => each.version === version) ?? (isCheckout ? sections.find((each) => each.version === unreleased) : undefined)
45
+ const changes = section === undefined ? [] : changeLines(section.text)
46
+ const nowAdded = added.filter((each) => !previous.added.includes(each))
47
+ const gone = previous.added.filter((each) => !added.includes(each))
48
+ const fallback = previous.added.length === 0 ? [] : [nowAdded.length > 0 ? `Now adds ${listed(nowAdded)}.` : undefined, gone.length > 0 ? `No longer adds ${listed(gone)}.` : undefined]
49
+ const home = await claude.env.home()
50
+ const folder = home !== undefined && plugin.root.startsWith(`${home}/`) ? `~${plugin.root.slice(home.length)}` : plugin.root
51
+ const repository = field('repository')
52
+ const lines = [
53
+ ...(changes.length === 0 ? fallback : changes.length > shownChanges ? [...changes.slice(0, shownChanges), '…'] : changes),
54
+ `From ${previous.version}${isCheckout ? `, loaded from ${folder}` : ''}.`,
55
+ repository === undefined || isCheckout ? undefined : `Release notes: ${repository.replace(/\/$/, '')}/releases/tag/v${version}`,
56
+ ]
57
+ return {
58
+ message: `Updated to ${version}`,
59
+ ...described(lines),
60
+ ...(sections.length > 0 ? { action: { label: 'Changelog', onClick: open(changelogPage) } } : {}),
61
+ }
62
+ }
63
+
64
+ function jsonOf(text: string | undefined): unknown {
65
+ try {
66
+ return text === undefined ? undefined : JSON.parse(text)
67
+ } catch (error) {
68
+ if (error instanceof SyntaxError) return undefined
69
+ throw error
70
+ }
71
+ }
72
+
73
+ function described(lines: readonly (string | undefined)[]): { readonly description?: string } {
74
+ const kept = lines.filter((line) => line !== undefined)
75
+ return kept.length === 0 ? {} : { description: kept.join('\n') }
76
+ }
@@ -0,0 +1,200 @@
1
+ import type { Args, Frozen } from 'claude-code'
2
+ import type { Disposable } from '../mod.js'
3
+ import { ContextEnd } from '../session/context/context.js'
4
+ import { Document } from '../session/context/turn/message/document.js'
5
+ import { Message, messagesOf, rowOfAppend, withTexts, type Unplaced } from '../session/context/turn/message/message.js'
6
+ import { Note } from '../session/context/turn/message/note.js'
7
+ import { Notice } from '../session/context/turn/message/notice.js'
8
+ import { Prompt } from '../session/context/turn/message/prompt.js'
9
+ import { Reply } from '../session/context/turn/message/reply.js'
10
+ import { senderOf } from '../session/context/turn/message/sender.js'
11
+ import { Skill } from '../session/context/turn/message/skill.js'
12
+ import { ToolCall } from '../session/context/turn/message/tool-call.js'
13
+ import { ToolResult } from '../session/context/turn/message/tool-result.js'
14
+ import { TurnEnd } from '../session/context/turn/turn.js'
15
+ import type { ConversationState, SessionChange, TaskData } from '../session/conversation.js'
16
+ import { TaskEnd, TaskStart } from '../session/goal/task.js'
17
+ import type { BeforeEvent } from '../session/session.js'
18
+ import type { Claude } from './claude.js'
19
+ import { cmodPluginName } from './dependencies.js'
20
+ import type { Router } from './router.js'
21
+ import { toolInputOf } from './tool-calls.js'
22
+
23
+ export type Place = Pick<ConversationState, 'session' | 'context' | 'contextOpenedAt' | 'turn' | 'tasks' | 'taskTurns'>
24
+
25
+ export type Decide = (event: BeforeEvent, change: SessionChange) => Promise<Partial<Answers> | undefined>
26
+
27
+ export type Answers = { readonly text: string; readonly refuse: string; readonly input: Readonly<Record<string, unknown>>; readonly result: string; readonly owner: { readonly model: string } }
28
+
29
+ export type BeforeHooks = { readonly place: () => Promise<Place>; readonly wants: (event: BeforeEvent) => Promise<boolean>; readonly decide: Decide }
30
+
31
+ const appended: readonly BeforeEvent[] = [Note, Reply, ToolResult, Notice]
32
+
33
+ const relayTag = /^(?:<cross-session-message[^>]*>\s*)?<cmod-relay>([\s\S]*)<\/cmod-relay>(?:\s*<\/cross-session-message>[\s\S]*)?$/
34
+
35
+ export function relayOf(e: Frozen<Args<'session.receive'>>): string | undefined {
36
+ const isFromCmod = e.origin.kind === 'peer' && !('teammate' in e.origin) && e.origin.plugin === cmodPluginName
37
+ return isFromCmod ? relayTag.exec(e.text)?.[1] : undefined
38
+ }
39
+
40
+ export function installBefore({ claude, router, hooks }: { readonly claude: Claude; readonly router: Router; readonly hooks: BeforeHooks }): Disposable {
41
+ const { place, wants, decide } = hooks
42
+ const now = () => new Date().toISOString()
43
+ const asked = async (event: BeforeEvent, change: SessionChange) => (await decide(event, change)) ?? {}
44
+ const message = (where: Place, unplaced: Unplaced, task: string | undefined, isInTurn = true): SessionChange => {
45
+ const turn = isInTurn ? (task === undefined ? where.turn : where.taskTurns[task]) : undefined
46
+ const context = task === undefined ? where.context : (where.tasks[task]?.context ?? where.context)
47
+ return { type: Message, message: { ...unplaced, session: where.session, context, ...(turn === undefined ? {} : { turn: turn.prompt.id }), ...(task === undefined ? {} : { task }) } as Message }
48
+ }
49
+ const added: Disposable[] = [
50
+ router.add('prompt.submit', async (e, next) => {
51
+ if (!(await wants(Prompt)) || e.text.startsWith('/')) return next(e)
52
+ const where = await place()
53
+ const answer = await asked(Prompt, message(where, { id: '', at: now(), type: Prompt, text: e.text, from: senderOf(e.origin, false) }, undefined, false))
54
+ if (answer.refuse !== undefined) return { drop: answer.refuse }
55
+ return next(answer.text === undefined ? e : { ...e, text: answer.text })
56
+ }),
57
+ router.add('command.run', async (e, next) => {
58
+ if (!(await wants(Prompt))) return next(e)
59
+ const typed = e.args === '' ? `/${e.command}` : `/${e.command} ${e.args}`
60
+ const where = await place()
61
+ const answer = await asked(Prompt, message(where, { id: '', at: now(), type: Prompt, text: typed, from: senderOf(e.origin, false) }, undefined, false))
62
+ if (answer.refuse !== undefined) return { text: answer.refuse }
63
+ if (answer.text === undefined || answer.text === typed) return next(e)
64
+ const same = new RegExp(`^/${e.command}(?:\\s+|$)`).exec(answer.text)
65
+ if (same !== null) return next({ ...e, args: answer.text.slice(same[0].length) })
66
+ void claude.prompt.submit({ text: answer.text })
67
+ return {}
68
+ }),
69
+ router.add('session.receive', async (e, next) => {
70
+ if (!(await wants(Prompt)) || relayOf(e) !== undefined) return next(e)
71
+ const where = await place()
72
+ const answer = await asked(Prompt, message(where, { id: '', at: now(), type: Prompt, text: e.text, from: { type: 'session' } }, e.agentId, false))
73
+ if (answer.refuse !== undefined) return { consumed: answer.refuse }
74
+ return next(answer.text === undefined ? e : { ...e, text: answer.text })
75
+ }),
76
+ router.add('skill.prompt', async (e, next) => {
77
+ if (!(await wants(Skill))) return next(e)
78
+ const where = await place()
79
+ const answer = await asked(Skill, message(where, { id: '', at: now(), type: Skill, name: e.skill, text: e.text }, undefined))
80
+ return answer.text === undefined ? next(e) : { text: answer.text }
81
+ }),
82
+ router.add('prompt.attachment', async (e, next) => {
83
+ if (!(await wants(Document)) || e.type !== 'nested_memory') return next(e)
84
+ const where = await place()
85
+ const path = /^Contents of (.+?) \(/.exec(e.text)?.[1] ?? ''
86
+ const answer = await asked(Document, message(where, { id: '', at: now(), type: Document, path, text: e.text }, e.agentId))
87
+ if (answer.refuse !== undefined) return { text: null }
88
+ return answer.text === undefined ? next(e) : { text: answer.text }
89
+ }),
90
+ router.add('session.append', async (e, next) => {
91
+ const wanted = new Set((await Promise.all(appended.map(async (event) => ((await wants(event)) ? [event] : [])))).flat())
92
+ if (wanted.size === 0) return next(e)
93
+ const row = rowOfAppend({ uuid: e.uuid, door: e.door, origin: e.origin, message: e.message }, now(), '')
94
+ const found = messagesOf(row, e.agentId !== undefined)
95
+ if (!found.some((each) => wanted.has(each.type))) return next(e)
96
+ const where = await place()
97
+ const texts = new Map<number, string>()
98
+ for (const [part, unplaced] of found.entries()) {
99
+ if (!wanted.has(unplaced.type)) continue
100
+ const answer = await asked(unplaced.type, message(where, unplaced, e.agentId))
101
+ if (answer.text !== undefined) texts.set(part, answer.text)
102
+ }
103
+ return next(texts.size === 0 ? e : { ...e, message: { ...e.message, content: withTexts(row, texts) as typeof e.message.content } })
104
+ }),
105
+ router.add('tool.call', async (e, next) => {
106
+ if (!(await wants(ToolCall))) return next(e)
107
+ const where = await place()
108
+ const input = toolInputOf(e)
109
+ const answer = await asked(ToolCall, message(where, { id: e.tool_use_id, at: now(), type: ToolCall, callId: e.tool_use_id, name: e.tool, input, cwd: await claude.session.cwd() }, e.agentId))
110
+ if (answer.refuse !== undefined) return { deny: answer.refuse }
111
+ if (answer.result !== undefined) return { result: answer.result }
112
+ if (answer.input === undefined) return next(e)
113
+ const reserved = Object.fromEntries(Object.entries(e).filter(([key]) => !(key in input)))
114
+ return next({ ...reserved, ...answer.input } as Frozen<Args<'tool.call'>>)
115
+ }),
116
+ router.add('agent.spawn', async (e, next) => {
117
+ if (!(await wants(TaskStart))) return next(e)
118
+ const where = await place()
119
+ const task: TaskData = { id: '', title: e.description, agentType: e.subagentType, model: e.model ?? e.parentModel, openedBy: e.tool_use_id, openedAt: now(), ...(e.parentAgentId === undefined ? {} : { parent: e.parentAgentId }) }
120
+ const answer = await asked(TaskStart, { type: TaskStart, task: { ...task, context: where.context } })
121
+ if (answer.refuse !== undefined) return { deny: answer.refuse }
122
+ return next(answer.owner === undefined ? e : { ...e, model: answer.owner.model })
123
+ }),
124
+ router.add('classic.SubagentStop', async (e, next) => {
125
+ if (!(await wants(TaskEnd)) && !(await wants(TurnEnd))) return next(e)
126
+ const where = await place()
127
+ const task = where.tasks[e.agent_id]
128
+ if (task === undefined) return next(e)
129
+ const turn = where.taskTurns[e.agent_id]
130
+ const ending = (await wants(TaskEnd)) ? await asked(TaskEnd, { type: TaskEnd, task: { ...task, closedAt: now() } }) : {}
131
+ const refuse = ending.refuse ?? ((await wants(TurnEnd)) && turn !== undefined ? (await asked(TurnEnd, { type: TurnEnd, turn: { ...turn, closedAt: now() } })).refuse : undefined)
132
+ const below = await next(e)
133
+ return refuse === undefined ? below : { ...below, block: refuse }
134
+ }),
135
+ router.add('classic.Stop', async (e, next) => {
136
+ if (!(await wants(TurnEnd))) return next(e)
137
+ const where = await place()
138
+ if (where.turn === undefined) return next(e)
139
+ const answer = await asked(TurnEnd, { type: TurnEnd, turn: { ...where.turn, closedAt: now() } })
140
+ const below = await next(e)
141
+ return answer.refuse === undefined ? below : { ...below, block: answer.refuse }
142
+ }),
143
+ router.add('session.compact', async (e, next) => {
144
+ if (!(await wants(ContextEnd)) || e.trigger === 'precompute') return next(e)
145
+ const where = await place()
146
+ const task = e.agentId === undefined ? undefined : where.tasks[e.agentId]
147
+ if (e.agentId !== undefined && task === undefined) return next(e)
148
+ const context = task === undefined ? { number: where.context, openedAt: where.contextOpenedAt } : { number: task.context, openedAt: task.contextOpenedAt ?? task.openedAt, task: task.id }
149
+ const answer = await asked(ContextEnd, { type: ContextEnd, context: { ...context, closedAt: now() } })
150
+ return answer.refuse === undefined ? next(e) : { skip: answer.refuse }
151
+ }),
152
+ ]
153
+ return { dispose: () => added.forEach((each) => each.dispose()) }
154
+ }
155
+
156
+ export function withAnswer(change: SessionChange, answer: Partial<Answers>): SessionChange {
157
+ if (change.type === Message && answer.text !== undefined) return { ...change, message: { ...change.message, text: answer.text } as Message }
158
+ if (change.type === Message && answer.input !== undefined && change.message.type === ToolCall) return { ...change, message: { ...change.message, input: answer.input } }
159
+ if (change.type === TaskStart && answer.owner !== undefined) return { ...change, task: { ...change.task, model: answer.owner.model } }
160
+ return change
161
+ }
162
+
163
+ export async function foldAnswers(event: BeforeEvent, change: SessionChange, askers: readonly ((change: SessionChange) => Promise<unknown>)[], report: (error: unknown) => void): Promise<Partial<Answers> | undefined> {
164
+ let latest = change
165
+ let result: Partial<Answers> | undefined
166
+ for (const ask of askers) {
167
+ try {
168
+ const answer = answerOf(event, await ask(latest))
169
+ if (answer === undefined) continue
170
+ result = { ...result, ...answer }
171
+ if (answer.refuse !== undefined || answer.result !== undefined) return result
172
+ latest = withAnswer(latest, answer)
173
+ } catch (error) {
174
+ report(error)
175
+ }
176
+ }
177
+ return result
178
+ }
179
+
180
+ export function answerOf(event: BeforeEvent, answer: unknown): Partial<Answers> | undefined {
181
+ if (typeof answer !== 'object' || answer === null) return undefined
182
+ const given = answer as Partial<Answers>
183
+ const allowed: Readonly<Record<BeforeEvent, readonly (keyof Answers)[]>> = {
184
+ prompt: ['text', 'refuse'],
185
+ note: ['text'],
186
+ skill: ['text'],
187
+ document: ['text', 'refuse'],
188
+ reply: ['text'],
189
+ toolCall: ['input', 'result', 'refuse'],
190
+ toolResult: ['text'],
191
+ notice: ['text'],
192
+ taskStart: ['owner', 'refuse'],
193
+ taskEnd: ['refuse'],
194
+ turnEnd: ['refuse'],
195
+ contextEnd: ['refuse'],
196
+ }
197
+ const unread = Object.keys(given).filter((key) => !allowed[event].includes(key as keyof Answers))
198
+ if (unread.length > 0) throw new Error(`a before(${event}) listener answered ${unread.join(' and ')}, which ${event} does not take. It takes ${allowed[event].map((key) => `{ ${key} }`).join(', ')}.`)
199
+ return given
200
+ }
@@ -0,0 +1,12 @@
1
+ import type { Imports } from '../mod.js'
2
+
3
+ let current: Imports | undefined
4
+
5
+ export function bind(imports: Imports): void {
6
+ current = imports
7
+ }
8
+
9
+ export function bound(): Imports {
10
+ if (current === undefined) throw new Error("A mod's imports work once its setup runs. Call them from setup or from a hook, never at the top of a module.")
11
+ return current
12
+ }
@@ -28,8 +28,11 @@ import type {
28
28
  Settings,
29
29
  SettingsReadArgs,
30
30
  Timer,
31
- ToastOptions,
32
31
  ToolSpec,
32
+ UiCopyArgs,
33
+ UiFocusArgs,
34
+ UiFocusResult,
35
+ UiCopyResult,
33
36
  UiLogOptions,
34
37
  UiOpenResult,
35
38
  UiPane,
@@ -40,7 +43,6 @@ import type {
40
43
  export type Claude = {
41
44
  readonly plugin: { readonly name: string; readonly root: string }
42
45
  readonly ui: {
43
- toast(text: string, options?: ToastOptions): void
44
46
  status(text: string | undefined): void
45
47
  log(text: string, options?: UiLogOptions): void
46
48
  notice(toolUseId: string, text: string | undefined): void
@@ -50,6 +52,8 @@ export type Claude = {
50
52
  close(pane: PaneCloseArgs): Promise<void>
51
53
  panes(): Promise<readonly UiPane[]>
52
54
  scroll(args: UiScrollArgs): Promise<UiScrollResult>
55
+ copy(args: UiCopyArgs): Promise<UiCopyResult>
56
+ focus(args: UiFocusArgs): Promise<UiFocusResult>
53
57
  resolve(e: ResolveInput): ElementTable<RenderSurface>
54
58
  }
55
59
  readonly process: {
@@ -86,6 +90,7 @@ export type Claude = {
86
90
  surfaces(): Promise<readonly RenderSurface[]>
87
91
  messages: EngineInterface['session']['messages']
88
92
  append: EngineInterface['session']['append']
93
+ send: EngineInterface['session']['send']
89
94
  }
90
95
  readonly prompt: { submit: EngineInterface['prompt']['submit'] }
91
96
  readonly model: { complete: EngineInterface['model']['complete'] }