@cmodjs/core 1.0.0 → 1.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/docs/commands.md +22 -11
- package/docs/dependencies.md +6 -4
- package/docs/hooks.md +26 -3
- package/docs/index.md +1 -1
- package/docs/install-steps.md +5 -3
- package/docs/mod.md +29 -3
- package/docs/permissions.md +4 -4
- package/docs/session.md +126 -63
- package/docs/testing.md +7 -5
- package/docs/ui.md +9 -1
- package/jobs/program.d.ts.map +1 -1
- package/jobs/program.js +7 -2
- package/mod.d.ts +30 -6
- package/mod.d.ts.map +1 -1
- package/mod.js +34 -1
- package/package.json +1 -1
- package/records.d.ts +4 -1
- package/records.d.ts.map +1 -1
- package/records.js +6 -1
- package/register.d.ts.map +1 -1
- package/register.js +17 -0
- package/runtime/announce.d.ts.map +1 -1
- package/runtime/announce.js +2 -0
- package/runtime/before.d.ts +32 -0
- package/runtime/before.d.ts.map +1 -0
- package/runtime/before.js +214 -0
- package/runtime/claude.d.ts +2 -1
- package/runtime/claude.d.ts.map +1 -1
- package/runtime/conversations.d.ts +1 -0
- package/runtime/conversations.d.ts.map +1 -1
- package/runtime/conversations.js +333 -61
- package/runtime/dependencies.js +1 -1
- package/runtime/engine.d.ts +2 -6
- package/runtime/engine.d.ts.map +1 -1
- package/runtime/grants.d.ts.map +1 -1
- package/runtime/grants.js +8 -8
- package/runtime/hooks.d.ts +1 -1
- package/runtime/hooks.d.ts.map +1 -1
- package/runtime/hooks.js +19 -4
- package/runtime/installer.d.ts +1 -1
- package/runtime/installer.d.ts.map +1 -1
- package/runtime/installer.js +6 -5
- package/runtime/lifecycle.d.ts.map +1 -1
- package/runtime/lifecycle.js +130 -59
- package/runtime/metadata.d.ts.map +1 -1
- package/runtime/metadata.js +5 -4
- package/runtime/sessions.d.ts +26 -4
- package/runtime/sessions.d.ts.map +1 -1
- package/runtime/sessions.js +315 -117
- package/runtime/toaster.d.ts +5 -12
- package/runtime/toaster.d.ts.map +1 -1
- package/runtime/toaster.js +158 -25
- package/runtime/ui.d.ts.map +1 -1
- package/runtime/ui.js +26 -6
- package/session/context/context.d.ts +3 -1
- package/session/context/context.d.ts.map +1 -1
- package/session/context/turn/message/message.d.ts +1 -0
- package/session/context/turn/message/message.d.ts.map +1 -1
- package/session/context/turn/message/message.js +42 -1
- package/session/context/turn/turn.d.ts +2 -1
- package/session/context/turn/turn.d.ts.map +1 -1
- package/session/conversation.d.ts +20 -11
- package/session/conversation.d.ts.map +1 -1
- package/session/conversation.js +50 -27
- package/session/goal/goal.d.ts +3 -2
- package/session/goal/goal.d.ts.map +1 -1
- package/session/goal/task.d.ts +4 -4
- package/session/goal/task.d.ts.map +1 -1
- package/session/session.d.ts +80 -21
- package/session/session.d.ts.map +1 -1
- package/session/session.js +1 -0
- package/src/jobs/program.ts +7 -2
- package/src/mod.ts +48 -5
- package/src/records.ts +6 -1
- package/src/register.ts +17 -0
- package/src/runtime/announce.ts +2 -0
- package/src/runtime/before.ts +200 -0
- package/src/runtime/claude.ts +3 -0
- package/src/runtime/conversations.ts +337 -60
- package/src/runtime/dependencies.ts +1 -1
- package/src/runtime/engine.ts +2 -1
- package/src/runtime/grants.ts +8 -10
- package/src/runtime/hooks.ts +20 -4
- package/src/runtime/installer.ts +6 -5
- package/src/runtime/lifecycle.ts +136 -56
- package/src/runtime/metadata.ts +5 -4
- package/src/runtime/sessions.ts +316 -111
- package/src/runtime/toaster.ts +183 -45
- package/src/runtime/ui.ts +23 -4
- package/src/session/context/context.ts +3 -1
- package/src/session/context/turn/message/message.ts +39 -1
- package/src/session/context/turn/turn.ts +2 -1
- package/src/session/conversation.ts +56 -31
- package/src/session/goal/goal.ts +3 -2
- package/src/session/goal/task.ts +4 -4
- package/src/session/session.ts +62 -15
- package/src/testing/fake-claude.ts +4 -2
- package/src/testing/fake-sessions.ts +94 -20
- package/src/testing.ts +27 -3
- package/src/utils/call-effects.ts +2 -2
- package/src/utils/disposable.ts +3 -0
- package/src/utils/matcher.ts +1 -1
- package/src/utils/paths.ts +6 -3
- package/testing/fake-claude.d.ts +1 -0
- package/testing/fake-claude.d.ts.map +1 -1
- package/testing/fake-claude.js +3 -1
- package/testing/fake-sessions.d.ts +8 -1
- package/testing/fake-sessions.d.ts.map +1 -1
- package/testing/fake-sessions.js +97 -18
- package/testing.d.ts +1 -0
- package/testing.d.ts.map +1 -1
- package/testing.js +23 -2
- package/utils/call-effects.d.ts +1 -0
- package/utils/call-effects.d.ts.map +1 -1
- package/utils/call-effects.js +1 -1
- package/utils/disposable.d.ts +4 -0
- package/utils/disposable.d.ts.map +1 -0
- package/utils/disposable.js +1 -0
- package/utils/matcher.js +1 -1
- package/utils/paths.d.ts +2 -1
- package/utils/paths.d.ts.map +1 -1
- package/utils/paths.js +6 -3
|
@@ -2,34 +2,62 @@ 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
4
|
import { Note } from '../session/context/turn/message/note.js'
|
|
5
|
-
import { Prompt } from '../session/context/turn/message/prompt.js'
|
|
6
5
|
import { Person } from '../session/context/turn/message/sender.js'
|
|
7
6
|
import { createConversation, emptyConversation, type Conversation, type ConversationState, type ContextData, type SessionChange, type SessionData, type TaskData, type TurnData } from '../session/conversation.js'
|
|
7
|
+
import { Session, UsageChange, type BeforeEvent, type Client, type SessionSend, type TaskStartArgs, type ToolCallArgs } from '../session/session.js'
|
|
8
|
+
import { GoalChange } from '../session/goal/goal.js'
|
|
8
9
|
import { receivedMatcher } from '../utils/matcher.js'
|
|
9
10
|
import { ifMissing } from '../utils/paths.js'
|
|
10
11
|
import { formatExit, messageOf } from '../utils/text.js'
|
|
12
|
+
import { foldAnswers, installBefore, relayOf, type Place } from './before.js'
|
|
11
13
|
import type { Claude } from './claude.js'
|
|
12
14
|
import { cmodPluginName } from './dependencies.js'
|
|
13
15
|
import { readLines, runCmod } from './programs.js'
|
|
14
16
|
import type { Router } from './router.js'
|
|
15
|
-
import { conversationMethods, isHeardBy, type Subscription } from './sessions.js'
|
|
17
|
+
import { actPermission, beforePermission, conversationMethods, everySession, isAct, isHeardBy, resultOf, usageOf, type Act, type Subscription } from './sessions.js'
|
|
16
18
|
|
|
17
19
|
type Request = Readonly<Record<string, unknown>>
|
|
18
20
|
|
|
19
21
|
type Heard = { readonly event: Subscription['event']; readonly matcher: unknown }
|
|
20
22
|
|
|
21
|
-
|
|
23
|
+
type Watch = { readonly mod: string; readonly target: string; readonly project: string; readonly events: readonly string[] }
|
|
24
|
+
|
|
25
|
+
type Watcher = Watch & { readonly from: string }
|
|
26
|
+
|
|
27
|
+
type Question =
|
|
28
|
+
| { readonly kind: 'act'; readonly caller: string; readonly action: Act; readonly input: unknown; readonly task?: string }
|
|
29
|
+
| { readonly kind: 'before'; readonly mod: string; readonly event: BeforeEvent; readonly change: SessionChange; readonly project: string }
|
|
30
|
+
|
|
31
|
+
type Relay = (Question & { readonly id: string; readonly from: string }) | { readonly kind: 'answer'; readonly id: string; readonly value?: unknown; readonly deny?: string } | { readonly kind: 'watch' } | { readonly kind: 'goal'; readonly task?: string; readonly text: string }
|
|
32
|
+
|
|
33
|
+
type Asked = { resolve(value: unknown): void; reject(error: Error): void }
|
|
34
|
+
|
|
22
35
|
|
|
23
36
|
const bufferedTaskRows = 200
|
|
24
37
|
|
|
38
|
+
const beforeDeadlineMs = 5000
|
|
39
|
+
|
|
40
|
+
const livenessMs = 5000
|
|
41
|
+
|
|
42
|
+
|
|
25
43
|
export type Conversations = {
|
|
26
44
|
answer(e: Frozen<Args<'cmod.call'>> & { readonly caller?: string }): Promise<EventResult<'cmod.call'> | undefined>
|
|
45
|
+
moved(): void
|
|
27
46
|
}
|
|
28
47
|
|
|
29
48
|
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
|
|
49
|
+
const folder = dataFolder(store, cmodPluginName)
|
|
50
|
+
const goals = `${folder}/goals`
|
|
51
|
+
const watches = `${folder}/before`
|
|
31
52
|
const heard = new Map<string, Map<string, readonly Heard[]>>()
|
|
53
|
+
const asked = new Map<string, Asked>()
|
|
54
|
+
const ownWatches = new Map<string, Watch>()
|
|
55
|
+
let watchers: readonly Watcher[] = []
|
|
56
|
+
let watching: Promise<void> = Promise.resolve()
|
|
57
|
+
let published = new Set<string>()
|
|
58
|
+
let publishing: Promise<unknown> = Promise.resolve()
|
|
32
59
|
let session = ''
|
|
60
|
+
let asks = 0
|
|
33
61
|
let conversation: Conversation = createConversation(emptyConversation(''))
|
|
34
62
|
let starting: Promise<void> = Promise.resolve()
|
|
35
63
|
let live: Message[] = []
|
|
@@ -47,34 +75,41 @@ export function serveConversations({ claude, router, store, project, report }: {
|
|
|
47
75
|
return (parseConsent(JSON.parse(text), path)[caller] ?? []).includes(item)
|
|
48
76
|
}
|
|
49
77
|
|
|
50
|
-
const
|
|
78
|
+
const ended = (id: string) => new Error(`session ${id} has ended`)
|
|
79
|
+
|
|
80
|
+
const isRunning = async (id: string) => (await json<SessionData[]>(['history', 'sessions', '--running', '--all'])).some((each) => each.id === id)
|
|
81
|
+
|
|
82
|
+
const dispatch = (to: string, changes: readonly SessionChange[], isInProject = true) => {
|
|
51
83
|
for (const change of changes) {
|
|
52
84
|
if (change.type === Message && !liveIds.has(change.message.id) && to === session) {
|
|
53
85
|
live.push(change.message)
|
|
54
86
|
liveIds.add(change.message.id)
|
|
55
87
|
}
|
|
56
88
|
for (const [mod, sessions] of heard) {
|
|
57
|
-
const subscriptions = sessions.get(to) ?? []
|
|
89
|
+
const subscriptions = [...(sessions.get(to) ?? []), ...(isInProject ? (sessions.get(everySession) ?? []) : [])]
|
|
58
90
|
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))
|
|
91
|
+
void claude.cmod.call({ to: mod, method: conversationMethods.changed, input: { session: to, change, isInProject } }).catch((error: unknown) => report(`changed:${mod}`, `Could not tell ${mod} about session ${to}`, error))
|
|
60
92
|
}
|
|
61
93
|
}
|
|
62
94
|
}
|
|
63
95
|
|
|
64
|
-
const restartFollower = () => {
|
|
65
|
-
const
|
|
66
|
-
const
|
|
96
|
+
const restartFollower = async () => {
|
|
97
|
+
const wantsEvery = [...heard.values()].some((sessions) => (sessions.get(everySession) ?? []).length > 0)
|
|
98
|
+
const named = [...heard.values()].flatMap((sessions) => [...sessions].filter(([id, list]) => id !== session && id !== everySession && list.length > 0).map(([id]) => id))
|
|
99
|
+
const others = [...new Set(named)].sort()
|
|
100
|
+
const args = [...others, ...(wantsEvery ? ['--running'] : [])]
|
|
101
|
+
const wanted = [session, project(), ...args].join(' ')
|
|
67
102
|
if (follower?.sessions === wanted) return
|
|
68
103
|
follower?.stop()
|
|
69
104
|
follower = undefined
|
|
70
|
-
if (
|
|
71
|
-
const stream = claude.process.spawn({ argv: ['cmod', 'history', 'follow', ...
|
|
105
|
+
if (args.length === 0) return
|
|
106
|
+
const stream = claude.process.spawn({ argv: ['cmod', 'history', 'follow', ...args, '--json', '--project', project()], env: { CLAUDE_CODE_SESSION_ID: session } })
|
|
72
107
|
const started = { sessions: wanted, stop: () => void stream.return(undefined as never) }
|
|
73
108
|
follower = started
|
|
74
109
|
void readLines(stream, (line) => {
|
|
75
110
|
if (line.trim() === '') return
|
|
76
|
-
const parsed = JSON.parse(line) as { readonly session: string; readonly change: SessionChange }
|
|
77
|
-
dispatch(parsed.session, [parsed.change])
|
|
111
|
+
const parsed = JSON.parse(line) as { readonly session: string; readonly isInProject: boolean; readonly change: SessionChange }
|
|
112
|
+
dispatch(parsed.session, [parsed.change], parsed.isInProject)
|
|
78
113
|
})
|
|
79
114
|
.then(({ code, lastError }) => {
|
|
80
115
|
if (follower === started) throw new Error(`cmod history follow ${formatExit(code, lastError)}`)
|
|
@@ -86,12 +121,125 @@ export function serveConversations({ claude, router, store, project, report }: {
|
|
|
86
121
|
})
|
|
87
122
|
}
|
|
88
123
|
|
|
89
|
-
const
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
124
|
+
const follow = () => void restartFollower().catch((error: unknown) => report('follow', 'Stopped hearing the other sessions', error))
|
|
125
|
+
|
|
126
|
+
const post = async (to: string, relay: Relay) => {
|
|
127
|
+
const sent = await claude.session.send({ to: { sessionId: to }, text: `<cmod-relay>${JSON.stringify(relay)}</cmod-relay>` })
|
|
128
|
+
if (sent.isDelivered !== true) throw ended(to)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const ask = (to: string, relay: Question, deadlineMs?: number) =>
|
|
132
|
+
new Promise<unknown>((resolve, reject) => {
|
|
133
|
+
const id = `${session}:${(asks += 1)}`
|
|
134
|
+
const timer =
|
|
135
|
+
deadlineMs === undefined
|
|
136
|
+
? claude.clock.every(livenessMs, () => {
|
|
137
|
+
void isRunning(to).then((isLive) => {
|
|
138
|
+
if (!isLive) settle.reject(ended(to))
|
|
139
|
+
})
|
|
140
|
+
})
|
|
141
|
+
: claude.clock.after(deadlineMs, () => settle.reject(new Error(`session ${to} did not answer within ${deadlineMs / 1000} s`)))
|
|
142
|
+
const settle: Asked = {
|
|
143
|
+
resolve(value) {
|
|
144
|
+
if (asked.delete(id)) timer.cancel()
|
|
145
|
+
resolve(value)
|
|
146
|
+
},
|
|
147
|
+
reject(error) {
|
|
148
|
+
if (asked.delete(id)) timer.cancel()
|
|
149
|
+
reject(error)
|
|
150
|
+
},
|
|
151
|
+
}
|
|
152
|
+
asked.set(id, settle)
|
|
153
|
+
post(to, { ...relay, id, from: session }).catch((error: unknown) => settle.reject(error instanceof Error ? error : new Error(messageOf(error))))
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
const publishWatches = (isEnding = false) => {
|
|
157
|
+
const next = publishing.then(() => publishNow(isEnding))
|
|
158
|
+
publishing = next.catch((error: unknown) => claude.ui.log(`cmod: publishing before listeners failed: ${messageOf(error)}`, { to: 'debug' }))
|
|
159
|
+
return next
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const publishNow = async (isEnding: boolean) => {
|
|
163
|
+
if (session === '') return
|
|
164
|
+
const list = isEnding ? [] : [...ownWatches.values()].filter((watch) => watch.events.length > 0)
|
|
165
|
+
await claude.fs.write(`${watches}/${session}.json`, JSON.stringify(list))
|
|
166
|
+
const targets = new Set([...published, ...list.map((watch) => watch.target)])
|
|
167
|
+
published = new Set(list.map((watch) => watch.target))
|
|
168
|
+
const running = targets.has(everySession) ? (await json<SessionData[]>(['history', 'sessions', '--running'])).map((each) => each.id) : []
|
|
169
|
+
const told = new Set([...running, ...targets].filter((id) => id !== everySession && id !== session))
|
|
170
|
+
await Promise.all([...told].map((id) => post(id, { kind: 'watch' }).catch((error: unknown) => claude.ui.log(`cmod: session ${id} did not hear of a before listener: ${messageOf(error)}`, { to: 'debug' }))))
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const readWatchers = async () => {
|
|
174
|
+
const files = await claude.fs.list(watches).catch(ifMissing([]))
|
|
175
|
+
const found: Watcher[] = []
|
|
176
|
+
for (const file of files) {
|
|
177
|
+
const from = file.name.replace(/\.json$/, '')
|
|
178
|
+
if (from === session || !file.name.endsWith('.json')) continue
|
|
179
|
+
const list = JSON.parse(await claude.fs.read(`${watches}/${file.name}`).catch(ifMissing('[]'))) as readonly Watch[]
|
|
180
|
+
found.push(...list.filter((watch) => watch.target === session || (watch.target === everySession && watch.project === project())).map((watch) => ({ ...watch, from })))
|
|
181
|
+
}
|
|
182
|
+
watchers = found
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const changeGoal = (task: string | undefined, text: string) => dispatch(session, task === undefined ? conversation.changeGoal(text) : [{ type: GoalChange, goal: { text, task } }])
|
|
186
|
+
|
|
187
|
+
const reloadWatchers = () => {
|
|
188
|
+
watching = readWatchers().catch((error: unknown) => report('before', 'Kept the other sessions’ before listeners from before', error))
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const place = (): Place => {
|
|
192
|
+
const { context, contextOpenedAt, turn, tasks, taskTurns } = conversation.state
|
|
193
|
+
return { session, context, contextOpenedAt, ...(turn === undefined ? {} : { turn }), tasks, taskTurns }
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const deciders = (event: string): Watcher[] => {
|
|
197
|
+
const all = [...[...ownWatches.values()].filter((watch) => watch.target === session || watch.target === everySession).map((watch) => ({ ...watch, from: session })), ...watchers]
|
|
198
|
+
return [...new Map(all.filter((watcher) => watcher.events.includes(event)).map((watcher) => [`${watcher.from}\n${watcher.mod}`, watcher])).values()]
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const askHere = async (mod: string, event: BeforeEvent, change: SessionChange, from = session, isInProject = true) => {
|
|
202
|
+
for (const needed of ['conversation', beforePermission(event)]) if (!(await isGranted(mod, needed))) throw new Error(`${mod} needs the ${needed} permission for before(${event}). Turn it on in /mods ${mod}.`)
|
|
203
|
+
return claude.cmod.call({ to: mod, method: conversationMethods.before, input: { session: from, event, change, isInProject } })
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const askAway = async (watcher: Watcher, event: BeforeEvent, change: SessionChange) => {
|
|
207
|
+
try {
|
|
208
|
+
return await ask(watcher.from, { kind: 'before', mod: watcher.mod, event, change, project: project() }, beforeDeadlineMs)
|
|
209
|
+
} catch (error) {
|
|
210
|
+
if (await isRunning(watcher.from)) throw error
|
|
211
|
+
watchers = watchers.filter((each) => each.from !== watcher.from)
|
|
212
|
+
claude.ui.log(`cmod: session ${watcher.from} ended without clearing its before listeners, so they are cleared now`, { to: 'debug' })
|
|
213
|
+
await claude.fs.write(`${watches}/${watcher.from}.json`, '[]')
|
|
214
|
+
return undefined
|
|
94
215
|
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
installBefore({
|
|
219
|
+
claude,
|
|
220
|
+
router,
|
|
221
|
+
hooks: {
|
|
222
|
+
place: async () => {
|
|
223
|
+
await starting
|
|
224
|
+
return place()
|
|
225
|
+
},
|
|
226
|
+
wants: async (event) => {
|
|
227
|
+
await Promise.all([starting, watching])
|
|
228
|
+
return deciders(event).length > 0
|
|
229
|
+
},
|
|
230
|
+
decide: (event, change) =>
|
|
231
|
+
foldAnswers(
|
|
232
|
+
event,
|
|
233
|
+
change,
|
|
234
|
+
deciders(event).map((watcher) => (latest: SessionChange) => (watcher.from === session ? askHere(watcher.mod, event, latest) : askAway(watcher, event, latest))),
|
|
235
|
+
(error) => report(`before:${event}`, `Passed the ${event} on without a mod's answer`, error),
|
|
236
|
+
),
|
|
237
|
+
},
|
|
238
|
+
})
|
|
239
|
+
|
|
240
|
+
const start = (id: string, source: string) => {
|
|
241
|
+
if (id === session) return
|
|
242
|
+
const previous = session
|
|
95
243
|
session = id
|
|
96
244
|
live = []
|
|
97
245
|
liveIds.clear()
|
|
@@ -102,11 +250,14 @@ export function serveConversations({ claude, router, store, project, report }: {
|
|
|
102
250
|
? Promise.resolve(dispatch(id, conversation.startContext(new Date().toISOString())))
|
|
103
251
|
: json<ConversationState>(['history', 'state', id]).then(
|
|
104
252
|
(state) => {
|
|
105
|
-
if (session === id) conversation = createConversation(state)
|
|
253
|
+
if (session === id) conversation = createConversation({ ...emptyConversation(id), ...state })
|
|
106
254
|
},
|
|
107
255
|
(error: unknown) => report('state', `Starts session ${id} without its earlier turns`, error),
|
|
108
256
|
)
|
|
109
|
-
|
|
257
|
+
follow()
|
|
258
|
+
reloadWatchers()
|
|
259
|
+
if (ownWatches.size === 0) return
|
|
260
|
+
void Promise.all([previous === '' ? undefined : claude.fs.write(`${watches}/${previous}.json`, '[]'), publishWatches()]).catch((error: unknown) => report('before', 'Kept its before listeners on the session from before', error))
|
|
110
261
|
}
|
|
111
262
|
|
|
112
263
|
const take = async (row: Row, task: string | undefined) => {
|
|
@@ -128,6 +279,10 @@ export function serveConversations({ claude, router, store, project, report }: {
|
|
|
128
279
|
start(e.session_id, e.source)
|
|
129
280
|
return next(e)
|
|
130
281
|
})
|
|
282
|
+
router.add('classic.SessionEnd', async (e, next) => {
|
|
283
|
+
if (ownWatches.size > 0) await publishWatches(true).catch((error: unknown) => report('before', 'Left its before listeners registered', error))
|
|
284
|
+
return next(e)
|
|
285
|
+
})
|
|
131
286
|
|
|
132
287
|
router.add('session.append', async (e, next) => {
|
|
133
288
|
const kept = await next(e)
|
|
@@ -139,13 +294,19 @@ export function serveConversations({ claude, router, store, project, report }: {
|
|
|
139
294
|
|
|
140
295
|
router.add('session.compact', async (e, next) => {
|
|
141
296
|
const compacted = await next(e)
|
|
142
|
-
if (e.
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
297
|
+
if (e.trigger === 'precompute' || compacted.skip !== undefined || session === '') return compacted
|
|
298
|
+
const at = new Date().toISOString()
|
|
299
|
+
const boundary: Row = { id: `compact:${e.agentId ?? session}:${at}`, at, type: 'system', name: 'compact_boundary', isMeta: false, isSummary: false, content: [] }
|
|
300
|
+
void take(boundary, e.agentId).catch((error: unknown) => report('compact', 'Missed a compaction', error))
|
|
146
301
|
return compacted
|
|
147
302
|
})
|
|
148
303
|
|
|
304
|
+
router.add('session.measure', async (e, next) => {
|
|
305
|
+
const measured = await next(e)
|
|
306
|
+
if (session !== '') dispatch(session, [{ type: UsageChange, usage: await usageOf(claude) }])
|
|
307
|
+
return measured
|
|
308
|
+
})
|
|
309
|
+
|
|
149
310
|
router.add('agent.spawn', async (e, next) => {
|
|
150
311
|
const spawned = await next(e)
|
|
151
312
|
if (spawned.deny !== undefined || spawned.agentId === undefined) return spawned
|
|
@@ -160,32 +321,124 @@ export function serveConversations({ claude, router, store, project, report }: {
|
|
|
160
321
|
})
|
|
161
322
|
|
|
162
323
|
router.add('session.receive', async (e, next) => {
|
|
163
|
-
const
|
|
164
|
-
const sent = isFromCmod ? delivery.exec(e.text)?.[1] : undefined
|
|
324
|
+
const sent = relayOf(e)
|
|
165
325
|
if (sent === undefined) return next(e)
|
|
166
|
-
const
|
|
167
|
-
|
|
168
|
-
|
|
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' }
|
|
326
|
+
const relay = JSON.parse(sent) as Relay
|
|
327
|
+
claude.clock.after(0, () => void received(relay).catch((error: unknown) => report('relay', 'Could not answer another session', error)))
|
|
328
|
+
return { consumed: 'cmod relay' }
|
|
174
329
|
})
|
|
175
330
|
|
|
331
|
+
const received = async (relay: Relay) => {
|
|
332
|
+
if (relay.kind === 'answer') {
|
|
333
|
+
const waiting = asked.get(relay.id)
|
|
334
|
+
if (relay.deny === undefined) waiting?.resolve(relay.value)
|
|
335
|
+
else waiting?.reject(new Error(relay.deny))
|
|
336
|
+
return
|
|
337
|
+
}
|
|
338
|
+
if (relay.kind === 'watch') return readWatchers()
|
|
339
|
+
if (relay.kind === 'goal') {
|
|
340
|
+
await starting
|
|
341
|
+
changeGoal(relay.task, relay.text)
|
|
342
|
+
return
|
|
343
|
+
}
|
|
344
|
+
const answered = async (run: () => Promise<unknown>) => {
|
|
345
|
+
try {
|
|
346
|
+
await post(relay.from, { kind: 'answer', id: relay.id, value: (await run()) ?? null })
|
|
347
|
+
} catch (error) {
|
|
348
|
+
await post(relay.from, { kind: 'answer', id: relay.id, deny: messageOf(error) })
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
if (relay.kind === 'act') return answered(() => perform(relay.caller, relay.action, relay.input, relay.task))
|
|
352
|
+
return answered(() => askHere(relay.mod, relay.event, relay.change, relay.from, relay.project === project()))
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const checkAct = async (caller: string, action: Act, input: unknown) => {
|
|
356
|
+
const needed = actPermission(action, input)
|
|
357
|
+
if (needed !== undefined && !(await isGranted(caller, needed))) throw new Error(`${caller} needs the ${needed} permission for ${action}. Turn it on in /mods ${caller}.`)
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const perform = async (caller: string, action: Act, input: unknown, task: string | undefined): Promise<unknown> => {
|
|
361
|
+
await checkAct(caller, action, input)
|
|
362
|
+
await starting
|
|
363
|
+
if (task !== undefined) return performInTask(action, input, task)
|
|
364
|
+
if (action === 'usage') return usageOf(claude)
|
|
365
|
+
if (action === 'clients') return [...(await claude.session.surfaces())] satisfies Client[]
|
|
366
|
+
if (action === 'send') {
|
|
367
|
+
const { type, text, as } = input as SessionSend
|
|
368
|
+
if (as === Session) throw new Error(`session ${session} cannot send as itself to itself.`)
|
|
369
|
+
const delivered = type === Note ? await claude.session.append({ message: { type: 'user', content: [{ type: 'text', text }] } }) : await claude.prompt.submit({ text, ...(as === Person ? { asUser: true } : {}) })
|
|
370
|
+
if ('deny' in delivered && typeof delivered.deny === 'string') throw new Error(`Claude Code refused the note: ${delivered.deny}`)
|
|
371
|
+
if ('drop' in delivered && typeof delivered.drop === 'string') throw new Error(`Claude Code dropped the prompt: ${delivered.drop}`)
|
|
372
|
+
return null
|
|
373
|
+
}
|
|
374
|
+
if (action === 'startTask') {
|
|
375
|
+
const args = input as TaskStartArgs
|
|
376
|
+
const started = await claude.agent.spawn({
|
|
377
|
+
prompt: args.prompt,
|
|
378
|
+
...(args.name === undefined ? {} : { description: args.name, name: args.name }),
|
|
379
|
+
...(args.agentType === undefined ? {} : { subagentType: args.agentType }),
|
|
380
|
+
...(args.model === undefined ? {} : { model: args.model }),
|
|
381
|
+
...(args.cwd === undefined ? {} : { cwd: args.cwd }),
|
|
382
|
+
})
|
|
383
|
+
if (started.deny !== undefined) throw new Error(`Claude Code refused the task: ${started.deny}`)
|
|
384
|
+
return { id: started.agentId ?? '', title: args.name ?? '', agentType: args.agentType ?? 'general-purpose', model: started.model, openedBy: '', openedAt: new Date().toISOString() } satisfies TaskData
|
|
385
|
+
}
|
|
386
|
+
const { name, input: toolInput } = input as ToolCallArgs
|
|
387
|
+
return resultOf(place(), await claude.tool.call({ ...toolInput, tool: name } as never))
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
const performInTask = async (action: Act, input: unknown, task: string): Promise<unknown> => {
|
|
391
|
+
const running = conversation.state.tasks[task]
|
|
392
|
+
if (action === 'usage') return json(['history', 'usage', session, '--task', task])
|
|
393
|
+
if (action === 'clients') return running === undefined || running.closedAt !== undefined ? [] : [...(await claude.session.surfaces())]
|
|
394
|
+
if (action !== 'send') throw new Error(`cmod: ${action} runs only on a session's own main loop.`)
|
|
395
|
+
if (running === undefined || running.closedAt !== undefined) throw ended(task)
|
|
396
|
+
const sent = await claude.session.send({ to: { agentId: task }, text: (input as SessionSend).text })
|
|
397
|
+
if (sent.isDelivered !== true) throw ended(task)
|
|
398
|
+
return null
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
const actElsewhere = async (caller: string, target: string, action: Act, input: unknown, task: string | undefined): Promise<unknown> => {
|
|
402
|
+
await checkAct(caller, action, input)
|
|
403
|
+
const isLive = await isRunning(target)
|
|
404
|
+
if (!isLive && action === 'usage') return json(['history', 'usage', target, ...(task === undefined ? [] : ['--task', task])])
|
|
405
|
+
if (!isLive && action === 'clients') return []
|
|
406
|
+
if (!isLive) throw ended(task ?? target)
|
|
407
|
+
if (action === 'send' && task === undefined && (input as SessionSend).as === Session) {
|
|
408
|
+
const sent = await claude.session.send({ to: { sessionId: target }, text: (input as SessionSend).text })
|
|
409
|
+
if (sent.isDelivered !== true) throw ended(target)
|
|
410
|
+
return null
|
|
411
|
+
}
|
|
412
|
+
return ask(target, { kind: 'act', caller, action, input, ...(task === undefined ? {} : { task }) })
|
|
413
|
+
}
|
|
414
|
+
|
|
176
415
|
const merged = <Item>(saved: readonly Item[], kept: readonly Item[], key: (item: Item) => string): Item[] => {
|
|
177
416
|
const keptKeys = new Set(kept.map(key))
|
|
178
417
|
return [...saved.filter((item) => !keptKeys.has(key(item))), ...kept]
|
|
179
418
|
}
|
|
180
419
|
|
|
181
|
-
const liveTurns = (): TurnData[] => {
|
|
182
|
-
const turn = conversation.state.turn
|
|
420
|
+
const liveTurns = (task: unknown): TurnData[] => {
|
|
421
|
+
const turn = typeof task === 'string' ? conversation.state.taskTurns[task] : conversation.state.turn
|
|
183
422
|
return turn === undefined ? [] : [turn]
|
|
184
423
|
}
|
|
185
424
|
|
|
186
|
-
const liveTasks = (): TaskData[] =>
|
|
425
|
+
const liveTasks = (): TaskData[] =>
|
|
426
|
+
Object.values(conversation.state.tasks).map((task) => ({
|
|
427
|
+
id: task.id,
|
|
428
|
+
title: task.title,
|
|
429
|
+
agentType: task.agentType,
|
|
430
|
+
...(task.model === undefined ? {} : { model: task.model }),
|
|
431
|
+
openedBy: task.openedBy,
|
|
432
|
+
openedAt: task.openedAt,
|
|
433
|
+
...(task.closedAt === undefined ? {} : { closedAt: task.closedAt }),
|
|
434
|
+
...(task.parent === undefined ? {} : { parent: task.parent }),
|
|
435
|
+
}))
|
|
187
436
|
|
|
188
|
-
const liveContext = (): ContextData[] => {
|
|
437
|
+
const liveContext = (task: unknown): ContextData[] => {
|
|
438
|
+
if (typeof task === 'string') {
|
|
439
|
+
const running = conversation.state.tasks[task]
|
|
440
|
+
return running === undefined ? [] : [{ number: running.context, openedAt: running.contextOpenedAt ?? running.openedAt, task, ...(running.closedAt === undefined ? {} : { closedAt: running.closedAt }) }]
|
|
441
|
+
}
|
|
189
442
|
const { context, contextOpenedAt } = conversation.state
|
|
190
443
|
return context === 0 ? [] : [{ number: context, openedAt: contextOpenedAt }]
|
|
191
444
|
}
|
|
@@ -199,7 +452,8 @@ export function serveConversations({ claude, router, store, project, report }: {
|
|
|
199
452
|
},
|
|
200
453
|
async [conversationMethods.search](request) {
|
|
201
454
|
const before = isObject(request['before']) ? request['before'] : undefined
|
|
202
|
-
|
|
455
|
+
const record = [...textArg('--session', request['session']), ...textArg('--task', request['task']), ...textArg('--turn', request['turn']), ...countArg('--context', request['context']), ...(request['isGoal'] === true ? ['--goal'] : [])]
|
|
456
|
+
return json(['history', 'search', String(request['text']), ...scopeArgs(request), ...record, ...(request['running'] === true ? ['--running'] : []), ...countArg('--limit', request['limit']), ...(before === undefined ? [] : ['--before', `${String(before['session'])}:${String(before['id'])}`])])
|
|
203
457
|
},
|
|
204
458
|
async [conversationMethods.messages](request) {
|
|
205
459
|
const id = String(request['session'])
|
|
@@ -214,43 +468,51 @@ export function serveConversations({ claude, router, store, project, report }: {
|
|
|
214
468
|
},
|
|
215
469
|
async [conversationMethods.turns](request) {
|
|
216
470
|
const id = String(request['session'])
|
|
217
|
-
const saved = await json<TurnData[]>(['history', 'turns', id, ...countArg('--context', request['context']), ...countArg('--last', request['last'])])
|
|
471
|
+
const saved = await json<TurnData[]>(['history', 'turns', id, ...textArg('--task', request['task']), ...countArg('--context', request['context']), ...countArg('--last', request['last'])])
|
|
218
472
|
if (id !== session) return saved
|
|
219
473
|
await starting
|
|
220
|
-
const kept = liveTurns().filter((turn) => request['context'] === undefined || turn.context === request['context'])
|
|
474
|
+
const kept = liveTurns(request['task']).filter((turn) => request['context'] === undefined || turn.context === request['context'])
|
|
221
475
|
return lastOf(merged(saved, kept, (turn) => turn.prompt.id), request['last'])
|
|
222
476
|
},
|
|
223
477
|
async [conversationMethods.contexts](request) {
|
|
224
478
|
const id = String(request['session'])
|
|
225
|
-
const saved = await json<ContextData[]>(['history', 'contexts', id])
|
|
479
|
+
const saved = await json<ContextData[]>(['history', 'contexts', id, ...textArg('--task', request['task'])]).catch((error: unknown) => {
|
|
480
|
+
if (id === session && typeof request['task'] === 'string') return []
|
|
481
|
+
throw error
|
|
482
|
+
})
|
|
226
483
|
if (id !== session) return saved
|
|
227
484
|
await starting
|
|
228
|
-
return merged(saved, liveContext(), (context) => String(context.number))
|
|
485
|
+
return merged(saved, liveContext(request['task']), (context) => String(context.number))
|
|
229
486
|
},
|
|
230
487
|
async [conversationMethods.tasks](request) {
|
|
231
488
|
const id = String(request['session'])
|
|
232
|
-
const
|
|
489
|
+
const isNested = request['all'] === true
|
|
490
|
+
const saved = await json<TaskData[]>(['history', 'tasks', id, ...(isNested ? ['--nested'] : textArg('--task', request['task']))])
|
|
233
491
|
if (id !== session) return saved
|
|
234
492
|
await starting
|
|
235
|
-
const kept = liveTasks().filter((task) => task.parent === request['task'])
|
|
493
|
+
const kept = liveTasks().filter((task) => isNested || task.parent === request['task'])
|
|
236
494
|
return merged(saved, kept, (task) => task.id)
|
|
237
495
|
},
|
|
238
496
|
async [conversationMethods.goal](request) {
|
|
239
497
|
const id = String(request['session'])
|
|
240
|
-
|
|
498
|
+
const task = typeof request['task'] === 'string' ? request['task'] : undefined
|
|
499
|
+
if (id === session && task === undefined) {
|
|
241
500
|
await starting
|
|
242
501
|
const text = conversation.state.goal
|
|
243
502
|
if (text !== undefined) return { text }
|
|
244
503
|
}
|
|
245
|
-
return json(['history', 'goal', id])
|
|
504
|
+
return json(['history', 'goal', id, ...textArg('--task', task)])
|
|
246
505
|
},
|
|
247
506
|
async [conversationMethods.updateGoal](request) {
|
|
248
507
|
const id = String(request['session'])
|
|
508
|
+
const task = typeof request['task'] === 'string' ? request['task'] : undefined
|
|
249
509
|
const text = String(request['text'])
|
|
250
|
-
await claude.fs.write(`${goals}/${id}.txt`, text)
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
510
|
+
await claude.fs.write(`${goals}/${task ?? id}.txt`, text)
|
|
511
|
+
await starting
|
|
512
|
+
if (id === session) changeGoal(task, text)
|
|
513
|
+
else {
|
|
514
|
+
dispatch(id, [{ type: GoalChange, goal: { text, ...(task === undefined ? {} : { task }) } }])
|
|
515
|
+
await post(id, { kind: 'goal', ...(task === undefined ? {} : { task }), text }).catch((error: unknown) => claude.ui.log(`cmod: session ${id} keeps its goal until it starts again: ${messageOf(error)}`, { to: 'debug' }))
|
|
254
516
|
}
|
|
255
517
|
return null
|
|
256
518
|
},
|
|
@@ -261,24 +523,40 @@ export function serveConversations({ claude, router, store, project, report }: {
|
|
|
261
523
|
const sessions = heard.get(caller) ?? new Map<string, readonly Heard[]>()
|
|
262
524
|
sessions.set(id, list.map(({ event, matcher }) => ({ event, matcher: matcher === undefined ? undefined : receivedMatcher(matcher) })))
|
|
263
525
|
heard.set(caller, sessions)
|
|
264
|
-
|
|
526
|
+
follow()
|
|
265
527
|
return null
|
|
266
528
|
},
|
|
267
|
-
async [conversationMethods.
|
|
268
|
-
if (
|
|
269
|
-
const
|
|
270
|
-
const
|
|
271
|
-
|
|
529
|
+
async [conversationMethods.watchBefore](request, caller) {
|
|
530
|
+
if (caller === undefined) throw new Error('cmod hears no caller for this before listener.')
|
|
531
|
+
const target = String(request['session'])
|
|
532
|
+
const list = Array.isArray(request['subscriptions']) ? (request['subscriptions'] as readonly Subscription[]) : []
|
|
533
|
+
ownWatches.set(`${caller}\n${target}`, { mod: caller, target, project: project(), events: [...new Set(list.map(({ event }) => event))] })
|
|
534
|
+
await publishWatches()
|
|
272
535
|
return null
|
|
273
536
|
},
|
|
537
|
+
async [conversationMethods.act](request, caller) {
|
|
538
|
+
if (caller === undefined) throw new Error('cmod hears no caller for this action.')
|
|
539
|
+
const target = String(request['session'])
|
|
540
|
+
const action = String(request['action']) as Act
|
|
541
|
+
const task = typeof request['task'] === 'string' ? request['task'] : undefined
|
|
542
|
+
if (!isAct(action)) throw new Error(`cmod takes no action named ${action}.`)
|
|
543
|
+
if (target === session) return perform(caller, action, request['input'], task)
|
|
544
|
+
return actElsewhere(caller, target, action, request['input'], task)
|
|
545
|
+
},
|
|
274
546
|
}
|
|
275
547
|
|
|
276
548
|
return {
|
|
549
|
+
moved() {
|
|
550
|
+
for (const [key, watch] of ownWatches) ownWatches.set(key, { ...watch, project: project() })
|
|
551
|
+
void publishWatches().catch((error: unknown) => report('before', 'Kept its before listeners on the project from before', error))
|
|
552
|
+
reloadWatchers()
|
|
553
|
+
follow()
|
|
554
|
+
},
|
|
277
555
|
async answer(e) {
|
|
278
556
|
const method = methods[e.method]
|
|
279
557
|
if (method === undefined) return undefined
|
|
280
558
|
const input = isObject(e.input) ? e.input : {}
|
|
281
|
-
if (e.method !== conversationMethods.
|
|
559
|
+
if (e.method !== conversationMethods.act && !(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
560
|
try {
|
|
283
561
|
return { value: await method(input, e.caller) }
|
|
284
562
|
} catch (error) {
|
|
@@ -299,4 +577,3 @@ function countArg(flag: string, value: unknown): string[] {
|
|
|
299
577
|
function textArg(flag: string, value: unknown): string[] {
|
|
300
578
|
return typeof value === 'string' ? [flag, value] : []
|
|
301
579
|
}
|
|
302
|
-
|
package/src/runtime/engine.ts
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import type { Claude } from './claude.js'
|
|
2
2
|
import type { Router } from './router.js'
|
|
3
|
+
import type { Failure } from './toaster.js'
|
|
3
4
|
import type { ToolCalls } from './tool-calls.js'
|
|
4
5
|
|
|
5
6
|
export type Engine = {
|
|
6
7
|
readonly claude: Claude
|
|
7
8
|
readonly router: Router
|
|
8
9
|
readonly toolCalls: ToolCalls
|
|
9
|
-
readonly report: (failure:
|
|
10
|
+
readonly report: (failure: Failure) => void
|
|
10
11
|
}
|
|
11
12
|
|
|
12
13
|
const engines = new WeakMap<object, Engine>()
|
package/src/runtime/grants.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { permissionWords } from '../records.js'
|
|
1
|
+
import { isObject, permissionWords } from '../records.js'
|
|
2
2
|
import { relativePath } from '../utils/paths.js'
|
|
3
3
|
import type { Claude } from './claude.js'
|
|
4
4
|
import { cmodPluginName } from './dependencies.js'
|
|
5
5
|
import type { RoutedEvent } from './hooks.js'
|
|
6
|
-
import { conversationMethods } from './sessions.js'
|
|
6
|
+
import { actPermission, conversationMethods, isAct, toolPermission } from './sessions.js'
|
|
7
7
|
|
|
8
8
|
export type Grants = {
|
|
9
9
|
readonly name: string
|
|
@@ -29,10 +29,6 @@ export function programAddresses(mod: object): Set<string> {
|
|
|
29
29
|
|
|
30
30
|
const projectSettingsFiles: readonly string[] = ['.claude/settings.json', '.claude/settings.local.json', '.mcp.json']
|
|
31
31
|
|
|
32
|
-
const startsTurns: readonly string[] = ['CronCreate']
|
|
33
|
-
|
|
34
|
-
const ownSchedule: readonly string[] = ['CronDelete', 'CronList']
|
|
35
|
-
|
|
36
32
|
const conversationCalls: Readonly<Record<string, string>> = {
|
|
37
33
|
[conversationMethods.sessions]: 'sessions.list',
|
|
38
34
|
[conversationMethods.search]: 'sessions.search',
|
|
@@ -43,7 +39,8 @@ const conversationCalls: Readonly<Record<string, string>> = {
|
|
|
43
39
|
[conversationMethods.goal]: 'goal',
|
|
44
40
|
[conversationMethods.updateGoal]: 'goal.update',
|
|
45
41
|
[conversationMethods.subscribe]: 'on',
|
|
46
|
-
[conversationMethods.
|
|
42
|
+
[conversationMethods.watchBefore]: 'before',
|
|
43
|
+
[conversationMethods.act]: 'act',
|
|
47
44
|
}
|
|
48
45
|
|
|
49
46
|
export async function checkGrant(grants: Grants, call: string, item: string | undefined): Promise<void> {
|
|
@@ -115,7 +112,9 @@ export function checkingGrants(claude: Claude, grants: Grants): Claude {
|
|
|
115
112
|
cmod: {
|
|
116
113
|
async call(args) {
|
|
117
114
|
const named = args.to === cmodPluginName ? conversationCalls[args.method] : undefined
|
|
118
|
-
|
|
115
|
+
const given = args.method === conversationMethods.act && isObject(args.input) ? String(args.input['action']) : undefined
|
|
116
|
+
const action = given !== undefined && isAct(given) ? given : undefined
|
|
117
|
+
if (named !== undefined) await gate(action ?? named, action === undefined ? 'conversation' : actPermission(action, isObject(args.input) ? args.input['input'] : undefined))
|
|
119
118
|
return claude.cmod.call(args)
|
|
120
119
|
},
|
|
121
120
|
},
|
|
@@ -135,8 +134,7 @@ export function checkingGrants(claude: Claude, grants: Grants): Claude {
|
|
|
135
134
|
tool: {
|
|
136
135
|
...claude.tool,
|
|
137
136
|
call: async (input: Parameters<Claude['tool']['call']>[0]) => {
|
|
138
|
-
|
|
139
|
-
await gate(`tool.call(${input.tool})`, item)
|
|
137
|
+
await gate(`tool.call(${input.tool})`, toolPermission(input.tool))
|
|
140
138
|
return claude.tool.call(input)
|
|
141
139
|
},
|
|
142
140
|
},
|