@cmodjs/core 2.0.1 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/docs/commands.md +2 -2
- package/docs/jobs.md +1 -1
- package/docs/mod.md +4 -3
- package/docs/permissions.md +1 -1
- package/docs/session.md +12 -2
- package/docs/testing.md +6 -3
- package/jobs/context.d.ts.map +1 -1
- package/jobs/context.js +2 -1
- package/jobs/permissions.d.ts.map +1 -1
- package/jobs/permissions.js +5 -2
- package/jobs/prompt.d.ts.map +1 -1
- package/jobs/prompt.js +2 -1
- package/jobs/slash-command.d.ts.map +1 -1
- package/jobs/slash-command.js +4 -3
- package/package.json +1 -1
- package/records.d.ts +1 -1
- package/records.js +1 -1
- package/register.d.ts.map +1 -1
- package/register.js +58 -51
- package/runtime/before.d.ts +3 -2
- package/runtime/before.d.ts.map +1 -1
- package/runtime/before.js +10 -7
- package/runtime/conversations.d.ts.map +1 -1
- package/runtime/conversations.js +46 -19
- package/runtime/engine.d.ts +1 -0
- package/runtime/engine.d.ts.map +1 -1
- package/runtime/hooks.d.ts +2 -2
- package/runtime/hooks.d.ts.map +1 -1
- package/runtime/hooks.js +7 -1
- package/runtime/lifecycle.d.ts +2 -1
- package/runtime/lifecycle.d.ts.map +1 -1
- package/runtime/lifecycle.js +33 -15
- package/runtime/permission-check.d.ts +3 -0
- package/runtime/permission-check.d.ts.map +1 -1
- package/runtime/permission-check.js +19 -6
- package/runtime/router.d.ts +3 -2
- package/runtime/router.d.ts.map +1 -1
- package/runtime/router.js +19 -12
- package/runtime/sessions.d.ts +14 -4
- package/runtime/sessions.d.ts.map +1 -1
- package/runtime/sessions.js +72 -28
- package/runtime/toaster.d.ts.map +1 -1
- package/runtime/toaster.js +3 -0
- package/session/context/turn/message/message.d.ts.map +1 -1
- package/session/context/turn/message/message.js +30 -2
- package/session/context/turn/message/note.d.ts +7 -0
- package/session/context/turn/message/note.d.ts.map +1 -1
- package/session/context/turn/message/note.js +12 -0
- package/session/session.d.ts +3 -1
- package/session/session.d.ts.map +1 -1
- package/src/jobs/context.ts +2 -1
- package/src/jobs/permissions.ts +11 -5
- package/src/jobs/prompt.ts +2 -1
- package/src/jobs/slash-command.ts +4 -3
- package/src/records.ts +1 -1
- package/src/register.ts +63 -52
- package/src/runtime/before.ts +13 -9
- package/src/runtime/conversations.ts +54 -27
- package/src/runtime/engine.ts +1 -0
- package/src/runtime/hooks.ts +9 -3
- package/src/runtime/lifecycle.ts +39 -19
- package/src/runtime/permission-check.ts +44 -22
- package/src/runtime/router.ts +23 -15
- package/src/runtime/sessions.ts +96 -38
- package/src/runtime/toaster.ts +2 -0
- package/src/session/context/turn/message/message.ts +27 -2
- package/src/session/context/turn/message/note.ts +16 -0
- package/src/session/session.ts +1 -2
- package/src/testing/fake-claude.ts +4 -3
- package/src/testing.ts +19 -12
- package/testing/fake-claude.d.ts +1 -0
- package/testing/fake-claude.d.ts.map +1 -1
- package/testing/fake-claude.js +3 -2
- package/testing.d.ts +7 -2
- package/testing.d.ts.map +1 -1
- package/testing.js +15 -9
package/src/register.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Args, EngineInterface, EventResult, Frozen, Next, On, PluginOptions, ToolCallArgs } from 'claude-code'
|
|
1
|
+
import type { Args, Caught, EngineInterface, EventResult, Frozen, Next, On, PluginOptions, ToolCallArgs } from 'claude-code'
|
|
2
2
|
import type { ModDefinition } from './mod.js'
|
|
3
3
|
import type { Options } from './options.js'
|
|
4
4
|
import type { Claude } from './runtime/claude.js'
|
|
@@ -94,9 +94,20 @@ function routeToMod<N extends RoutedEvent>(_$: EngineInterface, eventInput: Froz
|
|
|
94
94
|
return lifecycle.route(passOn.event, eventInput, (passed: unknown) => passOn(passed as Args<N>) as Promise<EventResult<N>>)
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
+
function routeFailure<N extends RoutedEvent>(_$: EngineInterface, eventInput: Frozen<Args<N>>, passOn: Next<N> & Caught): Promise<EventResult<N>> {
|
|
98
|
+
return lifecycle.caught(passOn.event, eventInput, (passed: unknown) => passOn(passed as Args<N>) as Promise<EventResult<N>>, passOn.error)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const withCaller = (eventInput: Frozen<Args<'cmod.call'>>, passOn: Next<'cmod.call'>) => ({ ...eventInput, caller: passOn.origin.plugin }) as Frozen<Args<'cmod.call'>>
|
|
102
|
+
|
|
103
|
+
const withoutCaller = (passOn: Next<'cmod.call'>) => (passed: Frozen<Args<'cmod.call'>>) => passOn(Object.fromEntries(Object.entries(passed).filter(([key]) => key !== 'caller')) as Args<'cmod.call'>)
|
|
104
|
+
|
|
97
105
|
function routeCallToMod(_$: EngineInterface, eventInput: Frozen<Args<'cmod.call'>>, passOn: Next<'cmod.call'>): Promise<EventResult<'cmod.call'>> {
|
|
98
|
-
|
|
99
|
-
|
|
106
|
+
return lifecycle.route('cmod.call', withCaller(eventInput, passOn), withoutCaller(passOn))
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function routeCallFailure(_$: EngineInterface, eventInput: Frozen<Args<'cmod.call'>>, passOn: Next<'cmod.call'> & Caught): Promise<EventResult<'cmod.call'>> {
|
|
110
|
+
return lifecycle.caught('cmod.call', withCaller(eventInput, passOn), withoutCaller(passOn), passOn.error)
|
|
100
111
|
}
|
|
101
112
|
|
|
102
113
|
export function registerMod<State extends object, Declared extends Options>(addHook: On, definition: ModDefinition<State, string, Declared>, options: PluginOptions): void {
|
|
@@ -104,58 +115,58 @@ export function registerMod<State extends object, Declared extends Options>(addH
|
|
|
104
115
|
registered = definition as ModDefinition<object>
|
|
105
116
|
lifecycle = createLifecycle(definition, () => permissionCheck, options)
|
|
106
117
|
addHook('session.start', startMod)
|
|
107
|
-
addHook('classic.Setup', routeToMod)
|
|
108
|
-
addHook('classic.SessionStart', routeToMod)
|
|
109
|
-
addHook('classic.SessionEnd', routeToMod)
|
|
110
|
-
addHook('classic.UserPromptSubmit', routeToMod)
|
|
111
|
-
addHook('classic.UserPromptExpansion', routeToMod)
|
|
112
|
-
addHook('classic.InstructionsLoaded', routeToMod)
|
|
113
|
-
addHook('classic.ConfigChange', routeToMod)
|
|
114
|
-
addHook('classic.DirectoryAdded', routeToMod)
|
|
115
|
-
addHook('classic.PreModelSwitch', routeToMod)
|
|
116
|
-
addHook('classic.PostModelSwitch', routeToMod)
|
|
117
|
-
addHook('classic.PermissionDenied', routeToMod)
|
|
118
|
-
addHook('classic.PostToolUse', routeToMod)
|
|
119
|
-
addHook('classic.PostToolUseFailure', routeToMod)
|
|
120
|
-
addHook('classic.PostToolBatch', routeToMod)
|
|
121
|
-
addHook('classic.MessageDisplay', routeToMod)
|
|
122
|
-
addHook('classic.SubagentStart', routeToMod)
|
|
123
|
-
addHook('classic.SubagentStop', routeToMod)
|
|
124
|
-
addHook('classic.TaskCreated', routeToMod)
|
|
125
|
-
addHook('classic.TaskCompleted', routeToMod)
|
|
126
|
-
addHook('classic.TeammateIdle', routeToMod)
|
|
127
|
-
addHook('classic.Elicitation', routeToMod)
|
|
128
|
-
addHook('classic.ElicitationResult', routeToMod)
|
|
129
|
-
addHook('classic.Notification', routeToMod)
|
|
130
|
-
addHook('classic.PreCompact', routeToMod)
|
|
131
|
-
addHook('classic.PostCompact', routeToMod)
|
|
132
|
-
addHook('classic.Stop', routeToMod)
|
|
133
|
-
addHook('classic.StopFailure', routeToMod)
|
|
134
|
-
addHook('classic.FileChanged', routeToMod)
|
|
135
|
-
addHook('classic.WorktreeCreate', routeToMod)
|
|
136
|
-
addHook('classic.WorktreeRemove', routeToMod)
|
|
137
|
-
addHook('tool.call', routeToMod)
|
|
138
|
-
addHook('prompt.submit', routeToMod)
|
|
139
|
-
addHook('prompt.context', routeToMod)
|
|
140
|
-
addHook('command.run', routeToMod)
|
|
141
|
-
addHook('session.measure', routeToMod)
|
|
142
|
-
addHook('skill.prompt', routeToMod)
|
|
143
|
-
addHook('ui.render', routeToMod)
|
|
144
|
-
addHook('ui.press', routeToMod)
|
|
145
|
-
addHook('ui.scroll', routeToMod)
|
|
146
|
-
addHook('ui.close', routeToMod)
|
|
147
|
-
addHook('cmod.call', routeCallToMod)
|
|
118
|
+
addHook('classic.Setup', routeToMod).catch(routeFailure)
|
|
119
|
+
addHook('classic.SessionStart', routeToMod).catch(routeFailure)
|
|
120
|
+
addHook('classic.SessionEnd', routeToMod).catch(routeFailure)
|
|
121
|
+
addHook('classic.UserPromptSubmit', routeToMod).catch(routeFailure)
|
|
122
|
+
addHook('classic.UserPromptExpansion', routeToMod).catch(routeFailure)
|
|
123
|
+
addHook('classic.InstructionsLoaded', routeToMod).catch(routeFailure)
|
|
124
|
+
addHook('classic.ConfigChange', routeToMod).catch(routeFailure)
|
|
125
|
+
addHook('classic.DirectoryAdded', routeToMod).catch(routeFailure)
|
|
126
|
+
addHook('classic.PreModelSwitch', routeToMod).catch(routeFailure)
|
|
127
|
+
addHook('classic.PostModelSwitch', routeToMod).catch(routeFailure)
|
|
128
|
+
addHook('classic.PermissionDenied', routeToMod).catch(routeFailure)
|
|
129
|
+
addHook('classic.PostToolUse', routeToMod).catch(routeFailure)
|
|
130
|
+
addHook('classic.PostToolUseFailure', routeToMod).catch(routeFailure)
|
|
131
|
+
addHook('classic.PostToolBatch', routeToMod).catch(routeFailure)
|
|
132
|
+
addHook('classic.MessageDisplay', routeToMod).catch(routeFailure)
|
|
133
|
+
addHook('classic.SubagentStart', routeToMod).catch(routeFailure)
|
|
134
|
+
addHook('classic.SubagentStop', routeToMod).catch(routeFailure)
|
|
135
|
+
addHook('classic.TaskCreated', routeToMod).catch(routeFailure)
|
|
136
|
+
addHook('classic.TaskCompleted', routeToMod).catch(routeFailure)
|
|
137
|
+
addHook('classic.TeammateIdle', routeToMod).catch(routeFailure)
|
|
138
|
+
addHook('classic.Elicitation', routeToMod).catch(routeFailure)
|
|
139
|
+
addHook('classic.ElicitationResult', routeToMod).catch(routeFailure)
|
|
140
|
+
addHook('classic.Notification', routeToMod).catch(routeFailure)
|
|
141
|
+
addHook('classic.PreCompact', routeToMod).catch(routeFailure)
|
|
142
|
+
addHook('classic.PostCompact', routeToMod).catch(routeFailure)
|
|
143
|
+
addHook('classic.Stop', routeToMod).catch(routeFailure)
|
|
144
|
+
addHook('classic.StopFailure', routeToMod).catch(routeFailure)
|
|
145
|
+
addHook('classic.FileChanged', routeToMod).catch(routeFailure)
|
|
146
|
+
addHook('classic.WorktreeCreate', routeToMod).catch(routeFailure)
|
|
147
|
+
addHook('classic.WorktreeRemove', routeToMod).catch(routeFailure)
|
|
148
|
+
addHook('tool.call', routeToMod).catch(routeFailure)
|
|
149
|
+
addHook('prompt.submit', routeToMod).catch(routeFailure)
|
|
150
|
+
addHook('prompt.context', routeToMod).catch(routeFailure)
|
|
151
|
+
addHook('command.run', routeToMod).catch(routeFailure)
|
|
152
|
+
addHook('session.measure', routeToMod).catch(routeFailure)
|
|
153
|
+
addHook('skill.prompt', routeToMod).catch(routeFailure)
|
|
154
|
+
addHook('ui.render', routeToMod).catch(routeFailure)
|
|
155
|
+
addHook('ui.press', routeToMod).catch(routeFailure)
|
|
156
|
+
addHook('ui.scroll', routeToMod).catch(routeFailure)
|
|
157
|
+
addHook('ui.close', routeToMod).catch(routeFailure)
|
|
158
|
+
addHook('cmod.call', routeCallToMod).catch(routeCallFailure)
|
|
159
|
+
addHook('session.compact', routeToMod).catch(routeFailure)
|
|
148
160
|
if (definition.name !== cmodPluginName) return
|
|
149
|
-
addHook('session.append', routeToMod)
|
|
150
|
-
addHook('session.
|
|
151
|
-
addHook('
|
|
152
|
-
addHook('
|
|
153
|
-
addHook('
|
|
154
|
-
addHook('turn.start', routeToMod)
|
|
161
|
+
addHook('session.append', routeToMod).catch(routeFailure)
|
|
162
|
+
addHook('session.receive', routeToMod).catch(routeFailure)
|
|
163
|
+
addHook('agent.spawn', routeToMod).catch(routeFailure)
|
|
164
|
+
addHook('prompt.attachment', routeToMod).catch(routeFailure)
|
|
165
|
+
addHook('turn.start', routeToMod).catch(routeFailure)
|
|
155
166
|
}
|
|
156
167
|
|
|
157
168
|
export function registerPermissionCheck(addHook: On): void {
|
|
158
169
|
permissionCheck = permissionRoutes
|
|
159
|
-
addHook('tool.check', routeToMod)
|
|
160
|
-
addHook('classic.PermissionRequest', routeToMod)
|
|
170
|
+
addHook('tool.check', routeToMod).catch(routeFailure)
|
|
171
|
+
addHook('classic.PermissionRequest', routeToMod).catch(routeFailure)
|
|
161
172
|
}
|
package/src/runtime/before.ts
CHANGED
|
@@ -3,7 +3,7 @@ import type { Disposable } from '../mod.js'
|
|
|
3
3
|
import { ContextEnd } from '../session/context/context.js'
|
|
4
4
|
import { InstructionFile, isRulePath } from '../session/context/turn/message/instruction-file.js'
|
|
5
5
|
import { instructionPathOf, Message, messagesOf, noFiles, rowOfAppend, withTexts, type Unplaced } from '../session/context/turn/message/message.js'
|
|
6
|
-
import { Note } from '../session/context/turn/message/note.js'
|
|
6
|
+
import { Note, untagged } from '../session/context/turn/message/note.js'
|
|
7
7
|
import { Notice } from '../session/context/turn/message/notice.js'
|
|
8
8
|
import { Prompt } from '../session/context/turn/message/prompt.js'
|
|
9
9
|
import { Reply } from '../session/context/turn/message/reply.js'
|
|
@@ -12,7 +12,7 @@ import { Skill } from '../session/context/turn/message/skill.js'
|
|
|
12
12
|
import { ToolCall } from '../session/context/turn/message/tool-call.js'
|
|
13
13
|
import { ToolResult } from '../session/context/turn/message/tool-result.js'
|
|
14
14
|
import { TurnEnd } from '../session/context/turn/turn.js'
|
|
15
|
-
import type { ConversationState, SessionChange, TaskData } from '../session/conversation.js'
|
|
15
|
+
import type { ContextData, ConversationState, RunningTask, SessionChange, TaskData } from '../session/conversation.js'
|
|
16
16
|
import { Task, TaskStart } from '../session/goal/task.js'
|
|
17
17
|
import { sessionEvents, type BeforeEvent, type SessionEvent } from '../session/session.js'
|
|
18
18
|
import type { Claude } from './claude.js'
|
|
@@ -37,7 +37,7 @@ export type Answers = {
|
|
|
37
37
|
readonly owner: { readonly model: string }
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
export type BeforeHooks = { readonly place: () => Promise<Place>; readonly wants: (event: BeforeEvent) => Promise<boolean>; readonly
|
|
40
|
+
export type BeforeHooks = { readonly place: () => Promise<Place>; readonly wants: (event: BeforeEvent) => Promise<boolean>; readonly fold: () => Decide }
|
|
41
41
|
|
|
42
42
|
const appended: readonly BeforeEvent[] = [Note, Reply, Notice]
|
|
43
43
|
|
|
@@ -56,9 +56,9 @@ export function relayOf(e: Frozen<Args<'session.receive'>>): string | undefined
|
|
|
56
56
|
}
|
|
57
57
|
|
|
58
58
|
export function installBefore({ claude, router, hooks }: { readonly claude: Claude; readonly router: Router; readonly hooks: BeforeHooks }): Disposable {
|
|
59
|
-
const { place, wants,
|
|
59
|
+
const { place, wants, fold } = hooks
|
|
60
60
|
const now = () => new Date().toISOString()
|
|
61
|
-
const asked = async (event: BeforeEvent, change: SessionChange) => (await decide(event, change)) ?? {}
|
|
61
|
+
const asked = async (event: BeforeEvent, change: SessionChange, decide: Decide = fold()) => (await decide(event, change)) ?? {}
|
|
62
62
|
const message = (where: Place, unplaced: Unplaced, task: string | undefined, isInTurn = true): SessionChange => {
|
|
63
63
|
const turn = isInTurn ? (task === undefined ? where.turn : where.taskTurns[task]) : undefined
|
|
64
64
|
const context = task === undefined ? where.context : (where.tasks[task]?.context ?? where.context)
|
|
@@ -79,7 +79,7 @@ export function installBefore({ claude, router, hooks }: { readonly claude: Clau
|
|
|
79
79
|
const answer = await asked(ToolResult, message(where, result, task))
|
|
80
80
|
const below = await next(e as never)
|
|
81
81
|
const rewritten = answer.output === undefined || isFailure ? {} : name.startsWith('mcp__') ? { updatedMCPToolOutput: answer.output } : { updatedToolOutput: answer.output }
|
|
82
|
-
const failed = answer.isError === true && !isFailure ? { block: answer.note
|
|
82
|
+
const failed = answer.isError === true && !isFailure ? { block: answer.note === undefined ? `${name} failed.` : untagged(answer.note) } : {}
|
|
83
83
|
return { ...withNote(below, answer.isError === true ? undefined : answer.note), ...rewritten, ...failed }
|
|
84
84
|
}
|
|
85
85
|
const added: Disposable[] = [
|
|
@@ -136,9 +136,10 @@ export function installBefore({ claude, router, hooks }: { readonly claude: Clau
|
|
|
136
136
|
if (!found.some((each) => wanted.has(each.type))) return next(e)
|
|
137
137
|
const where = await place()
|
|
138
138
|
const texts = new Map<number, string>()
|
|
139
|
+
const decide = fold()
|
|
139
140
|
for (const [part, unplaced] of found.entries()) {
|
|
140
141
|
if (!wanted.has(unplaced.type)) continue
|
|
141
|
-
const answer = await asked(unplaced.type, message(where, unplaced, e.agentId))
|
|
142
|
+
const answer = await asked(unplaced.type, message(where, unplaced, e.agentId), decide)
|
|
142
143
|
if (answer.text !== undefined) texts.set(part, answer.text)
|
|
143
144
|
}
|
|
144
145
|
return next(texts.size === 0 ? e : { ...e, message: { ...e.message, content: withTexts(row, texts) as typeof e.message.content } })
|
|
@@ -210,14 +211,17 @@ export function installBefore({ claude, router, hooks }: { readonly claude: Clau
|
|
|
210
211
|
const where = await place()
|
|
211
212
|
const task = e.agentId === undefined ? undefined : where.tasks[e.agentId]
|
|
212
213
|
if (e.agentId !== undefined && task === undefined) return next(e)
|
|
213
|
-
const
|
|
214
|
-
const answer = await asked(ContextEnd, { type: ContextEnd, context: { ...context, closedAt: now() } })
|
|
214
|
+
const answer = await asked(ContextEnd, { type: ContextEnd, context: closingContext(where, task, now()) })
|
|
215
215
|
return answer.refuse === undefined ? next(e) : { skip: answer.refuse }
|
|
216
216
|
}),
|
|
217
217
|
]
|
|
218
218
|
return { dispose: () => added.forEach((each) => each.dispose()) }
|
|
219
219
|
}
|
|
220
220
|
|
|
221
|
+
export function closingContext(where: Place, task: RunningTask | undefined, closedAt: string): ContextData {
|
|
222
|
+
return task === undefined ? { number: where.context, openedAt: where.contextOpenedAt, closedAt } : { number: task.context, openedAt: task.contextOpenedAt ?? task.openedAt, closedAt, task: task.id }
|
|
223
|
+
}
|
|
224
|
+
|
|
221
225
|
export function withAnswer(change: SessionChange, answer: Partial<Answers>): SessionChange {
|
|
222
226
|
if (change.type === Message && answer.output !== undefined && change.message.type === ToolResult) return { ...change, message: { ...change.message, output: answer.output } }
|
|
223
227
|
if (change.type === Message && answer.text !== undefined) return { ...change, message: { ...change.message, text: answer.text } as Message }
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Args, EventResult, Frozen } from 'claude-code'
|
|
2
2
|
import { consentPath, dataFolder, isObject, parseConsent } from '../records.js'
|
|
3
3
|
import { Message, rowOfAppend, type Row } from '../session/context/turn/message/message.js'
|
|
4
|
-
import { Note } from '../session/context/turn/message/note.js'
|
|
4
|
+
import { modNote, Note } from '../session/context/turn/message/note.js'
|
|
5
5
|
import { Person } from '../session/context/turn/message/sender.js'
|
|
6
6
|
import { createConversation, emptyConversation, 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'
|
|
@@ -9,18 +9,19 @@ import { GoalChange } from '../session/goal/goal.js'
|
|
|
9
9
|
import { receivedMatcher } from '../utils/matcher.js'
|
|
10
10
|
import { ifMissing } from '../utils/paths.js'
|
|
11
11
|
import { formatExit, messageOf } from '../utils/text.js'
|
|
12
|
-
import { foldBefore, installBefore, relayOf, type Place } from './before.js'
|
|
12
|
+
import { beforeAnswerKeys, foldBefore, installBefore, relayOf, type Place } from './before.js'
|
|
13
13
|
import type { Claude } from './claude.js'
|
|
14
14
|
import { cmodPluginName } from './dependencies.js'
|
|
15
15
|
import { readLines, runCmod } from './programs.js'
|
|
16
|
-
import type { Router } from './router.js'
|
|
17
|
-
import {
|
|
16
|
+
import type { RoutedHook, Router } from './router.js'
|
|
17
|
+
import { PassedTimeout } from './deadline.js'
|
|
18
|
+
import { actPermission, conversationMethods, everySession, isAct, isHeardBy, isInScope, resultOf, spawnTask, usageOf, type Act, type Subscription } from './sessions.js'
|
|
18
19
|
|
|
19
20
|
type Request = Readonly<Record<string, unknown>>
|
|
20
21
|
|
|
21
22
|
type Heard = { readonly event: Subscription['event']; readonly matcher: unknown }
|
|
22
23
|
|
|
23
|
-
type Watch = { readonly mod: string; readonly target: string; readonly project: string; readonly events: readonly string[] }
|
|
24
|
+
type Watch = { readonly mod: string; readonly target: string; readonly project: string; readonly events: readonly string[]; readonly refusing?: readonly Subscription[] }
|
|
24
25
|
|
|
25
26
|
type Watcher = Watch & { readonly from: string }
|
|
26
27
|
|
|
@@ -37,6 +38,8 @@ const bufferedTaskRows = 200
|
|
|
37
38
|
|
|
38
39
|
const beforeDeadlineMs = 5000
|
|
39
40
|
|
|
41
|
+
const awayFoldMs = 8000
|
|
42
|
+
|
|
40
43
|
const livenessMs = 5000
|
|
41
44
|
|
|
42
45
|
|
|
@@ -185,7 +188,7 @@ export function serveConversations({ claude, router, store, project, report }: {
|
|
|
185
188
|
reject(error)
|
|
186
189
|
},
|
|
187
190
|
}
|
|
188
|
-
const timer = deadlineMs === undefined ? watchLife(settle, to) : claude.clock.after(deadlineMs, () => settle.reject(new
|
|
191
|
+
const timer = deadlineMs === undefined ? watchLife(settle, to) : claude.clock.after(deadlineMs, () => settle.reject(new PassedTimeout(`session ${to} did not answer within ${deadlineMs / 1000} s`)))
|
|
189
192
|
asked.set(id, settle)
|
|
190
193
|
post(to, { ...relay, id, from: session }).catch((error: unknown) => settle.reject(error instanceof Error ? error : new Error(messageOf(error))))
|
|
191
194
|
})
|
|
@@ -240,11 +243,24 @@ export function serveConversations({ claude, router, store, project, report }: {
|
|
|
240
243
|
return claude.cmod.call({ to: mod, method: conversationMethods.before, input: { session: from, event, change, isInProject } })
|
|
241
244
|
}
|
|
242
245
|
|
|
243
|
-
const
|
|
246
|
+
const isUnderHere = (task: string, ancestor: string) => {
|
|
247
|
+
for (let at: string | undefined = task, steps = 0; at !== undefined && steps < 64; at = conversation.state.tasks[at]?.parent, steps += 1) if (at === ancestor) return true
|
|
248
|
+
return false
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const refuses = (watcher: Watcher, event: BeforeEvent, change: SessionChange) =>
|
|
252
|
+
beforeAnswerKeys[event].includes('refuse') && (watcher.refusing ?? []).some(({ event: heard, matcher, scope }) => isInScope(scope ?? {}, session, change, isUnderHere) && isHeardBy(heard, matcher === undefined ? undefined : receivedMatcher(matcher), change))
|
|
253
|
+
|
|
254
|
+
const askAway = async (watcher: Watcher, event: BeforeEvent, change: SessionChange, endsAt: number) => {
|
|
244
255
|
try {
|
|
245
|
-
|
|
256
|
+
const leftMs = Math.min(beforeDeadlineMs, endsAt - (await claude.clock.now()))
|
|
257
|
+
if (leftMs <= 0) throw new PassedTimeout(`the ${awayFoldMs / 1000} s cmod waits on other sessions ran out before session ${watcher.from} was asked`)
|
|
258
|
+
return await ask(watcher.from, { kind: 'before', mod: watcher.mod, event, change, project: project() }, leftMs)
|
|
246
259
|
} catch (error) {
|
|
247
|
-
if (await isRunning(watcher.from))
|
|
260
|
+
if (await isRunning(watcher.from)) {
|
|
261
|
+
if (error instanceof PassedTimeout && refuses(watcher, event, change)) return { refuse: `${watcher.mod}: ${error.message}` }
|
|
262
|
+
throw error
|
|
263
|
+
}
|
|
248
264
|
watchers = watchers.filter((each) => each.from !== watcher.from)
|
|
249
265
|
claude.ui.log(`cmod: session ${watcher.from} ended without clearing its before listeners, so they are cleared now`, { to: 'debug' })
|
|
250
266
|
await claude.fs.write(`${watches}/${watcher.from}.json`, '[]')
|
|
@@ -264,13 +280,20 @@ export function serveConversations({ claude, router, store, project, report }: {
|
|
|
264
280
|
await Promise.all([starting, watching])
|
|
265
281
|
return deciders(event).length > 0
|
|
266
282
|
},
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
283
|
+
fold: () => {
|
|
284
|
+
let endsAt: Promise<number> | undefined
|
|
285
|
+
return (event, change) =>
|
|
286
|
+
foldBefore(
|
|
287
|
+
event,
|
|
288
|
+
change,
|
|
289
|
+
deciders(event).map((watcher) => async (latest: SessionChange) => {
|
|
290
|
+
if (watcher.from === session) return askHere(watcher.mod, event, latest)
|
|
291
|
+
endsAt ??= claude.clock.now().then((now) => now + awayFoldMs)
|
|
292
|
+
return askAway(watcher, event, latest, await endsAt)
|
|
293
|
+
}),
|
|
294
|
+
(error) => report(`before:${event}`, `Passed the ${event} on without a mod's answer`, error),
|
|
295
|
+
)
|
|
296
|
+
},
|
|
274
297
|
},
|
|
275
298
|
})
|
|
276
299
|
|
|
@@ -321,22 +344,24 @@ export function serveConversations({ claude, router, store, project, report }: {
|
|
|
321
344
|
return next(e)
|
|
322
345
|
})
|
|
323
346
|
|
|
324
|
-
|
|
347
|
+
const recordAppend: RoutedHook<'session.append'> = async (e, next) => {
|
|
325
348
|
const kept = await next(e)
|
|
326
349
|
if (kept.deny !== undefined || session === '') return kept
|
|
327
350
|
const cwd = e.message.type === 'assistant' ? await claude.session.cwd() : ''
|
|
328
351
|
void take(rowOfAppend({ uuid: kept.uuid, door: e.door, origin: e.origin, message: kept.message }, new Date().toISOString(), cwd), e.agentId).catch((error: unknown) => report('append', 'Missed a message', error))
|
|
329
352
|
return kept
|
|
330
|
-
}
|
|
353
|
+
}
|
|
354
|
+
router.add('session.append', recordAppend, (e, _error, next) => recordAppend(e, next))
|
|
331
355
|
|
|
332
|
-
|
|
356
|
+
const recordCompaction: RoutedHook<'session.compact'> = async (e, next) => {
|
|
333
357
|
const compacted = await next(e)
|
|
334
358
|
if (e.trigger === 'precompute' || compacted.skip !== undefined || session === '') return compacted
|
|
335
359
|
const at = new Date().toISOString()
|
|
336
360
|
const boundary: Row = { id: `compact:${e.agentId ?? session}:${at}`, at, type: 'system', name: 'compact_boundary', isMeta: false, isSummary: false, content: [] }
|
|
337
361
|
void take(boundary, e.agentId).catch((error: unknown) => report('compact', 'Missed a compaction', error))
|
|
338
362
|
return compacted
|
|
339
|
-
}
|
|
363
|
+
}
|
|
364
|
+
router.add('session.compact', recordCompaction, (e, _error, next) => recordCompaction(e, next))
|
|
340
365
|
|
|
341
366
|
router.add('session.measure', async (e, next) => {
|
|
342
367
|
const measured = await next(e)
|
|
@@ -344,7 +369,7 @@ export function serveConversations({ claude, router, store, project, report }: {
|
|
|
344
369
|
return measured
|
|
345
370
|
})
|
|
346
371
|
|
|
347
|
-
|
|
372
|
+
const recordSpawn: RoutedHook<'agent.spawn'> = async (e, next) => {
|
|
348
373
|
const spawned = await next(e)
|
|
349
374
|
if (spawned.deny !== undefined || spawned.agentId === undefined) return spawned
|
|
350
375
|
const id = spawned.agentId
|
|
@@ -355,7 +380,8 @@ export function serveConversations({ claude, router, store, project, report }: {
|
|
|
355
380
|
waitingRows.delete(id)
|
|
356
381
|
for (const row of waiting) dispatch(session, conversation.add(row, id))
|
|
357
382
|
return spawned
|
|
358
|
-
}
|
|
383
|
+
}
|
|
384
|
+
router.add('agent.spawn', recordSpawn, (e, _error, next) => recordSpawn(e, next))
|
|
359
385
|
|
|
360
386
|
router.add('session.receive', async (e, next) => {
|
|
361
387
|
const sent = relayOf(e)
|
|
@@ -398,13 +424,13 @@ export function serveConversations({ claude, router, store, project, report }: {
|
|
|
398
424
|
await checkAct(caller, action, input)
|
|
399
425
|
await starting
|
|
400
426
|
if (action === 'abort') return abortTurn(input, task)
|
|
401
|
-
if (task !== undefined) return performInTask(action, input, task)
|
|
427
|
+
if (task !== undefined) return performInTask(caller, action, input, task)
|
|
402
428
|
if (action === 'usage') return usageOf(claude)
|
|
403
429
|
if (action === 'surfaces') return [...(await claude.session.surfaces())] satisfies Surface[]
|
|
404
430
|
if (action === 'send') {
|
|
405
431
|
const { type, text, as } = input as SessionSend
|
|
406
432
|
if (as === Session) throw new Error(`session ${session} cannot send as itself to itself.`)
|
|
407
|
-
const delivered = type === Note ? await claude.session.append({ message: { type: 'user', content: [{ type: 'text', text }] } }) : await claude.prompt.submit({ text, ...(as === Person ? { asUser: true } : {}) })
|
|
433
|
+
const delivered = type === Note ? await claude.session.append({ message: { type: 'user', content: [{ type: 'text', text: modNote(caller, text) }] } }) : await claude.prompt.submit({ text, ...(as === Person ? { asUser: true } : {}) })
|
|
408
434
|
if ('deny' in delivered && typeof delivered.deny === 'string') throw new Error(`Claude Code refused the note: ${delivered.deny}`)
|
|
409
435
|
if ('drop' in delivered && typeof delivered.drop === 'string') throw new Error(`Claude Code dropped the prompt: ${delivered.drop}`)
|
|
410
436
|
return null
|
|
@@ -424,13 +450,14 @@ export function serveConversations({ claude, router, store, project, report }: {
|
|
|
424
450
|
return null
|
|
425
451
|
}
|
|
426
452
|
|
|
427
|
-
const performInTask = async (action: Act, input: unknown, task: string): Promise<unknown> => {
|
|
453
|
+
const performInTask = async (caller: string, action: Act, input: unknown, task: string): Promise<unknown> => {
|
|
428
454
|
const running = conversation.state.tasks[task]
|
|
429
455
|
if (action === 'usage') return json(['history', 'usage', session, '--task', task])
|
|
430
456
|
if (action === 'surfaces') return running === undefined || running.closedAt !== undefined ? [] : [...(await claude.session.surfaces())]
|
|
431
457
|
if (action !== 'send') throw new Error(`cmod: ${action} runs only on a session's own main loop.`)
|
|
432
458
|
if (running === undefined || running.closedAt !== undefined) throw ended(task)
|
|
433
|
-
const
|
|
459
|
+
const { type, text } = input as SessionSend
|
|
460
|
+
const sent = await claude.session.send({ to: { agentId: task }, text: type === Note ? modNote(caller, text) : text })
|
|
434
461
|
if (sent.isDelivered !== true) throw ended(task)
|
|
435
462
|
return null
|
|
436
463
|
}
|
|
@@ -567,7 +594,7 @@ export function serveConversations({ claude, router, store, project, report }: {
|
|
|
567
594
|
if (caller === undefined) throw new Error('cmod hears no caller for this before listener.')
|
|
568
595
|
const target = String(request['session'])
|
|
569
596
|
const list = Array.isArray(request['subscriptions']) ? (request['subscriptions'] as readonly Subscription[]) : []
|
|
570
|
-
ownWatches.set(`${caller}\n${target}`, { mod: caller, target, project: project(), events: [...new Set(list.map(({ event }) => event))] })
|
|
597
|
+
ownWatches.set(`${caller}\n${target}`, { mod: caller, target, project: project(), events: [...new Set(list.map(({ event }) => event))], refusing: list.filter((each) => each.onFailure === 'refuse') })
|
|
571
598
|
await publishWatches()
|
|
572
599
|
return null
|
|
573
600
|
},
|
package/src/runtime/engine.ts
CHANGED
package/src/runtime/hooks.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { ClassicHookEvent, ClassicHookInputs } from 'claude-code'
|
|
2
2
|
import { configFolders } from '../records.js'
|
|
3
3
|
import type { Claude } from './claude.js'
|
|
4
|
-
import type { RoutedHook } from './router.js'
|
|
4
|
+
import type { RoutedFailure, RoutedHook } from './router.js'
|
|
5
5
|
import type { Failure } from './toaster.js'
|
|
6
6
|
import { callEffects, type CallBase, type FileAccess } from '../utils/call-effects.js'
|
|
7
7
|
import { dynamicPattern } from '../utils/parse-shell.js'
|
|
@@ -35,8 +35,6 @@ export type RoutedEvent =
|
|
|
35
35
|
| 'agent.spawn'
|
|
36
36
|
| 'turn.start'
|
|
37
37
|
|
|
38
|
-
export const permissionEvents: readonly RoutedEvent[] = ['tool.check', 'classic.PermissionRequest']
|
|
39
|
-
|
|
40
38
|
type CallFiles = { readonly read: string[]; readonly changed: string[] }
|
|
41
39
|
|
|
42
40
|
export function classicHook<H extends Hook>(event: H, hook: HookListener<H>, report: (failure: Failure) => void): RoutedHook<RoutedEvent> {
|
|
@@ -53,6 +51,14 @@ export function classicHook<H extends Hook>(event: H, hook: HookListener<H>, rep
|
|
|
53
51
|
return routed as unknown as RoutedHook<RoutedEvent>
|
|
54
52
|
}
|
|
55
53
|
|
|
54
|
+
export function classicFailure<H extends Hook>(failed: (input: ClassicHookInputs[H], error: Error) => HookFields | undefined | Promise<HookFields | undefined>): RoutedFailure<RoutedEvent> {
|
|
55
|
+
const routed = async (e: unknown, error: Error, next: (e: unknown) => Promise<unknown>) => {
|
|
56
|
+
const ours = await failed(e as ClassicHookInputs[H], error)
|
|
57
|
+
return ours === undefined ? undefined : mergeClassic((await next(e)) as HookFields, ours)
|
|
58
|
+
}
|
|
59
|
+
return routed as unknown as RoutedFailure<RoutedEvent>
|
|
60
|
+
}
|
|
61
|
+
|
|
56
62
|
const frontmatter = /^---\r?\n[\s\S]*?\r?\n---[ \t]*(?:\r?\n|$)/
|
|
57
63
|
|
|
58
64
|
const baseDirectoryLine = /^Base directory for this skill: [^\n]*\n\n/
|
package/src/runtime/lifecycle.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import type { Args, EventResult, Frozen, HookBudget, PluginOptions, RenderElement, Timer } from 'claude-code'
|
|
1
|
+
import type { Args, ClassicHookInputs, EventResult, Frozen, HookBudget, HookFailure, PluginOptions, RenderElement, Timer } from 'claude-code'
|
|
2
2
|
import type { Step } from '../installer.js'
|
|
3
3
|
import type { Allowed, FileChange, Disposable, Imports, Mod, ModDefinition, PaneHandle, SettingsFile, SettingsFileChange, SettingsFileChangeListener, Toast } from '../mod.js'
|
|
4
4
|
import type { Options, OptionValues } from '../options.js'
|
|
5
5
|
import { changelogPage, consentPath, dataFolder, finishStepsMethod, isAtLeast, marketplaceOf, oldestCmodFor, openPageMethod, parseConsent, pendingStepsMethod, parseEvent, permissionWords, readRecord, readSteps, recordPath, scriptsSha256, settingsPagesMethod, storeFolder, updatesToTurnOn, type ReadFile, type RunnerEvent, type Steps } from '../records.js'
|
|
6
6
|
import { homePrefix, ifMissing, relativePath } from '../utils/paths.js'
|
|
7
|
-
import { firstLine, formatExit,
|
|
7
|
+
import { firstLine, formatExit, messageOf } from '../utils/text.js'
|
|
8
8
|
import type { Claude } from './claude.js'
|
|
9
9
|
import { beforeDeadline } from './deadline.js'
|
|
10
10
|
import { announcedOf, announcementMs, announcementOf } from './announce.js'
|
|
@@ -12,7 +12,7 @@ import { bind } from './bound.js'
|
|
|
12
12
|
import { serveConversations } from './conversations.js'
|
|
13
13
|
import { answerCall, answeringOwnCalls, cmodPluginName, dependencyCalls, withBareRefusals } from './dependencies.js'
|
|
14
14
|
import { keepEngine } from './engine.js'
|
|
15
|
-
import {
|
|
15
|
+
import { classicFailure, classicHook, userSkillHook, type Hook, type HookFields, type HookListener, type RoutedEvent } from './hooks.js'
|
|
16
16
|
import type { PermissionRoutes } from './permission-check.js'
|
|
17
17
|
import { checkedAnswer, checkingGrants, checkSettings, checksAnswers, checkWrite, isCovered, itemOf, passedDown, programAddresses, type AnswerCheck, type Grants } from './grants.js'
|
|
18
18
|
import { cmodError, lastLineOf, locatingPrograms, readLines, type Programs } from './programs.js'
|
|
@@ -21,7 +21,7 @@ import { fitsOption } from '../options.js'
|
|
|
21
21
|
import { createInstaller } from './installer.js'
|
|
22
22
|
import { createOptions, MissingOptions } from './options.js'
|
|
23
23
|
import { createRouter, type Router, type RouterNext } from './router.js'
|
|
24
|
-
import { conversationMethods, createSessionsClient } from './sessions.js'
|
|
24
|
+
import { conversationMethods, createSessionsClient, type SessionsClient } from './sessions.js'
|
|
25
25
|
import type { SessionChange } from '../session/conversation.js'
|
|
26
26
|
import type { BeforeEvent } from '../session/session.js'
|
|
27
27
|
import { matches } from '../utils/matcher.js'
|
|
@@ -52,6 +52,7 @@ export type Lifecycle<State extends object> = {
|
|
|
52
52
|
readonly toasts: readonly ShownToast[]
|
|
53
53
|
start(claude: Claude, read: (claude: Claude) => Promise<Plugin>): Promise<void>
|
|
54
54
|
route<N extends RoutedEvent>(event: N, e: Frozen<Args<N>>, next: RouterNext<N>): Promise<EventResult<N>>
|
|
55
|
+
caught<N extends RoutedEvent>(event: N, e: Frozen<Args<N>>, next: RouterNext<N>, failure: HookFailure): Promise<EventResult<N>>
|
|
55
56
|
raise(method: string, payload: Readonly<Record<string, unknown>>): Promise<unknown>
|
|
56
57
|
}
|
|
57
58
|
|
|
@@ -72,7 +73,7 @@ type ActiveMod<State extends object, Declared extends Options> = {
|
|
|
72
73
|
readonly mod: Mod<State, Declared>
|
|
73
74
|
readonly added: readonly string[]
|
|
74
75
|
readonly imports: Imports
|
|
75
|
-
readonly raise:
|
|
76
|
+
readonly raise: SessionsClient['raise']
|
|
76
77
|
}
|
|
77
78
|
|
|
78
79
|
|
|
@@ -88,7 +89,9 @@ const cmodWaitMs = 60_000
|
|
|
88
89
|
|
|
89
90
|
const cmodSlowCheckMs = 10_000
|
|
90
91
|
|
|
91
|
-
const
|
|
92
|
+
const hookMs = 10_000 satisfies HookBudget['ms']
|
|
93
|
+
|
|
94
|
+
const startHoldMs = hookMs - 1_000
|
|
92
95
|
|
|
93
96
|
const dependencyCallMs = 30_000
|
|
94
97
|
|
|
@@ -169,7 +172,7 @@ export function createLifecycle<State extends object, Declared extends Options =
|
|
|
169
172
|
let activation: Promise<void> | undefined
|
|
170
173
|
let mod: Mod<State> | undefined
|
|
171
174
|
let imports: Imports | undefined
|
|
172
|
-
let raise:
|
|
175
|
+
let raise: SessionsClient['raise'] | undefined
|
|
173
176
|
let failure: unknown
|
|
174
177
|
let pluginOptions = options
|
|
175
178
|
const installer = createInstaller(definition.name, () => claude())
|
|
@@ -628,6 +631,16 @@ export function createLifecycle<State extends object, Declared extends Options =
|
|
|
628
631
|
if (event !== 'ui.render' || render.component !== 'AbovePrompt' || runtime === undefined) return answer
|
|
629
632
|
return runtime.toaster.draw((await answer) as EventResult<'ui.render'>, runtime.claude.ui.resolve(render), render.props, render.requestId) as EventResult<N>
|
|
630
633
|
},
|
|
634
|
+
async caught<N extends RoutedEvent>(event: N, e: Frozen<Args<N>>, next: RouterNext<N>, { kind, message }: HookFailure): Promise<EventResult<N>> {
|
|
635
|
+
const call = e as Frozen<Args<'cmod.call'>>
|
|
636
|
+
const isAsk = event === 'cmod.call' && call.to === definition.name && beforeAskOf(call) !== undefined
|
|
637
|
+
const reasons: Readonly<Record<string, string>> = { timeout: `it ran past the ${hookMs / 1000} s Claude Code gives a hook`, throw: message ?? 'it threw' }
|
|
638
|
+
const reason = reasons[kind]
|
|
639
|
+
if (reason === undefined) return isAsk ? ({ value: null } as EventResult<N>) : next(e)
|
|
640
|
+
const error = new Error(reason)
|
|
641
|
+
toastFailure({ id: `stopped:${event}`, message: `Claude Code stopped its ${event.replace(/^classic\./, '')} hook`, error })
|
|
642
|
+
return router.dispatchFailure(event, e, error, next)
|
|
643
|
+
},
|
|
631
644
|
}
|
|
632
645
|
}
|
|
633
646
|
|
|
@@ -704,7 +717,8 @@ async function createMod<State extends object, Declared extends Options>(definit
|
|
|
704
717
|
}))
|
|
705
718
|
const modState = createState<State>({ name: definition.name, initial: definition.state ?? {}, session, root, claude, report, changed: area.changed, forkParent })
|
|
706
719
|
const modOptions = createOptions({ name: definition.name, declared: definition.options ?? {}, fromClaude: runtime.options, claude, report, changed: modState.changed })
|
|
707
|
-
const hookOn = <H extends Hook>(hook: H, listener: HookListener<H
|
|
720
|
+
const hookOn = <H extends Hook>(hook: H, listener: HookListener<H>, failed?: (input: ClassicHookInputs[H], error: Error) => HookFields | undefined): Disposable =>
|
|
721
|
+
router.add(`classic.${hook}`, classicHook(hook, listener, report), failed === undefined ? undefined : classicFailure(failed))
|
|
708
722
|
const expand = (path: string) => (homePrefix.test(path) ? resolvedPath(path, places) : path)
|
|
709
723
|
const settingsFile = (source: 'user' | 'project' | 'local'): SettingsFile => ({
|
|
710
724
|
read: () => checked.settings.read({ source }),
|
|
@@ -718,8 +732,8 @@ async function createMod<State extends object, Declared extends Options>(definit
|
|
|
718
732
|
claude: checked,
|
|
719
733
|
report: (id, message, error) => report({ id, message, error }),
|
|
720
734
|
isGranted: (permission) => flag(permission),
|
|
721
|
-
hookOn: (hook, listener) => hookOn(hook as Hook, listener as HookListener<Hook
|
|
722
|
-
routeOn: (event, hook) => router.add(event, hook as never),
|
|
735
|
+
hookOn: (hook, listener, failed) => hookOn(hook as Hook, listener as HookListener<Hook>, failed),
|
|
736
|
+
routeOn: (event: RoutedEvent, hook: unknown, failed?: unknown) => router.add(event, hook as never, failed as never),
|
|
723
737
|
permissionRoutes: runtime.permissionRoutes,
|
|
724
738
|
})
|
|
725
739
|
let followed: Parameters<typeof client.follow>[0] | undefined
|
|
@@ -944,10 +958,8 @@ async function createMod<State extends object, Declared extends Options>(definit
|
|
|
944
958
|
client.changed(changedSession, change, isInProject !== false)
|
|
945
959
|
return { value: null }
|
|
946
960
|
}
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
return { value: (await client.answerBefore(asking, event, change, isInProject !== false)) ?? null }
|
|
950
|
-
}
|
|
961
|
+
const ask = beforeAskOf(call)
|
|
962
|
+
if (ask !== undefined) return { value: (await client.answerBefore(ask.session, ask.event, ask.change, ask.isInProject)) ?? null }
|
|
951
963
|
if (e.method === settingsPagesMethod) return { value: [...pages].map(([id, { title }]) => ({ id, title })) }
|
|
952
964
|
if (e.method === openPageMethod) {
|
|
953
965
|
await settingsPages.open(String(e.input))
|
|
@@ -955,9 +967,13 @@ async function createMod<State extends object, Declared extends Options>(definit
|
|
|
955
967
|
}
|
|
956
968
|
const served = await conversations?.answer(call)
|
|
957
969
|
return served ?? answerCall(definition.name, api, e)
|
|
970
|
+
}, (e, error) => {
|
|
971
|
+
if (e.to !== definition.name) return undefined
|
|
972
|
+
const ask = beforeAskOf(e)
|
|
973
|
+
return ask === undefined ? { deny: `${definition.name}: ${error.message}` } : { value: client.failedBefore(ask.session, ask.event, ask.change, ask.isInProject, error) ?? null }
|
|
958
974
|
})
|
|
959
975
|
const agents = toolCalls(claude, router)
|
|
960
|
-
keepEngine(mod, { claude: checked, router, toolCalls: agents, report })
|
|
976
|
+
keepEngine(mod, { claude: checked, router, toolCalls: agents, report, hasPermissionCheck: () => runtime.permissionRoutes() !== undefined })
|
|
961
977
|
const names = new Set<string>()
|
|
962
978
|
const reserveName = (type: string, name: string, taken: string): Disposable => {
|
|
963
979
|
const key = `${type}:${name}`
|
|
@@ -972,10 +988,6 @@ async function createMod<State extends object, Declared extends Options>(definit
|
|
|
972
988
|
await failsAs('the details of its session did not load', () => followCurrent(undefined))
|
|
973
989
|
await failsAs('its setup function threw', () => definition.setup(mod))
|
|
974
990
|
isSetUp = true
|
|
975
|
-
const permissionHooks = permissionEvents.filter((event) => router.has(event))
|
|
976
|
-
if (permissionHooks.length > 0 && runtime.permissionRoutes() === undefined) {
|
|
977
|
-
throw new Error(`it decides permissions on ${listed(permissionHooks)}, so hooks/register.ts must call registerPermissionCheck(addHook) after registerMod`)
|
|
978
|
-
}
|
|
979
991
|
await failsAs('its open panes did not load', () => area.restorePanes())
|
|
980
992
|
return { mod, added, imports, raise: client.raise }
|
|
981
993
|
}
|
|
@@ -1001,6 +1013,14 @@ function checkedMs(name: string, subject: string, ms: number): number {
|
|
|
1001
1013
|
return ms
|
|
1002
1014
|
}
|
|
1003
1015
|
|
|
1016
|
+
type BeforeAsk = { readonly session: string; readonly event: BeforeEvent; readonly change: SessionChange; readonly isInProject: boolean }
|
|
1017
|
+
|
|
1018
|
+
function beforeAskOf(call: Frozen<Args<'cmod.call'>> & { readonly caller?: string }): BeforeAsk | undefined {
|
|
1019
|
+
if (call.method !== conversationMethods.before || call.caller !== cmodPluginName) return undefined
|
|
1020
|
+
const { session, event, change, isInProject } = call.input as Omit<BeforeAsk, 'isInProject'> & { readonly isInProject?: boolean }
|
|
1021
|
+
return { session, event, change, isInProject: isInProject !== false }
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1004
1024
|
async function failsAs(subject: string, step: () => unknown): Promise<void> {
|
|
1005
1025
|
try {
|
|
1006
1026
|
await step()
|