@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.
- package/docs/commands.md +58 -10
- package/docs/dependencies.md +12 -12
- package/docs/hooks.md +41 -28
- package/docs/index.md +29 -25
- package/docs/install-steps.md +9 -7
- package/docs/jobs.md +59 -111
- package/docs/mod.md +200 -174
- package/docs/options.md +2 -2
- package/docs/permissions.md +27 -16
- package/docs/session.md +248 -0
- package/docs/state.md +11 -7
- package/docs/testing.md +66 -23
- package/docs/ui.md +108 -76
- package/jobs/check.d.ts.map +1 -1
- package/jobs/check.js +5 -2
- package/jobs/context.d.ts +7 -8
- package/jobs/context.d.ts.map +1 -1
- package/jobs/context.js +23 -73
- package/jobs/permissions/decide-permission.d.ts +5 -3
- package/jobs/permissions/decide-permission.d.ts.map +1 -1
- package/jobs/permissions/decide-permission.js +6 -4
- package/jobs/permissions/find-project-scope.d.ts +1 -1
- package/jobs/permissions/find-project-scope.d.ts.map +1 -1
- package/jobs/permissions/find-project-scope.js +8 -11
- package/jobs/permissions/match-target.d.ts +2 -2
- package/jobs/permissions/match-target.d.ts.map +1 -1
- package/jobs/permissions.d.ts +1 -1
- package/jobs/permissions.d.ts.map +1 -1
- package/jobs/permissions.js +6 -4
- package/jobs/program.d.ts.map +1 -1
- package/jobs/program.js +8 -3
- package/jobs/prompt.d.ts +4 -2
- package/jobs/prompt.d.ts.map +1 -1
- package/jobs/prompt.js +16 -15
- package/jobs/schedule.d.ts +2 -2
- package/jobs/schedule.d.ts.map +1 -1
- package/jobs/schedule.js +9 -9
- package/jobs/slash-command.d.ts +4 -4
- package/jobs/slash-command.d.ts.map +1 -1
- package/jobs/slash-command.js +14 -9
- package/jobs/status-line.d.ts +1 -11
- package/jobs/status-line.d.ts.map +1 -1
- package/jobs/status-line.js +8 -12
- package/jobs/tool.d.ts +2 -2
- package/jobs/tool.d.ts.map +1 -1
- package/jobs/tool.js +15 -9
- package/mod.d.ts +154 -75
- package/mod.d.ts.map +1 -1
- package/mod.js +94 -3
- package/options.d.ts +2 -2
- package/options.js +19 -19
- package/package.json +3 -3
- package/records.d.ts +10 -8
- package/records.d.ts.map +1 -1
- package/records.js +28 -17
- package/register.d.ts.map +1 -1
- package/register.js +18 -6
- package/runtime/announce.d.ts +18 -0
- package/runtime/announce.d.ts.map +1 -0
- package/runtime/announce.js +66 -0
- package/runtime/bound.d.ts +4 -0
- package/runtime/bound.d.ts.map +1 -0
- package/runtime/bound.js +9 -0
- package/runtime/claude.d.ts +3 -3
- package/runtime/claude.d.ts.map +1 -1
- package/runtime/conversations.d.ts +16 -0
- package/runtime/conversations.d.ts.map +1 -0
- package/runtime/conversations.js +291 -0
- package/runtime/deadline.d.ts +0 -1
- package/runtime/deadline.d.ts.map +1 -1
- package/runtime/deadline.js +1 -1
- package/runtime/dependencies.d.ts +2 -0
- package/runtime/dependencies.d.ts.map +1 -1
- package/runtime/dependencies.js +9 -0
- package/runtime/engine.d.ts +17 -0
- package/runtime/engine.d.ts.map +1 -0
- package/runtime/engine.js +10 -0
- package/runtime/grants.d.ts +4 -0
- package/runtime/grants.d.ts.map +1 -1
- package/runtime/grants.js +63 -33
- package/runtime/hooks.d.ts +5 -4
- package/runtime/hooks.d.ts.map +1 -1
- package/runtime/hooks.js +10 -5
- package/runtime/installer.d.ts.map +1 -1
- package/runtime/installer.js +11 -9
- package/runtime/lifecycle.d.ts +3 -1
- package/runtime/lifecycle.d.ts.map +1 -1
- package/runtime/lifecycle.js +317 -186
- package/runtime/metadata.d.ts +3 -1
- package/runtime/metadata.d.ts.map +1 -1
- package/runtime/metadata.js +21 -11
- package/runtime/options.d.ts +3 -1
- package/runtime/options.d.ts.map +1 -1
- package/runtime/options.js +4 -4
- package/runtime/programs.d.ts +8 -0
- package/runtime/programs.d.ts.map +1 -1
- package/runtime/programs.js +39 -1
- package/runtime/router.d.ts +2 -1
- package/runtime/router.d.ts.map +1 -1
- package/runtime/router.js +11 -3
- package/runtime/sessions.d.ts +36 -0
- package/runtime/sessions.d.ts.map +1 -0
- package/runtime/sessions.js +235 -0
- package/runtime/state.d.ts +6 -2
- package/runtime/state.d.ts.map +1 -1
- package/runtime/state.js +46 -13
- package/runtime/toaster.d.ts +31 -0
- package/runtime/toaster.d.ts.map +1 -0
- package/runtime/toaster.js +176 -0
- package/runtime/ui.d.ts +6 -17
- package/runtime/ui.d.ts.map +1 -1
- package/runtime/ui.js +36 -112
- package/session/context/context.d.ts +10 -0
- package/session/context/context.d.ts.map +1 -0
- package/session/context/context.js +2 -0
- package/session/context/turn/message/document.d.ts +7 -0
- package/session/context/turn/message/document.d.ts.map +1 -0
- package/session/context/turn/message/document.js +1 -0
- package/session/context/turn/message/message.d.ts +54 -0
- package/session/context/turn/message/message.d.ts.map +1 -0
- package/session/context/turn/message/message.js +137 -0
- package/session/context/turn/message/note.d.ts +8 -0
- package/session/context/turn/message/note.d.ts.map +1 -0
- package/session/context/turn/message/note.js +1 -0
- package/session/context/turn/message/notice.d.ts +6 -0
- package/session/context/turn/message/notice.d.ts.map +1 -0
- package/session/context/turn/message/notice.js +1 -0
- package/session/context/turn/message/prompt.d.ts +8 -0
- package/session/context/turn/message/prompt.d.ts.map +1 -0
- package/session/context/turn/message/prompt.js +1 -0
- package/session/context/turn/message/reply.d.ts +6 -0
- package/session/context/turn/message/reply.d.ts.map +1 -0
- package/session/context/turn/message/reply.js +1 -0
- package/session/context/turn/message/sender.d.ts +14 -0
- package/session/context/turn/message/sender.d.ts.map +1 -0
- package/session/context/turn/message/sender.js +30 -0
- package/session/context/turn/message/skill.d.ts +7 -0
- package/session/context/turn/message/skill.d.ts.map +1 -0
- package/session/context/turn/message/skill.js +1 -0
- package/session/context/turn/message/tool-call.d.ts +9 -0
- package/session/context/turn/message/tool-call.d.ts.map +1 -0
- package/session/context/turn/message/tool-call.js +1 -0
- package/session/context/turn/message/tool-result.d.ts +8 -0
- package/session/context/turn/message/tool-result.d.ts.map +1 -0
- package/session/context/turn/message/tool-result.js +1 -0
- package/session/context/turn/turn.d.ts +12 -0
- package/session/context/turn/turn.d.ts.map +1 -0
- package/session/context/turn/turn.js +2 -0
- package/session/conversation.d.ts +64 -0
- package/session/conversation.d.ts.map +1 -0
- package/session/conversation.js +123 -0
- package/session/goal/goal.d.ts +10 -0
- package/session/goal/goal.d.ts.map +1 -0
- package/session/goal/goal.js +1 -0
- package/session/goal/task.d.ts +16 -0
- package/session/goal/task.d.ts.map +1 -0
- package/session/goal/task.js +3 -0
- package/session/session.d.ts +99 -0
- package/session/session.d.ts.map +1 -0
- package/session/session.js +1 -0
- package/src/jobs/check.ts +7 -4
- package/src/jobs/context.ts +24 -82
- package/src/jobs/permissions/decide-permission.ts +10 -8
- package/src/jobs/permissions/find-project-scope.ts +8 -14
- package/src/jobs/permissions/match-target.ts +3 -3
- package/src/jobs/permissions.ts +8 -6
- package/src/jobs/program.ts +7 -3
- package/src/jobs/prompt.ts +21 -24
- package/src/jobs/schedule.ts +14 -14
- package/src/jobs/slash-command.ts +20 -14
- package/src/jobs/status-line.ts +9 -21
- package/src/jobs/tool.ts +17 -11
- package/src/mod.ts +209 -97
- package/src/options.ts +22 -22
- package/src/records.ts +35 -23
- package/src/register.ts +18 -6
- package/src/runtime/announce.ts +74 -0
- package/src/runtime/bound.ts +12 -0
- package/src/runtime/claude.ts +5 -3
- package/src/runtime/conversations.ts +302 -0
- package/src/runtime/deadline.ts +2 -2
- package/src/runtime/dependencies.ts +12 -0
- package/src/runtime/engine.ts +22 -0
- package/src/runtime/grants.ts +66 -31
- package/src/runtime/hooks.ts +19 -10
- package/src/runtime/installer.ts +10 -9
- package/src/runtime/lifecycle.ts +313 -181
- package/src/runtime/metadata.ts +33 -11
- package/src/runtime/options.ts +19 -4
- package/src/runtime/programs.ts +40 -1
- package/src/runtime/router.ts +11 -4
- package/src/runtime/sessions.ts +242 -0
- package/src/runtime/state.ts +50 -14
- package/src/runtime/toaster.ts +218 -0
- package/src/runtime/ui.ts +42 -139
- package/src/session/context/context.ts +12 -0
- package/src/session/context/turn/message/document.ts +8 -0
- package/src/session/context/turn/message/message.ts +165 -0
- package/src/session/context/turn/message/note.ts +9 -0
- package/src/session/context/turn/message/notice.ts +7 -0
- package/src/session/context/turn/message/prompt.ts +9 -0
- package/src/session/context/turn/message/reply.ts +7 -0
- package/src/session/context/turn/message/sender.ts +38 -0
- package/src/session/context/turn/message/skill.ts +8 -0
- package/src/session/context/turn/message/tool-call.ts +10 -0
- package/src/session/context/turn/message/tool-result.ts +9 -0
- package/src/session/context/turn/turn.ts +14 -0
- package/src/session/conversation.ts +169 -0
- package/src/session/goal/goal.ts +9 -0
- package/src/session/goal/task.ts +19 -0
- package/src/session/session.ts +94 -0
- package/src/testing/fake-claude.ts +23 -18
- package/src/testing/fake-elements.ts +3 -2
- package/src/testing/fake-files.ts +1 -1
- package/src/testing/fake-sessions.ts +203 -0
- package/src/testing.ts +112 -33
- package/src/ui/components.ts +3 -3
- package/src/ui/elements.ts +22 -8
- package/src/ui/markdown.ts +22 -26
- package/src/ui/places.ts +49 -0
- package/src/utils/call-effects.ts +3 -3
- package/src/utils/changelog.ts +32 -0
- package/src/utils/matcher.ts +37 -0
- package/src/utils/metadata.ts +17 -15
- package/src/utils/parse-shell.ts +24 -16
- package/src/utils/paths.ts +15 -1
- package/src/utils/text.ts +4 -0
- package/testing/fake-claude.d.ts +4 -2
- package/testing/fake-claude.d.ts.map +1 -1
- package/testing/fake-claude.js +18 -12
- package/testing/fake-elements.js +4 -2
- package/testing/fake-files.js +1 -1
- package/testing/fake-sessions.d.ts +74 -0
- package/testing/fake-sessions.d.ts.map +1 -0
- package/testing/fake-sessions.js +170 -0
- package/testing.d.ts +33 -13
- package/testing.d.ts.map +1 -1
- package/testing.js +92 -21
- package/ui/components.d.ts +2 -3
- package/ui/components.d.ts.map +1 -1
- package/ui/components.js +3 -3
- package/ui/elements.d.ts.map +1 -1
- package/ui/elements.js +19 -9
- package/ui/markdown.d.ts +35 -33
- package/ui/markdown.d.ts.map +1 -1
- package/ui/markdown.js +14 -16
- package/ui/places.d.ts +43 -0
- package/ui/places.d.ts.map +1 -0
- package/ui/places.js +31 -0
- package/utils/call-effects.d.ts +1 -1
- package/utils/call-effects.d.ts.map +1 -1
- package/utils/call-effects.js +2 -2
- package/utils/changelog.d.ts +9 -0
- package/utils/changelog.d.ts.map +1 -0
- package/utils/changelog.js +29 -0
- package/utils/matcher.d.ts +13 -0
- package/utils/matcher.d.ts.map +1 -0
- package/utils/matcher.js +32 -0
- package/utils/metadata.d.ts +3 -3
- package/utils/metadata.js +18 -14
- package/utils/parse-shell.d.ts +1 -0
- package/utils/parse-shell.d.ts.map +1 -1
- package/utils/parse-shell.js +22 -13
- package/utils/paths.d.ts +2 -0
- package/utils/paths.d.ts.map +1 -1
- package/utils/paths.js +13 -1
- package/utils/text.d.ts +1 -0
- package/utils/text.d.ts.map +1 -1
- package/utils/text.js +3 -0
- package/src/ui/slots.ts +0 -36
- package/ui/slots.d.ts +0 -208
- package/ui/slots.d.ts.map +0 -1
- package/ui/slots.js +0 -19
package/src/ui/places.ts
ADDED
|
@@ -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
|
|
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((
|
|
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
|
+
}
|
package/src/utils/metadata.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type MetadataFormat = { readonly
|
|
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 {
|
|
30
|
-
if (hashComments.includes(extension) || hashCommentNames.includes(name)) return {
|
|
31
|
-
if (slashComments.includes(extension)) return {
|
|
32
|
-
if (dashComments.includes(extension)) return {
|
|
33
|
-
if (extension === '' && firstLine?.startsWith('#!') === true) return {
|
|
34
|
-
return {
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
package/src/utils/parse-shell.ts
CHANGED
|
@@ -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
|
|
20
|
-
type
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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(
|
|
521
|
-
return { words: [], redirects: [],
|
|
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 ? {
|
|
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
|
|
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?.
|
|
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?.
|
|
666
|
-
if (flags.some((flag) => inlineFlags.includes(flag)) || stdin?.
|
|
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?.
|
|
698
|
-
if (stdin?.
|
|
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 {
|
package/src/utils/paths.ts
CHANGED
|
@@ -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((
|
|
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
|
}
|
package/testing/fake-claude.d.ts
CHANGED
|
@@ -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":"
|
|
1
|
+
{"version":3,"file":"fake-claude.d.ts","sourceRoot":"","sources":["../src/testing/fake-claude.ts"],"names":[],"mappings":"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"}
|
package/testing/fake-claude.js
CHANGED
|
@@ -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
|
|
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 ??
|
|
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:
|
|
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) },
|
package/testing/fake-elements.js
CHANGED
|
@@ -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
|
|
123
|
-
|
|
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 }, () => '');
|
package/testing/fake-files.js
CHANGED
|
@@ -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(
|
|
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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"}
|