@cmodjs/core 0.3.0 → 1.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 +50 -10
- package/docs/dependencies.md +12 -12
- package/docs/hooks.md +39 -27
- package/docs/index.md +29 -25
- package/docs/install-steps.md +9 -7
- package/docs/jobs.md +59 -111
- package/docs/mod.md +200 -174
- package/docs/options.md +2 -2
- package/docs/permissions.md +27 -16
- package/docs/session.md +248 -0
- package/docs/state.md +11 -7
- package/docs/testing.md +65 -22
- package/docs/ui.md +108 -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 +8 -3
- 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 +154 -75
- package/mod.d.ts.map +1 -1
- package/mod.js +94 -3
- package/options.d.ts +2 -2
- package/options.js +19 -19
- package/package.json +3 -3
- package/records.d.ts +10 -8
- package/records.d.ts.map +1 -1
- package/records.js +28 -17
- package/register.d.ts.map +1 -1
- package/register.js +17 -5
- package/runtime/announce.d.ts +18 -0
- package/runtime/announce.d.ts.map +1 -0
- package/runtime/announce.js +66 -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 +3 -2
- package/runtime/claude.d.ts.map +1 -1
- package/runtime/conversations.d.ts +16 -0
- package/runtime/conversations.d.ts.map +1 -0
- package/runtime/conversations.js +291 -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 +9 -0
- package/runtime/engine.d.ts +17 -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 +63 -26
- package/runtime/hooks.d.ts +5 -4
- package/runtime/hooks.d.ts.map +1 -1
- package/runtime/hooks.js +10 -5
- package/runtime/installer.d.ts.map +1 -1
- package/runtime/installer.js +11 -9
- package/runtime/lifecycle.d.ts +3 -1
- package/runtime/lifecycle.d.ts.map +1 -1
- package/runtime/lifecycle.js +316 -185
- package/runtime/metadata.d.ts +3 -1
- package/runtime/metadata.d.ts.map +1 -1
- package/runtime/metadata.js +21 -11
- 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 +36 -0
- package/runtime/sessions.d.ts.map +1 -0
- package/runtime/sessions.js +235 -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 +31 -0
- package/runtime/toaster.d.ts.map +1 -0
- package/runtime/toaster.js +176 -0
- package/runtime/ui.d.ts +6 -17
- package/runtime/ui.d.ts.map +1 -1
- package/runtime/ui.js +36 -112
- package/session/context/context.d.ts +10 -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 +54 -0
- package/session/context/turn/message/message.d.ts.map +1 -0
- package/session/context/turn/message/message.js +137 -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 +12 -0
- package/session/context/turn/turn.d.ts.map +1 -0
- package/session/context/turn/turn.js +2 -0
- package/session/conversation.d.ts +64 -0
- package/session/conversation.d.ts.map +1 -0
- package/session/conversation.js +123 -0
- package/session/goal/goal.d.ts +10 -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 +99 -0
- package/session/session.d.ts.map +1 -0
- package/session/session.js +1 -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 +7 -3
- 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 +209 -97
- package/src/options.ts +22 -22
- package/src/records.ts +35 -23
- package/src/register.ts +17 -5
- package/src/runtime/announce.ts +74 -0
- package/src/runtime/bound.ts +12 -0
- package/src/runtime/claude.ts +4 -2
- package/src/runtime/conversations.ts +302 -0
- package/src/runtime/deadline.ts +2 -2
- package/src/runtime/dependencies.ts +12 -0
- package/src/runtime/engine.ts +22 -0
- package/src/runtime/grants.ts +66 -24
- package/src/runtime/hooks.ts +19 -10
- package/src/runtime/installer.ts +10 -9
- package/src/runtime/lifecycle.ts +312 -180
- package/src/runtime/metadata.ts +33 -11
- 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 +242 -0
- package/src/runtime/state.ts +50 -14
- package/src/runtime/toaster.ts +218 -0
- package/src/runtime/ui.ts +42 -139
- package/src/session/context/context.ts +12 -0
- package/src/session/context/turn/message/document.ts +8 -0
- package/src/session/context/turn/message/message.ts +165 -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 +14 -0
- package/src/session/conversation.ts +169 -0
- package/src/session/goal/goal.ts +9 -0
- package/src/session/goal/task.ts +19 -0
- package/src/session/session.ts +94 -0
- package/src/testing/fake-claude.ts +22 -16
- package/src/testing/fake-elements.ts +3 -2
- package/src/testing/fake-files.ts +1 -1
- package/src/testing/fake-sessions.ts +203 -0
- package/src/testing.ts +112 -33
- 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 +3 -3
- package/src/utils/changelog.ts +32 -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 +15 -1
- package/src/utils/text.ts +4 -0
- package/testing/fake-claude.d.ts +4 -1
- package/testing/fake-claude.d.ts.map +1 -1
- package/testing/fake-claude.js +18 -11
- package/testing/fake-elements.js +4 -2
- package/testing/fake-files.js +1 -1
- package/testing/fake-sessions.d.ts +74 -0
- package/testing/fake-sessions.d.ts.map +1 -0
- package/testing/fake-sessions.js +170 -0
- package/testing.d.ts +33 -13
- package/testing.d.ts.map +1 -1
- package/testing.js +92 -21
- 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 +1 -1
- package/utils/call-effects.d.ts.map +1 -1
- package/utils/call-effects.js +2 -2
- package/utils/changelog.d.ts +9 -0
- package/utils/changelog.d.ts.map +1 -0
- package/utils/changelog.js +29 -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 +2 -0
- package/utils/paths.d.ts.map +1 -1
- package/utils/paths.js +13 -1
- 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/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,7 @@ 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),
|
|
32
33
|
resolve: (render) => $.ui.resolve(render),
|
|
33
34
|
},
|
|
34
35
|
process: {
|
|
@@ -65,11 +66,12 @@ async function startMod($: EngineInterface, eventInput: Frozen<Args<'session.sta
|
|
|
65
66
|
surfaces: () => $.session.surfaces(),
|
|
66
67
|
messages: ((args?: Parameters<Claude['session']['messages']>[0]) => (args === undefined ? $.session.messages() : $.session.messages(args))) as Claude['session']['messages'],
|
|
67
68
|
append: (args) => $.session.append(args),
|
|
69
|
+
send: (args) => $.session.send(args),
|
|
68
70
|
},
|
|
69
71
|
prompt: { submit: (args) => $.prompt.submit(args) },
|
|
70
72
|
model: { complete: (request, options) => $.model.complete(request, options) },
|
|
71
73
|
command: { register: (command) => $.command.register(command) },
|
|
72
|
-
tool: { register: (tool) => $.tool.register(tool), call: (
|
|
74
|
+
tool: { register: (tool) => $.tool.register(tool), call: (input: ToolCallArgs) => $.tool.call(input) },
|
|
73
75
|
agent: {
|
|
74
76
|
list: () => $.agent.list(),
|
|
75
77
|
spawn: (args) => $.agent.spawn(args),
|
|
@@ -81,14 +83,19 @@ async function startMod($: EngineInterface, eventInput: Frozen<Args<'session.sta
|
|
|
81
83
|
},
|
|
82
84
|
cmod: { call: (input) => $.cmod.call(input) },
|
|
83
85
|
}
|
|
84
|
-
await lifecycle.start(claude, readPlugin)
|
|
85
|
-
return
|
|
86
|
+
const [, answer] = await Promise.all([lifecycle.start(claude, readPlugin), passOn(eventInput)])
|
|
87
|
+
return answer
|
|
86
88
|
}
|
|
87
89
|
|
|
88
90
|
function routeToMod<N extends RoutedEvent>(_$: EngineInterface, eventInput: Frozen<Args<N>>, passOn: Next<N>): Promise<EventResult<N>> {
|
|
89
91
|
return lifecycle.route(passOn.event, eventInput, (passed: unknown) => passOn(passed as Args<N>) as Promise<EventResult<N>>)
|
|
90
92
|
}
|
|
91
93
|
|
|
94
|
+
function routeCallToMod(_$: EngineInterface, eventInput: Frozen<Args<'cmod.call'>>, passOn: Next<'cmod.call'>): Promise<EventResult<'cmod.call'>> {
|
|
95
|
+
const called = { ...eventInput, caller: passOn.origin.plugin } as Frozen<Args<'cmod.call'>>
|
|
96
|
+
return lifecycle.route('cmod.call', called, (passed) => passOn(Object.fromEntries(Object.entries(passed).filter(([key]) => key !== 'caller')) as Args<'cmod.call'>))
|
|
97
|
+
}
|
|
98
|
+
|
|
92
99
|
export function registerMod<State extends object, Declared extends Options>(addHook: On, definition: ModDefinition<State, string, Declared>, options: PluginOptions): void {
|
|
93
100
|
checksPermissions = false
|
|
94
101
|
registered = definition as ModDefinition<object>
|
|
@@ -119,7 +126,12 @@ export function registerMod<State extends object, Declared extends Options>(addH
|
|
|
119
126
|
addHook('ui.press', routeToMod)
|
|
120
127
|
addHook('ui.scroll', routeToMod)
|
|
121
128
|
addHook('ui.close', routeToMod)
|
|
122
|
-
addHook('cmod.call',
|
|
129
|
+
addHook('cmod.call', routeCallToMod)
|
|
130
|
+
if (definition.name !== cmodPluginName) return
|
|
131
|
+
addHook('session.append', routeToMod)
|
|
132
|
+
addHook('session.compact', routeToMod)
|
|
133
|
+
addHook('session.receive', routeToMod)
|
|
134
|
+
addHook('agent.spawn', routeToMod)
|
|
123
135
|
}
|
|
124
136
|
|
|
125
137
|
export function registerPermissionCheck(addHook: On): void {
|
|
@@ -0,0 +1,74 @@
|
|
|
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 lines = [
|
|
31
|
+
field('description'),
|
|
32
|
+
added.length > 0 ? `Adds ${listed(added)}.` : undefined,
|
|
33
|
+
program === undefined ? undefined : `Installed the ${program} program.`,
|
|
34
|
+
...Object.entries(keys ?? {}).map(([key, command]) => `Binds ${key} to /${command}.`),
|
|
35
|
+
...[...granted].map((item) => `${permissionWords(item)}.`),
|
|
36
|
+
]
|
|
37
|
+
return { message: 'Installed', ...described(lines), action: { label: 'Settings', onClick: open() } }
|
|
38
|
+
}
|
|
39
|
+
const version = plugin.version ?? ''
|
|
40
|
+
const isCheckout = marketplaceOf(plugin.root, plugin.name) === undefined
|
|
41
|
+
const sections = changelogSections((await read('CHANGELOG.md')) ?? '')
|
|
42
|
+
const section = sections.find((each) => each.version === version) ?? (isCheckout ? sections.find((each) => each.version === unreleased) : undefined)
|
|
43
|
+
const changes = section === undefined ? [] : changeLines(section.text)
|
|
44
|
+
const nowAdded = added.filter((each) => !previous.added.includes(each))
|
|
45
|
+
const gone = previous.added.filter((each) => !added.includes(each))
|
|
46
|
+
const fallback = previous.added.length === 0 ? [] : [nowAdded.length > 0 ? `Now adds ${listed(nowAdded)}.` : undefined, gone.length > 0 ? `No longer adds ${listed(gone)}.` : undefined]
|
|
47
|
+
const home = await claude.env.home()
|
|
48
|
+
const folder = home !== undefined && plugin.root.startsWith(`${home}/`) ? `~${plugin.root.slice(home.length)}` : plugin.root
|
|
49
|
+
const repository = field('repository')
|
|
50
|
+
const lines = [
|
|
51
|
+
...(changes.length === 0 ? fallback : changes.length > shownChanges ? [...changes.slice(0, shownChanges), '…'] : changes),
|
|
52
|
+
`From ${previous.version}${isCheckout ? `, loaded from ${folder}` : ''}.`,
|
|
53
|
+
repository === undefined || isCheckout ? undefined : `Release notes: ${repository.replace(/\/$/, '')}/releases/tag/v${version}`,
|
|
54
|
+
]
|
|
55
|
+
return {
|
|
56
|
+
message: `Updated to ${version}`,
|
|
57
|
+
...described(lines),
|
|
58
|
+
...(sections.length > 0 ? { action: { label: 'Changelog', onClick: open(changelogPage) } } : {}),
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function jsonOf(text: string | undefined): unknown {
|
|
63
|
+
try {
|
|
64
|
+
return text === undefined ? undefined : JSON.parse(text)
|
|
65
|
+
} catch (error) {
|
|
66
|
+
if (error instanceof SyntaxError) return undefined
|
|
67
|
+
throw error
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function described(lines: readonly (string | undefined)[]): { readonly description?: string } {
|
|
72
|
+
const kept = lines.filter((line) => line !== undefined)
|
|
73
|
+
return kept.length === 0 ? {} : { description: kept.join('\n') }
|
|
74
|
+
}
|
|
@@ -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,9 @@ import type {
|
|
|
28
28
|
Settings,
|
|
29
29
|
SettingsReadArgs,
|
|
30
30
|
Timer,
|
|
31
|
-
ToastOptions,
|
|
32
31
|
ToolSpec,
|
|
32
|
+
UiCopyArgs,
|
|
33
|
+
UiCopyResult,
|
|
33
34
|
UiLogOptions,
|
|
34
35
|
UiOpenResult,
|
|
35
36
|
UiPane,
|
|
@@ -40,7 +41,6 @@ import type {
|
|
|
40
41
|
export type Claude = {
|
|
41
42
|
readonly plugin: { readonly name: string; readonly root: string }
|
|
42
43
|
readonly ui: {
|
|
43
|
-
toast(text: string, options?: ToastOptions): void
|
|
44
44
|
status(text: string | undefined): void
|
|
45
45
|
log(text: string, options?: UiLogOptions): void
|
|
46
46
|
notice(toolUseId: string, text: string | undefined): void
|
|
@@ -50,6 +50,7 @@ export type Claude = {
|
|
|
50
50
|
close(pane: PaneCloseArgs): Promise<void>
|
|
51
51
|
panes(): Promise<readonly UiPane[]>
|
|
52
52
|
scroll(args: UiScrollArgs): Promise<UiScrollResult>
|
|
53
|
+
copy(args: UiCopyArgs): Promise<UiCopyResult>
|
|
53
54
|
resolve(e: ResolveInput): ElementTable<RenderSurface>
|
|
54
55
|
}
|
|
55
56
|
readonly process: {
|
|
@@ -86,6 +87,7 @@ export type Claude = {
|
|
|
86
87
|
surfaces(): Promise<readonly RenderSurface[]>
|
|
87
88
|
messages: EngineInterface['session']['messages']
|
|
88
89
|
append: EngineInterface['session']['append']
|
|
90
|
+
send: EngineInterface['session']['send']
|
|
89
91
|
}
|
|
90
92
|
readonly prompt: { submit: EngineInterface['prompt']['submit'] }
|
|
91
93
|
readonly model: { complete: EngineInterface['model']['complete'] }
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
import type { Args, EventResult, Frozen } from 'claude-code'
|
|
2
|
+
import { consentPath, dataFolder, isObject, parseConsent } from '../records.js'
|
|
3
|
+
import { Message, rowOfAppend, type Row } from '../session/context/turn/message/message.js'
|
|
4
|
+
import { Note } from '../session/context/turn/message/note.js'
|
|
5
|
+
import { Prompt } from '../session/context/turn/message/prompt.js'
|
|
6
|
+
import { Person } from '../session/context/turn/message/sender.js'
|
|
7
|
+
import { createConversation, emptyConversation, type Conversation, type ConversationState, type ContextData, type SessionChange, type SessionData, type TaskData, type TurnData } from '../session/conversation.js'
|
|
8
|
+
import { receivedMatcher } from '../utils/matcher.js'
|
|
9
|
+
import { ifMissing } from '../utils/paths.js'
|
|
10
|
+
import { formatExit, messageOf } from '../utils/text.js'
|
|
11
|
+
import type { Claude } from './claude.js'
|
|
12
|
+
import { cmodPluginName } from './dependencies.js'
|
|
13
|
+
import { readLines, runCmod } from './programs.js'
|
|
14
|
+
import type { Router } from './router.js'
|
|
15
|
+
import { conversationMethods, isHeardBy, type Subscription } from './sessions.js'
|
|
16
|
+
|
|
17
|
+
type Request = Readonly<Record<string, unknown>>
|
|
18
|
+
|
|
19
|
+
type Heard = { readonly event: Subscription['event']; readonly matcher: unknown }
|
|
20
|
+
|
|
21
|
+
const delivery = /^<cmod-delivery>([\s\S]*)<\/cmod-delivery>$/
|
|
22
|
+
|
|
23
|
+
const bufferedTaskRows = 200
|
|
24
|
+
|
|
25
|
+
export type Conversations = {
|
|
26
|
+
answer(e: Frozen<Args<'cmod.call'>> & { readonly caller?: string }): Promise<EventResult<'cmod.call'> | undefined>
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function serveConversations({ claude, router, store, project, report }: { readonly claude: Claude; readonly router: Router; readonly store: string; readonly project: () => string; readonly report: (id: string, message: string, error: unknown) => void }): Conversations {
|
|
30
|
+
const goals = `${dataFolder(store, cmodPluginName)}/goals`
|
|
31
|
+
const heard = new Map<string, Map<string, readonly Heard[]>>()
|
|
32
|
+
let session = ''
|
|
33
|
+
let conversation: Conversation = createConversation(emptyConversation(''))
|
|
34
|
+
let starting: Promise<void> = Promise.resolve()
|
|
35
|
+
let live: Message[] = []
|
|
36
|
+
const liveIds = new Set<string>()
|
|
37
|
+
const waitingRows = new Map<string, Row[]>()
|
|
38
|
+
let follower: { readonly sessions: string; stop(): void } | undefined
|
|
39
|
+
|
|
40
|
+
const json = async <Value>(args: readonly string[]): Promise<Value> => JSON.parse(await runCmod(claude, [...args, '--json', '--project', project()], session === '' ? await claude.session.id() : session)) as Value
|
|
41
|
+
|
|
42
|
+
const isGranted = async (caller: string | undefined, item: string) => {
|
|
43
|
+
if (caller === cmodPluginName) return true
|
|
44
|
+
if (caller === undefined) return false
|
|
45
|
+
const path = consentPath(store)
|
|
46
|
+
const text = await claude.fs.read(path).catch(ifMissing('{}'))
|
|
47
|
+
return (parseConsent(JSON.parse(text), path)[caller] ?? []).includes(item)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const dispatch = (to: string, changes: readonly SessionChange[]) => {
|
|
51
|
+
for (const change of changes) {
|
|
52
|
+
if (change.type === Message && !liveIds.has(change.message.id) && to === session) {
|
|
53
|
+
live.push(change.message)
|
|
54
|
+
liveIds.add(change.message.id)
|
|
55
|
+
}
|
|
56
|
+
for (const [mod, sessions] of heard) {
|
|
57
|
+
const subscriptions = sessions.get(to) ?? []
|
|
58
|
+
if (!subscriptions.some(({ event, matcher }) => isHeardBy(event, matcher, change))) continue
|
|
59
|
+
void claude.cmod.call({ to: mod, method: conversationMethods.changed, input: { session: to, change } }).catch((error: unknown) => report(`changed:${mod}`, `Could not tell ${mod} about session ${to}`, error))
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const restartFollower = () => {
|
|
65
|
+
const others = [...new Set([...heard.values()].flatMap((sessions) => [...sessions].filter(([id, list]) => id !== session && list.length > 0).map(([id]) => id)))].sort()
|
|
66
|
+
const wanted = others.join(' ')
|
|
67
|
+
if (follower?.sessions === wanted) return
|
|
68
|
+
follower?.stop()
|
|
69
|
+
follower = undefined
|
|
70
|
+
if (others.length === 0) return
|
|
71
|
+
const stream = claude.process.spawn({ argv: ['cmod', 'history', 'follow', ...others, '--json', '--project', project()], env: { CLAUDE_CODE_SESSION_ID: session } })
|
|
72
|
+
const started = { sessions: wanted, stop: () => void stream.return(undefined as never) }
|
|
73
|
+
follower = started
|
|
74
|
+
void readLines(stream, (line) => {
|
|
75
|
+
if (line.trim() === '') return
|
|
76
|
+
const parsed = JSON.parse(line) as { readonly session: string; readonly change: SessionChange }
|
|
77
|
+
dispatch(parsed.session, [parsed.change])
|
|
78
|
+
})
|
|
79
|
+
.then(({ code, lastError }) => {
|
|
80
|
+
if (follower === started) throw new Error(`cmod history follow ${formatExit(code, lastError)}`)
|
|
81
|
+
})
|
|
82
|
+
.catch((error: unknown) => {
|
|
83
|
+
if (follower !== started) return
|
|
84
|
+
follower = undefined
|
|
85
|
+
report('follow', 'Stopped hearing the other sessions', error)
|
|
86
|
+
})
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const start = (id: string, source: string) => {
|
|
90
|
+
if (id === session && source !== 'compact') return
|
|
91
|
+
if (id === session && source === 'compact') {
|
|
92
|
+
dispatch(session, conversation.startContext(new Date().toISOString()))
|
|
93
|
+
return
|
|
94
|
+
}
|
|
95
|
+
session = id
|
|
96
|
+
live = []
|
|
97
|
+
liveIds.clear()
|
|
98
|
+
waitingRows.clear()
|
|
99
|
+
conversation = createConversation(emptyConversation(id))
|
|
100
|
+
starting =
|
|
101
|
+
source === 'startup' || source === 'clear'
|
|
102
|
+
? Promise.resolve(dispatch(id, conversation.startContext(new Date().toISOString())))
|
|
103
|
+
: json<ConversationState>(['history', 'state', id]).then(
|
|
104
|
+
(state) => {
|
|
105
|
+
if (session === id) conversation = createConversation(state)
|
|
106
|
+
},
|
|
107
|
+
(error: unknown) => report('state', `Starts session ${id} without its earlier turns`, error),
|
|
108
|
+
)
|
|
109
|
+
restartFollower()
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const take = async (row: Row, task: string | undefined) => {
|
|
113
|
+
await starting
|
|
114
|
+
if (task !== undefined && conversation.state.tasks[task] === undefined) {
|
|
115
|
+
const waiting = waitingRows.get(task) ?? []
|
|
116
|
+
waiting.push(row)
|
|
117
|
+
waitingRows.set(task, waiting)
|
|
118
|
+
if (waiting.length < bufferedTaskRows) return
|
|
119
|
+
waitingRows.delete(task)
|
|
120
|
+
dispatch(session, conversation.startTask({ id: task, title: '', agentType: 'general-purpose', openedBy: '', openedAt: row.at }))
|
|
121
|
+
for (const each of waiting) dispatch(session, conversation.add(each, task))
|
|
122
|
+
return
|
|
123
|
+
}
|
|
124
|
+
dispatch(session, conversation.add(row, task))
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
router.add('classic.SessionStart', async (e, next) => {
|
|
128
|
+
start(e.session_id, e.source)
|
|
129
|
+
return next(e)
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
router.add('session.append', async (e, next) => {
|
|
133
|
+
const kept = await next(e)
|
|
134
|
+
if (kept.deny !== undefined || session === '') return kept
|
|
135
|
+
const cwd = e.message.type === 'assistant' ? await claude.session.cwd() : ''
|
|
136
|
+
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))
|
|
137
|
+
return kept
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
router.add('session.compact', async (e, next) => {
|
|
141
|
+
const compacted = await next(e)
|
|
142
|
+
if (e.agentId === undefined && e.trigger !== 'precompute' && compacted.skip === undefined) {
|
|
143
|
+
await starting
|
|
144
|
+
dispatch(session, conversation.endContext(new Date().toISOString()))
|
|
145
|
+
}
|
|
146
|
+
return compacted
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
router.add('agent.spawn', async (e, next) => {
|
|
150
|
+
const spawned = await next(e)
|
|
151
|
+
if (spawned.deny !== undefined || spawned.agentId === undefined) return spawned
|
|
152
|
+
const id = spawned.agentId
|
|
153
|
+
const opened: TaskData = { id, title: e.description, agentType: e.subagentType, model: spawned.model, openedBy: e.tool_use_id, openedAt: new Date().toISOString(), ...(e.parentAgentId === undefined ? {} : { parent: e.parentAgentId }) }
|
|
154
|
+
await starting
|
|
155
|
+
dispatch(session, conversation.startTask(opened))
|
|
156
|
+
const waiting = waitingRows.get(id) ?? []
|
|
157
|
+
waitingRows.delete(id)
|
|
158
|
+
for (const row of waiting) dispatch(session, conversation.add(row, id))
|
|
159
|
+
return spawned
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
router.add('session.receive', async (e, next) => {
|
|
163
|
+
const isFromCmod = e.origin.kind === 'peer' && !('teammate' in e.origin) && e.origin.plugin === cmodPluginName
|
|
164
|
+
const sent = isFromCmod ? delivery.exec(e.text)?.[1] : undefined
|
|
165
|
+
if (sent === undefined) return next(e)
|
|
166
|
+
const parsed: unknown = JSON.parse(sent)
|
|
167
|
+
if (!isObject(parsed) || typeof parsed['text'] !== 'string') return next(e)
|
|
168
|
+
const { type, text, as } = parsed
|
|
169
|
+
claude.clock.after(0, () => {
|
|
170
|
+
const delivered = type === Note ? claude.session.append({ message: { type: 'user', content: [{ type: 'text', text }] } }) : claude.prompt.submit({ text, ...(as === Person ? { asUser: true } : {}) })
|
|
171
|
+
void delivered.catch((error: unknown) => report('delivery', 'Could not deliver a message from another session', error))
|
|
172
|
+
})
|
|
173
|
+
return { consumed: 'cmod delivery' }
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
const merged = <Item>(saved: readonly Item[], kept: readonly Item[], key: (item: Item) => string): Item[] => {
|
|
177
|
+
const keptKeys = new Set(kept.map(key))
|
|
178
|
+
return [...saved.filter((item) => !keptKeys.has(key(item))), ...kept]
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const liveTurns = (): TurnData[] => {
|
|
182
|
+
const turn = conversation.state.turn
|
|
183
|
+
return turn === undefined ? [] : [turn]
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const liveTasks = (): TaskData[] => Object.values(conversation.state.tasks).map(({ context: _context, ...task }) => task)
|
|
187
|
+
|
|
188
|
+
const liveContext = (): ContextData[] => {
|
|
189
|
+
const { context, contextOpenedAt } = conversation.state
|
|
190
|
+
return context === 0 ? [] : [{ number: context, openedAt: contextOpenedAt }]
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const lastOf = <Item>(items: readonly Item[], last: unknown): Item[] => (typeof last === 'number' ? items.slice(-last) : [...items])
|
|
194
|
+
|
|
195
|
+
const methods: Readonly<Record<string, (request: Request, caller: string | undefined) => Promise<unknown>>> = {
|
|
196
|
+
async [conversationMethods.sessions](request) {
|
|
197
|
+
const found = await json<SessionData[]>(['history', 'sessions', ...scopeArgs(request), ...(request['running'] === true ? ['--running'] : [])])
|
|
198
|
+
return found.map((each) => (each.id === session ? { ...each, isRunning: true } : each))
|
|
199
|
+
},
|
|
200
|
+
async [conversationMethods.search](request) {
|
|
201
|
+
const before = isObject(request['before']) ? request['before'] : undefined
|
|
202
|
+
return json(['history', 'search', String(request['text']), ...scopeArgs(request), ...(request['running'] === true ? ['--running'] : []), ...countArg('--limit', request['limit']), ...(before === undefined ? [] : ['--before', `${String(before['session'])}:${String(before['id'])}`])])
|
|
203
|
+
},
|
|
204
|
+
async [conversationMethods.messages](request) {
|
|
205
|
+
const id = String(request['session'])
|
|
206
|
+
const filters = [...textArg('--turn', request['turn']), ...textArg('--task', request['task']), ...textArg('--after', request['after'])]
|
|
207
|
+
const saved = await json<Message[]>(['history', 'messages', id, ...filters, ...countArg('--last', request['last'])])
|
|
208
|
+
if (id !== session) return saved
|
|
209
|
+
await starting
|
|
210
|
+
const fits = (message: Message) => (request['turn'] === undefined || message.turn === request['turn']) && message.task === request['task']
|
|
211
|
+
const after = typeof request['after'] === 'string' ? live.findIndex((message) => message.id === request['after']) : -1
|
|
212
|
+
const kept = live.slice(after + 1).filter(fits)
|
|
213
|
+
return lastOf(merged(saved, kept, (message) => message.id), request['last'])
|
|
214
|
+
},
|
|
215
|
+
async [conversationMethods.turns](request) {
|
|
216
|
+
const id = String(request['session'])
|
|
217
|
+
const saved = await json<TurnData[]>(['history', 'turns', id, ...countArg('--context', request['context']), ...countArg('--last', request['last'])])
|
|
218
|
+
if (id !== session) return saved
|
|
219
|
+
await starting
|
|
220
|
+
const kept = liveTurns().filter((turn) => request['context'] === undefined || turn.context === request['context'])
|
|
221
|
+
return lastOf(merged(saved, kept, (turn) => turn.prompt.id), request['last'])
|
|
222
|
+
},
|
|
223
|
+
async [conversationMethods.contexts](request) {
|
|
224
|
+
const id = String(request['session'])
|
|
225
|
+
const saved = await json<ContextData[]>(['history', 'contexts', id])
|
|
226
|
+
if (id !== session) return saved
|
|
227
|
+
await starting
|
|
228
|
+
return merged(saved, liveContext(), (context) => String(context.number))
|
|
229
|
+
},
|
|
230
|
+
async [conversationMethods.tasks](request) {
|
|
231
|
+
const id = String(request['session'])
|
|
232
|
+
const saved = await json<TaskData[]>(['history', 'tasks', id, ...textArg('--task', request['task'])])
|
|
233
|
+
if (id !== session) return saved
|
|
234
|
+
await starting
|
|
235
|
+
const kept = liveTasks().filter((task) => task.parent === request['task'])
|
|
236
|
+
return merged(saved, kept, (task) => task.id)
|
|
237
|
+
},
|
|
238
|
+
async [conversationMethods.goal](request) {
|
|
239
|
+
const id = String(request['session'])
|
|
240
|
+
if (id === session) {
|
|
241
|
+
await starting
|
|
242
|
+
const text = conversation.state.goal
|
|
243
|
+
if (text !== undefined) return { text }
|
|
244
|
+
}
|
|
245
|
+
return json(['history', 'goal', id])
|
|
246
|
+
},
|
|
247
|
+
async [conversationMethods.updateGoal](request) {
|
|
248
|
+
const id = String(request['session'])
|
|
249
|
+
const text = String(request['text'])
|
|
250
|
+
await claude.fs.write(`${goals}/${id}.txt`, text)
|
|
251
|
+
if (id === session) {
|
|
252
|
+
await starting
|
|
253
|
+
dispatch(session, conversation.changeGoal(text))
|
|
254
|
+
}
|
|
255
|
+
return null
|
|
256
|
+
},
|
|
257
|
+
async [conversationMethods.subscribe](request, caller) {
|
|
258
|
+
if (caller === undefined) throw new Error('cmod hears no caller for this subscription.')
|
|
259
|
+
const id = String(request['session'])
|
|
260
|
+
const list = Array.isArray(request['subscriptions']) ? (request['subscriptions'] as readonly Subscription[]) : []
|
|
261
|
+
const sessions = heard.get(caller) ?? new Map<string, readonly Heard[]>()
|
|
262
|
+
sessions.set(id, list.map(({ event, matcher }) => ({ event, matcher: matcher === undefined ? undefined : receivedMatcher(matcher) })))
|
|
263
|
+
heard.set(caller, sessions)
|
|
264
|
+
restartFollower()
|
|
265
|
+
return null
|
|
266
|
+
},
|
|
267
|
+
async [conversationMethods.send](request, caller) {
|
|
268
|
+
if (!(await isGranted(caller, 'prompt'))) throw new Error(`${caller ?? 'A mod'} sends to another session without your grant to send prompts.`)
|
|
269
|
+
const type = request['type'] === Note ? Note : Prompt
|
|
270
|
+
const sent = await claude.session.send({ to: { sessionId: String(request['session']) }, text: `<cmod-delivery>${JSON.stringify({ type, text: request['text'], as: request['as'] })}</cmod-delivery>` })
|
|
271
|
+
if (sent.isDelivered !== true) throw new Error(`Claude Code could not deliver the message to session ${String(request['session'])}.`)
|
|
272
|
+
return null
|
|
273
|
+
},
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return {
|
|
277
|
+
async answer(e) {
|
|
278
|
+
const method = methods[e.method]
|
|
279
|
+
if (method === undefined) return undefined
|
|
280
|
+
const input = isObject(e.input) ? e.input : {}
|
|
281
|
+
if (e.method !== conversationMethods.send && !(await isGranted(e.caller, 'conversation'))) return { deny: `${e.caller ?? 'A mod'} reads your conversations without your grant. Turn it on in /mods ${e.caller ?? ''}.`.trim() }
|
|
282
|
+
try {
|
|
283
|
+
return { value: await method(input, e.caller) }
|
|
284
|
+
} catch (error) {
|
|
285
|
+
return { deny: messageOf(error) }
|
|
286
|
+
}
|
|
287
|
+
},
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function scopeArgs(request: Request): string[] {
|
|
292
|
+
return [...(typeof request['since'] === 'string' ? ['--since', request['since']] : []), ...(request['everyProject'] === true ? ['--all'] : [])]
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function countArg(flag: string, value: unknown): string[] {
|
|
296
|
+
return typeof value === 'number' ? [flag, String(value)] : []
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function textArg(flag: string, value: unknown): string[] {
|
|
300
|
+
return typeof value === 'string' ? [flag, value] : []
|
|
301
|
+
}
|
|
302
|
+
|
package/src/runtime/deadline.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import type { Claude } from './claude.js'
|
|
2
2
|
|
|
3
|
-
export type Deadline = { readonly ms: number; readonly job?: string
|
|
3
|
+
export type Deadline = { readonly ms: number; readonly job?: string }
|
|
4
4
|
|
|
5
5
|
export const longestMs = 600_000
|
|
6
6
|
|
|
7
|
-
export const toolDeadline: Deadline = { ms: 30000,
|
|
7
|
+
export const toolDeadline: Deadline = { ms: 30000, job: 'tool' }
|
|
8
8
|
|
|
9
9
|
export function beforeDeadline<Value>(claude: Claude, { ms, job }: Deadline, call: string, task: Promise<Value>): Promise<Value> {
|
|
10
10
|
return new Promise<Value>((resolve, reject) => {
|
|
@@ -2,6 +2,8 @@ import type { Args, CmodDependencies, EventResult, Frozen } from 'claude-code'
|
|
|
2
2
|
import { messageOf } from '../utils/text.js'
|
|
3
3
|
import type { Claude } from './claude.js'
|
|
4
4
|
|
|
5
|
+
export const cmodPluginName = 'cmod'
|
|
6
|
+
|
|
5
7
|
export async function answerCall(name: string, api: { readonly [method: string]: (input: never) => unknown }, e: Frozen<Args<'cmod.call'>>): Promise<EventResult<'cmod.call'>> {
|
|
6
8
|
const method = Object.hasOwn(api, e.method) ? api[e.method] : undefined
|
|
7
9
|
if (method === undefined) return { deny: `${name} has no method ${e.method}.` }
|
|
@@ -21,6 +23,16 @@ export function dependencyCalls(claude: Claude, within: (call: string, task: Pro
|
|
|
21
23
|
return new Proxy({}, { get: (_dependencies, to) => (typeof to === 'string' && to !== 'then' ? methodsOf(to) : undefined) }) as CmodDependencies
|
|
22
24
|
}
|
|
23
25
|
|
|
26
|
+
export function withBareRefusals(claude: Claude): Claude {
|
|
27
|
+
const prefix = `${claude.plugin.name}: $.cmod.call: `
|
|
28
|
+
const call: Claude['cmod']['call'] = (input) =>
|
|
29
|
+
claude.cmod.call(input).catch((error: unknown) => {
|
|
30
|
+
const message = messageOf(error)
|
|
31
|
+
throw message.startsWith(prefix) ? new Error(message.slice(prefix.length), { cause: error }) : error
|
|
32
|
+
})
|
|
33
|
+
return { ...claude, cmod: { call } }
|
|
34
|
+
}
|
|
35
|
+
|
|
24
36
|
export function notInstalled(name: string): string {
|
|
25
37
|
return `${name} is not installed. Run cmod install ${name}.`
|
|
26
38
|
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { Claude } from './claude.js'
|
|
2
|
+
import type { Router } from './router.js'
|
|
3
|
+
import type { ToolCalls } from './tool-calls.js'
|
|
4
|
+
|
|
5
|
+
export type Engine = {
|
|
6
|
+
readonly claude: Claude
|
|
7
|
+
readonly router: Router
|
|
8
|
+
readonly toolCalls: ToolCalls
|
|
9
|
+
readonly report: (failure: { readonly id: string; readonly message: string; readonly error?: unknown; readonly description?: string }) => void
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const engines = new WeakMap<object, Engine>()
|
|
13
|
+
|
|
14
|
+
export function keepEngine(mod: object, engine: Engine): void {
|
|
15
|
+
engines.set(mod, engine)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function engineOf(mod: object): Engine {
|
|
19
|
+
const engine = engines.get(mod)
|
|
20
|
+
if (engine === undefined) throw new Error('This mod has not started, so it reaches no Claude Code calls yet.')
|
|
21
|
+
return engine
|
|
22
|
+
}
|