@cmodjs/core 1.0.0 → 1.5.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 +22 -11
- package/docs/dependencies.md +6 -4
- package/docs/hooks.md +26 -3
- package/docs/index.md +1 -1
- package/docs/install-steps.md +5 -3
- package/docs/mod.md +29 -3
- package/docs/permissions.md +4 -4
- package/docs/session.md +126 -63
- package/docs/testing.md +7 -5
- package/docs/ui.md +9 -1
- package/jobs/program.d.ts.map +1 -1
- package/jobs/program.js +7 -2
- package/mod.d.ts +30 -6
- package/mod.d.ts.map +1 -1
- package/mod.js +34 -1
- package/package.json +1 -1
- package/records.d.ts +4 -1
- package/records.d.ts.map +1 -1
- package/records.js +6 -1
- package/register.d.ts.map +1 -1
- package/register.js +17 -0
- package/runtime/announce.d.ts.map +1 -1
- package/runtime/announce.js +2 -0
- package/runtime/before.d.ts +32 -0
- package/runtime/before.d.ts.map +1 -0
- package/runtime/before.js +214 -0
- package/runtime/claude.d.ts +2 -1
- package/runtime/claude.d.ts.map +1 -1
- package/runtime/conversations.d.ts +1 -0
- package/runtime/conversations.d.ts.map +1 -1
- package/runtime/conversations.js +333 -61
- package/runtime/dependencies.js +1 -1
- package/runtime/engine.d.ts +2 -6
- package/runtime/engine.d.ts.map +1 -1
- package/runtime/grants.d.ts.map +1 -1
- package/runtime/grants.js +8 -8
- package/runtime/hooks.d.ts +1 -1
- package/runtime/hooks.d.ts.map +1 -1
- package/runtime/hooks.js +19 -4
- package/runtime/installer.d.ts +1 -1
- package/runtime/installer.d.ts.map +1 -1
- package/runtime/installer.js +6 -5
- package/runtime/lifecycle.d.ts.map +1 -1
- package/runtime/lifecycle.js +129 -58
- package/runtime/metadata.d.ts.map +1 -1
- package/runtime/metadata.js +5 -4
- package/runtime/sessions.d.ts +26 -4
- package/runtime/sessions.d.ts.map +1 -1
- package/runtime/sessions.js +315 -117
- package/runtime/toaster.d.ts +5 -12
- package/runtime/toaster.d.ts.map +1 -1
- package/runtime/toaster.js +133 -23
- package/runtime/ui.d.ts.map +1 -1
- package/runtime/ui.js +26 -6
- package/session/context/context.d.ts +3 -1
- package/session/context/context.d.ts.map +1 -1
- package/session/context/turn/message/message.d.ts +1 -0
- package/session/context/turn/message/message.d.ts.map +1 -1
- package/session/context/turn/message/message.js +42 -1
- package/session/context/turn/turn.d.ts +2 -1
- package/session/context/turn/turn.d.ts.map +1 -1
- package/session/conversation.d.ts +20 -11
- package/session/conversation.d.ts.map +1 -1
- package/session/conversation.js +50 -27
- package/session/goal/goal.d.ts +3 -2
- package/session/goal/goal.d.ts.map +1 -1
- package/session/goal/task.d.ts +4 -4
- package/session/goal/task.d.ts.map +1 -1
- package/session/session.d.ts +80 -21
- package/session/session.d.ts.map +1 -1
- package/session/session.js +1 -0
- package/src/jobs/program.ts +7 -2
- package/src/mod.ts +48 -5
- package/src/records.ts +6 -1
- package/src/register.ts +17 -0
- package/src/runtime/announce.ts +2 -0
- package/src/runtime/before.ts +200 -0
- package/src/runtime/claude.ts +3 -0
- package/src/runtime/conversations.ts +337 -60
- package/src/runtime/dependencies.ts +1 -1
- package/src/runtime/engine.ts +2 -1
- package/src/runtime/grants.ts +8 -10
- package/src/runtime/hooks.ts +20 -4
- package/src/runtime/installer.ts +6 -5
- package/src/runtime/lifecycle.ts +135 -55
- package/src/runtime/metadata.ts +5 -4
- package/src/runtime/sessions.ts +316 -111
- package/src/runtime/toaster.ts +160 -44
- package/src/runtime/ui.ts +23 -4
- package/src/session/context/context.ts +3 -1
- package/src/session/context/turn/message/message.ts +39 -1
- package/src/session/context/turn/turn.ts +2 -1
- package/src/session/conversation.ts +56 -31
- package/src/session/goal/goal.ts +3 -2
- package/src/session/goal/task.ts +4 -4
- package/src/session/session.ts +62 -15
- package/src/testing/fake-claude.ts +4 -2
- package/src/testing/fake-sessions.ts +94 -20
- package/src/testing.ts +27 -3
- package/src/utils/call-effects.ts +2 -2
- package/src/utils/disposable.ts +3 -0
- package/src/utils/matcher.ts +1 -1
- package/src/utils/paths.ts +6 -3
- package/testing/fake-claude.d.ts +1 -0
- package/testing/fake-claude.d.ts.map +1 -1
- package/testing/fake-claude.js +3 -1
- package/testing/fake-sessions.d.ts +8 -1
- package/testing/fake-sessions.d.ts.map +1 -1
- package/testing/fake-sessions.js +97 -18
- package/testing.d.ts +1 -0
- package/testing.d.ts.map +1 -1
- package/testing.js +23 -2
- package/utils/call-effects.d.ts +1 -0
- package/utils/call-effects.d.ts.map +1 -1
- package/utils/call-effects.js +1 -1
- package/utils/disposable.d.ts +4 -0
- package/utils/disposable.d.ts.map +1 -0
- package/utils/disposable.js +1 -0
- package/utils/matcher.js +1 -1
- package/utils/paths.d.ts +2 -1
- package/utils/paths.d.ts.map +1 -1
- package/utils/paths.js +6 -3
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import type { Args, Frozen } from 'claude-code'
|
|
2
|
+
import type { Disposable } from '../mod.js'
|
|
3
|
+
import { ContextEnd } from '../session/context/context.js'
|
|
4
|
+
import { Document } from '../session/context/turn/message/document.js'
|
|
5
|
+
import { Message, messagesOf, rowOfAppend, withTexts, type Unplaced } from '../session/context/turn/message/message.js'
|
|
6
|
+
import { Note } from '../session/context/turn/message/note.js'
|
|
7
|
+
import { Notice } from '../session/context/turn/message/notice.js'
|
|
8
|
+
import { Prompt } from '../session/context/turn/message/prompt.js'
|
|
9
|
+
import { Reply } from '../session/context/turn/message/reply.js'
|
|
10
|
+
import { senderOf } from '../session/context/turn/message/sender.js'
|
|
11
|
+
import { Skill } from '../session/context/turn/message/skill.js'
|
|
12
|
+
import { ToolCall } from '../session/context/turn/message/tool-call.js'
|
|
13
|
+
import { ToolResult } from '../session/context/turn/message/tool-result.js'
|
|
14
|
+
import { TurnEnd } from '../session/context/turn/turn.js'
|
|
15
|
+
import type { ConversationState, SessionChange, TaskData } from '../session/conversation.js'
|
|
16
|
+
import { TaskEnd, TaskStart } from '../session/goal/task.js'
|
|
17
|
+
import type { BeforeEvent } from '../session/session.js'
|
|
18
|
+
import type { Claude } from './claude.js'
|
|
19
|
+
import { cmodPluginName } from './dependencies.js'
|
|
20
|
+
import type { Router } from './router.js'
|
|
21
|
+
import { toolInputOf } from './tool-calls.js'
|
|
22
|
+
|
|
23
|
+
export type Place = Pick<ConversationState, 'session' | 'context' | 'contextOpenedAt' | 'turn' | 'tasks' | 'taskTurns'>
|
|
24
|
+
|
|
25
|
+
export type Decide = (event: BeforeEvent, change: SessionChange) => Promise<Partial<Answers> | undefined>
|
|
26
|
+
|
|
27
|
+
export type Answers = { readonly text: string; readonly refuse: string; readonly input: Readonly<Record<string, unknown>>; readonly result: string; readonly owner: { readonly model: string } }
|
|
28
|
+
|
|
29
|
+
export type BeforeHooks = { readonly place: () => Promise<Place>; readonly wants: (event: BeforeEvent) => Promise<boolean>; readonly decide: Decide }
|
|
30
|
+
|
|
31
|
+
const appended: readonly BeforeEvent[] = [Note, Reply, ToolResult, Notice]
|
|
32
|
+
|
|
33
|
+
const relayTag = /^(?:<cross-session-message[^>]*>\s*)?<cmod-relay>([\s\S]*)<\/cmod-relay>(?:\s*<\/cross-session-message>[\s\S]*)?$/
|
|
34
|
+
|
|
35
|
+
export function relayOf(e: Frozen<Args<'session.receive'>>): string | undefined {
|
|
36
|
+
const isFromCmod = e.origin.kind === 'peer' && !('teammate' in e.origin) && e.origin.plugin === cmodPluginName
|
|
37
|
+
return isFromCmod ? relayTag.exec(e.text)?.[1] : undefined
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function installBefore({ claude, router, hooks }: { readonly claude: Claude; readonly router: Router; readonly hooks: BeforeHooks }): Disposable {
|
|
41
|
+
const { place, wants, decide } = hooks
|
|
42
|
+
const now = () => new Date().toISOString()
|
|
43
|
+
const asked = async (event: BeforeEvent, change: SessionChange) => (await decide(event, change)) ?? {}
|
|
44
|
+
const message = (where: Place, unplaced: Unplaced, task: string | undefined, isInTurn = true): SessionChange => {
|
|
45
|
+
const turn = isInTurn ? (task === undefined ? where.turn : where.taskTurns[task]) : undefined
|
|
46
|
+
const context = task === undefined ? where.context : (where.tasks[task]?.context ?? where.context)
|
|
47
|
+
return { type: Message, message: { ...unplaced, session: where.session, context, ...(turn === undefined ? {} : { turn: turn.prompt.id }), ...(task === undefined ? {} : { task }) } as Message }
|
|
48
|
+
}
|
|
49
|
+
const added: Disposable[] = [
|
|
50
|
+
router.add('prompt.submit', async (e, next) => {
|
|
51
|
+
if (!(await wants(Prompt)) || e.text.startsWith('/')) return next(e)
|
|
52
|
+
const where = await place()
|
|
53
|
+
const answer = await asked(Prompt, message(where, { id: '', at: now(), type: Prompt, text: e.text, from: senderOf(e.origin, false) }, undefined, false))
|
|
54
|
+
if (answer.refuse !== undefined) return { drop: answer.refuse }
|
|
55
|
+
return next(answer.text === undefined ? e : { ...e, text: answer.text })
|
|
56
|
+
}),
|
|
57
|
+
router.add('command.run', async (e, next) => {
|
|
58
|
+
if (!(await wants(Prompt))) return next(e)
|
|
59
|
+
const typed = e.args === '' ? `/${e.command}` : `/${e.command} ${e.args}`
|
|
60
|
+
const where = await place()
|
|
61
|
+
const answer = await asked(Prompt, message(where, { id: '', at: now(), type: Prompt, text: typed, from: senderOf(e.origin, false) }, undefined, false))
|
|
62
|
+
if (answer.refuse !== undefined) return { text: answer.refuse }
|
|
63
|
+
if (answer.text === undefined || answer.text === typed) return next(e)
|
|
64
|
+
const same = new RegExp(`^/${e.command}(?:\\s+|$)`).exec(answer.text)
|
|
65
|
+
if (same !== null) return next({ ...e, args: answer.text.slice(same[0].length) })
|
|
66
|
+
void claude.prompt.submit({ text: answer.text })
|
|
67
|
+
return {}
|
|
68
|
+
}),
|
|
69
|
+
router.add('session.receive', async (e, next) => {
|
|
70
|
+
if (!(await wants(Prompt)) || relayOf(e) !== undefined) return next(e)
|
|
71
|
+
const where = await place()
|
|
72
|
+
const answer = await asked(Prompt, message(where, { id: '', at: now(), type: Prompt, text: e.text, from: { type: 'session' } }, e.agentId, false))
|
|
73
|
+
if (answer.refuse !== undefined) return { consumed: answer.refuse }
|
|
74
|
+
return next(answer.text === undefined ? e : { ...e, text: answer.text })
|
|
75
|
+
}),
|
|
76
|
+
router.add('skill.prompt', async (e, next) => {
|
|
77
|
+
if (!(await wants(Skill))) return next(e)
|
|
78
|
+
const where = await place()
|
|
79
|
+
const answer = await asked(Skill, message(where, { id: '', at: now(), type: Skill, name: e.skill, text: e.text }, undefined))
|
|
80
|
+
return answer.text === undefined ? next(e) : { text: answer.text }
|
|
81
|
+
}),
|
|
82
|
+
router.add('prompt.attachment', async (e, next) => {
|
|
83
|
+
if (!(await wants(Document)) || e.type !== 'nested_memory') return next(e)
|
|
84
|
+
const where = await place()
|
|
85
|
+
const path = /^Contents of (.+?) \(/.exec(e.text)?.[1] ?? ''
|
|
86
|
+
const answer = await asked(Document, message(where, { id: '', at: now(), type: Document, path, text: e.text }, e.agentId))
|
|
87
|
+
if (answer.refuse !== undefined) return { text: null }
|
|
88
|
+
return answer.text === undefined ? next(e) : { text: answer.text }
|
|
89
|
+
}),
|
|
90
|
+
router.add('session.append', async (e, next) => {
|
|
91
|
+
const wanted = new Set((await Promise.all(appended.map(async (event) => ((await wants(event)) ? [event] : [])))).flat())
|
|
92
|
+
if (wanted.size === 0) return next(e)
|
|
93
|
+
const row = rowOfAppend({ uuid: e.uuid, door: e.door, origin: e.origin, message: e.message }, now(), '')
|
|
94
|
+
const found = messagesOf(row, e.agentId !== undefined)
|
|
95
|
+
if (!found.some((each) => wanted.has(each.type))) return next(e)
|
|
96
|
+
const where = await place()
|
|
97
|
+
const texts = new Map<number, string>()
|
|
98
|
+
for (const [part, unplaced] of found.entries()) {
|
|
99
|
+
if (!wanted.has(unplaced.type)) continue
|
|
100
|
+
const answer = await asked(unplaced.type, message(where, unplaced, e.agentId))
|
|
101
|
+
if (answer.text !== undefined) texts.set(part, answer.text)
|
|
102
|
+
}
|
|
103
|
+
return next(texts.size === 0 ? e : { ...e, message: { ...e.message, content: withTexts(row, texts) as typeof e.message.content } })
|
|
104
|
+
}),
|
|
105
|
+
router.add('tool.call', async (e, next) => {
|
|
106
|
+
if (!(await wants(ToolCall))) return next(e)
|
|
107
|
+
const where = await place()
|
|
108
|
+
const input = toolInputOf(e)
|
|
109
|
+
const answer = await asked(ToolCall, message(where, { id: e.tool_use_id, at: now(), type: ToolCall, callId: e.tool_use_id, name: e.tool, input, cwd: await claude.session.cwd() }, e.agentId))
|
|
110
|
+
if (answer.refuse !== undefined) return { deny: answer.refuse }
|
|
111
|
+
if (answer.result !== undefined) return { result: answer.result }
|
|
112
|
+
if (answer.input === undefined) return next(e)
|
|
113
|
+
const reserved = Object.fromEntries(Object.entries(e).filter(([key]) => !(key in input)))
|
|
114
|
+
return next({ ...reserved, ...answer.input } as Frozen<Args<'tool.call'>>)
|
|
115
|
+
}),
|
|
116
|
+
router.add('agent.spawn', async (e, next) => {
|
|
117
|
+
if (!(await wants(TaskStart))) return next(e)
|
|
118
|
+
const where = await place()
|
|
119
|
+
const task: TaskData = { 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 }) }
|
|
120
|
+
const answer = await asked(TaskStart, { type: TaskStart, task: { ...task, context: where.context } })
|
|
121
|
+
if (answer.refuse !== undefined) return { deny: answer.refuse }
|
|
122
|
+
return next(answer.owner === undefined ? e : { ...e, model: answer.owner.model })
|
|
123
|
+
}),
|
|
124
|
+
router.add('classic.SubagentStop', async (e, next) => {
|
|
125
|
+
if (!(await wants(TaskEnd)) && !(await wants(TurnEnd))) return next(e)
|
|
126
|
+
const where = await place()
|
|
127
|
+
const task = where.tasks[e.agent_id]
|
|
128
|
+
if (task === undefined) return next(e)
|
|
129
|
+
const turn = where.taskTurns[e.agent_id]
|
|
130
|
+
const ending = (await wants(TaskEnd)) ? await asked(TaskEnd, { type: TaskEnd, task: { ...task, closedAt: now() } }) : {}
|
|
131
|
+
const refuse = ending.refuse ?? ((await wants(TurnEnd)) && turn !== undefined ? (await asked(TurnEnd, { type: TurnEnd, turn: { ...turn, closedAt: now() } })).refuse : undefined)
|
|
132
|
+
const below = await next(e)
|
|
133
|
+
return refuse === undefined ? below : { ...below, block: refuse }
|
|
134
|
+
}),
|
|
135
|
+
router.add('classic.Stop', async (e, next) => {
|
|
136
|
+
if (!(await wants(TurnEnd))) return next(e)
|
|
137
|
+
const where = await place()
|
|
138
|
+
if (where.turn === undefined) return next(e)
|
|
139
|
+
const answer = await asked(TurnEnd, { type: TurnEnd, turn: { ...where.turn, closedAt: now() } })
|
|
140
|
+
const below = await next(e)
|
|
141
|
+
return answer.refuse === undefined ? below : { ...below, block: answer.refuse }
|
|
142
|
+
}),
|
|
143
|
+
router.add('session.compact', async (e, next) => {
|
|
144
|
+
if (!(await wants(ContextEnd)) || e.trigger === 'precompute') return next(e)
|
|
145
|
+
const where = await place()
|
|
146
|
+
const task = e.agentId === undefined ? undefined : where.tasks[e.agentId]
|
|
147
|
+
if (e.agentId !== undefined && task === undefined) return next(e)
|
|
148
|
+
const context = task === undefined ? { number: where.context, openedAt: where.contextOpenedAt } : { number: task.context, openedAt: task.contextOpenedAt ?? task.openedAt, task: task.id }
|
|
149
|
+
const answer = await asked(ContextEnd, { type: ContextEnd, context: { ...context, closedAt: now() } })
|
|
150
|
+
return answer.refuse === undefined ? next(e) : { skip: answer.refuse }
|
|
151
|
+
}),
|
|
152
|
+
]
|
|
153
|
+
return { dispose: () => added.forEach((each) => each.dispose()) }
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export function withAnswer(change: SessionChange, answer: Partial<Answers>): SessionChange {
|
|
157
|
+
if (change.type === Message && answer.text !== undefined) return { ...change, message: { ...change.message, text: answer.text } as Message }
|
|
158
|
+
if (change.type === Message && answer.input !== undefined && change.message.type === ToolCall) return { ...change, message: { ...change.message, input: answer.input } }
|
|
159
|
+
if (change.type === TaskStart && answer.owner !== undefined) return { ...change, task: { ...change.task, model: answer.owner.model } }
|
|
160
|
+
return change
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export async function foldAnswers(event: BeforeEvent, change: SessionChange, askers: readonly ((change: SessionChange) => Promise<unknown>)[], report: (error: unknown) => void): Promise<Partial<Answers> | undefined> {
|
|
164
|
+
let latest = change
|
|
165
|
+
let result: Partial<Answers> | undefined
|
|
166
|
+
for (const ask of askers) {
|
|
167
|
+
try {
|
|
168
|
+
const answer = answerOf(event, await ask(latest))
|
|
169
|
+
if (answer === undefined) continue
|
|
170
|
+
result = { ...result, ...answer }
|
|
171
|
+
if (answer.refuse !== undefined || answer.result !== undefined) return result
|
|
172
|
+
latest = withAnswer(latest, answer)
|
|
173
|
+
} catch (error) {
|
|
174
|
+
report(error)
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return result
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export function answerOf(event: BeforeEvent, answer: unknown): Partial<Answers> | undefined {
|
|
181
|
+
if (typeof answer !== 'object' || answer === null) return undefined
|
|
182
|
+
const given = answer as Partial<Answers>
|
|
183
|
+
const allowed: Readonly<Record<BeforeEvent, readonly (keyof Answers)[]>> = {
|
|
184
|
+
prompt: ['text', 'refuse'],
|
|
185
|
+
note: ['text'],
|
|
186
|
+
skill: ['text'],
|
|
187
|
+
document: ['text', 'refuse'],
|
|
188
|
+
reply: ['text'],
|
|
189
|
+
toolCall: ['input', 'result', 'refuse'],
|
|
190
|
+
toolResult: ['text'],
|
|
191
|
+
notice: ['text'],
|
|
192
|
+
taskStart: ['owner', 'refuse'],
|
|
193
|
+
taskEnd: ['refuse'],
|
|
194
|
+
turnEnd: ['refuse'],
|
|
195
|
+
contextEnd: ['refuse'],
|
|
196
|
+
}
|
|
197
|
+
const unread = Object.keys(given).filter((key) => !allowed[event].includes(key as keyof Answers))
|
|
198
|
+
if (unread.length > 0) throw new Error(`a before(${event}) listener answered ${unread.join(' and ')}, which ${event} does not take. It takes ${allowed[event].map((key) => `{ ${key} }`).join(', ')}.`)
|
|
199
|
+
return given
|
|
200
|
+
}
|
package/src/runtime/claude.ts
CHANGED
|
@@ -30,6 +30,8 @@ import type {
|
|
|
30
30
|
Timer,
|
|
31
31
|
ToolSpec,
|
|
32
32
|
UiCopyArgs,
|
|
33
|
+
UiFocusArgs,
|
|
34
|
+
UiFocusResult,
|
|
33
35
|
UiCopyResult,
|
|
34
36
|
UiLogOptions,
|
|
35
37
|
UiOpenResult,
|
|
@@ -51,6 +53,7 @@ export type Claude = {
|
|
|
51
53
|
panes(): Promise<readonly UiPane[]>
|
|
52
54
|
scroll(args: UiScrollArgs): Promise<UiScrollResult>
|
|
53
55
|
copy(args: UiCopyArgs): Promise<UiCopyResult>
|
|
56
|
+
focus(args: UiFocusArgs): Promise<UiFocusResult>
|
|
54
57
|
resolve(e: ResolveInput): ElementTable<RenderSurface>
|
|
55
58
|
}
|
|
56
59
|
readonly process: {
|