@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
@@ -0,0 +1,99 @@
1
+ import type { Disposable } from '../mod.js';
2
+ import type { Matcher } from '../utils/matcher.js';
3
+ import type { Context, ContextEnd, ContextStart } from './context/context.js';
4
+ import type { Document } from './context/turn/message/document.js';
5
+ import type { Message, MessageType } from './context/turn/message/message.js';
6
+ import type { Note } from './context/turn/message/note.js';
7
+ import type { Notice } from './context/turn/message/notice.js';
8
+ import type { Prompt } from './context/turn/message/prompt.js';
9
+ import type { Reply } from './context/turn/message/reply.js';
10
+ import type { Person } from './context/turn/message/sender.js';
11
+ import type { Skill } from './context/turn/message/skill.js';
12
+ import type { ToolCall } from './context/turn/message/tool-call.js';
13
+ import type { ToolResult } from './context/turn/message/tool-result.js';
14
+ import type { Turn, TurnEnd, TurnStart } from './context/turn/turn.js';
15
+ import type { Goal, GoalChange } from './goal/goal.js';
16
+ import type { Task, TaskEnd, TaskStart } from './goal/task.js';
17
+ export declare const Session = "session";
18
+ export type SessionEvent = typeof Message | MessageType | typeof ContextStart | typeof ContextEnd | typeof TurnStart | typeof TurnEnd | typeof TaskStart | typeof TaskEnd | typeof GoalChange;
19
+ export type SessionPayload = {
20
+ readonly message: Message;
21
+ readonly prompt: Prompt;
22
+ readonly note: Note;
23
+ readonly reply: Reply;
24
+ readonly toolCall: ToolCall;
25
+ readonly toolResult: ToolResult;
26
+ readonly skill: Skill;
27
+ readonly document: Document;
28
+ readonly notice: Notice;
29
+ readonly contextStart: Context;
30
+ readonly contextEnd: Context;
31
+ readonly turnStart: Turn;
32
+ readonly turnEnd: Turn;
33
+ readonly taskStart: Task;
34
+ readonly taskEnd: Task;
35
+ readonly goalChange: Goal;
36
+ };
37
+ export type Usage = {
38
+ readonly model: string;
39
+ readonly context: {
40
+ readonly tokens?: number;
41
+ readonly window: number;
42
+ readonly percent?: number;
43
+ };
44
+ readonly cost?: {
45
+ readonly usd: number;
46
+ };
47
+ };
48
+ export type Listener<Event extends SessionEvent> = (payload: SessionPayload[Event]) => unknown;
49
+ export type SessionSend = {
50
+ readonly type: typeof Prompt | typeof Note;
51
+ readonly text: string;
52
+ readonly as?: typeof Person | typeof Session;
53
+ };
54
+ export type TaskStartArgs = {
55
+ readonly prompt: string;
56
+ readonly agentType?: string;
57
+ readonly model?: string;
58
+ readonly name?: string;
59
+ readonly cwd?: string;
60
+ };
61
+ export type Session = {
62
+ readonly id: string;
63
+ readonly title: string;
64
+ readonly project: string;
65
+ readonly cwd: string;
66
+ readonly model: string;
67
+ readonly startedAt: string;
68
+ readonly lastAt: string;
69
+ readonly forkedFrom?: string;
70
+ readonly isRunning: boolean;
71
+ usage(): Promise<Usage>;
72
+ on<Event extends SessionEvent>(event: Event, listener: Listener<Event>): Disposable;
73
+ on<Event extends SessionEvent>(event: Event, matcher: Matcher<SessionPayload[Event]>, listener: Listener<Event>): Disposable;
74
+ send(message: SessionSend): Promise<void>;
75
+ startTask(task: TaskStartArgs): Promise<Task>;
76
+ messages(options?: {
77
+ readonly after?: string;
78
+ readonly last?: number;
79
+ }): Promise<Message[]>;
80
+ turns(options?: {
81
+ readonly last?: number;
82
+ }): Promise<Turn[]>;
83
+ contexts(): Promise<Context[]>;
84
+ goal(): Promise<Goal>;
85
+ };
86
+ export type SessionScope = {
87
+ readonly running?: boolean;
88
+ readonly since?: string;
89
+ readonly everyProject?: boolean;
90
+ };
91
+ export type Sessions = {
92
+ readonly current: Session;
93
+ list(options?: SessionScope): Promise<Session[]>;
94
+ search(text: string, options?: SessionScope & {
95
+ readonly limit?: number;
96
+ readonly before?: Pick<Message, 'session' | 'id'>;
97
+ }): Promise<Message[]>;
98
+ };
99
+ //# sourceMappingURL=session.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session.d.ts","sourceRoot":"","sources":["../src/session/session.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1 @@
1
+ export const Session = 'session';
package/src/jobs/check.ts CHANGED
@@ -1,9 +1,10 @@
1
1
  import type { Job } from '../mod.js'
2
2
  import { longestMs, type Deadline } from '../runtime/deadline.js'
3
- import { callEffects, type ToolCall } from '../utils/call-effects.js'
3
+ import { callEffects, type Call } from '../utils/call-effects.js'
4
4
  import { dirname } from '../vendor.js'
5
5
  import { listed, messageOf } from '../utils/text.js'
6
- import { afterCall, modOf, targetWords } from './context.js'
6
+ import { engineOf } from '../runtime/engine.js'
7
+ import { afterCall, targetWords, withinDeadline } from './context.js'
7
8
  import type { Workspace } from './permissions/find-project-scope.js'
8
9
  import { matchTarget, targetOf, type Target } from './permissions/match-target.js'
9
10
 
@@ -27,7 +28,9 @@ export function check<State extends object = Record<never, never>>(options: { re
27
28
  if (triggered === undefined) return []
28
29
  try {
29
30
  const { command, folder } = triggered
30
- const { exitCode, stdout, stderr } = await modOf(job, { folder }, workspace, deadline).process.run(command)
31
+ const workTree = await workspace.scope?.workTreeOf(folder)
32
+ const { claude } = engineOf(job.mod)
33
+ const { exitCode, stdout, stderr } = await withinDeadline(job.mod, deadline, command.join(' '), () => claude.process.run(command, { ...(workTree === undefined ? {} : { cwd: workTree }), timeoutMs }))
31
34
  return exitCode === 0 ? [] : [`${command.join(' ')} exited with ${exitCode}:\n${`${stdout}${stderr}`.trim()}`]
32
35
  } catch (error) {
33
36
  return [`The ${named} failed: ${messageOf(error)}`]
@@ -39,7 +42,7 @@ export function check<State extends object = Record<never, never>>(options: { re
39
42
  }
40
43
  }
41
44
 
42
- async function triggeredRun(after: readonly Target[], run: readonly string[], use: ToolCall, workspace: Workspace): Promise<{ command: string[]; folder: string } | undefined> {
45
+ async function triggeredRun(after: readonly Target[], run: readonly string[], use: Call, workspace: Workspace): Promise<{ command: string[]; folder: string } | undefined> {
43
46
  const effects = callEffects(use, workspace)
44
47
  const paths = new Set<string>()
45
48
  let commandFolder: string | undefined
@@ -1,48 +1,42 @@
1
- import type { Args, Frozen, HookStream, ProcessSpawnChunk, ProcessSpawnResult } from 'claude-code'
2
- import type { JobContext, Mod } from '../mod.js'
3
- import type { Claude } from '../runtime/claude.js'
1
+ import type { JobContext } from '../mod.js'
4
2
  import { beforeDeadline, type Deadline } from '../runtime/deadline.js'
5
- import { dependencyCalls } from '../runtime/dependencies.js'
3
+ import { engineOf } from '../runtime/engine.js'
6
4
  import { toolInputOf } from '../runtime/tool-calls.js'
7
5
  import { listed } from '../utils/text.js'
8
- import type { ToolCall } from '../utils/call-effects.js'
6
+ import type { Call } from '../utils/call-effects.js'
9
7
  import { findProjectScope, type Workspace } from './permissions/find-project-scope.js'
10
8
  import { targetOf, type Target } from './permissions/match-target.js'
11
9
  import type { FileSystem } from '../utils/paths.js'
12
10
 
13
11
  type Session = Omit<Workspace, 'projectRoot' | 'cwd'>
14
12
 
15
- type Stream = HookStream<ProcessSpawnChunk, ProcessSpawnResult>
16
-
17
- export function workspaceReader({ mod: { claude } }: JobContext): () => Promise<Workspace> {
13
+ export function workspaceReader({ mod }: JobContext): () => Promise<Workspace> {
14
+ const { claude } = engineOf(mod)
18
15
  let session: Promise<Session> | undefined
19
16
  return async () => {
20
- session ??= readSession(claude)
17
+ session ??= readSession()
21
18
  const [{ home, scope, fs }, projectRoot, cwd] = await Promise.all([session, claude.session.root(), claude.session.cwd()])
22
19
  return { projectRoot, cwd, home, fs, scope }
23
20
  }
24
- }
25
21
 
26
- async function readSession(claude: Claude): Promise<Session> {
27
- const fs: FileSystem = {
28
- read: (path) => claude.fs.read(path),
29
- stat: (path) => claude.fs.stat(path, { resolve: true }),
30
- exists: (path) => claude.fs.exists(path),
22
+ async function readSession(): Promise<Session> {
23
+ const fs: FileSystem = {
24
+ read: (path) => claude.fs.read(path),
25
+ stat: (path) => claude.fs.stat(path, { resolve: true }),
26
+ exists: (path) => claude.fs.exists(path),
27
+ }
28
+ const home = await claude.env.home()
29
+ if (home === undefined) throw new Error('HOME is not set, so ~ in a path has no meaning. Start Claude Code with HOME set.')
30
+ return { home, fs, scope: await findProjectScope(claude.plugin.root, home, fs) }
31
31
  }
32
- const home = await claude.env.home()
33
- if (home === undefined) throw new Error('HOME is not set, so ~ in a path has no meaning. Start Claude Code with HOME set.')
34
- return { home, fs, scope: await findProjectScope(claude.plugin.root, home, fs) }
35
32
  }
36
33
 
37
- export function afterCall<State extends object>(
38
- job: JobContext<State>,
39
- contextAfter: (call: ToolCall, workspace: Workspace) => Promise<readonly string[]>,
40
- failed: (error: unknown) => readonly string[],
41
- ): void {
34
+ export function afterCall<State extends object>(job: JobContext<State>, contextAfter: (call: Call, workspace: Workspace) => Promise<readonly string[]>, failed: (error: unknown) => readonly string[]): void {
35
+ const { router, toolCalls } = engineOf(job.mod)
42
36
  const readWorkspace = workspaceReader(job)
43
- job.mod.claude.on('tool.call', async (e, next) => {
44
- const before = Promise.all([useOf(job, e), readWorkspace()])
45
- await before.catch(() => undefined)
37
+ router.add('tool.call', async (e, next) => {
38
+ const before = Promise.all([toolCalls.agentOf(e.tool_use_id).then((agent): Call => ({ tool: e.tool, input: toolInputOf(e), ...agent })), readWorkspace()])
39
+ await Promise.allSettled([before])
46
40
  const result = await next(e)
47
41
  if (result.deny !== undefined || result.isError === true) return result
48
42
  const added = await before.then(([use, workspace]) => contextAfter(use, workspace)).catch(failed)
@@ -50,64 +44,12 @@ export function afterCall<State extends object>(
50
44
  })
51
45
  }
52
46
 
53
- async function useOf({ toolCalls }: JobContext, e: Frozen<Args<'tool.call'>>): Promise<ToolCall> {
54
- return { tool: e.tool, input: toolInputOf(e), ...(await toolCalls.agentOf(e.tool_use_id)) }
47
+ export async function inFolder(call: Call, folder: string, workspace: Workspace): Promise<Call & { readonly folder: string }> {
48
+ return { ...call, folder: (await workspace.scope?.workTreeOf('path' in call ? call.path : folder)) ?? folder }
55
49
  }
56
50
 
57
- export function modOf<State extends object>(job: JobContext<State>, { call, folder }: { readonly call?: ToolCall; readonly folder: string }, workspace: Workspace, deadline: Deadline): Mod<State> {
58
- return modWithin(job, deadline, async () => workspace.scope?.workTreeOf(call !== undefined && 'path' in call ? call.path : folder))
59
- }
60
-
61
- export function modWithin<State extends object>({ mod }: JobContext<State>, deadline: Deadline, workTree: () => Promise<string | undefined> = async () => undefined): Mod<State> {
62
- const { claude } = mod
63
- const within = <Value>(call: string, task: Promise<Value>) => beforeDeadline(claude, deadline, call, task)
64
- const folderOf = async (cwd: string | undefined) => {
65
- const folder = cwd ?? (await workTree())
66
- return folder === undefined ? {} : { cwd: folder }
67
- }
68
- const overrides: Pick<Mod<State>, 'process' | 'fs' | 'http' | 'dependencies'> = {
69
- process: {
70
- async run(argv, init) {
71
- const folder = await folderOf(init?.cwd)
72
- const timeoutMs = Math.min(init?.timeoutMs ?? deadline.ms, deadline.longestMs ?? deadline.ms)
73
- return beforeDeadline(claude, { ...deadline, ms: timeoutMs }, 'mod.process.run', mod.process.run(argv, { ...init, ...folder, timeoutMs }))
74
- },
75
- spawn: (argv, init) => spawnWithin(claude, deadline, folderOf(init?.cwd).then((folder) => mod.process.spawn(argv, { ...init, ...folder }))),
76
- },
77
- fs: {
78
- read: (path) => within('mod.fs.read', mod.fs.read(path)),
79
- write: (path, text) => within('mod.fs.write', mod.fs.write(path, text)),
80
- list: (path) => within('mod.fs.list', mod.fs.list(path)),
81
- exists: (path) => within('mod.fs.exists', mod.fs.exists(path)),
82
- stat: (path, options) => within('mod.fs.stat', mod.fs.stat(path, options)),
83
- find: (glob) => within('mod.fs.find', mod.fs.find(glob)),
84
- },
85
- http: { fetch: (url, init) => within('mod.http.fetch', mod.http.fetch(url, init)) },
86
- dependencies: dependencyCalls(claude, within),
87
- }
88
- return Object.assign(Object.create(mod) as Mod<State>, overrides)
89
- }
90
-
91
- function spawnWithin(claude: Claude, deadline: Deadline, started: Promise<Stream>): Stream {
92
- async function* pieces(): AsyncGenerator<ProcessSpawnChunk, ProcessSpawnResult> {
93
- const stream: AsyncIterator<ProcessSpawnChunk, ProcessSpawnResult> = await started
94
- let finish: () => void = () => undefined
95
- const running = beforeDeadline(claude, deadline, 'mod.process.spawn', new Promise<void>((resolve) => (finish = resolve)))
96
- const passed = running.then(() => new Promise<never>(() => undefined))
97
- try {
98
- for (;;) {
99
- const piece = await Promise.race([stream.next(), passed])
100
- if (piece.done === true) return piece.value
101
- yield piece.value
102
- }
103
- } finally {
104
- finish()
105
- void stream.return?.()
106
- }
107
- }
108
- const result = started.then((stream) => stream.result)
109
- result.catch(() => undefined)
110
- return Object.assign(pieces(), { result })
51
+ export function withinDeadline<Value>(mod: object, deadline: Deadline, subject: string, run: () => Value | Promise<Value>): Promise<Value> {
52
+ return beforeDeadline(engineOf(mod).claude, deadline, subject, Promise.resolve().then(run))
111
53
  }
112
54
 
113
55
  export function targetWords(target: Target): string {
@@ -1,17 +1,18 @@
1
1
  import { strictness } from '../../runtime/hooks.js'
2
2
  import { messageOf } from '../../utils/text.js'
3
- import { callBaseOf, callEffects, type CallEffects, type ToolCall } from '../../utils/call-effects.js'
3
+ import { callBaseOf, callEffects, type CallEffects, type Call } from '../../utils/call-effects.js'
4
4
  import type { Workspace } from './find-project-scope.js'
5
5
  import { folderInScope, matchTarget, targetOf, type MatchedCall, type Target, type TargetCalls, type TargetKey, type TargetOf } from './match-target.js'
6
6
  import { resolve } from '../../vendor.js'
7
+ import { inFolder } from '../context.js'
7
8
 
8
9
  export type Rule<Mod> = {
9
10
  [Key in TargetKey]: TargetOf<Key> & {
10
- when?: (call: TargetCalls[Key], mod: Mod) => boolean | Promise<boolean>
11
+ when?: (call: TargetCalls[Key] & { readonly folder: string }, mod: Mod) => boolean | Promise<boolean>
11
12
  reason?: string
12
13
  }
13
14
  }[TargetKey]
14
- type WhenOf<Mod> = { when?(call: ToolCall, mod: Mod): boolean | Promise<boolean> }
15
+ type WhenOf<Mod> = { when?(call: Call & { readonly folder: string }, mod: Mod): boolean | Promise<boolean> }
15
16
  export type PermissionRules<Mod> = { deny?: Rule<Mod>[]; ask?: Rule<Mod>[] }
16
17
  export type Decision = 'allow' | 'ask' | 'deny'
17
18
  export type Verdict = { decision: Decision; reason?: string }
@@ -22,12 +23,12 @@ export function stricterVerdict<Result extends Verdict>(first: Result, second: R
22
23
  return strictness.indexOf(second.decision) > strictness.indexOf(first.decision) ? second : first
23
24
  }
24
25
 
25
- export async function decidePermission<Mod>(rules: PermissionRules<Mod>, use: ToolCall, workspace: Workspace, modOf: (call: ToolCall, folder: string) => Mod): Promise<Verdict | undefined> {
26
+ export async function decidePermission<Mod>(rules: PermissionRules<Mod>, use: Call, workspace: Workspace, mod: Mod, within: (run: () => boolean | Promise<boolean>) => Promise<boolean>): Promise<Verdict | undefined> {
26
27
  try {
27
28
  const effects = callEffects(use, workspace)
28
29
  for (const decision of ['deny', 'ask'] as const) {
29
30
  for (const rule of rules[decision] ?? []) {
30
- const outcome = await ruleOutcome(rule, use, effects, workspace, modOf)
31
+ const outcome = await ruleOutcome(rule, use, effects, workspace, mod, within)
31
32
  if (outcome === undefined) continue
32
33
  const reason = [rule.reason, outcome.failure === undefined ? undefined : `Its when check failed: ${outcome.failure}`].filter((part) => part !== undefined).join(' ')
33
34
  return reason === '' ? { decision } : { decision, reason }
@@ -39,7 +40,7 @@ export async function decidePermission<Mod>(rules: PermissionRules<Mod>, use: To
39
40
  }
40
41
  }
41
42
 
42
- async function ruleOutcome<Mod>(rule: Rule<Mod>, use: ToolCall, effects: CallEffects, workspace: Workspace, modOf: (call: ToolCall, folder: string) => Mod): Promise<{ failure: string | undefined } | undefined> {
43
+ async function ruleOutcome<Mod>(rule: Rule<Mod>, use: Call, effects: CallEffects, workspace: Workspace, mod: Mod, within: (run: () => boolean | Promise<boolean>) => Promise<boolean>): Promise<{ failure: string | undefined } | undefined> {
43
44
  const matched = await matchTarget(rule, use, effects, workspace)
44
45
  const calls = matched.length > 0 ? matched : await unparsedMatches(rule, use, effects, workspace)
45
46
  if (calls.length === 0) return undefined
@@ -48,7 +49,8 @@ async function ruleOutcome<Mod>(rule: Rule<Mod>, use: ToolCall, effects: CallEff
48
49
  for (const { call, access, folder } of calls) {
49
50
  const fullCall = access?.contents === undefined ? call : { ...call, ...(await access.contents()) }
50
51
  try {
51
- if (await when(fullCall, modOf(fullCall, folder))) return { failure: undefined }
52
+ const placed = await inFolder(fullCall, folder, workspace)
53
+ if (await within(() => when(placed, mod))) return { failure: undefined }
52
54
  } catch (error) {
53
55
  return { failure: messageOf(error) }
54
56
  }
@@ -56,7 +58,7 @@ async function ruleOutcome<Mod>(rule: Rule<Mod>, use: ToolCall, effects: CallEff
56
58
  return undefined
57
59
  }
58
60
 
59
- async function unparsedMatches(target: Target, use: ToolCall, effects: CallEffects, workspace: Workspace): Promise<MatchedCall[]> {
61
+ async function unparsedMatches(target: Target, use: Call, effects: CallEffects, workspace: Workspace): Promise<MatchedCall[]> {
60
62
  const shell = effects.shell
61
63
  if (shell === undefined || shell.isFullyParsed) return []
62
64
  const { key, patterns } = targetOf(target)
@@ -1,4 +1,4 @@
1
- import type { FileSystem } from '../../utils/paths.js'
1
+ import { ifMissing, type FileSystem } from '../../utils/paths.js'
2
2
  import { basename, dirname, resolve } from '../../vendor.js'
3
3
 
4
4
  export type ProjectScope = { workTreeOf: (path: string) => Promise<string | undefined> }
@@ -33,19 +33,13 @@ export async function findProjectScope(pluginRoot: string, home: string, fs: Fil
33
33
 
34
34
  async function readRepository(workTree: string, fs: FileSystem): Promise<string | undefined> {
35
35
  const gitPath = resolve(workTree, '.git')
36
- const git = await fs.stat(gitPath).catch(() => undefined)
36
+ const git = await fs.stat(gitPath).catch(ifMissing(undefined))
37
37
  if (git === undefined) return undefined
38
38
  if (git.kind === 'dir') return git.realPath ?? gitPath
39
- try {
40
- const gitDirectory = /^gitdir:\s*(.+)$/m.exec(await fs.read(gitPath))?.[1]?.trim()
41
- if (gitDirectory === undefined) throw new Error(`${gitPath} has no gitdir line.`)
42
- const linkedDirectory = resolve(workTree, gitDirectory)
43
- const commonDirectory = await fs.read(resolve(linkedDirectory, 'commondir')).then(
44
- (text) => resolve(linkedDirectory, text.trim()),
45
- () => linkedDirectory,
46
- )
47
- return (await fs.stat(commonDirectory)).realPath ?? commonDirectory
48
- } catch {
49
- return workTree
50
- }
39
+ const gitDirectory = /^gitdir:\s*(.+)$/m.exec(await fs.read(gitPath))?.[1]?.trim()
40
+ if (gitDirectory === undefined) return workTree
41
+ const linkedDirectory = resolve(workTree, gitDirectory)
42
+ const commonDirectory = await fs.read(resolve(linkedDirectory, 'commondir')).then((text) => resolve(linkedDirectory, text.trim()), ifMissing(linkedDirectory))
43
+ const common = await fs.stat(commonDirectory).catch(ifMissing(undefined))
44
+ return common === undefined ? workTree : (common.realPath ?? commonDirectory)
51
45
  }
@@ -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,12 @@ 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
+ report({ id: `program:${executable}`, message: `The ${executable} program stopped`, description: reason })
30
32
  for (const waiter of waiting.splice(0)) waiter.reject(failure)
31
33
  }
32
34
 
@@ -52,12 +54,14 @@ export function program(options: { readonly command: readonly string[]; readonly
52
54
  }
53
55
  state = 'running'
54
56
  exits = 0
57
+ ownAddresses.add(address.socketPath ?? new URL(address.url).host)
55
58
  for (const waiter of waiting.splice(0)) waiter.resolve(address)
56
59
  }
57
60
  } catch (error) {
58
61
  if (error instanceof PermissionRefused) return stop(`${messageOf(error)} Add it, then run /reload-plugins.`)
59
62
  lastError = messageOf(error)
60
63
  }
64
+ if (address !== undefined) ownAddresses.delete(address.socketPath ?? new URL(address.url).host)
61
65
  address = undefined
62
66
  exits += 1
63
67
  const wait = backoffSeconds[exits - 1]
@@ -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 []