@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,141 @@
|
|
|
1
|
+
import type { Disposable } from '../utils/disposable.js'
|
|
2
|
+
import type { Matcher } from '../utils/matcher.js'
|
|
3
|
+
import type { Context, ContextEnd, ContextStart } from './context/context.js'
|
|
4
|
+
import type { Document } from './context/turn/message/document.js'
|
|
5
|
+
import type { Message, MessageType } from './context/turn/message/message.js'
|
|
6
|
+
import type { Note } from './context/turn/message/note.js'
|
|
7
|
+
import type { Notice } from './context/turn/message/notice.js'
|
|
8
|
+
import type { Prompt } from './context/turn/message/prompt.js'
|
|
9
|
+
import type { Reply } from './context/turn/message/reply.js'
|
|
10
|
+
import type { Person } from './context/turn/message/sender.js'
|
|
11
|
+
import type { Skill } from './context/turn/message/skill.js'
|
|
12
|
+
import type { ToolCall } from './context/turn/message/tool-call.js'
|
|
13
|
+
import type { ToolResult } from './context/turn/message/tool-result.js'
|
|
14
|
+
import type { Turn, TurnEnd, TurnStart } from './context/turn/turn.js'
|
|
15
|
+
import type { Goal, GoalChange } from './goal/goal.js'
|
|
16
|
+
import type { Task, TaskEnd, TaskStart } from './goal/task.js'
|
|
17
|
+
|
|
18
|
+
export const Session = 'session'
|
|
19
|
+
|
|
20
|
+
export const UsageChange = 'usageChange'
|
|
21
|
+
|
|
22
|
+
export type SessionEvent = typeof Message | MessageType | typeof ContextStart | typeof ContextEnd | typeof TurnStart | typeof TurnEnd | typeof TaskStart | typeof TaskEnd | typeof GoalChange | typeof UsageChange
|
|
23
|
+
|
|
24
|
+
export type SessionPayload = {
|
|
25
|
+
readonly message: Message
|
|
26
|
+
readonly prompt: Prompt
|
|
27
|
+
readonly note: Note
|
|
28
|
+
readonly reply: Reply
|
|
29
|
+
readonly toolCall: ToolCall
|
|
30
|
+
readonly toolResult: ToolResult
|
|
31
|
+
readonly skill: Skill
|
|
32
|
+
readonly document: Document
|
|
33
|
+
readonly notice: Notice
|
|
34
|
+
readonly contextStart: Context
|
|
35
|
+
readonly contextEnd: Context
|
|
36
|
+
readonly turnStart: Turn
|
|
37
|
+
readonly turnEnd: Turn
|
|
38
|
+
readonly taskStart: Task
|
|
39
|
+
readonly taskEnd: Task
|
|
40
|
+
readonly goalChange: Goal
|
|
41
|
+
readonly usageChange: Usage
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export type Refusal = { readonly refuse: string }
|
|
45
|
+
|
|
46
|
+
export type BeforeAnswer = {
|
|
47
|
+
readonly prompt: { readonly text: string } | Refusal
|
|
48
|
+
readonly note: { readonly text: string }
|
|
49
|
+
readonly skill: { readonly text: string }
|
|
50
|
+
readonly document: { readonly text: string } | Refusal
|
|
51
|
+
readonly reply: { readonly text: string }
|
|
52
|
+
readonly toolCall: { readonly input: Readonly<Record<string, unknown>> } | { readonly result: string } | Refusal
|
|
53
|
+
readonly toolResult: { readonly text: string }
|
|
54
|
+
readonly notice: { readonly text: string }
|
|
55
|
+
readonly taskStart: { readonly owner: { readonly model: string } } | Refusal
|
|
56
|
+
readonly taskEnd: Refusal
|
|
57
|
+
readonly turnEnd: Refusal
|
|
58
|
+
readonly contextEnd: Refusal
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export type BeforeEvent = keyof BeforeAnswer
|
|
62
|
+
|
|
63
|
+
export type Usage = {
|
|
64
|
+
readonly model: string
|
|
65
|
+
readonly context: { readonly tokens?: number; readonly window?: number; readonly percent?: number }
|
|
66
|
+
readonly cost?: { readonly usd: number }
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export type Client = 'terminal' | 'desktop' | 'mobile' | 'vscode'
|
|
70
|
+
|
|
71
|
+
export type Listener<Event extends SessionEvent> = (payload: SessionPayload[Event]) => unknown
|
|
72
|
+
|
|
73
|
+
export type BeforeListener<Event extends BeforeEvent> = (payload: SessionPayload[Event]) => BeforeAnswer[Event] | void | Promise<BeforeAnswer[Event] | void>
|
|
74
|
+
|
|
75
|
+
export type SearchOptions = {
|
|
76
|
+
readonly limit?: number
|
|
77
|
+
readonly before?: Pick<Message, 'session' | 'id'>
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export type RecordPart = {
|
|
81
|
+
search(text: string, options?: SearchOptions): Promise<Message[]>
|
|
82
|
+
on<Event extends SessionEvent>(event: Event, listener: Listener<Event>): Disposable
|
|
83
|
+
on<Event extends SessionEvent>(event: Event, matcher: Matcher<SessionPayload[Event]>, listener: Listener<Event>): Disposable
|
|
84
|
+
before<Event extends BeforeEvent>(event: Event, listener: BeforeListener<Event>): Disposable
|
|
85
|
+
before<Event extends BeforeEvent>(event: Event, matcher: Matcher<SessionPayload[Event]>, listener: BeforeListener<Event>): Disposable
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export type SessionSend = {
|
|
89
|
+
readonly type: typeof Prompt | typeof Note
|
|
90
|
+
readonly text: string
|
|
91
|
+
readonly as?: typeof Person | typeof Session
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export type TaskStartArgs = {
|
|
95
|
+
readonly prompt: string
|
|
96
|
+
readonly agentType?: string
|
|
97
|
+
readonly model?: string
|
|
98
|
+
readonly name?: string
|
|
99
|
+
readonly cwd?: string
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export type ToolCallArgs = {
|
|
103
|
+
readonly name: string
|
|
104
|
+
readonly input: Readonly<Record<string, unknown>>
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export type Session = RecordPart & {
|
|
108
|
+
readonly id: string
|
|
109
|
+
readonly title: string
|
|
110
|
+
readonly project: string
|
|
111
|
+
readonly agentType?: string
|
|
112
|
+
readonly model: string
|
|
113
|
+
readonly cwd: string
|
|
114
|
+
readonly isRunning: boolean
|
|
115
|
+
readonly startedAt: string
|
|
116
|
+
readonly lastAt: string
|
|
117
|
+
readonly forkedFrom?: string
|
|
118
|
+
goal(): Promise<Goal>
|
|
119
|
+
context(): Promise<Context>
|
|
120
|
+
contexts(): Promise<Context[]>
|
|
121
|
+
turn(): Promise<Turn | undefined>
|
|
122
|
+
turns(options?: { readonly last?: number }): Promise<Turn[]>
|
|
123
|
+
messages(options?: { readonly after?: string; readonly last?: number }): Promise<Message[]>
|
|
124
|
+
send(message: SessionSend): Promise<void>
|
|
125
|
+
startTask(task: TaskStartArgs): Promise<Task>
|
|
126
|
+
callTool(call: ToolCallArgs): Promise<ToolResult>
|
|
127
|
+
usage(): Promise<Usage>
|
|
128
|
+
clients(): Promise<Client[]>
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export type SessionScope = {
|
|
132
|
+
readonly running?: boolean
|
|
133
|
+
readonly since?: string
|
|
134
|
+
readonly everyProject?: boolean
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export type Sessions = Omit<RecordPart, 'search'> & {
|
|
138
|
+
readonly current: Session
|
|
139
|
+
list(options?: SessionScope): Promise<Session[]>
|
|
140
|
+
search(text: string, options?: SessionScope & SearchOptions): Promise<Message[]>
|
|
141
|
+
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import type { Timer } from 'claude-code'
|
|
1
|
+
import type { EventResult, Timer } from 'claude-code'
|
|
2
|
+
import { isObject } from '../records.js'
|
|
2
3
|
import type { Claude } from '../runtime/claude.js'
|
|
3
4
|
import { elements } from './fake-elements.js'
|
|
5
|
+
import { fakeFiles } from './fake-files.js'
|
|
4
6
|
|
|
5
7
|
export type TestCall = {
|
|
6
8
|
readonly call: string
|
|
@@ -13,8 +15,8 @@ export type Fakes = {
|
|
|
13
15
|
http: { fetch?: Claude['http']['fetch'] }
|
|
14
16
|
settings: { read?: Claude['settings']['read'] }
|
|
15
17
|
config: { list?: Claude['config']['list'] }
|
|
16
|
-
ui: { ask?: Claude['ui']['ask']; open?: Claude['ui']['open']; scroll?: Claude['ui']['scroll'] }
|
|
17
|
-
session: { messages?: Claude['session']['messages']; append?: Claude['session']['append'] }
|
|
18
|
+
ui: { ask?: Claude['ui']['ask']; open?: Claude['ui']['open']; scroll?: Claude['ui']['scroll']; copy?: Claude['ui']['copy']; focus?: Claude['ui']['focus'] }
|
|
19
|
+
session: { messages?: Claude['session']['messages']; append?: Claude['session']['append']; send?: Claude['session']['send'] }
|
|
18
20
|
prompt: { submit?: Claude['prompt']['submit'] }
|
|
19
21
|
model: { complete?: Claude['model']['complete'] }
|
|
20
22
|
agent: { list?: Claude['agent']['list']; spawn?: Claude['agent']['spawn'] }
|
|
@@ -26,7 +28,6 @@ export type Fakes = {
|
|
|
26
28
|
export type ShownSchedule = { readonly id: string; readonly cron: string; readonly prompt: string }
|
|
27
29
|
|
|
28
30
|
export type Shown = {
|
|
29
|
-
readonly toasts: string[]
|
|
30
31
|
readonly notes: string[]
|
|
31
32
|
readonly prompts: string[]
|
|
32
33
|
readonly logs: string[]
|
|
@@ -47,10 +48,16 @@ export type FakeClaude = {
|
|
|
47
48
|
settle(): Promise<void>
|
|
48
49
|
}
|
|
49
50
|
|
|
51
|
+
export function calledAnswer(caller: string, answer: EventResult<'cmod.call'>): unknown {
|
|
52
|
+
if (isObject(answer) && typeof answer['deny'] === 'string') throw new Error(`${caller}: $.cmod.call: ${answer['deny']}`)
|
|
53
|
+
return isObject(answer) ? answer['value'] : undefined
|
|
54
|
+
}
|
|
55
|
+
|
|
50
56
|
export function fakeClaude(plugin: { readonly name: string; readonly root: string }): FakeClaude {
|
|
51
57
|
const calls: TestCall[] = []
|
|
52
58
|
const fakes: Fakes = { process: {}, fs: {}, http: {}, settings: {}, config: {}, ui: {}, session: {}, prompt: {}, model: {}, agent: {}, clock: {}, cmod: {}, tool: {} }
|
|
53
|
-
const
|
|
59
|
+
const emptyDisk = fakeFiles({})
|
|
60
|
+
const shown: Shown = { notes: [], prompts: [], logs: [], debug: [], statuses: [], openPanes: new Set(), commands: [], tools: [], schedules: [] }
|
|
54
61
|
const unplacedPanes = new Set<string>()
|
|
55
62
|
let focusedPane: string | undefined
|
|
56
63
|
const store = new Map<string, unknown>()
|
|
@@ -72,7 +79,7 @@ export function fakeClaude(plugin: { readonly name: string; readonly root: strin
|
|
|
72
79
|
|
|
73
80
|
const appendNote: Claude['session']['append'] = async ({ message }) => {
|
|
74
81
|
shown.notes.push(message.content.map((block) => (typeof block['text'] === 'string' ? block['text'] : '')).join(''))
|
|
75
|
-
return { message, uuid: `note-${shown.notes.length}` }
|
|
82
|
+
return { message, uuid: `note-${shown.notes.length}` }
|
|
76
83
|
}
|
|
77
84
|
const submitPrompt: Claude['prompt']['submit'] = async ({ text }) => {
|
|
78
85
|
shown.prompts.push(text)
|
|
@@ -98,9 +105,6 @@ export function fakeClaude(plugin: { readonly name: string; readonly root: strin
|
|
|
98
105
|
const claude: Claude = {
|
|
99
106
|
plugin,
|
|
100
107
|
ui: {
|
|
101
|
-
toast: (text) => {
|
|
102
|
-
shown.toasts.push(text)
|
|
103
|
-
},
|
|
104
108
|
status: (text) => {
|
|
105
109
|
shown.statuses.push(text)
|
|
106
110
|
},
|
|
@@ -115,6 +119,7 @@ export function fakeClaude(plugin: { readonly name: string; readonly root: strin
|
|
|
115
119
|
calls.push({ call: 'ui.open', args: [pane] })
|
|
116
120
|
const answer = fakes.ui.open === undefined ? { isPlaced: true as const } : await fakes.ui.open(pane)
|
|
117
121
|
if (answer.isPlaced) {
|
|
122
|
+
shown.openPanes.delete(pane.id)
|
|
118
123
|
shown.openPanes.add(pane.id)
|
|
119
124
|
unplacedPanes.delete(pane.id)
|
|
120
125
|
if (pane.focus === true) focusedPane = pane.id
|
|
@@ -130,10 +135,12 @@ export function fakeClaude(plugin: { readonly name: string; readonly root: strin
|
|
|
130
135
|
if (focusedPane === pane.id) focusedPane = undefined
|
|
131
136
|
},
|
|
132
137
|
panes: async () => [
|
|
133
|
-
...[...shown.openPanes].map((id) => ({ id, title: id, isShown:
|
|
138
|
+
...[...shown.openPanes].map((id) => ({ id, title: id, isShown: id === [...shown.openPanes].at(-1), isFocused: id === focusedPane, isPlaced: true })),
|
|
134
139
|
...[...unplacedPanes].map((id) => ({ id, title: id, isShown: false, isFocused: false, isPlaced: false })),
|
|
135
140
|
],
|
|
136
141
|
scroll: rejected('ui.scroll', () => fakes.ui.scroll ?? (async () => ({}))),
|
|
142
|
+
copy: rejected('ui.copy', () => fakes.ui.copy ?? (async () => ({ isCopied: true as const }))),
|
|
143
|
+
focus: rejected('ui.focus', () => fakes.ui.focus ?? (async () => ({}))),
|
|
137
144
|
resolve: () => elements,
|
|
138
145
|
},
|
|
139
146
|
process: {
|
|
@@ -141,14 +148,14 @@ export function fakeClaude(plugin: { readonly name: string; readonly root: strin
|
|
|
141
148
|
spawn: faked('process.spawn', () => fakes.process.spawn),
|
|
142
149
|
},
|
|
143
150
|
fs: {
|
|
144
|
-
read: rejected('fs.read', () => fakes.fs.read),
|
|
145
|
-
write: rejected('fs.write', () => fakes.fs.write),
|
|
146
|
-
list: rejected('fs.list', () => fakes.fs.list),
|
|
147
|
-
exists: rejected('fs.exists', () => fakes.fs.exists ??
|
|
148
|
-
stat: rejected('fs.stat', () => fakes.fs.stat),
|
|
151
|
+
read: rejected('fs.read', () => fakes.fs.read ?? emptyDisk.read),
|
|
152
|
+
write: rejected('fs.write', () => fakes.fs.write ?? emptyDisk.write),
|
|
153
|
+
list: rejected('fs.list', () => fakes.fs.list ?? emptyDisk.list),
|
|
154
|
+
exists: rejected('fs.exists', () => fakes.fs.exists ?? emptyDisk.exists),
|
|
155
|
+
stat: rejected('fs.stat', () => fakes.fs.stat ?? emptyDisk.stat),
|
|
149
156
|
},
|
|
150
157
|
http: { fetch: rejected('http.fetch', () => fakes.http.fetch) },
|
|
151
|
-
settings: { read: rejected('settings.read', () => fakes.settings.read) },
|
|
158
|
+
settings: { read: rejected('settings.read', () => fakes.settings.read ?? (async () => ({}))) },
|
|
152
159
|
config: {
|
|
153
160
|
list: rejected('config.list', () => fakes.config.list ?? (async () => [])),
|
|
154
161
|
},
|
|
@@ -176,10 +183,11 @@ export function fakeClaude(plugin: { readonly name: string; readonly root: strin
|
|
|
176
183
|
root: async () => plugin.root,
|
|
177
184
|
cwd: async () => plugin.root,
|
|
178
185
|
model: async () => 'test-model',
|
|
179
|
-
usage: async () => ({ context: { window: 200000 }, rateLimits:
|
|
186
|
+
usage: async () => ({ startedAt: Date.parse('2026-01-01T00:00:00.000Z'), context: { window: 200000 }, rateLimits: [], cost: { usd: 0 } }),
|
|
180
187
|
surfaces: async () => ['terminal'],
|
|
181
188
|
messages: rejected('session.messages', () => fakes.session.messages as ((args?: unknown) => Promise<unknown>) | undefined) as Claude['session']['messages'],
|
|
182
189
|
append: rejected('session.append', () => fakes.session.append ?? appendNote),
|
|
190
|
+
send: rejected('session.send', () => fakes.session.send ?? (async () => ({ isDelivered: true as const }))),
|
|
183
191
|
},
|
|
184
192
|
prompt: { submit: rejected('prompt.submit', () => fakes.prompt.submit ?? submitPrompt) },
|
|
185
193
|
model: { complete: rejected('model.complete', () => fakes.model.complete) },
|
|
@@ -116,8 +116,9 @@ function sideBySide(children: readonly RenderNode[], width: number, gap: number)
|
|
|
116
116
|
|
|
117
117
|
function minWidthOf(node: RenderNode): number {
|
|
118
118
|
if (typeof node === 'string') return 0
|
|
119
|
-
const
|
|
120
|
-
|
|
119
|
+
const { props } = insideOf(node)
|
|
120
|
+
if (props['flexShrink'] === 0) return Number.POSITIVE_INFINITY
|
|
121
|
+
return typeof props['minWidth'] === 'number' ? props['minWidth'] : 0
|
|
121
122
|
}
|
|
122
123
|
|
|
123
124
|
function blankRows(count: number): string[] {
|
|
@@ -36,7 +36,7 @@ export function fakeFiles(files: Readonly<Record<string, string>>, links: Readon
|
|
|
36
36
|
modified.set(realPath, writes)
|
|
37
37
|
},
|
|
38
38
|
list: async (path) => {
|
|
39
|
-
if (path === undefined) throw new Error('fs.list() lists the working folder, which the fake files do not know. Name the folder, such as fs.list(
|
|
39
|
+
if (path === undefined) throw new Error('fs.list() lists the working folder, which the fake files do not know. Name the folder, such as fs.list(sessions.current.cwd).')
|
|
40
40
|
const folder = realPathOf(path)
|
|
41
41
|
if (kindOf(folder) !== 'dir') throw new Error(`ENOENT: no such directory, list '${path}'`)
|
|
42
42
|
const names = new Set(below(folder).map((file) => file.split('/')[0] as string))
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
import { isObject } from '../records.js'
|
|
2
|
+
import { Document } from '../session/context/turn/message/document.js'
|
|
3
|
+
import { Message, searchText, skillPrefix, type Row } from '../session/context/turn/message/message.js'
|
|
4
|
+
import { Note } from '../session/context/turn/message/note.js'
|
|
5
|
+
import { Notice } from '../session/context/turn/message/notice.js'
|
|
6
|
+
import { Prompt } from '../session/context/turn/message/prompt.js'
|
|
7
|
+
import { Reply } from '../session/context/turn/message/reply.js'
|
|
8
|
+
import { Mod, Person, Schedule, type Sender } from '../session/context/turn/message/sender.js'
|
|
9
|
+
import { GoalChange } from '../session/goal/goal.js'
|
|
10
|
+
import { Task } from '../session/goal/task.js'
|
|
11
|
+
import { Skill } from '../session/context/turn/message/skill.js'
|
|
12
|
+
import { ToolCall } from '../session/context/turn/message/tool-call.js'
|
|
13
|
+
import { ToolResult } from '../session/context/turn/message/tool-result.js'
|
|
14
|
+
import { createConversation, emptyConversation, searchLimit, type ContextData, type Conversation, type SessionChange, type SessionData, type TaskData, type TurnData } from '../session/conversation.js'
|
|
15
|
+
import type { Place } from '../runtime/before.js'
|
|
16
|
+
import type { Claude } from '../runtime/claude.js'
|
|
17
|
+
import { conversationMethods, everySession, isHeardBy, resultOf, type Subscription } from '../runtime/sessions.js'
|
|
18
|
+
import { receivedMatcher } from '../utils/matcher.js'
|
|
19
|
+
import { firstLine, messageOf } from '../utils/text.js'
|
|
20
|
+
|
|
21
|
+
export type MessageDraft =
|
|
22
|
+
| { readonly type: typeof Prompt; readonly text: string; readonly from?: Sender }
|
|
23
|
+
| { readonly type: typeof Note; readonly text: string; readonly from?: Sender }
|
|
24
|
+
| { readonly type: typeof Reply; readonly text: string }
|
|
25
|
+
| { readonly type: typeof ToolCall; readonly name: string; readonly input?: Readonly<Record<string, unknown>>; readonly callId?: string }
|
|
26
|
+
| { readonly type: typeof ToolResult; readonly callId: string; readonly text: string; readonly isError?: boolean }
|
|
27
|
+
| { readonly type: typeof Skill; readonly name: string; readonly text: string }
|
|
28
|
+
| { readonly type: typeof Document; readonly path: string; readonly text: string }
|
|
29
|
+
| { readonly type: typeof Notice; readonly text: string }
|
|
30
|
+
|
|
31
|
+
export type FakeSession = Partial<Omit<SessionData, 'id'>> & {
|
|
32
|
+
readonly id: string
|
|
33
|
+
readonly messages?: readonly MessageDraft[]
|
|
34
|
+
readonly goal?: string
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
type Kept = {
|
|
38
|
+
data: SessionData
|
|
39
|
+
readonly conversation: Conversation
|
|
40
|
+
readonly messages: Message[]
|
|
41
|
+
readonly turns: Map<string, TurnData>
|
|
42
|
+
readonly contexts: Map<number, ContextData>
|
|
43
|
+
readonly tasks: Map<string, TaskData>
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
type Heard = { readonly event: Subscription['event']; readonly matcher: unknown }
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
export type FakeSessions = {
|
|
50
|
+
answer(caller: string, method: string, input: unknown): unknown
|
|
51
|
+
add(draft: MessageDraft, task?: string): Message[]
|
|
52
|
+
place(): Place
|
|
53
|
+
wants(mod: string, event: string): boolean
|
|
54
|
+
endTurn(): void
|
|
55
|
+
compact(): void
|
|
56
|
+
startTask(task: { readonly title: string; readonly agentType?: string; readonly model?: string; readonly openedBy?: string; readonly parent?: string }): string
|
|
57
|
+
start(id: string): void
|
|
58
|
+
readonly sent: { readonly session: string; readonly task?: string; readonly type: string; readonly text: string; readonly as?: string }[]
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function fakeSessions({ project, given, push, callTool }: { readonly project: () => string; readonly given: readonly FakeSession[]; readonly push: (mod: string, session: string, change: SessionChange) => void; readonly callTool: Claude['tool']['call'] }): FakeSessions {
|
|
62
|
+
const kept = new Map<string, Kept>()
|
|
63
|
+
const heard = new Map<string, Map<string, readonly Heard[]>>()
|
|
64
|
+
const watches = new Map<string, Map<string, readonly string[]>>()
|
|
65
|
+
const goals = new Map<string, string>()
|
|
66
|
+
const sent: FakeSessions['sent'] = []
|
|
67
|
+
let current = ''
|
|
68
|
+
let clock = Date.parse('2026-01-01T00:00:00.000Z')
|
|
69
|
+
let ids = 0
|
|
70
|
+
const now = () => new Date((clock += 1000)).toISOString()
|
|
71
|
+
|
|
72
|
+
const keep = (data: SessionData): Kept => {
|
|
73
|
+
const session: Kept = { data, conversation: createConversation(emptyConversation(data.id)), messages: [], turns: new Map(), contexts: new Map(), tasks: new Map() }
|
|
74
|
+
kept.set(data.id, session)
|
|
75
|
+
return session
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const record = (session: Kept, changes: readonly SessionChange[]) => {
|
|
79
|
+
for (const change of changes) {
|
|
80
|
+
if (change.type === Message) session.messages.push(change.message)
|
|
81
|
+
if ('turn' in change) session.turns.set(change.turn.prompt.id, change.turn)
|
|
82
|
+
if ('context' in change) session.contexts.set(change.context.number, change.context)
|
|
83
|
+
if ('task' in change) session.tasks.set(change.task.id, change.task)
|
|
84
|
+
session.data = { ...session.data, lastAt: change.type === Message ? change.message.at : session.data.lastAt, ...(session.data.title === '' && change.type === Message && change.message.type === Prompt && change.message.from.type === Person ? { title: firstLine(change.message.text) } : {}) }
|
|
85
|
+
for (const [mod, sessions] of heard) {
|
|
86
|
+
if ([...(sessions.get(session.data.id) ?? []), ...(sessions.get(everySession) ?? [])].some(({ event, matcher }) => isHeardBy(event, matcher, change))) push(mod, session.data.id, change)
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const add = (session: Kept, draft: MessageDraft, task?: string) => {
|
|
92
|
+
const before = session.messages.length
|
|
93
|
+
record(session, session.conversation.add(rowOf(draft, `message-${(ids += 1)}`, now()), task))
|
|
94
|
+
return session.messages.slice(before)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
for (const each of given) {
|
|
98
|
+
const session = keep({ title: '', project: project(), cwd: project(), model: 'test-model', startedAt: now(), lastAt: '', isRunning: false, ...each })
|
|
99
|
+
for (const draft of each.messages ?? []) add(session, draft)
|
|
100
|
+
if (each.goal !== undefined) record(session, session.conversation.changeGoal(each.goal))
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const sessionOf = (id: string) => {
|
|
104
|
+
const session = kept.get(id)
|
|
105
|
+
if (session === undefined) throw new Error(`No Claude Code transcript is named ${id}.jsonl. Give it to testMod as { sessions: [{ id: '${id}' }] }.`)
|
|
106
|
+
return session
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const currentSession = () => sessionOf(current)
|
|
110
|
+
|
|
111
|
+
const place = (): Place => {
|
|
112
|
+
const { session, context, contextOpenedAt, turn, tasks, taskTurns } = currentSession().conversation.state
|
|
113
|
+
return { session, context, contextOpenedAt, ...(turn === undefined ? {} : { turn }), tasks, taskTurns }
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const taskOf = (session: Kept, id: string) => {
|
|
117
|
+
const task = session.tasks.get(id)
|
|
118
|
+
if (task === undefined) throw new Error(`Session ${session.data.id} has no task ${id}. Start it with tested.startTask.`)
|
|
119
|
+
return task
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const turnsOf = (session: Kept, task: unknown) => [...session.turns.values()].filter((turn) => turn.prompt.task === (typeof task === 'string' ? task : undefined))
|
|
123
|
+
|
|
124
|
+
const isUnder = (session: Kept | undefined, task: string, ancestor: string) => {
|
|
125
|
+
for (let at: string | undefined = task, steps = 0; at !== undefined && steps < 64; at = session?.tasks.get(at)?.parent, steps += 1) if (at === ancestor) return true
|
|
126
|
+
return false
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const lastOf = <Item>(items: readonly Item[], last: unknown): Item[] => (typeof last === 'number' ? items.slice(-last) : [...items])
|
|
130
|
+
|
|
131
|
+
const isInScope = (session: Kept, request: Readonly<Record<string, unknown>>) =>
|
|
132
|
+
(request['everyProject'] === true || session.data.project === project()) && session.data.lastAt >= (typeof request['since'] === 'string' ? request['since'] : '') && (request['running'] !== true || session.data.isRunning)
|
|
133
|
+
|
|
134
|
+
const methods: Readonly<Record<string, (request: Readonly<Record<string, unknown>>, caller: string) => unknown>> = {
|
|
135
|
+
[conversationMethods.sessions]: (request) =>
|
|
136
|
+
[...kept.values()]
|
|
137
|
+
.filter((session) => isInScope(session, request))
|
|
138
|
+
.map((session) => session.data)
|
|
139
|
+
.toSorted((first, second) => second.lastAt.localeCompare(first.lastAt)),
|
|
140
|
+
[conversationMethods.search](request) {
|
|
141
|
+
const text = String(request['text'])
|
|
142
|
+
if ([...text].length < 3) throw new Error('cmod history search takes at least 3 characters.')
|
|
143
|
+
const inRecord = (message: Message) => {
|
|
144
|
+
const session = kept.get(message.session)
|
|
145
|
+
const task = request['task']
|
|
146
|
+
if (request['session'] !== undefined && message.session !== request['session']) return false
|
|
147
|
+
if (typeof task === 'string' && (message.task === undefined || !isUnder(session, message.task, task) || (request['isGoal'] === true && message.task === task))) return false
|
|
148
|
+
if (task === undefined && request['isGoal'] === true && message.task === undefined) return false
|
|
149
|
+
return (request['turn'] === undefined || message.turn === request['turn']) && (request['context'] === undefined || message.context === request['context'])
|
|
150
|
+
}
|
|
151
|
+
const found = [...kept.values()]
|
|
152
|
+
.filter((session) => request['session'] !== undefined || isInScope(session, request))
|
|
153
|
+
.flatMap((session) => session.messages)
|
|
154
|
+
.filter((message) => inRecord(message) && searchText(message).toLowerCase().includes(text.toLowerCase()))
|
|
155
|
+
.toSorted((first, second) => second.at.localeCompare(first.at))
|
|
156
|
+
const before = isObject(request['before']) ? request['before'] : undefined
|
|
157
|
+
const start = before === undefined ? 0 : found.findIndex((message) => message.session === before['session'] && message.id === before['id']) + 1
|
|
158
|
+
return found.slice(start, start + (typeof request['limit'] === 'number' ? request['limit'] : searchLimit))
|
|
159
|
+
},
|
|
160
|
+
[conversationMethods.messages](request) {
|
|
161
|
+
const session = sessionOf(String(request['session']))
|
|
162
|
+
const fits = session.messages.filter((message) => message.task === request['task'] && (request['turn'] === undefined || message.turn === request['turn']))
|
|
163
|
+
const after = typeof request['after'] === 'string' ? fits.findIndex((message) => message.id === request['after']) + 1 : 0
|
|
164
|
+
return lastOf(fits.slice(after), request['last'])
|
|
165
|
+
},
|
|
166
|
+
[conversationMethods.turns]: (request) => lastOf(turnsOf(sessionOf(String(request['session'])), request['task']).filter((turn) => request['context'] === undefined || turn.context === request['context']), request['last']),
|
|
167
|
+
[conversationMethods.contexts](request) {
|
|
168
|
+
const session = sessionOf(String(request['session']))
|
|
169
|
+
if (typeof request['task'] !== 'string') return [...session.contexts.values()]
|
|
170
|
+
const task = taskOf(session, request['task'])
|
|
171
|
+
return [{ number: session.conversation.state.tasks[task.id]?.context ?? 1, openedAt: task.openedAt, ...(task.closedAt === undefined ? {} : { closedAt: task.closedAt }) }]
|
|
172
|
+
},
|
|
173
|
+
[conversationMethods.tasks]: (request) => [...sessionOf(String(request['session'])).tasks.values()].filter((task) => request['all'] === true || task.parent === request['task']),
|
|
174
|
+
[conversationMethods.goal](request) {
|
|
175
|
+
const session = sessionOf(String(request['session']))
|
|
176
|
+
const task = request['task']
|
|
177
|
+
if (typeof task !== 'string') return { text: session.conversation.state.goal ?? '' }
|
|
178
|
+
return { text: goals.get(task) ?? turnsOf(session, task)[0]?.prompt.text ?? '' }
|
|
179
|
+
},
|
|
180
|
+
[conversationMethods.updateGoal](request) {
|
|
181
|
+
const session = sessionOf(String(request['session']))
|
|
182
|
+
const task = typeof request['task'] === 'string' ? request['task'] : undefined
|
|
183
|
+
const text = String(request['text'])
|
|
184
|
+
if (task === undefined) record(session, session.conversation.changeGoal(text))
|
|
185
|
+
else {
|
|
186
|
+
goals.set(task, text)
|
|
187
|
+
record(session, [{ type: GoalChange, goal: { text, task } }])
|
|
188
|
+
}
|
|
189
|
+
return null
|
|
190
|
+
},
|
|
191
|
+
[conversationMethods.watchBefore](request, caller) {
|
|
192
|
+
const targets = watches.get(caller) ?? new Map<string, readonly string[]>()
|
|
193
|
+
targets.set(String(request['session']), (Array.isArray(request['subscriptions']) ? (request['subscriptions'] as readonly Subscription[]) : []).map(({ event }) => event))
|
|
194
|
+
watches.set(caller, targets)
|
|
195
|
+
return null
|
|
196
|
+
},
|
|
197
|
+
async [conversationMethods.act](request) {
|
|
198
|
+
const session = sessionOf(String(request['session']))
|
|
199
|
+
const action = String(request['action'])
|
|
200
|
+
const input = isObject(request['input']) ? request['input'] : {}
|
|
201
|
+
const task = typeof request['task'] === 'string' ? request['task'] : undefined
|
|
202
|
+
const isLive = task === undefined ? session.data.isRunning : session.data.isRunning && taskOf(session, task).closedAt === undefined
|
|
203
|
+
if (action === 'usage') return { model: task === undefined ? session.data.model : (taskOf(session, task).model ?? session.data.model), context: {} }
|
|
204
|
+
if (action === 'clients') return isLive ? ['terminal'] : []
|
|
205
|
+
if (!isLive) throw new Error(`session ${task ?? session.data.id} has ended`)
|
|
206
|
+
if (action === 'send') {
|
|
207
|
+
sent.push({ session: session.data.id, ...(task === undefined ? {} : { task }), type: String(input['type']), text: String(input['text']), ...(typeof input['as'] === 'string' ? { as: input['as'] } : {}) })
|
|
208
|
+
return null
|
|
209
|
+
}
|
|
210
|
+
if (action === 'startTask') {
|
|
211
|
+
const id = `task-${(ids += 1)}`
|
|
212
|
+
const data: TaskData = { id, title: typeof input['name'] === 'string' ? input['name'] : '', agentType: typeof input['agentType'] === 'string' ? input['agentType'] : 'general-purpose', openedBy: '', openedAt: now(), ...(typeof input['model'] === 'string' ? { model: input['model'] } : {}) }
|
|
213
|
+
record(session, session.conversation.startTask(data))
|
|
214
|
+
return data
|
|
215
|
+
}
|
|
216
|
+
if (session.data.id === current) return resultOf(place(), await callTool({ ...input['input'] as object, tool: String(input['name']) } as never))
|
|
217
|
+
return { id: '', type: ToolResult, at: now(), session: session.data.id, context: session.conversation.state.context, callId: '', text: '', isError: false }
|
|
218
|
+
},
|
|
219
|
+
[conversationMethods.subscribe](request, caller) {
|
|
220
|
+
const list = Array.isArray(request['subscriptions']) ? (request['subscriptions'] as readonly Subscription[]) : []
|
|
221
|
+
const sessions = heard.get(caller) ?? new Map<string, readonly Heard[]>()
|
|
222
|
+
sessions.set(String(request['session']), list.map(({ event, matcher }) => ({ event, matcher: matcher === undefined ? undefined : receivedMatcher(matcher) })))
|
|
223
|
+
heard.set(caller, sessions)
|
|
224
|
+
return null
|
|
225
|
+
},
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return {
|
|
229
|
+
answer(caller, method, input) {
|
|
230
|
+
const run = methods[method]
|
|
231
|
+
if (run === undefined) return undefined
|
|
232
|
+
try {
|
|
233
|
+
return { value: run(isObject(input) ? input : {}, caller) }
|
|
234
|
+
} catch (error) {
|
|
235
|
+
return { deny: messageOf(error) }
|
|
236
|
+
}
|
|
237
|
+
},
|
|
238
|
+
add: (draft, task) => add(currentSession(), draft, task),
|
|
239
|
+
place,
|
|
240
|
+
wants: (mod, event) => [current, everySession].some((target) => (watches.get(mod)?.get(target) ?? []).includes(event)),
|
|
241
|
+
endTurn() {
|
|
242
|
+
const session = currentSession()
|
|
243
|
+
record(session, session.conversation.add({ id: `turn-end-${(ids += 1)}`, at: now(), type: 'system', name: 'turn_duration', isMeta: false, isSummary: false, content: [] }))
|
|
244
|
+
},
|
|
245
|
+
compact() {
|
|
246
|
+
const session = currentSession()
|
|
247
|
+
record(session, session.conversation.add({ id: `compact-${(ids += 1)}`, at: now(), type: 'system', name: 'compact_boundary', isMeta: false, isSummary: false, content: [] }))
|
|
248
|
+
},
|
|
249
|
+
startTask({ title, agentType = 'general-purpose', model, openedBy = '', parent }) {
|
|
250
|
+
const session = currentSession()
|
|
251
|
+
const id = `task-${(ids += 1)}`
|
|
252
|
+
record(session, session.conversation.startTask({ id, title, agentType, openedBy, openedAt: now(), ...(model === undefined ? {} : { model }), ...(parent === undefined ? {} : { parent }) }))
|
|
253
|
+
return id
|
|
254
|
+
},
|
|
255
|
+
start(id) {
|
|
256
|
+
current = id
|
|
257
|
+
const session = kept.get(id) ?? keep({ id, title: '', project: project(), cwd: project(), model: 'test-model', startedAt: now(), lastAt: now(), isRunning: true })
|
|
258
|
+
session.data = { ...session.data, isRunning: true }
|
|
259
|
+
if (session.conversation.state.context === 0) record(session, session.conversation.startContext(now()))
|
|
260
|
+
},
|
|
261
|
+
sent,
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function rowOf(draft: MessageDraft, id: string, at: string): Row {
|
|
266
|
+
const base = { id, at, isMeta: false, isSummary: false }
|
|
267
|
+
const text = (value: string) => [{ type: 'text', text: value }]
|
|
268
|
+
const origin = (from: Sender | undefined) => (from === undefined || from.type === Person ? { kind: 'human' } : from.type === Mod ? { kind: 'plugin', name: from.name, ...(from.as === Person ? { asUser: true } : {}) } : from.type === 'session' ? { kind: 'peer' } : from.type === Task ? { kind: 'task-notification' } : { kind: from.type === Schedule ? 'scheduled-trigger' : from.type, ...(from.name === undefined ? {} : { server: from.name }) })
|
|
269
|
+
if (draft.type === Prompt) return { ...base, type: 'user', content: text(draft.text), origin: origin(draft.from) }
|
|
270
|
+
if (draft.type === Note) return { ...base, type: 'user', isMeta: true, content: text(draft.text), origin: origin(draft.from ?? { type: Mod, name: 'test' }) }
|
|
271
|
+
if (draft.type === Reply) return { ...base, type: 'assistant', content: text(draft.text) }
|
|
272
|
+
if (draft.type === ToolCall) return { ...base, type: 'assistant', content: [{ type: 'tool_use', id: draft.callId ?? `toolu_${id}`, name: draft.name, input: draft.input ?? {} }], cwd: '/test/project' }
|
|
273
|
+
if (draft.type === ToolResult) return { ...base, type: 'user', content: [{ type: 'tool_result', tool_use_id: draft.callId, content: draft.text, ...(draft.isError === true ? { is_error: true } : {}) }] }
|
|
274
|
+
if (draft.type === Skill) return { ...base, type: 'user', isMeta: true, content: text(`${skillPrefix} /skills/${draft.name}\n\n${draft.text}`) }
|
|
275
|
+
if (draft.type === Document) return { ...base, type: 'attachment', name: 'nested_memory', content: text(draft.text), path: draft.path }
|
|
276
|
+
return { ...base, type: 'system', name: 'informational', content: text(draft.text) }
|
|
277
|
+
}
|