@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.
- package/docs/commands.md +44 -9
- package/docs/index.md +3 -3
- package/docs/install-steps.md +8 -8
- package/docs/jobs.md +2 -1
- package/docs/mod.md +1 -1
- package/docs/options.md +1 -1
- package/docs/permissions.md +1 -1
- package/docs/session.md +46 -13
- package/docs/testing.md +15 -11
- package/docs/ui.md +43 -11
- package/installer.d.ts +3 -2
- package/installer.d.ts.map +1 -1
- package/jobs/program.d.ts +1 -0
- package/jobs/program.d.ts.map +1 -1
- package/jobs/program.js +39 -23
- package/mod.d.ts +3 -2
- package/mod.d.ts.map +1 -1
- package/mod.js +1 -0
- package/package.json +1 -1
- package/records.d.ts +15 -1
- package/records.d.ts.map +1 -1
- package/records.js +39 -1
- package/register.d.ts.map +1 -1
- package/register.js +8 -5
- package/runtime/before.d.ts +6 -2
- package/runtime/before.d.ts.map +1 -1
- package/runtime/before.js +56 -18
- package/runtime/buttons.d.ts +10 -0
- package/runtime/buttons.d.ts.map +1 -0
- package/runtime/buttons.js +12 -0
- package/runtime/conversations.d.ts +3 -0
- package/runtime/conversations.d.ts.map +1 -1
- package/runtime/conversations.js +164 -27
- package/runtime/engine.d.ts +2 -0
- package/runtime/engine.d.ts.map +1 -1
- package/runtime/grants.d.ts +1 -0
- package/runtime/grants.d.ts.map +1 -1
- package/runtime/grants.js +5 -2
- package/runtime/hotkeys.d.ts.map +1 -1
- package/runtime/hotkeys.js +7 -4
- package/runtime/installer.d.ts +10 -6
- package/runtime/installer.d.ts.map +1 -1
- package/runtime/installer.js +106 -48
- package/runtime/lifecycle.d.ts +2 -2
- package/runtime/lifecycle.d.ts.map +1 -1
- package/runtime/lifecycle.js +144 -42
- package/runtime/pages.d.ts +3 -1
- package/runtime/pages.d.ts.map +1 -1
- package/runtime/pages.js +48 -14
- package/runtime/parts.d.ts +1 -0
- package/runtime/parts.d.ts.map +1 -1
- package/runtime/parts.js +6 -3
- package/runtime/programs.d.ts +1 -0
- package/runtime/programs.d.ts.map +1 -1
- package/runtime/programs.js +4 -1
- package/runtime/sessions.d.ts +10 -3
- package/runtime/sessions.d.ts.map +1 -1
- package/runtime/sessions.js +104 -59
- package/runtime/toaster.d.ts.map +1 -1
- package/runtime/toaster.js +23 -9
- package/runtime/ui.d.ts +22 -4
- package/runtime/ui.d.ts.map +1 -1
- package/runtime/ui.js +161 -59
- package/runtime/watches.d.ts.map +1 -1
- package/runtime/watches.js +18 -4
- package/runtime/widgets.d.ts +7 -5
- package/runtime/widgets.d.ts.map +1 -1
- package/runtime/widgets.js +31 -21
- package/session/context/turn/message/attached-file.d.ts +13 -0
- package/session/context/turn/message/attached-file.d.ts.map +1 -0
- package/session/context/turn/message/attached-file.js +21 -0
- package/session/context/turn/message/message.d.ts +3 -2
- package/session/context/turn/message/message.d.ts.map +1 -1
- package/session/context/turn/message/message.js +20 -6
- package/session/conversation.d.ts +15 -3
- package/session/conversation.d.ts.map +1 -1
- package/session/conversation.js +23 -4
- package/session/goal/task.d.ts +5 -0
- package/session/goal/task.d.ts.map +1 -1
- package/session/session.d.ts +36 -7
- package/session/session.d.ts.map +1 -1
- package/session/session.js +2 -1
- package/src/installer.ts +3 -2
- package/src/jobs/program.ts +41 -21
- package/src/mod.ts +3 -2
- package/src/records.ts +49 -1
- package/src/register.ts +11 -8
- package/src/runtime/before.ts +61 -22
- package/src/runtime/buttons.ts +22 -0
- package/src/runtime/conversations.ts +170 -34
- package/src/runtime/engine.ts +2 -0
- package/src/runtime/grants.ts +6 -2
- package/src/runtime/hotkeys.ts +7 -4
- package/src/runtime/installer.ts +147 -83
- package/src/runtime/lifecycle.ts +149 -48
- package/src/runtime/pages.ts +56 -12
- package/src/runtime/parts.ts +8 -4
- package/src/runtime/programs.ts +5 -1
- package/src/runtime/sessions.ts +106 -59
- package/src/runtime/toaster.ts +27 -9
- package/src/runtime/ui.ts +166 -62
- package/src/runtime/watches.ts +20 -5
- package/src/runtime/widgets.ts +35 -23
- package/src/session/context/turn/message/attached-file.ts +36 -0
- package/src/session/context/turn/message/message.ts +20 -7
- package/src/session/conversation.ts +31 -6
- package/src/session/goal/task.ts +6 -0
- package/src/session/session.ts +18 -7
- package/src/testing/fake-claude.ts +21 -6
- package/src/testing/fake-elements.ts +57 -8
- package/src/testing/fake-sessions.ts +72 -24
- package/src/testing.ts +114 -33
- package/src/ui/areas.ts +1 -0
- package/src/ui/components.ts +14 -8
- package/src/ui/elements.ts +6 -2
- package/src/ui/markdown.ts +3 -3
- package/src/ui/panes.ts +2 -2
- package/src/ui/styles.ts +6 -2
- package/testing/fake-claude.d.ts +3 -0
- package/testing/fake-claude.d.ts.map +1 -1
- package/testing/fake-claude.js +18 -5
- package/testing/fake-elements.d.ts.map +1 -1
- package/testing/fake-elements.js +52 -7
- package/testing/fake-sessions.d.ts +24 -10
- package/testing/fake-sessions.d.ts.map +1 -1
- package/testing/fake-sessions.js +66 -20
- package/testing.d.ts +8 -12
- package/testing.d.ts.map +1 -1
- package/testing.js +113 -27
- package/ui/areas.d.ts +4 -0
- package/ui/areas.d.ts.map +1 -1
- package/ui/components.d.ts +1 -0
- package/ui/components.d.ts.map +1 -1
- package/ui/components.js +11 -7
- package/ui/elements.d.ts +3 -0
- package/ui/elements.d.ts.map +1 -1
- package/ui/elements.js +6 -1
- package/ui/markdown.d.ts +2 -2
- package/ui/markdown.d.ts.map +1 -1
- package/ui/panes.d.ts +2 -1
- package/ui/panes.d.ts.map +1 -1
- package/ui/styles.d.ts +11 -2
- 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
|
-
|
|
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
|
|
106
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
|
231
|
-
|
|
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,
|
|
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"}
|
package/runtime/conversations.js
CHANGED
|
@@ -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
|
|
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 (
|
|
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
|
|
261
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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() });
|
package/runtime/engine.d.ts
CHANGED
|
@@ -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
|
};
|
package/runtime/engine.d.ts.map
CHANGED
|
@@ -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"}
|
package/runtime/grants.d.ts
CHANGED
|
@@ -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;
|
package/runtime/grants.d.ts.map
CHANGED
|
@@ -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
|
|
50
|
+
await checkRun(grants, 'process.run', argv);
|
|
48
51
|
return claude.process.run(argv, init);
|
|
49
52
|
},
|
|
50
53
|
spawn(request) {
|
|
51
|
-
const started =
|
|
54
|
+
const started = checkRun(grants, 'process.spawn', request.argv).then(() => claude.process.spawn(request));
|
|
52
55
|
return spawned(started);
|
|
53
56
|
},
|
|
54
57
|
},
|