@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
|
@@ -0,0 +1,579 @@
|
|
|
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 { Person } from '../session/context/turn/message/sender.js'
|
|
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'
|
|
9
|
+
import { receivedMatcher } from '../utils/matcher.js'
|
|
10
|
+
import { ifMissing } from '../utils/paths.js'
|
|
11
|
+
import { formatExit, messageOf } from '../utils/text.js'
|
|
12
|
+
import { foldAnswers, installBefore, relayOf, type Place } from './before.js'
|
|
13
|
+
import type { Claude } from './claude.js'
|
|
14
|
+
import { cmodPluginName } from './dependencies.js'
|
|
15
|
+
import { readLines, runCmod } from './programs.js'
|
|
16
|
+
import type { Router } from './router.js'
|
|
17
|
+
import { actPermission, beforePermission, conversationMethods, everySession, isAct, isHeardBy, resultOf, usageOf, type Act, type Subscription } from './sessions.js'
|
|
18
|
+
|
|
19
|
+
type Request = Readonly<Record<string, unknown>>
|
|
20
|
+
|
|
21
|
+
type Heard = { readonly event: Subscription['event']; readonly matcher: unknown }
|
|
22
|
+
|
|
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
|
+
|
|
35
|
+
|
|
36
|
+
const bufferedTaskRows = 200
|
|
37
|
+
|
|
38
|
+
const beforeDeadlineMs = 5000
|
|
39
|
+
|
|
40
|
+
const livenessMs = 5000
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
export type Conversations = {
|
|
44
|
+
answer(e: Frozen<Args<'cmod.call'>> & { readonly caller?: string }): Promise<EventResult<'cmod.call'> | undefined>
|
|
45
|
+
moved(): void
|
|
46
|
+
}
|
|
47
|
+
|
|
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 {
|
|
49
|
+
const folder = dataFolder(store, cmodPluginName)
|
|
50
|
+
const goals = `${folder}/goals`
|
|
51
|
+
const watches = `${folder}/before`
|
|
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()
|
|
59
|
+
let session = ''
|
|
60
|
+
let asks = 0
|
|
61
|
+
let conversation: Conversation = createConversation(emptyConversation(''))
|
|
62
|
+
let starting: Promise<void> = Promise.resolve()
|
|
63
|
+
let live: Message[] = []
|
|
64
|
+
const liveIds = new Set<string>()
|
|
65
|
+
const waitingRows = new Map<string, Row[]>()
|
|
66
|
+
let follower: { readonly sessions: string; stop(): void } | undefined
|
|
67
|
+
|
|
68
|
+
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
|
|
69
|
+
|
|
70
|
+
const isGranted = async (caller: string | undefined, item: string) => {
|
|
71
|
+
if (caller === cmodPluginName) return true
|
|
72
|
+
if (caller === undefined) return false
|
|
73
|
+
const path = consentPath(store)
|
|
74
|
+
const text = await claude.fs.read(path).catch(ifMissing('{}'))
|
|
75
|
+
return (parseConsent(JSON.parse(text), path)[caller] ?? []).includes(item)
|
|
76
|
+
}
|
|
77
|
+
|
|
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) => {
|
|
83
|
+
for (const change of changes) {
|
|
84
|
+
if (change.type === Message && !liveIds.has(change.message.id) && to === session) {
|
|
85
|
+
live.push(change.message)
|
|
86
|
+
liveIds.add(change.message.id)
|
|
87
|
+
}
|
|
88
|
+
for (const [mod, sessions] of heard) {
|
|
89
|
+
const subscriptions = [...(sessions.get(to) ?? []), ...(isInProject ? (sessions.get(everySession) ?? []) : [])]
|
|
90
|
+
if (!subscriptions.some(({ event, matcher }) => isHeardBy(event, matcher, change))) continue
|
|
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))
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
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(' ')
|
|
102
|
+
if (follower?.sessions === wanted) return
|
|
103
|
+
follower?.stop()
|
|
104
|
+
follower = undefined
|
|
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 } })
|
|
107
|
+
const started = { sessions: wanted, stop: () => void stream.return(undefined as never) }
|
|
108
|
+
follower = started
|
|
109
|
+
void readLines(stream, (line) => {
|
|
110
|
+
if (line.trim() === '') return
|
|
111
|
+
const parsed = JSON.parse(line) as { readonly session: string; readonly isInProject: boolean; readonly change: SessionChange }
|
|
112
|
+
dispatch(parsed.session, [parsed.change], parsed.isInProject)
|
|
113
|
+
})
|
|
114
|
+
.then(({ code, lastError }) => {
|
|
115
|
+
if (follower === started) throw new Error(`cmod history follow ${formatExit(code, lastError)}`)
|
|
116
|
+
})
|
|
117
|
+
.catch((error: unknown) => {
|
|
118
|
+
if (follower !== started) return
|
|
119
|
+
follower = undefined
|
|
120
|
+
report('follow', 'Stopped hearing the other sessions', error)
|
|
121
|
+
})
|
|
122
|
+
}
|
|
123
|
+
|
|
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
|
|
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
|
|
243
|
+
session = id
|
|
244
|
+
live = []
|
|
245
|
+
liveIds.clear()
|
|
246
|
+
waitingRows.clear()
|
|
247
|
+
conversation = createConversation(emptyConversation(id))
|
|
248
|
+
starting =
|
|
249
|
+
source === 'startup' || source === 'clear'
|
|
250
|
+
? Promise.resolve(dispatch(id, conversation.startContext(new Date().toISOString())))
|
|
251
|
+
: json<ConversationState>(['history', 'state', id]).then(
|
|
252
|
+
(state) => {
|
|
253
|
+
if (session === id) conversation = createConversation({ ...emptyConversation(id), ...state })
|
|
254
|
+
},
|
|
255
|
+
(error: unknown) => report('state', `Starts session ${id} without its earlier turns`, error),
|
|
256
|
+
)
|
|
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))
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const take = async (row: Row, task: string | undefined) => {
|
|
264
|
+
await starting
|
|
265
|
+
if (task !== undefined && conversation.state.tasks[task] === undefined) {
|
|
266
|
+
const waiting = waitingRows.get(task) ?? []
|
|
267
|
+
waiting.push(row)
|
|
268
|
+
waitingRows.set(task, waiting)
|
|
269
|
+
if (waiting.length < bufferedTaskRows) return
|
|
270
|
+
waitingRows.delete(task)
|
|
271
|
+
dispatch(session, conversation.startTask({ id: task, title: '', agentType: 'general-purpose', openedBy: '', openedAt: row.at }))
|
|
272
|
+
for (const each of waiting) dispatch(session, conversation.add(each, task))
|
|
273
|
+
return
|
|
274
|
+
}
|
|
275
|
+
dispatch(session, conversation.add(row, task))
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
router.add('classic.SessionStart', async (e, next) => {
|
|
279
|
+
start(e.session_id, e.source)
|
|
280
|
+
return next(e)
|
|
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
|
+
})
|
|
286
|
+
|
|
287
|
+
router.add('session.append', async (e, next) => {
|
|
288
|
+
const kept = await next(e)
|
|
289
|
+
if (kept.deny !== undefined || session === '') return kept
|
|
290
|
+
const cwd = e.message.type === 'assistant' ? await claude.session.cwd() : ''
|
|
291
|
+
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))
|
|
292
|
+
return kept
|
|
293
|
+
})
|
|
294
|
+
|
|
295
|
+
router.add('session.compact', async (e, next) => {
|
|
296
|
+
const compacted = await next(e)
|
|
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))
|
|
301
|
+
return compacted
|
|
302
|
+
})
|
|
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
|
+
|
|
310
|
+
router.add('agent.spawn', async (e, next) => {
|
|
311
|
+
const spawned = await next(e)
|
|
312
|
+
if (spawned.deny !== undefined || spawned.agentId === undefined) return spawned
|
|
313
|
+
const id = spawned.agentId
|
|
314
|
+
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 }) }
|
|
315
|
+
await starting
|
|
316
|
+
dispatch(session, conversation.startTask(opened))
|
|
317
|
+
const waiting = waitingRows.get(id) ?? []
|
|
318
|
+
waitingRows.delete(id)
|
|
319
|
+
for (const row of waiting) dispatch(session, conversation.add(row, id))
|
|
320
|
+
return spawned
|
|
321
|
+
})
|
|
322
|
+
|
|
323
|
+
router.add('session.receive', async (e, next) => {
|
|
324
|
+
const sent = relayOf(e)
|
|
325
|
+
if (sent === undefined) return next(e)
|
|
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' }
|
|
329
|
+
})
|
|
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
|
+
|
|
415
|
+
const merged = <Item>(saved: readonly Item[], kept: readonly Item[], key: (item: Item) => string): Item[] => {
|
|
416
|
+
const keptKeys = new Set(kept.map(key))
|
|
417
|
+
return [...saved.filter((item) => !keptKeys.has(key(item))), ...kept]
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
const liveTurns = (task: unknown): TurnData[] => {
|
|
421
|
+
const turn = typeof task === 'string' ? conversation.state.taskTurns[task] : conversation.state.turn
|
|
422
|
+
return turn === undefined ? [] : [turn]
|
|
423
|
+
}
|
|
424
|
+
|
|
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
|
+
}))
|
|
436
|
+
|
|
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
|
+
}
|
|
442
|
+
const { context, contextOpenedAt } = conversation.state
|
|
443
|
+
return context === 0 ? [] : [{ number: context, openedAt: contextOpenedAt }]
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
const lastOf = <Item>(items: readonly Item[], last: unknown): Item[] => (typeof last === 'number' ? items.slice(-last) : [...items])
|
|
447
|
+
|
|
448
|
+
const methods: Readonly<Record<string, (request: Request, caller: string | undefined) => Promise<unknown>>> = {
|
|
449
|
+
async [conversationMethods.sessions](request) {
|
|
450
|
+
const found = await json<SessionData[]>(['history', 'sessions', ...scopeArgs(request), ...(request['running'] === true ? ['--running'] : [])])
|
|
451
|
+
return found.map((each) => (each.id === session ? { ...each, isRunning: true } : each))
|
|
452
|
+
},
|
|
453
|
+
async [conversationMethods.search](request) {
|
|
454
|
+
const before = isObject(request['before']) ? request['before'] : undefined
|
|
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'])}`])])
|
|
457
|
+
},
|
|
458
|
+
async [conversationMethods.messages](request) {
|
|
459
|
+
const id = String(request['session'])
|
|
460
|
+
const filters = [...textArg('--turn', request['turn']), ...textArg('--task', request['task']), ...textArg('--after', request['after'])]
|
|
461
|
+
const saved = await json<Message[]>(['history', 'messages', id, ...filters, ...countArg('--last', request['last'])])
|
|
462
|
+
if (id !== session) return saved
|
|
463
|
+
await starting
|
|
464
|
+
const fits = (message: Message) => (request['turn'] === undefined || message.turn === request['turn']) && message.task === request['task']
|
|
465
|
+
const after = typeof request['after'] === 'string' ? live.findIndex((message) => message.id === request['after']) : -1
|
|
466
|
+
const kept = live.slice(after + 1).filter(fits)
|
|
467
|
+
return lastOf(merged(saved, kept, (message) => message.id), request['last'])
|
|
468
|
+
},
|
|
469
|
+
async [conversationMethods.turns](request) {
|
|
470
|
+
const id = String(request['session'])
|
|
471
|
+
const saved = await json<TurnData[]>(['history', 'turns', id, ...textArg('--task', request['task']), ...countArg('--context', request['context']), ...countArg('--last', request['last'])])
|
|
472
|
+
if (id !== session) return saved
|
|
473
|
+
await starting
|
|
474
|
+
const kept = liveTurns(request['task']).filter((turn) => request['context'] === undefined || turn.context === request['context'])
|
|
475
|
+
return lastOf(merged(saved, kept, (turn) => turn.prompt.id), request['last'])
|
|
476
|
+
},
|
|
477
|
+
async [conversationMethods.contexts](request) {
|
|
478
|
+
const id = String(request['session'])
|
|
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
|
+
})
|
|
483
|
+
if (id !== session) return saved
|
|
484
|
+
await starting
|
|
485
|
+
return merged(saved, liveContext(request['task']), (context) => String(context.number))
|
|
486
|
+
},
|
|
487
|
+
async [conversationMethods.tasks](request) {
|
|
488
|
+
const id = String(request['session'])
|
|
489
|
+
const isNested = request['all'] === true
|
|
490
|
+
const saved = await json<TaskData[]>(['history', 'tasks', id, ...(isNested ? ['--nested'] : textArg('--task', request['task']))])
|
|
491
|
+
if (id !== session) return saved
|
|
492
|
+
await starting
|
|
493
|
+
const kept = liveTasks().filter((task) => isNested || task.parent === request['task'])
|
|
494
|
+
return merged(saved, kept, (task) => task.id)
|
|
495
|
+
},
|
|
496
|
+
async [conversationMethods.goal](request) {
|
|
497
|
+
const id = String(request['session'])
|
|
498
|
+
const task = typeof request['task'] === 'string' ? request['task'] : undefined
|
|
499
|
+
if (id === session && task === undefined) {
|
|
500
|
+
await starting
|
|
501
|
+
const text = conversation.state.goal
|
|
502
|
+
if (text !== undefined) return { text }
|
|
503
|
+
}
|
|
504
|
+
return json(['history', 'goal', id, ...textArg('--task', task)])
|
|
505
|
+
},
|
|
506
|
+
async [conversationMethods.updateGoal](request) {
|
|
507
|
+
const id = String(request['session'])
|
|
508
|
+
const task = typeof request['task'] === 'string' ? request['task'] : undefined
|
|
509
|
+
const text = String(request['text'])
|
|
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' }))
|
|
516
|
+
}
|
|
517
|
+
return null
|
|
518
|
+
},
|
|
519
|
+
async [conversationMethods.subscribe](request, caller) {
|
|
520
|
+
if (caller === undefined) throw new Error('cmod hears no caller for this subscription.')
|
|
521
|
+
const id = String(request['session'])
|
|
522
|
+
const list = Array.isArray(request['subscriptions']) ? (request['subscriptions'] as readonly Subscription[]) : []
|
|
523
|
+
const sessions = heard.get(caller) ?? new Map<string, readonly Heard[]>()
|
|
524
|
+
sessions.set(id, list.map(({ event, matcher }) => ({ event, matcher: matcher === undefined ? undefined : receivedMatcher(matcher) })))
|
|
525
|
+
heard.set(caller, sessions)
|
|
526
|
+
follow()
|
|
527
|
+
return null
|
|
528
|
+
},
|
|
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()
|
|
535
|
+
return null
|
|
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
|
+
},
|
|
546
|
+
}
|
|
547
|
+
|
|
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
|
+
},
|
|
555
|
+
async answer(e) {
|
|
556
|
+
const method = methods[e.method]
|
|
557
|
+
if (method === undefined) return undefined
|
|
558
|
+
const input = isObject(e.input) ? e.input : {}
|
|
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() }
|
|
560
|
+
try {
|
|
561
|
+
return { value: await method(input, e.caller) }
|
|
562
|
+
} catch (error) {
|
|
563
|
+
return { deny: messageOf(error) }
|
|
564
|
+
}
|
|
565
|
+
},
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
function scopeArgs(request: Request): string[] {
|
|
570
|
+
return [...(typeof request['since'] === 'string' ? ['--since', request['since']] : []), ...(request['everyProject'] === true ? ['--all'] : [])]
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
function countArg(flag: string, value: unknown): string[] {
|
|
574
|
+
return typeof value === 'number' ? [flag, String(value)] : []
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
function textArg(flag: string, value: unknown): string[] {
|
|
578
|
+
return typeof value === 'string' ? [flag, value] : []
|
|
579
|
+
}
|
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
|
-
return `${name} is not installed.
|
|
37
|
+
return `${name} is not installed. Add it with /plugin.`
|
|
26
38
|
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { Claude } from './claude.js'
|
|
2
|
+
import type { Router } from './router.js'
|
|
3
|
+
import type { Failure } from './toaster.js'
|
|
4
|
+
import type { ToolCalls } from './tool-calls.js'
|
|
5
|
+
|
|
6
|
+
export type Engine = {
|
|
7
|
+
readonly claude: Claude
|
|
8
|
+
readonly router: Router
|
|
9
|
+
readonly toolCalls: ToolCalls
|
|
10
|
+
readonly report: (failure: Failure) => void
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const engines = new WeakMap<object, Engine>()
|
|
14
|
+
|
|
15
|
+
export function keepEngine(mod: object, engine: Engine): void {
|
|
16
|
+
engines.set(mod, engine)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function engineOf(mod: object): Engine {
|
|
20
|
+
const engine = engines.get(mod)
|
|
21
|
+
if (engine === undefined) throw new Error('This mod has not started, so it reaches no Claude Code calls yet.')
|
|
22
|
+
return engine
|
|
23
|
+
}
|