@cmodjs/core 0.3.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 +61 -10
- package/docs/dependencies.md +16 -14
- package/docs/hooks.md +64 -29
- package/docs/index.md +29 -25
- package/docs/install-steps.md +13 -9
- package/docs/jobs.md +59 -111
- package/docs/mod.md +227 -175
- package/docs/options.md +2 -2
- package/docs/permissions.md +27 -16
- package/docs/session.md +311 -0
- package/docs/state.md +11 -7
- package/docs/testing.md +68 -23
- package/docs/ui.md +116 -76
- package/jobs/check.d.ts.map +1 -1
- package/jobs/check.js +5 -2
- package/jobs/context.d.ts +7 -8
- package/jobs/context.d.ts.map +1 -1
- package/jobs/context.js +23 -73
- package/jobs/permissions/decide-permission.d.ts +5 -3
- package/jobs/permissions/decide-permission.d.ts.map +1 -1
- package/jobs/permissions/decide-permission.js +6 -4
- package/jobs/permissions/find-project-scope.d.ts +1 -1
- package/jobs/permissions/find-project-scope.d.ts.map +1 -1
- package/jobs/permissions/find-project-scope.js +8 -11
- package/jobs/permissions/match-target.d.ts +2 -2
- package/jobs/permissions/match-target.d.ts.map +1 -1
- package/jobs/permissions.d.ts +1 -1
- package/jobs/permissions.d.ts.map +1 -1
- package/jobs/permissions.js +6 -4
- package/jobs/program.d.ts.map +1 -1
- package/jobs/program.js +14 -4
- package/jobs/prompt.d.ts +4 -2
- package/jobs/prompt.d.ts.map +1 -1
- package/jobs/prompt.js +16 -15
- package/jobs/schedule.d.ts +2 -2
- package/jobs/schedule.d.ts.map +1 -1
- package/jobs/schedule.js +9 -9
- package/jobs/slash-command.d.ts +4 -4
- package/jobs/slash-command.d.ts.map +1 -1
- package/jobs/slash-command.js +14 -9
- package/jobs/status-line.d.ts +1 -11
- package/jobs/status-line.d.ts.map +1 -1
- package/jobs/status-line.js +8 -12
- package/jobs/tool.d.ts +2 -2
- package/jobs/tool.d.ts.map +1 -1
- package/jobs/tool.js +15 -9
- package/mod.d.ts +178 -75
- package/mod.d.ts.map +1 -1
- package/mod.js +127 -3
- package/options.d.ts +2 -2
- package/options.js +19 -19
- package/package.json +3 -3
- package/records.d.ts +13 -8
- package/records.d.ts.map +1 -1
- package/records.js +33 -17
- package/register.d.ts.map +1 -1
- package/register.js +34 -5
- package/runtime/announce.d.ts +18 -0
- package/runtime/announce.d.ts.map +1 -0
- package/runtime/announce.js +68 -0
- package/runtime/before.d.ts +32 -0
- package/runtime/before.d.ts.map +1 -0
- package/runtime/before.js +214 -0
- package/runtime/bound.d.ts +4 -0
- package/runtime/bound.d.ts.map +1 -0
- package/runtime/bound.js +9 -0
- package/runtime/claude.d.ts +4 -2
- package/runtime/claude.d.ts.map +1 -1
- package/runtime/conversations.d.ts +17 -0
- package/runtime/conversations.d.ts.map +1 -0
- package/runtime/conversations.js +563 -0
- package/runtime/deadline.d.ts +0 -1
- package/runtime/deadline.d.ts.map +1 -1
- package/runtime/deadline.js +1 -1
- package/runtime/dependencies.d.ts +2 -0
- package/runtime/dependencies.d.ts.map +1 -1
- package/runtime/dependencies.js +10 -1
- package/runtime/engine.d.ts +13 -0
- package/runtime/engine.d.ts.map +1 -0
- package/runtime/engine.js +10 -0
- package/runtime/grants.d.ts +4 -0
- package/runtime/grants.d.ts.map +1 -1
- package/runtime/grants.js +68 -31
- package/runtime/hooks.d.ts +5 -4
- package/runtime/hooks.d.ts.map +1 -1
- package/runtime/hooks.js +29 -9
- package/runtime/installer.d.ts +1 -1
- package/runtime/installer.d.ts.map +1 -1
- package/runtime/installer.js +17 -14
- package/runtime/lifecycle.d.ts +3 -1
- package/runtime/lifecycle.d.ts.map +1 -1
- package/runtime/lifecycle.js +414 -212
- package/runtime/metadata.d.ts +3 -1
- package/runtime/metadata.d.ts.map +1 -1
- package/runtime/metadata.js +25 -14
- package/runtime/options.d.ts +3 -1
- package/runtime/options.d.ts.map +1 -1
- package/runtime/options.js +4 -4
- package/runtime/programs.d.ts +8 -0
- package/runtime/programs.d.ts.map +1 -1
- package/runtime/programs.js +39 -1
- package/runtime/router.d.ts +2 -1
- package/runtime/router.d.ts.map +1 -1
- package/runtime/router.js +11 -3
- package/runtime/sessions.d.ts +58 -0
- package/runtime/sessions.d.ts.map +1 -0
- package/runtime/sessions.js +433 -0
- package/runtime/state.d.ts +6 -2
- package/runtime/state.d.ts.map +1 -1
- package/runtime/state.js +46 -13
- package/runtime/toaster.d.ts +24 -0
- package/runtime/toaster.d.ts.map +1 -0
- package/runtime/toaster.js +286 -0
- package/runtime/ui.d.ts +6 -17
- package/runtime/ui.d.ts.map +1 -1
- package/runtime/ui.js +62 -118
- package/session/context/context.d.ts +12 -0
- package/session/context/context.d.ts.map +1 -0
- package/session/context/context.js +2 -0
- package/session/context/turn/message/document.d.ts +7 -0
- package/session/context/turn/message/document.d.ts.map +1 -0
- package/session/context/turn/message/document.js +1 -0
- package/session/context/turn/message/message.d.ts +55 -0
- package/session/context/turn/message/message.d.ts.map +1 -0
- package/session/context/turn/message/message.js +178 -0
- package/session/context/turn/message/note.d.ts +8 -0
- package/session/context/turn/message/note.d.ts.map +1 -0
- package/session/context/turn/message/note.js +1 -0
- package/session/context/turn/message/notice.d.ts +6 -0
- package/session/context/turn/message/notice.d.ts.map +1 -0
- package/session/context/turn/message/notice.js +1 -0
- package/session/context/turn/message/prompt.d.ts +8 -0
- package/session/context/turn/message/prompt.d.ts.map +1 -0
- package/session/context/turn/message/prompt.js +1 -0
- package/session/context/turn/message/reply.d.ts +6 -0
- package/session/context/turn/message/reply.d.ts.map +1 -0
- package/session/context/turn/message/reply.js +1 -0
- package/session/context/turn/message/sender.d.ts +14 -0
- package/session/context/turn/message/sender.d.ts.map +1 -0
- package/session/context/turn/message/sender.js +30 -0
- package/session/context/turn/message/skill.d.ts +7 -0
- package/session/context/turn/message/skill.d.ts.map +1 -0
- package/session/context/turn/message/skill.js +1 -0
- package/session/context/turn/message/tool-call.d.ts +9 -0
- package/session/context/turn/message/tool-call.d.ts.map +1 -0
- package/session/context/turn/message/tool-call.js +1 -0
- package/session/context/turn/message/tool-result.d.ts +8 -0
- package/session/context/turn/message/tool-result.d.ts.map +1 -0
- package/session/context/turn/message/tool-result.js +1 -0
- package/session/context/turn/turn.d.ts +13 -0
- package/session/context/turn/turn.d.ts.map +1 -0
- package/session/context/turn/turn.js +2 -0
- package/session/conversation.d.ts +73 -0
- package/session/conversation.d.ts.map +1 -0
- package/session/conversation.js +146 -0
- package/session/goal/goal.d.ts +11 -0
- package/session/goal/goal.d.ts.map +1 -0
- package/session/goal/goal.js +1 -0
- package/session/goal/task.d.ts +16 -0
- package/session/goal/task.d.ts.map +1 -0
- package/session/goal/task.js +3 -0
- package/session/session.d.ts +158 -0
- package/session/session.d.ts.map +1 -0
- package/session/session.js +2 -0
- package/src/jobs/check.ts +7 -4
- package/src/jobs/context.ts +24 -82
- package/src/jobs/permissions/decide-permission.ts +10 -8
- package/src/jobs/permissions/find-project-scope.ts +8 -14
- package/src/jobs/permissions/match-target.ts +3 -3
- package/src/jobs/permissions.ts +8 -6
- package/src/jobs/program.ts +13 -4
- package/src/jobs/prompt.ts +21 -24
- package/src/jobs/schedule.ts +14 -14
- package/src/jobs/slash-command.ts +20 -14
- package/src/jobs/status-line.ts +9 -21
- package/src/jobs/tool.ts +17 -11
- package/src/mod.ts +252 -97
- package/src/options.ts +22 -22
- package/src/records.ts +40 -23
- package/src/register.ts +34 -5
- package/src/runtime/announce.ts +76 -0
- package/src/runtime/before.ts +200 -0
- package/src/runtime/bound.ts +12 -0
- package/src/runtime/claude.ts +7 -2
- package/src/runtime/conversations.ts +579 -0
- package/src/runtime/deadline.ts +2 -2
- package/src/runtime/dependencies.ts +13 -1
- package/src/runtime/engine.ts +23 -0
- package/src/runtime/grants.ts +69 -29
- package/src/runtime/hooks.ts +39 -14
- package/src/runtime/installer.ts +16 -14
- package/src/runtime/lifecycle.ts +413 -201
- package/src/runtime/metadata.ts +37 -14
- package/src/runtime/options.ts +19 -4
- package/src/runtime/programs.ts +40 -1
- package/src/runtime/router.ts +11 -4
- package/src/runtime/sessions.ts +447 -0
- package/src/runtime/state.ts +50 -14
- package/src/runtime/toaster.ts +334 -0
- package/src/runtime/ui.ts +65 -143
- package/src/session/context/context.ts +14 -0
- package/src/session/context/turn/message/document.ts +8 -0
- package/src/session/context/turn/message/message.ts +203 -0
- package/src/session/context/turn/message/note.ts +9 -0
- package/src/session/context/turn/message/notice.ts +7 -0
- package/src/session/context/turn/message/prompt.ts +9 -0
- package/src/session/context/turn/message/reply.ts +7 -0
- package/src/session/context/turn/message/sender.ts +38 -0
- package/src/session/context/turn/message/skill.ts +8 -0
- package/src/session/context/turn/message/tool-call.ts +10 -0
- package/src/session/context/turn/message/tool-result.ts +9 -0
- package/src/session/context/turn/turn.ts +15 -0
- package/src/session/conversation.ts +194 -0
- package/src/session/goal/goal.ts +10 -0
- package/src/session/goal/task.ts +19 -0
- package/src/session/session.ts +141 -0
- package/src/testing/fake-claude.ts +25 -17
- package/src/testing/fake-elements.ts +3 -2
- package/src/testing/fake-files.ts +1 -1
- package/src/testing/fake-sessions.ts +277 -0
- package/src/testing.ts +138 -35
- package/src/ui/components.ts +3 -3
- package/src/ui/elements.ts +22 -8
- package/src/ui/markdown.ts +22 -26
- package/src/ui/places.ts +49 -0
- package/src/utils/call-effects.ts +5 -5
- package/src/utils/changelog.ts +32 -0
- package/src/utils/disposable.ts +3 -0
- package/src/utils/matcher.ts +37 -0
- package/src/utils/metadata.ts +17 -15
- package/src/utils/parse-shell.ts +9 -8
- package/src/utils/paths.ts +21 -4
- package/src/utils/text.ts +4 -0
- package/testing/fake-claude.d.ts +5 -1
- package/testing/fake-claude.d.ts.map +1 -1
- package/testing/fake-claude.js +21 -12
- package/testing/fake-elements.js +4 -2
- package/testing/fake-files.js +1 -1
- package/testing/fake-sessions.d.ts +81 -0
- package/testing/fake-sessions.d.ts.map +1 -0
- package/testing/fake-sessions.js +249 -0
- package/testing.d.ts +34 -13
- package/testing.d.ts.map +1 -1
- package/testing.js +115 -23
- package/ui/components.d.ts +2 -3
- package/ui/components.d.ts.map +1 -1
- package/ui/components.js +3 -3
- package/ui/elements.d.ts.map +1 -1
- package/ui/elements.js +19 -9
- package/ui/markdown.d.ts +35 -33
- package/ui/markdown.d.ts.map +1 -1
- package/ui/markdown.js +14 -16
- package/ui/places.d.ts +43 -0
- package/ui/places.d.ts.map +1 -0
- package/ui/places.js +31 -0
- package/utils/call-effects.d.ts +2 -1
- package/utils/call-effects.d.ts.map +1 -1
- package/utils/call-effects.js +3 -3
- package/utils/changelog.d.ts +9 -0
- package/utils/changelog.d.ts.map +1 -0
- package/utils/changelog.js +29 -0
- package/utils/disposable.d.ts +4 -0
- package/utils/disposable.d.ts.map +1 -0
- package/utils/disposable.js +1 -0
- package/utils/matcher.d.ts +13 -0
- package/utils/matcher.d.ts.map +1 -0
- package/utils/matcher.js +32 -0
- package/utils/metadata.d.ts +3 -3
- package/utils/metadata.js +18 -14
- package/utils/parse-shell.js +9 -7
- package/utils/paths.d.ts +4 -1
- package/utils/paths.d.ts.map +1 -1
- package/utils/paths.js +19 -4
- package/utils/text.d.ts +1 -0
- package/utils/text.d.ts.map +1 -1
- package/utils/text.js +3 -0
- package/src/ui/slots.ts +0 -36
- package/ui/slots.d.ts +0 -208
- package/ui/slots.d.ts.map +0 -1
- package/ui/slots.js +0 -19
package/src/records.ts
CHANGED
|
@@ -19,23 +19,31 @@ export type Steps = {
|
|
|
19
19
|
permissions?: readonly string[]
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
export const listPermissions = ['network', 'run', 'files'] as const
|
|
23
|
-
export const flagPermissions = ['conversation', 'prompt', 'model', '
|
|
22
|
+
export const listPermissions = ['network', 'run', 'files', 'config'] as const
|
|
23
|
+
export const flagPermissions = ['conversation', 'prompt', 'model', 'tasks', 'tools', 'approve'] as const
|
|
24
24
|
export type ListPermission = (typeof listPermissions)[number]
|
|
25
25
|
export type FlagPermission = (typeof flagPermissions)[number]
|
|
26
26
|
const permissionNames: readonly string[] = [...listPermissions, ...flagPermissions]
|
|
27
27
|
const hostName = /^[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?(\.[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?)*$/
|
|
28
28
|
const programName = /^[A-Za-z0-9][A-Za-z0-9._+-]*$/
|
|
29
|
+
const settingName = /^[A-Za-z][A-Za-z0-9]*$/
|
|
30
|
+
const listItemHint: Readonly<Record<ListPermission, string>> = {
|
|
31
|
+
network: 'a host alone, such as "api.github.com", or a socket path, such as "/var/run/docker.sock"',
|
|
32
|
+
run: 'a program\'s command, such as "gh"',
|
|
33
|
+
files: 'a path that starts with ~/ or /, such as "~/.zshrc"',
|
|
34
|
+
config: 'a Claude Code setting\'s name, such as "cleanupPeriodDays"',
|
|
35
|
+
}
|
|
29
36
|
|
|
30
37
|
export type ReadFile = (path: string) => Promise<string | undefined>
|
|
31
38
|
|
|
32
39
|
export type RunnerEvent =
|
|
33
|
-
| {
|
|
34
|
-
| {
|
|
35
|
-
| {
|
|
36
|
-
| {
|
|
37
|
-
| {
|
|
38
|
-
| {
|
|
40
|
+
| { type: 'progress'; done: number; total: number; label: string }
|
|
41
|
+
| { type: 'log'; text: string }
|
|
42
|
+
| { type: 'needs-consent'; sha256: string; install: string; uninstall: string; keys: string; permissions: readonly string[] }
|
|
43
|
+
| { type: 'done'; name: string; version?: string }
|
|
44
|
+
| { type: 'missing'; name: string }
|
|
45
|
+
| { type: 'retries'; when: 'changed' | 'later' }
|
|
46
|
+
| { type: 'failed'; code: number; message: string }
|
|
39
47
|
|
|
40
48
|
export const pluginName = /^[A-Za-z0-9][A-Za-z0-9._-]*$/
|
|
41
49
|
|
|
@@ -153,12 +161,12 @@ function readPermissions(steps: Record<string, unknown>): string[] {
|
|
|
153
161
|
items.push('run:*')
|
|
154
162
|
continue
|
|
155
163
|
}
|
|
156
|
-
const example =
|
|
164
|
+
const example = { network: '["api.github.com"]', run: '["gh"], or "*" for any program', files: '["~/.zshrc"]', config: '["cleanupPeriodDays"]' }[name]
|
|
157
165
|
if (!Array.isArray(value) || value.length === 0 || !value.every((each) => typeof each === 'string')) throw new Error(`${at} sets "${name}" to ${JSON.stringify(value)}. Write a list, such as "${name}": ${example}.`)
|
|
158
|
-
for (const each of value
|
|
166
|
+
for (const each of value) {
|
|
159
167
|
const isPath = /^(~\/|\/)/.test(each) && !each.split('/').includes('..')
|
|
160
|
-
const isValid =
|
|
161
|
-
if (
|
|
168
|
+
const isValid = { network: hostName.test(each) || isPath, run: programName.test(each), files: isPath, config: settingName.test(each) }[name]
|
|
169
|
+
if (isValid !== true) throw new Error(`${at} lists "${each}" under "${name}". Write ${listItemHint[name as ListPermission]}.`)
|
|
162
170
|
items.push(`${name}:${each}`)
|
|
163
171
|
}
|
|
164
172
|
}
|
|
@@ -176,17 +184,17 @@ export function permissionWords(item: string): string {
|
|
|
176
184
|
case 'files':
|
|
177
185
|
return `Change ${target}`
|
|
178
186
|
case 'conversation':
|
|
179
|
-
return 'Read
|
|
187
|
+
return 'Read your Claude Code conversations'
|
|
180
188
|
case 'prompt':
|
|
181
189
|
return 'Add text Claude reads and start turns'
|
|
182
190
|
case 'model':
|
|
183
191
|
return 'Ask a model, which uses your plan'
|
|
184
|
-
case '
|
|
185
|
-
return 'Start
|
|
192
|
+
case 'tasks':
|
|
193
|
+
return 'Start tasks that work beside Claude'
|
|
186
194
|
case 'tools':
|
|
187
195
|
return "Use and change Claude's tool calls"
|
|
188
196
|
case 'config':
|
|
189
|
-
return
|
|
197
|
+
return `Change ${target} in your Claude Code settings`
|
|
190
198
|
case 'approve':
|
|
191
199
|
return "Approve Claude's tool calls for you"
|
|
192
200
|
default:
|
|
@@ -196,6 +204,8 @@ export function permissionWords(item: string): string {
|
|
|
196
204
|
|
|
197
205
|
export const settingsPagesMethod = 'cmod:settingsPages'
|
|
198
206
|
|
|
207
|
+
export const changelogPage = 'changelog'
|
|
208
|
+
|
|
199
209
|
export const openPageMethod = 'cmod:openPage'
|
|
200
210
|
|
|
201
211
|
export const pendingStepsMethod = 'cmod:pendingSteps'
|
|
@@ -240,7 +250,10 @@ function readKeys(keys: unknown): Steps {
|
|
|
240
250
|
return { keys: commands }
|
|
241
251
|
}
|
|
242
252
|
|
|
253
|
+
export const coreVersion = '1.5.0'
|
|
254
|
+
|
|
243
255
|
export function oldestCmodFor(steps: Steps): string {
|
|
256
|
+
if (steps.permissions?.some((item) => item === 'tasks' || item.startsWith('config:')) === true) return '1.0.0'
|
|
244
257
|
if (steps.permissions !== undefined) return '0.2.0'
|
|
245
258
|
return Object.keys(steps.keys ?? {}).length > 0 ? '0.1.12' : '0.0.0'
|
|
246
259
|
}
|
|
@@ -303,7 +316,7 @@ export async function scriptsSha256(steps: Steps, files: { read: ReadFile; list:
|
|
|
303
316
|
}
|
|
304
317
|
|
|
305
318
|
export function formatEvent(event: RunnerEvent): string {
|
|
306
|
-
switch (event.
|
|
319
|
+
switch (event.type) {
|
|
307
320
|
case 'progress':
|
|
308
321
|
return `progress ${event.done} ${event.total}${event.label === '' ? '' : ` ${event.label}`}`
|
|
309
322
|
case 'log':
|
|
@@ -316,24 +329,28 @@ export function formatEvent(event: RunnerEvent): string {
|
|
|
316
329
|
return `missing ${event.name}`
|
|
317
330
|
case 'failed':
|
|
318
331
|
return `failed ${event.code}\t${event.message.replace(/\s*\n\s*/g, ' ')}`
|
|
332
|
+
case 'retries':
|
|
333
|
+
return `retries ${event.when}`
|
|
319
334
|
}
|
|
320
335
|
}
|
|
321
336
|
|
|
322
337
|
export function parseEvent(line: string): RunnerEvent {
|
|
323
338
|
const progress = /^progress (\d+) (\d+)(?: (.*))?$/.exec(line)
|
|
324
|
-
if (progress !== null) return {
|
|
339
|
+
if (progress !== null) return { type: 'progress', done: Number(progress[1]), total: Number(progress[2]), label: progress[3] ?? '' }
|
|
325
340
|
const consent = /^needs-consent (\S+)\t([^\t]*)\t([^\t]*)(?:\t(.*))?$/.exec(line)
|
|
326
341
|
if (consent !== null) {
|
|
327
342
|
const [keys = '', ...permissions] = (consent[4] ?? '').split('\t')
|
|
328
|
-
return {
|
|
343
|
+
return { type: 'needs-consent', sha256: consent[1] as string, install: consent[2] as string, uninstall: consent[3] as string, keys, permissions: permissions.filter((item) => item !== '') }
|
|
329
344
|
}
|
|
330
345
|
const done = /^done (\S+)(?: (\S+))?$/.exec(line)
|
|
331
|
-
if (done !== null) return {
|
|
346
|
+
if (done !== null) return { type: 'done', name: done[1] as string, ...(done[2] === undefined ? {} : { version: done[2] }) }
|
|
332
347
|
const missing = /^missing (\S+)$/.exec(line)
|
|
333
|
-
if (missing !== null) return {
|
|
348
|
+
if (missing !== null) return { type: 'missing', name: missing[1] as string }
|
|
334
349
|
const failed = /^failed (-?\d+)(?:\t(.*))?$/.exec(line)
|
|
335
|
-
if (failed !== null) return {
|
|
336
|
-
|
|
350
|
+
if (failed !== null) return { type: 'failed', code: Number(failed[1]), message: failed[2] ?? '' }
|
|
351
|
+
const retries = /^retries (changed|later)$/.exec(line)
|
|
352
|
+
if (retries !== null) return { type: 'retries', when: retries[1] as 'changed' | 'later' }
|
|
353
|
+
return { type: 'log', text: line.replace(/^log /, '') }
|
|
337
354
|
}
|
|
338
355
|
|
|
339
356
|
export function isAtLeast(version: string, minimum: string): boolean {
|
package/src/register.ts
CHANGED
|
@@ -3,6 +3,7 @@ import type { ModDefinition } from './mod.js'
|
|
|
3
3
|
import type { Options } from './options.js'
|
|
4
4
|
import type { Claude } from './runtime/claude.js'
|
|
5
5
|
import type { RoutedEvent } from './runtime/hooks.js'
|
|
6
|
+
import { cmodPluginName } from './runtime/dependencies.js'
|
|
6
7
|
import { createLifecycle, readPlugin, type Lifecycle } from './runtime/lifecycle.js'
|
|
7
8
|
|
|
8
9
|
let lifecycle: Lifecycle<object>
|
|
@@ -19,7 +20,6 @@ async function startMod($: EngineInterface, eventInput: Frozen<Args<'session.sta
|
|
|
19
20
|
const claude: Claude = {
|
|
20
21
|
plugin: { name: $.plugin.name, root: $.plugin.root },
|
|
21
22
|
ui: {
|
|
22
|
-
toast: (text, options) => $.ui.toast(text, options),
|
|
23
23
|
status: (text) => $.ui.status(text),
|
|
24
24
|
log: (text, options) => $.ui.log(text, options),
|
|
25
25
|
notice: (toolUseId, text) => $.ui.notice(toolUseId, text),
|
|
@@ -29,6 +29,8 @@ async function startMod($: EngineInterface, eventInput: Frozen<Args<'session.sta
|
|
|
29
29
|
close: (pane) => $.ui.close(pane),
|
|
30
30
|
panes: () => $.ui.panes(),
|
|
31
31
|
scroll: (args) => $.ui.scroll(args),
|
|
32
|
+
copy: (args) => $.ui.copy(args),
|
|
33
|
+
focus: (args) => $.ui.focus(args),
|
|
32
34
|
resolve: (render) => $.ui.resolve(render),
|
|
33
35
|
},
|
|
34
36
|
process: {
|
|
@@ -65,11 +67,12 @@ async function startMod($: EngineInterface, eventInput: Frozen<Args<'session.sta
|
|
|
65
67
|
surfaces: () => $.session.surfaces(),
|
|
66
68
|
messages: ((args?: Parameters<Claude['session']['messages']>[0]) => (args === undefined ? $.session.messages() : $.session.messages(args))) as Claude['session']['messages'],
|
|
67
69
|
append: (args) => $.session.append(args),
|
|
70
|
+
send: (args) => $.session.send(args),
|
|
68
71
|
},
|
|
69
72
|
prompt: { submit: (args) => $.prompt.submit(args) },
|
|
70
73
|
model: { complete: (request, options) => $.model.complete(request, options) },
|
|
71
74
|
command: { register: (command) => $.command.register(command) },
|
|
72
|
-
tool: { register: (tool) => $.tool.register(tool), call: (
|
|
75
|
+
tool: { register: (tool) => $.tool.register(tool), call: (input: ToolCallArgs) => $.tool.call(input) },
|
|
73
76
|
agent: {
|
|
74
77
|
list: () => $.agent.list(),
|
|
75
78
|
spawn: (args) => $.agent.spawn(args),
|
|
@@ -81,34 +84,54 @@ async function startMod($: EngineInterface, eventInput: Frozen<Args<'session.sta
|
|
|
81
84
|
},
|
|
82
85
|
cmod: { call: (input) => $.cmod.call(input) },
|
|
83
86
|
}
|
|
84
|
-
await lifecycle.start(claude, readPlugin)
|
|
85
|
-
return
|
|
87
|
+
const [, answer] = await Promise.all([lifecycle.start(claude, readPlugin), passOn(eventInput)])
|
|
88
|
+
return answer
|
|
86
89
|
}
|
|
87
90
|
|
|
88
91
|
function routeToMod<N extends RoutedEvent>(_$: EngineInterface, eventInput: Frozen<Args<N>>, passOn: Next<N>): Promise<EventResult<N>> {
|
|
89
92
|
return lifecycle.route(passOn.event, eventInput, (passed: unknown) => passOn(passed as Args<N>) as Promise<EventResult<N>>)
|
|
90
93
|
}
|
|
91
94
|
|
|
95
|
+
function routeCallToMod(_$: EngineInterface, eventInput: Frozen<Args<'cmod.call'>>, passOn: Next<'cmod.call'>): Promise<EventResult<'cmod.call'>> {
|
|
96
|
+
const called = { ...eventInput, caller: passOn.origin.plugin } as Frozen<Args<'cmod.call'>>
|
|
97
|
+
return lifecycle.route('cmod.call', called, (passed) => passOn(Object.fromEntries(Object.entries(passed).filter(([key]) => key !== 'caller')) as Args<'cmod.call'>))
|
|
98
|
+
}
|
|
99
|
+
|
|
92
100
|
export function registerMod<State extends object, Declared extends Options>(addHook: On, definition: ModDefinition<State, string, Declared>, options: PluginOptions): void {
|
|
93
101
|
checksPermissions = false
|
|
94
102
|
registered = definition as ModDefinition<object>
|
|
95
103
|
lifecycle = createLifecycle(definition, () => checksPermissions, options)
|
|
96
104
|
addHook('session.start', startMod)
|
|
105
|
+
addHook('classic.Setup', routeToMod)
|
|
97
106
|
addHook('classic.SessionStart', routeToMod)
|
|
98
107
|
addHook('classic.SessionEnd', routeToMod)
|
|
99
108
|
addHook('classic.UserPromptSubmit', routeToMod)
|
|
109
|
+
addHook('classic.UserPromptExpansion', routeToMod)
|
|
100
110
|
addHook('classic.InstructionsLoaded', routeToMod)
|
|
111
|
+
addHook('classic.ConfigChange', routeToMod)
|
|
112
|
+
addHook('classic.DirectoryAdded', routeToMod)
|
|
113
|
+
addHook('classic.PreModelSwitch', routeToMod)
|
|
114
|
+
addHook('classic.PostModelSwitch', routeToMod)
|
|
101
115
|
addHook('classic.PermissionDenied', routeToMod)
|
|
102
116
|
addHook('classic.PostToolUse', routeToMod)
|
|
103
117
|
addHook('classic.PostToolUseFailure', routeToMod)
|
|
104
118
|
addHook('classic.PostToolBatch', routeToMod)
|
|
119
|
+
addHook('classic.MessageDisplay', routeToMod)
|
|
105
120
|
addHook('classic.SubagentStart', routeToMod)
|
|
106
121
|
addHook('classic.SubagentStop', routeToMod)
|
|
122
|
+
addHook('classic.TaskCreated', routeToMod)
|
|
123
|
+
addHook('classic.TaskCompleted', routeToMod)
|
|
124
|
+
addHook('classic.TeammateIdle', routeToMod)
|
|
125
|
+
addHook('classic.Elicitation', routeToMod)
|
|
126
|
+
addHook('classic.ElicitationResult', routeToMod)
|
|
107
127
|
addHook('classic.Notification', routeToMod)
|
|
108
128
|
addHook('classic.PreCompact', routeToMod)
|
|
129
|
+
addHook('classic.PostCompact', routeToMod)
|
|
109
130
|
addHook('classic.Stop', routeToMod)
|
|
110
131
|
addHook('classic.StopFailure', routeToMod)
|
|
111
132
|
addHook('classic.FileChanged', routeToMod)
|
|
133
|
+
addHook('classic.WorktreeCreate', routeToMod)
|
|
134
|
+
addHook('classic.WorktreeRemove', routeToMod)
|
|
112
135
|
addHook('tool.call', routeToMod)
|
|
113
136
|
addHook('prompt.submit', routeToMod)
|
|
114
137
|
addHook('prompt.context', routeToMod)
|
|
@@ -119,7 +142,13 @@ export function registerMod<State extends object, Declared extends Options>(addH
|
|
|
119
142
|
addHook('ui.press', routeToMod)
|
|
120
143
|
addHook('ui.scroll', routeToMod)
|
|
121
144
|
addHook('ui.close', routeToMod)
|
|
122
|
-
addHook('cmod.call',
|
|
145
|
+
addHook('cmod.call', routeCallToMod)
|
|
146
|
+
if (definition.name !== cmodPluginName) return
|
|
147
|
+
addHook('session.append', routeToMod)
|
|
148
|
+
addHook('session.compact', routeToMod)
|
|
149
|
+
addHook('session.receive', routeToMod)
|
|
150
|
+
addHook('agent.spawn', routeToMod)
|
|
151
|
+
addHook('prompt.attachment', routeToMod)
|
|
123
152
|
}
|
|
124
153
|
|
|
125
154
|
export function registerPermissionCheck(addHook: On): void {
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { changelogPage, isObject, marketplaceOf, permissionWords } from '../records.js'
|
|
2
|
+
import { changeLines, changelogSections, unreleased } from '../utils/changelog.js'
|
|
3
|
+
import { ifMissing } from '../utils/paths.js'
|
|
4
|
+
import { listed } from '../utils/text.js'
|
|
5
|
+
import type { Claude } from './claude.js'
|
|
6
|
+
import { cmodPluginName } from './dependencies.js'
|
|
7
|
+
import type { Plugin } from './lifecycle.js'
|
|
8
|
+
|
|
9
|
+
export type Announced = { readonly version: string; readonly added: readonly string[] }
|
|
10
|
+
|
|
11
|
+
export type Announcement = { readonly message: string; readonly description?: string; readonly action?: { readonly label: string; readonly onClick: () => unknown } }
|
|
12
|
+
|
|
13
|
+
export const announcementMs = 10_000
|
|
14
|
+
|
|
15
|
+
const shownChanges = 3
|
|
16
|
+
|
|
17
|
+
export function announcedOf(saved: unknown): Announced | undefined {
|
|
18
|
+
if (typeof saved === 'string') return { version: saved, added: [] }
|
|
19
|
+
if (!isObject(saved) || typeof saved['version'] !== 'string') return undefined
|
|
20
|
+
return { version: saved['version'], added: Array.isArray(saved['added']) ? saved['added'].map(String) : [] }
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function announcementOf(claude: Claude, plugin: Plugin, granted: ReadonlySet<string>, added: readonly string[], previous: Announced | undefined): Promise<Announcement> {
|
|
24
|
+
const read = (path: string) => claude.fs.read(`${plugin.root}/${path}`).catch(ifMissing(undefined))
|
|
25
|
+
const manifest = jsonOf(await read('.claude-plugin/plugin.json'))
|
|
26
|
+
const field = (key: string) => (isObject(manifest) && typeof manifest[key] === 'string' ? manifest[key] : undefined)
|
|
27
|
+
const open = (page?: string) => () => claude.cmod.call({ to: cmodPluginName, method: 'openSettings', input: { mod: plugin.name, ...(page === undefined ? {} : { page }) } })
|
|
28
|
+
if (previous === undefined) {
|
|
29
|
+
const { program, keys } = plugin.steps
|
|
30
|
+
const rules = (await claude.fs.list(`${plugin.root}/rules`).catch(ifMissing([]))).filter((entry) => entry.kind === 'file' && entry.name.endsWith('.md')).map((entry) => entry.name.slice(0, -'.md'.length)).toSorted()
|
|
31
|
+
const lines = [
|
|
32
|
+
field('description'),
|
|
33
|
+
added.length > 0 ? `Adds ${listed(added)}.` : undefined,
|
|
34
|
+
rules.length > 0 ? `Adds ${rules.length} ${rules.length === 1 ? 'rule' : 'rules'}: ${listed(rules)}.` : undefined,
|
|
35
|
+
program === undefined ? undefined : `Installed the ${program} program.`,
|
|
36
|
+
...Object.entries(keys ?? {}).map(([key, command]) => `Binds ${key} to /${command}.`),
|
|
37
|
+
...[...granted].map((item) => `${permissionWords(item)}.`),
|
|
38
|
+
]
|
|
39
|
+
return { message: 'Installed', ...described(lines), action: { label: 'Settings', onClick: open() } }
|
|
40
|
+
}
|
|
41
|
+
const version = plugin.version ?? ''
|
|
42
|
+
const isCheckout = marketplaceOf(plugin.root, plugin.name) === undefined
|
|
43
|
+
const sections = changelogSections((await read('CHANGELOG.md')) ?? '')
|
|
44
|
+
const section = sections.find((each) => each.version === version) ?? (isCheckout ? sections.find((each) => each.version === unreleased) : undefined)
|
|
45
|
+
const changes = section === undefined ? [] : changeLines(section.text)
|
|
46
|
+
const nowAdded = added.filter((each) => !previous.added.includes(each))
|
|
47
|
+
const gone = previous.added.filter((each) => !added.includes(each))
|
|
48
|
+
const fallback = previous.added.length === 0 ? [] : [nowAdded.length > 0 ? `Now adds ${listed(nowAdded)}.` : undefined, gone.length > 0 ? `No longer adds ${listed(gone)}.` : undefined]
|
|
49
|
+
const home = await claude.env.home()
|
|
50
|
+
const folder = home !== undefined && plugin.root.startsWith(`${home}/`) ? `~${plugin.root.slice(home.length)}` : plugin.root
|
|
51
|
+
const repository = field('repository')
|
|
52
|
+
const lines = [
|
|
53
|
+
...(changes.length === 0 ? fallback : changes.length > shownChanges ? [...changes.slice(0, shownChanges), '…'] : changes),
|
|
54
|
+
`From ${previous.version}${isCheckout ? `, loaded from ${folder}` : ''}.`,
|
|
55
|
+
repository === undefined || isCheckout ? undefined : `Release notes: ${repository.replace(/\/$/, '')}/releases/tag/v${version}`,
|
|
56
|
+
]
|
|
57
|
+
return {
|
|
58
|
+
message: `Updated to ${version}`,
|
|
59
|
+
...described(lines),
|
|
60
|
+
...(sections.length > 0 ? { action: { label: 'Changelog', onClick: open(changelogPage) } } : {}),
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function jsonOf(text: string | undefined): unknown {
|
|
65
|
+
try {
|
|
66
|
+
return text === undefined ? undefined : JSON.parse(text)
|
|
67
|
+
} catch (error) {
|
|
68
|
+
if (error instanceof SyntaxError) return undefined
|
|
69
|
+
throw error
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function described(lines: readonly (string | undefined)[]): { readonly description?: string } {
|
|
74
|
+
const kept = lines.filter((line) => line !== undefined)
|
|
75
|
+
return kept.length === 0 ? {} : { description: kept.join('\n') }
|
|
76
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Imports } from '../mod.js'
|
|
2
|
+
|
|
3
|
+
let current: Imports | undefined
|
|
4
|
+
|
|
5
|
+
export function bind(imports: Imports): void {
|
|
6
|
+
current = imports
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function bound(): Imports {
|
|
10
|
+
if (current === undefined) throw new Error("A mod's imports work once its setup runs. Call them from setup or from a hook, never at the top of a module.")
|
|
11
|
+
return current
|
|
12
|
+
}
|
package/src/runtime/claude.ts
CHANGED
|
@@ -28,8 +28,11 @@ import type {
|
|
|
28
28
|
Settings,
|
|
29
29
|
SettingsReadArgs,
|
|
30
30
|
Timer,
|
|
31
|
-
ToastOptions,
|
|
32
31
|
ToolSpec,
|
|
32
|
+
UiCopyArgs,
|
|
33
|
+
UiFocusArgs,
|
|
34
|
+
UiFocusResult,
|
|
35
|
+
UiCopyResult,
|
|
33
36
|
UiLogOptions,
|
|
34
37
|
UiOpenResult,
|
|
35
38
|
UiPane,
|
|
@@ -40,7 +43,6 @@ import type {
|
|
|
40
43
|
export type Claude = {
|
|
41
44
|
readonly plugin: { readonly name: string; readonly root: string }
|
|
42
45
|
readonly ui: {
|
|
43
|
-
toast(text: string, options?: ToastOptions): void
|
|
44
46
|
status(text: string | undefined): void
|
|
45
47
|
log(text: string, options?: UiLogOptions): void
|
|
46
48
|
notice(toolUseId: string, text: string | undefined): void
|
|
@@ -50,6 +52,8 @@ export type Claude = {
|
|
|
50
52
|
close(pane: PaneCloseArgs): Promise<void>
|
|
51
53
|
panes(): Promise<readonly UiPane[]>
|
|
52
54
|
scroll(args: UiScrollArgs): Promise<UiScrollResult>
|
|
55
|
+
copy(args: UiCopyArgs): Promise<UiCopyResult>
|
|
56
|
+
focus(args: UiFocusArgs): Promise<UiFocusResult>
|
|
53
57
|
resolve(e: ResolveInput): ElementTable<RenderSurface>
|
|
54
58
|
}
|
|
55
59
|
readonly process: {
|
|
@@ -86,6 +90,7 @@ export type Claude = {
|
|
|
86
90
|
surfaces(): Promise<readonly RenderSurface[]>
|
|
87
91
|
messages: EngineInterface['session']['messages']
|
|
88
92
|
append: EngineInterface['session']['append']
|
|
93
|
+
send: EngineInterface['session']['send']
|
|
89
94
|
}
|
|
90
95
|
readonly prompt: { submit: EngineInterface['prompt']['submit'] }
|
|
91
96
|
readonly model: { complete: EngineInterface['model']['complete'] }
|