@cmodjs/core 1.6.0 → 2.0.1
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 +7 -7
- package/docs/dependencies.md +1 -1
- package/docs/index.md +12 -11
- package/docs/jobs.md +12 -16
- package/docs/mod.md +111 -36
- package/docs/permissions.md +14 -18
- package/docs/session.md +171 -82
- package/docs/state.md +5 -5
- package/docs/testing.md +57 -41
- package/docs/ui.md +9 -9
- package/jobs/permissions/decide-permission.d.ts.map +1 -1
- package/jobs/permissions/decide-permission.js +1 -1
- package/jobs/prompt.d.ts +1 -3
- package/jobs/prompt.d.ts.map +1 -1
- package/jobs/prompt.js +2 -21
- package/jobs/slash-command.d.ts +1 -1
- package/jobs/slash-command.d.ts.map +1 -1
- package/jobs/slash-command.js +1 -1
- package/jobs/status-line.d.ts +0 -1
- package/jobs/status-line.d.ts.map +1 -1
- package/jobs/status-line.js +3 -4
- package/mod.d.ts +53 -105
- package/mod.d.ts.map +1 -1
- package/mod.js +18 -80
- package/package.json +1 -1
- package/records.d.ts +1 -1
- package/records.js +1 -1
- package/register.d.ts.map +1 -1
- package/register.js +8 -5
- package/runtime/before.d.ts +11 -4
- package/runtime/before.d.ts.map +1 -1
- package/runtime/before.js +131 -67
- package/runtime/claude.d.ts +6 -1
- package/runtime/claude.d.ts.map +1 -1
- package/runtime/conversations.d.ts.map +1 -1
- package/runtime/conversations.js +82 -38
- package/runtime/deadline.d.ts +17 -0
- package/runtime/deadline.d.ts.map +1 -1
- package/runtime/deadline.js +34 -0
- package/runtime/dependencies.d.ts +2 -0
- package/runtime/dependencies.d.ts.map +1 -1
- package/runtime/dependencies.js +12 -0
- package/runtime/grants.d.ts.map +1 -1
- package/runtime/grants.js +13 -8
- package/runtime/hooks.d.ts +15 -11
- package/runtime/hooks.d.ts.map +1 -1
- package/runtime/hooks.js +8 -165
- package/runtime/lifecycle.d.ts +4 -2
- package/runtime/lifecycle.d.ts.map +1 -1
- package/runtime/lifecycle.js +96 -62
- package/runtime/permission-check.d.ts +15 -0
- package/runtime/permission-check.d.ts.map +1 -0
- package/runtime/permission-check.js +37 -0
- package/runtime/sessions.d.ts +35 -5
- package/runtime/sessions.d.ts.map +1 -1
- package/runtime/sessions.js +304 -82
- package/runtime/state.d.ts.map +1 -1
- package/runtime/state.js +14 -11
- package/runtime/tool-calls.d.ts +0 -1
- package/runtime/tool-calls.d.ts.map +1 -1
- package/runtime/tool-calls.js +0 -19
- package/session/context/turn/message/instruction-file.d.ts +9 -0
- package/session/context/turn/message/instruction-file.d.ts.map +1 -0
- package/session/context/turn/message/instruction-file.js +4 -0
- package/session/context/turn/message/message.d.ts +6 -4
- package/session/context/turn/message/message.d.ts.map +1 -1
- package/session/context/turn/message/message.js +12 -8
- package/session/context/turn/message/sender.d.ts +2 -1
- package/session/context/turn/message/sender.d.ts.map +1 -1
- package/session/context/turn/message/sender.js +4 -1
- package/session/context/turn/message/tool-call.d.ts +5 -0
- package/session/context/turn/message/tool-call.d.ts.map +1 -1
- package/session/context/turn/message/tool-result.d.ts +6 -1
- package/session/context/turn/message/tool-result.d.ts.map +1 -1
- package/session/context/turn/turn.d.ts +7 -0
- package/session/context/turn/turn.d.ts.map +1 -1
- package/session/conversation.d.ts +10 -3
- package/session/conversation.d.ts.map +1 -1
- package/session/conversation.js +14 -5
- package/session/goal/goal.d.ts +1 -1
- package/session/goal/goal.d.ts.map +1 -1
- package/session/goal/task.d.ts +2 -2
- package/session/goal/task.d.ts.map +1 -1
- package/session/session.d.ts +361 -26
- package/session/session.d.ts.map +1 -1
- package/session/session.js +40 -0
- package/src/jobs/permissions/decide-permission.ts +2 -1
- package/src/jobs/prompt.ts +3 -20
- package/src/jobs/slash-command.ts +2 -2
- package/src/jobs/status-line.ts +4 -7
- package/src/mod.ts +52 -147
- package/src/records.ts +1 -1
- package/src/register.ts +8 -5
- package/src/runtime/before.ts +140 -66
- package/src/runtime/claude.ts +5 -1
- package/src/runtime/conversations.ts +75 -37
- package/src/runtime/deadline.ts +38 -0
- package/src/runtime/dependencies.ts +12 -0
- package/src/runtime/grants.ts +11 -7
- package/src/runtime/hooks.ts +24 -163
- package/src/runtime/lifecycle.ts +110 -77
- package/src/runtime/permission-check.ts +47 -0
- package/src/runtime/sessions.ts +334 -91
- package/src/runtime/state.ts +15 -8
- package/src/runtime/tool-calls.ts +0 -19
- package/src/session/context/turn/message/instruction-file.ts +13 -0
- package/src/session/context/turn/message/message.ts +17 -11
- package/src/session/context/turn/message/sender.ts +5 -2
- package/src/session/context/turn/message/tool-call.ts +3 -0
- package/src/session/context/turn/message/tool-result.ts +6 -1
- package/src/session/context/turn/turn.ts +4 -0
- package/src/session/conversation.ts +21 -8
- package/src/session/goal/goal.ts +1 -1
- package/src/session/goal/task.ts +2 -2
- package/src/session/session.ts +227 -24
- package/src/testing/claude-code.ts +23 -0
- package/src/testing/fake-claude.ts +5 -1
- package/src/testing/fake-sessions.ts +38 -24
- package/src/testing.ts +88 -19
- package/testing/claude-code.d.ts +12 -0
- package/testing/claude-code.d.ts.map +1 -0
- package/testing/claude-code.js +10 -0
- package/testing/fake-claude.d.ts +4 -0
- package/testing/fake-claude.d.ts.map +1 -1
- package/testing/fake-claude.js +4 -1
- package/testing/fake-sessions.d.ts +8 -6
- package/testing/fake-sessions.d.ts.map +1 -1
- package/testing/fake-sessions.js +35 -20
- package/testing.d.ts +38 -9
- package/testing.d.ts.map +1 -1
- package/testing.js +58 -8
- package/docs/hooks.md +0 -259
- package/session/context/turn/message/document.d.ts +0 -7
- package/session/context/turn/message/document.d.ts.map +0 -1
- package/session/context/turn/message/document.js +0 -1
- package/src/session/context/turn/message/document.ts +0 -8
package/src/runtime/grants.ts
CHANGED
|
@@ -90,10 +90,6 @@ export function checkingGrants(claude: Claude, grants: Grants): Claude {
|
|
|
90
90
|
},
|
|
91
91
|
session: {
|
|
92
92
|
...claude.session,
|
|
93
|
-
messages: (async (...args: Parameters<Claude['session']['messages']>) => {
|
|
94
|
-
await gate('session.messages', 'conversation')
|
|
95
|
-
return claude.session.messages(...args)
|
|
96
|
-
}) as Claude['session']['messages'],
|
|
97
93
|
append: async (args: Parameters<Claude['session']['append']>[0]) => {
|
|
98
94
|
await gate('send', 'prompt')
|
|
99
95
|
return claude.session.append(args)
|
|
@@ -109,12 +105,22 @@ export function checkingGrants(claude: Claude, grants: Grants): Claude {
|
|
|
109
105
|
return claude.prompt.submit(args)
|
|
110
106
|
},
|
|
111
107
|
},
|
|
108
|
+
command: {
|
|
109
|
+
...claude.command,
|
|
110
|
+
run: async (args: Parameters<Claude['command']['run']>[0]) => {
|
|
111
|
+
await gate(`commands.run(${args.command})`, 'prompt')
|
|
112
|
+
return claude.command.run(args)
|
|
113
|
+
},
|
|
114
|
+
},
|
|
112
115
|
cmod: {
|
|
113
116
|
async call(args) {
|
|
114
117
|
const named = args.to === cmodPluginName ? conversationCalls[args.method] : undefined
|
|
115
118
|
const given = args.method === conversationMethods.act && isObject(args.input) ? String(args.input['action']) : undefined
|
|
116
119
|
const action = given !== undefined && isAct(given) ? given : undefined
|
|
117
|
-
|
|
120
|
+
const isThisSession = isObject(args.input) && args.input['session'] === (await claude.session.id())
|
|
121
|
+
const isDropping = (args.method === conversationMethods.subscribe || args.method === conversationMethods.watchBefore) && isObject(args.input) && Array.isArray(args.input['subscriptions']) && args.input['subscriptions'].length === 0
|
|
122
|
+
if (named !== undefined && action !== undefined) await gate(action, actPermission(action, isObject(args.input) ? args.input['input'] : undefined))
|
|
123
|
+
else if (named !== undefined && !isThisSession && !isDropping) await gate(named, 'conversation')
|
|
118
124
|
return claude.cmod.call(args)
|
|
119
125
|
},
|
|
120
126
|
},
|
|
@@ -211,10 +217,8 @@ type Fields = Record<string, unknown>
|
|
|
211
217
|
const classicParts: Readonly<Record<string, string>> = {
|
|
212
218
|
additionalContext: 'prompt',
|
|
213
219
|
initialUserMessage: 'prompt',
|
|
214
|
-
suppressOriginalPrompt: 'prompt',
|
|
215
220
|
updatedToolOutput: 'prompt',
|
|
216
221
|
updatedMCPToolOutput: 'prompt',
|
|
217
|
-
updatedInput: 'tools',
|
|
218
222
|
retry: 'approve',
|
|
219
223
|
}
|
|
220
224
|
|
package/src/runtime/hooks.ts
CHANGED
|
@@ -1,17 +1,21 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import type { Hook, HookAnswer, HookInput, HookListener } from '../mod.js'
|
|
1
|
+
import type { ClassicHookEvent, ClassicHookInputs } from 'claude-code'
|
|
3
2
|
import { configFolders } from '../records.js'
|
|
4
3
|
import type { Claude } from './claude.js'
|
|
5
4
|
import type { RoutedHook } from './router.js'
|
|
6
5
|
import type { Failure } from './toaster.js'
|
|
7
|
-
import {
|
|
8
|
-
import { callEffects, inputOf, type CallBase, type FileAccess } from '../utils/call-effects.js'
|
|
6
|
+
import { callEffects, type CallBase, type FileAccess } from '../utils/call-effects.js'
|
|
9
7
|
import { dynamicPattern } from '../utils/parse-shell.js'
|
|
10
8
|
import { isMissing } from '../utils/paths.js'
|
|
11
9
|
import { messageOf } from '../utils/text.js'
|
|
12
10
|
|
|
11
|
+
export type Hook = Exclude<ClassicHookEvent, 'PreToolUse'>
|
|
12
|
+
|
|
13
|
+
export type HookFields = Readonly<Record<string, unknown>>
|
|
14
|
+
|
|
15
|
+
export type HookListener<H extends Hook> = (input: ClassicHookInputs[H]) => HookFields | undefined | Promise<HookFields | undefined>
|
|
16
|
+
|
|
13
17
|
export type RoutedEvent =
|
|
14
|
-
| `classic.${
|
|
18
|
+
| `classic.${Hook}`
|
|
15
19
|
| 'tool.check'
|
|
16
20
|
| 'tool.call'
|
|
17
21
|
| 'prompt.submit'
|
|
@@ -29,125 +33,26 @@ export type RoutedEvent =
|
|
|
29
33
|
| 'session.compact'
|
|
30
34
|
| 'session.receive'
|
|
31
35
|
| 'agent.spawn'
|
|
36
|
+
| 'turn.start'
|
|
32
37
|
|
|
33
38
|
export const permissionEvents: readonly RoutedEvent[] = ['tool.check', 'classic.PermissionRequest']
|
|
34
39
|
|
|
35
|
-
type
|
|
36
|
-
|
|
37
|
-
type CallFiles = HookInput<'PreToolUse'>['files']
|
|
38
|
-
|
|
39
|
-
const flags: readonly string[] = ['suppressOriginalPrompt', 'reloadSkills', 'retry']
|
|
40
|
+
type CallFiles = { readonly read: string[]; readonly changed: string[] }
|
|
40
41
|
|
|
41
|
-
|
|
42
|
-
Setup: ['additionalContext'],
|
|
43
|
-
SessionStart: ['additionalContext', 'initialUserMessage', 'sessionTitle', 'watchPaths', 'reloadSkills'],
|
|
44
|
-
SessionEnd: [],
|
|
45
|
-
UserPromptSubmit: ['additionalContext', 'sessionTitle', 'suppressOriginalPrompt'],
|
|
46
|
-
UserPromptExpansion: ['additionalContext', 'suppressOriginalPrompt'],
|
|
47
|
-
InstructionsLoaded: [],
|
|
48
|
-
ConfigChange: [],
|
|
49
|
-
DirectoryAdded: [],
|
|
50
|
-
PreModelSwitch: ['permissionDecision', 'permissionDecisionReason'],
|
|
51
|
-
PostModelSwitch: ['additionalContext'],
|
|
52
|
-
PreToolUse: ['additionalContext', 'permissionDecision', 'permissionDecisionReason', 'updatedInput'],
|
|
53
|
-
PermissionRequest: ['decision'],
|
|
54
|
-
PermissionDenied: ['retry'],
|
|
55
|
-
PostToolUse: ['additionalContext', 'updatedToolOutput', 'updatedMCPToolOutput'],
|
|
56
|
-
PostToolUseFailure: ['additionalContext'],
|
|
57
|
-
PostToolBatch: ['additionalContext'],
|
|
58
|
-
MessageDisplay: ['displayContent'],
|
|
59
|
-
SubagentStart: ['additionalContext'],
|
|
60
|
-
SubagentStop: ['additionalContext'],
|
|
61
|
-
TaskCreated: [],
|
|
62
|
-
TaskCompleted: [],
|
|
63
|
-
TeammateIdle: [],
|
|
64
|
-
Elicitation: [],
|
|
65
|
-
ElicitationResult: [],
|
|
66
|
-
Notification: [],
|
|
67
|
-
PreCompact: [],
|
|
68
|
-
PostCompact: [],
|
|
69
|
-
Stop: ['additionalContext'],
|
|
70
|
-
StopFailure: [],
|
|
71
|
-
CwdChanged: [],
|
|
72
|
-
FileChanged: [],
|
|
73
|
-
WorktreeCreate: ['worktreePath'],
|
|
74
|
-
WorktreeRemove: [],
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
export function classicHook<E extends Exclude<Hook, 'PreToolUse'>>(name: string, event: E, hook: HookListener<E>, claude: Claude, calls: ToolCalls, report: (failure: Failure) => void): RoutedHook<RoutedEvent> {
|
|
78
|
-
const hookInputOf = async (e: unknown): Promise<unknown> => {
|
|
79
|
-
if (event !== 'PostToolUse' && event !== 'PostToolUseFailure') return e
|
|
80
|
-
const input = e as ClassicHookInputs['PostToolUse' | 'PostToolUseFailure']
|
|
81
|
-
const cwd = calls.cwdOf(input.tool_use_id) ?? input.cwd
|
|
82
|
-
return { ...input, files: await callFiles(name, claude, { tool: input.tool_name, input: inputOf(input.tool_input) }, cwd) }
|
|
83
|
-
}
|
|
84
|
-
const resultOf = async (e: unknown): Promise<ClassicFields | undefined> => {
|
|
85
|
-
const answer = await hook((await hookInputOf(e)) as HookInput<E>)
|
|
86
|
-
if (answer === undefined) return undefined
|
|
87
|
-
if (answer.systemMessage !== undefined) claude.ui.log(answer.systemMessage)
|
|
88
|
-
return classicResult(event, answer)
|
|
89
|
-
}
|
|
42
|
+
export function classicHook<H extends Hook>(event: H, hook: HookListener<H>, report: (failure: Failure) => void): RoutedHook<RoutedEvent> {
|
|
90
43
|
const routed = async (e: unknown, next: (e: unknown) => Promise<unknown>) => {
|
|
91
|
-
const ours = await
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
44
|
+
const ours = await Promise.resolve()
|
|
45
|
+
.then(() => hook(e as ClassicHookInputs[H]))
|
|
46
|
+
.catch((error: unknown) => {
|
|
47
|
+
report({ id: `hook:${event}`, message: `The ${event} listener failed`, error })
|
|
48
|
+
return undefined
|
|
49
|
+
})
|
|
96
50
|
if (ours === undefined) return next(e)
|
|
97
|
-
return mergeClassic((await next(e)) as
|
|
51
|
+
return mergeClassic((await next(e)) as HookFields, ours)
|
|
98
52
|
}
|
|
99
53
|
return routed as unknown as RoutedHook<RoutedEvent>
|
|
100
54
|
}
|
|
101
55
|
|
|
102
|
-
type HeldDecision = { readonly decision: 'allow' | 'ask'; readonly reason?: string }
|
|
103
|
-
|
|
104
|
-
export type HeldDecisions = Map<string, HeldDecision>
|
|
105
|
-
|
|
106
|
-
export function preToolUseHook(name: string, hook: HookListener<'PreToolUse'>, claude: Claude, calls: ToolCalls, held: HeldDecisions | undefined): RoutedHook<'tool.call'> {
|
|
107
|
-
return async (e, next) => {
|
|
108
|
-
let ours: ClassicFields
|
|
109
|
-
try {
|
|
110
|
-
const answer = await hook(await preToolUseInput(name, e, claude, calls.agentOf))
|
|
111
|
-
if (answer?.systemMessage !== undefined) claude.ui.log(answer.systemMessage)
|
|
112
|
-
ours = answer === undefined ? {} : classicResult('PreToolUse', answer)
|
|
113
|
-
} catch (error) {
|
|
114
|
-
return { deny: `${name}: the PreToolUse hook failed: ${messageOf(error)}` }
|
|
115
|
-
}
|
|
116
|
-
if (typeof ours['deny'] === 'string') return { deny: ours['deny'] }
|
|
117
|
-
const decided = heldDecisionOf(ours)
|
|
118
|
-
if (decided !== undefined && held === undefined) {
|
|
119
|
-
return { deny: `${name}: the PreToolUse hook answered permissionDecision "${decided.decision}", so hooks/register.ts must call registerPermissionCheck(addHook) after registerMod.` }
|
|
120
|
-
}
|
|
121
|
-
const updated = ours['updatedInput'] as Record<string, unknown> | undefined
|
|
122
|
-
const reserved = Object.fromEntries(Object.entries(e).filter(([key]) => reservedKeys.includes(key)))
|
|
123
|
-
const earlier = held?.get(e.tool_use_id)
|
|
124
|
-
if (decided !== undefined) held?.set(e.tool_use_id, earlier !== undefined && strictness.indexOf(earlier.decision) >= strictness.indexOf(decided.decision) ? earlier : decided)
|
|
125
|
-
let result: EventResult<'tool.call'>
|
|
126
|
-
try {
|
|
127
|
-
result = await next(updated === undefined ? e : ({ ...reserved, ...updated } as Frozen<Args<'tool.call'>>))
|
|
128
|
-
} finally {
|
|
129
|
-
if (decided !== undefined) held?.delete(e.tool_use_id)
|
|
130
|
-
}
|
|
131
|
-
const context = ours['additionalContext'] as string[] | undefined
|
|
132
|
-
if (context === undefined || result.deny !== undefined) return result
|
|
133
|
-
return { ...result, context: [...(result.context ?? []), ...context] }
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
export function heldDecisionHook(held: HeldDecisions): RoutedHook<'tool.check'> {
|
|
138
|
-
return async (e, next) => {
|
|
139
|
-
const below = await next(e)
|
|
140
|
-
const ours = e.tool_use_id === undefined ? undefined : held.get(e.tool_use_id)
|
|
141
|
-
return ours === undefined || below.decision === 'deny' ? below : ours
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
function heldDecisionOf(ours: ClassicFields): HeldDecision | undefined {
|
|
146
|
-
if (ours['allow'] !== undefined) return { decision: 'allow' }
|
|
147
|
-
if (typeof ours['ask'] !== 'string') return undefined
|
|
148
|
-
return ours['ask'] === '' ? { decision: 'ask' } : { decision: 'ask', reason: ours['ask'] }
|
|
149
|
-
}
|
|
150
|
-
|
|
151
56
|
const frontmatter = /^---\r?\n[\s\S]*?\r?\n---[ \t]*(?:\r?\n|$)/
|
|
152
57
|
|
|
153
58
|
const baseDirectoryLine = /^Base directory for this skill: [^\n]*\n\n/
|
|
@@ -169,26 +74,11 @@ export function userSkillHook(claude: Claude): RoutedHook<'skill.prompt'> {
|
|
|
169
74
|
}
|
|
170
75
|
}
|
|
171
76
|
|
|
172
|
-
|
|
173
|
-
const { tool, tool_use_id } = envelope
|
|
174
|
-
const [session_id, cwd, { agentId, agentType }] = await Promise.all([claude.session.id(), claude.session.cwd(), agentOf(tool_use_id)])
|
|
175
|
-
const tool_input = toolInputOf(envelope)
|
|
176
|
-
return {
|
|
177
|
-
session_id,
|
|
178
|
-
cwd,
|
|
179
|
-
hook_event_name: 'PreToolUse',
|
|
180
|
-
tool_name: tool,
|
|
181
|
-
tool_input,
|
|
182
|
-
tool_use_id,
|
|
183
|
-
...(agentId === undefined ? {} : { agent_id: agentId }),
|
|
184
|
-
...(agentType === undefined ? {} : { agent_type: agentType }),
|
|
185
|
-
files: await callFiles(name, claude, { tool, input: tool_input }, cwd),
|
|
186
|
-
}
|
|
187
|
-
}
|
|
77
|
+
export type HomeFolders = { readonly home: string | undefined; readonly configRoot: string | undefined }
|
|
188
78
|
|
|
189
|
-
async function callFiles(name: string, claude: Claude, use: CallBase, cwd: string): Promise<CallFiles> {
|
|
79
|
+
export async function callFiles(name: string, claude: Claude, folders: Promise<HomeFolders>, use: CallBase, cwd: string): Promise<CallFiles> {
|
|
190
80
|
try {
|
|
191
|
-
const
|
|
81
|
+
const { home, configRoot } = await folders
|
|
192
82
|
if (home === undefined) throw new Error('HOME is not set, so ~ in a path has no meaning.')
|
|
193
83
|
const { shell, reads, writes } = callEffects(use, { cwd, home, ...(configRoot === undefined ? {} : { configRoot }), fs: claude.fs })
|
|
194
84
|
const known = (accesses: FileAccess[]) => accesses.map(({ path }) => path).filter((path) => shell === undefined || !dynamicPattern.test(path))
|
|
@@ -204,37 +94,8 @@ async function callFiles(name: string, claude: Claude, use: CallBase, cwd: strin
|
|
|
204
94
|
}
|
|
205
95
|
}
|
|
206
96
|
|
|
207
|
-
function
|
|
208
|
-
const
|
|
209
|
-
const unread = (field: string) => new Error(`it answered ${field}, which ${event} does not read. Remove it from the answer.`)
|
|
210
|
-
if (event === 'PreToolUse') {
|
|
211
|
-
if (answer.continue === false || answer.stopReason !== undefined) throw unread('continue or stopReason')
|
|
212
|
-
if (answer.decision === 'block') result['deny'] = answer.reason ?? ''
|
|
213
|
-
} else {
|
|
214
|
-
if (answer.continue === false) result['preventContinuation'] = true
|
|
215
|
-
if (answer.stopReason !== undefined) result['stopReason'] = answer.stopReason
|
|
216
|
-
if (answer.decision === 'block') result['block'] = answer.reason ?? ''
|
|
217
|
-
}
|
|
218
|
-
const specific = answer.hookSpecificOutput ?? {}
|
|
219
|
-
if (specific.hookEventName !== undefined && specific.hookEventName !== event) {
|
|
220
|
-
throw new Error(`it answered hookSpecificOutput.hookEventName "${specific.hookEventName}". Set it to "${event}" or leave it out.`)
|
|
221
|
-
}
|
|
222
|
-
for (const [field, value] of Object.entries(specific)) {
|
|
223
|
-
if (field === 'hookEventName' || value === undefined || (value === false && flags.includes(field))) continue
|
|
224
|
-
if (!readFields[event].includes(field)) throw unread(`hookSpecificOutput.${field}`)
|
|
225
|
-
if (field === 'additionalContext') result['additionalContext'] = [value]
|
|
226
|
-
else if (field === 'permissionDecision' && event === 'PreToolUse') {
|
|
227
|
-
const reason = specific.permissionDecisionReason ?? ''
|
|
228
|
-
result[value as string] = value === 'allow' ? true : reason
|
|
229
|
-
} else if (field !== 'permissionDecisionReason' || event !== 'PreToolUse') result[field] = value
|
|
230
|
-
}
|
|
231
|
-
return result
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
export const strictness = ['allow', 'ask', 'deny'] as const
|
|
235
|
-
|
|
236
|
-
function mergeClassic(below: ClassicFields, ours: ClassicFields): ClassicFields {
|
|
237
|
-
const merged: ClassicFields = { ...below, ...ours }
|
|
97
|
+
function mergeClassic(below: HookFields, ours: HookFields): HookFields {
|
|
98
|
+
const merged: Record<string, unknown> = { ...below, ...ours }
|
|
238
99
|
const context = [...((below['additionalContext'] as string[] | undefined) ?? []), ...((ours['additionalContext'] as string[] | undefined) ?? [])]
|
|
239
100
|
if (context.length > 0) merged['additionalContext'] = context
|
|
240
101
|
return merged
|