@cmodjs/core 4.0.2 → 4.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/docs/commands.md +43 -8
- package/docs/index.md +3 -3
- package/docs/install-steps.md +6 -6
- 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 +44 -13
- package/docs/testing.md +14 -10
- 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 +5 -2
- 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.map +1 -1
- package/runtime/conversations.js +134 -25
- 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 +9 -6
- package/runtime/installer.d.ts.map +1 -1
- package/runtime/installer.js +104 -47
- package/runtime/lifecycle.d.ts.map +1 -1
- package/runtime/lifecycle.js +121 -40
- package/runtime/pages.d.ts +3 -1
- package/runtime/pages.d.ts.map +1 -1
- package/runtime/pages.js +34 -10
- 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 +2 -2
- package/runtime/sessions.d.ts.map +1 -1
- package/runtime/sessions.js +88 -58
- package/runtime/toaster.d.ts.map +1 -1
- package/runtime/toaster.js +23 -9
- package/runtime/ui.d.ts +21 -4
- package/runtime/ui.d.ts.map +1 -1
- package/runtime/ui.js +158 -58
- 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 +29 -20
- 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 +14 -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 +35 -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 +9 -6
- package/src/runtime/before.ts +61 -22
- package/src/runtime/buttons.ts +22 -0
- package/src/runtime/conversations.ts +138 -32
- 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 +144 -82
- package/src/runtime/lifecycle.ts +128 -43
- package/src/runtime/pages.ts +45 -8
- package/src/runtime/parts.ts +8 -4
- package/src/runtime/programs.ts +5 -1
- package/src/runtime/sessions.ts +83 -57
- package/src/runtime/toaster.ts +27 -9
- package/src/runtime/ui.ts +163 -61
- package/src/runtime/watches.ts +20 -5
- package/src/runtime/widgets.ts +34 -22
- 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 +30 -6
- package/src/session/goal/task.ts +6 -0
- package/src/session/session.ts +17 -7
- package/src/testing/fake-claude.ts +21 -6
- package/src/testing/fake-elements.ts +51 -8
- package/src/testing/fake-sessions.ts +60 -23
- package/src/testing.ts +86 -28
- 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 +46 -7
- package/testing/fake-sessions.d.ts +21 -10
- package/testing/fake-sessions.d.ts.map +1 -1
- package/testing/fake-sessions.js +58 -20
- package/testing.d.ts +8 -12
- package/testing.d.ts.map +1 -1
- package/testing.js +82 -21
- 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
|
@@ -3,17 +3,19 @@ import { consentPath, coreVersion, dataFolder, grantFix, isObject, parseConsent
|
|
|
3
3
|
import { Message, rowOfAppend, type Row } from '../session/context/turn/message/message.js'
|
|
4
4
|
import { modNote, Note } from '../session/context/turn/message/note.js'
|
|
5
5
|
import { Person } from '../session/context/turn/message/sender.js'
|
|
6
|
-
import { createConversation, emptyConversation, type Conversation, type ConversationState, type ContextData, type SessionChange, type SessionData, type TaskData, type TurnData, type TurnEnding } from '../session/conversation.js'
|
|
6
|
+
import { createConversation, emptyConversation, ownerOfSpawn, tasksWithAgents, type Conversation, type ConversationState, type ContextData, type SessionChange, type SessionData, type TaskData, type TurnData, type TurnEnding } from '../session/conversation.js'
|
|
7
7
|
import { Session, UsageChange, type BeforeEvent, type Surface, type SessionSend, type TaskStartArgs, type ToolCallArgs } from '../session/session.js'
|
|
8
8
|
import { GoalChange } from '../session/goal/goal.js'
|
|
9
9
|
import { TurnEnd, TurnStart } from '../session/context/turn/turn.js'
|
|
10
|
+
import { TaskStart } from '../session/goal/task.js'
|
|
10
11
|
import { receivedMatcher } from '../utils/matcher.js'
|
|
12
|
+
import { resolve } from '../vendor.js'
|
|
11
13
|
import { ifMissing } from '../utils/paths.js'
|
|
12
14
|
import { formatExit, messageOf } from '../utils/text.js'
|
|
13
|
-
import { beforeAnswerKeys, foldBefore, installBefore, relayOf, type Place } from './before.js'
|
|
15
|
+
import { beforeAnswerKeys, foldBefore, installBefore, relayOf, type Decide, type Place } from './before.js'
|
|
14
16
|
import type { Claude } from './claude.js'
|
|
15
17
|
import { cmodPluginName } from './dependencies.js'
|
|
16
|
-
import { readLines, runCmod } from './programs.js'
|
|
18
|
+
import { readLines, runCmod, spawnCmod } from './programs.js'
|
|
17
19
|
import type { RoutedHook, Router } from './router.js'
|
|
18
20
|
import { PassedTimeout } from './deadline.js'
|
|
19
21
|
import { actPermission, conversationMethods, everySession, isAct, isHeardBy, isInScope, resultOf, spawnTask, usageOf, type Act, type Listened } from './sessions.js'
|
|
@@ -30,9 +32,16 @@ type Question =
|
|
|
30
32
|
| { readonly kind: 'act'; readonly caller: string; readonly action: Act; readonly input: unknown; readonly task?: string }
|
|
31
33
|
| { readonly kind: 'before'; readonly mod: string; readonly event: BeforeEvent; readonly change: SessionChange; readonly project: string }
|
|
32
34
|
|
|
33
|
-
type Relay =
|
|
35
|
+
type Relay =
|
|
36
|
+
| (Question & { readonly id: string; readonly from: string })
|
|
37
|
+
| { readonly kind: 'answer'; readonly id: string; readonly value?: unknown; readonly deny?: string }
|
|
38
|
+
| { readonly kind: 'watch' }
|
|
39
|
+
| { readonly kind: 'goal'; readonly task?: string; readonly text: string }
|
|
40
|
+
| { readonly kind: 'taskEnd'; readonly task: TaskData }
|
|
34
41
|
|
|
35
|
-
const relayKinds: ReadonlySet<unknown> = new Set(['act', 'before', 'answer', 'watch', 'goal'] satisfies Relay['kind'][])
|
|
42
|
+
const relayKinds: ReadonlySet<unknown> = new Set(['act', 'before', 'answer', 'watch', 'goal', 'taskEnd'] satisfies Relay['kind'][])
|
|
43
|
+
|
|
44
|
+
const taskVariable = 'CMOD_TASK'
|
|
36
45
|
|
|
37
46
|
const isRelay = (value: unknown): value is Relay => isObject(value) && relayKinds.has(value['kind'])
|
|
38
47
|
|
|
@@ -75,8 +84,12 @@ export function serveConversations({
|
|
|
75
84
|
}): Conversations {
|
|
76
85
|
const folder = dataFolder(store, cmodPluginName)
|
|
77
86
|
const goals = `${folder}/goals`
|
|
87
|
+
const backgroundTasks = `${folder}/tasks`
|
|
78
88
|
const watches = `${folder}/before`
|
|
89
|
+
const listeners = `${folder}/listeners`
|
|
79
90
|
const heard = new Map<string, Map<string, readonly Heard[]>>()
|
|
91
|
+
const subscribed = new Map<string, Map<string, readonly Listened[]>>()
|
|
92
|
+
let savingListeners: Promise<unknown> = Promise.resolve()
|
|
80
93
|
const asked = new Map<string, Asked>()
|
|
81
94
|
const ownWatches = new Map<string, Watch>()
|
|
82
95
|
let watchers: readonly Watcher[] = []
|
|
@@ -106,6 +119,11 @@ export function serveConversations({
|
|
|
106
119
|
router.add('classic.Stop', async (e, next) => endTurn({ reason: 'answer', ...(e.last_assistant_message === undefined ? {} : { answer: e.last_assistant_message }) })(await next(e)))
|
|
107
120
|
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)))
|
|
108
121
|
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)))
|
|
122
|
+
router.add('classic.Stop', async (e, next) => {
|
|
123
|
+
const below = await next(e)
|
|
124
|
+
if (below.block === undefined) void answerTask().catch((error: unknown) => report('task', 'Could not record that this session answered its task', error))
|
|
125
|
+
return below
|
|
126
|
+
})
|
|
109
127
|
|
|
110
128
|
const json = async <Value>(args: readonly string[]): Promise<Value> => JSON.parse(await runCmod(claude, [...args, '--json', '--project', project()], await claude.session.id())) as Value
|
|
111
129
|
|
|
@@ -154,7 +172,7 @@ export function serveConversations({
|
|
|
154
172
|
follower?.stop()
|
|
155
173
|
follower = undefined
|
|
156
174
|
if (args.length === 0) return
|
|
157
|
-
const stream = claude
|
|
175
|
+
const stream = spawnCmod(claude, ['history', 'follow', ...args, '--json', '--project', project()], session)
|
|
158
176
|
const started = { sessions: wanted, stop: () => void stream.return(undefined as never) }
|
|
159
177
|
follower = started
|
|
160
178
|
void readLines(stream, (line) => {
|
|
@@ -241,13 +259,39 @@ export function serveConversations({
|
|
|
241
259
|
await Promise.all([...told].map((id) => post(id, { kind: 'watch' }).catch((error: unknown) => claude.ui.log(`cmod: session ${id} did not hear of a before listener: ${messageOf(error)}`, { to: 'debug' }))))
|
|
242
260
|
}
|
|
243
261
|
|
|
262
|
+
const hear = (caller: string, id: string, list: readonly Listened[]) => {
|
|
263
|
+
const sessions = heard.get(caller) ?? new Map<string, readonly Heard[]>()
|
|
264
|
+
sessions.set(id, list.map(({ event, matcher }) => ({ event, matcher: matcher === undefined ? undefined : receivedMatcher(matcher) })))
|
|
265
|
+
heard.set(caller, sessions)
|
|
266
|
+
const lists = subscribed.get(caller) ?? new Map<string, readonly Listened[]>()
|
|
267
|
+
lists.set(id, list)
|
|
268
|
+
subscribed.set(caller, lists)
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const saveListeners = (isEnding = false) => {
|
|
272
|
+
const at = session
|
|
273
|
+
const kept = isEnding ? {} : Object.fromEntries([...subscribed].map(([caller, lists]) => [caller, Object.fromEntries(lists)]))
|
|
274
|
+
const next = savingListeners.then(() => (at === '' ? undefined : claude.fs.write(`${listeners}/${at}.json`, JSON.stringify(kept))))
|
|
275
|
+
savingListeners = next.catch((error: unknown) => claude.ui.log(`cmod: saving the mods' listeners failed: ${messageOf(error)}`, { to: 'debug' }))
|
|
276
|
+
return next
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const restoreListeners = async () => {
|
|
280
|
+
const kept = JSON.parse(await claude.fs.read(`${listeners}/${session}.json`).catch(ifMissing('{}'))) as Readonly<Record<string, Readonly<Record<string, readonly Listened[]>>>>
|
|
281
|
+
for (const [caller, lists] of Object.entries(kept)) if (!subscribed.has(caller)) for (const [id, list] of Object.entries(lists)) hear(caller, id, list)
|
|
282
|
+
}
|
|
283
|
+
|
|
244
284
|
const readWatchers = async () => {
|
|
245
285
|
const files = await claude.fs.list(watches).catch(ifMissing([]))
|
|
246
286
|
const found: Watcher[] = []
|
|
247
287
|
for (const file of files) {
|
|
248
288
|
const from = file.name.replace(/\.json$/, '')
|
|
249
|
-
if (
|
|
289
|
+
if (!file.name.endsWith('.json')) continue
|
|
250
290
|
const list = JSON.parse(await claude.fs.read(`${watches}/${file.name}`).catch(ifMissing('[]'))) as readonly Watch[]
|
|
291
|
+
if (from === session) {
|
|
292
|
+
for (const watch of list) if (!ownWatches.has(`${watch.mod}\n${watch.target}`)) ownWatches.set(`${watch.mod}\n${watch.target}`, watch)
|
|
293
|
+
continue
|
|
294
|
+
}
|
|
251
295
|
found.push(...list.filter((watch) => watch.target === session || (watch.target === everySession && watch.project === project())).map((watch) => ({ ...watch, from })))
|
|
252
296
|
}
|
|
253
297
|
watchers = found
|
|
@@ -299,6 +343,26 @@ export function serveConversations({
|
|
|
299
343
|
}
|
|
300
344
|
}
|
|
301
345
|
|
|
346
|
+
const wants = async (event: BeforeEvent) => {
|
|
347
|
+
await Promise.all([starting, watching])
|
|
348
|
+
return deciders(event).length > 0
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const decide = (): Decide => {
|
|
352
|
+
let endsAt: Promise<number> | undefined
|
|
353
|
+
return (event, change) =>
|
|
354
|
+
foldBefore(
|
|
355
|
+
event,
|
|
356
|
+
change,
|
|
357
|
+
deciders(event).map((watcher) => async (latest: SessionChange) => {
|
|
358
|
+
if (watcher.from === session) return askHere(watcher.mod, event, latest)
|
|
359
|
+
endsAt ??= claude.clock.now().then((now) => now + awayFoldMs)
|
|
360
|
+
return askAway(watcher, event, latest, await endsAt)
|
|
361
|
+
}),
|
|
362
|
+
(error) => report(`before:${event}`, `Passed the ${event} on without a mod's answer`, error),
|
|
363
|
+
)
|
|
364
|
+
}
|
|
365
|
+
|
|
302
366
|
installBefore({
|
|
303
367
|
claude,
|
|
304
368
|
router,
|
|
@@ -307,24 +371,8 @@ export function serveConversations({
|
|
|
307
371
|
await starting
|
|
308
372
|
return place()
|
|
309
373
|
},
|
|
310
|
-
wants
|
|
311
|
-
|
|
312
|
-
return deciders(event).length > 0
|
|
313
|
-
},
|
|
314
|
-
fold: () => {
|
|
315
|
-
let endsAt: Promise<number> | undefined
|
|
316
|
-
return (event, change) =>
|
|
317
|
-
foldBefore(
|
|
318
|
-
event,
|
|
319
|
-
change,
|
|
320
|
-
deciders(event).map((watcher) => async (latest: SessionChange) => {
|
|
321
|
-
if (watcher.from === session) return askHere(watcher.mod, event, latest)
|
|
322
|
-
endsAt ??= claude.clock.now().then((now) => now + awayFoldMs)
|
|
323
|
-
return askAway(watcher, event, latest, await endsAt)
|
|
324
|
-
}),
|
|
325
|
-
(error) => report(`before:${event}`, `Passed the ${event} on without a mod's answer`, error),
|
|
326
|
-
)
|
|
327
|
-
},
|
|
374
|
+
wants,
|
|
375
|
+
fold: decide,
|
|
328
376
|
},
|
|
329
377
|
})
|
|
330
378
|
|
|
@@ -345,8 +393,12 @@ export function serveConversations({
|
|
|
345
393
|
},
|
|
346
394
|
(error: unknown) => report('state', `Starts session ${id} without its earlier turns`, error),
|
|
347
395
|
)
|
|
348
|
-
|
|
396
|
+
const restored = restoreListeners()
|
|
397
|
+
.catch((error: unknown) => report('listeners', "Could not read back the mods' listeners of this session", error))
|
|
398
|
+
.then(follow)
|
|
399
|
+
starting = Promise.all([starting, restored]).then(() => undefined)
|
|
349
400
|
reloadWatchers()
|
|
401
|
+
if (subscribed.size > 0) void Promise.all([previous === '' ? undefined : claude.fs.write(`${listeners}/${previous}.json`, '{}'), saveListeners()]).catch((error: unknown) => report('follow', 'Kept telling the mods about the session from before', error))
|
|
350
402
|
if (ownWatches.size === 0) return
|
|
351
403
|
void Promise.all([previous === '' ? undefined : claude.fs.write(`${watches}/${previous}.json`, '[]'), publishWatches()]).catch((error: unknown) => report('before', 'Kept its before listeners on the session from before', error))
|
|
352
404
|
}
|
|
@@ -375,6 +427,7 @@ export function serveConversations({
|
|
|
375
427
|
})
|
|
376
428
|
router.add('classic.SessionEnd', async (e, next) => {
|
|
377
429
|
if (ownWatches.size > 0) await publishWatches(true).catch((error: unknown) => report('before', 'Left its before listeners registered', error))
|
|
430
|
+
if (subscribed.size > 0) await saveListeners(true).catch((error: unknown) => report('follow', 'Left its listeners registered', error))
|
|
378
431
|
return next(e)
|
|
379
432
|
})
|
|
380
433
|
|
|
@@ -407,8 +460,8 @@ export function serveConversations({
|
|
|
407
460
|
const spawned = await next(e)
|
|
408
461
|
if (spawned.deny !== undefined || spawned.agentId === undefined) return spawned
|
|
409
462
|
const id = spawned.agentId
|
|
410
|
-
const opened: TaskData = { 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 }) }
|
|
411
463
|
await starting
|
|
464
|
+
const opened: TaskData = { 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 }) }
|
|
412
465
|
dispatch(session, conversation.startTask(opened))
|
|
413
466
|
const waiting = waitingRows.get(id) ?? []
|
|
414
467
|
waitingRows.delete(id)
|
|
@@ -445,6 +498,11 @@ export function serveConversations({
|
|
|
445
498
|
changeGoal(relay.task, relay.text)
|
|
446
499
|
return
|
|
447
500
|
}
|
|
501
|
+
if (relay.kind === 'taskEnd') {
|
|
502
|
+
await starting
|
|
503
|
+
dispatch(session, conversation.endTask(relay.task, relay.task.closedAt ?? new Date().toISOString()))
|
|
504
|
+
return
|
|
505
|
+
}
|
|
448
506
|
const answered = async (run: () => Promise<unknown>) => {
|
|
449
507
|
try {
|
|
450
508
|
await post(relay.from, { kind: 'answer', id: relay.id, value: (await run()) ?? null })
|
|
@@ -477,6 +535,9 @@ export function serveConversations({
|
|
|
477
535
|
return null
|
|
478
536
|
}
|
|
479
537
|
if (action === 'startTask') return spawnTask(claude, input as TaskStartArgs)
|
|
538
|
+
if (action === 'fork') return spawnTask(claude, input as TaskStartArgs, session)
|
|
539
|
+
if (action === 'startBackgroundTask') return startBackground(input as TaskStartArgs)
|
|
540
|
+
if (action === 'clone') return startBackground(input as TaskStartArgs, session)
|
|
480
541
|
const { name, input: toolInput } = input as ToolCallArgs
|
|
481
542
|
return resultOf(place(), { name, input: toolInput, cwd: await claude.session.cwd() }, await claude.tool.call({ ...toolInput, tool: name } as never))
|
|
482
543
|
}
|
|
@@ -503,6 +564,48 @@ export function serveConversations({
|
|
|
503
564
|
return null
|
|
504
565
|
}
|
|
505
566
|
|
|
567
|
+
const startBackground = async (task: TaskStartArgs, forkedFrom?: string): Promise<TaskData> => {
|
|
568
|
+
if (task.title.trim() === '') throw new Error('a task needs a title.')
|
|
569
|
+
if (task.name !== undefined) throw new Error('a background task takes no name.')
|
|
570
|
+
const fresh: Partial<TaskStartArgs> = forkedFrom === undefined ? task : {}
|
|
571
|
+
const asked: TaskData = { 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 }
|
|
572
|
+
const answer = (await wants(TaskStart)) ? await decide()(TaskStart, { type: TaskStart, task: { ...asked, context: conversation.state.context } }) : undefined
|
|
573
|
+
if (answer?.refuse !== undefined) throw new Error(`a beforeTaskStart listener refused the task: ${answer.refuse}`)
|
|
574
|
+
const owner = forkedFrom === undefined ? (answer?.owner ?? {}) : {}
|
|
575
|
+
const folder = owner.cwd ?? fresh.cwd
|
|
576
|
+
const here = await claude.session.cwd()
|
|
577
|
+
const args = [
|
|
578
|
+
'task',
|
|
579
|
+
'start',
|
|
580
|
+
'--title',
|
|
581
|
+
answer?.title ?? task.title,
|
|
582
|
+
...(forkedFrom === undefined ? [] : ['--clone']),
|
|
583
|
+
...textArg('--agent', owner.agentType ?? fresh.agentType),
|
|
584
|
+
...textArg('--model', owner.model ?? fresh.model),
|
|
585
|
+
'--cwd',
|
|
586
|
+
folder === undefined ? here : resolve(here, folder),
|
|
587
|
+
'--json',
|
|
588
|
+
'--',
|
|
589
|
+
answer?.prompt ?? task.prompt,
|
|
590
|
+
]
|
|
591
|
+
const started = JSON.parse(await runCmod(claude, args, session)) as TaskData
|
|
592
|
+
dispatch(session, conversation.startTask(started))
|
|
593
|
+
return started
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
const answerTask = async () => {
|
|
597
|
+
const { env } = await claude.settings.read()
|
|
598
|
+
const asked = isObject(env) ? env[taskVariable] : undefined
|
|
599
|
+
if (typeof asked !== 'string') return
|
|
600
|
+
const answered = `cmod:task-answered:${session}`
|
|
601
|
+
if ((await claude.store.get(answered)) === true) return
|
|
602
|
+
await claude.store.set(answered, true)
|
|
603
|
+
const { session: from, ...task } = JSON.parse(asked) as TaskData & { readonly session: string }
|
|
604
|
+
const ended: TaskData = { ...task, id: session, closedAt: new Date().toISOString() }
|
|
605
|
+
await claude.fs.write(`${backgroundTasks}/${from}/${session}.json`, JSON.stringify(ended))
|
|
606
|
+
await post(from, { kind: 'taskEnd', task: ended }).catch((error: unknown) => claude.ui.log(`cmod: session ${from} did not hear that its task ${session} answered: ${messageOf(error)}`, { to: 'debug' }))
|
|
607
|
+
}
|
|
608
|
+
|
|
506
609
|
const actElsewhere = async (caller: string, target: string, action: Act, input: unknown, task: string | undefined): Promise<unknown> => {
|
|
507
610
|
await checkAct(caller, action, input)
|
|
508
611
|
const isLive = await isRunning(target)
|
|
@@ -531,12 +634,14 @@ export function serveConversations({
|
|
|
531
634
|
Object.values(conversation.state.tasks).map((task) => ({
|
|
532
635
|
id: task.id,
|
|
533
636
|
title: task.title,
|
|
534
|
-
agentType: task.agentType,
|
|
637
|
+
...(task.agentType === undefined ? {} : { agentType: task.agentType }),
|
|
535
638
|
...(task.model === undefined ? {} : { model: task.model }),
|
|
536
639
|
openedBy: task.openedBy,
|
|
537
640
|
openedAt: task.openedAt,
|
|
538
641
|
...(task.closedAt === undefined ? {} : { closedAt: task.closedAt }),
|
|
539
642
|
...(task.parent === undefined ? {} : { parent: task.parent }),
|
|
643
|
+
...(task.forkedFrom === undefined ? {} : { forkedFrom: task.forkedFrom }),
|
|
644
|
+
...(task.isBackground === true ? { isBackground: true as const } : {}),
|
|
540
645
|
}))
|
|
541
646
|
|
|
542
647
|
const liveContext = (task: unknown): ContextData[] => {
|
|
@@ -600,7 +705,9 @@ export function serveConversations({
|
|
|
600
705
|
if (id !== session) return saved
|
|
601
706
|
await starting
|
|
602
707
|
const kept = liveTasks().filter((task) => isNested || task.parent === request['task'])
|
|
603
|
-
|
|
708
|
+
const running = await claude.agent.list().catch((error: unknown) => (claude.ui.log(`cmod: the tasks show no status, because Claude Code listed no agents: ${messageOf(error)}`, { to: 'debug' }), []))
|
|
709
|
+
const background = saved.flatMap(({ id, status }) => (status === undefined ? [] : [{ id, status }]))
|
|
710
|
+
return tasksWithAgents(merged(saved, kept, (task) => task.id), [...running, ...background])
|
|
604
711
|
},
|
|
605
712
|
async [conversationMethods.goal](request) {
|
|
606
713
|
const id = String(request['session'])
|
|
@@ -629,10 +736,9 @@ export function serveConversations({
|
|
|
629
736
|
if (caller === undefined) throw new Error('cmod hears no caller for this subscription.')
|
|
630
737
|
const id = String(request['session'])
|
|
631
738
|
const list = Array.isArray(request['subscriptions']) ? (request['subscriptions'] as readonly Listened[]) : []
|
|
632
|
-
|
|
633
|
-
sessions.set(id, list.map(({ event, matcher }) => ({ event, matcher: matcher === undefined ? undefined : receivedMatcher(matcher) })))
|
|
634
|
-
heard.set(caller, sessions)
|
|
739
|
+
hear(caller, id, list)
|
|
635
740
|
follow()
|
|
741
|
+
await saveListeners()
|
|
636
742
|
return null
|
|
637
743
|
},
|
|
638
744
|
async [conversationMethods.watchBefore](request, caller) {
|
package/src/runtime/engine.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'
|
|
@@ -12,6 +13,7 @@ export type Engine = {
|
|
|
12
13
|
readonly report: (failure: Failure) => void
|
|
13
14
|
readonly hasPermissionCheck: () => boolean
|
|
14
15
|
readonly answerInput: <Answer>(run: () => Promise<Answer>, passOn: () => Promise<unknown>) => Promise<Answer>
|
|
16
|
+
readonly spawnCmod: (args: readonly string[], program: ProcessSpawnRequest) => Promise<HookStream<ProcessSpawnChunk, ProcessSpawnResult>>
|
|
15
17
|
readonly widgets?: Widgets
|
|
16
18
|
readonly pages?: Pages
|
|
17
19
|
}
|
package/src/runtime/grants.ts
CHANGED
|
@@ -49,6 +49,10 @@ export async function checkGrant(grants: Grants, call: string, item: string | un
|
|
|
49
49
|
if (!isCovered(grants.granted(), item, grants.home)) throw new PermissionRefused(refusal(grants, call, item))
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
export async function checkRun(grants: Grants, call: 'process.run' | 'process.spawn', argv: readonly string[]): Promise<void> {
|
|
53
|
+
await checkGrant(grants, `${call}(${argv.join(' ')})`, `run:${programOf(argv)}`)
|
|
54
|
+
}
|
|
55
|
+
|
|
52
56
|
export async function checkWrite(grants: Grants, call: string, path: string): Promise<void> {
|
|
53
57
|
await checkGrant(grants, call, writeItem(grants, path))
|
|
54
58
|
}
|
|
@@ -63,11 +67,11 @@ export function checkingGrants(claude: Claude, grants: Grants): Claude {
|
|
|
63
67
|
...claude,
|
|
64
68
|
process: {
|
|
65
69
|
async run(argv, init) {
|
|
66
|
-
await
|
|
70
|
+
await checkRun(grants, 'process.run', argv)
|
|
67
71
|
return claude.process.run(argv, init)
|
|
68
72
|
},
|
|
69
73
|
spawn(request) {
|
|
70
|
-
const started =
|
|
74
|
+
const started = checkRun(grants, 'process.spawn', request.argv).then(() => claude.process.spawn(request))
|
|
71
75
|
return spawned(started)
|
|
72
76
|
},
|
|
73
77
|
},
|
package/src/runtime/hotkeys.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { boundCombos, hotkeyCommand } from '../records.js'
|
|
|
3
3
|
import { ifMissing } from '../utils/paths.js'
|
|
4
4
|
import { messageOf } from '../utils/text.js'
|
|
5
5
|
import type { Claude } from './claude.js'
|
|
6
|
+
import { cmodPluginName } from './dependencies.js'
|
|
6
7
|
import type { Router } from './router.js'
|
|
7
8
|
import type { Failure } from './toaster.js'
|
|
8
9
|
|
|
@@ -34,6 +35,12 @@ export async function createHotkeys({
|
|
|
34
35
|
readonly answerInput: <Answer>(run: () => Promise<Answer>, passOn: () => Promise<unknown>) => Promise<Answer>
|
|
35
36
|
readonly report: (failure: Failure) => void
|
|
36
37
|
}): Promise<Readonly<Record<string, Hotkey>>> {
|
|
38
|
+
if (name === cmodPluginName) {
|
|
39
|
+
router.add('command.describe', async (e, next) => {
|
|
40
|
+
const described = await next(e)
|
|
41
|
+
return e.command.startsWith(hotkeyCommand(e.provider.plugin, '')) ? { ...described, isHidden: true } : described
|
|
42
|
+
})
|
|
43
|
+
}
|
|
37
44
|
const entries = Object.entries(declared)
|
|
38
45
|
if (entries.length === 0) return {}
|
|
39
46
|
const combos = await readCombos(claude, configRoot)
|
|
@@ -46,10 +53,6 @@ export async function createHotkeys({
|
|
|
46
53
|
(error: unknown) => report({ id: `hotkey:${hotkey}`, message: `The ${title} hotkey is not added`, error }),
|
|
47
54
|
)
|
|
48
55
|
}
|
|
49
|
-
router.add('command.describe', async (e, next) => {
|
|
50
|
-
const described = await next(e)
|
|
51
|
-
return commands.has(e.command) ? { ...described, isHidden: true } : described
|
|
52
|
-
})
|
|
53
56
|
router.add('command.run', async (e, next) => {
|
|
54
57
|
const hotkey = commands.get(e.command)
|
|
55
58
|
if (hotkey === undefined) return next(e)
|