@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/runtime/grants.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { permissionWords } from '../records.js'
|
|
2
2
|
import { relativePath } from '../utils/paths.js'
|
|
3
3
|
import type { Claude } from './claude.js'
|
|
4
|
+
import { cmodPluginName } from './dependencies.js'
|
|
4
5
|
import type { RoutedEvent } from './hooks.js'
|
|
6
|
+
import { conversationMethods } from './sessions.js'
|
|
5
7
|
|
|
6
8
|
export type Grants = {
|
|
7
9
|
readonly name: string
|
|
@@ -12,24 +14,50 @@ export type Grants = {
|
|
|
12
14
|
readonly configRoot: string | undefined
|
|
13
15
|
readonly projectRoot: () => string
|
|
14
16
|
readonly freeFolders: () => readonly string[]
|
|
17
|
+
readonly freeAddresses: () => ReadonlySet<string>
|
|
15
18
|
}
|
|
16
19
|
|
|
17
20
|
export class PermissionRefused extends Error {}
|
|
18
21
|
|
|
19
|
-
const
|
|
22
|
+
const addressesOfPrograms = new WeakMap<object, Set<string>>()
|
|
23
|
+
|
|
24
|
+
export function programAddresses(mod: object): Set<string> {
|
|
25
|
+
const known = addressesOfPrograms.get(mod) ?? new Set<string>()
|
|
26
|
+
addressesOfPrograms.set(mod, known)
|
|
27
|
+
return known
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const projectSettingsFiles: readonly string[] = ['.claude/settings.json', '.claude/settings.local.json', '.mcp.json']
|
|
20
31
|
|
|
21
32
|
const startsTurns: readonly string[] = ['CronCreate']
|
|
22
33
|
|
|
23
34
|
const ownSchedule: readonly string[] = ['CronDelete', 'CronList']
|
|
24
35
|
|
|
25
|
-
|
|
36
|
+
const conversationCalls: Readonly<Record<string, string>> = {
|
|
37
|
+
[conversationMethods.sessions]: 'sessions.list',
|
|
38
|
+
[conversationMethods.search]: 'sessions.search',
|
|
39
|
+
[conversationMethods.messages]: 'messages',
|
|
40
|
+
[conversationMethods.turns]: 'turns',
|
|
41
|
+
[conversationMethods.contexts]: 'contexts',
|
|
42
|
+
[conversationMethods.tasks]: 'tasks',
|
|
43
|
+
[conversationMethods.goal]: 'goal',
|
|
44
|
+
[conversationMethods.updateGoal]: 'goal.update',
|
|
45
|
+
[conversationMethods.subscribe]: 'on',
|
|
46
|
+
[conversationMethods.send]: 'send',
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export async function checkGrant(grants: Grants, call: string, item: string | undefined): Promise<void> {
|
|
26
50
|
if (item === undefined) return
|
|
27
51
|
await grants.refresh()
|
|
28
52
|
if (!isCovered(grants.granted(), item, grants.home)) throw new PermissionRefused(refusal(grants, call, item))
|
|
29
53
|
}
|
|
30
54
|
|
|
31
|
-
export function checkWrite(grants: Grants, call: string, path: string): Promise<void> {
|
|
32
|
-
|
|
55
|
+
export async function checkWrite(grants: Grants, call: string, path: string): Promise<void> {
|
|
56
|
+
await checkGrant(grants, call, writeItem(grants, path))
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export async function checkSettings(grants: Grants, source: 'user' | 'project' | 'local', keys: readonly string[]): Promise<void> {
|
|
60
|
+
for (const key of keys) await checkGrant(grants, `settings.${source}.update(${key})`, `config:${key}`)
|
|
33
61
|
}
|
|
34
62
|
|
|
35
63
|
export function checkingGrants(claude: Claude, grants: Grants): Claude {
|
|
@@ -59,54 +87,58 @@ export function checkingGrants(claude: Claude, grants: Grants): Claude {
|
|
|
59
87
|
},
|
|
60
88
|
http: {
|
|
61
89
|
async fetch(url, init) {
|
|
62
|
-
await gate(`http.fetch(${url})`, `network:${init?.socketPath ?? hostOf(url)}`)
|
|
90
|
+
if (!grants.freeAddresses().has(init?.socketPath ?? addressOf(url))) await gate(`http.fetch(${url})`, `network:${init?.socketPath ?? hostOf(url)}`)
|
|
63
91
|
return claude.http.fetch(url, init)
|
|
64
92
|
},
|
|
65
93
|
},
|
|
66
|
-
config: {
|
|
67
|
-
...claude.config,
|
|
68
|
-
set: (async (args: Parameters<Claude['config']['set']>[0]) => {
|
|
69
|
-
await gate('config.set', 'config')
|
|
70
|
-
return claude.config.set(args)
|
|
71
|
-
}) as Claude['config']['set'],
|
|
72
|
-
},
|
|
73
94
|
session: {
|
|
74
95
|
...claude.session,
|
|
75
96
|
messages: (async (...args: Parameters<Claude['session']['messages']>) => {
|
|
76
97
|
await gate('session.messages', 'conversation')
|
|
77
98
|
return claude.session.messages(...args)
|
|
78
99
|
}) as Claude['session']['messages'],
|
|
79
|
-
append:
|
|
80
|
-
await gate('
|
|
100
|
+
append: async (args: Parameters<Claude['session']['append']>[0]) => {
|
|
101
|
+
await gate('send', 'prompt')
|
|
81
102
|
return claude.session.append(args)
|
|
82
|
-
}
|
|
103
|
+
},
|
|
104
|
+
send: async (args: Parameters<Claude['session']['send']>[0]) => {
|
|
105
|
+
await gate('send', 'prompt')
|
|
106
|
+
return claude.session.send(args)
|
|
107
|
+
},
|
|
83
108
|
},
|
|
84
109
|
prompt: {
|
|
85
|
-
submit:
|
|
86
|
-
await gate('
|
|
110
|
+
submit: async (args: Parameters<Claude['prompt']['submit']>[0]) => {
|
|
111
|
+
await gate('send', 'prompt')
|
|
87
112
|
return claude.prompt.submit(args)
|
|
88
|
-
}
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
cmod: {
|
|
116
|
+
async call(args) {
|
|
117
|
+
const named = args.to === cmodPluginName ? conversationCalls[args.method] : undefined
|
|
118
|
+
if (named !== undefined) await gate(named, args.method === conversationMethods.send ? 'prompt' : 'conversation')
|
|
119
|
+
return claude.cmod.call(args)
|
|
120
|
+
},
|
|
89
121
|
},
|
|
90
122
|
model: {
|
|
91
|
-
complete:
|
|
123
|
+
complete: async (...args: Parameters<Claude['model']['complete']>) => {
|
|
92
124
|
await gate('model.complete', 'model')
|
|
93
125
|
return claude.model.complete(...args)
|
|
94
|
-
}
|
|
126
|
+
},
|
|
95
127
|
},
|
|
96
128
|
agent: {
|
|
97
129
|
...claude.agent,
|
|
98
130
|
async spawn(args) {
|
|
99
|
-
await gate('
|
|
131
|
+
await gate('startTask', 'tasks')
|
|
100
132
|
return claude.agent.spawn(args)
|
|
101
133
|
},
|
|
102
134
|
},
|
|
103
135
|
tool: {
|
|
104
136
|
...claude.tool,
|
|
105
|
-
call:
|
|
137
|
+
call: async (input: Parameters<Claude['tool']['call']>[0]) => {
|
|
106
138
|
const item = startsTurns.includes(input.tool) ? 'prompt' : ownSchedule.includes(input.tool) ? undefined : 'tools'
|
|
107
139
|
await gate(`tool.call(${input.tool})`, item)
|
|
108
140
|
return claude.tool.call(input)
|
|
109
|
-
}
|
|
141
|
+
},
|
|
110
142
|
},
|
|
111
143
|
}
|
|
112
144
|
}
|
|
@@ -119,8 +151,8 @@ function spawned(started: Promise<Stream>): Stream {
|
|
|
119
151
|
const stream = {
|
|
120
152
|
next: async () => (await started).next(),
|
|
121
153
|
async return(value: never) {
|
|
122
|
-
const
|
|
123
|
-
return
|
|
154
|
+
const [start] = await Promise.allSettled([started])
|
|
155
|
+
return start.status === 'fulfilled' ? start.value.return(value) : { done: true as const, value }
|
|
124
156
|
},
|
|
125
157
|
[Symbol.asyncIterator]: () => stream,
|
|
126
158
|
result,
|
|
@@ -151,7 +183,10 @@ function readItem(grants: Grants, path: string): string | undefined {
|
|
|
151
183
|
|
|
152
184
|
function writeItem(grants: Grants, path: string): string | undefined {
|
|
153
185
|
const root = grants.projectRoot()
|
|
154
|
-
|
|
186
|
+
const settingsFiles = [...projectSettingsFiles.map((file) => `${root}/${file}`), ...(grants.configRoot === undefined ? [] : [`${grants.configRoot}/settings.json`])]
|
|
187
|
+
if (settingsFiles.includes(path)) {
|
|
188
|
+
throw new PermissionRefused(`${grants.name} writes ${path} by hand. Change Claude Code's settings with settings.user.update, settings.project.update, or settings.local.update, which keep the person's own edits and take the mod's values out when it is removed.`)
|
|
189
|
+
}
|
|
155
190
|
if (grants.freeFolders().some((folder) => relativePath(folder, path) !== undefined)) return undefined
|
|
156
191
|
return `files:${path}`
|
|
157
192
|
}
|
|
@@ -294,11 +329,11 @@ function programOf(argv: readonly string[]): string {
|
|
|
294
329
|
}
|
|
295
330
|
|
|
296
331
|
function hostOf(url: string): string {
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
332
|
+
return URL.canParse(url) ? new URL(url).hostname : url
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function addressOf(url: string): string {
|
|
336
|
+
return URL.canParse(url) ? new URL(url).host : url
|
|
302
337
|
}
|
|
303
338
|
|
|
304
339
|
function expanded(path: string, home: string | undefined): string {
|
package/src/runtime/hooks.ts
CHANGED
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
import type { Args, ClassicHookInputs, EventResult, Frozen } from 'claude-code'
|
|
2
|
-
import type { HookAnswer, HookInput,
|
|
2
|
+
import type { Hook, HookAnswer, HookInput, HookListener } 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 type { Failure } from './toaster.js'
|
|
6
7
|
import { reservedKeys, toolInputOf, type ToolCalls } from './tool-calls.js'
|
|
7
8
|
import { callEffects, inputOf, type CallBase, type FileAccess } from '../utils/call-effects.js'
|
|
8
9
|
import { dynamicPattern } from '../utils/parse-shell.js'
|
|
10
|
+
import { isMissing } from '../utils/paths.js'
|
|
9
11
|
import { messageOf } from '../utils/text.js'
|
|
10
12
|
|
|
11
13
|
export type RoutedEvent =
|
|
12
|
-
| `classic.${Exclude<
|
|
14
|
+
| `classic.${Exclude<Hook, 'PreToolUse'>}`
|
|
13
15
|
| 'tool.check'
|
|
14
16
|
| 'tool.call'
|
|
15
17
|
| 'prompt.submit'
|
|
@@ -22,6 +24,10 @@ export type RoutedEvent =
|
|
|
22
24
|
| 'ui.scroll'
|
|
23
25
|
| 'ui.close'
|
|
24
26
|
| 'cmod.call'
|
|
27
|
+
| 'session.append'
|
|
28
|
+
| 'session.compact'
|
|
29
|
+
| 'session.receive'
|
|
30
|
+
| 'agent.spawn'
|
|
25
31
|
|
|
26
32
|
export const permissionEvents: readonly RoutedEvent[] = ['tool.check', 'classic.PermissionRequest']
|
|
27
33
|
|
|
@@ -31,7 +37,7 @@ type CallFiles = HookInput<'PreToolUse'>['files']
|
|
|
31
37
|
|
|
32
38
|
const flags: readonly string[] = ['suppressOriginalPrompt', 'reloadSkills', 'retry']
|
|
33
39
|
|
|
34
|
-
const readFields: Record<
|
|
40
|
+
const readFields: Record<Hook, readonly string[]> = {
|
|
35
41
|
SessionStart: ['additionalContext', 'initialUserMessage', 'sessionTitle', 'watchPaths', 'reloadSkills'],
|
|
36
42
|
SessionEnd: [],
|
|
37
43
|
UserPromptSubmit: ['additionalContext', 'sessionTitle', 'suppressOriginalPrompt'],
|
|
@@ -52,7 +58,7 @@ const readFields: Record<ModEvent, readonly string[]> = {
|
|
|
52
58
|
FileChanged: [],
|
|
53
59
|
}
|
|
54
60
|
|
|
55
|
-
export function classicHook<E extends Exclude<
|
|
61
|
+
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> {
|
|
56
62
|
const hookInputOf = async (e: unknown): Promise<unknown> => {
|
|
57
63
|
if (event !== 'PostToolUse' && event !== 'PostToolUseFailure') return e
|
|
58
64
|
const input = e as ClassicHookInputs['PostToolUse' | 'PostToolUseFailure']
|
|
@@ -67,9 +73,8 @@ export function classicHook<E extends Exclude<ModEvent, 'PreToolUse'>>(name: str
|
|
|
67
73
|
}
|
|
68
74
|
const routed = async (e: unknown, next: (e: unknown) => Promise<unknown>) => {
|
|
69
75
|
const ours = await resultOf(e).catch((error: unknown): ClassicFields | undefined => {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
claude.ui.log(reason)
|
|
76
|
+
if (event === 'PermissionRequest') return { decision: { behavior: 'deny', message: `${name}: the ${event} hook failed: ${messageOf(error)}` } }
|
|
77
|
+
report({ id: `hook:${event}`, message: `The ${event} hook failed`, error })
|
|
73
78
|
return undefined
|
|
74
79
|
})
|
|
75
80
|
if (ours === undefined) return next(e)
|
|
@@ -82,7 +87,7 @@ type HeldDecision = { readonly decision: 'allow' | 'ask'; readonly reason?: stri
|
|
|
82
87
|
|
|
83
88
|
export type HeldDecisions = Map<string, HeldDecision>
|
|
84
89
|
|
|
85
|
-
export function preToolUseHook(name: string, hook:
|
|
90
|
+
export function preToolUseHook(name: string, hook: HookListener<'PreToolUse'>, claude: Claude, calls: ToolCalls, held: HeldDecisions | undefined): RoutedHook<'tool.call'> {
|
|
86
91
|
return async (e, next) => {
|
|
87
92
|
let ours: ClassicFields
|
|
88
93
|
try {
|
|
@@ -171,7 +176,11 @@ async function callFiles(name: string, claude: Claude, use: CallBase, cwd: strin
|
|
|
171
176
|
if (home === undefined) throw new Error('HOME is not set, so ~ in a path has no meaning.')
|
|
172
177
|
const { shell, reads, writes } = callEffects(use, { cwd, home, fs: claude.fs })
|
|
173
178
|
const known = (accesses: FileAccess[]) => accesses.map(({ path }) => path).filter((path) => shell === undefined || !dynamicPattern.test(path))
|
|
174
|
-
const
|
|
179
|
+
const unchecked = (path: string) => (error: unknown) => {
|
|
180
|
+
if (!isMissing(error)) claude.ui.log(`${name}: the ${use.tool} call reads ${path}, which could not be checked: ${messageOf(error)}`, { to: 'debug' })
|
|
181
|
+
return undefined
|
|
182
|
+
}
|
|
183
|
+
const read = await Promise.all(known(reads).map(async (path) => ((await claude.fs.stat(path).catch(unchecked(path)))?.kind === 'file' ? [path] : [])))
|
|
175
184
|
return { read: read.flat(), changed: known(writes) }
|
|
176
185
|
} catch (error) {
|
|
177
186
|
claude.ui.log(`${name}: the ${use.tool} call lists no files: ${messageOf(error)}`, { to: 'debug' })
|
|
@@ -179,7 +188,7 @@ async function callFiles(name: string, claude: Claude, use: CallBase, cwd: strin
|
|
|
179
188
|
}
|
|
180
189
|
}
|
|
181
190
|
|
|
182
|
-
function classicResult(event:
|
|
191
|
+
function classicResult(event: Hook, answer: HookAnswer): ClassicFields {
|
|
183
192
|
const result: ClassicFields = {}
|
|
184
193
|
const unread = (field: string) => new Error(`it answered ${field}, which ${event} does not read. Remove it from the answer.`)
|
|
185
194
|
if (event === 'PreToolUse') {
|
package/src/runtime/installer.ts
CHANGED
|
@@ -102,12 +102,12 @@ export function createInstaller(name: string, claude: () => Claude): Installer {
|
|
|
102
102
|
`${name} needs ${missing.length === 1 ? 'one setting' : `${missing.length} settings`}:`,
|
|
103
103
|
missing.map(([key, option]) => {
|
|
104
104
|
const label = Text({ children: [Text({ bold: true, children: option.title }), option.description === '' ? '' : Text({ dimColor: true, children: ` ${option.description}` })] })
|
|
105
|
-
if (option.
|
|
105
|
+
if (option.type === 'secret') return Box({ flexDirection: 'column', children: [label, Text({ dimColor: true, children: `Set it in /config, where Claude Code keeps it in your keychain, then press Next.` })] })
|
|
106
106
|
const failure = failures.get(key)
|
|
107
107
|
const field = Input({
|
|
108
108
|
key: `option:${key}`,
|
|
109
109
|
label: '› ',
|
|
110
|
-
placeholder: option.
|
|
110
|
+
placeholder: option.type === 'choice' ? (option.choices ?? []).join(', ') : option.type === 'toggle' ? 'true or false' : option.type === 'list' ? 'comma-separated' : '',
|
|
111
111
|
value: saved.get(key) ?? '',
|
|
112
112
|
submitLabel: 'save',
|
|
113
113
|
onSubmit: (value: string) =>
|
|
@@ -131,7 +131,7 @@ export function createInstaller(name: string, claude: () => Claude): Installer {
|
|
|
131
131
|
)
|
|
132
132
|
},
|
|
133
133
|
step(mod, step, index, total) {
|
|
134
|
-
let notice: string | undefined
|
|
134
|
+
let notice: { readonly color: 'warning' | 'error'; readonly text: string } | undefined
|
|
135
135
|
return show<boolean>(
|
|
136
136
|
(answer) => ({
|
|
137
137
|
draw: (props) => {
|
|
@@ -142,16 +142,17 @@ export function createInstaller(name: string, claude: () => Claude): Installer {
|
|
|
142
142
|
body = Text({ color: 'error', children: `The ${step.title} step could not draw: ${messageOf(error)}` })
|
|
143
143
|
}
|
|
144
144
|
const next = async () => {
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
145
|
+
try {
|
|
146
|
+
if (await step.isDone(mod)) return answer(true)
|
|
147
|
+
notice = { color: 'warning', text: `Finish ${step.title} first.` }
|
|
148
|
+
} catch (error) {
|
|
149
|
+
notice = { color: 'error', text: `The ${step.title} step's isDone failed: ${messageOf(error)}` }
|
|
150
150
|
}
|
|
151
|
+
claude().ui.invalidate('ui.render')
|
|
151
152
|
}
|
|
152
153
|
return frame(
|
|
153
154
|
`${step.title} (${index} of ${total})`,
|
|
154
|
-
[body, ...(notice === undefined ? [] : [Text({ color:
|
|
155
|
+
[body, ...(notice === undefined ? [] : [Text({ color: notice.color, children: notice.text })])],
|
|
155
156
|
[Button({ key: 'not-now', hotkey: 'n', label: 'Not now', onPress: () => answer(false) }), Button({ key: 'next', hotkey: 'x', label: index === total ? 'Finish' : 'Next', onPress: () => void next() })],
|
|
156
157
|
)
|
|
157
158
|
},
|