@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.
Files changed (280) hide show
  1. package/docs/commands.md +61 -10
  2. package/docs/dependencies.md +16 -14
  3. package/docs/hooks.md +64 -29
  4. package/docs/index.md +29 -25
  5. package/docs/install-steps.md +13 -9
  6. package/docs/jobs.md +59 -111
  7. package/docs/mod.md +227 -175
  8. package/docs/options.md +2 -2
  9. package/docs/permissions.md +27 -16
  10. package/docs/session.md +311 -0
  11. package/docs/state.md +11 -7
  12. package/docs/testing.md +68 -23
  13. package/docs/ui.md +116 -76
  14. package/jobs/check.d.ts.map +1 -1
  15. package/jobs/check.js +5 -2
  16. package/jobs/context.d.ts +7 -8
  17. package/jobs/context.d.ts.map +1 -1
  18. package/jobs/context.js +23 -73
  19. package/jobs/permissions/decide-permission.d.ts +5 -3
  20. package/jobs/permissions/decide-permission.d.ts.map +1 -1
  21. package/jobs/permissions/decide-permission.js +6 -4
  22. package/jobs/permissions/find-project-scope.d.ts +1 -1
  23. package/jobs/permissions/find-project-scope.d.ts.map +1 -1
  24. package/jobs/permissions/find-project-scope.js +8 -11
  25. package/jobs/permissions/match-target.d.ts +2 -2
  26. package/jobs/permissions/match-target.d.ts.map +1 -1
  27. package/jobs/permissions.d.ts +1 -1
  28. package/jobs/permissions.d.ts.map +1 -1
  29. package/jobs/permissions.js +6 -4
  30. package/jobs/program.d.ts.map +1 -1
  31. package/jobs/program.js +14 -4
  32. package/jobs/prompt.d.ts +4 -2
  33. package/jobs/prompt.d.ts.map +1 -1
  34. package/jobs/prompt.js +16 -15
  35. package/jobs/schedule.d.ts +2 -2
  36. package/jobs/schedule.d.ts.map +1 -1
  37. package/jobs/schedule.js +9 -9
  38. package/jobs/slash-command.d.ts +4 -4
  39. package/jobs/slash-command.d.ts.map +1 -1
  40. package/jobs/slash-command.js +14 -9
  41. package/jobs/status-line.d.ts +1 -11
  42. package/jobs/status-line.d.ts.map +1 -1
  43. package/jobs/status-line.js +8 -12
  44. package/jobs/tool.d.ts +2 -2
  45. package/jobs/tool.d.ts.map +1 -1
  46. package/jobs/tool.js +15 -9
  47. package/mod.d.ts +178 -75
  48. package/mod.d.ts.map +1 -1
  49. package/mod.js +127 -3
  50. package/options.d.ts +2 -2
  51. package/options.js +19 -19
  52. package/package.json +3 -3
  53. package/records.d.ts +13 -8
  54. package/records.d.ts.map +1 -1
  55. package/records.js +33 -17
  56. package/register.d.ts.map +1 -1
  57. package/register.js +34 -5
  58. package/runtime/announce.d.ts +18 -0
  59. package/runtime/announce.d.ts.map +1 -0
  60. package/runtime/announce.js +68 -0
  61. package/runtime/before.d.ts +32 -0
  62. package/runtime/before.d.ts.map +1 -0
  63. package/runtime/before.js +214 -0
  64. package/runtime/bound.d.ts +4 -0
  65. package/runtime/bound.d.ts.map +1 -0
  66. package/runtime/bound.js +9 -0
  67. package/runtime/claude.d.ts +4 -2
  68. package/runtime/claude.d.ts.map +1 -1
  69. package/runtime/conversations.d.ts +17 -0
  70. package/runtime/conversations.d.ts.map +1 -0
  71. package/runtime/conversations.js +563 -0
  72. package/runtime/deadline.d.ts +0 -1
  73. package/runtime/deadline.d.ts.map +1 -1
  74. package/runtime/deadline.js +1 -1
  75. package/runtime/dependencies.d.ts +2 -0
  76. package/runtime/dependencies.d.ts.map +1 -1
  77. package/runtime/dependencies.js +10 -1
  78. package/runtime/engine.d.ts +13 -0
  79. package/runtime/engine.d.ts.map +1 -0
  80. package/runtime/engine.js +10 -0
  81. package/runtime/grants.d.ts +4 -0
  82. package/runtime/grants.d.ts.map +1 -1
  83. package/runtime/grants.js +68 -31
  84. package/runtime/hooks.d.ts +5 -4
  85. package/runtime/hooks.d.ts.map +1 -1
  86. package/runtime/hooks.js +29 -9
  87. package/runtime/installer.d.ts +1 -1
  88. package/runtime/installer.d.ts.map +1 -1
  89. package/runtime/installer.js +17 -14
  90. package/runtime/lifecycle.d.ts +3 -1
  91. package/runtime/lifecycle.d.ts.map +1 -1
  92. package/runtime/lifecycle.js +414 -212
  93. package/runtime/metadata.d.ts +3 -1
  94. package/runtime/metadata.d.ts.map +1 -1
  95. package/runtime/metadata.js +25 -14
  96. package/runtime/options.d.ts +3 -1
  97. package/runtime/options.d.ts.map +1 -1
  98. package/runtime/options.js +4 -4
  99. package/runtime/programs.d.ts +8 -0
  100. package/runtime/programs.d.ts.map +1 -1
  101. package/runtime/programs.js +39 -1
  102. package/runtime/router.d.ts +2 -1
  103. package/runtime/router.d.ts.map +1 -1
  104. package/runtime/router.js +11 -3
  105. package/runtime/sessions.d.ts +58 -0
  106. package/runtime/sessions.d.ts.map +1 -0
  107. package/runtime/sessions.js +433 -0
  108. package/runtime/state.d.ts +6 -2
  109. package/runtime/state.d.ts.map +1 -1
  110. package/runtime/state.js +46 -13
  111. package/runtime/toaster.d.ts +24 -0
  112. package/runtime/toaster.d.ts.map +1 -0
  113. package/runtime/toaster.js +286 -0
  114. package/runtime/ui.d.ts +6 -17
  115. package/runtime/ui.d.ts.map +1 -1
  116. package/runtime/ui.js +62 -118
  117. package/session/context/context.d.ts +12 -0
  118. package/session/context/context.d.ts.map +1 -0
  119. package/session/context/context.js +2 -0
  120. package/session/context/turn/message/document.d.ts +7 -0
  121. package/session/context/turn/message/document.d.ts.map +1 -0
  122. package/session/context/turn/message/document.js +1 -0
  123. package/session/context/turn/message/message.d.ts +55 -0
  124. package/session/context/turn/message/message.d.ts.map +1 -0
  125. package/session/context/turn/message/message.js +178 -0
  126. package/session/context/turn/message/note.d.ts +8 -0
  127. package/session/context/turn/message/note.d.ts.map +1 -0
  128. package/session/context/turn/message/note.js +1 -0
  129. package/session/context/turn/message/notice.d.ts +6 -0
  130. package/session/context/turn/message/notice.d.ts.map +1 -0
  131. package/session/context/turn/message/notice.js +1 -0
  132. package/session/context/turn/message/prompt.d.ts +8 -0
  133. package/session/context/turn/message/prompt.d.ts.map +1 -0
  134. package/session/context/turn/message/prompt.js +1 -0
  135. package/session/context/turn/message/reply.d.ts +6 -0
  136. package/session/context/turn/message/reply.d.ts.map +1 -0
  137. package/session/context/turn/message/reply.js +1 -0
  138. package/session/context/turn/message/sender.d.ts +14 -0
  139. package/session/context/turn/message/sender.d.ts.map +1 -0
  140. package/session/context/turn/message/sender.js +30 -0
  141. package/session/context/turn/message/skill.d.ts +7 -0
  142. package/session/context/turn/message/skill.d.ts.map +1 -0
  143. package/session/context/turn/message/skill.js +1 -0
  144. package/session/context/turn/message/tool-call.d.ts +9 -0
  145. package/session/context/turn/message/tool-call.d.ts.map +1 -0
  146. package/session/context/turn/message/tool-call.js +1 -0
  147. package/session/context/turn/message/tool-result.d.ts +8 -0
  148. package/session/context/turn/message/tool-result.d.ts.map +1 -0
  149. package/session/context/turn/message/tool-result.js +1 -0
  150. package/session/context/turn/turn.d.ts +13 -0
  151. package/session/context/turn/turn.d.ts.map +1 -0
  152. package/session/context/turn/turn.js +2 -0
  153. package/session/conversation.d.ts +73 -0
  154. package/session/conversation.d.ts.map +1 -0
  155. package/session/conversation.js +146 -0
  156. package/session/goal/goal.d.ts +11 -0
  157. package/session/goal/goal.d.ts.map +1 -0
  158. package/session/goal/goal.js +1 -0
  159. package/session/goal/task.d.ts +16 -0
  160. package/session/goal/task.d.ts.map +1 -0
  161. package/session/goal/task.js +3 -0
  162. package/session/session.d.ts +158 -0
  163. package/session/session.d.ts.map +1 -0
  164. package/session/session.js +2 -0
  165. package/src/jobs/check.ts +7 -4
  166. package/src/jobs/context.ts +24 -82
  167. package/src/jobs/permissions/decide-permission.ts +10 -8
  168. package/src/jobs/permissions/find-project-scope.ts +8 -14
  169. package/src/jobs/permissions/match-target.ts +3 -3
  170. package/src/jobs/permissions.ts +8 -6
  171. package/src/jobs/program.ts +13 -4
  172. package/src/jobs/prompt.ts +21 -24
  173. package/src/jobs/schedule.ts +14 -14
  174. package/src/jobs/slash-command.ts +20 -14
  175. package/src/jobs/status-line.ts +9 -21
  176. package/src/jobs/tool.ts +17 -11
  177. package/src/mod.ts +252 -97
  178. package/src/options.ts +22 -22
  179. package/src/records.ts +40 -23
  180. package/src/register.ts +34 -5
  181. package/src/runtime/announce.ts +76 -0
  182. package/src/runtime/before.ts +200 -0
  183. package/src/runtime/bound.ts +12 -0
  184. package/src/runtime/claude.ts +7 -2
  185. package/src/runtime/conversations.ts +579 -0
  186. package/src/runtime/deadline.ts +2 -2
  187. package/src/runtime/dependencies.ts +13 -1
  188. package/src/runtime/engine.ts +23 -0
  189. package/src/runtime/grants.ts +69 -29
  190. package/src/runtime/hooks.ts +39 -14
  191. package/src/runtime/installer.ts +16 -14
  192. package/src/runtime/lifecycle.ts +413 -201
  193. package/src/runtime/metadata.ts +37 -14
  194. package/src/runtime/options.ts +19 -4
  195. package/src/runtime/programs.ts +40 -1
  196. package/src/runtime/router.ts +11 -4
  197. package/src/runtime/sessions.ts +447 -0
  198. package/src/runtime/state.ts +50 -14
  199. package/src/runtime/toaster.ts +334 -0
  200. package/src/runtime/ui.ts +65 -143
  201. package/src/session/context/context.ts +14 -0
  202. package/src/session/context/turn/message/document.ts +8 -0
  203. package/src/session/context/turn/message/message.ts +203 -0
  204. package/src/session/context/turn/message/note.ts +9 -0
  205. package/src/session/context/turn/message/notice.ts +7 -0
  206. package/src/session/context/turn/message/prompt.ts +9 -0
  207. package/src/session/context/turn/message/reply.ts +7 -0
  208. package/src/session/context/turn/message/sender.ts +38 -0
  209. package/src/session/context/turn/message/skill.ts +8 -0
  210. package/src/session/context/turn/message/tool-call.ts +10 -0
  211. package/src/session/context/turn/message/tool-result.ts +9 -0
  212. package/src/session/context/turn/turn.ts +15 -0
  213. package/src/session/conversation.ts +194 -0
  214. package/src/session/goal/goal.ts +10 -0
  215. package/src/session/goal/task.ts +19 -0
  216. package/src/session/session.ts +141 -0
  217. package/src/testing/fake-claude.ts +25 -17
  218. package/src/testing/fake-elements.ts +3 -2
  219. package/src/testing/fake-files.ts +1 -1
  220. package/src/testing/fake-sessions.ts +277 -0
  221. package/src/testing.ts +138 -35
  222. package/src/ui/components.ts +3 -3
  223. package/src/ui/elements.ts +22 -8
  224. package/src/ui/markdown.ts +22 -26
  225. package/src/ui/places.ts +49 -0
  226. package/src/utils/call-effects.ts +5 -5
  227. package/src/utils/changelog.ts +32 -0
  228. package/src/utils/disposable.ts +3 -0
  229. package/src/utils/matcher.ts +37 -0
  230. package/src/utils/metadata.ts +17 -15
  231. package/src/utils/parse-shell.ts +9 -8
  232. package/src/utils/paths.ts +21 -4
  233. package/src/utils/text.ts +4 -0
  234. package/testing/fake-claude.d.ts +5 -1
  235. package/testing/fake-claude.d.ts.map +1 -1
  236. package/testing/fake-claude.js +21 -12
  237. package/testing/fake-elements.js +4 -2
  238. package/testing/fake-files.js +1 -1
  239. package/testing/fake-sessions.d.ts +81 -0
  240. package/testing/fake-sessions.d.ts.map +1 -0
  241. package/testing/fake-sessions.js +249 -0
  242. package/testing.d.ts +34 -13
  243. package/testing.d.ts.map +1 -1
  244. package/testing.js +115 -23
  245. package/ui/components.d.ts +2 -3
  246. package/ui/components.d.ts.map +1 -1
  247. package/ui/components.js +3 -3
  248. package/ui/elements.d.ts.map +1 -1
  249. package/ui/elements.js +19 -9
  250. package/ui/markdown.d.ts +35 -33
  251. package/ui/markdown.d.ts.map +1 -1
  252. package/ui/markdown.js +14 -16
  253. package/ui/places.d.ts +43 -0
  254. package/ui/places.d.ts.map +1 -0
  255. package/ui/places.js +31 -0
  256. package/utils/call-effects.d.ts +2 -1
  257. package/utils/call-effects.d.ts.map +1 -1
  258. package/utils/call-effects.js +3 -3
  259. package/utils/changelog.d.ts +9 -0
  260. package/utils/changelog.d.ts.map +1 -0
  261. package/utils/changelog.js +29 -0
  262. package/utils/disposable.d.ts +4 -0
  263. package/utils/disposable.d.ts.map +1 -0
  264. package/utils/disposable.js +1 -0
  265. package/utils/matcher.d.ts +13 -0
  266. package/utils/matcher.d.ts.map +1 -0
  267. package/utils/matcher.js +32 -0
  268. package/utils/metadata.d.ts +3 -3
  269. package/utils/metadata.js +18 -14
  270. package/utils/parse-shell.js +9 -7
  271. package/utils/paths.d.ts +4 -1
  272. package/utils/paths.d.ts.map +1 -1
  273. package/utils/paths.js +19 -4
  274. package/utils/text.d.ts +1 -0
  275. package/utils/text.d.ts.map +1 -1
  276. package/utils/text.js +3 -0
  277. package/src/ui/slots.ts +0 -36
  278. package/ui/slots.d.ts +0 -208
  279. package/ui/slots.d.ts.map +0 -1
  280. 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 shown: Shown = { toasts: [], notes: [], prompts: [], logs: [], debug: [], statuses: [], openPanes: new Set(), commands: [], tools: [], schedules: [] }
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}` } as unknown as Awaited<ReturnType<Claude['session']['append']>>
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: true, isFocused: id === focusedPane, isPlaced: true })),
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 ?? (async () => false)),
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: {}, cost: { usd: 0 } }) as unknown as Awaited<ReturnType<Claude['session']['usage']>>,
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 minWidth = insideOf(node).props['minWidth']
120
- return typeof minWidth === 'number' ? minWidth : 0
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(mod.cwd).')
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
+ }