@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/runtime/lifecycle.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { consentPath, dataFolder, finishStepsMethod, grantFix, coreVersion, isAtLeast, marketplaceOf, parseConsent, pendingStepsMethod, parseEvent, permissionWords, readRecord, readSteps, recordPath, scriptsSha256, storeFolder, updatesToTurnOn } from '../records.js';
|
|
2
2
|
import { homePrefix, ifMissing, relativePath } from '../utils/paths.js';
|
|
3
3
|
import { firstLine, formatExit, messageOf } from '../utils/text.js';
|
|
4
4
|
import { beforeDeadline } from './deadline.js';
|
|
@@ -9,7 +9,7 @@ import { answerCall, answeringOwnCalls, cmodPluginName, dependencyCalls, withBar
|
|
|
9
9
|
import { keepEngine } from './engine.js';
|
|
10
10
|
import { classicFailure, classicHook, userSkillHook } from './hooks.js';
|
|
11
11
|
import { checkedAnswer, checkingGrants, checkSettings, checksAnswers, checkWrite, isCovered, itemOf, passedDown, programAddresses } from './grants.js';
|
|
12
|
-
import { cmodError,
|
|
12
|
+
import { cmodError, locatingPrograms, readLines } from './programs.js';
|
|
13
13
|
import { createModFiles, resolvedPath } from './metadata.js';
|
|
14
14
|
import { fitsOption } from '../options.js';
|
|
15
15
|
import { createInstaller } from './installer.js';
|
|
@@ -20,11 +20,19 @@ import { matches } from '../utils/matcher.js';
|
|
|
20
20
|
import { createState } from './state.js';
|
|
21
21
|
import { createToaster } from './toaster.js';
|
|
22
22
|
import { toolCalls } from './tool-calls.js';
|
|
23
|
-
import {
|
|
23
|
+
import { createHotkeys } from './hotkeys.js';
|
|
24
|
+
import { servePages } from './pages.js';
|
|
25
|
+
import { drawnParts } from './parts.js';
|
|
26
|
+
import { createWatches } from './watches.js';
|
|
27
|
+
import { createUi, partMethods } from './ui.js';
|
|
28
|
+
import { serveWidgets } from './widgets.js';
|
|
24
29
|
import { Progress } from '../ui/components.js';
|
|
25
30
|
import { Box, Text } from '../ui/elements.js';
|
|
26
31
|
const announcedKey = 'cmod:announced';
|
|
32
|
+
const cmodUpdatedKey = 'cmod:cmod-updated-for';
|
|
27
33
|
const installId = 'cmod:install';
|
|
34
|
+
const askingId = 'cmod:asking';
|
|
35
|
+
const recordId = 'cmod:record';
|
|
28
36
|
const installRetrySeconds = [2, 4, 8, 16, 32, 64, 128];
|
|
29
37
|
const cmodCheckMs = 1000;
|
|
30
38
|
const cmodWaitMs = 60_000;
|
|
@@ -32,7 +40,6 @@ const cmodSlowCheckMs = 10_000;
|
|
|
32
40
|
const hookMs = 10_000;
|
|
33
41
|
const startHoldMs = hookMs - 1_000;
|
|
34
42
|
const dependencyCallMs = 30_000;
|
|
35
|
-
const cannotStart = /failed to start: /;
|
|
36
43
|
const shellTools = ['Bash', 'PowerShell'];
|
|
37
44
|
export async function readPlugin(claude) {
|
|
38
45
|
const { name, root } = claude.plugin;
|
|
@@ -58,7 +65,10 @@ export async function readPlugin(claude) {
|
|
|
58
65
|
read: (path) => read(`${root}/${path}`),
|
|
59
66
|
list: (folder) => filesBelow(claude, `${root}/${folder}`),
|
|
60
67
|
});
|
|
61
|
-
|
|
68
|
+
if (version === undefined || record?.scriptsSha256 !== scripts)
|
|
69
|
+
return { ...plugin, isInstalled: false, shouldRecord: false };
|
|
70
|
+
const isInstalled = record.version === version || record.root === root;
|
|
71
|
+
return { ...plugin, isInstalled, shouldRecord: isInstalled && record.version !== version };
|
|
62
72
|
}
|
|
63
73
|
async function filesBelow(claude, folder) {
|
|
64
74
|
const entries = await claude.fs.list(folder);
|
|
@@ -83,17 +93,18 @@ async function readJson(read, path) {
|
|
|
83
93
|
}
|
|
84
94
|
}
|
|
85
95
|
async function cmodVersion(claude) {
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
throw error;
|
|
90
|
-
});
|
|
91
|
-
if (result === undefined)
|
|
96
|
+
const store = storeFolder({ HOME: await claude.env.home(), XDG_DATA_HOME: await claude.env.dataHome() });
|
|
97
|
+
const exists = async (path) => (await claude.fs.stat(path).catch(ifMissing(undefined))) !== undefined;
|
|
98
|
+
if (!(await exists(`${store}/programs/cmod`)))
|
|
92
99
|
return undefined;
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
100
|
+
const versions = (await claude.fs.list(`${store}/bin/cmod`).catch(ifMissing([]))).filter((entry) => entry.kind === 'dir' && releaseVersion.test(entry.name)).map((entry) => entry.name);
|
|
101
|
+
const newestFirst = versions.toSorted((one, other) => (isAtLeast(one, other) ? -1 : 1));
|
|
102
|
+
for (const version of newestFirst)
|
|
103
|
+
if (await exists(`${store}/bin/cmod/${version}/cmod`))
|
|
104
|
+
return version;
|
|
105
|
+
return undefined;
|
|
96
106
|
}
|
|
107
|
+
const releaseVersion = /^\d+\.\d+\.\d+$/;
|
|
97
108
|
export function createLifecycle(definition, permissionRoutes = () => undefined, options = {}, asksPerson = true) {
|
|
98
109
|
let router = createRouter();
|
|
99
110
|
let phase = 'starting';
|
|
@@ -103,14 +114,25 @@ export function createLifecycle(definition, permissionRoutes = () => undefined,
|
|
|
103
114
|
const granted = new Set();
|
|
104
115
|
let activation;
|
|
105
116
|
let mod;
|
|
117
|
+
const openSettings = async (tab) => {
|
|
118
|
+
await claude().cmod.call({ to: cmodPluginName, method: partMethods.changed, input: { requests: [{ settings: true, ...(tab === undefined ? {} : { tab }) }], isAsked: true } });
|
|
119
|
+
};
|
|
106
120
|
let imports;
|
|
107
121
|
let raise;
|
|
108
122
|
let failure;
|
|
109
123
|
let pluginOptions = options;
|
|
110
|
-
const installer = createInstaller(definition.name, () => claude()
|
|
124
|
+
const installer = createInstaller(definition.name, () => claude(), {
|
|
125
|
+
show: (open) => {
|
|
126
|
+
if (phase !== 'active')
|
|
127
|
+
runtime?.toaster.toast.dismiss(installId);
|
|
128
|
+
runtime?.toaster.toast.info('Installing', { id: askingId, description: 'Waiting for your answer', duration: Infinity, action: { label: 'Answer', onClick: open } });
|
|
129
|
+
},
|
|
130
|
+
hide: () => runtime?.toaster.toast.dismiss(askingId),
|
|
131
|
+
});
|
|
111
132
|
let shouldRecord = false;
|
|
112
133
|
let recording;
|
|
113
134
|
let missedSessionStart;
|
|
135
|
+
let hasHeardSessionStart = false;
|
|
114
136
|
let settleStart = () => undefined;
|
|
115
137
|
const startSettled = new Promise((resolve) => (settleStart = resolve));
|
|
116
138
|
const claude = () => {
|
|
@@ -120,14 +142,15 @@ export function createLifecycle(definition, permissionRoutes = () => undefined,
|
|
|
120
142
|
};
|
|
121
143
|
const installing = (description) => runtime?.toaster.toast.loading('Installing', { id: installId, ...(description === undefined ? {} : { description }) });
|
|
122
144
|
const toastFailure = (shown) => runtime?.toaster.fail(shown);
|
|
123
|
-
const fail = (reason, fix, retry) => {
|
|
145
|
+
const fail = (reason, fix, retry, error) => {
|
|
124
146
|
phase = 'failed';
|
|
125
|
-
failure ??= new Error(`${definition.name}: ${reason}${/[.!?]$/.test(reason) ? '' : '.'}${fix === undefined ? '' : ` ${fix}`}
|
|
147
|
+
failure ??= new Error(`${definition.name}: ${reason}${/[.!?]$/.test(reason) ? '' : '.'}${fix === undefined ? '' : ` ${fix}`}`, { cause: error });
|
|
126
148
|
const again = retry === undefined ? {} : { retry: () => ((failure = undefined), stopRetrying(), retry().catch(report)) };
|
|
127
|
-
toastFailure({ id: installId, message: reason, ...(fix === undefined ? {} : { description: fix }), ...again });
|
|
149
|
+
toastFailure({ id: installId, message: reason, ...(fix === undefined ? {} : { description: fix }), ...(error === undefined ? {} : { error }), ...again });
|
|
128
150
|
};
|
|
129
151
|
let consented;
|
|
130
152
|
const installAgain = () => install(consented);
|
|
153
|
+
const updateAgain = () => claude().store.delete(cmodUpdatedKey).then(installAgain);
|
|
131
154
|
let retrying;
|
|
132
155
|
const retryLater = (next) => {
|
|
133
156
|
const seconds = installRetrySeconds[Math.min(next.tries, installRetrySeconds.length - 1)];
|
|
@@ -139,47 +162,47 @@ export function createLifecycle(definition, permissionRoutes = () => undefined,
|
|
|
139
162
|
retrying?.timer.cancel();
|
|
140
163
|
retrying = undefined;
|
|
141
164
|
};
|
|
142
|
-
const
|
|
143
|
-
if (plugin === undefined)
|
|
144
|
-
return undefined;
|
|
145
|
-
const lines = [];
|
|
146
|
-
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));
|
|
147
|
-
return code === 0 ? lines.join('\n') : undefined;
|
|
148
|
-
};
|
|
149
|
-
const failInstall = async (reason, when) => {
|
|
165
|
+
const failInstall = (reason, when) => {
|
|
150
166
|
if (when === undefined) {
|
|
151
167
|
stopRetrying();
|
|
152
168
|
return fail(reason, undefined, installAgain);
|
|
153
169
|
}
|
|
154
|
-
retryLater({ when, tries: retrying?.when === when ? retrying.tries : 0,
|
|
170
|
+
retryLater({ when, tries: retrying?.when === when ? retrying.tries : 0, reason });
|
|
155
171
|
};
|
|
156
172
|
const retryInBackground = async () => {
|
|
157
|
-
|
|
158
|
-
if (phase !== 'failed' || waiting === undefined)
|
|
173
|
+
if (phase !== 'failed' || retrying === undefined)
|
|
159
174
|
return;
|
|
160
175
|
if (await recover()) {
|
|
161
176
|
stopRetrying();
|
|
162
177
|
return whenActive();
|
|
163
178
|
}
|
|
164
|
-
if (waiting.when === 'changed' && waiting.checkout !== undefined && (await checkoutState()) === waiting.checkout)
|
|
165
|
-
return retryLater(waiting);
|
|
166
179
|
await install(consented, true);
|
|
167
180
|
await whenActive();
|
|
168
181
|
};
|
|
169
182
|
const finish = async () => {
|
|
170
183
|
await programs?.refresh();
|
|
171
184
|
phase = 'ready';
|
|
185
|
+
activation ??= new Promise((resolve) => claude().clock.after(0, () => resolve(activate().catch(report))));
|
|
172
186
|
claude().ui.invalidate('ui.render');
|
|
173
187
|
};
|
|
174
188
|
const recordThroughCmod = async () => {
|
|
175
189
|
if (plugin === undefined || (await cmodVersion(claude())) === undefined)
|
|
176
190
|
return;
|
|
191
|
+
let failed;
|
|
177
192
|
const { code, lastError } = await readLines(claude().process.spawn({ argv: ['cmod', 'setup', plugin.root, '--events'] }), (text) => {
|
|
178
|
-
|
|
193
|
+
const event = parseEvent(text);
|
|
194
|
+
if (event.type === 'done')
|
|
179
195
|
shouldRecord = false;
|
|
196
|
+
if (event.type === 'failed')
|
|
197
|
+
failed = event.message;
|
|
180
198
|
});
|
|
181
|
-
if (shouldRecord)
|
|
182
|
-
|
|
199
|
+
if (!shouldRecord) {
|
|
200
|
+
runtime?.toaster.toast.dismiss(recordId);
|
|
201
|
+
return programs?.refresh();
|
|
202
|
+
}
|
|
203
|
+
if (failed !== undefined)
|
|
204
|
+
return void toastFailure({ id: recordId, message: failed, retry: record });
|
|
205
|
+
throw new Error(`cmod setup ${formatExit(code, lastError)}`);
|
|
183
206
|
};
|
|
184
207
|
const record = () => {
|
|
185
208
|
recording ??= recordThroughCmod()
|
|
@@ -239,9 +262,9 @@ export function createLifecycle(definition, permissionRoutes = () => undefined,
|
|
|
239
262
|
if (!isAnswered) {
|
|
240
263
|
runtime?.toaster.toast.warning(steps.length - index === 1 ? 'Needs one more step' : `Needs ${steps.length - index} more steps`, {
|
|
241
264
|
id: 'cmod:steps',
|
|
242
|
-
description: `Starting with ${step.title}
|
|
265
|
+
description: `Starting with ${step.title}.`,
|
|
243
266
|
duration: Infinity,
|
|
244
|
-
action: { label: 'Finish', onClick: () => active
|
|
267
|
+
action: { label: 'Finish', onClick: () => runSteps(active) },
|
|
245
268
|
});
|
|
246
269
|
break;
|
|
247
270
|
}
|
|
@@ -255,7 +278,7 @@ export function createLifecycle(definition, permissionRoutes = () => undefined,
|
|
|
255
278
|
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) => {
|
|
256
279
|
if (error instanceof MissingOptions && (await askOptions(error)))
|
|
257
280
|
return undefined;
|
|
258
|
-
fail(messageOf(error), error instanceof MissingOptions ? `
|
|
281
|
+
fail(messageOf(error), error instanceof MissingOptions ? `Retry asks for ${error.keys.length === 1 ? 'it' : 'them'}.` : undefined, begin, error);
|
|
259
282
|
throw error;
|
|
260
283
|
});
|
|
261
284
|
if (active === undefined)
|
|
@@ -270,20 +293,8 @@ export function createLifecycle(definition, permissionRoutes = () => undefined,
|
|
|
270
293
|
void runSteps(active.mod).catch((error) => toastFailure({ id: 'cmod:steps', message: 'Its installer stopped', error }));
|
|
271
294
|
const missed = missedSessionStart;
|
|
272
295
|
missedSessionStart = undefined;
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
toastFailure({ id: 'classic.SessionStart', message: 'The SessionStart hook failed', error });
|
|
276
|
-
return {};
|
|
277
|
-
});
|
|
278
|
-
const { additionalContext, ...unread } = lateAnswer;
|
|
279
|
-
for (const text of additionalContext ?? []) {
|
|
280
|
-
const added = await runtime.claude.session.append({ message: { type: 'user', content: [{ type: 'text', text }] } });
|
|
281
|
-
if (added.deny !== undefined)
|
|
282
|
-
runtime.claude.ui.log(`${definition.name}: Claude Code refused its SessionStart note: ${added.deny}`, { to: 'debug' });
|
|
283
|
-
}
|
|
284
|
-
if (Object.keys(unread).length > 0) {
|
|
285
|
-
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' });
|
|
286
|
-
}
|
|
296
|
+
if (missed !== undefined)
|
|
297
|
+
await replaySessionStart(missed);
|
|
287
298
|
const version = plugin.version ?? '';
|
|
288
299
|
const announced = announcedOf(await runtime.claude.store.get(announcedKey));
|
|
289
300
|
if (announced?.version === version) {
|
|
@@ -292,10 +303,27 @@ export function createLifecycle(definition, permissionRoutes = () => undefined,
|
|
|
292
303
|
}
|
|
293
304
|
await runtime.claude.store.set(announcedKey, { version, added });
|
|
294
305
|
const { toaster } = runtime;
|
|
295
|
-
void announcementOf(runtime.claude, plugin, granted, added, announced)
|
|
306
|
+
void announcementOf(runtime.claude, plugin, granted, added, announced, openSettings)
|
|
296
307
|
.then(({ message, ...options }) => toaster.toast.success(message, { id: installId, duration: announcementMs, ...options }))
|
|
297
308
|
.catch((error) => toastFailure({ id: installId, message: 'Could not say what it installed', error }));
|
|
298
309
|
};
|
|
310
|
+
const replaySessionStart = async (e) => {
|
|
311
|
+
hasHeardSessionStart = true;
|
|
312
|
+
const ran = definition.name === cmodPluginName ? router.dispatch('classic.SessionStart', e, async () => ({})) : checkedRoute('classic.SessionStart', e, async () => ({}));
|
|
313
|
+
const answer = await ran.catch((error) => {
|
|
314
|
+
toastFailure({ id: 'classic.SessionStart', message: 'The SessionStart hook failed', error });
|
|
315
|
+
return {};
|
|
316
|
+
});
|
|
317
|
+
const { additionalContext, ...unread } = answer;
|
|
318
|
+
for (const text of additionalContext ?? []) {
|
|
319
|
+
const added = await claude().session.append({ message: { type: 'user', content: [{ type: 'text', text }] } });
|
|
320
|
+
if (added.deny !== undefined)
|
|
321
|
+
claude().ui.log(`${definition.name}: Claude Code refused its SessionStart note: ${added.deny}`, { to: 'debug' });
|
|
322
|
+
}
|
|
323
|
+
if (Object.keys(unread).length > 0) {
|
|
324
|
+
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' });
|
|
325
|
+
}
|
|
326
|
+
};
|
|
299
327
|
const updatesToShow = async () => {
|
|
300
328
|
const [home, configHome] = await Promise.all([claude().env.home(), claude().env.configHome()]);
|
|
301
329
|
const configRoot = configHome ?? `${home ?? ''}/.claude`;
|
|
@@ -311,7 +339,6 @@ export function createLifecycle(definition, permissionRoutes = () => undefined,
|
|
|
311
339
|
claude().ui.log(`${name} waits for consent to run ${event.install}. Run cmod install ${name} in a terminal.`);
|
|
312
340
|
return;
|
|
313
341
|
}
|
|
314
|
-
runtime?.toaster.toast.info('Installing', { id: installId, description: 'Waiting for your answer', duration: Infinity });
|
|
315
342
|
const updates = await updatesToShow().catch((error) => {
|
|
316
343
|
toastFailure({ id: 'cmod:updates', message: 'Could not read whether its marketplace updates automatically', error });
|
|
317
344
|
return undefined;
|
|
@@ -358,7 +385,7 @@ export function createLifecycle(definition, permissionRoutes = () => undefined,
|
|
|
358
385
|
return askConsent(outcome);
|
|
359
386
|
return failInstall(outcome?.type === 'failed' ? outcome.message : `cmod setup ${formatExit(code, lastError)}`, retries);
|
|
360
387
|
};
|
|
361
|
-
const fits = (found) => found !== undefined && isAtLeast(found,
|
|
388
|
+
const fits = (found) => found !== undefined && isAtLeast(found, coreVersion);
|
|
362
389
|
const waitForcmod = (found) => {
|
|
363
390
|
phase = 'waiting';
|
|
364
391
|
installing('Waiting for cmod');
|
|
@@ -387,9 +414,26 @@ export function createLifecycle(definition, permissionRoutes = () => undefined,
|
|
|
387
414
|
const longWait = claude().clock.after(cmodWaitMs, () => {
|
|
388
415
|
timer.cancel();
|
|
389
416
|
timer = poll(cmodSlowCheckMs);
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
417
|
+
if (latest === undefined)
|
|
418
|
+
return installing("Still waiting for cmod to download cmod. See cmod's own line.");
|
|
419
|
+
const found = latest;
|
|
420
|
+
const tried = `${found} to ${coreVersion}`;
|
|
421
|
+
return claude()
|
|
422
|
+
.store.get(cmodUpdatedKey)
|
|
423
|
+
.then(async (updated) => {
|
|
424
|
+
if (updated === tried) {
|
|
425
|
+
stop();
|
|
426
|
+
return fail(`${definition.name} needs cmod ${coreVersion} or later, and the newest cmod is ${found}.`, undefined, updateAgain);
|
|
427
|
+
}
|
|
428
|
+
await claude().store.set(cmodUpdatedKey, tried);
|
|
429
|
+
installing(`${definition.name} needs cmod ${coreVersion} or later, and this computer has cmod ${found}, so cmod is updating itself.`);
|
|
430
|
+
const { code, lastError } = await readLines(claude().process.spawn({ argv: ['cmod', 'update', 'cmod'] }), () => undefined);
|
|
431
|
+
if (code === 0)
|
|
432
|
+
return;
|
|
433
|
+
stop();
|
|
434
|
+
fail(`${definition.name} needs cmod ${coreVersion} or later, and updating cmod ${found} ${formatExit(code, lastError)}`, undefined, updateAgain);
|
|
435
|
+
})
|
|
436
|
+
.catch(report);
|
|
393
437
|
});
|
|
394
438
|
};
|
|
395
439
|
const bootstrap = async (root) => {
|
|
@@ -427,7 +471,7 @@ export function createLifecycle(definition, permissionRoutes = () => undefined,
|
|
|
427
471
|
const report = (error) => {
|
|
428
472
|
failure = error;
|
|
429
473
|
if (phase !== 'failed')
|
|
430
|
-
fail(messageOf(error), undefined, begin);
|
|
474
|
+
fail(messageOf(error), undefined, begin, error);
|
|
431
475
|
};
|
|
432
476
|
let consentMs = 0;
|
|
433
477
|
const stampOf = async (path) => {
|
|
@@ -479,8 +523,13 @@ export function createLifecycle(definition, permissionRoutes = () => undefined,
|
|
|
479
523
|
return;
|
|
480
524
|
droppedParts.add(item);
|
|
481
525
|
const declared = plugin?.steps.permissions ?? [];
|
|
482
|
-
const
|
|
483
|
-
runtime?.toaster.toast.warning(`Answered ${part} without your grant to "${permissionWords(item)}", so cmod dropped it`, {
|
|
526
|
+
const isDeclared = declared.includes(item);
|
|
527
|
+
runtime?.toaster.toast.warning(`Answered ${part} without your grant to "${permissionWords(item)}", so cmod dropped it`, {
|
|
528
|
+
id: `dropped:${item}`,
|
|
529
|
+
description: isDeclared ? grantFix : `It needs "permissions": { "${item}": true } in package.json "cmod".`,
|
|
530
|
+
duration: Infinity,
|
|
531
|
+
...(isDeclared ? { action: { label: 'Settings', onClick: () => openSettings('permissions') } } : {}),
|
|
532
|
+
});
|
|
484
533
|
},
|
|
485
534
|
};
|
|
486
535
|
const checkedRoute = async (event, e, next) => {
|
|
@@ -527,7 +576,7 @@ export function createLifecycle(definition, permissionRoutes = () => undefined,
|
|
|
527
576
|
if (runtime !== undefined)
|
|
528
577
|
return;
|
|
529
578
|
programs = locatingPrograms(claudeCalls);
|
|
530
|
-
runtime = { claude: programs.claude, toaster: createToaster(claudeCalls, definition.name, () => plugin?.version) };
|
|
579
|
+
runtime = { claude: programs.claude, toaster: createToaster(claudeCalls, definition.name, () => plugin?.version, () => openSettings('permissions')) };
|
|
531
580
|
readStarted = read;
|
|
532
581
|
try {
|
|
533
582
|
await programs.refresh();
|
|
@@ -544,8 +593,10 @@ export function createLifecycle(definition, permissionRoutes = () => undefined,
|
|
|
544
593
|
return installer.draw(e);
|
|
545
594
|
if (event === 'ui.close')
|
|
546
595
|
installer.closed(e.id);
|
|
547
|
-
if (event === 'classic.SessionStart')
|
|
596
|
+
if (event === 'classic.SessionStart') {
|
|
548
597
|
missedSessionStart = undefined;
|
|
598
|
+
hasHeardSessionStart = true;
|
|
599
|
+
}
|
|
549
600
|
if (event === 'classic.SessionStart' && runtime === undefined) {
|
|
550
601
|
missedSessionStart = e;
|
|
551
602
|
return next(e);
|
|
@@ -575,6 +626,10 @@ export function createLifecycle(definition, permissionRoutes = () => undefined,
|
|
|
575
626
|
}
|
|
576
627
|
if (event === 'classic.UserPromptSubmit' || event === 'classic.SessionStart')
|
|
577
628
|
await refreshGrants().catch((error) => claude().ui.log(`${definition.name} keeps its grants from before: ${messageOf(error)}`, { to: 'debug' }));
|
|
629
|
+
if (event === 'classic.UserPromptSubmit' && phase === 'active' && !hasHeardSessionStart) {
|
|
630
|
+
const { session_id, transcript_path, cwd } = e;
|
|
631
|
+
await replaySessionStart({ session_id, transcript_path, cwd, hook_event_name: 'SessionStart', source: 'resume' });
|
|
632
|
+
}
|
|
578
633
|
if (checksAnswers(event) && phase === 'active' && definition.name !== cmodPluginName)
|
|
579
634
|
return checkedRoute(event, e, next);
|
|
580
635
|
const answer = router.dispatch(event, e, next);
|
|
@@ -599,7 +654,7 @@ export function createLifecycle(definition, permissionRoutes = () => undefined,
|
|
|
599
654
|
async function createMod(definition, runtime) {
|
|
600
655
|
const { router, toaster } = runtime;
|
|
601
656
|
const report = (failure) => void toaster.fail(failure);
|
|
602
|
-
const claude = answeringOwnCalls(withBareRefusals(
|
|
657
|
+
const claude = answeringOwnCalls(withBareRefusals(runtime.claude), router);
|
|
603
658
|
const added = [];
|
|
604
659
|
const announce = (feature) => {
|
|
605
660
|
added.push(feature);
|
|
@@ -627,26 +682,6 @@ async function createMod(definition, runtime) {
|
|
|
627
682
|
checkWrite: (path) => checkWrite(grants, `metadata.update(${path})`, path),
|
|
628
683
|
report,
|
|
629
684
|
});
|
|
630
|
-
const pages = new Map();
|
|
631
|
-
const settingsPages = {
|
|
632
|
-
page(page) {
|
|
633
|
-
if (pages.has(page.id))
|
|
634
|
-
throw new Error(`${definition.name}: the settings page "${page.id}" is already added. Give each page its own id.`);
|
|
635
|
-
if (page.id === changelogPage)
|
|
636
|
-
throw new Error(`${definition.name}: the settings page id "${changelogPage}" opens the mod's CHANGELOG.md in /mods. Give the page another id.`);
|
|
637
|
-
pages.set(page.id, { title: page.title, handle: area.ui.pane(page) });
|
|
638
|
-
},
|
|
639
|
-
async open(pageId) {
|
|
640
|
-
if (pageId === undefined) {
|
|
641
|
-
await claude.cmod.call({ to: cmodPluginName, method: 'openSettings', input: { mod: definition.name } });
|
|
642
|
-
return;
|
|
643
|
-
}
|
|
644
|
-
const page = pages.get(pageId);
|
|
645
|
-
if (page === undefined)
|
|
646
|
-
throw new Error(`${definition.name} has no settings page "${pageId}". It has: ${[...pages.keys()].join(', ') || 'none'}.`);
|
|
647
|
-
await page.handle.open({ focus: true });
|
|
648
|
-
},
|
|
649
|
-
};
|
|
650
685
|
let cwd = startCwd;
|
|
651
686
|
let loadedCwd = startCwd;
|
|
652
687
|
let staleCwd;
|
|
@@ -654,7 +689,7 @@ async function createMod(definition, runtime) {
|
|
|
654
689
|
let parentAsked;
|
|
655
690
|
let hasEarlierPrompts = true;
|
|
656
691
|
let agentType;
|
|
657
|
-
const area = createUi({ name: definition.name, claude, router,
|
|
692
|
+
const area = createUi({ name: definition.name, claude, router, report, announce, mod: () => mod });
|
|
658
693
|
const askParent = async () => {
|
|
659
694
|
if (definition.name === cmodPluginName)
|
|
660
695
|
return undefined;
|
|
@@ -692,8 +727,7 @@ async function createMod(definition, runtime) {
|
|
|
692
727
|
permissionRoutes: runtime.permissionRoutes,
|
|
693
728
|
});
|
|
694
729
|
let followed;
|
|
695
|
-
|
|
696
|
-
const watches = new Set();
|
|
730
|
+
const watches = createWatches(claude, report);
|
|
697
731
|
const followFolders = (project, folder) => {
|
|
698
732
|
if (followed === undefined)
|
|
699
733
|
return;
|
|
@@ -750,23 +784,10 @@ async function createMod(definition, runtime) {
|
|
|
750
784
|
const { kind, size, mtimeMs, isLink, realPath } = await checked.fs.stat(expand(path), options);
|
|
751
785
|
return { isFile: kind === 'file', isFolder: kind === 'dir', isLink, size, modifiedAt: mtimeMs, ...(realPath === undefined ? {} : { realPath }) };
|
|
752
786
|
},
|
|
753
|
-
onChange({ path }, listener) {
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
const watch = { paths, listener };
|
|
758
|
-
watches.add(watch);
|
|
759
|
-
if (watches.size === 1) {
|
|
760
|
-
hookOn('SessionStart', () => ({ watchPaths: [...new Set([...watches].flatMap((each) => each.paths))] }));
|
|
761
|
-
hookOn('FileChanged', ({ file_path, event }) => {
|
|
762
|
-
for (const each of watches)
|
|
763
|
-
if (each.paths.some((watched) => relativePath(watched, file_path) !== undefined))
|
|
764
|
-
void Promise.resolve(each.listener({ path: file_path, type: event })).catch((error) => report({ id: 'fs:onChange', message: 'The fs.onChange listener failed', error }));
|
|
765
|
-
return undefined;
|
|
766
|
-
});
|
|
767
|
-
}
|
|
768
|
-
return { dispose: () => void watches.delete(watch) };
|
|
769
|
-
},
|
|
787
|
+
onChange: ({ path }, listener) => watches.add([path].flat().map((each) => resolvedPath(each, places)), (change) => {
|
|
788
|
+
bind(imports);
|
|
789
|
+
return listener(change);
|
|
790
|
+
}),
|
|
770
791
|
},
|
|
771
792
|
metadata: {
|
|
772
793
|
read: (path) => files.read(path),
|
|
@@ -777,15 +798,23 @@ async function createMod(definition, runtime) {
|
|
|
777
798
|
spawn: (argv, init) => checked.process.spawn({ ...init, argv }),
|
|
778
799
|
},
|
|
779
800
|
http: { fetch: (url, init) => checked.http.fetch(url, init) },
|
|
801
|
+
clipboard: {
|
|
802
|
+
async write(text) {
|
|
803
|
+
await toaster.copy(text);
|
|
804
|
+
},
|
|
805
|
+
},
|
|
780
806
|
};
|
|
781
807
|
const allowed = (permission) => ({ allows: (value) => isCovered(granted, itemOf(permission, value), home) });
|
|
782
808
|
const flag = (permission) => isCovered(granted, permission, home);
|
|
783
|
-
const timer = (ms, subject,
|
|
784
|
-
let
|
|
785
|
-
|
|
786
|
-
|
|
809
|
+
const timer = (ms, subject, repeats, listener) => {
|
|
810
|
+
let isFiring = false;
|
|
811
|
+
let isRunning = true;
|
|
812
|
+
const fire = () => {
|
|
813
|
+
if (!repeats)
|
|
814
|
+
isRunning = false;
|
|
815
|
+
if (isFiring)
|
|
787
816
|
return;
|
|
788
|
-
|
|
817
|
+
isFiring = true;
|
|
789
818
|
void Promise.resolve()
|
|
790
819
|
.then(() => {
|
|
791
820
|
bind(imports);
|
|
@@ -793,11 +822,22 @@ async function createMod(definition, runtime) {
|
|
|
793
822
|
})
|
|
794
823
|
.catch((error) => report({ id: `${subject}:${ms}`, message: `The ${subject} ${ms} ms timer failed`, error }))
|
|
795
824
|
.finally(() => {
|
|
796
|
-
|
|
825
|
+
isFiring = false;
|
|
797
826
|
});
|
|
798
|
-
}
|
|
799
|
-
|
|
827
|
+
};
|
|
828
|
+
const checked = checkedMs(definition.name, `mod.${subject}`, ms);
|
|
829
|
+
const started = repeats ? claude.clock.every(checked, fire) : claude.clock.after(checked, fire);
|
|
830
|
+
return {
|
|
831
|
+
get isRunning() {
|
|
832
|
+
return isRunning;
|
|
833
|
+
},
|
|
834
|
+
stop() {
|
|
835
|
+
isRunning = false;
|
|
836
|
+
started.cancel();
|
|
837
|
+
},
|
|
838
|
+
};
|
|
800
839
|
};
|
|
840
|
+
const hotkeys = await createHotkeys({ name: definition.name, declared: definition.hotkeys ?? {}, configRoot, claude, router, answerInput: area.answerInput, report });
|
|
801
841
|
const mod = {
|
|
802
842
|
name: definition.name,
|
|
803
843
|
folder: claude.plugin.root,
|
|
@@ -830,12 +870,18 @@ async function createMod(definition, runtime) {
|
|
|
830
870
|
files: allowed('files'),
|
|
831
871
|
config: allowed('config'),
|
|
832
872
|
},
|
|
833
|
-
|
|
834
|
-
|
|
873
|
+
hotkeys,
|
|
874
|
+
every: (ms, listener) => timer(ms, 'every', true, listener),
|
|
875
|
+
after: (ms, listener) => timer(ms, 'after', false, listener),
|
|
835
876
|
use: (job) => job({ mod, announce, reserveName }),
|
|
836
877
|
log: (text, options) => claude.ui.log(`${definition.name}: ${text}`, { to: options?.to ?? 'debug' }),
|
|
837
|
-
|
|
838
|
-
|
|
878
|
+
panes: area.panes,
|
|
879
|
+
sidebar: area.sidebar,
|
|
880
|
+
settings: area.settings,
|
|
881
|
+
widgets: area.widgets,
|
|
882
|
+
styles: area.styles,
|
|
883
|
+
toast: toaster.toast,
|
|
884
|
+
ask: (question, options) => claude.ui.ask(question, options),
|
|
839
885
|
dependencies: dependencyCalls(claude, (call, task) => beforeDeadline(claude, { ms: dependencyCallMs }, call, task)),
|
|
840
886
|
};
|
|
841
887
|
bind(imports);
|
|
@@ -917,8 +963,14 @@ async function createMod(definition, runtime) {
|
|
|
917
963
|
return answer;
|
|
918
964
|
});
|
|
919
965
|
router.add('skill.prompt', userSkillHook(claude));
|
|
920
|
-
|
|
921
|
-
|
|
966
|
+
router.add('ui.press', (e, next) => area.asked(() => next(e)));
|
|
967
|
+
let api = {};
|
|
968
|
+
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;
|
|
969
|
+
let pages;
|
|
970
|
+
const reportPart = (id, message, error) => report({ id, message, error });
|
|
971
|
+
const parts = drawnParts({ claude, report: reportPart, press: (to, input) => pages?.press(to, input) });
|
|
972
|
+
const widgets = conversations === undefined ? undefined : serveWidgets({ claude, router, store: runtime.store, report: reportPart, turnClosedBy: (rowId) => conversations.turnClosedBy(rowId), parts });
|
|
973
|
+
pages = widgets === undefined ? undefined : servePages({ claude, router, report: reportPart, widgets, parts });
|
|
922
974
|
router.add('cmod.call', async (e, next) => {
|
|
923
975
|
if (e.to !== definition.name)
|
|
924
976
|
return next(e);
|
|
@@ -933,11 +985,14 @@ async function createMod(definition, runtime) {
|
|
|
933
985
|
const ask = beforeAskOf(call);
|
|
934
986
|
if (ask !== undefined)
|
|
935
987
|
return { value: (await client.answerBefore(ask.session, ask.event, ask.change, ask.isInProject)) ?? null };
|
|
936
|
-
if (e.method ===
|
|
937
|
-
return { value:
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
988
|
+
if (e.method === partMethods.changed && call.caller !== undefined && pages !== undefined) {
|
|
989
|
+
return { value: { isHeld: pages.update(call.caller, e.input) } };
|
|
990
|
+
}
|
|
991
|
+
if (call.caller === cmodPluginName) {
|
|
992
|
+
const turn = e.method === partMethods.draw ? e.input.turn : undefined;
|
|
993
|
+
const answered = area.answer(e.method, turn === undefined ? e.input : { ...e.input, turn: client.turn(turn) });
|
|
994
|
+
if (answered !== undefined)
|
|
995
|
+
return answered;
|
|
941
996
|
}
|
|
942
997
|
const served = await conversations?.answer(call);
|
|
943
998
|
return served ?? answerCall(definition.name, api, e);
|
|
@@ -948,14 +1003,14 @@ async function createMod(definition, runtime) {
|
|
|
948
1003
|
return ask === undefined ? { deny: `${definition.name}: ${error.message}` } : { value: client.failedBefore(ask.session, ask.event, ask.change, ask.isInProject, error) ?? null };
|
|
949
1004
|
});
|
|
950
1005
|
const agents = toolCalls(claude, router);
|
|
951
|
-
keepEngine(mod, { claude: checked, router, toolCalls: agents, report, hasPermissionCheck: () => runtime.permissionRoutes() !== undefined });
|
|
1006
|
+
keepEngine(mod, { claude: checked, router, toolCalls: agents, report, hasPermissionCheck: () => runtime.permissionRoutes() !== undefined, answerInput: area.answerInput, ...(widgets === undefined ? {} : { widgets }), ...(pages === undefined ? {} : { pages }) });
|
|
952
1007
|
const names = new Set();
|
|
953
1008
|
const reserveName = (type, name, taken) => {
|
|
954
1009
|
const key = `${type}:${name}`;
|
|
955
1010
|
if (names.has(key))
|
|
956
1011
|
throw new Error(taken);
|
|
957
1012
|
names.add(key);
|
|
958
|
-
return {
|
|
1013
|
+
return { remove: () => void names.delete(key) };
|
|
959
1014
|
};
|
|
960
1015
|
await failsAs('its state did not load', () => modState.load());
|
|
961
1016
|
await failsAs('its options did not load', () => modOptions.load(root));
|
|
@@ -963,24 +1018,18 @@ async function createMod(definition, runtime) {
|
|
|
963
1018
|
if (missing.length > 0)
|
|
964
1019
|
throw new MissingOptions(missing, missing.map((key) => definition.options?.[key]?.title ?? key));
|
|
965
1020
|
await failsAs('the details of its session did not load', () => followCurrent(undefined));
|
|
966
|
-
await failsAs('its setup function threw', () => definition.setup(mod));
|
|
967
|
-
|
|
1021
|
+
const returned = await failsAs('its setup function threw', () => definition.setup(mod));
|
|
1022
|
+
api = apiOf(definition.name, returned);
|
|
968
1023
|
await failsAs('its open panes did not load', () => area.restorePanes());
|
|
969
|
-
|
|
1024
|
+
area.started();
|
|
1025
|
+
return { mod, added, imports, raise: client.raise, area };
|
|
970
1026
|
}
|
|
971
|
-
function
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
if (commandName === command.name && command.immediate !== true) {
|
|
978
|
-
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 });
|
|
979
|
-
}
|
|
980
|
-
}
|
|
981
|
-
return claude.command.register(command);
|
|
982
|
-
};
|
|
983
|
-
return { ...claude, command: { ...claude.command, register: addCommand } };
|
|
1027
|
+
function apiOf(name, returned) {
|
|
1028
|
+
if (returned === undefined || returned === null)
|
|
1029
|
+
return {};
|
|
1030
|
+
if (typeof returned !== 'object' || !Object.values(returned).every((method) => typeof method === 'function'))
|
|
1031
|
+
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}.`);
|
|
1032
|
+
return returned;
|
|
984
1033
|
}
|
|
985
1034
|
const longestTimerMs = 2_147_483_647;
|
|
986
1035
|
function checkedMs(name, subject, ms) {
|
|
@@ -996,7 +1045,7 @@ function beforeAskOf(call) {
|
|
|
996
1045
|
}
|
|
997
1046
|
async function failsAs(subject, step) {
|
|
998
1047
|
try {
|
|
999
|
-
await step();
|
|
1048
|
+
return await step();
|
|
1000
1049
|
}
|
|
1001
1050
|
catch (error) {
|
|
1002
1051
|
throw new Error(`${subject}: ${messageOf(error)}`, { cause: error });
|
package/runtime/options.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { fitsOption, isUnset } from '../options.js';
|
|
2
|
-
import { configFolders } from '../records.js';
|
|
2
|
+
import { configFolders, secretFix } from '../records.js';
|
|
3
3
|
import { listed, messageOf } from '../utils/text.js';
|
|
4
4
|
export class MissingOptions extends Error {
|
|
5
5
|
keys;
|
|
@@ -50,7 +50,7 @@ export function createOptions({ name, declared, fromClaude, claude, report, chan
|
|
|
50
50
|
if (option === undefined)
|
|
51
51
|
ignore(`sets ${key}, which ${name} does not declare. ${Object.keys(declared).length === 0 ? 'Remove it.' : `Remove it, or use one of: ${Object.keys(declared).join(', ')}.`}`);
|
|
52
52
|
else if (option.type === 'secret')
|
|
53
|
-
ignore(`sets ${key}, a secret, and a project file is committed for everyone to read. Remove it
|
|
53
|
+
ignore(`sets ${key}, a secret, and a project file is committed for everyone to read. Remove it. ${secretFix}`);
|
|
54
54
|
else {
|
|
55
55
|
const misfit = fitsOption(option, value);
|
|
56
56
|
if (misfit === undefined)
|