@cmodjs/core 0.3.0 → 1.5.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 +61 -10
- package/docs/dependencies.md +16 -14
- package/docs/hooks.md +64 -29
- package/docs/index.md +29 -25
- package/docs/install-steps.md +13 -9
- package/docs/jobs.md +59 -111
- package/docs/mod.md +227 -175
- package/docs/options.md +2 -2
- package/docs/permissions.md +27 -16
- package/docs/session.md +311 -0
- package/docs/state.md +11 -7
- package/docs/testing.md +68 -23
- package/docs/ui.md +116 -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 +14 -4
- 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 +178 -75
- package/mod.d.ts.map +1 -1
- package/mod.js +127 -3
- package/options.d.ts +2 -2
- package/options.js +19 -19
- package/package.json +3 -3
- package/records.d.ts +13 -8
- package/records.d.ts.map +1 -1
- package/records.js +33 -17
- package/register.d.ts.map +1 -1
- package/register.js +34 -5
- package/runtime/announce.d.ts +18 -0
- package/runtime/announce.d.ts.map +1 -0
- package/runtime/announce.js +68 -0
- package/runtime/before.d.ts +32 -0
- package/runtime/before.d.ts.map +1 -0
- package/runtime/before.js +214 -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 +4 -2
- package/runtime/claude.d.ts.map +1 -1
- package/runtime/conversations.d.ts +17 -0
- package/runtime/conversations.d.ts.map +1 -0
- package/runtime/conversations.js +563 -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 +10 -1
- package/runtime/engine.d.ts +13 -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 +68 -31
- package/runtime/hooks.d.ts +5 -4
- package/runtime/hooks.d.ts.map +1 -1
- package/runtime/hooks.js +29 -9
- package/runtime/installer.d.ts +1 -1
- package/runtime/installer.d.ts.map +1 -1
- package/runtime/installer.js +17 -14
- package/runtime/lifecycle.d.ts +3 -1
- package/runtime/lifecycle.d.ts.map +1 -1
- package/runtime/lifecycle.js +414 -212
- package/runtime/metadata.d.ts +3 -1
- package/runtime/metadata.d.ts.map +1 -1
- package/runtime/metadata.js +25 -14
- 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 +58 -0
- package/runtime/sessions.d.ts.map +1 -0
- package/runtime/sessions.js +433 -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 +24 -0
- package/runtime/toaster.d.ts.map +1 -0
- package/runtime/toaster.js +286 -0
- package/runtime/ui.d.ts +6 -17
- package/runtime/ui.d.ts.map +1 -1
- package/runtime/ui.js +62 -118
- package/session/context/context.d.ts +12 -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 +55 -0
- package/session/context/turn/message/message.d.ts.map +1 -0
- package/session/context/turn/message/message.js +178 -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 +13 -0
- package/session/context/turn/turn.d.ts.map +1 -0
- package/session/context/turn/turn.js +2 -0
- package/session/conversation.d.ts +73 -0
- package/session/conversation.d.ts.map +1 -0
- package/session/conversation.js +146 -0
- package/session/goal/goal.d.ts +11 -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 +158 -0
- package/session/session.d.ts.map +1 -0
- package/session/session.js +2 -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 +13 -4
- 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 +252 -97
- package/src/options.ts +22 -22
- package/src/records.ts +40 -23
- package/src/register.ts +34 -5
- package/src/runtime/announce.ts +76 -0
- package/src/runtime/before.ts +200 -0
- package/src/runtime/bound.ts +12 -0
- package/src/runtime/claude.ts +7 -2
- package/src/runtime/conversations.ts +579 -0
- package/src/runtime/deadline.ts +2 -2
- package/src/runtime/dependencies.ts +13 -1
- package/src/runtime/engine.ts +23 -0
- package/src/runtime/grants.ts +69 -29
- package/src/runtime/hooks.ts +39 -14
- package/src/runtime/installer.ts +16 -14
- package/src/runtime/lifecycle.ts +413 -201
- package/src/runtime/metadata.ts +37 -14
- 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 +447 -0
- package/src/runtime/state.ts +50 -14
- package/src/runtime/toaster.ts +334 -0
- package/src/runtime/ui.ts +65 -143
- package/src/session/context/context.ts +14 -0
- package/src/session/context/turn/message/document.ts +8 -0
- package/src/session/context/turn/message/message.ts +203 -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 +15 -0
- package/src/session/conversation.ts +194 -0
- package/src/session/goal/goal.ts +10 -0
- package/src/session/goal/task.ts +19 -0
- package/src/session/session.ts +141 -0
- package/src/testing/fake-claude.ts +25 -17
- package/src/testing/fake-elements.ts +3 -2
- package/src/testing/fake-files.ts +1 -1
- package/src/testing/fake-sessions.ts +277 -0
- package/src/testing.ts +138 -35
- 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 +5 -5
- package/src/utils/changelog.ts +32 -0
- package/src/utils/disposable.ts +3 -0
- package/src/utils/matcher.ts +37 -0
- package/src/utils/metadata.ts +17 -15
- package/src/utils/parse-shell.ts +9 -8
- package/src/utils/paths.ts +21 -4
- package/src/utils/text.ts +4 -0
- package/testing/fake-claude.d.ts +5 -1
- package/testing/fake-claude.d.ts.map +1 -1
- package/testing/fake-claude.js +21 -12
- package/testing/fake-elements.js +4 -2
- package/testing/fake-files.js +1 -1
- package/testing/fake-sessions.d.ts +81 -0
- package/testing/fake-sessions.d.ts.map +1 -0
- package/testing/fake-sessions.js +249 -0
- package/testing.d.ts +34 -13
- package/testing.d.ts.map +1 -1
- package/testing.js +115 -23
- 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 +2 -1
- package/utils/call-effects.d.ts.map +1 -1
- package/utils/call-effects.js +3 -3
- package/utils/changelog.d.ts +9 -0
- package/utils/changelog.d.ts.map +1 -0
- package/utils/changelog.js +29 -0
- package/utils/disposable.d.ts +4 -0
- package/utils/disposable.d.ts.map +1 -0
- package/utils/disposable.js +1 -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.js +9 -7
- package/utils/paths.d.ts +4 -1
- package/utils/paths.d.ts.map +1 -1
- package/utils/paths.js +19 -4
- 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
|
-
import { permissionWords } from '../records.js'
|
|
1
|
+
import { isObject, 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 { actPermission, conversationMethods, isAct, toolPermission } from './sessions.js'
|
|
5
7
|
|
|
6
8
|
export type Grants = {
|
|
7
9
|
readonly name: string
|
|
@@ -12,24 +14,47 @@ 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>>()
|
|
20
23
|
|
|
21
|
-
|
|
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
|
+
}
|
|
22
29
|
|
|
23
|
-
const
|
|
30
|
+
const projectSettingsFiles: readonly string[] = ['.claude/settings.json', '.claude/settings.local.json', '.mcp.json']
|
|
31
|
+
|
|
32
|
+
const conversationCalls: Readonly<Record<string, string>> = {
|
|
33
|
+
[conversationMethods.sessions]: 'sessions.list',
|
|
34
|
+
[conversationMethods.search]: 'sessions.search',
|
|
35
|
+
[conversationMethods.messages]: 'messages',
|
|
36
|
+
[conversationMethods.turns]: 'turns',
|
|
37
|
+
[conversationMethods.contexts]: 'contexts',
|
|
38
|
+
[conversationMethods.tasks]: 'tasks',
|
|
39
|
+
[conversationMethods.goal]: 'goal',
|
|
40
|
+
[conversationMethods.updateGoal]: 'goal.update',
|
|
41
|
+
[conversationMethods.subscribe]: 'on',
|
|
42
|
+
[conversationMethods.watchBefore]: 'before',
|
|
43
|
+
[conversationMethods.act]: 'act',
|
|
44
|
+
}
|
|
24
45
|
|
|
25
|
-
async function checkGrant(grants: Grants, call: string, item: string | undefined): Promise<void> {
|
|
46
|
+
export async function checkGrant(grants: Grants, call: string, item: string | undefined): Promise<void> {
|
|
26
47
|
if (item === undefined) return
|
|
27
48
|
await grants.refresh()
|
|
28
49
|
if (!isCovered(grants.granted(), item, grants.home)) throw new PermissionRefused(refusal(grants, call, item))
|
|
29
50
|
}
|
|
30
51
|
|
|
31
|
-
export function checkWrite(grants: Grants, call: string, path: string): Promise<void> {
|
|
32
|
-
|
|
52
|
+
export async function checkWrite(grants: Grants, call: string, path: string): Promise<void> {
|
|
53
|
+
await checkGrant(grants, call, writeItem(grants, path))
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export async function checkSettings(grants: Grants, source: 'user' | 'project' | 'local', keys: readonly string[]): Promise<void> {
|
|
57
|
+
for (const key of keys) await checkGrant(grants, `settings.${source}.update(${key})`, `config:${key}`)
|
|
33
58
|
}
|
|
34
59
|
|
|
35
60
|
export function checkingGrants(claude: Claude, grants: Grants): Claude {
|
|
@@ -59,7 +84,7 @@ export function checkingGrants(claude: Claude, grants: Grants): Claude {
|
|
|
59
84
|
},
|
|
60
85
|
http: {
|
|
61
86
|
async fetch(url, init) {
|
|
62
|
-
await gate(`http.fetch(${url})`, `network:${init?.socketPath ?? hostOf(url)}`)
|
|
87
|
+
if (!grants.freeAddresses().has(init?.socketPath ?? addressOf(url))) await gate(`http.fetch(${url})`, `network:${init?.socketPath ?? hostOf(url)}`)
|
|
63
88
|
return claude.http.fetch(url, init)
|
|
64
89
|
},
|
|
65
90
|
},
|
|
@@ -69,37 +94,49 @@ export function checkingGrants(claude: Claude, grants: Grants): Claude {
|
|
|
69
94
|
await gate('session.messages', 'conversation')
|
|
70
95
|
return claude.session.messages(...args)
|
|
71
96
|
}) as Claude['session']['messages'],
|
|
72
|
-
append:
|
|
73
|
-
await gate('
|
|
97
|
+
append: async (args: Parameters<Claude['session']['append']>[0]) => {
|
|
98
|
+
await gate('send', 'prompt')
|
|
74
99
|
return claude.session.append(args)
|
|
75
|
-
}
|
|
100
|
+
},
|
|
101
|
+
send: async (args: Parameters<Claude['session']['send']>[0]) => {
|
|
102
|
+
await gate('send', 'prompt')
|
|
103
|
+
return claude.session.send(args)
|
|
104
|
+
},
|
|
76
105
|
},
|
|
77
106
|
prompt: {
|
|
78
|
-
submit:
|
|
79
|
-
await gate('
|
|
107
|
+
submit: async (args: Parameters<Claude['prompt']['submit']>[0]) => {
|
|
108
|
+
await gate('send', 'prompt')
|
|
80
109
|
return claude.prompt.submit(args)
|
|
81
|
-
}
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
cmod: {
|
|
113
|
+
async call(args) {
|
|
114
|
+
const named = args.to === cmodPluginName ? conversationCalls[args.method] : undefined
|
|
115
|
+
const given = args.method === conversationMethods.act && isObject(args.input) ? String(args.input['action']) : undefined
|
|
116
|
+
const action = given !== undefined && isAct(given) ? given : undefined
|
|
117
|
+
if (named !== undefined) await gate(action ?? named, action === undefined ? 'conversation' : actPermission(action, isObject(args.input) ? args.input['input'] : undefined))
|
|
118
|
+
return claude.cmod.call(args)
|
|
119
|
+
},
|
|
82
120
|
},
|
|
83
121
|
model: {
|
|
84
|
-
complete:
|
|
122
|
+
complete: async (...args: Parameters<Claude['model']['complete']>) => {
|
|
85
123
|
await gate('model.complete', 'model')
|
|
86
124
|
return claude.model.complete(...args)
|
|
87
|
-
}
|
|
125
|
+
},
|
|
88
126
|
},
|
|
89
127
|
agent: {
|
|
90
128
|
...claude.agent,
|
|
91
129
|
async spawn(args) {
|
|
92
|
-
await gate('
|
|
130
|
+
await gate('startTask', 'tasks')
|
|
93
131
|
return claude.agent.spawn(args)
|
|
94
132
|
},
|
|
95
133
|
},
|
|
96
134
|
tool: {
|
|
97
135
|
...claude.tool,
|
|
98
|
-
call:
|
|
99
|
-
|
|
100
|
-
await gate(`tool.call(${input.tool})`, item)
|
|
136
|
+
call: async (input: Parameters<Claude['tool']['call']>[0]) => {
|
|
137
|
+
await gate(`tool.call(${input.tool})`, toolPermission(input.tool))
|
|
101
138
|
return claude.tool.call(input)
|
|
102
|
-
}
|
|
139
|
+
},
|
|
103
140
|
},
|
|
104
141
|
}
|
|
105
142
|
}
|
|
@@ -112,8 +149,8 @@ function spawned(started: Promise<Stream>): Stream {
|
|
|
112
149
|
const stream = {
|
|
113
150
|
next: async () => (await started).next(),
|
|
114
151
|
async return(value: never) {
|
|
115
|
-
const
|
|
116
|
-
return
|
|
152
|
+
const [start] = await Promise.allSettled([started])
|
|
153
|
+
return start.status === 'fulfilled' ? start.value.return(value) : { done: true as const, value }
|
|
117
154
|
},
|
|
118
155
|
[Symbol.asyncIterator]: () => stream,
|
|
119
156
|
result,
|
|
@@ -144,7 +181,10 @@ function readItem(grants: Grants, path: string): string | undefined {
|
|
|
144
181
|
|
|
145
182
|
function writeItem(grants: Grants, path: string): string | undefined {
|
|
146
183
|
const root = grants.projectRoot()
|
|
147
|
-
|
|
184
|
+
const settingsFiles = [...projectSettingsFiles.map((file) => `${root}/${file}`), ...(grants.configRoot === undefined ? [] : [`${grants.configRoot}/settings.json`])]
|
|
185
|
+
if (settingsFiles.includes(path)) {
|
|
186
|
+
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.`)
|
|
187
|
+
}
|
|
148
188
|
if (grants.freeFolders().some((folder) => relativePath(folder, path) !== undefined)) return undefined
|
|
149
189
|
return `files:${path}`
|
|
150
190
|
}
|
|
@@ -287,11 +327,11 @@ function programOf(argv: readonly string[]): string {
|
|
|
287
327
|
}
|
|
288
328
|
|
|
289
329
|
function hostOf(url: string): string {
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
330
|
+
return URL.canParse(url) ? new URL(url).hostname : url
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function addressOf(url: string): string {
|
|
334
|
+
return URL.canParse(url) ? new URL(url).host : url
|
|
295
335
|
}
|
|
296
336
|
|
|
297
337
|
function expanded(path: string, home: string | undefined): string {
|
package/src/runtime/hooks.ts
CHANGED
|
@@ -1,19 +1,22 @@
|
|
|
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'
|
|
16
18
|
| 'prompt.context'
|
|
19
|
+
| 'prompt.attachment'
|
|
17
20
|
| 'command.run'
|
|
18
21
|
| 'session.measure'
|
|
19
22
|
| 'skill.prompt'
|
|
@@ -22,6 +25,10 @@ export type RoutedEvent =
|
|
|
22
25
|
| 'ui.scroll'
|
|
23
26
|
| 'ui.close'
|
|
24
27
|
| 'cmod.call'
|
|
28
|
+
| 'session.append'
|
|
29
|
+
| 'session.compact'
|
|
30
|
+
| 'session.receive'
|
|
31
|
+
| 'agent.spawn'
|
|
25
32
|
|
|
26
33
|
export const permissionEvents: readonly RoutedEvent[] = ['tool.check', 'classic.PermissionRequest']
|
|
27
34
|
|
|
@@ -31,28 +38,43 @@ type CallFiles = HookInput<'PreToolUse'>['files']
|
|
|
31
38
|
|
|
32
39
|
const flags: readonly string[] = ['suppressOriginalPrompt', 'reloadSkills', 'retry']
|
|
33
40
|
|
|
34
|
-
const readFields: Record<
|
|
41
|
+
const readFields: Record<Hook, readonly string[]> = {
|
|
42
|
+
Setup: ['additionalContext'],
|
|
35
43
|
SessionStart: ['additionalContext', 'initialUserMessage', 'sessionTitle', 'watchPaths', 'reloadSkills'],
|
|
36
44
|
SessionEnd: [],
|
|
37
45
|
UserPromptSubmit: ['additionalContext', 'sessionTitle', 'suppressOriginalPrompt'],
|
|
46
|
+
UserPromptExpansion: ['additionalContext', 'suppressOriginalPrompt'],
|
|
38
47
|
InstructionsLoaded: [],
|
|
48
|
+
ConfigChange: [],
|
|
49
|
+
DirectoryAdded: [],
|
|
50
|
+
PreModelSwitch: ['permissionDecision', 'permissionDecisionReason'],
|
|
51
|
+
PostModelSwitch: ['additionalContext'],
|
|
39
52
|
PreToolUse: ['additionalContext', 'permissionDecision', 'permissionDecisionReason', 'updatedInput'],
|
|
40
53
|
PermissionRequest: ['decision'],
|
|
41
54
|
PermissionDenied: ['retry'],
|
|
42
55
|
PostToolUse: ['additionalContext', 'updatedToolOutput', 'updatedMCPToolOutput'],
|
|
43
56
|
PostToolUseFailure: ['additionalContext'],
|
|
44
57
|
PostToolBatch: ['additionalContext'],
|
|
58
|
+
MessageDisplay: ['displayContent'],
|
|
45
59
|
SubagentStart: ['additionalContext'],
|
|
46
60
|
SubagentStop: ['additionalContext'],
|
|
61
|
+
TaskCreated: [],
|
|
62
|
+
TaskCompleted: [],
|
|
63
|
+
TeammateIdle: [],
|
|
64
|
+
Elicitation: [],
|
|
65
|
+
ElicitationResult: [],
|
|
47
66
|
Notification: [],
|
|
48
67
|
PreCompact: [],
|
|
68
|
+
PostCompact: [],
|
|
49
69
|
Stop: ['additionalContext'],
|
|
50
70
|
StopFailure: [],
|
|
51
71
|
CwdChanged: [],
|
|
52
72
|
FileChanged: [],
|
|
73
|
+
WorktreeCreate: ['worktreePath'],
|
|
74
|
+
WorktreeRemove: [],
|
|
53
75
|
}
|
|
54
76
|
|
|
55
|
-
export function classicHook<E extends Exclude<
|
|
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> {
|
|
56
78
|
const hookInputOf = async (e: unknown): Promise<unknown> => {
|
|
57
79
|
if (event !== 'PostToolUse' && event !== 'PostToolUseFailure') return e
|
|
58
80
|
const input = e as ClassicHookInputs['PostToolUse' | 'PostToolUseFailure']
|
|
@@ -67,9 +89,8 @@ export function classicHook<E extends Exclude<ModEvent, 'PreToolUse'>>(name: str
|
|
|
67
89
|
}
|
|
68
90
|
const routed = async (e: unknown, next: (e: unknown) => Promise<unknown>) => {
|
|
69
91
|
const ours = await resultOf(e).catch((error: unknown): ClassicFields | undefined => {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
claude.ui.log(reason)
|
|
92
|
+
if (event === 'PermissionRequest') return { decision: { behavior: 'deny', message: `${name}: the ${event} hook failed: ${messageOf(error)}` } }
|
|
93
|
+
report({ id: `hook:${event}`, message: `The ${event} hook failed`, error })
|
|
73
94
|
return undefined
|
|
74
95
|
})
|
|
75
96
|
if (ours === undefined) return next(e)
|
|
@@ -82,7 +103,7 @@ type HeldDecision = { readonly decision: 'allow' | 'ask'; readonly reason?: stri
|
|
|
82
103
|
|
|
83
104
|
export type HeldDecisions = Map<string, HeldDecision>
|
|
84
105
|
|
|
85
|
-
export function preToolUseHook(name: string, hook:
|
|
106
|
+
export function preToolUseHook(name: string, hook: HookListener<'PreToolUse'>, claude: Claude, calls: ToolCalls, held: HeldDecisions | undefined): RoutedHook<'tool.call'> {
|
|
86
107
|
return async (e, next) => {
|
|
87
108
|
let ours: ClassicFields
|
|
88
109
|
try {
|
|
@@ -167,11 +188,15 @@ async function preToolUseInput(name: string, envelope: Frozen<Args<'tool.call'>>
|
|
|
167
188
|
|
|
168
189
|
async function callFiles(name: string, claude: Claude, use: CallBase, cwd: string): Promise<CallFiles> {
|
|
169
190
|
try {
|
|
170
|
-
const home = await claude.env.home()
|
|
191
|
+
const [home, configRoot] = await Promise.all([claude.env.home(), claude.env.configHome()])
|
|
171
192
|
if (home === undefined) throw new Error('HOME is not set, so ~ in a path has no meaning.')
|
|
172
|
-
const { shell, reads, writes } = callEffects(use, { cwd, home, fs: claude.fs })
|
|
193
|
+
const { shell, reads, writes } = callEffects(use, { cwd, home, ...(configRoot === undefined ? {} : { configRoot }), fs: claude.fs })
|
|
173
194
|
const known = (accesses: FileAccess[]) => accesses.map(({ path }) => path).filter((path) => shell === undefined || !dynamicPattern.test(path))
|
|
174
|
-
const
|
|
195
|
+
const unchecked = (path: string) => (error: unknown) => {
|
|
196
|
+
if (!isMissing(error)) claude.ui.log(`${name}: the ${use.tool} call reads ${path}, which could not be checked: ${messageOf(error)}`, { to: 'debug' })
|
|
197
|
+
return undefined
|
|
198
|
+
}
|
|
199
|
+
const read = await Promise.all(known(reads).map(async (path) => ((await claude.fs.stat(path).catch(unchecked(path)))?.kind === 'file' ? [path] : [])))
|
|
175
200
|
return { read: read.flat(), changed: known(writes) }
|
|
176
201
|
} catch (error) {
|
|
177
202
|
claude.ui.log(`${name}: the ${use.tool} call lists no files: ${messageOf(error)}`, { to: 'debug' })
|
|
@@ -179,7 +204,7 @@ async function callFiles(name: string, claude: Claude, use: CallBase, cwd: strin
|
|
|
179
204
|
}
|
|
180
205
|
}
|
|
181
206
|
|
|
182
|
-
function classicResult(event:
|
|
207
|
+
function classicResult(event: Hook, answer: HookAnswer): ClassicFields {
|
|
183
208
|
const result: ClassicFields = {}
|
|
184
209
|
const unread = (field: string) => new Error(`it answered ${field}, which ${event} does not read. Remove it from the answer.`)
|
|
185
210
|
if (event === 'PreToolUse') {
|
|
@@ -198,10 +223,10 @@ function classicResult(event: ModEvent, answer: HookAnswer): ClassicFields {
|
|
|
198
223
|
if (field === 'hookEventName' || value === undefined || (value === false && flags.includes(field))) continue
|
|
199
224
|
if (!readFields[event].includes(field)) throw unread(`hookSpecificOutput.${field}`)
|
|
200
225
|
if (field === 'additionalContext') result['additionalContext'] = [value]
|
|
201
|
-
else if (field === 'permissionDecision') {
|
|
226
|
+
else if (field === 'permissionDecision' && event === 'PreToolUse') {
|
|
202
227
|
const reason = specific.permissionDecisionReason ?? ''
|
|
203
228
|
result[value as string] = value === 'allow' ? true : reason
|
|
204
|
-
} else if (field !== 'permissionDecisionReason') result[field] = value
|
|
229
|
+
} else if (field !== 'permissionDecisionReason' || event !== 'PreToolUse') result[field] = value
|
|
205
230
|
}
|
|
206
231
|
return result
|
|
207
232
|
}
|
package/src/runtime/installer.ts
CHANGED
|
@@ -7,7 +7,7 @@ import { Box, Button, drawWith, Input, Text } from '../ui/elements.js'
|
|
|
7
7
|
import { messageOf } from '../utils/text.js'
|
|
8
8
|
import type { Claude } from './claude.js'
|
|
9
9
|
|
|
10
|
-
export const
|
|
10
|
+
export const installerPaneOf = (name: string): string => `installer-${name.replace(/[^A-Za-z0-9_-]/g, '_')}`.slice(0, 64)
|
|
11
11
|
|
|
12
12
|
export type Consent = {
|
|
13
13
|
readonly install: string
|
|
@@ -33,6 +33,7 @@ export type Installer = {
|
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
export function createInstaller(name: string, claude: () => Claude): Installer {
|
|
36
|
+
const paneId = installerPaneOf(name)
|
|
36
37
|
let page: Page | undefined
|
|
37
38
|
let isOpen = false
|
|
38
39
|
|
|
@@ -48,7 +49,7 @@ export function createInstaller(name: string, claude: () => Claude): Installer {
|
|
|
48
49
|
})
|
|
49
50
|
if (!isOpen) {
|
|
50
51
|
isOpen = true
|
|
51
|
-
await claude().ui.open({ id:
|
|
52
|
+
await claude().ui.open({ id: paneId, title: `Install ${name}`, focus: true, holdToasts: true, closeOnEscape: true })
|
|
52
53
|
}
|
|
53
54
|
claude().ui.invalidate('ui.render')
|
|
54
55
|
return answer
|
|
@@ -60,12 +61,12 @@ export function createInstaller(name: string, claude: () => Claude): Installer {
|
|
|
60
61
|
const bullet = (text: string) => Text({ children: [Text({ color: 'claude', children: '◆ ' }), text] })
|
|
61
62
|
|
|
62
63
|
return {
|
|
63
|
-
draws: (e) => e.component === 'Pane' && e.requestId ===
|
|
64
|
+
draws: (e) => e.component === 'Pane' && e.requestId === paneId,
|
|
64
65
|
draw(e) {
|
|
65
66
|
return drawWith(claude().ui.resolve(e), () => page?.draw(e.props as Frozen<RenderPropsOf['Pane']>) ?? Text({ dimColor: true, children: `${name} is installing.` }))
|
|
66
67
|
},
|
|
67
68
|
closed(id) {
|
|
68
|
-
if (id !==
|
|
69
|
+
if (id !== paneId) return
|
|
69
70
|
isOpen = false
|
|
70
71
|
page?.cancel()
|
|
71
72
|
},
|
|
@@ -102,12 +103,12 @@ export function createInstaller(name: string, claude: () => Claude): Installer {
|
|
|
102
103
|
`${name} needs ${missing.length === 1 ? 'one setting' : `${missing.length} settings`}:`,
|
|
103
104
|
missing.map(([key, option]) => {
|
|
104
105
|
const label = Text({ children: [Text({ bold: true, children: option.title }), option.description === '' ? '' : Text({ dimColor: true, children: ` ${option.description}` })] })
|
|
105
|
-
if (option.
|
|
106
|
+
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
107
|
const failure = failures.get(key)
|
|
107
108
|
const field = Input({
|
|
108
109
|
key: `option:${key}`,
|
|
109
110
|
label: '› ',
|
|
110
|
-
placeholder: option.
|
|
111
|
+
placeholder: option.type === 'choice' ? (option.choices ?? []).join(', ') : option.type === 'toggle' ? 'true or false' : option.type === 'list' ? 'comma-separated' : '',
|
|
111
112
|
value: saved.get(key) ?? '',
|
|
112
113
|
submitLabel: 'save',
|
|
113
114
|
onSubmit: (value: string) =>
|
|
@@ -131,7 +132,7 @@ export function createInstaller(name: string, claude: () => Claude): Installer {
|
|
|
131
132
|
)
|
|
132
133
|
},
|
|
133
134
|
step(mod, step, index, total) {
|
|
134
|
-
let notice: string | undefined
|
|
135
|
+
let notice: { readonly color: 'warning' | 'error'; readonly text: string } | undefined
|
|
135
136
|
return show<boolean>(
|
|
136
137
|
(answer) => ({
|
|
137
138
|
draw: (props) => {
|
|
@@ -142,16 +143,17 @@ export function createInstaller(name: string, claude: () => Claude): Installer {
|
|
|
142
143
|
body = Text({ color: 'error', children: `The ${step.title} step could not draw: ${messageOf(error)}` })
|
|
143
144
|
}
|
|
144
145
|
const next = async () => {
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
146
|
+
try {
|
|
147
|
+
if (await step.isDone(mod)) return answer(true)
|
|
148
|
+
notice = { color: 'warning', text: `Finish ${step.title} first.` }
|
|
149
|
+
} catch (error) {
|
|
150
|
+
notice = { color: 'error', text: `The ${step.title} step's isDone failed: ${messageOf(error)}` }
|
|
150
151
|
}
|
|
152
|
+
claude().ui.invalidate('ui.render')
|
|
151
153
|
}
|
|
152
154
|
return frame(
|
|
153
155
|
`${step.title} (${index} of ${total})`,
|
|
154
|
-
[body, ...(notice === undefined ? [] : [Text({ color:
|
|
156
|
+
[body, ...(notice === undefined ? [] : [Text({ color: notice.color, children: notice.text })])],
|
|
155
157
|
[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
158
|
)
|
|
157
159
|
},
|
|
@@ -163,7 +165,7 @@ export function createInstaller(name: string, claude: () => Claude): Installer {
|
|
|
163
165
|
page = undefined
|
|
164
166
|
if (!isOpen) return
|
|
165
167
|
isOpen = false
|
|
166
|
-
await claude().ui.close({ id:
|
|
168
|
+
await claude().ui.close({ id: paneId })
|
|
167
169
|
},
|
|
168
170
|
}
|
|
169
171
|
}
|