@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
@@ -1,7 +1,9 @@
1
1
  import { permissionWords } from '../records.js'
2
2
  import { relativePath } from '../utils/paths.js'
3
3
  import type { Claude } from './claude.js'
4
+ import { cmodPluginName } from './dependencies.js'
4
5
  import type { RoutedEvent } from './hooks.js'
6
+ import { conversationMethods } from './sessions.js'
5
7
 
6
8
  export type Grants = {
7
9
  readonly name: string
@@ -12,24 +14,50 @@ export type Grants = {
12
14
  readonly configRoot: string | undefined
13
15
  readonly projectRoot: () => string
14
16
  readonly freeFolders: () => readonly string[]
17
+ readonly freeAddresses: () => ReadonlySet<string>
15
18
  }
16
19
 
17
20
  export class PermissionRefused extends Error {}
18
21
 
19
- const configFiles: readonly string[] = ['.claude/settings.json', '.claude/settings.local.json', '.mcp.json']
22
+ const addressesOfPrograms = new WeakMap<object, Set<string>>()
23
+
24
+ export function programAddresses(mod: object): Set<string> {
25
+ const known = addressesOfPrograms.get(mod) ?? new Set<string>()
26
+ addressesOfPrograms.set(mod, known)
27
+ return known
28
+ }
29
+
30
+ const projectSettingsFiles: readonly string[] = ['.claude/settings.json', '.claude/settings.local.json', '.mcp.json']
20
31
 
21
32
  const startsTurns: readonly string[] = ['CronCreate']
22
33
 
23
34
  const ownSchedule: readonly string[] = ['CronDelete', 'CronList']
24
35
 
25
- async function checkGrant(grants: Grants, call: string, item: string | undefined): Promise<void> {
36
+ const conversationCalls: Readonly<Record<string, string>> = {
37
+ [conversationMethods.sessions]: 'sessions.list',
38
+ [conversationMethods.search]: 'sessions.search',
39
+ [conversationMethods.messages]: 'messages',
40
+ [conversationMethods.turns]: 'turns',
41
+ [conversationMethods.contexts]: 'contexts',
42
+ [conversationMethods.tasks]: 'tasks',
43
+ [conversationMethods.goal]: 'goal',
44
+ [conversationMethods.updateGoal]: 'goal.update',
45
+ [conversationMethods.subscribe]: 'on',
46
+ [conversationMethods.send]: 'send',
47
+ }
48
+
49
+ export async function checkGrant(grants: Grants, call: string, item: string | undefined): Promise<void> {
26
50
  if (item === undefined) return
27
51
  await grants.refresh()
28
52
  if (!isCovered(grants.granted(), item, grants.home)) throw new PermissionRefused(refusal(grants, call, item))
29
53
  }
30
54
 
31
- export function checkWrite(grants: Grants, call: string, path: string): Promise<void> {
32
- return checkGrant(grants, call, writeItem(grants, path))
55
+ export async function checkWrite(grants: Grants, call: string, path: string): Promise<void> {
56
+ await checkGrant(grants, call, writeItem(grants, path))
57
+ }
58
+
59
+ export async function checkSettings(grants: Grants, source: 'user' | 'project' | 'local', keys: readonly string[]): Promise<void> {
60
+ for (const key of keys) await checkGrant(grants, `settings.${source}.update(${key})`, `config:${key}`)
33
61
  }
34
62
 
35
63
  export function checkingGrants(claude: Claude, grants: Grants): Claude {
@@ -59,54 +87,58 @@ export function checkingGrants(claude: Claude, grants: Grants): Claude {
59
87
  },
60
88
  http: {
61
89
  async fetch(url, init) {
62
- await gate(`http.fetch(${url})`, `network:${init?.socketPath ?? hostOf(url)}`)
90
+ if (!grants.freeAddresses().has(init?.socketPath ?? addressOf(url))) await gate(`http.fetch(${url})`, `network:${init?.socketPath ?? hostOf(url)}`)
63
91
  return claude.http.fetch(url, init)
64
92
  },
65
93
  },
66
- config: {
67
- ...claude.config,
68
- set: (async (args: Parameters<Claude['config']['set']>[0]) => {
69
- await gate('config.set', 'config')
70
- return claude.config.set(args)
71
- }) as Claude['config']['set'],
72
- },
73
94
  session: {
74
95
  ...claude.session,
75
96
  messages: (async (...args: Parameters<Claude['session']['messages']>) => {
76
97
  await gate('session.messages', 'conversation')
77
98
  return claude.session.messages(...args)
78
99
  }) as Claude['session']['messages'],
79
- append: (async (args: Parameters<Claude['session']['append']>[0]) => {
80
- await gate('session.append', 'prompt')
100
+ append: async (args: Parameters<Claude['session']['append']>[0]) => {
101
+ await gate('send', 'prompt')
81
102
  return claude.session.append(args)
82
- }) as Claude['session']['append'],
103
+ },
104
+ send: async (args: Parameters<Claude['session']['send']>[0]) => {
105
+ await gate('send', 'prompt')
106
+ return claude.session.send(args)
107
+ },
83
108
  },
84
109
  prompt: {
85
- submit: (async (args: Parameters<Claude['prompt']['submit']>[0]) => {
86
- await gate('session.submit', 'prompt')
110
+ submit: async (args: Parameters<Claude['prompt']['submit']>[0]) => {
111
+ await gate('send', 'prompt')
87
112
  return claude.prompt.submit(args)
88
- }) as Claude['prompt']['submit'],
113
+ },
114
+ },
115
+ cmod: {
116
+ async call(args) {
117
+ const named = args.to === cmodPluginName ? conversationCalls[args.method] : undefined
118
+ if (named !== undefined) await gate(named, args.method === conversationMethods.send ? 'prompt' : 'conversation')
119
+ return claude.cmod.call(args)
120
+ },
89
121
  },
90
122
  model: {
91
- complete: (async (...args: Parameters<Claude['model']['complete']>) => {
123
+ complete: async (...args: Parameters<Claude['model']['complete']>) => {
92
124
  await gate('model.complete', 'model')
93
125
  return claude.model.complete(...args)
94
- }) as Claude['model']['complete'],
126
+ },
95
127
  },
96
128
  agent: {
97
129
  ...claude.agent,
98
130
  async spawn(args) {
99
- await gate('agent.spawn', 'agents')
131
+ await gate('startTask', 'tasks')
100
132
  return claude.agent.spawn(args)
101
133
  },
102
134
  },
103
135
  tool: {
104
136
  ...claude.tool,
105
- call: (async (input: Parameters<Claude['tool']['call']>[0]) => {
137
+ call: async (input: Parameters<Claude['tool']['call']>[0]) => {
106
138
  const item = startsTurns.includes(input.tool) ? 'prompt' : ownSchedule.includes(input.tool) ? undefined : 'tools'
107
139
  await gate(`tool.call(${input.tool})`, item)
108
140
  return claude.tool.call(input)
109
- }) as Claude['tool']['call'],
141
+ },
110
142
  },
111
143
  }
112
144
  }
@@ -119,8 +151,8 @@ function spawned(started: Promise<Stream>): Stream {
119
151
  const stream = {
120
152
  next: async () => (await started).next(),
121
153
  async return(value: never) {
122
- const running = await started.catch(() => undefined)
123
- return (await running?.return?.(value)) ?? { done: true as const, value }
154
+ const [start] = await Promise.allSettled([started])
155
+ return start.status === 'fulfilled' ? start.value.return(value) : { done: true as const, value }
124
156
  },
125
157
  [Symbol.asyncIterator]: () => stream,
126
158
  result,
@@ -151,7 +183,10 @@ function readItem(grants: Grants, path: string): string | undefined {
151
183
 
152
184
  function writeItem(grants: Grants, path: string): string | undefined {
153
185
  const root = grants.projectRoot()
154
- if (configFiles.some((file) => `${root}/${file}` === path)) return 'config'
186
+ const settingsFiles = [...projectSettingsFiles.map((file) => `${root}/${file}`), ...(grants.configRoot === undefined ? [] : [`${grants.configRoot}/settings.json`])]
187
+ if (settingsFiles.includes(path)) {
188
+ throw new PermissionRefused(`${grants.name} writes ${path} by hand. Change Claude Code's settings with settings.user.update, settings.project.update, or settings.local.update, which keep the person's own edits and take the mod's values out when it is removed.`)
189
+ }
155
190
  if (grants.freeFolders().some((folder) => relativePath(folder, path) !== undefined)) return undefined
156
191
  return `files:${path}`
157
192
  }
@@ -294,11 +329,11 @@ function programOf(argv: readonly string[]): string {
294
329
  }
295
330
 
296
331
  function hostOf(url: string): string {
297
- try {
298
- return new URL(url).hostname
299
- } catch {
300
- return url
301
- }
332
+ return URL.canParse(url) ? new URL(url).hostname : url
333
+ }
334
+
335
+ function addressOf(url: string): string {
336
+ return URL.canParse(url) ? new URL(url).host : url
302
337
  }
303
338
 
304
339
  function expanded(path: string, home: string | undefined): string {
@@ -1,15 +1,17 @@
1
1
  import type { Args, ClassicHookInputs, EventResult, Frozen } from 'claude-code'
2
- import type { HookAnswer, HookInput, ModEvent, ModHook } from '../mod.js'
2
+ import type { Hook, HookAnswer, HookInput, HookListener } from '../mod.js'
3
3
  import { configFolders } from '../records.js'
4
4
  import type { Claude } from './claude.js'
5
5
  import type { RoutedHook } from './router.js'
6
+ import type { Failure } from './toaster.js'
6
7
  import { reservedKeys, toolInputOf, type ToolCalls } from './tool-calls.js'
7
8
  import { callEffects, inputOf, type CallBase, type FileAccess } from '../utils/call-effects.js'
8
9
  import { dynamicPattern } from '../utils/parse-shell.js'
10
+ import { isMissing } from '../utils/paths.js'
9
11
  import { messageOf } from '../utils/text.js'
10
12
 
11
13
  export type RoutedEvent =
12
- | `classic.${Exclude<ModEvent, 'PreToolUse'>}`
14
+ | `classic.${Exclude<Hook, 'PreToolUse'>}`
13
15
  | 'tool.check'
14
16
  | 'tool.call'
15
17
  | 'prompt.submit'
@@ -22,6 +24,10 @@ export type RoutedEvent =
22
24
  | 'ui.scroll'
23
25
  | 'ui.close'
24
26
  | 'cmod.call'
27
+ | 'session.append'
28
+ | 'session.compact'
29
+ | 'session.receive'
30
+ | 'agent.spawn'
25
31
 
26
32
  export const permissionEvents: readonly RoutedEvent[] = ['tool.check', 'classic.PermissionRequest']
27
33
 
@@ -31,7 +37,7 @@ type CallFiles = HookInput<'PreToolUse'>['files']
31
37
 
32
38
  const flags: readonly string[] = ['suppressOriginalPrompt', 'reloadSkills', 'retry']
33
39
 
34
- const readFields: Record<ModEvent, readonly string[]> = {
40
+ const readFields: Record<Hook, readonly string[]> = {
35
41
  SessionStart: ['additionalContext', 'initialUserMessage', 'sessionTitle', 'watchPaths', 'reloadSkills'],
36
42
  SessionEnd: [],
37
43
  UserPromptSubmit: ['additionalContext', 'sessionTitle', 'suppressOriginalPrompt'],
@@ -52,7 +58,7 @@ const readFields: Record<ModEvent, readonly string[]> = {
52
58
  FileChanged: [],
53
59
  }
54
60
 
55
- export function classicHook<E extends Exclude<ModEvent, 'PreToolUse'>>(name: string, event: E, hook: ModHook<E>, claude: Claude, calls: ToolCalls): RoutedHook<RoutedEvent> {
61
+ export function classicHook<E extends Exclude<Hook, 'PreToolUse'>>(name: string, event: E, hook: HookListener<E>, claude: Claude, calls: ToolCalls, report: (failure: Failure) => void): RoutedHook<RoutedEvent> {
56
62
  const hookInputOf = async (e: unknown): Promise<unknown> => {
57
63
  if (event !== 'PostToolUse' && event !== 'PostToolUseFailure') return e
58
64
  const input = e as ClassicHookInputs['PostToolUse' | 'PostToolUseFailure']
@@ -67,9 +73,8 @@ export function classicHook<E extends Exclude<ModEvent, 'PreToolUse'>>(name: str
67
73
  }
68
74
  const routed = async (e: unknown, next: (e: unknown) => Promise<unknown>) => {
69
75
  const ours = await resultOf(e).catch((error: unknown): ClassicFields | undefined => {
70
- const reason = `${name}: the ${event} hook failed: ${messageOf(error)}`
71
- if (event === 'PermissionRequest') return { decision: { behavior: 'deny', message: reason } }
72
- claude.ui.log(reason)
76
+ if (event === 'PermissionRequest') return { decision: { behavior: 'deny', message: `${name}: the ${event} hook failed: ${messageOf(error)}` } }
77
+ report({ id: `hook:${event}`, message: `The ${event} hook failed`, error })
73
78
  return undefined
74
79
  })
75
80
  if (ours === undefined) return next(e)
@@ -82,7 +87,7 @@ type HeldDecision = { readonly decision: 'allow' | 'ask'; readonly reason?: stri
82
87
 
83
88
  export type HeldDecisions = Map<string, HeldDecision>
84
89
 
85
- export function preToolUseHook(name: string, hook: ModHook<'PreToolUse'>, claude: Claude, calls: ToolCalls, held: HeldDecisions | undefined): RoutedHook<'tool.call'> {
90
+ export function preToolUseHook(name: string, hook: HookListener<'PreToolUse'>, claude: Claude, calls: ToolCalls, held: HeldDecisions | undefined): RoutedHook<'tool.call'> {
86
91
  return async (e, next) => {
87
92
  let ours: ClassicFields
88
93
  try {
@@ -171,7 +176,11 @@ async function callFiles(name: string, claude: Claude, use: CallBase, cwd: strin
171
176
  if (home === undefined) throw new Error('HOME is not set, so ~ in a path has no meaning.')
172
177
  const { shell, reads, writes } = callEffects(use, { cwd, home, fs: claude.fs })
173
178
  const known = (accesses: FileAccess[]) => accesses.map(({ path }) => path).filter((path) => shell === undefined || !dynamicPattern.test(path))
174
- const read = await Promise.all(known(reads).map(async (path) => ((await claude.fs.stat(path).catch(() => undefined))?.kind === 'file' ? [path] : [])))
179
+ const unchecked = (path: string) => (error: unknown) => {
180
+ if (!isMissing(error)) claude.ui.log(`${name}: the ${use.tool} call reads ${path}, which could not be checked: ${messageOf(error)}`, { to: 'debug' })
181
+ return undefined
182
+ }
183
+ const read = await Promise.all(known(reads).map(async (path) => ((await claude.fs.stat(path).catch(unchecked(path)))?.kind === 'file' ? [path] : [])))
175
184
  return { read: read.flat(), changed: known(writes) }
176
185
  } catch (error) {
177
186
  claude.ui.log(`${name}: the ${use.tool} call lists no files: ${messageOf(error)}`, { to: 'debug' })
@@ -179,7 +188,7 @@ async function callFiles(name: string, claude: Claude, use: CallBase, cwd: strin
179
188
  }
180
189
  }
181
190
 
182
- function classicResult(event: ModEvent, answer: HookAnswer): ClassicFields {
191
+ function classicResult(event: Hook, answer: HookAnswer): ClassicFields {
183
192
  const result: ClassicFields = {}
184
193
  const unread = (field: string) => new Error(`it answered ${field}, which ${event} does not read. Remove it from the answer.`)
185
194
  if (event === 'PreToolUse') {
@@ -102,12 +102,12 @@ export function createInstaller(name: string, claude: () => Claude): Installer {
102
102
  `${name} needs ${missing.length === 1 ? 'one setting' : `${missing.length} settings`}:`,
103
103
  missing.map(([key, option]) => {
104
104
  const label = Text({ children: [Text({ bold: true, children: option.title }), option.description === '' ? '' : Text({ dimColor: true, children: ` ${option.description}` })] })
105
- if (option.kind === 'secret') return Box({ flexDirection: 'column', children: [label, Text({ dimColor: true, children: `Set it in /config, where Claude Code keeps it in your keychain, then press Next.` })] })
105
+ if (option.type === 'secret') return Box({ flexDirection: 'column', children: [label, Text({ dimColor: true, children: `Set it in /config, where Claude Code keeps it in your keychain, then press Next.` })] })
106
106
  const failure = failures.get(key)
107
107
  const field = Input({
108
108
  key: `option:${key}`,
109
109
  label: '› ',
110
- placeholder: option.kind === 'choice' ? (option.choices ?? []).join(', ') : option.kind === 'toggle' ? 'true or false' : option.kind === 'list' ? 'comma-separated' : '',
110
+ placeholder: option.type === 'choice' ? (option.choices ?? []).join(', ') : option.type === 'toggle' ? 'true or false' : option.type === 'list' ? 'comma-separated' : '',
111
111
  value: saved.get(key) ?? '',
112
112
  submitLabel: 'save',
113
113
  onSubmit: (value: string) =>
@@ -131,7 +131,7 @@ export function createInstaller(name: string, claude: () => Claude): Installer {
131
131
  )
132
132
  },
133
133
  step(mod, step, index, total) {
134
- let notice: string | undefined
134
+ let notice: { readonly color: 'warning' | 'error'; readonly text: string } | undefined
135
135
  return show<boolean>(
136
136
  (answer) => ({
137
137
  draw: (props) => {
@@ -142,16 +142,17 @@ export function createInstaller(name: string, claude: () => Claude): Installer {
142
142
  body = Text({ color: 'error', children: `The ${step.title} step could not draw: ${messageOf(error)}` })
143
143
  }
144
144
  const next = async () => {
145
- const isDone = await Promise.resolve(step.isDone(mod)).catch(() => false)
146
- if (isDone) answer(true)
147
- else {
148
- notice = `Finish ${step.title} first.`
149
- claude().ui.invalidate('ui.render')
145
+ try {
146
+ if (await step.isDone(mod)) return answer(true)
147
+ notice = { color: 'warning', text: `Finish ${step.title} first.` }
148
+ } catch (error) {
149
+ notice = { color: 'error', text: `The ${step.title} step's isDone failed: ${messageOf(error)}` }
150
150
  }
151
+ claude().ui.invalidate('ui.render')
151
152
  }
152
153
  return frame(
153
154
  `${step.title} (${index} of ${total})`,
154
- [body, ...(notice === undefined ? [] : [Text({ color: 'warning', children: notice })])],
155
+ [body, ...(notice === undefined ? [] : [Text({ color: notice.color, children: notice.text })])],
155
156
  [Button({ key: 'not-now', hotkey: 'n', label: 'Not now', onPress: () => answer(false) }), Button({ key: 'next', hotkey: 'x', label: index === total ? 'Finish' : 'Next', onPress: () => void next() })],
156
157
  )
157
158
  },