@cmodjs/core 0.2.3 → 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 (273) hide show
  1. package/docs/commands.md +58 -10
  2. package/docs/dependencies.md +12 -12
  3. package/docs/hooks.md +41 -28
  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 +66 -23
  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 +18 -6
  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 -3
  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 -33
  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 +317 -186
  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 +18 -6
  177. package/src/runtime/announce.ts +74 -0
  178. package/src/runtime/bound.ts +12 -0
  179. package/src/runtime/claude.ts +5 -3
  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 -31
  185. package/src/runtime/hooks.ts +19 -10
  186. package/src/runtime/installer.ts +10 -9
  187. package/src/runtime/lifecycle.ts +313 -181
  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 +23 -18
  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 +24 -16
  226. package/src/utils/paths.ts +15 -1
  227. package/src/utils/text.ts +4 -0
  228. package/testing/fake-claude.d.ts +4 -2
  229. package/testing/fake-claude.d.ts.map +1 -1
  230. package/testing/fake-claude.js +18 -12
  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.d.ts +1 -0
  262. package/utils/parse-shell.d.ts.map +1 -1
  263. package/utils/parse-shell.js +22 -13
  264. package/utils/paths.d.ts +2 -0
  265. package/utils/paths.d.ts.map +1 -1
  266. package/utils/paths.js +13 -1
  267. package/utils/text.d.ts +1 -0
  268. package/utils/text.d.ts.map +1 -1
  269. package/utils/text.js +3 -0
  270. package/src/ui/slots.ts +0 -36
  271. package/ui/slots.d.ts +0 -208
  272. package/ui/slots.d.ts.map +0 -1
  273. 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
 
@@ -1,6 +1,6 @@
1
1
  import { basename, parse, type GlobPattern, type ParseEntry } from '../vendor.js'
2
2
 
3
- export type ShellCommand = { argv: [string, ...string[]]; folder: string }
3
+ export type ShellCommand = { argv: [string, ...string[]]; folder: string; input?: number }
4
4
 
5
5
  export type ParsedShell = {
6
6
  commands: ShellCommand[]
@@ -16,8 +16,9 @@ type Expansion = Marker & { end: number }
16
16
  type Heredoc = { delimiter: string; stripsTabs: boolean; isQuoted: boolean; marker: Marker; end: number }
17
17
  type Scan = { text: string; markers: Marker[]; end: number }
18
18
  type Redirect = { operator: string; target: Word }
19
- type Command = { words: Word[]; redirects: Redirect[]; isPipedIn: boolean }
20
- type Stdin = { kind: 'text'; text: string } | { kind: 'pipe' }
19
+ type Pipe = { writer: number | undefined }
20
+ type Command = { words: Word[]; redirects: Redirect[]; pipe: Pipe | undefined }
21
+ type Stdin = { type: 'text'; text: string } | { type: 'pipe' }
21
22
  type Access = 'reads' | 'writes'
22
23
  type OptionSpec = { valued?: string; optional?: string; longValued?: readonly string[] }
23
24
  type Option = { name: string; value: Word | undefined }
@@ -210,7 +211,8 @@ function parseLine(line: string, folder: string, result: ParsedShell): void {
210
211
  try {
211
212
  const { text, markers } = scan(line, 0, false)
212
213
  walk(parse(text, (name) => (name === '' ? undefined : `$${name}`)), markers, folder, result)
213
- } catch {
214
+ } catch (error) {
215
+ if (!(error instanceof SyntaxError) && !(error instanceof Error && error.message.startsWith('Bad substitution: '))) throw error
214
216
  result.isFullyParsed = false
215
217
  }
216
218
  }
@@ -470,7 +472,7 @@ function readHeredocBodies(source: string, from: number, heredocs: Heredoc[]): n
470
472
  function walk(tokens: ParseEntry[], markers: Marker[], start: string, result: ParsedShell): void {
471
473
  let folder = start
472
474
  const subshells: string[] = []
473
- let command = newCommand(false)
475
+ let command = newCommand(undefined)
474
476
  let caseState: 'header' | 'pattern' | undefined
475
477
  for (let index = 0; index < tokens.length; index += 1) {
476
478
  const token = tokens[index]!
@@ -500,12 +502,14 @@ function walk(tokens: ParseEntry[], markers: Marker[], start: string, result: Pa
500
502
  }
501
503
  const following = tokens[index + 1]
502
504
  if (operator === '(' && command.words.length === 1 && following !== undefined && !isWordToken(following) && 'op' in following && following.op === ')') {
503
- command = newCommand(false)
505
+ command = newCommand(undefined)
504
506
  index += 1
505
507
  continue
506
508
  }
509
+ const before = result.commands.length
507
510
  folder = finish(command, operator, folder, result)
508
- command = newCommand(operator === '|' || operator === '|&')
511
+ const writer = result.commands.length > before ? result.commands.length - 1 : undefined
512
+ command = newCommand(operator === '|' || operator === '|&' ? { writer } : undefined)
509
513
  if (operator === '(') subshells.push(folder)
510
514
  else if (operator === ')') {
511
515
  const outer = subshells.pop()
@@ -517,8 +521,8 @@ function walk(tokens: ParseEntry[], markers: Marker[], start: string, result: Pa
517
521
  finish(command, ';', folder, result)
518
522
  }
519
523
 
520
- function newCommand(isPipedIn: boolean): Command {
521
- return { words: [], redirects: [], isPipedIn }
524
+ function newCommand(pipe: Pipe | undefined): Command {
525
+ return { words: [], redirects: [], pipe }
522
526
  }
523
527
 
524
528
  function isWordToken(token: ParseEntry): token is string | GlobPattern {
@@ -543,9 +547,13 @@ function finish(command: Command, operator: string, folder: string, result: Pars
543
547
  const [first, ...args] = words
544
548
  if (first === undefined || first.text === 'for' || first.text === 'select') return folder
545
549
  const here = command.redirects.findLast((redirect) => redirect.operator === '<<<')
546
- const stdin: Stdin | undefined = here !== undefined ? { kind: 'text', text: here.target.text } : command.isPipedIn ? { kind: 'pipe' } : undefined
550
+ const stdin: Stdin | undefined = here !== undefined ? { type: 'text', text: here.target.text } : command.pipe !== undefined ? { type: 'pipe' } : undefined
551
+ const reader = result.commands.length
547
552
  const changed = addCommand(first, args, folder, stdin, result)
548
- const isPiped = command.isPipedIn || operator === '|' || operator === '|&' || operator === '&'
553
+ const read = result.commands[reader]
554
+ const writer = command.pipe?.writer
555
+ if (read !== undefined && writer !== undefined && here === undefined) result.commands[reader] = { ...read, input: writer }
556
+ const isPiped = command.pipe !== undefined || operator === '|' || operator === '|&' || operator === '&'
549
557
  return changed === undefined || isPiped ? folder : changed
550
558
  }
551
559
 
@@ -622,7 +630,7 @@ function findStarts(args: Word[]): Word[] {
622
630
 
623
631
  function addWrapper(first: Word, program: string, args: Word[], folder: string, stdin: Stdin | undefined, result: ParsedShell): void {
624
632
  const wrapper = wrappers.get(program)!
625
- if (program === 'xargs' && stdin?.kind !== 'text') result.isFullyParsed = false
633
+ if (program === 'xargs' && stdin?.type !== 'text') result.isFullyParsed = false
626
634
  const { options, operands } = readArguments(args, wrapper, true)
627
635
  let start = args.length - operands.length
628
636
  if (options.some((option) => wrapper.stops?.includes(option.name) === true)) start = args.length
@@ -662,8 +670,8 @@ function addEffects(program: string, args: Word[], flags: string[], folder: stri
662
670
  else if (inlineFlags !== undefined) {
663
671
  if (program === 'perl' && readArguments(args, perl, false).options.some((option) => option.name === '-i')) addFileArguments(perl, args, folder, result)
664
672
  const script = args.find((word) => !word.text.startsWith('-') || word.text === '-')
665
- const readsCodeFromPipe = stdin?.kind === 'pipe' && (script === undefined || script.text === '-')
666
- 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
667
675
  }
668
676
  }
669
677
 
@@ -694,8 +702,8 @@ function addFetches(fetcher: OptionSpec, args: Word[], result: ParsedShell): voi
694
702
  }
695
703
 
696
704
  function addStdinCode(stdin: Stdin | undefined, folder: string, result: ParsedShell): void {
697
- if (stdin?.kind === 'text') parseLine(stdin.text, folder, result)
698
- 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
699
707
  }
700
708
 
701
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;
@@ -23,16 +24,17 @@ export type Fakes = {
23
24
  };
24
25
  config: {
25
26
  list?: Claude['config']['list'];
26
- set?: Claude['config']['set'];
27
27
  };
28
28
  ui: {
29
29
  ask?: Claude['ui']['ask'];
30
30
  open?: Claude['ui']['open'];
31
31
  scroll?: Claude['ui']['scroll'];
32
+ copy?: Claude['ui']['copy'];
32
33
  };
33
34
  session: {
34
35
  messages?: Claude['session']['messages'];
35
36
  append?: Claude['session']['append'];
37
+ send?: Claude['session']['send'];
36
38
  };
37
39
  prompt: {
38
40
  submit?: Claude['prompt']['submit'];
@@ -61,7 +63,6 @@ export type ShownSchedule = {
61
63
  readonly prompt: string;
62
64
  };
63
65
  export type Shown = {
64
- readonly toasts: string[];
65
66
  readonly notes: string[];
66
67
  readonly prompts: string[];
67
68
  readonly logs: string[];
@@ -80,6 +81,7 @@ export type FakeClaude = {
80
81
  readonly store: Map<string, unknown>;
81
82
  settle(): Promise<void>;
82
83
  };
84
+ export declare function calledAnswer(caller: string, answer: EventResult<'cmod.call'>): unknown;
83
85
  export declare function fakeClaude(plugin: {
84
86
  readonly name: string;
85
87
  readonly root: string;
@@ -1 +1 @@
1
- {"version":3,"file":"fake-claude.d.ts","sourceRoot":"","sources":["../src/testing/fake-claude.ts"],"names":[],"mappings":"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"}
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,17 +101,16 @@ 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
- set: rejected('config.set', () => fakes.config.set ?? (async ({ value }) => ({ value }))),
109
114
  },
110
115
  agent: {
111
116
  list: rejected('agent.list', () => fakes.agent.list),
@@ -131,10 +136,11 @@ export function fakeClaude(plugin) {
131
136
  root: async () => plugin.root,
132
137
  cwd: async () => plugin.root,
133
138
  model: async () => 'test-model',
134
- 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 } }),
135
140
  surfaces: async () => ['terminal'],
136
141
  messages: rejected('session.messages', () => fakes.session.messages),
137
142
  append: rejected('session.append', () => fakes.session.append ?? appendNote),
143
+ send: rejected('session.send', () => fakes.session.send ?? (async () => ({ isDelivered: true }))),
138
144
  },
139
145
  prompt: { submit: rejected('prompt.submit', () => fakes.prompt.submit ?? submitPrompt) },
140
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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"}