@cmodjs/core 4.0.3 → 4.1.1

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 (143) hide show
  1. package/docs/commands.md +44 -9
  2. package/docs/index.md +3 -3
  3. package/docs/install-steps.md +8 -8
  4. package/docs/jobs.md +2 -1
  5. package/docs/mod.md +1 -1
  6. package/docs/options.md +1 -1
  7. package/docs/permissions.md +1 -1
  8. package/docs/session.md +46 -13
  9. package/docs/testing.md +15 -11
  10. package/docs/ui.md +43 -11
  11. package/installer.d.ts +3 -2
  12. package/installer.d.ts.map +1 -1
  13. package/jobs/program.d.ts +1 -0
  14. package/jobs/program.d.ts.map +1 -1
  15. package/jobs/program.js +39 -23
  16. package/mod.d.ts +3 -2
  17. package/mod.d.ts.map +1 -1
  18. package/mod.js +1 -0
  19. package/package.json +1 -1
  20. package/records.d.ts +15 -1
  21. package/records.d.ts.map +1 -1
  22. package/records.js +39 -1
  23. package/register.d.ts.map +1 -1
  24. package/register.js +8 -5
  25. package/runtime/before.d.ts +6 -2
  26. package/runtime/before.d.ts.map +1 -1
  27. package/runtime/before.js +56 -18
  28. package/runtime/buttons.d.ts +10 -0
  29. package/runtime/buttons.d.ts.map +1 -0
  30. package/runtime/buttons.js +12 -0
  31. package/runtime/conversations.d.ts +3 -0
  32. package/runtime/conversations.d.ts.map +1 -1
  33. package/runtime/conversations.js +164 -27
  34. package/runtime/engine.d.ts +2 -0
  35. package/runtime/engine.d.ts.map +1 -1
  36. package/runtime/grants.d.ts +1 -0
  37. package/runtime/grants.d.ts.map +1 -1
  38. package/runtime/grants.js +5 -2
  39. package/runtime/hotkeys.d.ts.map +1 -1
  40. package/runtime/hotkeys.js +7 -4
  41. package/runtime/installer.d.ts +10 -6
  42. package/runtime/installer.d.ts.map +1 -1
  43. package/runtime/installer.js +106 -48
  44. package/runtime/lifecycle.d.ts +2 -2
  45. package/runtime/lifecycle.d.ts.map +1 -1
  46. package/runtime/lifecycle.js +144 -42
  47. package/runtime/pages.d.ts +3 -1
  48. package/runtime/pages.d.ts.map +1 -1
  49. package/runtime/pages.js +48 -14
  50. package/runtime/parts.d.ts +1 -0
  51. package/runtime/parts.d.ts.map +1 -1
  52. package/runtime/parts.js +6 -3
  53. package/runtime/programs.d.ts +1 -0
  54. package/runtime/programs.d.ts.map +1 -1
  55. package/runtime/programs.js +4 -1
  56. package/runtime/sessions.d.ts +10 -3
  57. package/runtime/sessions.d.ts.map +1 -1
  58. package/runtime/sessions.js +104 -59
  59. package/runtime/toaster.d.ts.map +1 -1
  60. package/runtime/toaster.js +23 -9
  61. package/runtime/ui.d.ts +22 -4
  62. package/runtime/ui.d.ts.map +1 -1
  63. package/runtime/ui.js +161 -59
  64. package/runtime/watches.d.ts.map +1 -1
  65. package/runtime/watches.js +18 -4
  66. package/runtime/widgets.d.ts +7 -5
  67. package/runtime/widgets.d.ts.map +1 -1
  68. package/runtime/widgets.js +31 -21
  69. package/session/context/turn/message/attached-file.d.ts +13 -0
  70. package/session/context/turn/message/attached-file.d.ts.map +1 -0
  71. package/session/context/turn/message/attached-file.js +21 -0
  72. package/session/context/turn/message/message.d.ts +3 -2
  73. package/session/context/turn/message/message.d.ts.map +1 -1
  74. package/session/context/turn/message/message.js +20 -6
  75. package/session/conversation.d.ts +15 -3
  76. package/session/conversation.d.ts.map +1 -1
  77. package/session/conversation.js +23 -4
  78. package/session/goal/task.d.ts +5 -0
  79. package/session/goal/task.d.ts.map +1 -1
  80. package/session/session.d.ts +36 -7
  81. package/session/session.d.ts.map +1 -1
  82. package/session/session.js +2 -1
  83. package/src/installer.ts +3 -2
  84. package/src/jobs/program.ts +41 -21
  85. package/src/mod.ts +3 -2
  86. package/src/records.ts +49 -1
  87. package/src/register.ts +11 -8
  88. package/src/runtime/before.ts +61 -22
  89. package/src/runtime/buttons.ts +22 -0
  90. package/src/runtime/conversations.ts +170 -34
  91. package/src/runtime/engine.ts +2 -0
  92. package/src/runtime/grants.ts +6 -2
  93. package/src/runtime/hotkeys.ts +7 -4
  94. package/src/runtime/installer.ts +147 -83
  95. package/src/runtime/lifecycle.ts +149 -48
  96. package/src/runtime/pages.ts +56 -12
  97. package/src/runtime/parts.ts +8 -4
  98. package/src/runtime/programs.ts +5 -1
  99. package/src/runtime/sessions.ts +106 -59
  100. package/src/runtime/toaster.ts +27 -9
  101. package/src/runtime/ui.ts +166 -62
  102. package/src/runtime/watches.ts +20 -5
  103. package/src/runtime/widgets.ts +35 -23
  104. package/src/session/context/turn/message/attached-file.ts +36 -0
  105. package/src/session/context/turn/message/message.ts +20 -7
  106. package/src/session/conversation.ts +31 -6
  107. package/src/session/goal/task.ts +6 -0
  108. package/src/session/session.ts +18 -7
  109. package/src/testing/fake-claude.ts +21 -6
  110. package/src/testing/fake-elements.ts +57 -8
  111. package/src/testing/fake-sessions.ts +72 -24
  112. package/src/testing.ts +114 -33
  113. package/src/ui/areas.ts +1 -0
  114. package/src/ui/components.ts +14 -8
  115. package/src/ui/elements.ts +6 -2
  116. package/src/ui/markdown.ts +3 -3
  117. package/src/ui/panes.ts +2 -2
  118. package/src/ui/styles.ts +6 -2
  119. package/testing/fake-claude.d.ts +3 -0
  120. package/testing/fake-claude.d.ts.map +1 -1
  121. package/testing/fake-claude.js +18 -5
  122. package/testing/fake-elements.d.ts.map +1 -1
  123. package/testing/fake-elements.js +52 -7
  124. package/testing/fake-sessions.d.ts +24 -10
  125. package/testing/fake-sessions.d.ts.map +1 -1
  126. package/testing/fake-sessions.js +66 -20
  127. package/testing.d.ts +8 -12
  128. package/testing.d.ts.map +1 -1
  129. package/testing.js +113 -27
  130. package/ui/areas.d.ts +4 -0
  131. package/ui/areas.d.ts.map +1 -1
  132. package/ui/components.d.ts +1 -0
  133. package/ui/components.d.ts.map +1 -1
  134. package/ui/components.js +11 -7
  135. package/ui/elements.d.ts +3 -0
  136. package/ui/elements.d.ts.map +1 -1
  137. package/ui/elements.js +6 -1
  138. package/ui/markdown.d.ts +2 -2
  139. package/ui/markdown.d.ts.map +1 -1
  140. package/ui/panes.d.ts +2 -1
  141. package/ui/panes.d.ts.map +1 -1
  142. package/ui/styles.d.ts +11 -2
  143. package/ui/styles.d.ts.map +1 -1
package/runtime/before.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { ContextEnd } from '../session/context/context.js';
2
+ import { AttachedFile, attachedPathOf, attachmentTypeOf } from '../session/context/turn/message/attached-file.js';
2
3
  import { InstructionFile, isRulePath } from '../session/context/turn/message/instruction-file.js';
3
4
  import { instructionPathOf, Message, messagesOf, noFiles, rowOfAppend, withTexts } from '../session/context/turn/message/message.js';
4
5
  import { Note, untagged } from '../session/context/turn/message/note.js';
@@ -10,6 +11,7 @@ import { Skill } from '../session/context/turn/message/skill.js';
10
11
  import { ToolCall } from '../session/context/turn/message/tool-call.js';
11
12
  import { ToolResult } from '../session/context/turn/message/tool-result.js';
12
13
  import { TurnEnd } from '../session/context/turn/turn.js';
14
+ import { ownerOfSpawn } from '../session/conversation.js';
13
15
  import { Task, TaskStart } from '../session/goal/task.js';
14
16
  import { sessionEvents } from '../session/session.js';
15
17
  import { cmodPluginName } from './dependencies.js';
@@ -20,6 +22,7 @@ const withNote = (below, note) => {
20
22
  const fields = (below ?? {});
21
23
  return note === undefined ? fields : { ...fields, additionalContext: [...(fields['additionalContext'] ?? []), note] };
22
24
  };
25
+ const withContext = (ran, note) => note === undefined || ran.deny !== undefined ? ran : { ...ran, context: [...(ran.context ?? []), note] };
23
26
  const relayTag = /^(?:<cross-session-message[^>]*>\s*)?<cmod-relay>([\s\S]*)<\/cmod-relay>(?:\s*<\/cross-session-message>[\s\S]*)?$/;
24
27
  export function relayOf(e) {
25
28
  const isFromCmod = e.origin.kind === 'peer' && !('teammate' in e.origin) && e.origin.plugin === cmodPluginName;
@@ -37,6 +40,14 @@ export function installBefore({ claude, router, hooks }) {
37
40
  };
38
41
  const titles = new Map();
39
42
  const spawned = new Map();
43
+ const startedBy = new Map();
44
+ const startingSpawn = (agentId, agentType) => {
45
+ const known = startedBy.get(agentId);
46
+ if (known !== undefined)
47
+ return known;
48
+ const waiting = [...spawned].filter(([call, spawn]) => spawn.agentType === agentType && ![...startedBy.values()].includes(call));
49
+ return waiting.length === 1 ? waiting[0]?.[0] : undefined;
50
+ };
40
51
  const agentFolders = new Map();
41
52
  const toolResultHook = (isFailure) => async (e, next) => {
42
53
  if (!(await wants(ToolResult)))
@@ -99,11 +110,15 @@ export function installBefore({ claude, router, hooks }) {
99
110
  return answer.text === undefined ? next(e) : { text: answer.text };
100
111
  }),
101
112
  router.add('prompt.attachment', async (e, next) => {
102
- if (!(await wants(InstructionFile)) || e.type !== 'nested_memory')
113
+ const attachmentType = attachmentTypeOf(e.type);
114
+ const event = e.type === 'nested_memory' ? InstructionFile : attachmentType === undefined ? undefined : AttachedFile;
115
+ if (event === undefined || !(await wants(event)))
103
116
  return next(e);
104
117
  const where = await place();
105
- const path = instructionPathOf(e.text);
106
- const answer = await asked(InstructionFile, message(where, { id: '', at: now(), type: InstructionFile, path, text: e.text, isRule: isRulePath(path) }, e.agentId));
118
+ const found = attachmentType === undefined
119
+ ? { id: '', at: now(), type: InstructionFile, path: instructionPathOf(e.text), text: e.text, isRule: isRulePath(instructionPathOf(e.text)) }
120
+ : { id: '', at: now(), type: AttachedFile, path: attachedPathOf(e.text), attachmentType, text: e.text };
121
+ const answer = await asked(event, message(where, found, e.agentId));
107
122
  if (answer.refuse !== undefined)
108
123
  return { text: null };
109
124
  return answer.text === undefined ? next(e) : { text: answer.text };
@@ -138,16 +153,20 @@ export function installBefore({ claude, router, hooks }) {
138
153
  if (answer.refuse !== undefined)
139
154
  return { deny: answer.refuse };
140
155
  if (answer.result !== undefined)
141
- return { result: answer.result };
156
+ return withContext({ result: answer.result }, answer.note);
142
157
  if (answer.input === undefined)
143
- return next(e);
158
+ return withContext(await next(e), answer.note);
144
159
  const reserved = Object.fromEntries(Object.entries(e).filter(([key]) => !(key in input)));
145
- return next({ ...reserved, ...answer.input });
160
+ return withContext(await next({ ...reserved, ...answer.input }), answer.note);
146
161
  }),
147
162
  router.add('agent.spawn', async (e, next) => {
148
- spawned.set(e.tool_use_id, e.prompt);
163
+ spawned.set(e.tool_use_id, { prompt: e.prompt, agentType: e.subagentType });
149
164
  const start = async (args) => {
150
165
  const started = await next(args);
166
+ if (started.agentId === undefined)
167
+ spawned.delete(e.tool_use_id);
168
+ else if (spawned.has(e.tool_use_id))
169
+ startedBy.set(started.agentId, e.tool_use_id);
151
170
  const folder = args.isTeammate === true ? undefined : (args.cwd ?? (args.parentAgentId === undefined ? undefined : agentFolders.get(args.parentAgentId)));
152
171
  if (started.agentId !== undefined && folder !== undefined)
153
172
  agentFolders.set(started.agentId, resolve(await claude.session.cwd(), folder));
@@ -156,20 +175,36 @@ export function installBefore({ claude, router, hooks }) {
156
175
  if (!(await wants(TaskStart)))
157
176
  return start(e);
158
177
  const where = await place();
159
- const task = { id: '', title: e.description, agentType: e.subagentType, model: e.model ?? e.parentModel, openedBy: e.tool_use_id, openedAt: now(), ...(e.parentAgentId === undefined ? {} : { parent: e.parentAgentId }) };
178
+ const task = { id: '', title: e.description, ...ownerOfSpawn(e.subagentType, where.session), model: e.model ?? e.parentModel, openedBy: e.tool_use_id, openedAt: now(), ...(e.parentAgentId === undefined ? {} : { parent: e.parentAgentId }) };
160
179
  const answer = await asked(TaskStart, { type: TaskStart, task: { ...task, context: where.context } });
161
180
  if (answer.refuse !== undefined) {
162
181
  spawned.delete(e.tool_use_id);
163
182
  return { deny: answer.refuse };
164
183
  }
165
- return start(answer.owner === undefined ? e : { ...e, model: answer.owner.model });
184
+ if (e.workflow !== undefined)
185
+ return start(e);
186
+ const isFork = task.forkedFrom !== undefined;
187
+ const { agentType, model, cwd } = (isFork ? undefined : answer.owner) ?? {};
188
+ const isAsync = isFork ? undefined : answer.isAsync;
189
+ const answered = {
190
+ ...e,
191
+ ...(answer.prompt === undefined ? {} : { prompt: answer.prompt }),
192
+ ...(answer.title === undefined ? {} : { description: answer.title }),
193
+ ...(agentType === undefined ? {} : { subagentType: agentType }),
194
+ ...(model === undefined ? {} : { model }),
195
+ ...(cwd === undefined ? {} : { cwd }),
196
+ ...(isAsync === undefined ? {} : { background: isAsync }),
197
+ };
198
+ spawned.set(e.tool_use_id, { prompt: answered.prompt, agentType: answered.subagentType });
199
+ return start(answered);
166
200
  }),
167
201
  router.add('classic.SubagentStart', async (e, next) => {
168
202
  const where = await place();
169
- const openedBy = where.tasks[e.agent_id]?.openedBy;
170
- const prompt = openedBy === undefined ? undefined : spawned.get(openedBy);
203
+ const openedBy = where.tasks[e.agent_id]?.openedBy ?? startingSpawn(e.agent_id, e.agent_type);
204
+ const prompt = openedBy === undefined ? undefined : spawned.get(openedBy)?.prompt;
171
205
  if (openedBy !== undefined)
172
206
  spawned.delete(openedBy);
207
+ startedBy.delete(e.agent_id);
173
208
  if (!(await wants(Prompt)))
174
209
  return next(e);
175
210
  const open = where.taskTurns[e.agent_id]?.prompt;
@@ -192,7 +227,7 @@ export function installBefore({ claude, router, hooks }) {
192
227
  return stopped(await next(e));
193
228
  const answer = await asked(TurnEnd, { type: TurnEnd, turn: { ...turn, closedAt: now() } });
194
229
  const below = await next(e);
195
- return stopped(answer.refuse === undefined ? below : { ...below, block: answer.refuse });
230
+ return stopped(answer.refuse === undefined ? withNote(below, answer.note) : { ...below, block: answer.refuse });
196
231
  }),
197
232
  router.add('classic.Stop', async (e, next) => {
198
233
  if (!(await wants(TurnEnd)))
@@ -202,7 +237,7 @@ export function installBefore({ claude, router, hooks }) {
202
237
  return next(e);
203
238
  const answer = await asked(TurnEnd, { type: TurnEnd, turn: { ...where.turn, closedAt: now() } });
204
239
  const below = await next(e);
205
- return answer.refuse === undefined ? below : { ...below, block: answer.refuse };
240
+ return answer.refuse === undefined ? withNote(below, answer.note) : { ...below, block: answer.refuse };
206
241
  }),
207
242
  router.add('session.compact', async (e, next) => {
208
243
  if (!(await wants(ContextEnd)) || e.trigger === 'precompute')
@@ -227,8 +262,10 @@ export function withAnswer(change, answer) {
227
262
  return { ...change, message: { ...change.message, text: answer.text } };
228
263
  if (change.type === Message && answer.input !== undefined && change.message.type === ToolCall)
229
264
  return { ...change, message: { ...change.message, input: answer.input } };
230
- if (change.type === TaskStart && answer.owner !== undefined)
231
- return { ...change, task: { ...change.task, model: answer.owner.model } };
265
+ if (change.type === TaskStart) {
266
+ const { agentType, model } = answer.owner ?? {};
267
+ return { ...change, task: { ...change.task, ...(answer.title === undefined ? {} : { title: answer.title }), ...(agentType === undefined ? {} : { agentType }), ...(model === undefined ? {} : { model }) } };
268
+ }
232
269
  return change;
233
270
  }
234
271
  export const methodOf = (prefix, event) => `${prefix}${Object.entries(sessionEvents).find(([, each]) => each === event)?.[0] ?? event}`;
@@ -237,12 +274,13 @@ export const beforeAnswerKeys = {
237
274
  note: ['text'],
238
275
  skill: ['text'],
239
276
  instructionFile: ['text', 'refuse'],
277
+ attachedFile: ['text', 'refuse'],
240
278
  reply: ['text'],
241
- toolCall: ['input', 'result', 'refuse'],
279
+ toolCall: ['input', 'result', 'refuse', 'note'],
242
280
  toolResult: ['output', 'isError', 'note'],
243
281
  notice: ['text'],
244
- taskStart: ['owner', 'refuse'],
245
- turnEnd: ['refuse'],
282
+ taskStart: ['prompt', 'title', 'owner', 'isAsync', 'refuse'],
283
+ turnEnd: ['refuse', 'note'],
246
284
  contextEnd: ['refuse'],
247
285
  };
248
286
  export function foldBefore(event, change, askers, report) {
@@ -0,0 +1,10 @@
1
+ import type { RenderElement } from 'claude-code';
2
+ export type FilledButton = {
3
+ readonly key: string;
4
+ readonly label: string;
5
+ readonly hotkey?: string;
6
+ readonly variant?: 'primary' | 'secondary';
7
+ readonly onPress: () => void;
8
+ };
9
+ export declare function filledButton({ key, label, hotkey, variant, onPress }: FilledButton): RenderElement;
10
+ //# sourceMappingURL=buttons.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"buttons.d.ts","sourceRoot":"","sources":["../src/runtime/buttons.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;AAGhD,MAAM,MAAM,YAAY,GAAG;IACzB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;IACpB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,OAAO,CAAC,EAAE,SAAS,GAAG,WAAW,CAAA;IAC1C,QAAQ,CAAC,OAAO,EAAE,MAAM,IAAI,CAAA;CAC7B,CAAA;AAED,wBAAgB,YAAY,CAAC,EAAE,GAAG,EAAE,KAAK,EAAE,MAAM,EAAE,OAAqB,EAAE,OAAO,EAAE,EAAE,YAAY,GAAG,aAAa,CAUhH"}
@@ -0,0 +1,12 @@
1
+ import { Button, Text } from '../ui/elements.js';
2
+ export function filledButton({ key, label, hotkey, variant = 'secondary', onPress }) {
3
+ const isPrimary = variant === 'primary';
4
+ return Button({
5
+ plain: true,
6
+ key,
7
+ label,
8
+ ...(hotkey === undefined ? {} : { hotkey }),
9
+ onPress,
10
+ children: Text({ backgroundColor: isPrimary ? 'claude' : 'userMessageBackground', color: isPrimary ? 'inverseText' : 'text', bold: isPrimary, children: ` ${label} ` }),
11
+ });
12
+ }
@@ -2,12 +2,15 @@ import type { Args, EventResult, Frozen } from 'claude-code';
2
2
  import { type TurnData } from '../session/conversation.js';
3
3
  import type { Claude } from './claude.js';
4
4
  import type { Router } from './router.js';
5
+ import { type SessionColor } from './sessions.js';
5
6
  export type Conversations = {
6
7
  answer(e: Frozen<Args<'cmod.call'>> & {
7
8
  readonly caller?: string;
8
9
  }): Promise<EventResult<'cmod.call'> | undefined>;
9
10
  moved(): void;
10
11
  turnClosedBy(rowId: string): TurnData | undefined;
12
+ color(): SessionColor;
13
+ onColor(listener: (told: SessionColor) => void): void;
11
14
  };
12
15
  export declare function serveConversations({ claude, router, store, project, report, clear, }: {
13
16
  readonly claude: Claude;
@@ -1 +1 @@
1
- {"version":3,"file":"conversations.d.ts","sourceRoot":"","sources":["../src/runtime/conversations.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,WAAW,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AAK5D,OAAO,EAA2J,KAAK,QAAQ,EAAmB,MAAM,4BAA4B,CAAA;AAQpO,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AAGzC,OAAO,KAAK,EAAc,MAAM,EAAE,MAAM,aAAa,CAAA;AAkCrD,MAAM,MAAM,aAAa,GAAG;IAC1B,MAAM,CAAC,CAAC,EAAE,MAAM,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC,GAAG;QAAE,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,WAAW,CAAC,WAAW,CAAC,GAAG,SAAS,CAAC,CAAA;IAClH,KAAK,IAAI,IAAI,CAAA;IACb,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,QAAQ,GAAG,SAAS,CAAA;CAClD,CAAA;AAMD,wBAAgB,kBAAkB,CAAC,EACjC,MAAM,EACN,MAAM,EACN,KAAK,EACL,OAAO,EACP,MAAM,EACN,KAAK,GACN,EAAE;IACD,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,OAAO,EAAE,MAAM,MAAM,CAAA;IAC9B,QAAQ,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,KAAK,IAAI,CAAA;IACtE,QAAQ,CAAC,KAAK,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAA;CACrC,GAAG,aAAa,CA2lBhB"}
1
+ {"version":3,"file":"conversations.d.ts","sourceRoot":"","sources":["../src/runtime/conversations.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,WAAW,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AAK5D,OAAO,EAA0L,KAAK,QAAQ,EAAmB,MAAM,4BAA4B,CAAA;AAUnQ,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AAGzC,OAAO,KAAK,EAAc,MAAM,EAAE,MAAM,aAAa,CAAA;AAErD,OAAO,EAAqJ,KAAK,YAAY,EAAE,MAAM,eAAe,CAAA;AAuCpM,MAAM,MAAM,aAAa,GAAG;IAC1B,MAAM,CAAC,CAAC,EAAE,MAAM,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC,GAAG;QAAE,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,WAAW,CAAC,WAAW,CAAC,GAAG,SAAS,CAAC,CAAA;IAClH,KAAK,IAAI,IAAI,CAAA;IACb,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,QAAQ,GAAG,SAAS,CAAA;IACjD,KAAK,IAAI,YAAY,CAAA;IACrB,OAAO,CAAC,QAAQ,EAAE,CAAC,IAAI,EAAE,YAAY,KAAK,IAAI,GAAG,IAAI,CAAA;CACtD,CAAA;AAMD,wBAAgB,kBAAkB,CAAC,EACjC,MAAM,EACN,MAAM,EACN,KAAK,EACL,OAAO,EACP,MAAM,EACN,KAAK,GACN,EAAE;IACD,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,OAAO,EAAE,MAAM,MAAM,CAAA;IAC9B,QAAQ,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,KAAK,IAAI,CAAA;IACtE,QAAQ,CAAC,KAAK,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAA;CACrC,GAAG,aAAa,CAwtBhB"}
@@ -2,19 +2,22 @@ import { consentPath, coreVersion, dataFolder, grantFix, isObject, parseConsent
2
2
  import { Message, rowOfAppend } from '../session/context/turn/message/message.js';
3
3
  import { modNote, Note } from '../session/context/turn/message/note.js';
4
4
  import { Person } from '../session/context/turn/message/sender.js';
5
- import { createConversation, emptyConversation } from '../session/conversation.js';
5
+ import { createConversation, emptyConversation, ownerOfSpawn, tasksWithAgents } from '../session/conversation.js';
6
6
  import { Session, UsageChange } from '../session/session.js';
7
7
  import { GoalChange } from '../session/goal/goal.js';
8
8
  import { TurnEnd, TurnStart } from '../session/context/turn/turn.js';
9
+ import { TaskStart } from '../session/goal/task.js';
9
10
  import { receivedMatcher } from '../utils/matcher.js';
11
+ import { resolve } from '../vendor.js';
10
12
  import { ifMissing } from '../utils/paths.js';
11
13
  import { formatExit, messageOf } from '../utils/text.js';
12
14
  import { beforeAnswerKeys, foldBefore, installBefore, relayOf } from './before.js';
13
15
  import { cmodPluginName } from './dependencies.js';
14
- import { readLines, runCmod } from './programs.js';
16
+ import { readLines, runCmod, spawnCmod } from './programs.js';
15
17
  import { PassedTimeout } from './deadline.js';
16
- import { actPermission, conversationMethods, everySession, isAct, isHeardBy, isInScope, resultOf, spawnTask, usageOf } from './sessions.js';
17
- const relayKinds = new Set(['act', 'before', 'answer', 'watch', 'goal']);
18
+ import { actPermission, conversationMethods, everySession, isAct, isHeardBy, isInScope, pickedColor, resultOf, spawnTask, usageOf } from './sessions.js';
19
+ const relayKinds = new Set(['act', 'before', 'answer', 'watch', 'goal', 'taskEnd']);
20
+ const taskVariable = 'CMOD_TASK';
18
21
  const isRelay = (value) => isObject(value) && relayKinds.has(value['kind']);
19
22
  const bufferedTaskRows = 200;
20
23
  const beforeDeadlineMs = 5000;
@@ -25,8 +28,12 @@ const keptMessages = 200;
25
28
  export function serveConversations({ claude, router, store, project, report, clear, }) {
26
29
  const folder = dataFolder(store, cmodPluginName);
27
30
  const goals = `${folder}/goals`;
31
+ const backgroundTasks = `${folder}/tasks`;
28
32
  const watches = `${folder}/before`;
33
+ const listeners = `${folder}/listeners`;
29
34
  const heard = new Map();
35
+ const subscribed = new Map();
36
+ let savingListeners = Promise.resolve();
30
37
  const asked = new Map();
31
38
  const ownWatches = new Map();
32
39
  let watchers = [];
@@ -57,6 +64,12 @@ export function serveConversations({ claude, router, store, project, report, cle
57
64
  router.add('classic.Stop', async (e, next) => endTurn({ reason: 'answer', ...(e.last_assistant_message === undefined ? {} : { answer: e.last_assistant_message }) })(await next(e)));
58
65
  router.add('classic.StopFailure', async (e, next) => endTurn({ reason: 'error', failure: { type: String(e.error), text: e.error_details ?? e.last_assistant_message ?? String(e.error) } })(await next(e)));
59
66
  router.add('classic.SubagentStop', async (e, next) => endTurn({ reason: 'answer', ...(e.last_assistant_message === undefined ? {} : { answer: e.last_assistant_message }) }, e.agent_id)(await next(e)));
67
+ router.add('classic.Stop', async (e, next) => {
68
+ const below = await next(e);
69
+ if (below.block === undefined)
70
+ void answerTask().catch((error) => report('task', 'Could not record that this session answered its task', error));
71
+ return below;
72
+ });
60
73
  const json = async (args) => JSON.parse(await runCmod(claude, [...args, '--json', '--project', project()], await claude.session.id()));
61
74
  const isGranted = async (caller, item) => {
62
75
  if (caller === cmodPluginName)
@@ -69,6 +82,27 @@ export function serveConversations({ claude, router, store, project, report, cle
69
82
  };
70
83
  const ended = (id) => new Error(`session ${id} has ended`);
71
84
  const isRunning = async (id) => (await json(['history', 'sessions', '--running', '--all'])).some((each) => each.id === id);
85
+ let color = { session: '' };
86
+ let onColor = () => undefined;
87
+ const recolor = (id, picked) => {
88
+ if (session !== id)
89
+ return;
90
+ color = { session: id, ...(picked === undefined ? {} : { color: picked }) };
91
+ onColor(color);
92
+ };
93
+ const readColor = (id) => void json(['history', 'sessions', '--running', '--all'])
94
+ .then((running) => recolor(id, running.find((each) => each.id === id)?.color))
95
+ .catch((error) => report('color', `Could not read the color of session ${id}`, error));
96
+ router.add('command.run', async (e, next) => {
97
+ const answer = await next(e);
98
+ if (e.command !== 'color' || session === '')
99
+ return answer;
100
+ if (e.args.trim() === '')
101
+ readColor(session);
102
+ else
103
+ recolor(session, pickedColor(e.args));
104
+ return answer;
105
+ });
72
106
  let lastTurn;
73
107
  const closings = new Map();
74
108
  const dispatch = (to, changes, isInProject = true) => {
@@ -103,7 +137,7 @@ export function serveConversations({ claude, router, store, project, report, cle
103
137
  follower = undefined;
104
138
  if (args.length === 0)
105
139
  return;
106
- const stream = claude.process.spawn({ argv: ['cmod', 'history', 'follow', ...args, '--json', '--project', project()], env: { CLAUDE_CODE_SESSION_ID: session } });
140
+ const stream = spawnCmod(claude, ['history', 'follow', ...args, '--json', '--project', project()], session);
107
141
  const started = { sessions: wanted, stop: () => void stream.return(undefined) };
108
142
  follower = started;
109
143
  void readLines(stream, (line) => {
@@ -194,14 +228,42 @@ export function serveConversations({ claude, router, store, project, report, cle
194
228
  const told = new Set([...running, ...targets].filter((id) => id !== everySession && id !== session));
195
229
  await Promise.all([...told].map((id) => post(id, { kind: 'watch' }).catch((error) => claude.ui.log(`cmod: session ${id} did not hear of a before listener: ${messageOf(error)}`, { to: 'debug' }))));
196
230
  };
231
+ const hear = (caller, id, list) => {
232
+ const sessions = heard.get(caller) ?? new Map();
233
+ sessions.set(id, list.map(({ event, matcher }) => ({ event, matcher: matcher === undefined ? undefined : receivedMatcher(matcher) })));
234
+ heard.set(caller, sessions);
235
+ const lists = subscribed.get(caller) ?? new Map();
236
+ lists.set(id, list);
237
+ subscribed.set(caller, lists);
238
+ };
239
+ const saveListeners = (isEnding = false) => {
240
+ const at = session;
241
+ const kept = isEnding ? {} : Object.fromEntries([...subscribed].map(([caller, lists]) => [caller, Object.fromEntries(lists)]));
242
+ const next = savingListeners.then(() => (at === '' ? undefined : claude.fs.write(`${listeners}/${at}.json`, JSON.stringify(kept))));
243
+ savingListeners = next.catch((error) => claude.ui.log(`cmod: saving the mods' listeners failed: ${messageOf(error)}`, { to: 'debug' }));
244
+ return next;
245
+ };
246
+ const restoreListeners = async () => {
247
+ const kept = JSON.parse(await claude.fs.read(`${listeners}/${session}.json`).catch(ifMissing('{}')));
248
+ for (const [caller, lists] of Object.entries(kept))
249
+ if (!subscribed.has(caller))
250
+ for (const [id, list] of Object.entries(lists))
251
+ hear(caller, id, list);
252
+ };
197
253
  const readWatchers = async () => {
198
254
  const files = await claude.fs.list(watches).catch(ifMissing([]));
199
255
  const found = [];
200
256
  for (const file of files) {
201
257
  const from = file.name.replace(/\.json$/, '');
202
- if (from === session || !file.name.endsWith('.json'))
258
+ if (!file.name.endsWith('.json'))
203
259
  continue;
204
260
  const list = JSON.parse(await claude.fs.read(`${watches}/${file.name}`).catch(ifMissing('[]')));
261
+ if (from === session) {
262
+ for (const watch of list)
263
+ if (!ownWatches.has(`${watch.mod}\n${watch.target}`))
264
+ ownWatches.set(`${watch.mod}\n${watch.target}`, watch);
265
+ continue;
266
+ }
205
267
  found.push(...list.filter((watch) => watch.target === session || (watch.target === everySession && watch.project === project())).map((watch) => ({ ...watch, from })));
206
268
  }
207
269
  watchers = found;
@@ -249,6 +311,19 @@ export function serveConversations({ claude, router, store, project, report, cle
249
311
  return undefined;
250
312
  }
251
313
  };
314
+ const wants = async (event) => {
315
+ await Promise.all([starting, watching]);
316
+ return deciders(event).length > 0;
317
+ };
318
+ const decide = () => {
319
+ let endsAt;
320
+ return (event, change) => foldBefore(event, change, deciders(event).map((watcher) => async (latest) => {
321
+ if (watcher.from === session)
322
+ return askHere(watcher.mod, event, latest);
323
+ endsAt ??= claude.clock.now().then((now) => now + awayFoldMs);
324
+ return askAway(watcher, event, latest, await endsAt);
325
+ }), (error) => report(`before:${event}`, `Passed the ${event} on without a mod's answer`, error));
326
+ };
252
327
  installBefore({
253
328
  claude,
254
329
  router,
@@ -257,19 +332,8 @@ export function serveConversations({ claude, router, store, project, report, cle
257
332
  await starting;
258
333
  return place();
259
334
  },
260
- wants: async (event) => {
261
- await Promise.all([starting, watching]);
262
- return deciders(event).length > 0;
263
- },
264
- fold: () => {
265
- let endsAt;
266
- return (event, change) => foldBefore(event, change, deciders(event).map((watcher) => async (latest) => {
267
- if (watcher.from === session)
268
- return askHere(watcher.mod, event, latest);
269
- endsAt ??= claude.clock.now().then((now) => now + awayFoldMs);
270
- return askAway(watcher, event, latest, await endsAt);
271
- }), (error) => report(`before:${event}`, `Passed the ${event} on without a mod's answer`, error));
272
- },
335
+ wants,
336
+ fold: decide,
273
337
  },
274
338
  });
275
339
  const start = (id, source) => {
@@ -281,15 +345,23 @@ export function serveConversations({ claude, router, store, project, report, cle
281
345
  liveIds.clear();
282
346
  waitingRows.clear();
283
347
  conversation = createConversation(emptyConversation(id));
348
+ const isNew = source === 'startup' || source === 'clear';
349
+ if (!isNew)
350
+ readColor(id);
284
351
  starting =
285
- source === 'startup' || source === 'clear'
352
+ isNew
286
353
  ? Promise.resolve(dispatch(id, conversation.startContext(new Date().toISOString())))
287
354
  : json(['history', 'state', id]).then((state) => {
288
355
  if (session === id)
289
356
  conversation = createConversation({ ...emptyConversation(id), ...state });
290
357
  }, (error) => report('state', `Starts session ${id} without its earlier turns`, error));
291
- follow();
358
+ const restored = restoreListeners()
359
+ .catch((error) => report('listeners', "Could not read back the mods' listeners of this session", error))
360
+ .then(follow);
361
+ starting = Promise.all([starting, restored]).then(() => undefined);
292
362
  reloadWatchers();
363
+ if (subscribed.size > 0)
364
+ void Promise.all([previous === '' ? undefined : claude.fs.write(`${listeners}/${previous}.json`, '{}'), saveListeners()]).catch((error) => report('follow', 'Kept telling the mods about the session from before', error));
293
365
  if (ownWatches.size === 0)
294
366
  return;
295
367
  void Promise.all([previous === '' ? undefined : claude.fs.write(`${watches}/${previous}.json`, '[]'), publishWatches()]).catch((error) => report('before', 'Kept its before listeners on the session from before', error));
@@ -322,6 +394,8 @@ export function serveConversations({ claude, router, store, project, report, cle
322
394
  router.add('classic.SessionEnd', async (e, next) => {
323
395
  if (ownWatches.size > 0)
324
396
  await publishWatches(true).catch((error) => report('before', 'Left its before listeners registered', error));
397
+ if (subscribed.size > 0)
398
+ await saveListeners(true).catch((error) => report('follow', 'Left its listeners registered', error));
325
399
  return next(e);
326
400
  });
327
401
  const recordAppend = async (e, next) => {
@@ -354,8 +428,8 @@ export function serveConversations({ claude, router, store, project, report, cle
354
428
  if (spawned.deny !== undefined || spawned.agentId === undefined)
355
429
  return spawned;
356
430
  const id = spawned.agentId;
357
- const opened = { id, title: e.description, agentType: e.subagentType, model: spawned.model, openedBy: e.tool_use_id, openedAt: new Date().toISOString(), ...(e.parentAgentId === undefined ? {} : { parent: e.parentAgentId }) };
358
431
  await starting;
432
+ const opened = { id, title: e.description, ...ownerOfSpawn(e.subagentType, session), model: spawned.model, openedBy: e.tool_use_id, openedAt: new Date().toISOString(), ...(e.parentAgentId === undefined ? {} : { parent: e.parentAgentId }) };
359
433
  dispatch(session, conversation.startTask(opened));
360
434
  const waiting = waitingRows.get(id) ?? [];
361
435
  waitingRows.delete(id);
@@ -394,6 +468,11 @@ export function serveConversations({ claude, router, store, project, report, cle
394
468
  changeGoal(relay.task, relay.text);
395
469
  return;
396
470
  }
471
+ if (relay.kind === 'taskEnd') {
472
+ await starting;
473
+ dispatch(session, conversation.endTask(relay.task, relay.task.closedAt ?? new Date().toISOString()));
474
+ return;
475
+ }
397
476
  const answered = async (run) => {
398
477
  try {
399
478
  await post(relay.from, { kind: 'answer', id: relay.id, value: (await run()) ?? null });
@@ -435,6 +514,12 @@ export function serveConversations({ claude, router, store, project, report, cle
435
514
  }
436
515
  if (action === 'startTask')
437
516
  return spawnTask(claude, input);
517
+ if (action === 'fork')
518
+ return spawnTask(claude, input, session);
519
+ if (action === 'startBackgroundTask')
520
+ return startBackground(input);
521
+ if (action === 'clone')
522
+ return startBackground(input, session);
438
523
  const { name, input: toolInput } = input;
439
524
  return resultOf(place(), { name, input: toolInput, cwd: await claude.session.cwd() }, await claude.tool.call({ ...toolInput, tool: name }));
440
525
  };
@@ -465,6 +550,51 @@ export function serveConversations({ claude, router, store, project, report, cle
465
550
  throw ended(task);
466
551
  return null;
467
552
  };
553
+ const startBackground = async (task, forkedFrom) => {
554
+ if (task.title.trim() === '')
555
+ throw new Error('a task needs a title.');
556
+ if (task.name !== undefined)
557
+ throw new Error('a background task takes no name.');
558
+ const fresh = forkedFrom === undefined ? task : {};
559
+ const asked = { id: '', title: task.title, ...(forkedFrom === undefined ? {} : { forkedFrom }), ...(fresh.agentType === undefined ? {} : { agentType: fresh.agentType }), ...(fresh.model === undefined ? {} : { model: fresh.model }), openedBy: '', openedAt: new Date().toISOString(), isBackground: true };
560
+ const answer = (await wants(TaskStart)) ? await decide()(TaskStart, { type: TaskStart, task: { ...asked, context: conversation.state.context } }) : undefined;
561
+ if (answer?.refuse !== undefined)
562
+ throw new Error(`a beforeTaskStart listener refused the task: ${answer.refuse}`);
563
+ const owner = forkedFrom === undefined ? (answer?.owner ?? {}) : {};
564
+ const folder = owner.cwd ?? fresh.cwd;
565
+ const here = await claude.session.cwd();
566
+ const args = [
567
+ 'task',
568
+ 'start',
569
+ '--title',
570
+ answer?.title ?? task.title,
571
+ ...(forkedFrom === undefined ? [] : ['--clone']),
572
+ ...textArg('--agent', owner.agentType ?? fresh.agentType),
573
+ ...textArg('--model', owner.model ?? fresh.model),
574
+ '--cwd',
575
+ folder === undefined ? here : resolve(here, folder),
576
+ '--json',
577
+ '--',
578
+ answer?.prompt ?? task.prompt,
579
+ ];
580
+ const started = JSON.parse(await runCmod(claude, args, session));
581
+ dispatch(session, conversation.startTask(started));
582
+ return started;
583
+ };
584
+ const answerTask = async () => {
585
+ const { env } = await claude.settings.read();
586
+ const asked = isObject(env) ? env[taskVariable] : undefined;
587
+ if (typeof asked !== 'string')
588
+ return;
589
+ const answered = `cmod:task-answered:${session}`;
590
+ if ((await claude.store.get(answered)) === true)
591
+ return;
592
+ await claude.store.set(answered, true);
593
+ const { session: from, ...task } = JSON.parse(asked);
594
+ const ended = { ...task, id: session, closedAt: new Date().toISOString() };
595
+ await claude.fs.write(`${backgroundTasks}/${from}/${session}.json`, JSON.stringify(ended));
596
+ await post(from, { kind: 'taskEnd', task: ended }).catch((error) => claude.ui.log(`cmod: session ${from} did not hear that its task ${session} answered: ${messageOf(error)}`, { to: 'debug' }));
597
+ };
468
598
  const actElsewhere = async (caller, target, action, input, task) => {
469
599
  await checkAct(caller, action, input);
470
600
  const isLive = await isRunning(target);
@@ -493,12 +623,14 @@ export function serveConversations({ claude, router, store, project, report, cle
493
623
  const liveTasks = () => Object.values(conversation.state.tasks).map((task) => ({
494
624
  id: task.id,
495
625
  title: task.title,
496
- agentType: task.agentType,
626
+ ...(task.agentType === undefined ? {} : { agentType: task.agentType }),
497
627
  ...(task.model === undefined ? {} : { model: task.model }),
498
628
  openedBy: task.openedBy,
499
629
  openedAt: task.openedAt,
500
630
  ...(task.closedAt === undefined ? {} : { closedAt: task.closedAt }),
501
631
  ...(task.parent === undefined ? {} : { parent: task.parent }),
632
+ ...(task.forkedFrom === undefined ? {} : { forkedFrom: task.forkedFrom }),
633
+ ...(task.isBackground === true ? { isBackground: true } : {}),
502
634
  }));
503
635
  const liveContext = (task) => {
504
636
  if (typeof task === 'string') {
@@ -565,7 +697,9 @@ export function serveConversations({ claude, router, store, project, report, cle
565
697
  return saved;
566
698
  await starting;
567
699
  const kept = liveTasks().filter((task) => isNested || task.parent === request['task']);
568
- return merged(saved, kept, (task) => task.id);
700
+ const running = await claude.agent.list().catch((error) => (claude.ui.log(`cmod: the tasks show no status, because Claude Code listed no agents: ${messageOf(error)}`, { to: 'debug' }), []));
701
+ const background = saved.flatMap(({ id, status }) => (status === undefined ? [] : [{ id, status }]));
702
+ return tasksWithAgents(merged(saved, kept, (task) => task.id), [...running, ...background]);
569
703
  },
570
704
  async [conversationMethods.goal](request) {
571
705
  const id = String(request['session']);
@@ -597,10 +731,9 @@ export function serveConversations({ claude, router, store, project, report, cle
597
731
  throw new Error('cmod hears no caller for this subscription.');
598
732
  const id = String(request['session']);
599
733
  const list = Array.isArray(request['subscriptions']) ? request['subscriptions'] : [];
600
- const sessions = heard.get(caller) ?? new Map();
601
- sessions.set(id, list.map(({ event, matcher }) => ({ event, matcher: matcher === undefined ? undefined : receivedMatcher(matcher) })));
602
- heard.set(caller, sessions);
734
+ hear(caller, id, list);
603
735
  follow();
736
+ await saveListeners();
604
737
  return null;
605
738
  },
606
739
  async [conversationMethods.watchBefore](request, caller) {
@@ -627,6 +760,10 @@ export function serveConversations({ claude, router, store, project, report, cle
627
760
  };
628
761
  return {
629
762
  turnClosedBy: (rowId) => closings.get(rowId),
763
+ color: () => color,
764
+ onColor(listener) {
765
+ onColor = listener;
766
+ },
630
767
  moved() {
631
768
  for (const [key, watch] of ownWatches)
632
769
  ownWatches.set(key, { ...watch, project: project() });
@@ -1,3 +1,4 @@
1
+ import type { HookStream, ProcessSpawnChunk, ProcessSpawnRequest, ProcessSpawnResult } from 'claude-code';
1
2
  import type { Claude } from './claude.js';
2
3
  import type { Pages } from './pages.js';
3
4
  import type { Router } from './router.js';
@@ -11,6 +12,7 @@ export type Engine = {
11
12
  readonly report: (failure: Failure) => void;
12
13
  readonly hasPermissionCheck: () => boolean;
13
14
  readonly answerInput: <Answer>(run: () => Promise<Answer>, passOn: () => Promise<unknown>) => Promise<Answer>;
15
+ readonly spawnCmod: (args: readonly string[], program: ProcessSpawnRequest) => Promise<HookStream<ProcessSpawnChunk, ProcessSpawnResult>>;
14
16
  readonly widgets?: Widgets;
15
17
  readonly pages?: Pages;
16
18
  };
@@ -1 +1 @@
1
- {"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../src/runtime/engine.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AACzC,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,YAAY,CAAA;AACvC,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AACzC,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAA;AAC3C,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAA;AAChD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAA;AAE3C,MAAM,MAAM,MAAM,GAAG;IACnB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,SAAS,EAAE,SAAS,CAAA;IAC7B,QAAQ,CAAC,MAAM,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,IAAI,CAAA;IAC3C,QAAQ,CAAC,kBAAkB,EAAE,MAAM,OAAO,CAAA;IAC1C,QAAQ,CAAC,WAAW,EAAE,CAAC,MAAM,EAAE,GAAG,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,EAAE,MAAM,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,KAAK,OAAO,CAAC,MAAM,CAAC,CAAA;IAC7G,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,CAAA;IAC1B,QAAQ,CAAC,KAAK,CAAC,EAAE,KAAK,CAAA;CACvB,CAAA;AAID,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI,CAE5D;AAED,wBAAgB,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAI5C"}
1
+ {"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../src/runtime/engine.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,iBAAiB,EAAE,mBAAmB,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAA;AACzG,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AACzC,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,YAAY,CAAA;AACvC,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AACzC,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAA;AAC3C,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAA;AAChD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAA;AAE3C,MAAM,MAAM,MAAM,GAAG;IACnB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,SAAS,EAAE,SAAS,CAAA;IAC7B,QAAQ,CAAC,MAAM,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,IAAI,CAAA;IAC3C,QAAQ,CAAC,kBAAkB,EAAE,MAAM,OAAO,CAAA;IAC1C,QAAQ,CAAC,WAAW,EAAE,CAAC,MAAM,EAAE,GAAG,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,EAAE,MAAM,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,KAAK,OAAO,CAAC,MAAM,CAAC,CAAA;IAC7G,QAAQ,CAAC,SAAS,EAAE,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,EAAE,OAAO,EAAE,mBAAmB,KAAK,OAAO,CAAC,UAAU,CAAC,iBAAiB,EAAE,kBAAkB,CAAC,CAAC,CAAA;IACzI,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,CAAA;IAC1B,QAAQ,CAAC,KAAK,CAAC,EAAE,KAAK,CAAA;CACvB,CAAA;AAID,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI,CAE5D;AAED,wBAAgB,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAI5C"}
@@ -15,6 +15,7 @@ export declare class PermissionRefused extends Error {
15
15
  }
16
16
  export declare function programAddresses(mod: object): Set<string>;
17
17
  export declare function checkGrant(grants: Grants, call: string, item: string | undefined): Promise<void>;
18
+ export declare function checkRun(grants: Grants, call: 'process.run' | 'process.spawn', argv: readonly string[]): Promise<void>;
18
19
  export declare function checkWrite(grants: Grants, call: string, path: string): Promise<void>;
19
20
  export declare function checkSettings(grants: Grants, source: 'user' | 'project' | 'local', values: Readonly<Record<string, unknown>>): Promise<void>;
20
21
  export declare function checkingGrants(claude: Claude, grants: Grants): Claude;
@@ -1 +1 @@
1
- {"version":3,"file":"grants.d.ts","sourceRoot":"","sources":["../src/runtime/grants.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AAEzC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAA;AAG7C,MAAM,MAAM,MAAM,GAAG;IACnB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAA;IACpC,QAAQ,CAAC,OAAO,EAAE,MAAM,WAAW,CAAC,MAAM,CAAC,CAAA;IAC3C,QAAQ,CAAC,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;IACrC,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,CAAA;IACjC,QAAQ,CAAC,UAAU,EAAE,MAAM,GAAG,SAAS,CAAA;IACvC,QAAQ,CAAC,WAAW,EAAE,MAAM,MAAM,CAAA;IAClC,QAAQ,CAAC,WAAW,EAAE,MAAM,SAAS,MAAM,EAAE,CAAA;IAC7C,QAAQ,CAAC,aAAa,EAAE,MAAM,WAAW,CAAC,MAAM,CAAC,CAAA;CAClD,CAAA;AAED,qBAAa,iBAAkB,SAAQ,KAAK;CAAG;AAI/C,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAC,MAAM,CAAC,CAIzD;AAkBD,wBAAsB,UAAU,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC,CAItG;AAED,wBAAsB,UAAU,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAE1F;AAED,wBAAsB,aAAa,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,EAAE,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAElJ;AAED,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAwFrE;AAmBD,wBAAgB,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAE3D;AAED,wBAAgB,SAAS,CAAC,OAAO,EAAE,WAAW,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAWvG;AAiBD,wBAAgB,OAAO,CAAC,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,MAAM,GAAG,UAAU,GAAG,MAAM,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAI9G;AA0BD,MAAM,MAAM,WAAW,GAAG;IACxB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAA;IAChC,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAAA;CAC1C,CAAA;AAED,wBAAgB,aAAa,CAAC,KAAK,EAAE,WAAW,GAAG,OAAO,CAEzD;AAED,wBAAgB,UAAU,CAAC,KAAK,EAAE,WAAW,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,KAAK,EAAE,WAAW,GAAG,OAAO,CAa9G;AAED,wBAAsB,aAAa,CAAC,KAAK,EAAE,WAAW,EAAE,CAAC,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,EAAE,KAAK,EAAE,WAAW,GAAG,OAAO,CAAC,OAAO,CAAC,CA+BxJ"}
1
+ {"version":3,"file":"grants.d.ts","sourceRoot":"","sources":["../src/runtime/grants.ts"],"names":[],"mappings":"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"}
package/runtime/grants.js CHANGED
@@ -31,6 +31,9 @@ export async function checkGrant(grants, call, item) {
31
31
  if (!isCovered(grants.granted(), item, grants.home))
32
32
  throw new PermissionRefused(refusal(grants, call, item));
33
33
  }
34
+ export async function checkRun(grants, call, argv) {
35
+ await checkGrant(grants, `${call}(${argv.join(' ')})`, `run:${programOf(argv)}`);
36
+ }
34
37
  export async function checkWrite(grants, call, path) {
35
38
  await checkGrant(grants, call, writeItem(grants, path));
36
39
  }
@@ -44,11 +47,11 @@ export function checkingGrants(claude, grants) {
44
47
  ...claude,
45
48
  process: {
46
49
  async run(argv, init) {
47
- await gate(`process.run(${argv.join(' ')})`, `run:${programOf(argv)}`);
50
+ await checkRun(grants, 'process.run', argv);
48
51
  return claude.process.run(argv, init);
49
52
  },
50
53
  spawn(request) {
51
- const started = gate(`process.spawn(${request.argv.join(' ')})`, `run:${programOf(request.argv)}`).then(() => claude.process.spawn(request));
54
+ const started = checkRun(grants, 'process.spawn', request.argv).then(() => claude.process.spawn(request));
52
55
  return spawned(started);
53
56
  },
54
57
  },