@cmodjs/core 0.1.8 → 0.1.9
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 +2 -2
- package/docs/hooks.md +62 -6
- package/docs/index.md +6 -4
- package/docs/install-steps.md +3 -3
- package/docs/jobs.md +5 -4
- package/docs/mod.md +39 -7
- package/docs/testing.md +25 -9
- package/docs/ui.md +27 -3
- package/jobs/check.js +2 -2
- package/jobs/context.d.ts.map +1 -1
- package/jobs/context.js +2 -0
- package/jobs/permissions/decide-permission.js +2 -2
- 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 +9 -9
- package/jobs/permissions/match-target.d.ts.map +1 -1
- package/jobs/permissions/match-target.js +4 -3
- package/mod.d.ts +15 -5
- package/mod.d.ts.map +1 -1
- package/package.json +2 -1
- package/path.d.ts +2 -0
- package/path.d.ts.map +1 -0
- package/path.js +1 -0
- package/register.d.ts.map +1 -1
- package/register.js +8 -2
- package/runtime/claude.d.ts +4 -1
- package/runtime/claude.d.ts.map +1 -1
- package/runtime/hooks.d.ts +10 -2
- package/runtime/hooks.d.ts.map +1 -1
- package/runtime/hooks.js +56 -18
- package/runtime/lifecycle.d.ts.map +1 -1
- package/runtime/lifecycle.js +12 -2
- package/runtime/ui.d.ts.map +1 -1
- package/runtime/ui.js +38 -8
- package/shell.d.ts +2 -0
- package/shell.d.ts.map +1 -0
- package/shell.js +1 -0
- package/src/jobs/check.ts +2 -2
- package/src/jobs/context.ts +2 -0
- package/src/jobs/permissions/decide-permission.ts +2 -2
- package/src/jobs/permissions/find-project-scope.ts +11 -10
- package/src/jobs/permissions/match-target.ts +4 -3
- package/src/mod.ts +22 -3
- package/src/path.ts +1 -0
- package/src/register.ts +8 -2
- package/src/runtime/claude.ts +11 -1
- package/src/runtime/hooks.ts +62 -23
- package/src/runtime/lifecycle.ts +11 -4
- package/src/runtime/ui.ts +35 -8
- package/src/shell.ts +1 -0
- package/src/testing/fake-claude.ts +14 -5
- package/src/testing/fake-elements.ts +5 -7
- package/src/testing/fake-files.ts +5 -5
- package/src/testing.ts +83 -11
- package/src/ui/define-pane.ts +5 -1
- package/src/utils/call-effects.ts +12 -11
- package/src/utils/parse-shell.ts +28 -36
- package/src/utils/paths.ts +4 -19
- package/src/vendor.ts +1 -0
- package/testing/fake-claude.d.ts +5 -0
- package/testing/fake-claude.d.ts.map +1 -1
- package/testing/fake-claude.js +13 -3
- package/testing/fake-elements.d.ts +5 -8
- package/testing/fake-elements.d.ts.map +1 -1
- package/testing/fake-elements.js +6 -6
- package/testing/fake-files.js +5 -5
- package/testing.d.ts +4 -2
- package/testing.d.ts.map +1 -1
- package/testing.js +65 -9
- package/ui/define-pane.d.ts +5 -1
- package/ui/define-pane.d.ts.map +1 -1
- package/utils/call-effects.d.ts.map +1 -1
- package/utils/call-effects.js +12 -11
- package/utils/parse-shell.d.ts +5 -9
- package/utils/parse-shell.d.ts.map +1 -1
- package/utils/parse-shell.js +5 -10
- package/utils/paths.d.ts +0 -3
- package/utils/paths.d.ts.map +1 -1
- package/utils/paths.js +3 -18
- package/vendor.d.ts +1 -0
- package/vendor.d.ts.map +1 -1
- package/vendor.js +209 -12
|
@@ -3,7 +3,7 @@ import { messageOf } from '../../utils/text.js'
|
|
|
3
3
|
import { callBaseOf, callEffects, type CallEffects, type ToolCall, type ToolUse } from '../../utils/call-effects.js'
|
|
4
4
|
import type { Workspace } from './find-project-scope.js'
|
|
5
5
|
import { folderInScope, matchTarget, targetOf, type MatchedCall, type Target, type TargetCalls, type TargetKey, type TargetOf } from './match-target.js'
|
|
6
|
-
import {
|
|
6
|
+
import { resolve } from '../../vendor.js'
|
|
7
7
|
|
|
8
8
|
export type Rule<Mod> = {
|
|
9
9
|
[Key in TargetKey]: TargetOf<Key> & {
|
|
@@ -71,7 +71,7 @@ async function unparsedMatches(target: Target, use: ToolUse, effects: CallEffect
|
|
|
71
71
|
const named = patterns.map((pattern) => pattern.slice(pattern.lastIndexOf('/') + 1)).filter((segment) => segment !== '' && !/[*?[\]{}]/.test(segment) && line.includes(segment.toLowerCase()))
|
|
72
72
|
const folder = named.length === 0 ? undefined : await folderInScope(shell, workspace)
|
|
73
73
|
if (folder === undefined) return []
|
|
74
|
-
return named.map((segment) => ({ call: { ...base, path:
|
|
74
|
+
return named.map((segment) => ({ call: { ...base, path: resolve(folder, segment) }, access: undefined, folder }))
|
|
75
75
|
}
|
|
76
76
|
|
|
77
77
|
function containsWord(line: string, word: string): boolean {
|
|
@@ -1,14 +1,15 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { FileSystem } from '../../utils/paths.js'
|
|
2
|
+
import { basename, dirname, resolve } from '../../vendor.js'
|
|
2
3
|
|
|
3
4
|
export type ProjectScope = { workTreeOf: (path: string) => Promise<string | undefined> }
|
|
4
5
|
|
|
5
6
|
export type Workspace = { projectRoot: string; cwd: string; home: string; fs: FileSystem; scope: ProjectScope | undefined }
|
|
6
7
|
|
|
7
8
|
export async function findProjectScope(pluginRoot: string, home: string, fs: FileSystem): Promise<ProjectScope | undefined> {
|
|
8
|
-
const skills =
|
|
9
|
-
const claude =
|
|
10
|
-
const root =
|
|
11
|
-
if (
|
|
9
|
+
const skills = dirname(pluginRoot)
|
|
10
|
+
const claude = dirname(skills)
|
|
11
|
+
const root = dirname(claude)
|
|
12
|
+
if (basename(skills) !== 'skills' || basename(claude) !== '.claude' || root === resolve('/', home)) return undefined
|
|
12
13
|
const repositories = new Map<string, string>()
|
|
13
14
|
const repositoryOf = async (workTree: string) => {
|
|
14
15
|
const known = repositories.get(workTree)
|
|
@@ -21,7 +22,7 @@ export async function findProjectScope(pluginRoot: string, home: string, fs: Fil
|
|
|
21
22
|
if (project === undefined) return undefined
|
|
22
23
|
return {
|
|
23
24
|
async workTreeOf(path) {
|
|
24
|
-
for (let folder = path; ; folder =
|
|
25
|
+
for (let folder = path; ; folder = dirname(folder)) {
|
|
25
26
|
const repository = await repositoryOf(folder)
|
|
26
27
|
if (repository !== undefined) return repository === project ? folder : undefined
|
|
27
28
|
if (folder === '/') return undefined
|
|
@@ -31,16 +32,16 @@ export async function findProjectScope(pluginRoot: string, home: string, fs: Fil
|
|
|
31
32
|
}
|
|
32
33
|
|
|
33
34
|
async function readRepository(workTree: string, fs: FileSystem): Promise<string | undefined> {
|
|
34
|
-
const gitPath =
|
|
35
|
+
const gitPath = resolve(workTree, '.git')
|
|
35
36
|
const git = await fs.stat(gitPath).catch(() => undefined)
|
|
36
37
|
if (git === undefined) return undefined
|
|
37
38
|
if (git.kind === 'dir') return git.realPath ?? gitPath
|
|
38
39
|
try {
|
|
39
40
|
const gitDirectory = /^gitdir:\s*(.+)$/m.exec(await fs.read(gitPath))?.[1]?.trim()
|
|
40
41
|
if (gitDirectory === undefined) throw new Error(`${gitPath} has no gitdir line.`)
|
|
41
|
-
const linkedDirectory =
|
|
42
|
-
const commonDirectory = await fs.read(
|
|
43
|
-
(text) =>
|
|
42
|
+
const linkedDirectory = resolve(workTree, gitDirectory)
|
|
43
|
+
const commonDirectory = await fs.read(resolve(linkedDirectory, 'commondir')).then(
|
|
44
|
+
(text) => resolve(linkedDirectory, text.trim()),
|
|
44
45
|
() => linkedDirectory,
|
|
45
46
|
)
|
|
46
47
|
return (await fs.stat(commonDirectory)).realPath ?? commonDirectory
|
|
@@ -2,7 +2,8 @@ import { matchesGlob } from '../../utils/matches-glob.js'
|
|
|
2
2
|
import { parseShell } from '../../utils/parse-shell.js'
|
|
3
3
|
import { callBaseOf, type CallBase, type CallEffects, type CommandCall, type FetchCall, type FileAccess, type FileCall, type ToolCall, type ToolUse } from '../../utils/call-effects.js'
|
|
4
4
|
import type { Workspace } from './find-project-scope.js'
|
|
5
|
-
import {
|
|
5
|
+
import { realPathOf, relativePath } from '../../utils/paths.js'
|
|
6
|
+
import { basename } from '../../vendor.js'
|
|
6
7
|
|
|
7
8
|
export type TargetCalls = { command: CommandCall; read: FileCall; write: FileCall; fetch: FetchCall; subagent: CallBase; tool: CallBase }
|
|
8
9
|
export type TargetKey = keyof TargetCalls
|
|
@@ -56,11 +57,11 @@ export async function matchTarget(target: Target, use: ToolUse, effects: CallEff
|
|
|
56
57
|
function commandPattern(pattern: string): [string, ...string[]] {
|
|
57
58
|
const command = parseShell(pattern).commands[0]
|
|
58
59
|
if (command === undefined) throw new Error(`The command pattern '${pattern}' names no program.`)
|
|
59
|
-
return command
|
|
60
|
+
return command.argv
|
|
60
61
|
}
|
|
61
62
|
|
|
62
63
|
function commandMatches(pattern: [string, ...string[]], argv: [string, ...string[]]): boolean {
|
|
63
|
-
if (
|
|
64
|
+
if (basename(argv[0]) !== basename(pattern[0])) return false
|
|
64
65
|
const wanted = splitArguments(pattern.slice(1))
|
|
65
66
|
const actual = splitArguments(argv.slice(1))
|
|
66
67
|
let next = 0
|
package/src/mod.ts
CHANGED
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
import type {
|
|
2
|
+
AgentSpawnArgs,
|
|
3
|
+
AgentSpawnResult,
|
|
2
4
|
AskOptions,
|
|
3
5
|
ClassicHookInputs,
|
|
4
6
|
CmodDependencies,
|
|
5
7
|
FsEntry,
|
|
8
|
+
FsStat,
|
|
9
|
+
FsStatOptions,
|
|
6
10
|
HookStream,
|
|
7
11
|
HttpInit,
|
|
8
12
|
HttpResponse,
|
|
13
|
+
PaneOpenArgs,
|
|
9
14
|
PermissionRequestDecision,
|
|
10
15
|
ProcessRunInit,
|
|
11
16
|
ProcessRunResult,
|
|
@@ -15,6 +20,8 @@ import type {
|
|
|
15
20
|
RenderElement,
|
|
16
21
|
Settings,
|
|
17
22
|
SettingsReadArgs,
|
|
23
|
+
UiScrollArgs,
|
|
24
|
+
UiScrollResult,
|
|
18
25
|
} from 'claude-code'
|
|
19
26
|
import type { Claude } from './runtime/claude.js'
|
|
20
27
|
import type { RoutedEvent } from './runtime/hooks.js'
|
|
@@ -30,9 +37,9 @@ export type ProgressStep = {
|
|
|
30
37
|
}
|
|
31
38
|
|
|
32
39
|
export type PaneHandle = {
|
|
33
|
-
open(): Promise<void>
|
|
40
|
+
open(options?: Readonly<Pick<PaneOpenArgs, 'focus'>>): Promise<void>
|
|
34
41
|
close(): Promise<void>
|
|
35
|
-
toggle(): Promise<void>
|
|
42
|
+
toggle(options?: Readonly<Pick<PaneOpenArgs, 'focus'>>): Promise<void>
|
|
36
43
|
readonly isOpen: boolean
|
|
37
44
|
}
|
|
38
45
|
|
|
@@ -59,6 +66,7 @@ export type ModEvent =
|
|
|
59
66
|
| 'Stop'
|
|
60
67
|
| 'StopFailure'
|
|
61
68
|
| 'CwdChanged'
|
|
69
|
+
| 'FileChanged'
|
|
62
70
|
|
|
63
71
|
export type HookAnswer = {
|
|
64
72
|
continue?: boolean
|
|
@@ -104,7 +112,9 @@ type HookInputs = Omit<ClassicHookInputs, 'PreToolUse' | 'PostToolUse' | 'PostTo
|
|
|
104
112
|
CwdChanged: Omit<ClassicHookInputs['CwdChanged'], 'transcript_path'>
|
|
105
113
|
}
|
|
106
114
|
|
|
107
|
-
export type
|
|
115
|
+
export type HookInput<E extends ModEvent> = HookInputs[E]
|
|
116
|
+
|
|
117
|
+
export type ModHook<E extends ModEvent> = (input: HookInput<E>) => HookAnswer | void | Promise<HookAnswer | void>
|
|
108
118
|
|
|
109
119
|
export type Mod<State extends object = Record<never, never>> = {
|
|
110
120
|
readonly name: string
|
|
@@ -118,6 +128,7 @@ export type Mod<State extends object = Record<never, never>> = {
|
|
|
118
128
|
toast(text: string): void
|
|
119
129
|
progress<T>(title: string, task: (report: (step: ProgressStep) => void) => Promise<T>): Promise<T>
|
|
120
130
|
ask(question: string, options?: readonly string[] | AskOptions): Promise<string>
|
|
131
|
+
scroll(args: UiScrollArgs): Promise<UiScrollResult>
|
|
121
132
|
}
|
|
122
133
|
readonly process: {
|
|
123
134
|
run(argv: readonly string[], init?: ProcessRunInit): Promise<ProcessRunResult>
|
|
@@ -127,6 +138,8 @@ export type Mod<State extends object = Record<never, never>> = {
|
|
|
127
138
|
read(path: string): Promise<string>
|
|
128
139
|
write(path: string, text: string): Promise<void>
|
|
129
140
|
list(path?: string): Promise<FsEntry[]>
|
|
141
|
+
exists(path: string): Promise<boolean>
|
|
142
|
+
stat(path: string, options?: FsStatOptions): Promise<FsStat>
|
|
130
143
|
}
|
|
131
144
|
readonly http: {
|
|
132
145
|
fetch(url: string, init?: HttpInit): Promise<HttpResponse>
|
|
@@ -134,6 +147,12 @@ export type Mod<State extends object = Record<never, never>> = {
|
|
|
134
147
|
readonly settings: {
|
|
135
148
|
read(args?: SettingsReadArgs): Promise<Settings>
|
|
136
149
|
}
|
|
150
|
+
readonly session: {
|
|
151
|
+
messages: Claude['session']['messages']
|
|
152
|
+
}
|
|
153
|
+
readonly agent: {
|
|
154
|
+
spawn(args: AgentSpawnArgs): Promise<AgentSpawnResult>
|
|
155
|
+
}
|
|
137
156
|
readonly projectRoot: string
|
|
138
157
|
readonly cwd: string
|
|
139
158
|
readonly dependencies: CmodDependencies
|
package/src/path.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { basename, dirname, extname, isAbsolute, join, normalize, relative, resolve } from './vendor.js'
|
package/src/register.ts
CHANGED
|
@@ -21,6 +21,7 @@ async function startMod($: EngineInterface, eventInput: Frozen<Args<'session.sta
|
|
|
21
21
|
open: (pane) => $.ui.open(pane),
|
|
22
22
|
close: (pane) => $.ui.close(pane),
|
|
23
23
|
panes: () => $.ui.panes(),
|
|
24
|
+
scroll: (args) => $.ui.scroll(args),
|
|
24
25
|
resolve: (render) => $.ui.resolve(render),
|
|
25
26
|
},
|
|
26
27
|
process: {
|
|
@@ -54,10 +55,14 @@ async function startMod($: EngineInterface, eventInput: Frozen<Args<'session.sta
|
|
|
54
55
|
model: () => $.session.model(),
|
|
55
56
|
usage: () => $.session.usage(),
|
|
56
57
|
surfaces: () => $.session.surfaces(),
|
|
58
|
+
messages: ((args?: Parameters<Claude['session']['messages']>[0]) => (args === undefined ? $.session.messages() : $.session.messages(args))) as Claude['session']['messages'],
|
|
57
59
|
},
|
|
58
60
|
command: { register: (command) => $.command.register(command) },
|
|
59
61
|
tool: { register: (tool) => $.tool.register(tool) },
|
|
60
|
-
agent: {
|
|
62
|
+
agent: {
|
|
63
|
+
list: () => $.agent.list(),
|
|
64
|
+
spawn: (args) => $.agent.spawn(args),
|
|
65
|
+
},
|
|
61
66
|
env: {
|
|
62
67
|
home: () => $.env.get('HOME'),
|
|
63
68
|
dataHome: () => $.env.get('XDG_DATA_HOME'),
|
|
@@ -91,6 +96,7 @@ export function registerMod<State extends object>(addHook: On, definition: ModDe
|
|
|
91
96
|
addHook('classic.PreCompact', routeToMod)
|
|
92
97
|
addHook('classic.Stop', routeToMod)
|
|
93
98
|
addHook('classic.StopFailure', routeToMod)
|
|
99
|
+
addHook('classic.FileChanged', routeToMod)
|
|
94
100
|
addHook('tool.call', routeToMod)
|
|
95
101
|
addHook('prompt.submit', routeToMod)
|
|
96
102
|
addHook('prompt.context', routeToMod)
|
|
@@ -99,6 +105,7 @@ export function registerMod<State extends object>(addHook: On, definition: ModDe
|
|
|
99
105
|
addHook('skill.prompt', routeToMod)
|
|
100
106
|
addHook('ui.render', routeToMod)
|
|
101
107
|
addHook('ui.press', routeToMod)
|
|
108
|
+
addHook('ui.scroll', routeToMod)
|
|
102
109
|
addHook('ui.close', routeToMod)
|
|
103
110
|
addHook('cmod.call', routeToMod)
|
|
104
111
|
}
|
|
@@ -106,6 +113,5 @@ export function registerMod<State extends object>(addHook: On, definition: ModDe
|
|
|
106
113
|
export function registerPermissionCheck(addHook: On): void {
|
|
107
114
|
checksPermissions = true
|
|
108
115
|
addHook('tool.check', routeToMod)
|
|
109
|
-
addHook('classic.PreToolUse', routeToMod)
|
|
110
116
|
addHook('classic.PermissionRequest', routeToMod)
|
|
111
117
|
}
|
package/src/runtime/claude.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
AgentInfo,
|
|
3
|
+
AgentSpawnArgs,
|
|
4
|
+
AgentSpawnResult,
|
|
3
5
|
Args,
|
|
6
|
+
EngineInterface,
|
|
4
7
|
AskOptions,
|
|
5
8
|
CommandSpec,
|
|
6
9
|
ElementTable,
|
|
@@ -29,6 +32,8 @@ import type {
|
|
|
29
32
|
UiLogOptions,
|
|
30
33
|
UiOpenResult,
|
|
31
34
|
UiPane,
|
|
35
|
+
UiScrollArgs,
|
|
36
|
+
UiScrollResult,
|
|
32
37
|
} from 'claude-code'
|
|
33
38
|
|
|
34
39
|
export type Claude = {
|
|
@@ -43,6 +48,7 @@ export type Claude = {
|
|
|
43
48
|
open(pane: PaneOpenArgs): Promise<UiOpenResult>
|
|
44
49
|
close(pane: PaneCloseArgs): Promise<void>
|
|
45
50
|
panes(): Promise<readonly UiPane[]>
|
|
51
|
+
scroll(args: UiScrollArgs): Promise<UiScrollResult>
|
|
46
52
|
resolve(e: ResolveInput): ElementTable<RenderSurface>
|
|
47
53
|
}
|
|
48
54
|
readonly process: {
|
|
@@ -76,10 +82,14 @@ export type Claude = {
|
|
|
76
82
|
model(): Promise<string>
|
|
77
83
|
usage(): Promise<SessionUsage>
|
|
78
84
|
surfaces(): Promise<readonly RenderSurface[]>
|
|
85
|
+
messages: EngineInterface['session']['messages']
|
|
79
86
|
}
|
|
80
87
|
readonly command: { register(command: CommandSpec): Promise<{ command: string }> }
|
|
81
88
|
readonly tool: { register(tool: ToolSpec): Promise<{ tool: string }> }
|
|
82
|
-
readonly agent: {
|
|
89
|
+
readonly agent: {
|
|
90
|
+
list(): Promise<AgentInfo[]>
|
|
91
|
+
spawn(args: AgentSpawnArgs): Promise<AgentSpawnResult>
|
|
92
|
+
}
|
|
83
93
|
readonly env: {
|
|
84
94
|
home(): Promise<string | undefined>
|
|
85
95
|
dataHome(): Promise<string | undefined>
|
package/src/runtime/hooks.ts
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
import type { Args, ClassicHookInputs, Frozen } from 'claude-code'
|
|
2
|
-
import type { HookAnswer, ModEvent, ModHook } from '../mod.js'
|
|
1
|
+
import type { Args, ClassicHookInputs, EventResult, Frozen } from 'claude-code'
|
|
2
|
+
import type { HookAnswer, HookInput, ModEvent, ModHook } from '../mod.js'
|
|
3
3
|
import { configFolders } from '../records.js'
|
|
4
4
|
import type { Claude } from './claude.js'
|
|
5
5
|
import type { RoutedHook } from './router.js'
|
|
6
|
-
import { toolInputOf, type ToolCalls } from './tool-calls.js'
|
|
6
|
+
import { reservedKeys, toolInputOf, type ToolCalls } from './tool-calls.js'
|
|
7
7
|
import { callEffects, type FileAccess, type ToolUse } from '../utils/call-effects.js'
|
|
8
8
|
import { dynamicPattern } from '../utils/parse-shell.js'
|
|
9
9
|
import { messageOf } from '../utils/text.js'
|
|
10
10
|
|
|
11
11
|
export type RoutedEvent =
|
|
12
|
-
| `classic.${ModEvent}`
|
|
12
|
+
| `classic.${Exclude<ModEvent, 'PreToolUse'>}`
|
|
13
13
|
| 'tool.check'
|
|
14
14
|
| 'tool.call'
|
|
15
15
|
| 'prompt.submit'
|
|
@@ -19,16 +19,15 @@ export type RoutedEvent =
|
|
|
19
19
|
| 'skill.prompt'
|
|
20
20
|
| 'ui.render'
|
|
21
21
|
| 'ui.press'
|
|
22
|
+
| 'ui.scroll'
|
|
22
23
|
| 'ui.close'
|
|
23
24
|
| 'cmod.call'
|
|
24
25
|
|
|
25
|
-
export const permissionEvents: readonly RoutedEvent[] = ['tool.check', 'classic.
|
|
26
|
+
export const permissionEvents: readonly RoutedEvent[] = ['tool.check', 'classic.PermissionRequest']
|
|
26
27
|
|
|
27
28
|
type ClassicFields = Record<string, unknown>
|
|
28
29
|
|
|
29
|
-
type
|
|
30
|
-
|
|
31
|
-
type CallFiles = PreToolUseInput['files']
|
|
30
|
+
type CallFiles = HookInput<'PreToolUse'>['files']
|
|
32
31
|
|
|
33
32
|
const flags: readonly string[] = ['suppressOriginalPrompt', 'reloadSkills', 'retry']
|
|
34
33
|
|
|
@@ -50,18 +49,18 @@ const readFields: Record<ModEvent, readonly string[]> = {
|
|
|
50
49
|
Stop: ['additionalContext'],
|
|
51
50
|
StopFailure: [],
|
|
52
51
|
CwdChanged: [],
|
|
52
|
+
FileChanged: [],
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
-
export function classicHook<E extends ModEvent
|
|
55
|
+
export function classicHook<E extends Exclude<ModEvent, 'PreToolUse'>>(name: string, event: E, hook: ModHook<E>, claude: Claude, calls: ToolCalls): RoutedHook<RoutedEvent> {
|
|
56
56
|
const inputOf = async (e: unknown): Promise<unknown> => {
|
|
57
|
-
if (event === 'PreToolUse') return preToolUseInput(name, e as Frozen<Args<'classic.PreToolUse'>>, claude, calls.agentOf)
|
|
58
57
|
if (event !== 'PostToolUse' && event !== 'PostToolUseFailure') return e
|
|
59
58
|
const input = e as ClassicHookInputs['PostToolUse' | 'PostToolUseFailure']
|
|
60
59
|
const cwd = calls.cwdOf(input.tool_use_id) ?? input.cwd
|
|
61
60
|
return { ...input, files: await callFiles(name, claude, { tool: input.tool_name, input: input.tool_input }, cwd) }
|
|
62
61
|
}
|
|
63
62
|
const resultOf = async (e: unknown): Promise<ClassicFields | undefined> => {
|
|
64
|
-
const answer = await hook((await inputOf(e)) as
|
|
63
|
+
const answer = await hook((await inputOf(e)) as HookInput<E>)
|
|
65
64
|
if (answer === undefined) return undefined
|
|
66
65
|
if (answer.systemMessage !== undefined) claude.ui.log(answer.systemMessage)
|
|
67
66
|
return classicResult(event, answer)
|
|
@@ -69,17 +68,65 @@ export function classicHook<E extends ModEvent>(name: string, event: E, hook: Mo
|
|
|
69
68
|
const routed = async (e: unknown, next: (e: unknown) => Promise<unknown>) => {
|
|
70
69
|
const ours = await resultOf(e).catch((error: unknown): ClassicFields | undefined => {
|
|
71
70
|
const reason = `${name}: the ${event} hook failed: ${messageOf(error)}`
|
|
72
|
-
if (event === 'PreToolUse') return { deny: reason }
|
|
73
71
|
if (event === 'PermissionRequest') return { decision: { behavior: 'deny', message: reason } }
|
|
74
72
|
claude.ui.log(reason)
|
|
75
73
|
return undefined
|
|
76
74
|
})
|
|
77
75
|
if (ours === undefined) return next(e)
|
|
78
|
-
return mergeClassic(
|
|
76
|
+
return mergeClassic((await next(e)) as ClassicFields, ours)
|
|
79
77
|
}
|
|
80
78
|
return routed as unknown as RoutedHook<RoutedEvent>
|
|
81
79
|
}
|
|
82
80
|
|
|
81
|
+
type HeldDecision = { readonly decision: 'allow' | 'ask'; readonly reason?: string }
|
|
82
|
+
|
|
83
|
+
export type HeldDecisions = Map<string, HeldDecision>
|
|
84
|
+
|
|
85
|
+
export function preToolUseHook(name: string, hook: ModHook<'PreToolUse'>, claude: Claude, calls: ToolCalls, held: HeldDecisions | undefined): RoutedHook<'tool.call'> {
|
|
86
|
+
return async (e, next) => {
|
|
87
|
+
let ours: ClassicFields
|
|
88
|
+
try {
|
|
89
|
+
const answer = await hook(await preToolUseInput(name, e, claude, calls.agentOf))
|
|
90
|
+
if (answer?.systemMessage !== undefined) claude.ui.log(answer.systemMessage)
|
|
91
|
+
ours = answer === undefined ? {} : classicResult('PreToolUse', answer)
|
|
92
|
+
} catch (error) {
|
|
93
|
+
return { deny: `${name}: the PreToolUse hook failed: ${messageOf(error)}` }
|
|
94
|
+
}
|
|
95
|
+
if (typeof ours['deny'] === 'string') return { deny: ours['deny'] }
|
|
96
|
+
const decided = heldDecisionOf(ours)
|
|
97
|
+
if (decided !== undefined && held === undefined) {
|
|
98
|
+
return { deny: `${name}: the PreToolUse hook answered permissionDecision "${decided.decision}", so hooks/register.ts must call registerPermissionCheck(addHook) after registerMod.` }
|
|
99
|
+
}
|
|
100
|
+
const updated = ours['updatedInput'] as Record<string, unknown> | undefined
|
|
101
|
+
const reserved = Object.fromEntries(Object.entries(e).filter(([key]) => reservedKeys.includes(key)))
|
|
102
|
+
const earlier = held?.get(e.tool_use_id)
|
|
103
|
+
if (decided !== undefined) held?.set(e.tool_use_id, earlier !== undefined && strictness.indexOf(earlier.decision) >= strictness.indexOf(decided.decision) ? earlier : decided)
|
|
104
|
+
let result: EventResult<'tool.call'>
|
|
105
|
+
try {
|
|
106
|
+
result = await next(updated === undefined ? e : ({ ...reserved, ...updated } as Frozen<Args<'tool.call'>>))
|
|
107
|
+
} finally {
|
|
108
|
+
if (decided !== undefined) held?.delete(e.tool_use_id)
|
|
109
|
+
}
|
|
110
|
+
const context = ours['additionalContext'] as string[] | undefined
|
|
111
|
+
if (context === undefined || result.deny !== undefined) return result
|
|
112
|
+
return { ...result, context: [...(result.context ?? []), ...context] }
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function heldDecisionHook(held: HeldDecisions): RoutedHook<'tool.check'> {
|
|
117
|
+
return async (e, next) => {
|
|
118
|
+
const below = await next(e)
|
|
119
|
+
const ours = e.tool_use_id === undefined ? undefined : held.get(e.tool_use_id)
|
|
120
|
+
return ours === undefined || below.decision === 'deny' ? below : ours
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function heldDecisionOf(ours: ClassicFields): HeldDecision | undefined {
|
|
125
|
+
if (ours['allow'] !== undefined) return { decision: 'allow' }
|
|
126
|
+
if (typeof ours['ask'] !== 'string') return undefined
|
|
127
|
+
return ours['ask'] === '' ? { decision: 'ask' } : { decision: 'ask', reason: ours['ask'] }
|
|
128
|
+
}
|
|
129
|
+
|
|
83
130
|
const frontmatter = /^---\r?\n[\s\S]*?\r?\n---[ \t]*(?:\r?\n|$)/
|
|
84
131
|
|
|
85
132
|
const baseDirectoryLine = /^Base directory for this skill: [^\n]*\n\n/
|
|
@@ -101,7 +148,7 @@ export function userSkillHook(claude: Claude): RoutedHook<'skill.prompt'> {
|
|
|
101
148
|
}
|
|
102
149
|
}
|
|
103
150
|
|
|
104
|
-
async function preToolUseInput(name: string, envelope: Frozen<Args<'
|
|
151
|
+
async function preToolUseInput(name: string, envelope: Frozen<Args<'tool.call'>>, claude: Claude, agentOf: ToolCalls['agentOf']): Promise<HookInput<'PreToolUse'>> {
|
|
105
152
|
const { tool, tool_use_id } = envelope
|
|
106
153
|
const [session_id, cwd, { agentId, agentType }] = await Promise.all([claude.session.id(), claude.session.cwd(), agentOf(tool_use_id)])
|
|
107
154
|
const tool_input = toolInputOf(envelope)
|
|
@@ -161,17 +208,9 @@ function classicResult(event: ModEvent, answer: HookAnswer): ClassicFields {
|
|
|
161
208
|
|
|
162
209
|
export const strictness = ['allow', 'ask', 'deny'] as const
|
|
163
210
|
|
|
164
|
-
function
|
|
165
|
-
return strictness.findLastIndex((decision) => result[decision] !== undefined)
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
function mergeClassic(event: ModEvent, below: ClassicFields, ours: ClassicFields): ClassicFields {
|
|
211
|
+
function mergeClassic(below: ClassicFields, ours: ClassicFields): ClassicFields {
|
|
169
212
|
const merged: ClassicFields = { ...below, ...ours }
|
|
170
213
|
const context = [...((below['additionalContext'] as string[] | undefined) ?? []), ...((ours['additionalContext'] as string[] | undefined) ?? [])]
|
|
171
214
|
if (context.length > 0) merged['additionalContext'] = context
|
|
172
|
-
if (event !== 'PreToolUse') return merged
|
|
173
|
-
const strictest = strictness[Math.max(strictnessOf(below), strictnessOf(ours))]
|
|
174
|
-
for (const decision of strictness) delete merged[decision]
|
|
175
|
-
if (strictest !== undefined) merged[strictest] = ours[strictest] ?? below[strictest]
|
|
176
215
|
return merged
|
|
177
216
|
}
|
package/src/runtime/lifecycle.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import type { Args, EventResult, Frozen, HookBudget, HookStream, ProcessSpawnChunk, ProcessSpawnResult } from 'claude-code'
|
|
2
|
-
import type { Mod, ModDefinition, ModEvent, ModHook } from '../mod.js'
|
|
2
|
+
import type { HookInput, Mod, ModDefinition, ModEvent, ModHook } from '../mod.js'
|
|
3
3
|
import { dataFolder, isAtLeast, parseEvent, readRecord, readSteps, scriptsSha256, storeFolder, type ReadFile, type RunnerEvent } from '../records.js'
|
|
4
4
|
import { relativePath } from '../utils/paths.js'
|
|
5
5
|
import { formatExit, listed, messageOf } from '../utils/text.js'
|
|
6
6
|
import type { Claude } from './claude.js'
|
|
7
7
|
import { beforeDeadline } from './deadline.js'
|
|
8
8
|
import { answerCall, dependencyCalls, notInstalled } from './dependencies.js'
|
|
9
|
-
import { classicHook, permissionEvents, userSkillHook, type RoutedEvent } from './hooks.js'
|
|
9
|
+
import { classicHook, heldDecisionHook, permissionEvents, preToolUseHook, userSkillHook, type HeldDecisions, type RoutedEvent } from './hooks.js'
|
|
10
10
|
import { createRouter, type Router, type RouterNext } from './router.js'
|
|
11
11
|
import { createState } from './state.js'
|
|
12
12
|
import { toolCalls } from './tool-calls.js'
|
|
@@ -362,13 +362,15 @@ async function createMod<State extends object>(definition: ModDefinition<State>,
|
|
|
362
362
|
let staleCwd: string | undefined
|
|
363
363
|
const area = createUi<State>({ name: definition.name, claude, router, progress, announce, mod: () => mod })
|
|
364
364
|
const modState = createState<State>({ name: definition.name, initial: definition.state ?? {}, session, root, claude, changed: area.changed })
|
|
365
|
+
const held: HeldDecisions | undefined = runtime.checksPermissions() ? new Map() : undefined
|
|
365
366
|
const mod: Mod<State> = {
|
|
366
367
|
name: definition.name,
|
|
367
368
|
state: modState.state,
|
|
368
369
|
dataFolder: runtime.dataFolder,
|
|
369
370
|
on(event, hook) {
|
|
370
371
|
if (!hookEvents.includes(event)) hookEvents.push(event)
|
|
371
|
-
router.add(
|
|
372
|
+
if (event === 'PreToolUse') router.add('tool.call', preToolUseHook(definition.name, hook as ModHook<'PreToolUse'>, claude, agents, held))
|
|
373
|
+
else router.add(`classic.${event as Exclude<ModEvent, 'PreToolUse'>}`, classicHook(definition.name, event as Exclude<ModEvent, 'PreToolUse'>, hook as ModHook<Exclude<ModEvent, 'PreToolUse'>>, claude, agents))
|
|
372
374
|
},
|
|
373
375
|
use: (job) => job({ mod, claude, on: (event, hook) => router.add(event, hook), announce, reserveName, toolCalls: agents }),
|
|
374
376
|
ui: area.ui,
|
|
@@ -380,9 +382,13 @@ async function createMod<State extends object>(definition: ModDefinition<State>,
|
|
|
380
382
|
read: (path) => claude.fs.read(path),
|
|
381
383
|
write: (path, text) => claude.fs.write(path, text),
|
|
382
384
|
list: (path) => claude.fs.list(path),
|
|
385
|
+
exists: (path) => claude.fs.exists(path),
|
|
386
|
+
stat: (path, options) => claude.fs.stat(path, options),
|
|
383
387
|
},
|
|
384
388
|
http: { fetch: (url, init) => claude.http.fetch(url, init) },
|
|
385
389
|
settings: { read: (args) => claude.settings.read(args) },
|
|
390
|
+
session: { messages: claude.session.messages },
|
|
391
|
+
agent: { spawn: (args) => claude.agent.spawn(args) },
|
|
386
392
|
get projectRoot() {
|
|
387
393
|
return modState.root
|
|
388
394
|
},
|
|
@@ -408,7 +414,7 @@ async function createMod<State extends object>(definition: ModDefinition<State>,
|
|
|
408
414
|
loadedCwd = nextCwd
|
|
409
415
|
if (!hasMovedRoot) area.changed()
|
|
410
416
|
if (nextCwd === oldCwd) return
|
|
411
|
-
const moved:
|
|
417
|
+
const moved: HookInput<'CwdChanged'> = { session_id: await claude.session.id(), cwd: nextCwd, hook_event_name: 'CwdChanged', old_cwd: oldCwd, new_cwd: nextCwd }
|
|
412
418
|
await router.dispatch('classic.CwdChanged', moved as Frozen<Args<'classic.CwdChanged'>>, async () => ({}))
|
|
413
419
|
} catch (error) {
|
|
414
420
|
claude.ui.log(`${definition.name} keeps the state of ${modState.root} until the next prompt or folder move: ${messageOf(error)}`)
|
|
@@ -452,6 +458,7 @@ async function createMod<State extends object>(definition: ModDefinition<State>,
|
|
|
452
458
|
if (permissionHooks.length > 0 && !runtime.checksPermissions()) {
|
|
453
459
|
throw new Error(`it decides permissions on ${listed(permissionHooks)}, so hooks/register.ts must call registerPermissionCheck(addHook) after registerMod`)
|
|
454
460
|
}
|
|
461
|
+
if (held !== undefined && hookEvents.includes('PreToolUse')) router.add('tool.check', heldDecisionHook(held))
|
|
455
462
|
await failsAs('its open panes did not load', () => area.restorePanes())
|
|
456
463
|
if (hookEvents.length > 0) added.push(`${hookEvents.length === 1 ? 'a hook' : 'hooks'} on ${listed(hookEvents)}`)
|
|
457
464
|
return { mod, added }
|
package/src/runtime/ui.ts
CHANGED
|
@@ -224,6 +224,7 @@ export type UiArea<State extends object> = {
|
|
|
224
224
|
|
|
225
225
|
export function createUi<State extends object>({ name, claude, router, progress, announce, mod }: UiOptions<State>): UiArea<State> {
|
|
226
226
|
const panes = new Map<string, Pane<State>>()
|
|
227
|
+
const paneLogs = new Map<string, (failure: string) => void>()
|
|
227
228
|
const openPanes = new Set<string>()
|
|
228
229
|
const requestedSizes = new Map<string, PaneSize>()
|
|
229
230
|
const renders = new Set<string>()
|
|
@@ -291,9 +292,10 @@ export function createUi<State extends object>({ name, claude, router, progress,
|
|
|
291
292
|
})
|
|
292
293
|
}
|
|
293
294
|
|
|
294
|
-
const openAt = (pane: Pane<State>, size: PaneSize) => {
|
|
295
|
+
const openAt = (pane: Pane<State>, size: PaneSize, focus?: true) => {
|
|
295
296
|
requestedSizes.set(pane.id, size)
|
|
296
|
-
|
|
297
|
+
const { id, title, closeOnEscape, holdToasts } = pane
|
|
298
|
+
return claude.ui.open({ id, title, ...size, ...(closeOnEscape === true ? { closeOnEscape } : {}), ...(holdToasts === true ? { holdToasts } : {}), ...(focus === true ? { focus } : {}) })
|
|
297
299
|
}
|
|
298
300
|
|
|
299
301
|
const resize = async (pane: Pane<State>) => {
|
|
@@ -315,34 +317,58 @@ export function createUi<State extends object>({ name, claude, router, progress,
|
|
|
315
317
|
if (pane === undefined || e.component !== 'Pane') return next(e)
|
|
316
318
|
openPanes.add(pane.id)
|
|
317
319
|
const table = claude.ui.resolve(e)
|
|
318
|
-
|
|
320
|
+
try {
|
|
321
|
+
return drawWith(table, () => pane.render(mod(), e.props), markdownIn(table))
|
|
322
|
+
} catch (error) {
|
|
323
|
+
paneLogs.get(pane.id)?.(`threw: ${messageOf(error)}`)
|
|
324
|
+
return drawWith(table, () => Text({ color: 'error', children: `The ${pane.title} pane could not draw: ${messageOf(error)}` }))
|
|
325
|
+
}
|
|
326
|
+
})
|
|
327
|
+
router.add('ui.scroll', async (e, next) => {
|
|
328
|
+
const pane = panes.get(e.requestId)
|
|
329
|
+
const moved = await next(e)
|
|
330
|
+
if (pane?.onScroll === undefined || e.component !== 'Pane' || moved.deny !== undefined) return moved
|
|
331
|
+
await handle(pane, 'onScroll', () => pane.onScroll?.(mod(), e))
|
|
332
|
+
return moved
|
|
319
333
|
})
|
|
320
|
-
router.add('ui.close', (e, next) => {
|
|
334
|
+
router.add('ui.close', async (e, next) => {
|
|
321
335
|
openPanes.delete(e.id)
|
|
322
|
-
|
|
336
|
+
const closed = await next(e)
|
|
337
|
+
const pane = panes.get(e.id)
|
|
338
|
+
if (pane?.onClose !== undefined) await handle(pane, 'onClose', () => pane.onClose?.(mod(), e))
|
|
339
|
+
return closed
|
|
323
340
|
})
|
|
324
341
|
}
|
|
325
342
|
|
|
343
|
+
const handle = async (pane: Pane<State>, handler: string, run: () => unknown) => {
|
|
344
|
+
try {
|
|
345
|
+
await run()
|
|
346
|
+
} catch (error) {
|
|
347
|
+
claude.ui.log(`${name}: the ${pane.title} pane's ${handler} threw: ${messageOf(error)}`)
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
326
351
|
return {
|
|
327
352
|
ui: {
|
|
328
353
|
pane(pane) {
|
|
329
354
|
if (panes.has(pane.id)) throw new Error(`${name}: the pane "${pane.id}" is already added. Give each pane its own id.`)
|
|
330
355
|
panes.set(pane.id, pane)
|
|
356
|
+
paneLogs.set(pane.id, logOnce(`${pane.title} pane`))
|
|
331
357
|
announce(`the ${pane.title} pane`)
|
|
332
358
|
route()
|
|
333
359
|
const handle: PaneHandle = {
|
|
334
360
|
get isOpen() {
|
|
335
361
|
return openPanes.has(pane.id)
|
|
336
362
|
},
|
|
337
|
-
async open() {
|
|
338
|
-
const { isPlaced } = await openAt(pane, paneSize(pane, mod().state))
|
|
363
|
+
async open(options) {
|
|
364
|
+
const { isPlaced } = await openAt(pane, paneSize(pane, mod().state), options?.focus)
|
|
339
365
|
if (isPlaced) openPanes.add(pane.id)
|
|
340
366
|
},
|
|
341
367
|
async close() {
|
|
342
368
|
await claude.ui.close({ id: pane.id })
|
|
343
369
|
openPanes.delete(pane.id)
|
|
344
370
|
},
|
|
345
|
-
toggle: () => (openPanes.has(pane.id) ? handle.close() : handle.open()),
|
|
371
|
+
toggle: (options) => (openPanes.has(pane.id) ? handle.close() : handle.open(options)),
|
|
346
372
|
}
|
|
347
373
|
return handle
|
|
348
374
|
},
|
|
@@ -386,6 +412,7 @@ export function createUi<State extends object>({ name, claude, router, progress,
|
|
|
386
412
|
}
|
|
387
413
|
},
|
|
388
414
|
ask: (question, options) => claude.ui.ask(question, options),
|
|
415
|
+
scroll: (args) => claude.ui.scroll(args),
|
|
389
416
|
},
|
|
390
417
|
changed() {
|
|
391
418
|
if (panes.size === 0 && renders.size === 0) return
|
package/src/shell.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { parseShell, type ParsedShell, type ShellCommand } from './utils/parse-shell.js'
|
|
@@ -12,8 +12,9 @@ export type Fakes = {
|
|
|
12
12
|
fs: { read?: Claude['fs']['read']; write?: Claude['fs']['write']; list?: Claude['fs']['list']; exists?: Claude['fs']['exists']; stat?: Claude['fs']['stat'] }
|
|
13
13
|
http: { fetch?: Claude['http']['fetch'] }
|
|
14
14
|
settings: { read?: Claude['settings']['read'] }
|
|
15
|
-
ui: { ask?: Claude['ui']['ask']; open?: Claude['ui']['open'] }
|
|
16
|
-
|
|
15
|
+
ui: { ask?: Claude['ui']['ask']; open?: Claude['ui']['open']; scroll?: Claude['ui']['scroll'] }
|
|
16
|
+
session: { messages?: Claude['session']['messages'] }
|
|
17
|
+
agent: { list?: Claude['agent']['list']; spawn?: Claude['agent']['spawn'] }
|
|
17
18
|
clock: { after?: Claude['clock']['after']; every?: Claude['clock']['every'] }
|
|
18
19
|
cmod: { call?: Claude['cmod']['call'] }
|
|
19
20
|
}
|
|
@@ -39,9 +40,10 @@ export type FakeClaude = {
|
|
|
39
40
|
|
|
40
41
|
export function fakeClaude(plugin: { readonly name: string; readonly root: string }): FakeClaude {
|
|
41
42
|
const calls: TestCall[] = []
|
|
42
|
-
const fakes: Fakes = { process: {}, fs: {}, http: {}, settings: {}, ui: {}, agent: {}, clock: {}, cmod: {} }
|
|
43
|
+
const fakes: Fakes = { process: {}, fs: {}, http: {}, settings: {}, ui: {}, session: {}, agent: {}, clock: {}, cmod: {} }
|
|
43
44
|
const shown: Shown = { toasts: [], logs: [], debug: [], statuses: [], openPanes: new Set(), commands: [], tools: [] }
|
|
44
45
|
const unplacedPanes = new Set<string>()
|
|
46
|
+
let focusedPane: string | undefined
|
|
45
47
|
const store = new Map<string, unknown>()
|
|
46
48
|
|
|
47
49
|
const faked = <Args extends readonly unknown[], Result>(call: string, answer: () => ((...args: Args) => Result) | undefined) => {
|
|
@@ -81,6 +83,7 @@ export function fakeClaude(plugin: { readonly name: string; readonly root: strin
|
|
|
81
83
|
if (answer.isPlaced) {
|
|
82
84
|
shown.openPanes.add(pane.id)
|
|
83
85
|
unplacedPanes.delete(pane.id)
|
|
86
|
+
if (pane.focus === true) focusedPane = pane.id
|
|
84
87
|
} else if (!shown.openPanes.has(pane.id)) {
|
|
85
88
|
unplacedPanes.add(pane.id)
|
|
86
89
|
}
|
|
@@ -90,11 +93,13 @@ export function fakeClaude(plugin: { readonly name: string; readonly root: strin
|
|
|
90
93
|
calls.push({ call: 'ui.close', args: [pane] })
|
|
91
94
|
shown.openPanes.delete(pane.id)
|
|
92
95
|
unplacedPanes.delete(pane.id)
|
|
96
|
+
if (focusedPane === pane.id) focusedPane = undefined
|
|
93
97
|
},
|
|
94
98
|
panes: async () => [
|
|
95
|
-
...[...shown.openPanes].map((id) => ({ id, title: id, isShown: true, isFocused:
|
|
99
|
+
...[...shown.openPanes].map((id) => ({ id, title: id, isShown: true, isFocused: id === focusedPane, isPlaced: true })),
|
|
96
100
|
...[...unplacedPanes].map((id) => ({ id, title: id, isShown: false, isFocused: false, isPlaced: false })),
|
|
97
101
|
],
|
|
102
|
+
scroll: rejected('ui.scroll', () => fakes.ui.scroll ?? (async () => ({}))),
|
|
98
103
|
resolve: () => elements,
|
|
99
104
|
},
|
|
100
105
|
process: {
|
|
@@ -110,7 +115,10 @@ export function fakeClaude(plugin: { readonly name: string; readonly root: strin
|
|
|
110
115
|
},
|
|
111
116
|
http: { fetch: rejected('http.fetch', () => fakes.http.fetch) },
|
|
112
117
|
settings: { read: rejected('settings.read', () => fakes.settings.read) },
|
|
113
|
-
agent: {
|
|
118
|
+
agent: {
|
|
119
|
+
list: rejected('agent.list', () => fakes.agent.list),
|
|
120
|
+
spawn: rejected('agent.spawn', () => fakes.agent.spawn),
|
|
121
|
+
},
|
|
114
122
|
store: {
|
|
115
123
|
get: async (key) => store.get(key),
|
|
116
124
|
set: async (key, value) => {
|
|
@@ -133,6 +141,7 @@ export function fakeClaude(plugin: { readonly name: string; readonly root: strin
|
|
|
133
141
|
model: async () => 'test-model',
|
|
134
142
|
usage: async () => ({ context: { window: 200000 }, rateLimits: {}, cost: { usd: 0 } }) as unknown as Awaited<ReturnType<Claude['session']['usage']>>,
|
|
135
143
|
surfaces: async () => ['terminal'],
|
|
144
|
+
messages: rejected('session.messages', () => fakes.session.messages as ((args?: unknown) => Promise<unknown>) | undefined) as Claude['session']['messages'],
|
|
136
145
|
},
|
|
137
146
|
command: {
|
|
138
147
|
register: async (command) => {
|