@cmodjs/core 1.0.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 +22 -11
- package/docs/dependencies.md +6 -4
- package/docs/hooks.md +26 -3
- package/docs/index.md +1 -1
- package/docs/install-steps.md +5 -3
- package/docs/mod.md +29 -3
- package/docs/permissions.md +4 -4
- package/docs/session.md +126 -63
- package/docs/testing.md +7 -5
- package/docs/ui.md +9 -1
- package/jobs/program.d.ts.map +1 -1
- package/jobs/program.js +7 -2
- package/mod.d.ts +30 -6
- package/mod.d.ts.map +1 -1
- package/mod.js +34 -1
- package/package.json +1 -1
- package/records.d.ts +4 -1
- package/records.d.ts.map +1 -1
- package/records.js +6 -1
- package/register.d.ts.map +1 -1
- package/register.js +17 -0
- package/runtime/announce.d.ts.map +1 -1
- package/runtime/announce.js +2 -0
- package/runtime/before.d.ts +32 -0
- package/runtime/before.d.ts.map +1 -0
- package/runtime/before.js +214 -0
- package/runtime/claude.d.ts +2 -1
- package/runtime/claude.d.ts.map +1 -1
- package/runtime/conversations.d.ts +1 -0
- package/runtime/conversations.d.ts.map +1 -1
- package/runtime/conversations.js +333 -61
- package/runtime/dependencies.js +1 -1
- package/runtime/engine.d.ts +2 -6
- package/runtime/engine.d.ts.map +1 -1
- package/runtime/grants.d.ts.map +1 -1
- package/runtime/grants.js +8 -8
- package/runtime/hooks.d.ts +1 -1
- package/runtime/hooks.d.ts.map +1 -1
- package/runtime/hooks.js +19 -4
- package/runtime/installer.d.ts +1 -1
- package/runtime/installer.d.ts.map +1 -1
- package/runtime/installer.js +6 -5
- package/runtime/lifecycle.d.ts.map +1 -1
- package/runtime/lifecycle.js +129 -58
- package/runtime/metadata.d.ts.map +1 -1
- package/runtime/metadata.js +5 -4
- package/runtime/sessions.d.ts +26 -4
- package/runtime/sessions.d.ts.map +1 -1
- package/runtime/sessions.js +315 -117
- package/runtime/toaster.d.ts +5 -12
- package/runtime/toaster.d.ts.map +1 -1
- package/runtime/toaster.js +133 -23
- package/runtime/ui.d.ts.map +1 -1
- package/runtime/ui.js +26 -6
- package/session/context/context.d.ts +3 -1
- package/session/context/context.d.ts.map +1 -1
- package/session/context/turn/message/message.d.ts +1 -0
- package/session/context/turn/message/message.d.ts.map +1 -1
- package/session/context/turn/message/message.js +42 -1
- package/session/context/turn/turn.d.ts +2 -1
- package/session/context/turn/turn.d.ts.map +1 -1
- package/session/conversation.d.ts +20 -11
- package/session/conversation.d.ts.map +1 -1
- package/session/conversation.js +50 -27
- package/session/goal/goal.d.ts +3 -2
- package/session/goal/goal.d.ts.map +1 -1
- package/session/goal/task.d.ts +4 -4
- package/session/goal/task.d.ts.map +1 -1
- package/session/session.d.ts +80 -21
- package/session/session.d.ts.map +1 -1
- package/session/session.js +1 -0
- package/src/jobs/program.ts +7 -2
- package/src/mod.ts +48 -5
- package/src/records.ts +6 -1
- package/src/register.ts +17 -0
- package/src/runtime/announce.ts +2 -0
- package/src/runtime/before.ts +200 -0
- package/src/runtime/claude.ts +3 -0
- package/src/runtime/conversations.ts +337 -60
- package/src/runtime/dependencies.ts +1 -1
- package/src/runtime/engine.ts +2 -1
- package/src/runtime/grants.ts +8 -10
- package/src/runtime/hooks.ts +20 -4
- package/src/runtime/installer.ts +6 -5
- package/src/runtime/lifecycle.ts +135 -55
- package/src/runtime/metadata.ts +5 -4
- package/src/runtime/sessions.ts +316 -111
- package/src/runtime/toaster.ts +160 -44
- package/src/runtime/ui.ts +23 -4
- package/src/session/context/context.ts +3 -1
- package/src/session/context/turn/message/message.ts +39 -1
- package/src/session/context/turn/turn.ts +2 -1
- package/src/session/conversation.ts +56 -31
- package/src/session/goal/goal.ts +3 -2
- package/src/session/goal/task.ts +4 -4
- package/src/session/session.ts +62 -15
- package/src/testing/fake-claude.ts +4 -2
- package/src/testing/fake-sessions.ts +94 -20
- package/src/testing.ts +27 -3
- package/src/utils/call-effects.ts +2 -2
- package/src/utils/disposable.ts +3 -0
- package/src/utils/matcher.ts +1 -1
- package/src/utils/paths.ts +6 -3
- package/testing/fake-claude.d.ts +1 -0
- package/testing/fake-claude.d.ts.map +1 -1
- package/testing/fake-claude.js +3 -1
- package/testing/fake-sessions.d.ts +8 -1
- package/testing/fake-sessions.d.ts.map +1 -1
- package/testing/fake-sessions.js +97 -18
- package/testing.d.ts +1 -0
- package/testing.d.ts.map +1 -1
- package/testing.js +23 -2
- package/utils/call-effects.d.ts +1 -0
- package/utils/call-effects.d.ts.map +1 -1
- package/utils/call-effects.js +1 -1
- package/utils/disposable.d.ts +4 -0
- package/utils/disposable.d.ts.map +1 -0
- package/utils/disposable.js +1 -0
- package/utils/matcher.js +1 -1
- package/utils/paths.d.ts +2 -1
- package/utils/paths.d.ts.map +1 -1
- package/utils/paths.js +6 -3
package/src/session/goal/task.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Message } from '../context/turn/message/message.js'
|
|
2
|
+
import type { RecordPart, Session } from '../session.js'
|
|
2
3
|
|
|
3
4
|
export const Task = 'task'
|
|
4
5
|
|
|
@@ -6,14 +7,13 @@ export const TaskStart = 'taskStart'
|
|
|
6
7
|
|
|
7
8
|
export const TaskEnd = 'taskEnd'
|
|
8
9
|
|
|
9
|
-
export type Task = {
|
|
10
|
+
export type Task = RecordPart & {
|
|
10
11
|
readonly id: string
|
|
11
12
|
readonly title: string
|
|
12
|
-
readonly
|
|
13
|
-
readonly model?: string
|
|
13
|
+
readonly owner: Session
|
|
14
14
|
readonly openedBy: string
|
|
15
15
|
readonly openedAt: string
|
|
16
16
|
readonly closedAt?: string
|
|
17
|
-
messages(): Promise<Message[]>
|
|
18
17
|
tasks(): Promise<Task[]>
|
|
18
|
+
messages(): Promise<Message[]>
|
|
19
19
|
}
|
package/src/session/session.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Disposable } from '../
|
|
1
|
+
import type { Disposable } from '../utils/disposable.js'
|
|
2
2
|
import type { Matcher } from '../utils/matcher.js'
|
|
3
3
|
import type { Context, ContextEnd, ContextStart } from './context/context.js'
|
|
4
4
|
import type { Document } from './context/turn/message/document.js'
|
|
@@ -17,7 +17,9 @@ import type { Task, TaskEnd, TaskStart } from './goal/task.js'
|
|
|
17
17
|
|
|
18
18
|
export const Session = 'session'
|
|
19
19
|
|
|
20
|
-
export
|
|
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
|
|
21
23
|
|
|
22
24
|
export type SessionPayload = {
|
|
23
25
|
readonly message: Message
|
|
@@ -36,16 +38,53 @@ export type SessionPayload = {
|
|
|
36
38
|
readonly taskStart: Task
|
|
37
39
|
readonly taskEnd: Task
|
|
38
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
|
|
39
59
|
}
|
|
40
60
|
|
|
61
|
+
export type BeforeEvent = keyof BeforeAnswer
|
|
62
|
+
|
|
41
63
|
export type Usage = {
|
|
42
64
|
readonly model: string
|
|
43
|
-
readonly context: { readonly tokens?: number; readonly window
|
|
65
|
+
readonly context: { readonly tokens?: number; readonly window?: number; readonly percent?: number }
|
|
44
66
|
readonly cost?: { readonly usd: number }
|
|
45
67
|
}
|
|
46
68
|
|
|
69
|
+
export type Client = 'terminal' | 'desktop' | 'mobile' | 'vscode'
|
|
70
|
+
|
|
47
71
|
export type Listener<Event extends SessionEvent> = (payload: SessionPayload[Event]) => unknown
|
|
48
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
|
+
|
|
49
88
|
export type SessionSend = {
|
|
50
89
|
readonly type: typeof Prompt | typeof Note
|
|
51
90
|
readonly text: string
|
|
@@ -60,25 +99,33 @@ export type TaskStartArgs = {
|
|
|
60
99
|
readonly cwd?: string
|
|
61
100
|
}
|
|
62
101
|
|
|
63
|
-
export type
|
|
102
|
+
export type ToolCallArgs = {
|
|
103
|
+
readonly name: string
|
|
104
|
+
readonly input: Readonly<Record<string, unknown>>
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export type Session = RecordPart & {
|
|
64
108
|
readonly id: string
|
|
65
109
|
readonly title: string
|
|
66
110
|
readonly project: string
|
|
67
|
-
readonly
|
|
111
|
+
readonly agentType?: string
|
|
68
112
|
readonly model: string
|
|
113
|
+
readonly cwd: string
|
|
114
|
+
readonly isRunning: boolean
|
|
69
115
|
readonly startedAt: string
|
|
70
116
|
readonly lastAt: string
|
|
71
117
|
readonly forkedFrom?: string
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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[]>
|
|
76
124
|
send(message: SessionSend): Promise<void>
|
|
77
125
|
startTask(task: TaskStartArgs): Promise<Task>
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
goal(): Promise<Goal>
|
|
126
|
+
callTool(call: ToolCallArgs): Promise<ToolResult>
|
|
127
|
+
usage(): Promise<Usage>
|
|
128
|
+
clients(): Promise<Client[]>
|
|
82
129
|
}
|
|
83
130
|
|
|
84
131
|
export type SessionScope = {
|
|
@@ -87,8 +134,8 @@ export type SessionScope = {
|
|
|
87
134
|
readonly everyProject?: boolean
|
|
88
135
|
}
|
|
89
136
|
|
|
90
|
-
export type Sessions = {
|
|
137
|
+
export type Sessions = Omit<RecordPart, 'search'> & {
|
|
91
138
|
readonly current: Session
|
|
92
139
|
list(options?: SessionScope): Promise<Session[]>
|
|
93
|
-
search(text: string, options?: SessionScope &
|
|
140
|
+
search(text: string, options?: SessionScope & SearchOptions): Promise<Message[]>
|
|
94
141
|
}
|
|
@@ -15,7 +15,7 @@ export type Fakes = {
|
|
|
15
15
|
http: { fetch?: Claude['http']['fetch'] }
|
|
16
16
|
settings: { read?: Claude['settings']['read'] }
|
|
17
17
|
config: { list?: Claude['config']['list'] }
|
|
18
|
-
ui: { ask?: Claude['ui']['ask']; open?: Claude['ui']['open']; scroll?: Claude['ui']['scroll']; copy?: Claude['ui']['copy'] }
|
|
18
|
+
ui: { ask?: Claude['ui']['ask']; open?: Claude['ui']['open']; scroll?: Claude['ui']['scroll']; copy?: Claude['ui']['copy']; focus?: Claude['ui']['focus'] }
|
|
19
19
|
session: { messages?: Claude['session']['messages']; append?: Claude['session']['append']; send?: Claude['session']['send'] }
|
|
20
20
|
prompt: { submit?: Claude['prompt']['submit'] }
|
|
21
21
|
model: { complete?: Claude['model']['complete'] }
|
|
@@ -119,6 +119,7 @@ export function fakeClaude(plugin: { readonly name: string; readonly root: strin
|
|
|
119
119
|
calls.push({ call: 'ui.open', args: [pane] })
|
|
120
120
|
const answer = fakes.ui.open === undefined ? { isPlaced: true as const } : await fakes.ui.open(pane)
|
|
121
121
|
if (answer.isPlaced) {
|
|
122
|
+
shown.openPanes.delete(pane.id)
|
|
122
123
|
shown.openPanes.add(pane.id)
|
|
123
124
|
unplacedPanes.delete(pane.id)
|
|
124
125
|
if (pane.focus === true) focusedPane = pane.id
|
|
@@ -134,11 +135,12 @@ export function fakeClaude(plugin: { readonly name: string; readonly root: strin
|
|
|
134
135
|
if (focusedPane === pane.id) focusedPane = undefined
|
|
135
136
|
},
|
|
136
137
|
panes: async () => [
|
|
137
|
-
...[...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 })),
|
|
138
139
|
...[...unplacedPanes].map((id) => ({ id, title: id, isShown: false, isFocused: false, isPlaced: false })),
|
|
139
140
|
],
|
|
140
141
|
scroll: rejected('ui.scroll', () => fakes.ui.scroll ?? (async () => ({}))),
|
|
141
142
|
copy: rejected('ui.copy', () => fakes.ui.copy ?? (async () => ({ isCopied: true as const }))),
|
|
143
|
+
focus: rejected('ui.focus', () => fakes.ui.focus ?? (async () => ({}))),
|
|
142
144
|
resolve: () => elements,
|
|
143
145
|
},
|
|
144
146
|
process: {
|
|
@@ -6,12 +6,15 @@ import { Notice } from '../session/context/turn/message/notice.js'
|
|
|
6
6
|
import { Prompt } from '../session/context/turn/message/prompt.js'
|
|
7
7
|
import { Reply } from '../session/context/turn/message/reply.js'
|
|
8
8
|
import { Mod, Person, Schedule, type Sender } from '../session/context/turn/message/sender.js'
|
|
9
|
+
import { GoalChange } from '../session/goal/goal.js'
|
|
9
10
|
import { Task } from '../session/goal/task.js'
|
|
10
11
|
import { Skill } from '../session/context/turn/message/skill.js'
|
|
11
12
|
import { ToolCall } from '../session/context/turn/message/tool-call.js'
|
|
12
13
|
import { ToolResult } from '../session/context/turn/message/tool-result.js'
|
|
13
14
|
import { createConversation, emptyConversation, searchLimit, type ContextData, type Conversation, type SessionChange, type SessionData, type TaskData, type TurnData } from '../session/conversation.js'
|
|
14
|
-
import {
|
|
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'
|
|
15
18
|
import { receivedMatcher } from '../utils/matcher.js'
|
|
16
19
|
import { firstLine, messageOf } from '../utils/text.js'
|
|
17
20
|
|
|
@@ -46,16 +49,20 @@ type Heard = { readonly event: Subscription['event']; readonly matcher: unknown
|
|
|
46
49
|
export type FakeSessions = {
|
|
47
50
|
answer(caller: string, method: string, input: unknown): unknown
|
|
48
51
|
add(draft: MessageDraft, task?: string): Message[]
|
|
52
|
+
place(): Place
|
|
53
|
+
wants(mod: string, event: string): boolean
|
|
49
54
|
endTurn(): void
|
|
50
55
|
compact(): void
|
|
51
|
-
startTask(task: { readonly title: string; readonly agentType?: string; readonly model?: string; readonly openedBy?: string }): string
|
|
56
|
+
startTask(task: { readonly title: string; readonly agentType?: string; readonly model?: string; readonly openedBy?: string; readonly parent?: string }): string
|
|
52
57
|
start(id: string): void
|
|
53
|
-
readonly sent: { readonly session: string; readonly type: string; readonly text: string; readonly as?: string }[]
|
|
58
|
+
readonly sent: { readonly session: string; readonly task?: string; readonly type: string; readonly text: string; readonly as?: string }[]
|
|
54
59
|
}
|
|
55
60
|
|
|
56
|
-
export function fakeSessions({ project, given, push }: { readonly project: () => string; readonly given: readonly FakeSession[]; readonly push: (mod: string, session: string, change: SessionChange) => void }): FakeSessions {
|
|
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 {
|
|
57
62
|
const kept = new Map<string, Kept>()
|
|
58
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>()
|
|
59
66
|
const sent: FakeSessions['sent'] = []
|
|
60
67
|
let current = ''
|
|
61
68
|
let clock = Date.parse('2026-01-01T00:00:00.000Z')
|
|
@@ -76,7 +83,7 @@ export function fakeSessions({ project, given, push }: { readonly project: () =>
|
|
|
76
83
|
if ('task' in change) session.tasks.set(change.task.id, change.task)
|
|
77
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) } : {}) }
|
|
78
85
|
for (const [mod, sessions] of heard) {
|
|
79
|
-
if ((sessions.get(session.data.id) ?? []).some(({ event, matcher }) => isHeardBy(event, matcher, change))) push(mod, session.data.id, change)
|
|
86
|
+
if ([...(sessions.get(session.data.id) ?? []), ...(sessions.get(everySession) ?? [])].some(({ event, matcher }) => isHeardBy(event, matcher, change))) push(mod, session.data.id, change)
|
|
80
87
|
}
|
|
81
88
|
}
|
|
82
89
|
}
|
|
@@ -101,6 +108,24 @@ export function fakeSessions({ project, given, push }: { readonly project: () =>
|
|
|
101
108
|
|
|
102
109
|
const currentSession = () => sessionOf(current)
|
|
103
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
|
+
|
|
104
129
|
const lastOf = <Item>(items: readonly Item[], last: unknown): Item[] => (typeof last === 'number' ? items.slice(-last) : [...items])
|
|
105
130
|
|
|
106
131
|
const isInScope = (session: Kept, request: Readonly<Record<string, unknown>>) =>
|
|
@@ -115,10 +140,18 @@ export function fakeSessions({ project, given, push }: { readonly project: () =>
|
|
|
115
140
|
[conversationMethods.search](request) {
|
|
116
141
|
const text = String(request['text'])
|
|
117
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
|
+
}
|
|
118
151
|
const found = [...kept.values()]
|
|
119
|
-
.filter((session) => isInScope(session, request))
|
|
152
|
+
.filter((session) => request['session'] !== undefined || isInScope(session, request))
|
|
120
153
|
.flatMap((session) => session.messages)
|
|
121
|
-
.filter((message) => searchText(message).toLowerCase().includes(text.toLowerCase()))
|
|
154
|
+
.filter((message) => inRecord(message) && searchText(message).toLowerCase().includes(text.toLowerCase()))
|
|
122
155
|
.toSorted((first, second) => second.at.localeCompare(first.at))
|
|
123
156
|
const before = isObject(request['before']) ? request['before'] : undefined
|
|
124
157
|
const start = before === undefined ? 0 : found.findIndex((message) => message.session === before['session'] && message.id === before['id']) + 1
|
|
@@ -130,15 +163,59 @@ export function fakeSessions({ project, given, push }: { readonly project: () =>
|
|
|
130
163
|
const after = typeof request['after'] === 'string' ? fits.findIndex((message) => message.id === request['after']) + 1 : 0
|
|
131
164
|
return lastOf(fits.slice(after), request['last'])
|
|
132
165
|
},
|
|
133
|
-
[conversationMethods.turns]: (request) => lastOf(
|
|
134
|
-
[conversationMethods.contexts]
|
|
135
|
-
|
|
136
|
-
|
|
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
|
+
},
|
|
137
180
|
[conversationMethods.updateGoal](request) {
|
|
138
181
|
const session = sessionOf(String(request['session']))
|
|
139
|
-
|
|
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
|
+
}
|
|
140
189
|
return null
|
|
141
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
|
+
},
|
|
142
219
|
[conversationMethods.subscribe](request, caller) {
|
|
143
220
|
const list = Array.isArray(request['subscriptions']) ? (request['subscriptions'] as readonly Subscription[]) : []
|
|
144
221
|
const sessions = heard.get(caller) ?? new Map<string, readonly Heard[]>()
|
|
@@ -146,10 +223,6 @@ export function fakeSessions({ project, given, push }: { readonly project: () =>
|
|
|
146
223
|
heard.set(caller, sessions)
|
|
147
224
|
return null
|
|
148
225
|
},
|
|
149
|
-
[conversationMethods.send](request) {
|
|
150
|
-
sent.push({ session: String(request['session']), type: String(request['type']), text: String(request['text']), ...(typeof request['as'] === 'string' ? { as: request['as'] } : {}) })
|
|
151
|
-
return null
|
|
152
|
-
},
|
|
153
226
|
}
|
|
154
227
|
|
|
155
228
|
return {
|
|
@@ -163,19 +236,20 @@ export function fakeSessions({ project, given, push }: { readonly project: () =>
|
|
|
163
236
|
}
|
|
164
237
|
},
|
|
165
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)),
|
|
166
241
|
endTurn() {
|
|
167
242
|
const session = currentSession()
|
|
168
243
|
record(session, session.conversation.add({ id: `turn-end-${(ids += 1)}`, at: now(), type: 'system', name: 'turn_duration', isMeta: false, isSummary: false, content: [] }))
|
|
169
244
|
},
|
|
170
245
|
compact() {
|
|
171
246
|
const session = currentSession()
|
|
172
|
-
|
|
173
|
-
record(session, [...session.conversation.endContext(at), ...session.conversation.startContext(at)])
|
|
247
|
+
record(session, session.conversation.add({ id: `compact-${(ids += 1)}`, at: now(), type: 'system', name: 'compact_boundary', isMeta: false, isSummary: false, content: [] }))
|
|
174
248
|
},
|
|
175
|
-
startTask({ title, agentType = 'general-purpose', model, openedBy = '' }) {
|
|
249
|
+
startTask({ title, agentType = 'general-purpose', model, openedBy = '', parent }) {
|
|
176
250
|
const session = currentSession()
|
|
177
251
|
const id = `task-${(ids += 1)}`
|
|
178
|
-
record(session, session.conversation.startTask({ id, title, agentType, openedBy, openedAt: now(), ...(model === undefined ? {} : { model }) }))
|
|
252
|
+
record(session, session.conversation.startTask({ id, title, agentType, openedBy, openedAt: now(), ...(model === undefined ? {} : { model }), ...(parent === undefined ? {} : { parent }) }))
|
|
179
253
|
return id
|
|
180
254
|
},
|
|
181
255
|
start(id) {
|
package/src/testing.ts
CHANGED
|
@@ -23,7 +23,9 @@ import type { Options, OptionValues } from './options.js'
|
|
|
23
23
|
import { readSteps, storeFolder, type Steps } from './records.js'
|
|
24
24
|
import type { Claude } from './runtime/claude.js'
|
|
25
25
|
import { answerCall, cmodPluginName, notInstalled } from './runtime/dependencies.js'
|
|
26
|
+
import { foldAnswers, installBefore } from './runtime/before.js'
|
|
26
27
|
import type { RoutedEvent } from './runtime/hooks.js'
|
|
28
|
+
import { createRouter } from './runtime/router.js'
|
|
27
29
|
import { toolInputOf } from './runtime/tool-calls.js'
|
|
28
30
|
import { createLifecycle } from './runtime/lifecycle.js'
|
|
29
31
|
import { MissingOptions } from './runtime/options.js'
|
|
@@ -33,11 +35,13 @@ import type { Message } from './session/context/turn/message/message.js'
|
|
|
33
35
|
import { Note } from './session/context/turn/message/note.js'
|
|
34
36
|
import { Prompt } from './session/context/turn/message/prompt.js'
|
|
35
37
|
import { Mod, Person } from './session/context/turn/message/sender.js'
|
|
38
|
+
import type { SessionChange } from './session/conversation.js'
|
|
39
|
+
import type { BeforeEvent } from './session/session.js'
|
|
36
40
|
import { calledAnswer, fakeClaude, type Fakes, type Shown as ShownByClaude, type TestCall } from './testing/fake-claude.js'
|
|
37
41
|
import { elements, findElement, rowsOf } from './testing/fake-elements.js'
|
|
38
42
|
import { fakeFiles } from './testing/fake-files.js'
|
|
39
43
|
import { fakeSessions, type FakeSession, type FakeSessions, type MessageDraft } from './testing/fake-sessions.js'
|
|
40
|
-
import { listed } from './utils/text.js'
|
|
44
|
+
import { listed, messageOf } from './utils/text.js'
|
|
41
45
|
import { componentOf, type ComponentPlace, type PlaceProps } from './ui/places.js'
|
|
42
46
|
|
|
43
47
|
export type { Fakes, TestCall } from './testing/fake-claude.js'
|
|
@@ -72,7 +76,7 @@ export type TestedMod<State extends object> = {
|
|
|
72
76
|
add(message: MessageDraft, options?: { readonly task?: string }): Promise<Message[]>
|
|
73
77
|
endTurn(): Promise<void>
|
|
74
78
|
compact(): Promise<void>
|
|
75
|
-
startTask(task: { readonly title: string; readonly agentType?: string; readonly model?: string; readonly openedBy?: string }): Promise<string>
|
|
79
|
+
startTask(task: { readonly title: string; readonly agentType?: string; readonly model?: string; readonly openedBy?: string; readonly parent?: string }): Promise<string>
|
|
76
80
|
lines(paneId: string): Promise<string[]>
|
|
77
81
|
lines<P extends ComponentPlace>(place: P, props: Omit<PlaceProps<P>, 'Default'>, requestId?: string): Promise<string[]>
|
|
78
82
|
type(line: string): Promise<CommandReply>
|
|
@@ -111,6 +115,7 @@ export function testMod<State extends object, Declared extends Options = Options
|
|
|
111
115
|
fake.claude.session.root = async () => projectRoot
|
|
112
116
|
fake.claude.session.cwd = async () => cwd
|
|
113
117
|
const conversations = fakeSessions({
|
|
118
|
+
callTool: (args: Parameters<Claude["tool"]["call"]>[0]) => fake.claude.tool.call(args),
|
|
114
119
|
project: () => projectRoot,
|
|
115
120
|
given: options.sessions ?? [],
|
|
116
121
|
push: (mod, session, change) => {
|
|
@@ -171,9 +176,24 @@ export function testMod<State extends object, Declared extends Options = Options
|
|
|
171
176
|
if (lifecycle.phase !== 'active') throw failure ?? new Error(`${name} did not start: the lifecycle is ${lifecycle.phase}.`)
|
|
172
177
|
}
|
|
173
178
|
|
|
179
|
+
const cmodPlugin = createRouter()
|
|
180
|
+
const askMod = async (event: BeforeEvent, change: SessionChange) => {
|
|
181
|
+
const asked = { to: name, method: conversationMethods.before, input: { session: sessionId, event, change }, caller: cmodPluginName } as unknown as Frozen<Args<'cmod.call'>>
|
|
182
|
+
return calledAnswer(cmodPluginName, await lifecycle.route('cmod.call', asked, async () => ({ deny: notInstalled(name) })))
|
|
183
|
+
}
|
|
184
|
+
installBefore({
|
|
185
|
+
claude: fake.claude,
|
|
186
|
+
router: cmodPlugin,
|
|
187
|
+
hooks: {
|
|
188
|
+
place: async () => conversations.place(),
|
|
189
|
+
wants: async (event) => conversations.wants(name, event),
|
|
190
|
+
decide: (event, change) => foldAnswers(event, change, [(latest) => askMod(event, latest)], (error) => fake.claude.ui.log(`cmod: passed the ${event} on without ${name}'s answer: ${messageOf(error)}`, { to: 'debug' })),
|
|
191
|
+
},
|
|
192
|
+
})
|
|
193
|
+
|
|
174
194
|
const route = async <N extends RoutedEvent>(event: N, input: unknown, below: unknown): Promise<EventResult<N>> => {
|
|
175
195
|
await start()
|
|
176
|
-
return
|
|
196
|
+
return cmodPlugin.dispatch(event, input as Frozen<Args<N>>, (passed) => lifecycle.route(event, passed, async (e) => (below ?? claudeCodeAnswer(event, e)) as EventResult<N>))
|
|
177
197
|
}
|
|
178
198
|
|
|
179
199
|
const drawPane = async (paneId: string) => {
|
|
@@ -360,6 +380,10 @@ function fakeDependencies(caller: string, dependencies: NonNullable<TestOptions<
|
|
|
360
380
|
}
|
|
361
381
|
|
|
362
382
|
function claudeCodeAnswer(event: RoutedEvent, e: unknown): unknown {
|
|
383
|
+
if (event === 'agent.spawn') {
|
|
384
|
+
const { model, parentModel } = e as Args<'agent.spawn'>
|
|
385
|
+
return { agentId: 'agent-1', model: model ?? parentModel }
|
|
386
|
+
}
|
|
363
387
|
if (event !== 'prompt.submit') return {}
|
|
364
388
|
const { text, context, origin } = e as Args<'prompt.submit'>
|
|
365
389
|
return { text, ...(context === undefined ? {} : { context }), origin }
|
|
@@ -18,9 +18,9 @@ export type CallEffects = {
|
|
|
18
18
|
subagent: string | undefined
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
export function callEffects(use: CallBase, workspace: { cwd: string; home: string; fs: FileSystem }): CallEffects {
|
|
21
|
+
export function callEffects(use: CallBase, workspace: { cwd: string; home: string; configRoot?: string; fs: FileSystem }): CallEffects {
|
|
22
22
|
const { fs } = workspace
|
|
23
|
-
const absolute = (path: string, folder = workspace.cwd) => resolve(folder, expandHome(path, workspace.home))
|
|
23
|
+
const absolute = (path: string, folder = workspace.cwd) => resolve(folder, expandHome(path, workspace.home, workspace.configRoot))
|
|
24
24
|
const access = (path: string): FileAccess => ({ path: absolute(path), contents: undefined })
|
|
25
25
|
const effects: CallEffects = { shell: undefined, reads: [], writes: [], urls: [], subagent: undefined }
|
|
26
26
|
if (use.tool === 'Bash') {
|
package/src/utils/matcher.ts
CHANGED
|
@@ -13,7 +13,7 @@ export type Matcher<Value> = Value extends string
|
|
|
13
13
|
export type SentMatcher = string | number | boolean | null | { readonly regexp: string; readonly flags: string } | { readonly [key: string]: SentMatcher } | readonly SentMatcher[]
|
|
14
14
|
|
|
15
15
|
export function matches(value: unknown, matcher: unknown): boolean {
|
|
16
|
-
if (matcher instanceof RegExp) return matcher.test(String(value))
|
|
16
|
+
if (matcher instanceof RegExp) return value !== undefined && value !== null && matcher.test(String(value))
|
|
17
17
|
if (Array.isArray(matcher)) return matcher.some((each) => matches(value, each))
|
|
18
18
|
if (typeof matcher !== 'object' || matcher === null) return value === matcher
|
|
19
19
|
if (Array.isArray(value)) return value.some((each) => matches(each, matcher))
|
package/src/utils/paths.ts
CHANGED
|
@@ -20,9 +20,12 @@ export type FileSystem = {
|
|
|
20
20
|
exists: (path: string) => Promise<boolean>
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
export
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
export const homePrefix = /^(~|\$HOME|\$CLAUDE_CONFIG_DIR)(?=\/|$)/
|
|
24
|
+
|
|
25
|
+
export function expandHome(path: string, home: string, configRoot = `${home}/.claude`): string {
|
|
26
|
+
const prefix = homePrefix.exec(path)?.[0]
|
|
27
|
+
if (prefix === undefined) return path
|
|
28
|
+
return `${prefix === '$CLAUDE_CONFIG_DIR' ? configRoot : home}${path.slice(prefix.length)}`
|
|
26
29
|
}
|
|
27
30
|
|
|
28
31
|
export function relativePath(base: string, path: string): string | undefined {
|
package/testing/fake-claude.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fake-claude.d.ts","sourceRoot":"","sources":["../src/testing/fake-claude.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAS,MAAM,aAAa,CAAA;AAErD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,sBAAsB,CAAA;AAIlD,MAAM,MAAM,QAAQ,GAAG;IACrB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,IAAI,EAAE,SAAS,OAAO,EAAE,CAAA;CAClC,CAAA;AAED,MAAM,MAAM,KAAK,GAAG;IAClB,OAAO,EAAE;QAAE,GAAG,CAAC,EAAE,MAAM,CAAC,SAAS,CAAC,CAAC,KAAK,CAAC,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC,SAAS,CAAC,CAAC,OAAO,CAAC,CAAA;KAAE,CAAA;IAC/E,EAAE,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC,IAAI,CAAC,CAAC,OAAO,CAAC,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAC,IAAI,CAAC,CAAC,QAAQ,CAAC,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,CAAA;KAAE,CAAA;IAC7J,IAAI,EAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC,OAAO,CAAC,CAAA;KAAE,CAAA;IACzC,QAAQ,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC,CAAC,MAAM,CAAC,CAAA;KAAE,CAAA;IAC/C,MAAM,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,CAAA;KAAE,CAAA;IAC3C,EAAE,EAAE;QAAE,GAAG,CAAC,EAAE,MAAM,CAAC,IAAI,CAAC,CAAC,KAAK,CAAC,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAC,IAAI,CAAC,CAAC,QAAQ,CAAC,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,CAAA;KAAE,CAAA;
|
|
1
|
+
{"version":3,"file":"fake-claude.d.ts","sourceRoot":"","sources":["../src/testing/fake-claude.ts"],"names":[],"mappings":"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"}
|
package/testing/fake-claude.js
CHANGED
|
@@ -71,6 +71,7 @@ export function fakeClaude(plugin) {
|
|
|
71
71
|
calls.push({ call: 'ui.open', args: [pane] });
|
|
72
72
|
const answer = fakes.ui.open === undefined ? { isPlaced: true } : await fakes.ui.open(pane);
|
|
73
73
|
if (answer.isPlaced) {
|
|
74
|
+
shown.openPanes.delete(pane.id);
|
|
74
75
|
shown.openPanes.add(pane.id);
|
|
75
76
|
unplacedPanes.delete(pane.id);
|
|
76
77
|
if (pane.focus === true)
|
|
@@ -89,11 +90,12 @@ export function fakeClaude(plugin) {
|
|
|
89
90
|
focusedPane = undefined;
|
|
90
91
|
},
|
|
91
92
|
panes: async () => [
|
|
92
|
-
...[...shown.openPanes].map((id) => ({ id, title: id, isShown:
|
|
93
|
+
...[...shown.openPanes].map((id) => ({ id, title: id, isShown: id === [...shown.openPanes].at(-1), isFocused: id === focusedPane, isPlaced: true })),
|
|
93
94
|
...[...unplacedPanes].map((id) => ({ id, title: id, isShown: false, isFocused: false, isPlaced: false })),
|
|
94
95
|
],
|
|
95
96
|
scroll: rejected('ui.scroll', () => fakes.ui.scroll ?? (async () => ({}))),
|
|
96
97
|
copy: rejected('ui.copy', () => fakes.ui.copy ?? (async () => ({ isCopied: true }))),
|
|
98
|
+
focus: rejected('ui.focus', () => fakes.ui.focus ?? (async () => ({}))),
|
|
97
99
|
resolve: () => elements,
|
|
98
100
|
},
|
|
99
101
|
process: {
|
|
@@ -9,6 +9,8 @@ import { Skill } from '../session/context/turn/message/skill.js';
|
|
|
9
9
|
import { ToolCall } from '../session/context/turn/message/tool-call.js';
|
|
10
10
|
import { ToolResult } from '../session/context/turn/message/tool-result.js';
|
|
11
11
|
import { type SessionChange, type SessionData } from '../session/conversation.js';
|
|
12
|
+
import type { Place } from '../runtime/before.js';
|
|
13
|
+
import type { Claude } from '../runtime/claude.js';
|
|
12
14
|
export type MessageDraft = {
|
|
13
15
|
readonly type: typeof Prompt;
|
|
14
16
|
readonly text: string;
|
|
@@ -50,6 +52,8 @@ export type FakeSession = Partial<Omit<SessionData, 'id'>> & {
|
|
|
50
52
|
export type FakeSessions = {
|
|
51
53
|
answer(caller: string, method: string, input: unknown): unknown;
|
|
52
54
|
add(draft: MessageDraft, task?: string): Message[];
|
|
55
|
+
place(): Place;
|
|
56
|
+
wants(mod: string, event: string): boolean;
|
|
53
57
|
endTurn(): void;
|
|
54
58
|
compact(): void;
|
|
55
59
|
startTask(task: {
|
|
@@ -57,18 +61,21 @@ export type FakeSessions = {
|
|
|
57
61
|
readonly agentType?: string;
|
|
58
62
|
readonly model?: string;
|
|
59
63
|
readonly openedBy?: string;
|
|
64
|
+
readonly parent?: string;
|
|
60
65
|
}): string;
|
|
61
66
|
start(id: string): void;
|
|
62
67
|
readonly sent: {
|
|
63
68
|
readonly session: string;
|
|
69
|
+
readonly task?: string;
|
|
64
70
|
readonly type: string;
|
|
65
71
|
readonly text: string;
|
|
66
72
|
readonly as?: string;
|
|
67
73
|
}[];
|
|
68
74
|
};
|
|
69
|
-
export declare function fakeSessions({ project, given, push }: {
|
|
75
|
+
export declare function fakeSessions({ project, given, push, callTool }: {
|
|
70
76
|
readonly project: () => string;
|
|
71
77
|
readonly given: readonly FakeSession[];
|
|
72
78
|
readonly push: (mod: string, session: string, change: SessionChange) => void;
|
|
79
|
+
readonly callTool: Claude['tool']['call'];
|
|
73
80
|
}): FakeSessions;
|
|
74
81
|
//# sourceMappingURL=fake-sessions.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fake-sessions.d.ts","sourceRoot":"","sources":["../src/testing/fake-sessions.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,QAAQ,EAAE,MAAM,6CAA6C,CAAA;AACtE,OAAO,EAAE,OAAO,EAAqC,MAAM,4CAA4C,CAAA;AACvG,OAAO,EAAE,IAAI,EAAE,MAAM,yCAAyC,CAAA;AAC9D,OAAO,EAAE,MAAM,EAAE,MAAM,2CAA2C,CAAA;AAClE,OAAO,EAAE,MAAM,EAAE,MAAM,2CAA2C,CAAA;AAClE,OAAO,EAAE,KAAK,EAAE,MAAM,0CAA0C,CAAA;AAChE,OAAO,EAAyB,KAAK,MAAM,EAAE,MAAM,2CAA2C,CAAA;
|
|
1
|
+
{"version":3,"file":"fake-sessions.d.ts","sourceRoot":"","sources":["../src/testing/fake-sessions.ts"],"names":[],"mappings":"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"}
|