@cmodjs/core 3.0.0 → 4.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 +39 -22
- package/docs/dependencies.md +27 -20
- package/docs/index.md +21 -17
- package/docs/install-steps.md +37 -29
- package/docs/jobs.md +25 -52
- package/docs/mod.md +94 -66
- package/docs/options.md +2 -2
- package/docs/permissions.md +7 -5
- package/docs/session.md +14 -12
- package/docs/testing.md +55 -32
- package/docs/ui.md +255 -263
- package/hotkeys.d.ts +22 -0
- package/hotkeys.d.ts.map +1 -0
- package/hotkeys.js +26 -0
- package/installer.d.ts +1 -1
- package/installer.d.ts.map +1 -1
- package/installer.js +11 -6
- package/jobs/check.d.ts +2 -1
- package/jobs/check.d.ts.map +1 -1
- package/jobs/check.js +2 -2
- package/jobs/context.d.ts +2 -1
- package/jobs/context.d.ts.map +1 -1
- package/jobs/context.js +1 -1
- package/jobs/permissions.d.ts +2 -1
- package/jobs/permissions.d.ts.map +1 -1
- package/jobs/permissions.js +2 -2
- package/jobs/program.js +1 -1
- package/jobs/prompt.d.ts +2 -1
- package/jobs/prompt.d.ts.map +1 -1
- package/jobs/prompt.js +20 -13
- package/jobs/schedule.d.ts +2 -1
- package/jobs/schedule.d.ts.map +1 -1
- package/jobs/schedule.js +15 -1
- package/jobs/slash-command.d.ts +6 -2
- package/jobs/slash-command.d.ts.map +1 -1
- package/jobs/slash-command.js +6 -5
- package/jobs/tool.d.ts +6 -4
- package/jobs/tool.d.ts.map +1 -1
- package/jobs/tool.js +3 -3
- package/mod.d.ts +63 -42
- package/mod.d.ts.map +1 -1
- package/mod.js +11 -1
- package/package.json +1 -1
- package/records.d.ts +14 -8
- package/records.d.ts.map +1 -1
- package/records.js +44 -31
- package/register.d.ts.map +1 -1
- package/register.js +1 -2
- package/runtime/announce.d.ts +1 -1
- package/runtime/announce.d.ts.map +1 -1
- package/runtime/announce.js +6 -7
- package/runtime/before.d.ts +2 -2
- package/runtime/before.d.ts.map +1 -1
- package/runtime/before.js +1 -1
- package/runtime/claude.d.ts +0 -1
- package/runtime/claude.d.ts.map +1 -1
- package/runtime/conversations.d.ts +4 -1
- package/runtime/conversations.d.ts.map +1 -1
- package/runtime/conversations.js +42 -14
- package/runtime/dependencies.d.ts.map +1 -1
- package/runtime/dependencies.js +2 -1
- package/runtime/engine.d.ts +5 -0
- package/runtime/engine.d.ts.map +1 -1
- package/runtime/grants.js +2 -2
- package/runtime/hooks.d.ts +1 -1
- package/runtime/hooks.d.ts.map +1 -1
- package/runtime/hotkeys.d.ts +15 -0
- package/runtime/hotkeys.d.ts.map +1 -0
- package/runtime/hotkeys.js +58 -0
- package/runtime/installer.d.ts +5 -2
- package/runtime/installer.d.ts.map +1 -1
- package/runtime/installer.js +23 -12
- package/runtime/lifecycle.d.ts.map +1 -1
- package/runtime/lifecycle.js +190 -141
- package/runtime/options.js +2 -2
- package/runtime/pages.d.ts +28 -0
- package/runtime/pages.d.ts.map +1 -0
- package/runtime/pages.js +98 -0
- package/runtime/parts.d.ts +14 -0
- package/runtime/parts.d.ts.map +1 -0
- package/runtime/parts.js +63 -0
- package/runtime/permission-check.d.ts +2 -2
- package/runtime/permission-check.d.ts.map +1 -1
- package/runtime/permission-check.js +12 -4
- package/runtime/programs.js +1 -1
- package/runtime/router.d.ts +2 -2
- package/runtime/router.d.ts.map +1 -1
- package/runtime/router.js +1 -1
- package/runtime/sessions.d.ts +10 -9
- package/runtime/sessions.d.ts.map +1 -1
- package/runtime/sessions.js +14 -5
- package/runtime/state.d.ts +2 -1
- package/runtime/state.d.ts.map +1 -1
- package/runtime/state.js +1 -0
- package/runtime/toaster.d.ts +1 -1
- package/runtime/toaster.d.ts.map +1 -1
- package/runtime/toaster.js +10 -4
- package/runtime/ui.d.ts +87 -5
- package/runtime/ui.d.ts.map +1 -1
- package/runtime/ui.js +372 -109
- package/runtime/watches.d.ts +9 -0
- package/runtime/watches.d.ts.map +1 -0
- package/runtime/watches.js +73 -0
- package/runtime/widgets.d.ts +48 -0
- package/runtime/widgets.d.ts.map +1 -0
- package/runtime/widgets.js +219 -0
- package/session/context/turn/message/message.d.ts.map +1 -1
- package/session/context/turn/message/message.js +7 -2
- package/session/session.d.ts +11 -5
- package/session/session.d.ts.map +1 -1
- package/src/hotkeys.ts +51 -0
- package/src/installer.ts +8 -4
- package/src/jobs/check.ts +4 -3
- package/src/jobs/context.ts +3 -2
- package/src/jobs/permissions.ts +4 -3
- package/src/jobs/program.ts +1 -1
- package/src/jobs/prompt.ts +23 -14
- package/src/jobs/schedule.ts +16 -2
- package/src/jobs/slash-command.ts +10 -7
- package/src/jobs/tool.ts +7 -5
- package/src/mod.ts +59 -47
- package/src/records.ts +50 -31
- package/src/register.ts +2 -3
- package/src/runtime/announce.ts +6 -7
- package/src/runtime/before.ts +4 -4
- package/src/runtime/claude.ts +0 -1
- package/src/runtime/conversations.ts +66 -20
- package/src/runtime/dependencies.ts +2 -1
- package/src/runtime/engine.ts +5 -0
- package/src/runtime/grants.ts +2 -2
- package/src/runtime/hooks.ts +1 -0
- package/src/runtime/hotkeys.ts +80 -0
- package/src/runtime/installer.ts +24 -12
- package/src/runtime/lifecycle.ts +189 -136
- package/src/runtime/options.ts +2 -2
- package/src/runtime/pages.ts +127 -0
- package/src/runtime/parts.ts +72 -0
- package/src/runtime/permission-check.ts +13 -5
- package/src/runtime/programs.ts +1 -1
- package/src/runtime/router.ts +3 -3
- package/src/runtime/sessions.ts +24 -15
- package/src/runtime/state.ts +3 -1
- package/src/runtime/toaster.ts +10 -4
- package/src/runtime/ui.ts +405 -115
- package/src/runtime/watches.ts +75 -0
- package/src/runtime/widgets.ts +287 -0
- package/src/session/context/turn/message/message.ts +7 -2
- package/src/session/session.ts +10 -6
- package/src/testing/fake-claude.ts +9 -6
- package/src/testing/fake-elements.ts +7 -4
- package/src/testing/fake-sessions.ts +6 -4
- package/src/testing.ts +203 -46
- package/src/ui/areas.ts +27 -0
- package/src/ui/elements.ts +35 -0
- package/src/ui/markdown.ts +18 -18
- package/src/ui/panes.ts +73 -0
- package/src/ui/styles.ts +53 -0
- package/src/utils/subscription.ts +1 -0
- package/testing/fake-claude.d.ts +0 -1
- package/testing/fake-claude.d.ts.map +1 -1
- package/testing/fake-claude.js +9 -5
- package/testing/fake-elements.js +7 -4
- package/testing/fake-sessions.d.ts +2 -1
- package/testing/fake-sessions.d.ts.map +1 -1
- package/testing/fake-sessions.js +1 -0
- package/testing.d.ts +21 -6
- package/testing.d.ts.map +1 -1
- package/testing.js +206 -45
- package/ui/areas.d.ts +29 -0
- package/ui/areas.d.ts.map +1 -0
- package/ui/areas.js +6 -0
- package/ui/elements.d.ts +3 -0
- package/ui/elements.d.ts.map +1 -1
- package/ui/elements.js +36 -0
- package/ui/markdown.d.ts +14 -14
- package/ui/markdown.d.ts.map +1 -1
- package/ui/markdown.js +11 -11
- package/ui/panes.d.ts +71 -0
- package/ui/panes.d.ts.map +1 -0
- package/ui/panes.js +16 -0
- package/ui/styles.d.ts +44 -0
- package/ui/styles.d.ts.map +1 -0
- package/ui/styles.js +24 -0
- package/utils/subscription.d.ts +4 -0
- package/utils/subscription.d.ts.map +1 -0
- package/jobs/status-line.d.ts +0 -6
- package/jobs/status-line.d.ts.map +0 -1
- package/jobs/status-line.js +0 -39
- package/src/jobs/status-line.ts +0 -44
- package/src/ui/define-pane.ts +0 -27
- package/src/ui/places.ts +0 -49
- package/src/utils/disposable.ts +0 -3
- package/ui/define-pane.d.ts +0 -15
- package/ui/define-pane.d.ts.map +0 -1
- package/ui/define-pane.js +0 -12
- package/ui/places.d.ts +0 -43
- package/ui/places.d.ts.map +0 -1
- package/ui/places.js +0 -31
- package/utils/disposable.d.ts +0 -4
- package/utils/disposable.d.ts.map +0 -1
- /package/utils/{disposable.js → subscription.js} +0 -0
package/src/runtime/lifecycle.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import type { Args, ClassicHookInputs, EventResult, Frozen, HookBudget, HookFailure, PluginOptions, RenderElement, Timer } from 'claude-code'
|
|
2
2
|
import type { Step } from '../installer.js'
|
|
3
|
-
|
|
3
|
+
|
|
4
|
+
import type { Allowed, Imports, Mod, ModDefinition, Reservation, SettingsFile, SettingsFileChange, SettingsFileChangeListener, Subscription, Timer as ModTimer } from '../mod.js'
|
|
4
5
|
import type { Options, OptionValues } from '../options.js'
|
|
5
|
-
import {
|
|
6
|
+
import { consentPath, dataFolder, finishStepsMethod, grantFix, coreVersion, isAtLeast, marketplaceOf, parseConsent, pendingStepsMethod, parseEvent, permissionWords, readRecord, readSteps, recordPath, scriptsSha256, storeFolder, updatesToTurnOn, type ReadFile, type RunnerEvent, type Steps } from '../records.js'
|
|
6
7
|
import { homePrefix, ifMissing, relativePath } from '../utils/paths.js'
|
|
7
8
|
import { firstLine, formatExit, messageOf } from '../utils/text.js'
|
|
8
9
|
import type { Claude } from './claude.js'
|
|
@@ -15,20 +16,25 @@ import { keepEngine } from './engine.js'
|
|
|
15
16
|
import { classicFailure, classicHook, userSkillHook, type Hook, type HookFields, type HookListener, type RoutedEvent } from './hooks.js'
|
|
16
17
|
import type { PermissionRoutes } from './permission-check.js'
|
|
17
18
|
import { checkedAnswer, checkingGrants, checkSettings, checksAnswers, checkWrite, isCovered, itemOf, passedDown, programAddresses, type AnswerCheck, type Grants } from './grants.js'
|
|
18
|
-
import { cmodError,
|
|
19
|
+
import { cmodError, locatingPrograms, readLines, type Programs } from './programs.js'
|
|
19
20
|
import { createModFiles, resolvedPath, type Places } from './metadata.js'
|
|
20
21
|
import { fitsOption } from '../options.js'
|
|
21
22
|
import { createInstaller } from './installer.js'
|
|
22
23
|
import { createOptions, MissingOptions } from './options.js'
|
|
23
24
|
import { createRouter, type Router, type RouterNext } from './router.js'
|
|
24
25
|
import { conversationMethods, createSessionsClient, type SessionsClient } from './sessions.js'
|
|
25
|
-
import type { SessionChange } from '../session/conversation.js'
|
|
26
|
+
import type { SessionChange, TurnData } from '../session/conversation.js'
|
|
26
27
|
import type { BeforeEvent } from '../session/session.js'
|
|
27
28
|
import { matches } from '../utils/matcher.js'
|
|
28
29
|
import { createState } from './state.js'
|
|
29
30
|
import { createToaster, type Failure, type ShownToast, type Toaster } from './toaster.js'
|
|
30
31
|
import { toolCalls } from './tool-calls.js'
|
|
31
|
-
import {
|
|
32
|
+
import { createHotkeys } from './hotkeys.js'
|
|
33
|
+
import { servePages, type Pages } from './pages.js'
|
|
34
|
+
import { drawnParts } from './parts.js'
|
|
35
|
+
import { createWatches } from './watches.js'
|
|
36
|
+
import { createUi, partMethods, type PartDraw, type PartsUpdate, type SettingsTab, type UiArea } from './ui.js'
|
|
37
|
+
import { serveWidgets } from './widgets.js'
|
|
32
38
|
import { Progress } from '../ui/components.js'
|
|
33
39
|
import { Box, Text } from '../ui/elements.js'
|
|
34
40
|
|
|
@@ -74,12 +80,18 @@ type ActiveMod<State extends object, Declared extends Options> = {
|
|
|
74
80
|
readonly added: readonly string[]
|
|
75
81
|
readonly imports: Imports
|
|
76
82
|
readonly raise: SessionsClient['raise']
|
|
83
|
+
readonly area: UiArea<State>
|
|
77
84
|
}
|
|
78
85
|
|
|
79
86
|
|
|
80
87
|
const announcedKey = 'cmod:announced'
|
|
81
88
|
|
|
89
|
+
const cmodUpdatedKey = 'cmod:cmod-updated-for'
|
|
90
|
+
|
|
82
91
|
const installId = 'cmod:install'
|
|
92
|
+
const askingId = 'cmod:asking'
|
|
93
|
+
|
|
94
|
+
const recordId = 'cmod:record'
|
|
83
95
|
|
|
84
96
|
const installRetrySeconds = [2, 4, 8, 16, 32, 64, 128] as const
|
|
85
97
|
|
|
@@ -95,7 +107,6 @@ const startHoldMs = hookMs - 1_000
|
|
|
95
107
|
|
|
96
108
|
const dependencyCallMs = 30_000
|
|
97
109
|
|
|
98
|
-
const cannotStart = /failed to start: /
|
|
99
110
|
|
|
100
111
|
const shellTools: readonly string[] = ['Bash', 'PowerShell']
|
|
101
112
|
|
|
@@ -122,7 +133,9 @@ export async function readPlugin(claude: Claude): Promise<Plugin> {
|
|
|
122
133
|
read: (path) => read(`${root}/${path}`),
|
|
123
134
|
list: (folder) => filesBelow(claude, `${root}/${folder}`),
|
|
124
135
|
})
|
|
125
|
-
|
|
136
|
+
if (version === undefined || record?.scriptsSha256 !== scripts) return { ...plugin, isInstalled: false, shouldRecord: false }
|
|
137
|
+
const isInstalled = record.version === version || record.root === root
|
|
138
|
+
return { ...plugin, isInstalled, shouldRecord: isInstalled && record.version !== version }
|
|
126
139
|
}
|
|
127
140
|
|
|
128
141
|
async function filesBelow(claude: Claude, folder: string): Promise<string[]> {
|
|
@@ -148,15 +161,17 @@ async function readJson(read: ReadFile, path: string): Promise<unknown> {
|
|
|
148
161
|
}
|
|
149
162
|
|
|
150
163
|
async function cmodVersion(claude: Claude): Promise<string | undefined> {
|
|
151
|
-
const
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
})
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
return
|
|
164
|
+
const store = storeFolder({ HOME: await claude.env.home(), XDG_DATA_HOME: await claude.env.dataHome() })
|
|
165
|
+
const exists = async (path: string) => (await claude.fs.stat(path).catch(ifMissing(undefined))) !== undefined
|
|
166
|
+
if (!(await exists(`${store}/programs/cmod`))) return undefined
|
|
167
|
+
const versions = (await claude.fs.list(`${store}/bin/cmod`).catch(ifMissing([]))).filter((entry) => entry.kind === 'dir' && releaseVersion.test(entry.name)).map((entry) => entry.name)
|
|
168
|
+
const newestFirst = versions.toSorted((one, other) => (isAtLeast(one, other) ? -1 : 1))
|
|
169
|
+
for (const version of newestFirst) if (await exists(`${store}/bin/cmod/${version}/cmod`)) return version
|
|
170
|
+
return undefined
|
|
158
171
|
}
|
|
159
172
|
|
|
173
|
+
const releaseVersion = /^\d+\.\d+\.\d+$/
|
|
174
|
+
|
|
160
175
|
export function createLifecycle<State extends object, Declared extends Options = Options>(
|
|
161
176
|
definition: ModDefinition<State, string, Declared>,
|
|
162
177
|
permissionRoutes: () => PermissionRoutes | undefined = () => undefined,
|
|
@@ -171,14 +186,24 @@ export function createLifecycle<State extends object, Declared extends Options =
|
|
|
171
186
|
const granted = new Set<string>()
|
|
172
187
|
let activation: Promise<void> | undefined
|
|
173
188
|
let mod: Mod<State> | undefined
|
|
189
|
+
const openSettings = async (tab?: SettingsTab) => {
|
|
190
|
+
await claude().cmod.call({ to: cmodPluginName, method: partMethods.changed, input: { requests: [{ settings: true, ...(tab === undefined ? {} : { tab }) }], isAsked: true } })
|
|
191
|
+
}
|
|
174
192
|
let imports: Imports | undefined
|
|
175
193
|
let raise: SessionsClient['raise'] | undefined
|
|
176
194
|
let failure: unknown
|
|
177
195
|
let pluginOptions = options
|
|
178
|
-
const installer = createInstaller(definition.name, () => claude()
|
|
196
|
+
const installer = createInstaller(definition.name, () => claude(), {
|
|
197
|
+
show: (open) => {
|
|
198
|
+
if (phase !== 'active') runtime?.toaster.toast.dismiss(installId)
|
|
199
|
+
runtime?.toaster.toast.info('Installing', { id: askingId, description: 'Waiting for your answer', duration: Infinity, action: { label: 'Answer', onClick: open } })
|
|
200
|
+
},
|
|
201
|
+
hide: () => runtime?.toaster.toast.dismiss(askingId),
|
|
202
|
+
})
|
|
179
203
|
let shouldRecord = false
|
|
180
204
|
let recording: Promise<void> | undefined
|
|
181
205
|
let missedSessionStart: Frozen<Args<'classic.SessionStart'>> | undefined
|
|
206
|
+
let hasHeardSessionStart = false
|
|
182
207
|
let settleStart: () => void = () => undefined
|
|
183
208
|
const startSettled = new Promise<void>((resolve) => (settleStart = resolve))
|
|
184
209
|
|
|
@@ -191,17 +216,18 @@ export function createLifecycle<State extends object, Declared extends Options =
|
|
|
191
216
|
|
|
192
217
|
const toastFailure = (shown: Failure) => runtime?.toaster.fail(shown)
|
|
193
218
|
|
|
194
|
-
const fail = (reason: string, fix: string | undefined, retry?: () => Promise<void
|
|
219
|
+
const fail = (reason: string, fix: string | undefined, retry?: () => Promise<void>, error?: unknown) => {
|
|
195
220
|
phase = 'failed'
|
|
196
|
-
failure ??= new Error(`${definition.name}: ${reason}${/[.!?]$/.test(reason) ? '' : '.'}${fix === undefined ? '' : ` ${fix}`}
|
|
221
|
+
failure ??= new Error(`${definition.name}: ${reason}${/[.!?]$/.test(reason) ? '' : '.'}${fix === undefined ? '' : ` ${fix}`}`, { cause: error })
|
|
197
222
|
const again = retry === undefined ? {} : { retry: () => ((failure = undefined), stopRetrying(), retry().catch(report)) }
|
|
198
|
-
toastFailure({ id: installId, message: reason, ...(fix === undefined ? {} : { description: fix }), ...again })
|
|
223
|
+
toastFailure({ id: installId, message: reason, ...(fix === undefined ? {} : { description: fix }), ...(error === undefined ? {} : { error }), ...again })
|
|
199
224
|
}
|
|
200
225
|
|
|
201
226
|
let consented: string | undefined
|
|
202
227
|
const installAgain = () => install(consented)
|
|
228
|
+
const updateAgain = () => claude().store.delete(cmodUpdatedKey).then(installAgain)
|
|
203
229
|
|
|
204
|
-
type Retrying = { readonly when: 'changed' | 'later'; readonly tries: number; readonly
|
|
230
|
+
type Retrying = { readonly when: 'changed' | 'later'; readonly tries: number; readonly reason: string; readonly timer: Timer }
|
|
205
231
|
let retrying: Retrying | undefined
|
|
206
232
|
|
|
207
233
|
const retryLater = (next: Omit<Retrying, 'timer'>) => {
|
|
@@ -216,29 +242,20 @@ export function createLifecycle<State extends object, Declared extends Options =
|
|
|
216
242
|
retrying = undefined
|
|
217
243
|
}
|
|
218
244
|
|
|
219
|
-
const
|
|
220
|
-
if (plugin === undefined) return undefined
|
|
221
|
-
const lines: string[] = []
|
|
222
|
-
const { code } = await readLines(claude().process.spawn({ argv: ['sh', '-c', 'git rev-parse HEAD && git status --porcelain=v1 -uall && git diff HEAD'], cwd: plugin.root }), (text) => void lines.push(text))
|
|
223
|
-
return code === 0 ? lines.join('\n') : undefined
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
const failInstall = async (reason: string, when: 'changed' | 'later' | undefined) => {
|
|
245
|
+
const failInstall = (reason: string, when: 'changed' | 'later' | undefined) => {
|
|
227
246
|
if (when === undefined) {
|
|
228
247
|
stopRetrying()
|
|
229
248
|
return fail(reason, undefined, installAgain)
|
|
230
249
|
}
|
|
231
|
-
retryLater({ when, tries: retrying?.when === when ? retrying.tries : 0,
|
|
250
|
+
retryLater({ when, tries: retrying?.when === when ? retrying.tries : 0, reason })
|
|
232
251
|
}
|
|
233
252
|
|
|
234
253
|
const retryInBackground = async () => {
|
|
235
|
-
|
|
236
|
-
if (phase !== 'failed' || waiting === undefined) return
|
|
254
|
+
if (phase !== 'failed' || retrying === undefined) return
|
|
237
255
|
if (await recover()) {
|
|
238
256
|
stopRetrying()
|
|
239
257
|
return whenActive()
|
|
240
258
|
}
|
|
241
|
-
if (waiting.when === 'changed' && waiting.checkout !== undefined && (await checkoutState()) === waiting.checkout) return retryLater(waiting)
|
|
242
259
|
await install(consented, true)
|
|
243
260
|
await whenActive()
|
|
244
261
|
}
|
|
@@ -246,15 +263,24 @@ export function createLifecycle<State extends object, Declared extends Options =
|
|
|
246
263
|
const finish = async () => {
|
|
247
264
|
await programs?.refresh()
|
|
248
265
|
phase = 'ready'
|
|
266
|
+
activation ??= new Promise<void>((resolve) => claude().clock.after(0, () => resolve(activate().catch(report))))
|
|
249
267
|
claude().ui.invalidate('ui.render')
|
|
250
268
|
}
|
|
251
269
|
|
|
252
270
|
const recordThroughCmod = async () => {
|
|
253
271
|
if (plugin === undefined || (await cmodVersion(claude())) === undefined) return
|
|
272
|
+
let failed: string | undefined
|
|
254
273
|
const { code, lastError } = await readLines(claude().process.spawn({ argv: ['cmod', 'setup', plugin.root, '--events'] }), (text) => {
|
|
255
|
-
|
|
274
|
+
const event = parseEvent(text)
|
|
275
|
+
if (event.type === 'done') shouldRecord = false
|
|
276
|
+
if (event.type === 'failed') failed = event.message
|
|
256
277
|
})
|
|
257
|
-
if (shouldRecord)
|
|
278
|
+
if (!shouldRecord) {
|
|
279
|
+
runtime?.toaster.toast.dismiss(recordId)
|
|
280
|
+
return programs?.refresh()
|
|
281
|
+
}
|
|
282
|
+
if (failed !== undefined) return void toastFailure({ id: recordId, message: failed, retry: record })
|
|
283
|
+
throw new Error(`cmod setup ${formatExit(code, lastError)}`)
|
|
258
284
|
}
|
|
259
285
|
|
|
260
286
|
const record = () => {
|
|
@@ -312,9 +338,9 @@ export function createLifecycle<State extends object, Declared extends Options =
|
|
|
312
338
|
if (!isAnswered) {
|
|
313
339
|
runtime?.toaster.toast.warning(steps.length - index === 1 ? 'Needs one more step' : `Needs ${steps.length - index} more steps`, {
|
|
314
340
|
id: 'cmod:steps',
|
|
315
|
-
description: `Starting with ${step.title}
|
|
341
|
+
description: `Starting with ${step.title}.`,
|
|
316
342
|
duration: Infinity,
|
|
317
|
-
action: { label: 'Finish', onClick: () => active
|
|
343
|
+
action: { label: 'Finish', onClick: () => runSteps(active) },
|
|
318
344
|
})
|
|
319
345
|
break
|
|
320
346
|
}
|
|
@@ -327,7 +353,7 @@ export function createLifecycle<State extends object, Declared extends Options =
|
|
|
327
353
|
const activeRouter = createRouter()
|
|
328
354
|
const active = await createMod(definition, { ...runtime, router: activeRouter, dataFolder: dataFolder(plugin.store, plugin.name), store: plugin.store, steps: plugin.steps, granted, refreshGrants, permissionRoutes, options: pluginOptions }).catch(async (error: unknown) => {
|
|
329
355
|
if (error instanceof MissingOptions && (await askOptions(error))) return undefined
|
|
330
|
-
fail(messageOf(error), error instanceof MissingOptions ? `
|
|
356
|
+
fail(messageOf(error), error instanceof MissingOptions ? `Retry asks for ${error.keys.length === 1 ? 'it' : 'them'}.` : undefined, begin, error)
|
|
331
357
|
throw error
|
|
332
358
|
})
|
|
333
359
|
if (active === undefined) return activate()
|
|
@@ -341,19 +367,7 @@ export function createLifecycle<State extends object, Declared extends Options =
|
|
|
341
367
|
void runSteps(active.mod).catch((error: unknown) => toastFailure({ id: 'cmod:steps', message: 'Its installer stopped', error }))
|
|
342
368
|
const missed = missedSessionStart
|
|
343
369
|
missedSessionStart = undefined
|
|
344
|
-
|
|
345
|
-
const lateAnswer = missed === undefined ? {} : await replay(missed).catch((error: unknown) => {
|
|
346
|
-
toastFailure({ id: 'classic.SessionStart', message: 'The SessionStart hook failed', error })
|
|
347
|
-
return {}
|
|
348
|
-
})
|
|
349
|
-
const { additionalContext, ...unread } = lateAnswer as { readonly additionalContext?: readonly string[] } & Record<string, unknown>
|
|
350
|
-
for (const text of additionalContext ?? []) {
|
|
351
|
-
const added = await runtime.claude.session.append({ message: { type: 'user', content: [{ type: 'text', text }] } })
|
|
352
|
-
if (added.deny !== undefined) runtime.claude.ui.log(`${definition.name}: Claude Code refused its SessionStart note: ${added.deny}`, { to: 'debug' })
|
|
353
|
-
}
|
|
354
|
-
if (Object.keys(unread).length > 0) {
|
|
355
|
-
runtime.claude.ui.log(`${definition.name} started after Claude Code's SessionStart, so Claude Code did not read ${Object.keys(unread).join(', ')} from its SessionStart hooks.`, { to: 'debug' })
|
|
356
|
-
}
|
|
370
|
+
if (missed !== undefined) await replaySessionStart(missed)
|
|
357
371
|
const version = plugin.version ?? ''
|
|
358
372
|
const announced = announcedOf(await runtime.claude.store.get(announcedKey))
|
|
359
373
|
if (announced?.version === version) {
|
|
@@ -362,11 +376,28 @@ export function createLifecycle<State extends object, Declared extends Options =
|
|
|
362
376
|
}
|
|
363
377
|
await runtime.claude.store.set(announcedKey, { version, added })
|
|
364
378
|
const { toaster } = runtime
|
|
365
|
-
void announcementOf(runtime.claude, plugin, granted, added, announced)
|
|
379
|
+
void announcementOf(runtime.claude, plugin, granted, added, announced, openSettings)
|
|
366
380
|
.then(({ message, ...options }) => toaster.toast.success(message, { id: installId, duration: announcementMs, ...options }))
|
|
367
381
|
.catch((error: unknown) => toastFailure({ id: installId, message: 'Could not say what it installed', error }))
|
|
368
382
|
}
|
|
369
383
|
|
|
384
|
+
const replaySessionStart = async (e: Frozen<Args<'classic.SessionStart'>>) => {
|
|
385
|
+
hasHeardSessionStart = true
|
|
386
|
+
const ran = definition.name === cmodPluginName ? router.dispatch('classic.SessionStart', e, async () => ({})) : checkedRoute('classic.SessionStart', e, async () => ({}))
|
|
387
|
+
const answer = await ran.catch((error: unknown) => {
|
|
388
|
+
toastFailure({ id: 'classic.SessionStart', message: 'The SessionStart hook failed', error })
|
|
389
|
+
return {}
|
|
390
|
+
})
|
|
391
|
+
const { additionalContext, ...unread } = answer as { readonly additionalContext?: readonly string[] } & Record<string, unknown>
|
|
392
|
+
for (const text of additionalContext ?? []) {
|
|
393
|
+
const added = await claude().session.append({ message: { type: 'user', content: [{ type: 'text', text }] } })
|
|
394
|
+
if (added.deny !== undefined) claude().ui.log(`${definition.name}: Claude Code refused its SessionStart note: ${added.deny}`, { to: 'debug' })
|
|
395
|
+
}
|
|
396
|
+
if (Object.keys(unread).length > 0) {
|
|
397
|
+
claude().ui.log(`${definition.name} started after Claude Code's SessionStart, so Claude Code did not read ${Object.keys(unread).join(', ')} from its SessionStart hooks.`, { to: 'debug' })
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
370
401
|
const updatesToShow = async () => {
|
|
371
402
|
const [home, configHome] = await Promise.all([claude().env.home(), claude().env.configHome()])
|
|
372
403
|
const configRoot = configHome ?? `${home ?? ''}/.claude`
|
|
@@ -383,7 +414,6 @@ export function createLifecycle<State extends object, Declared extends Options =
|
|
|
383
414
|
claude().ui.log(`${name} waits for consent to run ${event.install}. Run cmod install ${name} in a terminal.`)
|
|
384
415
|
return
|
|
385
416
|
}
|
|
386
|
-
runtime?.toaster.toast.info('Installing', { id: installId, description: 'Waiting for your answer', duration: Infinity })
|
|
387
417
|
const updates = await updatesToShow().catch((error: unknown) => {
|
|
388
418
|
toastFailure({ id: 'cmod:updates', message: 'Could not read whether its marketplace updates automatically', error })
|
|
389
419
|
return undefined
|
|
@@ -423,7 +453,7 @@ export function createLifecycle<State extends object, Declared extends Options =
|
|
|
423
453
|
return failInstall(outcome?.type === 'failed' ? outcome.message : `cmod setup ${formatExit(code, lastError)}`, retries)
|
|
424
454
|
}
|
|
425
455
|
|
|
426
|
-
const fits = (found: string | undefined): found is string => found !== undefined && isAtLeast(found,
|
|
456
|
+
const fits = (found: string | undefined): found is string => found !== undefined && isAtLeast(found, coreVersion)
|
|
427
457
|
|
|
428
458
|
const waitForcmod = (found: string | undefined) => {
|
|
429
459
|
phase = 'waiting'
|
|
@@ -453,11 +483,24 @@ export function createLifecycle<State extends object, Declared extends Options =
|
|
|
453
483
|
const longWait = claude().clock.after(cmodWaitMs, () => {
|
|
454
484
|
timer.cancel()
|
|
455
485
|
timer = poll(cmodSlowCheckMs)
|
|
456
|
-
installing(
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
486
|
+
if (latest === undefined) return installing("Still waiting for cmod to download cmod. See cmod's own line.")
|
|
487
|
+
const found = latest
|
|
488
|
+
const tried = `${found} to ${coreVersion}`
|
|
489
|
+
return claude()
|
|
490
|
+
.store.get(cmodUpdatedKey)
|
|
491
|
+
.then(async (updated) => {
|
|
492
|
+
if (updated === tried) {
|
|
493
|
+
stop()
|
|
494
|
+
return fail(`${definition.name} needs cmod ${coreVersion} or later, and the newest cmod is ${found}.`, undefined, updateAgain)
|
|
495
|
+
}
|
|
496
|
+
await claude().store.set(cmodUpdatedKey, tried)
|
|
497
|
+
installing(`${definition.name} needs cmod ${coreVersion} or later, and this computer has cmod ${found}, so cmod is updating itself.`)
|
|
498
|
+
const { code, lastError } = await readLines(claude().process.spawn({ argv: ['cmod', 'update', 'cmod'] }), () => undefined)
|
|
499
|
+
if (code === 0) return
|
|
500
|
+
stop()
|
|
501
|
+
fail(`${definition.name} needs cmod ${coreVersion} or later, and updating cmod ${found} ${formatExit(code, lastError)}`, undefined, updateAgain)
|
|
502
|
+
})
|
|
503
|
+
.catch(report)
|
|
461
504
|
})
|
|
462
505
|
}
|
|
463
506
|
|
|
@@ -491,7 +534,7 @@ export function createLifecycle<State extends object, Declared extends Options =
|
|
|
491
534
|
|
|
492
535
|
const report = (error: unknown) => {
|
|
493
536
|
failure = error
|
|
494
|
-
if (phase !== 'failed') fail(messageOf(error), undefined, begin)
|
|
537
|
+
if (phase !== 'failed') fail(messageOf(error), undefined, begin, error)
|
|
495
538
|
}
|
|
496
539
|
|
|
497
540
|
let consentMs = 0
|
|
@@ -539,8 +582,13 @@ export function createLifecycle<State extends object, Declared extends Options =
|
|
|
539
582
|
if (droppedParts.has(item)) return
|
|
540
583
|
droppedParts.add(item)
|
|
541
584
|
const declared = plugin?.steps.permissions ?? []
|
|
542
|
-
const
|
|
543
|
-
runtime?.toaster.toast.warning(`Answered ${part} without your grant to "${permissionWords(item)}", so cmod dropped it`, {
|
|
585
|
+
const isDeclared = declared.includes(item)
|
|
586
|
+
runtime?.toaster.toast.warning(`Answered ${part} without your grant to "${permissionWords(item)}", so cmod dropped it`, {
|
|
587
|
+
id: `dropped:${item}`,
|
|
588
|
+
description: isDeclared ? grantFix : `It needs "permissions": { "${item}": true } in package.json "cmod".`,
|
|
589
|
+
duration: Infinity,
|
|
590
|
+
...(isDeclared ? { action: { label: 'Settings', onClick: () => openSettings('permissions') } } : {}),
|
|
591
|
+
})
|
|
544
592
|
},
|
|
545
593
|
}
|
|
546
594
|
|
|
@@ -588,7 +636,7 @@ export function createLifecycle<State extends object, Declared extends Options =
|
|
|
588
636
|
async start(claudeCalls, read) {
|
|
589
637
|
if (runtime !== undefined) return
|
|
590
638
|
programs = locatingPrograms(claudeCalls)
|
|
591
|
-
runtime = { claude: programs.claude, toaster: createToaster(claudeCalls, definition.name, () => plugin?.version) }
|
|
639
|
+
runtime = { claude: programs.claude, toaster: createToaster(claudeCalls, definition.name, () => plugin?.version, () => openSettings('permissions')) }
|
|
592
640
|
readStarted = read
|
|
593
641
|
try {
|
|
594
642
|
await programs.refresh()
|
|
@@ -601,7 +649,10 @@ export function createLifecycle<State extends object, Declared extends Options =
|
|
|
601
649
|
if (imports !== undefined) bind(imports)
|
|
602
650
|
if (event === 'ui.render' && installer.draws(e as Frozen<Args<'ui.render'>>)) return installer.draw(e as Frozen<Args<'ui.render'>>) as EventResult<N>
|
|
603
651
|
if (event === 'ui.close') installer.closed((e as Frozen<Args<'ui.close'>>).id)
|
|
604
|
-
if (event === 'classic.SessionStart')
|
|
652
|
+
if (event === 'classic.SessionStart') {
|
|
653
|
+
missedSessionStart = undefined
|
|
654
|
+
hasHeardSessionStart = true
|
|
655
|
+
}
|
|
605
656
|
if (event === 'classic.SessionStart' && runtime === undefined) {
|
|
606
657
|
missedSessionStart = e as Frozen<Args<'classic.SessionStart'>>
|
|
607
658
|
return next(e)
|
|
@@ -625,6 +676,10 @@ export function createLifecycle<State extends object, Declared extends Options =
|
|
|
625
676
|
if (call.method === finishStepsMethod) return (await runSteps(mod), { value: null }) as EventResult<N>
|
|
626
677
|
}
|
|
627
678
|
if (event === 'classic.UserPromptSubmit' || event === 'classic.SessionStart') await refreshGrants().catch((error: unknown) => claude().ui.log(`${definition.name} keeps its grants from before: ${messageOf(error)}`, { to: 'debug' }))
|
|
679
|
+
if (event === 'classic.UserPromptSubmit' && phase === 'active' && !hasHeardSessionStart) {
|
|
680
|
+
const { session_id, transcript_path, cwd } = e as Frozen<Args<'classic.UserPromptSubmit'>>
|
|
681
|
+
await replaySessionStart({ session_id, transcript_path, cwd, hook_event_name: 'SessionStart', source: 'resume' })
|
|
682
|
+
}
|
|
628
683
|
if (checksAnswers(event) && phase === 'active' && definition.name !== cmodPluginName) return checkedRoute(event, e, next)
|
|
629
684
|
const answer = router.dispatch(event, e, next)
|
|
630
685
|
const render = e as Frozen<Args<'ui.render'>>
|
|
@@ -647,7 +702,7 @@ export function createLifecycle<State extends object, Declared extends Options =
|
|
|
647
702
|
async function createMod<State extends object, Declared extends Options>(definition: ModDefinition<State, string, Declared>, runtime: ModRuntime): Promise<ActiveMod<State, Declared>> {
|
|
648
703
|
const { router, toaster } = runtime
|
|
649
704
|
const report = (failure: Failure) => void toaster.fail(failure)
|
|
650
|
-
const claude = answeringOwnCalls(withBareRefusals(
|
|
705
|
+
const claude = answeringOwnCalls(withBareRefusals(runtime.claude), router)
|
|
651
706
|
const added: string[] = []
|
|
652
707
|
const announce = (feature: string) => {
|
|
653
708
|
added.push(feature)
|
|
@@ -675,23 +730,6 @@ async function createMod<State extends object, Declared extends Options>(definit
|
|
|
675
730
|
checkWrite: (path) => checkWrite(grants, `metadata.update(${path})`, path),
|
|
676
731
|
report,
|
|
677
732
|
})
|
|
678
|
-
const pages = new Map<string, { readonly title: string; readonly handle: PaneHandle }>()
|
|
679
|
-
const settingsPages: Mod<State>['settings'] = {
|
|
680
|
-
page(page) {
|
|
681
|
-
if (pages.has(page.id)) throw new Error(`${definition.name}: the settings page "${page.id}" is already added. Give each page its own id.`)
|
|
682
|
-
if (page.id === changelogPage) throw new Error(`${definition.name}: the settings page id "${changelogPage}" opens the mod's CHANGELOG.md in /mods. Give the page another id.`)
|
|
683
|
-
pages.set(page.id, { title: page.title, handle: area.ui.pane(page) })
|
|
684
|
-
},
|
|
685
|
-
async open(pageId) {
|
|
686
|
-
if (pageId === undefined) {
|
|
687
|
-
await claude.cmod.call({ to: cmodPluginName, method: 'openSettings', input: { mod: definition.name } })
|
|
688
|
-
return
|
|
689
|
-
}
|
|
690
|
-
const page = pages.get(pageId)
|
|
691
|
-
if (page === undefined) throw new Error(`${definition.name} has no settings page "${pageId}". It has: ${[...pages.keys()].join(', ') || 'none'}.`)
|
|
692
|
-
await page.handle.open({ focus: true })
|
|
693
|
-
},
|
|
694
|
-
}
|
|
695
733
|
let cwd = startCwd
|
|
696
734
|
let loadedCwd = startCwd
|
|
697
735
|
let staleCwd: string | undefined
|
|
@@ -699,7 +737,7 @@ async function createMod<State extends object, Declared extends Options>(definit
|
|
|
699
737
|
let parentAsked: Promise<string | undefined> | undefined
|
|
700
738
|
let hasEarlierPrompts = true
|
|
701
739
|
let agentType: string | undefined
|
|
702
|
-
const area = createUi<State>({ name: definition.name, claude, router,
|
|
740
|
+
const area = createUi<State>({ name: definition.name, claude, router, report, announce, mod: () => mod })
|
|
703
741
|
const askParent = async (): Promise<string | undefined> => {
|
|
704
742
|
if (definition.name === cmodPluginName) return undefined
|
|
705
743
|
try {
|
|
@@ -717,7 +755,7 @@ async function createMod<State extends object, Declared extends Options>(definit
|
|
|
717
755
|
}))
|
|
718
756
|
const modState = createState<State>({ name: definition.name, initial: definition.state ?? {}, session, root, claude, report, changed: area.changed, forkParent })
|
|
719
757
|
const modOptions = createOptions({ name: definition.name, declared: definition.options ?? {}, fromClaude: runtime.options, claude, report, changed: modState.changed })
|
|
720
|
-
const hookOn = <H extends Hook>(hook: H, listener: HookListener<H>, failed?: (input: ClassicHookInputs[H], error: Error) => HookFields | undefined):
|
|
758
|
+
const hookOn = <H extends Hook>(hook: H, listener: HookListener<H>, failed?: (input: ClassicHookInputs[H], error: Error) => HookFields | undefined): Subscription =>
|
|
721
759
|
router.add(`classic.${hook}`, classicHook(hook, listener, report), failed === undefined ? undefined : classicFailure(failed))
|
|
722
760
|
const expand = (path: string) => (homePrefix.test(path) ? resolvedPath(path, places) : path)
|
|
723
761
|
const settingsFile = (source: 'user' | 'project' | 'local'): SettingsFile => ({
|
|
@@ -737,8 +775,7 @@ async function createMod<State extends object, Declared extends Options>(definit
|
|
|
737
775
|
permissionRoutes: runtime.permissionRoutes,
|
|
738
776
|
})
|
|
739
777
|
let followed: Parameters<typeof client.follow>[0] | undefined
|
|
740
|
-
|
|
741
|
-
const watches = new Set<{ readonly paths: readonly string[]; readonly listener: (change: FileChange) => unknown }>()
|
|
778
|
+
const watches = createWatches(claude, report)
|
|
742
779
|
const followFolders = (project: string, folder: string) => {
|
|
743
780
|
if (followed === undefined) return
|
|
744
781
|
followed = { ...followed, project, cwd: folder }
|
|
@@ -794,20 +831,10 @@ async function createMod<State extends object, Declared extends Options>(definit
|
|
|
794
831
|
const { kind, size, mtimeMs, isLink, realPath } = await checked.fs.stat(expand(path), options)
|
|
795
832
|
return { isFile: kind === 'file', isFolder: kind === 'dir', isLink, size, modifiedAt: mtimeMs, ...(realPath === undefined ? {} : { realPath }) }
|
|
796
833
|
},
|
|
797
|
-
onChange({ path }, listener) {
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
watches.add(watch)
|
|
802
|
-
if (watches.size === 1) {
|
|
803
|
-
hookOn('SessionStart', () => ({ watchPaths: [...new Set([...watches].flatMap((each) => each.paths))] }))
|
|
804
|
-
hookOn('FileChanged', ({ file_path, event }) => {
|
|
805
|
-
for (const each of watches) if (each.paths.some((watched) => relativePath(watched, file_path) !== undefined)) void Promise.resolve(each.listener({ path: file_path, type: event })).catch((error: unknown) => report({ id: 'fs:onChange', message: 'The fs.onChange listener failed', error }))
|
|
806
|
-
return undefined
|
|
807
|
-
})
|
|
808
|
-
}
|
|
809
|
-
return { dispose: () => void watches.delete(watch) }
|
|
810
|
-
},
|
|
834
|
+
onChange: ({ path }, listener) => watches.add([path].flat().map((each) => resolvedPath(each, places)), (change) => {
|
|
835
|
+
bind(imports)
|
|
836
|
+
return listener(change)
|
|
837
|
+
}),
|
|
811
838
|
},
|
|
812
839
|
metadata: {
|
|
813
840
|
read: (path) => files.read(path),
|
|
@@ -818,14 +845,21 @@ async function createMod<State extends object, Declared extends Options>(definit
|
|
|
818
845
|
spawn: (argv, init) => checked.process.spawn({ ...init, argv }),
|
|
819
846
|
},
|
|
820
847
|
http: { fetch: (url, init) => checked.http.fetch(url, init) },
|
|
848
|
+
clipboard: {
|
|
849
|
+
async write(text) {
|
|
850
|
+
await toaster.copy(text)
|
|
851
|
+
},
|
|
852
|
+
},
|
|
821
853
|
}
|
|
822
854
|
const allowed = (permission: string): Allowed => ({ allows: (value) => isCovered(granted, itemOf(permission, value), home) })
|
|
823
855
|
const flag = (permission: string) => isCovered(granted, permission, home)
|
|
824
|
-
const timer = (ms: number, subject: string,
|
|
825
|
-
let
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
isRunning =
|
|
856
|
+
const timer = (ms: number, subject: string, repeats: boolean, listener: () => unknown): ModTimer => {
|
|
857
|
+
let isFiring = false
|
|
858
|
+
let isRunning = true
|
|
859
|
+
const fire = () => {
|
|
860
|
+
if (!repeats) isRunning = false
|
|
861
|
+
if (isFiring) return
|
|
862
|
+
isFiring = true
|
|
829
863
|
void Promise.resolve()
|
|
830
864
|
.then(() => {
|
|
831
865
|
bind(imports)
|
|
@@ -833,11 +867,22 @@ async function createMod<State extends object, Declared extends Options>(definit
|
|
|
833
867
|
})
|
|
834
868
|
.catch((error: unknown) => report({ id: `${subject}:${ms}`, message: `The ${subject} ${ms} ms timer failed`, error }))
|
|
835
869
|
.finally(() => {
|
|
836
|
-
|
|
870
|
+
isFiring = false
|
|
837
871
|
})
|
|
838
|
-
}
|
|
839
|
-
|
|
872
|
+
}
|
|
873
|
+
const checked = checkedMs(definition.name, `mod.${subject}`, ms)
|
|
874
|
+
const started = repeats ? claude.clock.every(checked, fire) : claude.clock.after(checked, fire)
|
|
875
|
+
return {
|
|
876
|
+
get isRunning() {
|
|
877
|
+
return isRunning
|
|
878
|
+
},
|
|
879
|
+
stop() {
|
|
880
|
+
isRunning = false
|
|
881
|
+
started.cancel()
|
|
882
|
+
},
|
|
883
|
+
}
|
|
840
884
|
}
|
|
885
|
+
const hotkeys = await createHotkeys({ name: definition.name, declared: definition.hotkeys ?? {}, configRoot, claude, router, answerInput: area.answerInput, report })
|
|
841
886
|
const mod: Mod<State, Declared> = {
|
|
842
887
|
name: definition.name,
|
|
843
888
|
folder: claude.plugin.root,
|
|
@@ -870,12 +915,18 @@ async function createMod<State extends object, Declared extends Options>(definit
|
|
|
870
915
|
files: allowed('files'),
|
|
871
916
|
config: allowed('config'),
|
|
872
917
|
},
|
|
873
|
-
|
|
874
|
-
|
|
918
|
+
hotkeys,
|
|
919
|
+
every: (ms, listener) => timer(ms, 'every', true, listener),
|
|
920
|
+
after: (ms, listener) => timer(ms, 'after', false, listener),
|
|
875
921
|
use: (job) => job({ mod, announce, reserveName }),
|
|
876
922
|
log: (text, options) => claude.ui.log(`${definition.name}: ${text}`, { to: options?.to ?? 'debug' }),
|
|
877
|
-
|
|
878
|
-
|
|
923
|
+
panes: area.panes,
|
|
924
|
+
sidebar: area.sidebar,
|
|
925
|
+
settings: area.settings,
|
|
926
|
+
widgets: area.widgets,
|
|
927
|
+
styles: area.styles,
|
|
928
|
+
toast: toaster.toast,
|
|
929
|
+
ask: (question, options) => claude.ui.ask(question, options),
|
|
879
930
|
dependencies: dependencyCalls(claude, (call, task) => beforeDeadline(claude, { ms: dependencyCallMs }, call, task)),
|
|
880
931
|
}
|
|
881
932
|
bind(imports)
|
|
@@ -947,8 +998,14 @@ async function createMod<State extends object, Declared extends Options>(definit
|
|
|
947
998
|
return answer
|
|
948
999
|
})
|
|
949
1000
|
router.add('skill.prompt', userSkillHook(claude))
|
|
950
|
-
|
|
951
|
-
|
|
1001
|
+
router.add('ui.press', (e, next) => area.asked(() => next(e)))
|
|
1002
|
+
let api: Readonly<Record<string, (input: never) => unknown>> = {}
|
|
1003
|
+
const conversations = definition.name === cmodPluginName ? serveConversations({ claude, router, store: runtime.store, project: () => modState.root, report: (id, message, error) => report({ id, message, error }), clear: (id) => toaster.toast.dismiss(id) }) : undefined
|
|
1004
|
+
let pages: Pages | undefined
|
|
1005
|
+
const reportPart = (id: string, message: string, error: unknown) => report({ id, message, error })
|
|
1006
|
+
const parts = drawnParts({ claude, report: reportPart, press: (to, input) => pages?.press(to, input) })
|
|
1007
|
+
const widgets = conversations === undefined ? undefined : serveWidgets({ claude, router, store: runtime.store, report: reportPart, turnClosedBy: (rowId) => conversations.turnClosedBy(rowId), parts })
|
|
1008
|
+
pages = widgets === undefined ? undefined : servePages({ claude, router, report: reportPart, widgets, parts })
|
|
952
1009
|
router.add('cmod.call', async (e, next) => {
|
|
953
1010
|
if (e.to !== definition.name) return next(e)
|
|
954
1011
|
const call = e as typeof e & { readonly caller?: string }
|
|
@@ -960,10 +1017,13 @@ async function createMod<State extends object, Declared extends Options>(definit
|
|
|
960
1017
|
}
|
|
961
1018
|
const ask = beforeAskOf(call)
|
|
962
1019
|
if (ask !== undefined) return { value: (await client.answerBefore(ask.session, ask.event, ask.change, ask.isInProject)) ?? null }
|
|
963
|
-
if (e.method ===
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
1020
|
+
if (e.method === partMethods.changed && call.caller !== undefined && pages !== undefined) {
|
|
1021
|
+
return { value: { isHeld: pages.update(call.caller, e.input as PartsUpdate) } }
|
|
1022
|
+
}
|
|
1023
|
+
if (call.caller === cmodPluginName) {
|
|
1024
|
+
const turn = e.method === partMethods.draw ? (e.input as { readonly turn?: TurnData }).turn : undefined
|
|
1025
|
+
const answered = area.answer(e.method, turn === undefined ? e.input : { ...(e.input as PartDraw), turn: client.turn(turn) })
|
|
1026
|
+
if (answered !== undefined) return answered
|
|
967
1027
|
}
|
|
968
1028
|
const served = await conversations?.answer(call)
|
|
969
1029
|
return served ?? answerCall(definition.name, api, e)
|
|
@@ -973,37 +1033,30 @@ async function createMod<State extends object, Declared extends Options>(definit
|
|
|
973
1033
|
return ask === undefined ? { deny: `${definition.name}: ${error.message}` } : { value: client.failedBefore(ask.session, ask.event, ask.change, ask.isInProject, error) ?? null }
|
|
974
1034
|
})
|
|
975
1035
|
const agents = toolCalls(claude, router)
|
|
976
|
-
keepEngine(mod, { claude: checked, router, toolCalls: agents, report, hasPermissionCheck: () => runtime.permissionRoutes() !== undefined })
|
|
1036
|
+
keepEngine(mod, { claude: checked, router, toolCalls: agents, report, hasPermissionCheck: () => runtime.permissionRoutes() !== undefined, answerInput: area.answerInput, ...(widgets === undefined ? {} : { widgets }), ...(pages === undefined ? {} : { pages }) })
|
|
977
1037
|
const names = new Set<string>()
|
|
978
|
-
const reserveName = (type: string, name: string, taken: string):
|
|
1038
|
+
const reserveName = (type: string, name: string, taken: string): Reservation => {
|
|
979
1039
|
const key = `${type}:${name}`
|
|
980
1040
|
if (names.has(key)) throw new Error(taken)
|
|
981
1041
|
names.add(key)
|
|
982
|
-
return {
|
|
1042
|
+
return { remove: () => void names.delete(key) }
|
|
983
1043
|
}
|
|
984
1044
|
await failsAs('its state did not load', () => modState.load())
|
|
985
1045
|
await failsAs('its options did not load', () => modOptions.load(root))
|
|
986
1046
|
const missing = modOptions.missing
|
|
987
1047
|
if (missing.length > 0) throw new MissingOptions(missing, missing.map((key) => definition.options?.[key]?.title ?? key))
|
|
988
1048
|
await failsAs('the details of its session did not load', () => followCurrent(undefined))
|
|
989
|
-
await failsAs('its setup function threw', () => definition.setup(mod))
|
|
990
|
-
|
|
1049
|
+
const returned = await failsAs('its setup function threw', () => definition.setup(mod))
|
|
1050
|
+
api = apiOf(definition.name, returned)
|
|
991
1051
|
await failsAs('its open panes did not load', () => area.restorePanes())
|
|
992
|
-
|
|
1052
|
+
area.started()
|
|
1053
|
+
return { mod, added, imports, raise: client.raise, area }
|
|
993
1054
|
}
|
|
994
1055
|
|
|
995
|
-
function
|
|
996
|
-
|
|
997
|
-
if (
|
|
998
|
-
|
|
999
|
-
for (const [key, commandName] of bound) {
|
|
1000
|
-
if (commandName === command.name && command.immediate !== true) {
|
|
1001
|
-
toast.warning(`${key} runs /${command.name}, which waits for Claude's turn to end`, { id: `key:${key}`, description: `It adds a row to the conversation. Give /${command.name} immediate: true.`, duration: Infinity })
|
|
1002
|
-
}
|
|
1003
|
-
}
|
|
1004
|
-
return claude.command.register(command)
|
|
1005
|
-
}
|
|
1006
|
-
return { ...claude, command: { ...claude.command, register: addCommand } }
|
|
1056
|
+
function apiOf(name: string, returned: unknown): Readonly<Record<string, (input: never) => unknown>> {
|
|
1057
|
+
if (returned === undefined || returned === null) return {}
|
|
1058
|
+
if (typeof returned !== 'object' || !Object.values(returned).every((method) => typeof method === 'function')) throw new Error(`${name}: setup returns its API, an object of methods other mods call, or nothing. It returned ${typeof returned === 'object' ? 'an object with values that are not methods' : typeof returned}.`)
|
|
1059
|
+
return returned as Readonly<Record<string, (input: never) => unknown>>
|
|
1007
1060
|
}
|
|
1008
1061
|
|
|
1009
1062
|
const longestTimerMs = 2_147_483_647
|
|
@@ -1021,9 +1074,9 @@ function beforeAskOf(call: Frozen<Args<'cmod.call'>> & { readonly caller?: strin
|
|
|
1021
1074
|
return { session, event, change, isInProject: isInProject !== false }
|
|
1022
1075
|
}
|
|
1023
1076
|
|
|
1024
|
-
async function failsAs(subject: string, step: () =>
|
|
1077
|
+
async function failsAs<Value>(subject: string, step: () => Value | Promise<Value>): Promise<Value> {
|
|
1025
1078
|
try {
|
|
1026
|
-
await step()
|
|
1079
|
+
return await step()
|
|
1027
1080
|
} catch (error) {
|
|
1028
1081
|
throw new Error(`${subject}: ${messageOf(error)}`, { cause: error })
|
|
1029
1082
|
}
|