@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
@@ -1,6 +1,6 @@
1
1
  import { matchesGlob } from '../../utils/matches-glob.js'
2
2
  import { parseShell } from '../../utils/parse-shell.js'
3
- import { callBaseOf, type CallBase, type CallEffects, type CommandCall, type FetchCall, type FileAccess, type FileCall, type ToolCall } from '../../utils/call-effects.js'
3
+ import { callBaseOf, type CallBase, type CallEffects, type CommandCall, type FetchCall, type FileAccess, type FileCall, type Call } from '../../utils/call-effects.js'
4
4
  import type { Workspace } from './find-project-scope.js'
5
5
  import { realPathOf, relativePath } from '../../utils/paths.js'
6
6
  import { basename } from '../../vendor.js'
@@ -9,7 +9,7 @@ export type TargetCalls = { command: CommandCall; read: FileCall; write: FileCal
9
9
  export type TargetKey = keyof TargetCalls
10
10
  export type TargetOf<Key extends TargetKey> = Record<Key, string | string[]> & Partial<Record<Exclude<TargetKey, Key>, never>>
11
11
  export type Target = { [Key in TargetKey]: TargetOf<Key> }[TargetKey]
12
- export type MatchedCall = { call: ToolCall; access: FileAccess | undefined; folder: string }
12
+ export type MatchedCall = { call: Call; access: FileAccess | undefined; folder: string }
13
13
 
14
14
  const targetKeys: readonly TargetKey[] = ['command', 'read', 'write', 'fetch', 'subagent', 'tool']
15
15
 
@@ -29,7 +29,7 @@ export function targetOf(target: Target): { key: TargetKey; patterns: string[];
29
29
  export async function matchTarget(target: Target, call: CallBase, effects: CallEffects, workspace: Workspace): Promise<MatchedCall[]> {
30
30
  const { key, patterns, exclusions } = targetOf(target)
31
31
  const base = callBaseOf(call)
32
- const only = (matched: ToolCall, folder = workspace.cwd): MatchedCall[] => [{ call: matched, access: undefined, folder }]
32
+ const only = (matched: Call, folder = workspace.cwd): MatchedCall[] => [{ call: matched, access: undefined, folder }]
33
33
  const isNamed = (value: string) => matchesAny(value, patterns) && !matchesAny(value, exclusions)
34
34
  if (key === 'read' || key === 'write') {
35
35
  const matched: MatchedCall[] = []
@@ -1,13 +1,14 @@
1
1
  import type { Args, Frozen } from 'claude-code'
2
2
  import type { Job, Mod } from '../mod.js'
3
3
  import type { Deadline } from '../runtime/deadline.js'
4
+ import { engineOf } from '../runtime/engine.js'
4
5
  import { inputOf } from '../utils/call-effects.js'
5
6
  import { decidePermission, stricterVerdict, type PermissionRules as RulesFor, type Rule as RuleFor, type Verdict } from './permissions/decide-permission.js'
6
7
  import { targetOf } from './permissions/match-target.js'
7
8
  import { messageOf } from '../utils/text.js'
8
- import { modOf, workspaceReader } from './context.js'
9
+ import { withinDeadline, workspaceReader } from './context.js'
9
10
 
10
- export type { CommandCall, FetchCall, FileCall, ToolCall } from '../utils/call-effects.js'
11
+ export type { CommandCall, FetchCall, FileCall, Call } from '../utils/call-effects.js'
11
12
  export type { Target } from './permissions/match-target.js'
12
13
  export type Rule<State extends object = Record<never, never>> = RuleFor<Mod<State>>
13
14
  export type PermissionRules<State extends object = Record<never, never>> = RulesFor<Mod<State>>
@@ -19,25 +20,26 @@ export function permissions<State extends object = Record<never, never>>(rules:
19
20
  if (count === 0) throw new Error('permissions: give it a deny or an ask rule, or remove it from setup.')
20
21
 
21
22
  return (job) => {
23
+ const { router, toolCalls } = engineOf(job.mod)
22
24
  const readWorkspace = workspaceReader(job)
23
25
 
24
26
  const verdictOf = async (e: Frozen<Args<'tool.check'>>): Promise<Verdict | undefined> => {
25
27
  let current: PermissionRules<State>
26
28
  try {
27
- current = typeof rules === 'function' ? rules(job.mod.state as State) : rules
29
+ current = typeof rules === 'function' ? rules(job.mod.state) : rules
28
30
  checkedCount(current)
29
31
  } catch (error) {
30
32
  return { decision: 'deny', reason: `The permission rules of ${job.mod.name} failed, so it denies the call: ${messageOf(error)}` }
31
33
  }
32
34
  try {
33
- const [agent, workspace] = await Promise.all([job.toolCalls.agentOf(e.tool_use_id), readWorkspace()])
34
- return await decidePermission(current, { tool: e.tool, input: inputOf(e.input), ...agent }, workspace, (call, folder) => modOf(job, { call, folder }, workspace, deadline))
35
+ const [agent, workspace] = await Promise.all([toolCalls.agentOf(e.tool_use_id), readWorkspace()])
36
+ return await decidePermission(current, { tool: e.tool, input: inputOf(e.input), ...agent }, workspace, job.mod, (run) => withinDeadline(job.mod, deadline, 'it', run))
35
37
  } catch (error) {
36
38
  return { decision: 'deny', reason: `The permissions job could not read the session, so it denies the call: ${messageOf(error)}` }
37
39
  }
38
40
  }
39
41
 
40
- job.mod.claude.on('tool.check', async (e, next) => {
42
+ router.add('tool.check', async (e, next) => {
41
43
  const [below, ours] = await Promise.all([next(e), verdictOf(e)])
42
44
  return ours === undefined ? below : stricterVerdict<Verdict>(below, ours)
43
45
  })
@@ -1,5 +1,6 @@
1
1
  import type { Job } from '../mod.js'
2
- import { PermissionRefused } from '../runtime/grants.js'
2
+ import { PermissionRefused, programAddresses } from '../runtime/grants.js'
3
+ import { engineOf } from '../runtime/engine.js'
3
4
  import { messageOf } from '../utils/text.js'
4
5
 
5
6
  export type Program = {
@@ -14,7 +15,7 @@ const backoffSeconds = [1, 2, 4, 8, 16] as const
14
15
  export function program(options: { readonly command: readonly string[]; readonly environment?: Record<string, string> }): Job<Program> {
15
16
  const { command, environment } = options
16
17
  return ({ mod, announce }) => {
17
- const { claude } = mod
18
+ const { claude, report } = engineOf(mod)
18
19
  const [executable] = command
19
20
  if (executable === undefined) throw new Error(`${mod.name}: the program has no command. Name the program on PATH and its arguments, such as ['preview-server', '--port', '0'].`)
20
21
  let state: Program['state'] = 'stopped'
@@ -22,11 +23,17 @@ export function program(options: { readonly command: readonly string[]; readonly
22
23
  let failure: Error | undefined
23
24
  let exits = 0
24
25
  const waiting: { resolve: (address: Address) => void; reject: (error: Error) => void }[] = []
26
+ const ownAddresses = programAddresses(mod)
25
27
 
26
28
  const stop = (reason: string) => {
27
29
  state = 'fatal'
28
30
  failure = new Error(`${mod.name}: the ${executable} program stopped: ${reason}`)
29
- claude.ui.log(failure.message)
31
+ const retry = () => {
32
+ exits = 0
33
+ failure = undefined
34
+ void start()
35
+ }
36
+ report({ id: `program:${executable}`, message: `The ${executable} program stopped`, description: reason, retry })
30
37
  for (const waiter of waiting.splice(0)) waiter.reject(failure)
31
38
  }
32
39
 
@@ -52,17 +59,19 @@ export function program(options: { readonly command: readonly string[]; readonly
52
59
  }
53
60
  state = 'running'
54
61
  exits = 0
62
+ ownAddresses.add(address.socketPath ?? new URL(address.url).host)
55
63
  for (const waiter of waiting.splice(0)) waiter.resolve(address)
56
64
  }
57
65
  } catch (error) {
58
66
  if (error instanceof PermissionRefused) return stop(`${messageOf(error)} Add it, then run /reload-plugins.`)
59
67
  lastError = messageOf(error)
60
68
  }
69
+ if (address !== undefined) ownAddresses.delete(address.socketPath ?? new URL(address.url).host)
61
70
  address = undefined
62
71
  exits += 1
63
72
  const wait = backoffSeconds[exits - 1]
64
73
  if (wait === undefined) {
65
- stop(`it did not start again after ${backoffSeconds.length} tries: ${lastError === '' ? 'no standard error' : lastError}. Fix it, then run /reload-plugins.`)
74
+ stop(`it did not start again after ${backoffSeconds.length} tries: ${lastError === '' ? 'no standard error' : lastError}.`)
66
75
  return
67
76
  }
68
77
  state = 'backoff'
@@ -1,12 +1,13 @@
1
1
  import type { Job, JobContext, Mod } from '../mod.js'
2
- import { longestMs, type Deadline } from '../runtime/deadline.js'
2
+ import type { Deadline } from '../runtime/deadline.js'
3
+ import { engineOf } from '../runtime/engine.js'
3
4
  import { onStateChange } from '../runtime/state.js'
4
5
  import { listed, messageOf } from '../utils/text.js'
5
- import { callEffects, type ToolCall } from '../utils/call-effects.js'
6
+ import { callEffects, type Call } from '../utils/call-effects.js'
6
7
  import { matchTarget, targetOf, type Target } from './permissions/match-target.js'
7
- import { afterCall, modOf, modWithin, targetWords } from './context.js'
8
+ import { afterCall, inFolder, targetWords, withinDeadline } from './context.js'
8
9
 
9
- type PromptInput = { userPrompt?: string; call?: ToolCall }
10
+ type PromptInput = { userPrompt?: string; call?: Call & { readonly folder: string } }
10
11
 
11
12
  type PromptOptions<State extends object> = {
12
13
  name: string
@@ -15,7 +16,7 @@ type PromptOptions<State extends object> = {
15
16
  after?: Target | Target[]
16
17
  }
17
18
 
18
- const deadline: Deadline = { ms: 5000, longestMs, job: 'prompt' }
19
+ const deadline: Deadline = { ms: 5000, job: 'prompt' }
19
20
 
20
21
  export function prompt<State extends object = Record<never, never>>({ name, prompt: text, when, after }: PromptOptions<State>): Job<void, State> {
21
22
  if (name.trim() === '') throw new Error('prompt: give the block a name. Claude reads the text under it.')
@@ -26,10 +27,10 @@ export function prompt<State extends object = Record<never, never>>({ name, prom
26
27
  return (job) => {
27
28
  job.reserveName('prompt', name, `prompt: ${job.mod.name} adds the name "${name}" two times. Give each prompt its own name.`)
28
29
 
29
- const log = (reason: string) => job.mod.claude.ui.log(`prompt "${name}" added nothing: ${reason}`, { to: 'debug' })
30
- const textFor = async (input: PromptInput, mod: Mod<State>) => {
30
+ const log = (reason: string) => job.mod.log(`prompt "${name}" added nothing: ${reason}`)
31
+ const textFor = async (input: PromptInput) => {
31
32
  try {
32
- const value = typeof text === 'string' ? text : await text(input, mod)
33
+ const value = typeof text === 'string' ? text : await withinDeadline(job.mod, deadline, `the ${name} prompt`, () => text(input, job.mod))
33
34
  return value === '' ? undefined : value
34
35
  } catch (error) {
35
36
  log(messageOf(error))
@@ -37,36 +38,37 @@ export function prompt<State extends object = Record<never, never>>({ name, prom
37
38
  }
38
39
  }
39
40
 
40
- const headedText = async (input: PromptInput, mod: Mod<State>) => {
41
- const value = await textFor(input, mod)
41
+ const headedText = async (input: PromptInput) => {
42
+ const value = await textFor(input)
42
43
  return value === undefined ? undefined : `# ${name}\n${value}`
43
44
  }
44
45
 
45
46
  if (when !== undefined) {
46
- addAfterUserPrompts(job, when, (userPrompt) => headedText({ userPrompt }, modWithin(job, deadline)), log)
47
+ addAfterUserPrompts(job, when, (userPrompt) => headedText({ userPrompt }), log)
47
48
  job.announce(`the ${name} prompt after matching user prompts`)
48
49
  } else if (targets.length > 0) {
49
50
  addAfterCalls(job, targets, headedText, log)
50
51
  job.announce(`the ${name} prompt after ${listed(targets.map(targetWords))}`)
51
52
  } else {
52
- addKeptCurrent(job, name, () => textFor({}, modWithin(job, deadline)), log)
53
+ addKeptCurrent(job, name, () => textFor({}), log)
53
54
  job.announce(`the ${name} prompt`)
54
55
  }
55
56
  }
56
57
  }
57
58
 
58
59
  function addKeptCurrent<State extends object>(job: JobContext<State>, name: string, textFor: () => Promise<string | undefined>, log: (reason: string) => void): void {
60
+ const { claude, router } = engineOf(job.mod)
59
61
  let given: string | undefined
60
62
  let hasStarted = false
61
63
  let isQueued = false
62
64
  let updates = Promise.resolve()
63
65
 
64
- job.mod.claude.on('classic.SessionStart', (e, next) => {
66
+ router.add('classic.SessionStart', (e, next) => {
65
67
  hasStarted = e.source === 'resume' || e.source === 'compact'
66
68
  if (!hasStarted) given = undefined
67
69
  return next(e)
68
70
  })
69
- job.mod.claude.on('prompt.context', async (e, next) => {
71
+ router.add('prompt.context', async (e, next) => {
70
72
  const below = await next(e)
71
73
  hasStarted = true
72
74
  if (below.blocks.some((block) => block.name === name)) {
@@ -83,7 +85,7 @@ function addKeptCurrent<State extends object>(job: JobContext<State>, name: stri
83
85
  const value = await textFor()
84
86
  if (value === undefined || value === given) return
85
87
  given = value
86
- await job.mod.claude.session.append({ message: { type: 'user', content: [{ type: 'text', text: `# ${name}\n${value}` }] } }).then((added) => {
88
+ await claude.session.append({ message: { type: 'user', content: [{ type: 'text', text: `# ${name}\n${value}` }] } }).then((added) => {
87
89
  if (added.deny !== undefined) log(`Claude Code refused the new text: ${added.deny}`)
88
90
  })
89
91
  }
@@ -94,8 +96,8 @@ function addKeptCurrent<State extends object>(job: JobContext<State>, name: stri
94
96
  })
95
97
  }
96
98
 
97
- function addAfterUserPrompts(job: JobContext, when: RegExp | ((userPrompt: string) => boolean), textFor: (userPrompt: string) => Promise<string | undefined>, log: (reason: string) => void): void {
98
- job.mod.claude.on('prompt.submit', async (e, next) => {
99
+ function addAfterUserPrompts<State extends object>(job: JobContext<State>, when: RegExp | ((userPrompt: string) => boolean), textFor: (userPrompt: string) => Promise<string | undefined>, log: (reason: string) => void): void {
100
+ engineOf(job.mod).router.add('prompt.submit', async (e, next) => {
99
101
  let value: string | undefined
100
102
  try {
101
103
  if (typeof when === 'function' ? when(e.text) : e.text.search(when) >= 0) value = await textFor(e.text)
@@ -106,12 +108,7 @@ function addAfterUserPrompts(job: JobContext, when: RegExp | ((userPrompt: strin
106
108
  })
107
109
  }
108
110
 
109
- function addAfterCalls<State extends object>(
110
- job: JobContext<State>,
111
- targets: Target[],
112
- textFor: (input: PromptInput, mod: Mod<State>) => Promise<string | undefined>,
113
- log: (reason: string) => void,
114
- ): void {
111
+ function addAfterCalls<State extends object>(job: JobContext<State>, targets: Target[], textFor: (input: PromptInput) => Promise<string | undefined>, log: (reason: string) => void): void {
115
112
  afterCall(
116
113
  job,
117
114
  async (use, workspace) => {
@@ -119,7 +116,7 @@ function addAfterCalls<State extends object>(
119
116
  for (const target of targets) {
120
117
  const [matched] = await matchTarget(target, use, effects, workspace)
121
118
  if (matched === undefined) continue
122
- const value = await textFor({ call: matched.call }, modOf(job, matched, workspace, deadline))
119
+ const value = await textFor({ call: await inFolder(matched.call, matched.folder, workspace) })
123
120
  return value === undefined ? [] : [value]
124
121
  }
125
122
  return []
@@ -1,37 +1,37 @@
1
1
  import type { Job } from '../mod.js'
2
2
  import { onStateChange } from '../runtime/state.js'
3
- import { messageOf } from '../utils/text.js'
3
+ import { engineOf } from '../runtime/engine.js'
4
4
 
5
- export type Schedule = { readonly cron: string; readonly prompt: string }
5
+ export type ScheduledPrompt = { readonly cron: string; readonly prompt: string }
6
6
 
7
7
  const scheduleCounts = new WeakMap<object, number>()
8
8
 
9
- export function schedule<State extends object = Record<never, never>>(entry: Schedule | ((state: State) => Schedule | undefined)): Job<void, State> {
9
+ export function schedule<State extends object = Record<never, never>>(entry: ScheduledPrompt | ((state: State) => ScheduledPrompt | undefined)): Job<void, State> {
10
10
  if (typeof entry !== 'function') checked(entry)
11
11
 
12
12
  return (job) => {
13
13
  const { mod } = job
14
- const { claude } = mod
14
+ const { claude, report: reportFailure } = engineOf(mod)
15
15
  const order = scheduleCounts.get(mod) ?? 0
16
16
  scheduleCounts.set(mod, order + 1)
17
17
  const storeKey = `cmod:schedule:${order}`
18
- const log = (reason: string) => claude.ui.log(`${mod.name}: the schedule ${reason}`)
18
+ const report = (message: string, error: unknown) => reportFailure({ id: storeKey, message: `The schedule ${message}`, error })
19
19
  let current: { readonly id: string; readonly wanted: string } | undefined
20
20
  let isQueued = false
21
21
 
22
22
  const remove = async (id: string) => {
23
- await claude.tool.call({ tool: 'CronDelete', id }).catch(() => undefined)
23
+ await claude.tool.call({ tool: 'CronDelete', id }).catch((error: unknown) => report(`could not delete its last cron ${id}`, error))
24
24
  await claude.store.delete(storeKey)
25
25
  }
26
26
 
27
27
  const update = async () => {
28
28
  isQueued = false
29
- let next: Schedule | undefined
29
+ let next: ScheduledPrompt | undefined
30
30
  try {
31
- next = typeof entry === 'function' ? entry(mod.state as State) : entry
31
+ next = typeof entry === 'function' ? entry(mod.state) : entry
32
32
  if (next !== undefined) checked(next)
33
33
  } catch (error) {
34
- log(`kept its last cron: ${messageOf(error)}`)
34
+ report('kept its last cron', error)
35
35
  return
36
36
  }
37
37
  const wanted = next === undefined ? undefined : JSON.stringify([next.cron, next.prompt])
@@ -40,8 +40,8 @@ export function schedule<State extends object = Record<never, never>>(entry: Sch
40
40
  current = undefined
41
41
  if (next === undefined) return
42
42
  const answer = await claude.tool.call({ tool: 'CronCreate', cron: next.cron, prompt: next.prompt, recurring: true })
43
- if (answer.deny !== undefined) return log(`was refused: ${answer.deny}`)
44
- if (answer.isError === true) return log(`failed: ${answer.text ?? 'CronCreate answered an error'}`)
43
+ if (answer.deny !== undefined) return report('was refused', answer.deny)
44
+ if (answer.isError === true) return report('failed', answer.text ?? 'CronCreate answered an error')
45
45
  current = { id: answer.result.id, wanted: wanted as string }
46
46
  await claude.store.set(storeKey, current.id)
47
47
  }
@@ -50,17 +50,17 @@ export function schedule<State extends object = Record<never, never>>(entry: Sch
50
50
  .get(storeKey)
51
51
  .then((left) => (typeof left === 'string' ? remove(left) : undefined))
52
52
  .then(update)
53
- .catch((error: unknown) => log(`failed: ${messageOf(error)}`))
53
+ .catch((error: unknown) => report('failed', error))
54
54
  onStateChange(mod.state, () => {
55
55
  if (isQueued) return
56
56
  isQueued = true
57
- updates = updates.then(update).catch((error: unknown) => log(`failed: ${messageOf(error)}`))
57
+ updates = updates.then(update).catch((error: unknown) => report('failed', error))
58
58
  })
59
59
  job.announce(typeof entry === 'function' ? 'a schedule from its state' : `a schedule at ${entry.cron}`)
60
60
  }
61
61
  }
62
62
 
63
- function checked(entry: Schedule): void {
63
+ function checked(entry: ScheduledPrompt): void {
64
64
  if (entry.cron.trim() === '') throw new Error('schedule: give it a cron, such as "*/10 * * * *" for every 10 minutes.')
65
65
  if (entry.prompt.trim() === '') throw new Error('schedule: give it the prompt Claude gets at each fire.')
66
66
  }
@@ -1,13 +1,14 @@
1
1
  import type { CommandRunResult } from 'claude-code'
2
- import type { Job, Mod } from '../mod.js'
2
+ import type { Disposable, Job, Mod } from '../mod.js'
3
3
  import { parse } from '../vendor.js'
4
- import { longestMs, type Deadline } from '../runtime/deadline.js'
4
+ import type { Deadline } from '../runtime/deadline.js'
5
+ import { engineOf } from '../runtime/engine.js'
5
6
  import { messageOf } from '../utils/text.js'
6
- import { modWithin } from './context.js'
7
+ import { withinDeadline } from './context.js'
7
8
 
8
- export type Reply = string | { text?: string; context?: string } | undefined
9
+ export type CommandReply = string | { text?: string; context?: string } | undefined
9
10
 
10
- const deadline: Deadline = { ms: 30000, longestMs, job: 'slashCommand' }
11
+ const deadline: Deadline = { ms: 30000, job: 'slashCommand' }
11
12
 
12
13
  const commandName = /^[A-Za-z0-9_-]{1,64}$/
13
14
 
@@ -16,14 +17,14 @@ export function slashCommand<State extends object = Record<never, never>>(option
16
17
  readonly description: string
17
18
  readonly argumentHint?: string
18
19
  readonly immediate?: true
19
- readonly reply: (input: { args: string; positionals: string[] }, mod: Mod<State>) => Reply | void | Promise<Reply | void>
20
- }): Job<void, State> {
20
+ readonly reply: (input: { args: string; positionals: string[] }, mod: Mod<State>) => CommandReply | void | Promise<CommandReply | void>
21
+ }): Job<Disposable, State> {
21
22
  const { name, description, argumentHint, immediate, reply } = options
22
23
  return (job) => {
23
24
  const { mod, announce, reserveName } = job
24
- const { claude } = mod
25
+ const { claude, router, report } = engineOf(mod)
25
26
  if (!commandName.test(name)) throw new Error(`${mod.name}: "${name}" is not a slash command name. Use 1 to 64 letters, digits, "_", or "-", without the slash.`)
26
- reserveName('slashCommand', name, `${mod.name}: the slash command /${name} is already added. Give each slashCommand its own name.`)
27
+ const reserved = reserveName('slashCommand', name, `${mod.name}: the slash command /${name} is already added. Give each slashCommand its own name.`)
27
28
 
28
29
  let registered: string | undefined
29
30
  claude.command.register({ name, description, ...(argumentHint === undefined ? {} : { argumentHint }), ...(immediate === undefined ? {} : { immediate }) }).then(
@@ -31,24 +32,29 @@ export function slashCommand<State extends object = Record<never, never>>(option
31
32
  registered = command
32
33
  },
33
34
  (error: unknown) => {
34
- claude.ui.log(`${mod.name}: /${name} is not added: ${oneLine(error)}`)
35
+ report({ id: `command:${name}`, message: `/${name} is not added`, error })
35
36
  },
36
37
  )
37
38
  announce(`/${name}`)
38
39
 
39
- const replyMod = modWithin(job, deadline)
40
- claude.on('command.run', async (e, next) => {
40
+ const answering = router.add('command.run', async (e, next) => {
41
41
  if (e.command !== registered) return next(e)
42
42
  try {
43
- return answerOf(await reply({ args: e.args, positionals: splitWords(e.args) }, replyMod))
43
+ return answerOf(await withinDeadline(mod, deadline, `/${name}`, () => reply({ args: e.args, positionals: splitWords(e.args) }, mod)))
44
44
  } catch (error) {
45
45
  return { text: `/${name} failed: ${oneLine(error)}` }
46
46
  }
47
47
  })
48
+ return {
49
+ dispose() {
50
+ answering.dispose()
51
+ reserved.dispose()
52
+ },
53
+ }
48
54
  }
49
55
  }
50
56
 
51
- function answerOf(reply: Reply | void): CommandRunResult {
57
+ function answerOf(reply: CommandReply | void): CommandRunResult {
52
58
  if (reply === undefined) return {}
53
59
  if (typeof reply === 'string') return { text: reply }
54
60
  return { ...(reply.text === undefined ? {} : { text: reply.text }), ...(reply.context === undefined ? {} : { context: [reply.context] }) }
@@ -1,13 +1,10 @@
1
1
  import type { Job, Mod } from '../mod.js'
2
2
  import type { Deadline } from '../runtime/deadline.js'
3
+ import { engineOf } from '../runtime/engine.js'
4
+ import { usageOf } from '../runtime/sessions.js'
5
+ import type { Usage } from '../session/session.js'
3
6
  import { messageOf } from '../utils/text.js'
4
- import { modWithin } from './context.js'
5
-
6
- export type Usage = {
7
- model: string
8
- context: { tokens?: number; window: number; percent?: number }
9
- cost?: { usd: number }
10
- }
7
+ import { withinDeadline } from './context.js'
11
8
 
12
9
  const deadline: Deadline = { ms: 2000, job: 'statusLine' }
13
10
 
@@ -18,30 +15,21 @@ export function statusLine<State extends object = Record<never, never>>(options:
18
15
  const { text, interval = 10000 } = options
19
16
  return (job) => {
20
17
  const { mod, announce, reserveName } = job
21
- const { claude } = mod
18
+ const { claude, router } = engineOf(mod)
22
19
  if (!(interval >= 1)) throw new Error(`${mod.name}: the status line interval is ${interval}. Give it in milliseconds, 1 or more.`)
23
20
  reserveName('statusLine', '', `${mod.name}: a status line is already added. A mod has one status line: join the texts in one statusLine.`)
24
21
 
25
- const textMod = modWithin(job, deadline)
26
22
  let shown: string | undefined
27
23
  let queue = Promise.resolve()
28
24
  const update = async () => {
29
25
  try {
30
- const [model, usage] = await Promise.all([claude.session.model(), claude.session.usage()])
31
- const { tokens, window, percent } = usage.context
32
- const next = await text(
33
- {
34
- model,
35
- context: { window, ...(tokens === undefined ? {} : { tokens }), ...(percent === undefined ? {} : { percent }) },
36
- ...(usage.cost === undefined ? {} : { cost: { usd: usage.cost.usd } }),
37
- },
38
- textMod,
39
- )
26
+ const usage = await usageOf(claude)
27
+ const next = await withinDeadline(mod, deadline, 'the status line', () => text(usage, mod))
40
28
  if (next === shown) return
41
29
  shown = next
42
30
  claude.ui.status(next)
43
31
  } catch (error) {
44
- claude.ui.log(`${mod.name}: the status line kept its last text: ${messageOf(error)}`, { to: 'debug' })
32
+ mod.log(`the status line kept its last text: ${messageOf(error)}`)
45
33
  }
46
34
  }
47
35
  const refresh = () => {
@@ -49,7 +37,7 @@ export function statusLine<State extends object = Record<never, never>>(options:
49
37
  }
50
38
 
51
39
  announce('a status line')
52
- claude.on('session.measure', (e, next) => {
40
+ router.add('session.measure', (e, next) => {
53
41
  refresh()
54
42
  return next(e)
55
43
  })
package/src/jobs/tool.ts CHANGED
@@ -1,10 +1,11 @@
1
1
  import type { FromSchema, JSONSchema } from 'json-schema-to-ts'
2
- import type { Job, Mod } from '../mod.js'
2
+ import type { Disposable, Job, Mod } from '../mod.js'
3
3
  import { Validator } from '../vendor.js'
4
4
  import { toolDeadline } from '../runtime/deadline.js'
5
+ import { engineOf } from '../runtime/engine.js'
5
6
  import { reservedKeys, toolInputOf } from '../runtime/tool-calls.js'
6
7
  import { messageOf } from '../utils/text.js'
7
- import { modWithin } from './context.js'
8
+ import { withinDeadline } from './context.js'
8
9
 
9
10
  const toolName = /^[A-Za-z0-9_-]{1,64}$/
10
11
 
@@ -13,42 +14,47 @@ export function tool<const Schema extends object = { type: 'object' }, State ext
13
14
  readonly description: string
14
15
  readonly inputSchema?: Schema
15
16
  readonly execute: (input: Schema extends JSONSchema ? FromSchema<Schema> : never, mod: Mod<State>) => unknown
16
- }): Job<{ readonly name: string }, State> {
17
+ }): Job<Disposable & { readonly name: string }, State> {
17
18
  const { name, description, inputSchema, execute } = options
18
19
  return (job) => {
19
20
  const { mod, announce, reserveName } = job
20
- const { claude } = mod
21
+ const { claude, router, report } = engineOf(mod)
21
22
  if (!toolName.test(name)) throw new Error(`${mod.name}: "${name}" is not a tool name. Use 1 to 64 letters, digits, "_", or "-".`)
22
23
  const properties = (inputSchema as { properties?: object } | undefined)?.properties ?? {}
23
24
  const reserved = reservedKeys.filter((key) => Object.hasOwn(properties, key))
24
25
  if (reserved.length > 0) throw new Error(`${mod.name}: the ${name} tool's inputSchema has the property ${reserved.join(', ')}, which Claude Code keeps for itself. Rename it.`)
25
- reserveName('tool', name, `${mod.name}: the tool ${name} is already added. Give each tool its own name.`)
26
+ const named = reserveName('tool', name, `${mod.name}: the tool ${name} is already added. Give each tool its own name.`)
26
27
 
27
- const validator = new Validator((inputSchema ?? { type: 'object' }) as ConstructorParameters<typeof Validator>[0], '2020-12', false)
28
+ const validator = new Validator(inputSchema ?? { type: 'object' }, '2020-12', false)
28
29
  let registered: string | undefined
29
30
  claude.tool.register({ name, description, ...(inputSchema === undefined ? {} : { inputSchema: inputSchema as Record<string, unknown> }) }).then(
30
31
  (answer) => {
31
32
  registered = answer.tool
32
33
  },
33
34
  (error: unknown) => {
34
- claude.ui.log(`${mod.name}: the ${name} tool is not added: ${messageOf(error)}`)
35
+ report({ id: `tool:${name}`, message: `The ${name} tool is not added`, error })
35
36
  },
36
37
  )
37
38
  announce(`the ${name} tool`)
38
39
 
39
- const executeMod = modWithin(job, toolDeadline)
40
- claude.on('tool.call', async (e, next) => {
40
+ const answering = router.add('tool.call', async (e, next) => {
41
41
  if (e.tool !== registered) return next(e)
42
42
  const input = toolInputOf(e)
43
43
  const { valid, errors } = validator.validate(input)
44
44
  if (!valid) return { deny: `The ${name} tool input is not valid:\n${errors.map((error) => `- ${error.instanceLocation}: ${error.error}`).join('\n')}` }
45
45
  try {
46
- return { result: resultOf(await execute(input as Parameters<typeof execute>[0], executeMod)) }
46
+ return { result: resultOf(await withinDeadline(mod, toolDeadline, `the ${name} tool`, () => execute(input as Parameters<typeof execute>[0], mod))) }
47
47
  } catch (error) {
48
48
  return { deny: `The ${name} tool failed: ${messageOf(error)}` }
49
49
  }
50
50
  })
51
- return { name: `mcp__${claude.plugin.name}__${name}` }
51
+ return {
52
+ name: `mcp__${claude.plugin.name}__${name}`,
53
+ dispose() {
54
+ answering.dispose()
55
+ named.dispose()
56
+ },
57
+ }
52
58
  }
53
59
  }
54
60