@cmodjs/core 0.3.0 → 1.0.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 (271) hide show
  1. package/docs/commands.md +50 -10
  2. package/docs/dependencies.md +12 -12
  3. package/docs/hooks.md +39 -27
  4. package/docs/index.md +29 -25
  5. package/docs/install-steps.md +9 -7
  6. package/docs/jobs.md +59 -111
  7. package/docs/mod.md +200 -174
  8. package/docs/options.md +2 -2
  9. package/docs/permissions.md +27 -16
  10. package/docs/session.md +248 -0
  11. package/docs/state.md +11 -7
  12. package/docs/testing.md +65 -22
  13. package/docs/ui.md +108 -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 +8 -3
  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 +154 -75
  48. package/mod.d.ts.map +1 -1
  49. package/mod.js +94 -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 +10 -8
  54. package/records.d.ts.map +1 -1
  55. package/records.js +28 -17
  56. package/register.d.ts.map +1 -1
  57. package/register.js +17 -5
  58. package/runtime/announce.d.ts +18 -0
  59. package/runtime/announce.d.ts.map +1 -0
  60. package/runtime/announce.js +66 -0
  61. package/runtime/bound.d.ts +4 -0
  62. package/runtime/bound.d.ts.map +1 -0
  63. package/runtime/bound.js +9 -0
  64. package/runtime/claude.d.ts +3 -2
  65. package/runtime/claude.d.ts.map +1 -1
  66. package/runtime/conversations.d.ts +16 -0
  67. package/runtime/conversations.d.ts.map +1 -0
  68. package/runtime/conversations.js +291 -0
  69. package/runtime/deadline.d.ts +0 -1
  70. package/runtime/deadline.d.ts.map +1 -1
  71. package/runtime/deadline.js +1 -1
  72. package/runtime/dependencies.d.ts +2 -0
  73. package/runtime/dependencies.d.ts.map +1 -1
  74. package/runtime/dependencies.js +9 -0
  75. package/runtime/engine.d.ts +17 -0
  76. package/runtime/engine.d.ts.map +1 -0
  77. package/runtime/engine.js +10 -0
  78. package/runtime/grants.d.ts +4 -0
  79. package/runtime/grants.d.ts.map +1 -1
  80. package/runtime/grants.js +63 -26
  81. package/runtime/hooks.d.ts +5 -4
  82. package/runtime/hooks.d.ts.map +1 -1
  83. package/runtime/hooks.js +10 -5
  84. package/runtime/installer.d.ts.map +1 -1
  85. package/runtime/installer.js +11 -9
  86. package/runtime/lifecycle.d.ts +3 -1
  87. package/runtime/lifecycle.d.ts.map +1 -1
  88. package/runtime/lifecycle.js +316 -185
  89. package/runtime/metadata.d.ts +3 -1
  90. package/runtime/metadata.d.ts.map +1 -1
  91. package/runtime/metadata.js +21 -11
  92. package/runtime/options.d.ts +3 -1
  93. package/runtime/options.d.ts.map +1 -1
  94. package/runtime/options.js +4 -4
  95. package/runtime/programs.d.ts +8 -0
  96. package/runtime/programs.d.ts.map +1 -1
  97. package/runtime/programs.js +39 -1
  98. package/runtime/router.d.ts +2 -1
  99. package/runtime/router.d.ts.map +1 -1
  100. package/runtime/router.js +11 -3
  101. package/runtime/sessions.d.ts +36 -0
  102. package/runtime/sessions.d.ts.map +1 -0
  103. package/runtime/sessions.js +235 -0
  104. package/runtime/state.d.ts +6 -2
  105. package/runtime/state.d.ts.map +1 -1
  106. package/runtime/state.js +46 -13
  107. package/runtime/toaster.d.ts +31 -0
  108. package/runtime/toaster.d.ts.map +1 -0
  109. package/runtime/toaster.js +176 -0
  110. package/runtime/ui.d.ts +6 -17
  111. package/runtime/ui.d.ts.map +1 -1
  112. package/runtime/ui.js +36 -112
  113. package/session/context/context.d.ts +10 -0
  114. package/session/context/context.d.ts.map +1 -0
  115. package/session/context/context.js +2 -0
  116. package/session/context/turn/message/document.d.ts +7 -0
  117. package/session/context/turn/message/document.d.ts.map +1 -0
  118. package/session/context/turn/message/document.js +1 -0
  119. package/session/context/turn/message/message.d.ts +54 -0
  120. package/session/context/turn/message/message.d.ts.map +1 -0
  121. package/session/context/turn/message/message.js +137 -0
  122. package/session/context/turn/message/note.d.ts +8 -0
  123. package/session/context/turn/message/note.d.ts.map +1 -0
  124. package/session/context/turn/message/note.js +1 -0
  125. package/session/context/turn/message/notice.d.ts +6 -0
  126. package/session/context/turn/message/notice.d.ts.map +1 -0
  127. package/session/context/turn/message/notice.js +1 -0
  128. package/session/context/turn/message/prompt.d.ts +8 -0
  129. package/session/context/turn/message/prompt.d.ts.map +1 -0
  130. package/session/context/turn/message/prompt.js +1 -0
  131. package/session/context/turn/message/reply.d.ts +6 -0
  132. package/session/context/turn/message/reply.d.ts.map +1 -0
  133. package/session/context/turn/message/reply.js +1 -0
  134. package/session/context/turn/message/sender.d.ts +14 -0
  135. package/session/context/turn/message/sender.d.ts.map +1 -0
  136. package/session/context/turn/message/sender.js +30 -0
  137. package/session/context/turn/message/skill.d.ts +7 -0
  138. package/session/context/turn/message/skill.d.ts.map +1 -0
  139. package/session/context/turn/message/skill.js +1 -0
  140. package/session/context/turn/message/tool-call.d.ts +9 -0
  141. package/session/context/turn/message/tool-call.d.ts.map +1 -0
  142. package/session/context/turn/message/tool-call.js +1 -0
  143. package/session/context/turn/message/tool-result.d.ts +8 -0
  144. package/session/context/turn/message/tool-result.d.ts.map +1 -0
  145. package/session/context/turn/message/tool-result.js +1 -0
  146. package/session/context/turn/turn.d.ts +12 -0
  147. package/session/context/turn/turn.d.ts.map +1 -0
  148. package/session/context/turn/turn.js +2 -0
  149. package/session/conversation.d.ts +64 -0
  150. package/session/conversation.d.ts.map +1 -0
  151. package/session/conversation.js +123 -0
  152. package/session/goal/goal.d.ts +10 -0
  153. package/session/goal/goal.d.ts.map +1 -0
  154. package/session/goal/goal.js +1 -0
  155. package/session/goal/task.d.ts +16 -0
  156. package/session/goal/task.d.ts.map +1 -0
  157. package/session/goal/task.js +3 -0
  158. package/session/session.d.ts +99 -0
  159. package/session/session.d.ts.map +1 -0
  160. package/session/session.js +1 -0
  161. package/src/jobs/check.ts +7 -4
  162. package/src/jobs/context.ts +24 -82
  163. package/src/jobs/permissions/decide-permission.ts +10 -8
  164. package/src/jobs/permissions/find-project-scope.ts +8 -14
  165. package/src/jobs/permissions/match-target.ts +3 -3
  166. package/src/jobs/permissions.ts +8 -6
  167. package/src/jobs/program.ts +7 -3
  168. package/src/jobs/prompt.ts +21 -24
  169. package/src/jobs/schedule.ts +14 -14
  170. package/src/jobs/slash-command.ts +20 -14
  171. package/src/jobs/status-line.ts +9 -21
  172. package/src/jobs/tool.ts +17 -11
  173. package/src/mod.ts +209 -97
  174. package/src/options.ts +22 -22
  175. package/src/records.ts +35 -23
  176. package/src/register.ts +17 -5
  177. package/src/runtime/announce.ts +74 -0
  178. package/src/runtime/bound.ts +12 -0
  179. package/src/runtime/claude.ts +4 -2
  180. package/src/runtime/conversations.ts +302 -0
  181. package/src/runtime/deadline.ts +2 -2
  182. package/src/runtime/dependencies.ts +12 -0
  183. package/src/runtime/engine.ts +22 -0
  184. package/src/runtime/grants.ts +66 -24
  185. package/src/runtime/hooks.ts +19 -10
  186. package/src/runtime/installer.ts +10 -9
  187. package/src/runtime/lifecycle.ts +312 -180
  188. package/src/runtime/metadata.ts +33 -11
  189. package/src/runtime/options.ts +19 -4
  190. package/src/runtime/programs.ts +40 -1
  191. package/src/runtime/router.ts +11 -4
  192. package/src/runtime/sessions.ts +242 -0
  193. package/src/runtime/state.ts +50 -14
  194. package/src/runtime/toaster.ts +218 -0
  195. package/src/runtime/ui.ts +42 -139
  196. package/src/session/context/context.ts +12 -0
  197. package/src/session/context/turn/message/document.ts +8 -0
  198. package/src/session/context/turn/message/message.ts +165 -0
  199. package/src/session/context/turn/message/note.ts +9 -0
  200. package/src/session/context/turn/message/notice.ts +7 -0
  201. package/src/session/context/turn/message/prompt.ts +9 -0
  202. package/src/session/context/turn/message/reply.ts +7 -0
  203. package/src/session/context/turn/message/sender.ts +38 -0
  204. package/src/session/context/turn/message/skill.ts +8 -0
  205. package/src/session/context/turn/message/tool-call.ts +10 -0
  206. package/src/session/context/turn/message/tool-result.ts +9 -0
  207. package/src/session/context/turn/turn.ts +14 -0
  208. package/src/session/conversation.ts +169 -0
  209. package/src/session/goal/goal.ts +9 -0
  210. package/src/session/goal/task.ts +19 -0
  211. package/src/session/session.ts +94 -0
  212. package/src/testing/fake-claude.ts +22 -16
  213. package/src/testing/fake-elements.ts +3 -2
  214. package/src/testing/fake-files.ts +1 -1
  215. package/src/testing/fake-sessions.ts +203 -0
  216. package/src/testing.ts +112 -33
  217. package/src/ui/components.ts +3 -3
  218. package/src/ui/elements.ts +22 -8
  219. package/src/ui/markdown.ts +22 -26
  220. package/src/ui/places.ts +49 -0
  221. package/src/utils/call-effects.ts +3 -3
  222. package/src/utils/changelog.ts +32 -0
  223. package/src/utils/matcher.ts +37 -0
  224. package/src/utils/metadata.ts +17 -15
  225. package/src/utils/parse-shell.ts +9 -8
  226. package/src/utils/paths.ts +15 -1
  227. package/src/utils/text.ts +4 -0
  228. package/testing/fake-claude.d.ts +4 -1
  229. package/testing/fake-claude.d.ts.map +1 -1
  230. package/testing/fake-claude.js +18 -11
  231. package/testing/fake-elements.js +4 -2
  232. package/testing/fake-files.js +1 -1
  233. package/testing/fake-sessions.d.ts +74 -0
  234. package/testing/fake-sessions.d.ts.map +1 -0
  235. package/testing/fake-sessions.js +170 -0
  236. package/testing.d.ts +33 -13
  237. package/testing.d.ts.map +1 -1
  238. package/testing.js +92 -21
  239. package/ui/components.d.ts +2 -3
  240. package/ui/components.d.ts.map +1 -1
  241. package/ui/components.js +3 -3
  242. package/ui/elements.d.ts.map +1 -1
  243. package/ui/elements.js +19 -9
  244. package/ui/markdown.d.ts +35 -33
  245. package/ui/markdown.d.ts.map +1 -1
  246. package/ui/markdown.js +14 -16
  247. package/ui/places.d.ts +43 -0
  248. package/ui/places.d.ts.map +1 -0
  249. package/ui/places.js +31 -0
  250. package/utils/call-effects.d.ts +1 -1
  251. package/utils/call-effects.d.ts.map +1 -1
  252. package/utils/call-effects.js +2 -2
  253. package/utils/changelog.d.ts +9 -0
  254. package/utils/changelog.d.ts.map +1 -0
  255. package/utils/changelog.js +29 -0
  256. package/utils/matcher.d.ts +13 -0
  257. package/utils/matcher.d.ts.map +1 -0
  258. package/utils/matcher.js +32 -0
  259. package/utils/metadata.d.ts +3 -3
  260. package/utils/metadata.js +18 -14
  261. package/utils/parse-shell.js +9 -7
  262. package/utils/paths.d.ts +2 -0
  263. package/utils/paths.d.ts.map +1 -1
  264. package/utils/paths.js +13 -1
  265. package/utils/text.d.ts +1 -0
  266. package/utils/text.d.ts.map +1 -1
  267. package/utils/text.js +3 -0
  268. package/src/ui/slots.ts +0 -36
  269. package/ui/slots.d.ts +0 -208
  270. package/ui/slots.d.ts.map +0 -1
  271. package/ui/slots.js +0 -19
@@ -0,0 +1,49 @@
1
+ import type { Frozen, RenderComponent, RenderElement, RenderPropsOf } from 'claude-code'
2
+ import type { BlockProps, MarkdownPlace } from './markdown.js'
3
+
4
+ export const ToolGroup = 'toolGroup'
5
+ export const ToolProgress = 'toolProgress'
6
+ export const CommandOutput = 'commandOutput'
7
+ export const AskUserQuestion = 'askUserQuestion'
8
+ export const Spinner = 'spinner'
9
+ export const TurnDuration = 'turnDuration'
10
+ export const SessionMode = 'sessionMode'
11
+ export const PromptHint = 'promptHint'
12
+ export const AbovePrompt = 'abovePrompt'
13
+
14
+ const components = {
15
+ prompt: 'UserMessage',
16
+ reply: 'AssistantMessage',
17
+ toolCall: 'ToolUse',
18
+ toolResult: 'ToolResult',
19
+ notice: 'InfoNotice',
20
+ [ToolGroup]: 'ToolGroup',
21
+ [ToolProgress]: 'ToolProgress',
22
+ [CommandOutput]: 'CommandOutput',
23
+ [AskUserQuestion]: 'AskUserQuestion',
24
+ [Spinner]: 'Spinner',
25
+ [TurnDuration]: 'TurnDuration',
26
+ [SessionMode]: 'SessionMode',
27
+ [PromptHint]: 'PromptHint',
28
+ [AbovePrompt]: 'AbovePrompt',
29
+ } as const satisfies Readonly<Record<string, Exclude<RenderComponent, 'Pane'>>>
30
+
31
+ export type ComponentPlace = keyof typeof components
32
+
33
+ export type Place = ComponentPlace | MarkdownPlace
34
+
35
+ type ComponentProps = { readonly [P in ComponentPlace]: RenderPropsOf[(typeof components)[P]] }
36
+
37
+ type PropsOf<P extends Place> = P extends ComponentPlace ? ComponentProps[P] : P extends MarkdownPlace<infer Type> ? BlockProps<Type> : never
38
+
39
+ type DefaultOf<P extends Place> = P extends ComponentPlace ? Partial<Omit<ComponentProps[P], 'onScreen'>> : { readonly source?: string }
40
+
41
+ export type PlaceProps<P extends Place> = Frozen<PropsOf<P>> & { readonly Default: (props: Frozen<DefaultOf<P>>) => RenderElement }
42
+
43
+ export function componentOf(place: Place): Exclude<RenderComponent, 'Pane'> | undefined {
44
+ return typeof place === 'string' && Object.hasOwn(components, place) ? components[place] : undefined
45
+ }
46
+
47
+ export function nameOf(place: Place): string {
48
+ return typeof place === 'string' ? `${place.charAt(0).toUpperCase()}${place.slice(1)}` : place.name
49
+ }
@@ -1,12 +1,12 @@
1
1
  import { parseShell, type ShellCommand } from './parse-shell.js'
2
- import { expandHome, type FileSystem } from './paths.js'
2
+ import { expandHome, ifMissing, type FileSystem } from './paths.js'
3
3
  import { resolve } from '../vendor.js'
4
4
 
5
5
  export type CallBase = { tool: string; input: Readonly<Record<string, unknown>>; agentId?: string; agentType?: string }
6
6
  export type CommandCall = CallBase & { commands: [string, ...string[]][]; isFullyParsed: boolean }
7
7
  export type FileCall = CallBase & { path: string; content?: string; previousContent?: string }
8
8
  export type FetchCall = CallBase & { url: string }
9
- export type ToolCall = CommandCall | FileCall | FetchCall | CallBase
9
+ export type Call = CommandCall | FileCall | FetchCall | CallBase
10
10
 
11
11
  export type Contents = { content?: string; previousContent?: string }
12
12
  export type FileAccess = { path: string; contents: (() => Promise<Contents>) | undefined }
@@ -79,7 +79,7 @@ async function editedContents(use: CallBase, path: string, fs: FileSystem): Prom
79
79
  }
80
80
 
81
81
  async function previousContentOf(path: string, fs: FileSystem): Promise<string | undefined> {
82
- const stat = await fs.stat(path).catch(() => undefined)
82
+ const stat = await fs.stat(path).catch(ifMissing(undefined))
83
83
  return stat?.kind === 'file' ? fs.read(path) : undefined
84
84
  }
85
85
 
@@ -0,0 +1,32 @@
1
+ export type ChangelogSection = { readonly version: string; readonly date?: string; readonly text: string }
2
+
3
+ export const unreleased = 'Unreleased'
4
+
5
+ const heading = /^##\s+\[?([^\]\s]+)\]?(?:\s+-\s+(\S+))?\s*$/
6
+
7
+ const linkReference = /^\[[^\]]+\]:\s/
8
+
9
+ export function changelogSections(markdown: string): ChangelogSection[] {
10
+ const sections: { readonly version: string; readonly date: string | undefined; readonly lines: string[] }[] = []
11
+ for (const line of markdown.split('\n')) {
12
+ const match = heading.exec(line)
13
+ if (match !== null) sections.push({ version: match[1] as string, date: match[2], lines: [] })
14
+ else if (!linkReference.test(line)) sections.at(-1)?.lines.push(line)
15
+ }
16
+ return sections.map(({ version, date, lines }) => ({ version, ...(date === undefined ? {} : { date }), text: lines.join('\n').trim() }))
17
+ }
18
+
19
+ export function changeLines(text: string): string[] {
20
+ let group: string | undefined
21
+ return text.split('\n').flatMap((line) => {
22
+ const trimmed = line.trim()
23
+ const subheading = /^###\s+(.+)$/.exec(trimmed)?.[1]
24
+ if (subheading !== undefined) {
25
+ group = subheading
26
+ return []
27
+ }
28
+ const item = /^[-*]\s+(.+)$/.exec(trimmed)?.[1] ?? trimmed
29
+ if (item === '') return []
30
+ return [group === undefined ? item : `${group}: ${item}`]
31
+ })
32
+ }
@@ -0,0 +1,37 @@
1
+ export type Matcher<Value> = Value extends string
2
+ ? string | RegExp
3
+ : Value extends number | boolean | null | undefined
4
+ ? Value
5
+ : Value extends readonly (infer Item)[]
6
+ ? Matcher<Item>
7
+ : Value extends (...args: never[]) => unknown
8
+ ? never
9
+ : Value extends object
10
+ ? { readonly [Key in keyof Value]?: Matcher<Value[Key]> }
11
+ : Value
12
+
13
+ export type SentMatcher = string | number | boolean | null | { readonly regexp: string; readonly flags: string } | { readonly [key: string]: SentMatcher } | readonly SentMatcher[]
14
+
15
+ export function matches(value: unknown, matcher: unknown): boolean {
16
+ if (matcher instanceof RegExp) return matcher.test(String(value))
17
+ if (Array.isArray(matcher)) return matcher.some((each) => matches(value, each))
18
+ if (typeof matcher !== 'object' || matcher === null) return value === matcher
19
+ if (Array.isArray(value)) return value.some((each) => matches(each, matcher))
20
+ if (typeof value !== 'object' || value === null) return false
21
+ return Object.entries(matcher).every(([key, wanted]) => wanted === undefined || matches((value as Record<string, unknown>)[key], wanted))
22
+ }
23
+
24
+ export function sentMatcher(matcher: unknown): SentMatcher {
25
+ if (matcher instanceof RegExp) return { regexp: matcher.source, flags: matcher.flags }
26
+ if (Array.isArray(matcher)) return matcher.map(sentMatcher)
27
+ if (typeof matcher !== 'object' || matcher === null) return matcher as SentMatcher
28
+ return Object.fromEntries(Object.entries(matcher).filter(([, value]) => value !== undefined).map(([key, value]) => [key, sentMatcher(value)]))
29
+ }
30
+
31
+ export function receivedMatcher(sent: SentMatcher): unknown {
32
+ if (Array.isArray(sent)) return sent.map(receivedMatcher)
33
+ if (typeof sent !== 'object' || sent === null) return sent
34
+ const fields = sent as Readonly<Record<string, unknown>>
35
+ if (typeof fields['regexp'] === 'string' && typeof fields['flags'] === 'string' && Object.keys(fields).length === 2) return new RegExp(fields['regexp'], fields['flags'])
36
+ return Object.fromEntries(Object.entries(fields).map(([key, value]) => [key, receivedMatcher(value as SentMatcher)]))
37
+ }
@@ -1,4 +1,4 @@
1
- export type MetadataFormat = { readonly kind: 'frontmatter' } | { readonly kind: 'comment'; readonly prefix: string } | { readonly kind: 'sidecar' }
1
+ export type MetadataFormat = { readonly type: 'frontmatter' } | { readonly type: 'comment'; readonly prefix: string } | { readonly type: 'sidecar' }
2
2
 
3
3
  export type ReadMetadata = {
4
4
  readonly metadata: Readonly<Record<string, string>>
@@ -26,12 +26,12 @@ export function formatOf(path: string, firstLine: string | undefined): MetadataF
26
26
  const name = path.slice(path.lastIndexOf('/') + 1)
27
27
  const dot = name.lastIndexOf('.')
28
28
  const extension = dot <= 0 ? '' : name.slice(dot + 1).toLowerCase()
29
- if (markdown.includes(extension)) return { kind: 'frontmatter' }
30
- if (hashComments.includes(extension) || hashCommentNames.includes(name)) return { kind: 'comment', prefix: '#' }
31
- if (slashComments.includes(extension)) return { kind: 'comment', prefix: '//' }
32
- if (dashComments.includes(extension)) return { kind: 'comment', prefix: '--' }
33
- if (extension === '' && firstLine?.startsWith('#!') === true) return { kind: 'comment', prefix: '#' }
34
- return { kind: 'sidecar' }
29
+ if (markdown.includes(extension)) return { type: 'frontmatter' }
30
+ if (hashComments.includes(extension) || hashCommentNames.includes(name)) return { type: 'comment', prefix: '#' }
31
+ if (slashComments.includes(extension)) return { type: 'comment', prefix: '//' }
32
+ if (dashComments.includes(extension)) return { type: 'comment', prefix: '--' }
33
+ if (extension === '' && firstLine?.startsWith('#!') === true) return { type: 'comment', prefix: '#' }
34
+ return { type: 'sidecar' }
35
35
  }
36
36
 
37
37
  type Region = { readonly start: number; readonly end: number; readonly lines: readonly string[] }
@@ -42,21 +42,21 @@ type Block = { readonly start: number; readonly end: number; readonly entries: r
42
42
 
43
43
  export function readMetadata(text: string, format: MetadataFormat): ReadMetadata {
44
44
  const lines = text.split('\n')
45
- if (format.kind === 'comment') {
45
+ if (format.type === 'comment') {
46
46
  const region = commentRegion(lines, format.prefix)
47
47
  return { metadata: region === undefined ? {} : asRecord(mapEntries(region.lines, region.start + 2, 0)) }
48
48
  }
49
- const region = format.kind === 'frontmatter' ? frontmatterRegion(lines) : { start: -1, end: lines.length, lines }
49
+ const region = format.type === 'frontmatter' ? frontmatterRegion(lines) : { start: -1, end: lines.length, lines }
50
50
  if (region === undefined) return { metadata: {} }
51
51
  const block = metadataBlock(region)
52
- const name = format.kind === 'frontmatter' ? topLevelScalar(region, 'name') : undefined
52
+ const name = format.type === 'frontmatter' ? topLevelScalar(region, 'name') : undefined
53
53
  return { metadata: block === undefined ? {} : asRecord(block.entries), ...(name === undefined ? {} : { name }) }
54
54
  }
55
55
 
56
56
  export function writeMetadata(text: string, format: MetadataFormat, metadata: Readonly<Record<string, string>>): string {
57
57
  const lines = text.split('\n')
58
58
  const entries = Object.entries(metadata)
59
- if (format.kind === 'comment') {
59
+ if (format.type === 'comment') {
60
60
  const { prefix } = format
61
61
  const block = entries.length === 0 ? [] : [`${prefix} /// metadata`, ...entries.map(([key, value]) => `${prefix} ${scalar(key)}: ${scalar(value)}`), `${prefix} ///`]
62
62
  const region = commentRegion(lines, prefix)
@@ -66,7 +66,7 @@ export function writeMetadata(text: string, format: MetadataFormat, metadata: Re
66
66
  return [...lines.slice(0, at), ...block, ...lines.slice(at)].join('\n')
67
67
  }
68
68
  const map = entries.length === 0 ? [] : ['metadata:', ...entries.map(([key, value]) => ` ${scalar(key)}: ${scalar(value)}`)]
69
- if (format.kind === 'sidecar') {
69
+ if (format.type === 'sidecar') {
70
70
  const block = metadataBlock({ start: -1, end: lines.length, lines })
71
71
  const rest = block === undefined ? lines : [...lines.slice(0, block.start), ...lines.slice(block.end)]
72
72
  const kept = rest.join('\n').trim()
@@ -206,7 +206,8 @@ function unquoted(text: string, lineNumber: number): string {
206
206
  if (!text.endsWith('"') || text.length < 2) throw new MetadataError(lineNumber, `${text} opens a double quote that never closes.`)
207
207
  try {
208
208
  return JSON.parse(text) as string
209
- } catch {
209
+ } catch (error) {
210
+ if (!(error instanceof SyntaxError)) throw error
210
211
  throw new MetadataError(lineNumber, `${text} is not a double-quoted text. Escape a " inside it as \\".`)
211
212
  }
212
213
  }
@@ -224,8 +225,9 @@ function topLevelScalar(region: Region, key: string): string | undefined {
224
225
  if (text === '') return undefined
225
226
  try {
226
227
  return unquoted(text, region.start + 2 + at)
227
- } catch {
228
- return undefined
228
+ } catch (error) {
229
+ if (error instanceof MetadataError) return undefined
230
+ throw error
229
231
  }
230
232
  }
231
233
 
@@ -18,7 +18,7 @@ type Scan = { text: string; markers: Marker[]; end: number }
18
18
  type Redirect = { operator: string; target: Word }
19
19
  type Pipe = { writer: number | undefined }
20
20
  type Command = { words: Word[]; redirects: Redirect[]; pipe: Pipe | undefined }
21
- type Stdin = { kind: 'text'; text: string } | { kind: 'pipe' }
21
+ type Stdin = { type: 'text'; text: string } | { type: 'pipe' }
22
22
  type Access = 'reads' | 'writes'
23
23
  type OptionSpec = { valued?: string; optional?: string; longValued?: readonly string[] }
24
24
  type Option = { name: string; value: Word | undefined }
@@ -211,7 +211,8 @@ function parseLine(line: string, folder: string, result: ParsedShell): void {
211
211
  try {
212
212
  const { text, markers } = scan(line, 0, false)
213
213
  walk(parse(text, (name) => (name === '' ? undefined : `$${name}`)), markers, folder, result)
214
- } catch {
214
+ } catch (error) {
215
+ if (!(error instanceof SyntaxError) && !(error instanceof Error && error.message.startsWith('Bad substitution: '))) throw error
215
216
  result.isFullyParsed = false
216
217
  }
217
218
  }
@@ -546,7 +547,7 @@ function finish(command: Command, operator: string, folder: string, result: Pars
546
547
  const [first, ...args] = words
547
548
  if (first === undefined || first.text === 'for' || first.text === 'select') return folder
548
549
  const here = command.redirects.findLast((redirect) => redirect.operator === '<<<')
549
- const stdin: Stdin | undefined = here !== undefined ? { kind: 'text', text: here.target.text } : command.pipe !== undefined ? { kind: 'pipe' } : undefined
550
+ const stdin: Stdin | undefined = here !== undefined ? { type: 'text', text: here.target.text } : command.pipe !== undefined ? { type: 'pipe' } : undefined
550
551
  const reader = result.commands.length
551
552
  const changed = addCommand(first, args, folder, stdin, result)
552
553
  const read = result.commands[reader]
@@ -629,7 +630,7 @@ function findStarts(args: Word[]): Word[] {
629
630
 
630
631
  function addWrapper(first: Word, program: string, args: Word[], folder: string, stdin: Stdin | undefined, result: ParsedShell): void {
631
632
  const wrapper = wrappers.get(program)!
632
- if (program === 'xargs' && stdin?.kind !== 'text') result.isFullyParsed = false
633
+ if (program === 'xargs' && stdin?.type !== 'text') result.isFullyParsed = false
633
634
  const { options, operands } = readArguments(args, wrapper, true)
634
635
  let start = args.length - operands.length
635
636
  if (options.some((option) => wrapper.stops?.includes(option.name) === true)) start = args.length
@@ -669,8 +670,8 @@ function addEffects(program: string, args: Word[], flags: string[], folder: stri
669
670
  else if (inlineFlags !== undefined) {
670
671
  if (program === 'perl' && readArguments(args, perl, false).options.some((option) => option.name === '-i')) addFileArguments(perl, args, folder, result)
671
672
  const script = args.find((word) => !word.text.startsWith('-') || word.text === '-')
672
- const readsCodeFromPipe = stdin?.kind === 'pipe' && (script === undefined || script.text === '-')
673
- if (flags.some((flag) => inlineFlags.includes(flag)) || stdin?.kind === 'text' || readsCodeFromPipe) result.isFullyParsed = false
673
+ const readsCodeFromPipe = stdin?.type === 'pipe' && (script === undefined || script.text === '-')
674
+ if (flags.some((flag) => inlineFlags.includes(flag)) || stdin?.type === 'text' || readsCodeFromPipe) result.isFullyParsed = false
674
675
  }
675
676
  }
676
677
 
@@ -701,8 +702,8 @@ function addFetches(fetcher: OptionSpec, args: Word[], result: ParsedShell): voi
701
702
  }
702
703
 
703
704
  function addStdinCode(stdin: Stdin | undefined, folder: string, result: ParsedShell): void {
704
- if (stdin?.kind === 'text') parseLine(stdin.text, folder, result)
705
- if (stdin?.kind === 'pipe') result.isFullyParsed = false
705
+ if (stdin?.type === 'text') parseLine(stdin.text, folder, result)
706
+ if (stdin?.type === 'pipe') result.isFullyParsed = false
706
707
  }
707
708
 
708
709
  function addCopy(program: CopyProgram, args: Word[], folder: string, result: ParsedShell): void {
@@ -1,4 +1,18 @@
1
1
  import { basename, dirname, resolve } from '../vendor.js'
2
+ import { messageOf } from './text.js'
3
+
4
+ const missingPath = /(?:^|: )(?:ENOENT|ENOTDIR)\b/
5
+
6
+ export function isMissing(error: unknown): boolean {
7
+ return missingPath.test(messageOf(error))
8
+ }
9
+
10
+ export function ifMissing<Value>(value: Value): (error: unknown) => Value {
11
+ return (error) => {
12
+ if (isMissing(error)) return value
13
+ throw error
14
+ }
15
+ }
2
16
 
3
17
  export type FileSystem = {
4
18
  read: (path: string) => Promise<string>
@@ -18,7 +32,7 @@ export function relativePath(base: string, path: string): string | undefined {
18
32
  }
19
33
 
20
34
  export async function realPathOf(path: string, fs: FileSystem): Promise<string | undefined> {
21
- const stat = await fs.stat(path).catch(() => undefined)
35
+ const stat = await fs.stat(path).catch(ifMissing(undefined))
22
36
  if (stat !== undefined || path === '/') return stat?.realPath
23
37
  const folder = await realPathOf(dirname(path), fs)
24
38
  return folder === undefined ? undefined : resolve(folder, basename(path))
package/src/utils/text.ts CHANGED
@@ -3,6 +3,10 @@ export function listed(items: readonly string[]): string {
3
3
  return `${items.slice(0, -1).join(', ')} and ${items.at(-1)}`
4
4
  }
5
5
 
6
+ export function firstLine(text: string): string {
7
+ return text.split('\n').find((line) => line.trim() !== '')?.trim() ?? ''
8
+ }
9
+
6
10
  export function messageOf(error: unknown): string {
7
11
  return error instanceof Error ? error.message : String(error)
8
12
  }
@@ -1,3 +1,4 @@
1
+ import type { EventResult } from 'claude-code';
1
2
  import type { Claude } from '../runtime/claude.js';
2
3
  export type TestCall = {
3
4
  readonly call: string;
@@ -28,10 +29,12 @@ export type Fakes = {
28
29
  ask?: Claude['ui']['ask'];
29
30
  open?: Claude['ui']['open'];
30
31
  scroll?: Claude['ui']['scroll'];
32
+ copy?: Claude['ui']['copy'];
31
33
  };
32
34
  session: {
33
35
  messages?: Claude['session']['messages'];
34
36
  append?: Claude['session']['append'];
37
+ send?: Claude['session']['send'];
35
38
  };
36
39
  prompt: {
37
40
  submit?: Claude['prompt']['submit'];
@@ -60,7 +63,6 @@ export type ShownSchedule = {
60
63
  readonly prompt: string;
61
64
  };
62
65
  export type Shown = {
63
- readonly toasts: string[];
64
66
  readonly notes: string[];
65
67
  readonly prompts: string[];
66
68
  readonly logs: string[];
@@ -79,6 +81,7 @@ export type FakeClaude = {
79
81
  readonly store: Map<string, unknown>;
80
82
  settle(): Promise<void>;
81
83
  };
84
+ export declare function calledAnswer(caller: string, answer: EventResult<'cmod.call'>): unknown;
82
85
  export declare function fakeClaude(plugin: {
83
86
  readonly name: string;
84
87
  readonly root: string;
@@ -1 +1 @@
1
- {"version":3,"file":"fake-claude.d.ts","sourceRoot":"","sources":["../src/testing/fake-claude.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,sBAAsB,CAAA;AAGlD,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,CAAA;KAAE,CAAA;IAC/F,OAAO,EAAE;QAAE,QAAQ,CAAC,EAAE,MAAM,CAAC,SAAS,CAAC,CAAC,UAAU,CAAC,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAC,SAAS,CAAC,CAAC,QAAQ,CAAC,CAAA;KAAE,CAAA;IAC3F,MAAM,EAAE;QAAE,MAAM,CAAC,EAAE,MAAM,CAAC,QAAQ,CAAC,CAAC,QAAQ,CAAC,CAAA;KAAE,CAAA;IAC/C,KAAK,EAAE;QAAE,QAAQ,CAAC,EAAE,MAAM,CAAC,OAAO,CAAC,CAAC,UAAU,CAAC,CAAA;KAAE,CAAA;IACjD,KAAK,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC,OAAO,CAAC,CAAC,MAAM,CAAC,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC,OAAO,CAAC,CAAC,OAAO,CAAC,CAAA;KAAE,CAAA;IAC3E,KAAK,EAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAC,OAAO,CAAC,CAAC,OAAO,CAAC,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC,OAAO,CAAC,CAAC,OAAO,CAAC,CAAA;KAAE,CAAA;IAC7E,IAAI,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC,CAAA;KAAE,CAAA;IACvC,IAAI,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC,CAAA;KAAE,CAAA;CACxC,CAAA;AAED,MAAM,MAAM,aAAa,GAAG;IAAE,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAA;AAEnG,MAAM,MAAM,KAAK,GAAG;IAClB,QAAQ,CAAC,MAAM,EAAE,MAAM,EAAE,CAAA;IACzB,QAAQ,CAAC,KAAK,EAAE,MAAM,EAAE,CAAA;IACxB,QAAQ,CAAC,OAAO,EAAE,MAAM,EAAE,CAAA;IAC1B,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,CAAA;IACvB,QAAQ,CAAC,KAAK,EAAE,MAAM,EAAE,CAAA;IACxB,QAAQ,CAAC,QAAQ,EAAE,CAAC,MAAM,GAAG,SAAS,CAAC,EAAE,CAAA;IACzC,QAAQ,CAAC,SAAS,EAAE,GAAG,CAAC,MAAM,CAAC,CAAA;IAC/B,QAAQ,CAAC,QAAQ,EAAE,MAAM,EAAE,CAAA;IAC3B,QAAQ,CAAC,KAAK,EAAE,MAAM,EAAE,CAAA;IACxB,QAAQ,CAAC,SAAS,EAAE,aAAa,EAAE,CAAA;CACpC,CAAA;AAED,MAAM,MAAM,UAAU,GAAG;IACvB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,KAAK,EAAE,QAAQ,EAAE,CAAA;IAC1B,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAA;IACrB,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAA;IACrB,QAAQ,CAAC,KAAK,EAAE,GAAG,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IACpC,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;CACxB,CAAA;AAED,wBAAgB,UAAU,CAAC,MAAM,EAAE;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAAG,UAAU,CAsK/F"}
1
+ {"version":3,"file":"fake-claude.d.ts","sourceRoot":"","sources":["../src/testing/fake-claude.ts"],"names":[],"mappings":"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"}
@@ -1,8 +1,16 @@
1
+ import { isObject } from '../records.js';
1
2
  import { elements } from './fake-elements.js';
3
+ import { fakeFiles } from './fake-files.js';
4
+ export function calledAnswer(caller, answer) {
5
+ if (isObject(answer) && typeof answer['deny'] === 'string')
6
+ throw new Error(`${caller}: $.cmod.call: ${answer['deny']}`);
7
+ return isObject(answer) ? answer['value'] : undefined;
8
+ }
2
9
  export function fakeClaude(plugin) {
3
10
  const calls = [];
4
11
  const fakes = { process: {}, fs: {}, http: {}, settings: {}, config: {}, ui: {}, session: {}, prompt: {}, model: {}, agent: {}, clock: {}, cmod: {}, tool: {} };
5
- const shown = { toasts: [], notes: [], prompts: [], logs: [], debug: [], statuses: [], openPanes: new Set(), commands: [], tools: [], schedules: [] };
12
+ const emptyDisk = fakeFiles({});
13
+ const shown = { notes: [], prompts: [], logs: [], debug: [], statuses: [], openPanes: new Set(), commands: [], tools: [], schedules: [] };
6
14
  const unplacedPanes = new Set();
7
15
  let focusedPane;
8
16
  const store = new Map();
@@ -49,9 +57,6 @@ export function fakeClaude(plugin) {
49
57
  const claude = {
50
58
  plugin,
51
59
  ui: {
52
- toast: (text) => {
53
- shown.toasts.push(text);
54
- },
55
60
  status: (text) => {
56
61
  shown.statuses.push(text);
57
62
  },
@@ -88,6 +93,7 @@ export function fakeClaude(plugin) {
88
93
  ...[...unplacedPanes].map((id) => ({ id, title: id, isShown: false, isFocused: false, isPlaced: false })),
89
94
  ],
90
95
  scroll: rejected('ui.scroll', () => fakes.ui.scroll ?? (async () => ({}))),
96
+ copy: rejected('ui.copy', () => fakes.ui.copy ?? (async () => ({ isCopied: true }))),
91
97
  resolve: () => elements,
92
98
  },
93
99
  process: {
@@ -95,14 +101,14 @@ export function fakeClaude(plugin) {
95
101
  spawn: faked('process.spawn', () => fakes.process.spawn),
96
102
  },
97
103
  fs: {
98
- read: rejected('fs.read', () => fakes.fs.read),
99
- write: rejected('fs.write', () => fakes.fs.write),
100
- list: rejected('fs.list', () => fakes.fs.list),
101
- exists: rejected('fs.exists', () => fakes.fs.exists ?? (async () => false)),
102
- stat: rejected('fs.stat', () => fakes.fs.stat),
104
+ read: rejected('fs.read', () => fakes.fs.read ?? emptyDisk.read),
105
+ write: rejected('fs.write', () => fakes.fs.write ?? emptyDisk.write),
106
+ list: rejected('fs.list', () => fakes.fs.list ?? emptyDisk.list),
107
+ exists: rejected('fs.exists', () => fakes.fs.exists ?? emptyDisk.exists),
108
+ stat: rejected('fs.stat', () => fakes.fs.stat ?? emptyDisk.stat),
103
109
  },
104
110
  http: { fetch: rejected('http.fetch', () => fakes.http.fetch) },
105
- settings: { read: rejected('settings.read', () => fakes.settings.read) },
111
+ settings: { read: rejected('settings.read', () => fakes.settings.read ?? (async () => ({}))) },
106
112
  config: {
107
113
  list: rejected('config.list', () => fakes.config.list ?? (async () => [])),
108
114
  },
@@ -130,10 +136,11 @@ export function fakeClaude(plugin) {
130
136
  root: async () => plugin.root,
131
137
  cwd: async () => plugin.root,
132
138
  model: async () => 'test-model',
133
- usage: async () => ({ context: { window: 200000 }, rateLimits: {}, cost: { usd: 0 } }),
139
+ usage: async () => ({ startedAt: Date.parse('2026-01-01T00:00:00.000Z'), context: { window: 200000 }, rateLimits: [], cost: { usd: 0 } }),
134
140
  surfaces: async () => ['terminal'],
135
141
  messages: rejected('session.messages', () => fakes.session.messages),
136
142
  append: rejected('session.append', () => fakes.session.append ?? appendNote),
143
+ send: rejected('session.send', () => fakes.session.send ?? (async () => ({ isDelivered: true }))),
137
144
  },
138
145
  prompt: { submit: rejected('prompt.submit', () => fakes.prompt.submit ?? submitPrompt) },
139
146
  model: { complete: rejected('model.complete', () => fakes.model.complete) },
@@ -119,8 +119,10 @@ function sideBySide(children, width, gap) {
119
119
  function minWidthOf(node) {
120
120
  if (typeof node === 'string')
121
121
  return 0;
122
- const minWidth = insideOf(node).props['minWidth'];
123
- return typeof minWidth === 'number' ? minWidth : 0;
122
+ const { props } = insideOf(node);
123
+ if (props['flexShrink'] === 0)
124
+ return Number.POSITIVE_INFINITY;
125
+ return typeof props['minWidth'] === 'number' ? props['minWidth'] : 0;
124
126
  }
125
127
  function blankRows(count) {
126
128
  return Array.from({ length: count }, () => '');
@@ -37,7 +37,7 @@ export function fakeFiles(files, links = {}) {
37
37
  },
38
38
  list: async (path) => {
39
39
  if (path === undefined)
40
- 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).');
40
+ 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).');
41
41
  const folder = realPathOf(path);
42
42
  if (kindOf(folder) !== 'dir')
43
43
  throw new Error(`ENOENT: no such directory, list '${path}'`);
@@ -0,0 +1,74 @@
1
+ import { Document } from '../session/context/turn/message/document.js';
2
+ import { Message } from '../session/context/turn/message/message.js';
3
+ import { Note } from '../session/context/turn/message/note.js';
4
+ import { Notice } from '../session/context/turn/message/notice.js';
5
+ import { Prompt } from '../session/context/turn/message/prompt.js';
6
+ import { Reply } from '../session/context/turn/message/reply.js';
7
+ import { type Sender } from '../session/context/turn/message/sender.js';
8
+ import { Skill } from '../session/context/turn/message/skill.js';
9
+ import { ToolCall } from '../session/context/turn/message/tool-call.js';
10
+ import { ToolResult } from '../session/context/turn/message/tool-result.js';
11
+ import { type SessionChange, type SessionData } from '../session/conversation.js';
12
+ export type MessageDraft = {
13
+ readonly type: typeof Prompt;
14
+ readonly text: string;
15
+ readonly from?: Sender;
16
+ } | {
17
+ readonly type: typeof Note;
18
+ readonly text: string;
19
+ readonly from?: Sender;
20
+ } | {
21
+ readonly type: typeof Reply;
22
+ readonly text: string;
23
+ } | {
24
+ readonly type: typeof ToolCall;
25
+ readonly name: string;
26
+ readonly input?: Readonly<Record<string, unknown>>;
27
+ readonly callId?: string;
28
+ } | {
29
+ readonly type: typeof ToolResult;
30
+ readonly callId: string;
31
+ readonly text: string;
32
+ readonly isError?: boolean;
33
+ } | {
34
+ readonly type: typeof Skill;
35
+ readonly name: string;
36
+ readonly text: string;
37
+ } | {
38
+ readonly type: typeof Document;
39
+ readonly path: string;
40
+ readonly text: string;
41
+ } | {
42
+ readonly type: typeof Notice;
43
+ readonly text: string;
44
+ };
45
+ export type FakeSession = Partial<Omit<SessionData, 'id'>> & {
46
+ readonly id: string;
47
+ readonly messages?: readonly MessageDraft[];
48
+ readonly goal?: string;
49
+ };
50
+ export type FakeSessions = {
51
+ answer(caller: string, method: string, input: unknown): unknown;
52
+ add(draft: MessageDraft, task?: string): Message[];
53
+ endTurn(): void;
54
+ compact(): void;
55
+ startTask(task: {
56
+ readonly title: string;
57
+ readonly agentType?: string;
58
+ readonly model?: string;
59
+ readonly openedBy?: string;
60
+ }): string;
61
+ start(id: string): void;
62
+ readonly sent: {
63
+ readonly session: string;
64
+ readonly type: string;
65
+ readonly text: string;
66
+ readonly as?: string;
67
+ }[];
68
+ };
69
+ export declare function fakeSessions({ project, given, push }: {
70
+ readonly project: () => string;
71
+ readonly given: readonly FakeSession[];
72
+ readonly push: (mod: string, session: string, change: SessionChange) => void;
73
+ }): FakeSessions;
74
+ //# sourceMappingURL=fake-sessions.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fake-sessions.d.ts","sourceRoot":"","sources":["../src/testing/fake-sessions.ts"],"names":[],"mappings":"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"}