@cmodjs/core 0.3.0 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/docs/commands.md +61 -10
- package/docs/dependencies.md +16 -14
- package/docs/hooks.md +64 -29
- package/docs/index.md +29 -25
- package/docs/install-steps.md +13 -9
- package/docs/jobs.md +59 -111
- package/docs/mod.md +227 -175
- package/docs/options.md +2 -2
- package/docs/permissions.md +27 -16
- package/docs/session.md +311 -0
- package/docs/state.md +11 -7
- package/docs/testing.md +68 -23
- package/docs/ui.md +116 -76
- package/jobs/check.d.ts.map +1 -1
- package/jobs/check.js +5 -2
- package/jobs/context.d.ts +7 -8
- package/jobs/context.d.ts.map +1 -1
- package/jobs/context.js +23 -73
- package/jobs/permissions/decide-permission.d.ts +5 -3
- package/jobs/permissions/decide-permission.d.ts.map +1 -1
- package/jobs/permissions/decide-permission.js +6 -4
- package/jobs/permissions/find-project-scope.d.ts +1 -1
- package/jobs/permissions/find-project-scope.d.ts.map +1 -1
- package/jobs/permissions/find-project-scope.js +8 -11
- package/jobs/permissions/match-target.d.ts +2 -2
- package/jobs/permissions/match-target.d.ts.map +1 -1
- package/jobs/permissions.d.ts +1 -1
- package/jobs/permissions.d.ts.map +1 -1
- package/jobs/permissions.js +6 -4
- package/jobs/program.d.ts.map +1 -1
- package/jobs/program.js +14 -4
- package/jobs/prompt.d.ts +4 -2
- package/jobs/prompt.d.ts.map +1 -1
- package/jobs/prompt.js +16 -15
- package/jobs/schedule.d.ts +2 -2
- package/jobs/schedule.d.ts.map +1 -1
- package/jobs/schedule.js +9 -9
- package/jobs/slash-command.d.ts +4 -4
- package/jobs/slash-command.d.ts.map +1 -1
- package/jobs/slash-command.js +14 -9
- package/jobs/status-line.d.ts +1 -11
- package/jobs/status-line.d.ts.map +1 -1
- package/jobs/status-line.js +8 -12
- package/jobs/tool.d.ts +2 -2
- package/jobs/tool.d.ts.map +1 -1
- package/jobs/tool.js +15 -9
- package/mod.d.ts +178 -75
- package/mod.d.ts.map +1 -1
- package/mod.js +127 -3
- package/options.d.ts +2 -2
- package/options.js +19 -19
- package/package.json +3 -3
- package/records.d.ts +13 -8
- package/records.d.ts.map +1 -1
- package/records.js +33 -17
- package/register.d.ts.map +1 -1
- package/register.js +34 -5
- package/runtime/announce.d.ts +18 -0
- package/runtime/announce.d.ts.map +1 -0
- package/runtime/announce.js +68 -0
- package/runtime/before.d.ts +32 -0
- package/runtime/before.d.ts.map +1 -0
- package/runtime/before.js +214 -0
- package/runtime/bound.d.ts +4 -0
- package/runtime/bound.d.ts.map +1 -0
- package/runtime/bound.js +9 -0
- package/runtime/claude.d.ts +4 -2
- package/runtime/claude.d.ts.map +1 -1
- package/runtime/conversations.d.ts +17 -0
- package/runtime/conversations.d.ts.map +1 -0
- package/runtime/conversations.js +563 -0
- package/runtime/deadline.d.ts +0 -1
- package/runtime/deadline.d.ts.map +1 -1
- package/runtime/deadline.js +1 -1
- package/runtime/dependencies.d.ts +2 -0
- package/runtime/dependencies.d.ts.map +1 -1
- package/runtime/dependencies.js +10 -1
- package/runtime/engine.d.ts +13 -0
- package/runtime/engine.d.ts.map +1 -0
- package/runtime/engine.js +10 -0
- package/runtime/grants.d.ts +4 -0
- package/runtime/grants.d.ts.map +1 -1
- package/runtime/grants.js +68 -31
- package/runtime/hooks.d.ts +5 -4
- package/runtime/hooks.d.ts.map +1 -1
- package/runtime/hooks.js +29 -9
- package/runtime/installer.d.ts +1 -1
- package/runtime/installer.d.ts.map +1 -1
- package/runtime/installer.js +17 -14
- package/runtime/lifecycle.d.ts +3 -1
- package/runtime/lifecycle.d.ts.map +1 -1
- package/runtime/lifecycle.js +414 -212
- package/runtime/metadata.d.ts +3 -1
- package/runtime/metadata.d.ts.map +1 -1
- package/runtime/metadata.js +25 -14
- package/runtime/options.d.ts +3 -1
- package/runtime/options.d.ts.map +1 -1
- package/runtime/options.js +4 -4
- package/runtime/programs.d.ts +8 -0
- package/runtime/programs.d.ts.map +1 -1
- package/runtime/programs.js +39 -1
- package/runtime/router.d.ts +2 -1
- package/runtime/router.d.ts.map +1 -1
- package/runtime/router.js +11 -3
- package/runtime/sessions.d.ts +58 -0
- package/runtime/sessions.d.ts.map +1 -0
- package/runtime/sessions.js +433 -0
- package/runtime/state.d.ts +6 -2
- package/runtime/state.d.ts.map +1 -1
- package/runtime/state.js +46 -13
- package/runtime/toaster.d.ts +24 -0
- package/runtime/toaster.d.ts.map +1 -0
- package/runtime/toaster.js +286 -0
- package/runtime/ui.d.ts +6 -17
- package/runtime/ui.d.ts.map +1 -1
- package/runtime/ui.js +62 -118
- package/session/context/context.d.ts +12 -0
- package/session/context/context.d.ts.map +1 -0
- package/session/context/context.js +2 -0
- package/session/context/turn/message/document.d.ts +7 -0
- package/session/context/turn/message/document.d.ts.map +1 -0
- package/session/context/turn/message/document.js +1 -0
- package/session/context/turn/message/message.d.ts +55 -0
- package/session/context/turn/message/message.d.ts.map +1 -0
- package/session/context/turn/message/message.js +178 -0
- package/session/context/turn/message/note.d.ts +8 -0
- package/session/context/turn/message/note.d.ts.map +1 -0
- package/session/context/turn/message/note.js +1 -0
- package/session/context/turn/message/notice.d.ts +6 -0
- package/session/context/turn/message/notice.d.ts.map +1 -0
- package/session/context/turn/message/notice.js +1 -0
- package/session/context/turn/message/prompt.d.ts +8 -0
- package/session/context/turn/message/prompt.d.ts.map +1 -0
- package/session/context/turn/message/prompt.js +1 -0
- package/session/context/turn/message/reply.d.ts +6 -0
- package/session/context/turn/message/reply.d.ts.map +1 -0
- package/session/context/turn/message/reply.js +1 -0
- package/session/context/turn/message/sender.d.ts +14 -0
- package/session/context/turn/message/sender.d.ts.map +1 -0
- package/session/context/turn/message/sender.js +30 -0
- package/session/context/turn/message/skill.d.ts +7 -0
- package/session/context/turn/message/skill.d.ts.map +1 -0
- package/session/context/turn/message/skill.js +1 -0
- package/session/context/turn/message/tool-call.d.ts +9 -0
- package/session/context/turn/message/tool-call.d.ts.map +1 -0
- package/session/context/turn/message/tool-call.js +1 -0
- package/session/context/turn/message/tool-result.d.ts +8 -0
- package/session/context/turn/message/tool-result.d.ts.map +1 -0
- package/session/context/turn/message/tool-result.js +1 -0
- package/session/context/turn/turn.d.ts +13 -0
- package/session/context/turn/turn.d.ts.map +1 -0
- package/session/context/turn/turn.js +2 -0
- package/session/conversation.d.ts +73 -0
- package/session/conversation.d.ts.map +1 -0
- package/session/conversation.js +146 -0
- package/session/goal/goal.d.ts +11 -0
- package/session/goal/goal.d.ts.map +1 -0
- package/session/goal/goal.js +1 -0
- package/session/goal/task.d.ts +16 -0
- package/session/goal/task.d.ts.map +1 -0
- package/session/goal/task.js +3 -0
- package/session/session.d.ts +158 -0
- package/session/session.d.ts.map +1 -0
- package/session/session.js +2 -0
- package/src/jobs/check.ts +7 -4
- package/src/jobs/context.ts +24 -82
- package/src/jobs/permissions/decide-permission.ts +10 -8
- package/src/jobs/permissions/find-project-scope.ts +8 -14
- package/src/jobs/permissions/match-target.ts +3 -3
- package/src/jobs/permissions.ts +8 -6
- package/src/jobs/program.ts +13 -4
- package/src/jobs/prompt.ts +21 -24
- package/src/jobs/schedule.ts +14 -14
- package/src/jobs/slash-command.ts +20 -14
- package/src/jobs/status-line.ts +9 -21
- package/src/jobs/tool.ts +17 -11
- package/src/mod.ts +252 -97
- package/src/options.ts +22 -22
- package/src/records.ts +40 -23
- package/src/register.ts +34 -5
- package/src/runtime/announce.ts +76 -0
- package/src/runtime/before.ts +200 -0
- package/src/runtime/bound.ts +12 -0
- package/src/runtime/claude.ts +7 -2
- package/src/runtime/conversations.ts +579 -0
- package/src/runtime/deadline.ts +2 -2
- package/src/runtime/dependencies.ts +13 -1
- package/src/runtime/engine.ts +23 -0
- package/src/runtime/grants.ts +69 -29
- package/src/runtime/hooks.ts +39 -14
- package/src/runtime/installer.ts +16 -14
- package/src/runtime/lifecycle.ts +413 -201
- package/src/runtime/metadata.ts +37 -14
- package/src/runtime/options.ts +19 -4
- package/src/runtime/programs.ts +40 -1
- package/src/runtime/router.ts +11 -4
- package/src/runtime/sessions.ts +447 -0
- package/src/runtime/state.ts +50 -14
- package/src/runtime/toaster.ts +334 -0
- package/src/runtime/ui.ts +65 -143
- package/src/session/context/context.ts +14 -0
- package/src/session/context/turn/message/document.ts +8 -0
- package/src/session/context/turn/message/message.ts +203 -0
- package/src/session/context/turn/message/note.ts +9 -0
- package/src/session/context/turn/message/notice.ts +7 -0
- package/src/session/context/turn/message/prompt.ts +9 -0
- package/src/session/context/turn/message/reply.ts +7 -0
- package/src/session/context/turn/message/sender.ts +38 -0
- package/src/session/context/turn/message/skill.ts +8 -0
- package/src/session/context/turn/message/tool-call.ts +10 -0
- package/src/session/context/turn/message/tool-result.ts +9 -0
- package/src/session/context/turn/turn.ts +15 -0
- package/src/session/conversation.ts +194 -0
- package/src/session/goal/goal.ts +10 -0
- package/src/session/goal/task.ts +19 -0
- package/src/session/session.ts +141 -0
- package/src/testing/fake-claude.ts +25 -17
- package/src/testing/fake-elements.ts +3 -2
- package/src/testing/fake-files.ts +1 -1
- package/src/testing/fake-sessions.ts +277 -0
- package/src/testing.ts +138 -35
- package/src/ui/components.ts +3 -3
- package/src/ui/elements.ts +22 -8
- package/src/ui/markdown.ts +22 -26
- package/src/ui/places.ts +49 -0
- package/src/utils/call-effects.ts +5 -5
- package/src/utils/changelog.ts +32 -0
- package/src/utils/disposable.ts +3 -0
- package/src/utils/matcher.ts +37 -0
- package/src/utils/metadata.ts +17 -15
- package/src/utils/parse-shell.ts +9 -8
- package/src/utils/paths.ts +21 -4
- package/src/utils/text.ts +4 -0
- package/testing/fake-claude.d.ts +5 -1
- package/testing/fake-claude.d.ts.map +1 -1
- package/testing/fake-claude.js +21 -12
- package/testing/fake-elements.js +4 -2
- package/testing/fake-files.js +1 -1
- package/testing/fake-sessions.d.ts +81 -0
- package/testing/fake-sessions.d.ts.map +1 -0
- package/testing/fake-sessions.js +249 -0
- package/testing.d.ts +34 -13
- package/testing.d.ts.map +1 -1
- package/testing.js +115 -23
- package/ui/components.d.ts +2 -3
- package/ui/components.d.ts.map +1 -1
- package/ui/components.js +3 -3
- package/ui/elements.d.ts.map +1 -1
- package/ui/elements.js +19 -9
- package/ui/markdown.d.ts +35 -33
- package/ui/markdown.d.ts.map +1 -1
- package/ui/markdown.js +14 -16
- package/ui/places.d.ts +43 -0
- package/ui/places.d.ts.map +1 -0
- package/ui/places.js +31 -0
- package/utils/call-effects.d.ts +2 -1
- package/utils/call-effects.d.ts.map +1 -1
- package/utils/call-effects.js +3 -3
- package/utils/changelog.d.ts +9 -0
- package/utils/changelog.d.ts.map +1 -0
- package/utils/changelog.js +29 -0
- package/utils/disposable.d.ts +4 -0
- package/utils/disposable.d.ts.map +1 -0
- package/utils/disposable.js +1 -0
- package/utils/matcher.d.ts +13 -0
- package/utils/matcher.d.ts.map +1 -0
- package/utils/matcher.js +32 -0
- package/utils/metadata.d.ts +3 -3
- package/utils/metadata.js +18 -14
- package/utils/parse-shell.js +9 -7
- package/utils/paths.d.ts +4 -1
- package/utils/paths.d.ts.map +1 -1
- package/utils/paths.js +19 -4
- package/utils/text.d.ts +1 -0
- package/utils/text.d.ts.map +1 -1
- package/utils/text.js +3 -0
- package/src/ui/slots.ts +0 -36
- package/ui/slots.d.ts +0 -208
- package/ui/slots.d.ts.map +0 -1
- package/ui/slots.js +0 -19
package/runtime/lifecycle.js
CHANGED
|
@@ -1,36 +1,42 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import { hooks, PreToolUse } from '../mod.js';
|
|
2
|
+
import { changelogPage, consentPath, dataFolder, finishStepsMethod, isAtLeast, marketplaceOf, oldestCmodFor, openPageMethod, parseConsent, pendingStepsMethod, parseEvent, permissionWords, readRecord, readSteps, recordPath, scriptsSha256, settingsPagesMethod, storeFolder, updatesToTurnOn } from '../records.js';
|
|
3
|
+
import { homePrefix, ifMissing, relativePath } from '../utils/paths.js';
|
|
4
|
+
import { firstLine, formatExit, listed, messageOf } from '../utils/text.js';
|
|
4
5
|
import { beforeDeadline } from './deadline.js';
|
|
5
|
-
import {
|
|
6
|
+
import { announcedOf, announcementMs, announcementOf } from './announce.js';
|
|
7
|
+
import { bind } from './bound.js';
|
|
8
|
+
import { serveConversations } from './conversations.js';
|
|
9
|
+
import { answerCall, cmodPluginName, dependencyCalls, withBareRefusals } from './dependencies.js';
|
|
10
|
+
import { keepEngine } from './engine.js';
|
|
6
11
|
import { classicHook, heldDecisionHook, permissionEvents, preToolUseHook, userSkillHook } from './hooks.js';
|
|
7
|
-
import { checkedAnswer, checkingGrants, checksAnswers, checkWrite, isCovered, itemOf, passedDown } from './grants.js';
|
|
8
|
-
import { locatingPrograms } from './programs.js';
|
|
9
|
-
import { createModFiles } from './metadata.js';
|
|
12
|
+
import { checkedAnswer, checkingGrants, checkSettings, checksAnswers, checkWrite, isCovered, itemOf, passedDown, programAddresses } from './grants.js';
|
|
13
|
+
import { cmodError, lastLineOf, locatingPrograms, readLines } from './programs.js';
|
|
14
|
+
import { createModFiles, resolvedPath } from './metadata.js';
|
|
10
15
|
import { fitsOption } from '../options.js';
|
|
11
16
|
import { createInstaller } from './installer.js';
|
|
12
17
|
import { createOptions, MissingOptions } from './options.js';
|
|
13
18
|
import { createRouter } from './router.js';
|
|
19
|
+
import { conversationMethods, createSessionsClient } from './sessions.js';
|
|
20
|
+
import { matches } from '../utils/matcher.js';
|
|
14
21
|
import { createState } from './state.js';
|
|
22
|
+
import { createToaster } from './toaster.js';
|
|
15
23
|
import { toolCalls } from './tool-calls.js';
|
|
16
|
-
import {
|
|
17
|
-
|
|
24
|
+
import { createUi } from './ui.js';
|
|
25
|
+
import { Progress } from '../ui/components.js';
|
|
26
|
+
import { Box, Text } from '../ui/elements.js';
|
|
18
27
|
const announcedKey = 'cmod:announced';
|
|
28
|
+
const installId = 'cmod:install';
|
|
29
|
+
const installRetrySeconds = [2, 4, 8, 16, 32, 64, 128];
|
|
19
30
|
const cmodCheckMs = 1000;
|
|
20
31
|
const cmodWaitMs = 60_000;
|
|
21
|
-
const
|
|
32
|
+
const startHoldMs = 10_000 - 1_000;
|
|
22
33
|
const dependencyCallMs = 30_000;
|
|
23
34
|
const cannotStart = /failed to start: /;
|
|
24
|
-
const missingPath = /(?:^|: )(?:ENOENT|ENOTDIR)\b/;
|
|
25
35
|
const shellTools = ['Bash', 'PowerShell'];
|
|
26
36
|
export async function readPlugin(claude) {
|
|
27
37
|
const { name, root } = claude.plugin;
|
|
28
38
|
const read = async (path) => {
|
|
29
|
-
const stat = await claude.fs.stat(path).catch((
|
|
30
|
-
if (missingPath.test(messageOf(error)))
|
|
31
|
-
return undefined;
|
|
32
|
-
throw error;
|
|
33
|
-
});
|
|
39
|
+
const stat = await claude.fs.stat(path).catch(ifMissing(undefined));
|
|
34
40
|
return stat?.kind === 'file' ? claude.fs.read(path) : undefined;
|
|
35
41
|
};
|
|
36
42
|
const manifest = (await readJson(read, `${root}/.claude-plugin/plugin.json`));
|
|
@@ -72,7 +78,7 @@ async function readJson(read, path) {
|
|
|
72
78
|
return JSON.parse(text);
|
|
73
79
|
}
|
|
74
80
|
catch (error) {
|
|
75
|
-
throw new Error(`${path} is not JSON (${messageOf(error)}). Fix
|
|
81
|
+
throw new Error(`${path} is not JSON (${messageOf(error)}). Fix it, then press Retry.`);
|
|
76
82
|
}
|
|
77
83
|
}
|
|
78
84
|
async function cmodVersion(claude) {
|
|
@@ -94,9 +100,9 @@ export function createLifecycle(definition, checksPermissions = () => true, opti
|
|
|
94
100
|
let programs;
|
|
95
101
|
let plugin;
|
|
96
102
|
const granted = new Set();
|
|
97
|
-
let line;
|
|
98
103
|
let activation;
|
|
99
104
|
let mod;
|
|
105
|
+
let imports;
|
|
100
106
|
let failure;
|
|
101
107
|
let pluginOptions = options;
|
|
102
108
|
const installer = createInstaller(definition.name, () => claude());
|
|
@@ -110,18 +116,53 @@ export function createLifecycle(definition, checksPermissions = () => true, opti
|
|
|
110
116
|
throw new Error(`${definition.name}: the lifecycle has not started. registerMod(addHook, mod, options) starts it at session.start.`);
|
|
111
117
|
return runtime.claude;
|
|
112
118
|
};
|
|
113
|
-
const
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
};
|
|
117
|
-
const endLine = () => {
|
|
118
|
-
line?.end();
|
|
119
|
-
line = undefined;
|
|
120
|
-
};
|
|
121
|
-
const fail = (reason, fix) => {
|
|
119
|
+
const installing = (description) => runtime?.toaster.toast.loading('Installing', { id: installId, ...(description === undefined ? {} : { description }) });
|
|
120
|
+
const toastFailure = (shown) => runtime?.toaster.fail(shown);
|
|
121
|
+
const fail = (reason, fix, retry) => {
|
|
122
122
|
phase = 'failed';
|
|
123
|
-
failure ??= new Error(`${definition.name}: ${reason}${/[.!?]$/.test(reason) ? '' : '.'} ${fix}`);
|
|
124
|
-
|
|
123
|
+
failure ??= new Error(`${definition.name}: ${reason}${/[.!?]$/.test(reason) ? '' : '.'}${fix === undefined ? '' : ` ${fix}`}`);
|
|
124
|
+
const again = retry === undefined ? {} : { retry: () => ((failure = undefined), stopRetrying(), retry().catch(report)) };
|
|
125
|
+
toastFailure({ id: installId, message: reason, ...(fix === undefined ? {} : { description: fix }), ...again });
|
|
126
|
+
};
|
|
127
|
+
let consented;
|
|
128
|
+
const installAgain = () => install(consented);
|
|
129
|
+
let retrying;
|
|
130
|
+
const retryLater = (next) => {
|
|
131
|
+
const seconds = installRetrySeconds[Math.min(next.tries, installRetrySeconds.length - 1)];
|
|
132
|
+
retrying?.timer.cancel();
|
|
133
|
+
retrying = { ...next, tries: next.tries + 1, timer: claude().clock.after(seconds * 1000, () => void retryInBackground().catch(report)) };
|
|
134
|
+
fail(next.reason, `Trying again in ${seconds} s.`, installAgain);
|
|
135
|
+
};
|
|
136
|
+
const stopRetrying = () => {
|
|
137
|
+
retrying?.timer.cancel();
|
|
138
|
+
retrying = undefined;
|
|
139
|
+
};
|
|
140
|
+
const checkoutState = async () => {
|
|
141
|
+
if (plugin === undefined)
|
|
142
|
+
return undefined;
|
|
143
|
+
const lines = [];
|
|
144
|
+
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));
|
|
145
|
+
return code === 0 ? lines.join('\n') : undefined;
|
|
146
|
+
};
|
|
147
|
+
const failInstall = async (reason, when) => {
|
|
148
|
+
if (when === undefined) {
|
|
149
|
+
stopRetrying();
|
|
150
|
+
return fail(reason, undefined, installAgain);
|
|
151
|
+
}
|
|
152
|
+
retryLater({ when, tries: retrying?.when === when ? retrying.tries : 0, checkout: when === 'changed' ? await checkoutState() : undefined, reason });
|
|
153
|
+
};
|
|
154
|
+
const retryInBackground = async () => {
|
|
155
|
+
const waiting = retrying;
|
|
156
|
+
if (phase !== 'failed' || waiting === undefined)
|
|
157
|
+
return;
|
|
158
|
+
if (await recover()) {
|
|
159
|
+
stopRetrying();
|
|
160
|
+
return whenActive();
|
|
161
|
+
}
|
|
162
|
+
if (waiting.when === 'changed' && waiting.checkout !== undefined && (await checkoutState()) === waiting.checkout)
|
|
163
|
+
return retryLater(waiting);
|
|
164
|
+
await install(consented, true);
|
|
165
|
+
await whenActive();
|
|
125
166
|
};
|
|
126
167
|
const finish = async () => {
|
|
127
168
|
await programs?.refresh();
|
|
@@ -132,7 +173,7 @@ export function createLifecycle(definition, checksPermissions = () => true, opti
|
|
|
132
173
|
if (plugin === undefined || (await cmodVersion(claude())) === undefined)
|
|
133
174
|
return;
|
|
134
175
|
const { code, lastError } = await readLines(claude().process.spawn({ argv: ['cmod', 'setup', plugin.root, '--events'] }), (text) => {
|
|
135
|
-
if (parseEvent(text).
|
|
176
|
+
if (parseEvent(text).type === 'done')
|
|
136
177
|
shouldRecord = false;
|
|
137
178
|
});
|
|
138
179
|
if (shouldRecord)
|
|
@@ -149,12 +190,12 @@ export function createLifecycle(definition, checksPermissions = () => true, opti
|
|
|
149
190
|
const option = definition.options?.[key];
|
|
150
191
|
if (option === undefined || plugin === undefined)
|
|
151
192
|
return `${definition.name} has no option ${key}.`;
|
|
152
|
-
const value = option.
|
|
153
|
-
const misfit = option.
|
|
193
|
+
const value = option.type === 'number' ? Number(text) : option.type === 'toggle' ? text.trim() === 'true' : option.type === 'list' ? text.split(',').map((item) => item.trim()).filter((item) => item !== '') : text;
|
|
194
|
+
const misfit = option.type === 'toggle' && !['true', 'false'].includes(text.trim()) ? 'takes true or false' : fitsOption(option, value);
|
|
154
195
|
if (misfit !== undefined)
|
|
155
196
|
return `${option.title} ${misfit}.`;
|
|
156
197
|
const saved = await claude().process.run(['cmod', 'option', plugin.name, key, Array.isArray(value) ? value.join(',') : String(value)]);
|
|
157
|
-
return saved.exitCode === 0 ? undefined :
|
|
198
|
+
return saved.exitCode === 0 ? undefined : cmodError(saved.stderr, 'option') || `cmod option ${formatExit(saved.exitCode, '')}.`;
|
|
158
199
|
};
|
|
159
200
|
const askOptions = async (missing) => {
|
|
160
201
|
if (!asksPerson || plugin === undefined || (await claude().session.surfaces()).length === 0)
|
|
@@ -171,21 +212,35 @@ export function createLifecycle(definition, checksPermissions = () => true, opti
|
|
|
171
212
|
pluginOptions = { ...pluginOptions, ...Object.fromEntries(rows.filter((row) => row.key.startsWith(prefix)).map((row) => [row.key.slice(prefix.length), row.value])) };
|
|
172
213
|
return true;
|
|
173
214
|
};
|
|
215
|
+
const isStepDone = async (active, step) => {
|
|
216
|
+
try {
|
|
217
|
+
return await step.isDone(active);
|
|
218
|
+
}
|
|
219
|
+
catch (error) {
|
|
220
|
+
toastFailure({ id: `step:${step.title}`, message: `The ${step.title} step's isDone failed`, error });
|
|
221
|
+
return false;
|
|
222
|
+
}
|
|
223
|
+
};
|
|
174
224
|
const pendingSteps = async (active) => {
|
|
175
225
|
const pending = [];
|
|
176
226
|
for (const step of definition.installer ?? [])
|
|
177
|
-
if (!(await
|
|
227
|
+
if (!(await isStepDone(active, step)))
|
|
178
228
|
pending.push(step.title);
|
|
179
229
|
return pending;
|
|
180
230
|
};
|
|
181
231
|
const runSteps = async (active) => {
|
|
182
232
|
const steps = asksPerson ? (definition.installer ?? []) : [];
|
|
183
233
|
for (const [index, step] of steps.entries()) {
|
|
184
|
-
if (await
|
|
234
|
+
if (await isStepDone(active, step))
|
|
185
235
|
continue;
|
|
186
236
|
const isAnswered = (await claude().session.surfaces()).length > 0 && (await installer.step(active, step, index + 1, steps.length));
|
|
187
237
|
if (!isAnswered) {
|
|
188
|
-
|
|
238
|
+
runtime?.toaster.toast.warning(steps.length - index === 1 ? 'Needs one more step' : `Needs ${steps.length - index} more steps`, {
|
|
239
|
+
id: 'cmod:steps',
|
|
240
|
+
description: `Starting with ${step.title}. Finish them in /mods ${definition.name}.`,
|
|
241
|
+
duration: Infinity,
|
|
242
|
+
action: { label: 'Finish', onClick: () => active.settings.open() },
|
|
243
|
+
});
|
|
189
244
|
break;
|
|
190
245
|
}
|
|
191
246
|
}
|
|
@@ -195,60 +250,72 @@ export function createLifecycle(definition, checksPermissions = () => true, opti
|
|
|
195
250
|
if (runtime === undefined || plugin === undefined)
|
|
196
251
|
return;
|
|
197
252
|
const activeRouter = createRouter();
|
|
198
|
-
const active = await createMod(definition, { ...runtime, router: activeRouter, dataFolder: dataFolder(plugin.store, plugin.name), steps: plugin.steps, granted, refreshGrants, checksPermissions, options: pluginOptions }).catch(async (error) => {
|
|
253
|
+
const active = await createMod(definition, { ...runtime, router: activeRouter, dataFolder: dataFolder(plugin.store, plugin.name), store: plugin.store, steps: plugin.steps, granted, refreshGrants, checksPermissions, options: pluginOptions }).catch(async (error) => {
|
|
199
254
|
if (error instanceof MissingOptions && (await askOptions(error)))
|
|
200
255
|
return undefined;
|
|
201
|
-
fail(messageOf(error), error instanceof MissingOptions ? `Set ${error.keys.length === 1 ? 'it' : 'them'} in /config.` :
|
|
256
|
+
fail(messageOf(error), error instanceof MissingOptions ? `Set ${error.keys.length === 1 ? 'it' : 'them'} in /config.` : undefined, begin);
|
|
202
257
|
throw error;
|
|
203
258
|
});
|
|
204
259
|
if (active === undefined)
|
|
205
260
|
return activate();
|
|
206
261
|
const { added } = active;
|
|
207
262
|
mod = active.mod;
|
|
263
|
+
imports = active.imports;
|
|
208
264
|
router = activeRouter;
|
|
209
265
|
phase = 'active';
|
|
210
266
|
runtime.claude.ui.invalidate('ui.render');
|
|
211
|
-
|
|
212
|
-
void runSteps(active.mod).catch((error) => claude().ui.log(`${definition.name}: its installer stopped: ${messageOf(error)}`));
|
|
267
|
+
void runSteps(active.mod).catch((error) => toastFailure({ id: 'cmod:steps', message: 'Its installer stopped', error }));
|
|
213
268
|
const missed = missedSessionStart;
|
|
214
269
|
missedSessionStart = undefined;
|
|
215
270
|
const lateAnswer = missed === undefined ? {} : await activeRouter.dispatch('classic.SessionStart', missed, async () => ({})).catch((error) => {
|
|
216
|
-
|
|
271
|
+
toastFailure({ id: 'classic.SessionStart', message: 'The SessionStart hook failed', error });
|
|
217
272
|
return {};
|
|
218
273
|
});
|
|
219
274
|
if (Object.keys(lateAnswer).length > 0) {
|
|
220
275
|
runtime.claude.ui.log(`${definition.name} started after Claude Code's SessionStart, so Claude Code did not read the answer of its SessionStart hooks.`, { to: 'debug' });
|
|
221
276
|
}
|
|
222
277
|
const version = plugin.version ?? '';
|
|
223
|
-
|
|
278
|
+
const announced = announcedOf(await runtime.claude.store.get(announcedKey));
|
|
279
|
+
if (announced?.version === version) {
|
|
280
|
+
runtime.toaster.toast.dismiss(installId);
|
|
224
281
|
return;
|
|
225
|
-
|
|
226
|
-
runtime.claude.
|
|
227
|
-
|
|
228
|
-
|
|
282
|
+
}
|
|
283
|
+
await runtime.claude.store.set(announcedKey, { version, added });
|
|
284
|
+
const { toaster } = runtime;
|
|
285
|
+
void announcementOf(runtime.claude, plugin, granted, added, announced)
|
|
286
|
+
.then(({ message, ...options }) => toaster.toast.success(message, { id: installId, duration: announcementMs, ...options }))
|
|
287
|
+
.catch((error) => toastFailure({ id: installId, message: 'Could not say what it installed', error }));
|
|
229
288
|
};
|
|
230
289
|
const updatesToShow = async () => {
|
|
231
290
|
const [home, configHome] = await Promise.all([claude().env.home(), claude().env.configHome()]);
|
|
232
291
|
const configRoot = configHome ?? `${home ?? ''}/.claude`;
|
|
233
|
-
const known = await claude()
|
|
292
|
+
const known = await claude()
|
|
293
|
+
.fs.read(`${configRoot}/plugins/known_marketplaces.json`)
|
|
294
|
+
.then((text) => JSON.parse(text))
|
|
295
|
+
.catch(ifMissing(undefined));
|
|
234
296
|
return updatesToTurnOn(marketplaceOf(claude().plugin.root, claude().plugin.name), await claude().settings.read({ source: 'user' }), known);
|
|
235
297
|
};
|
|
236
298
|
const askConsent = async (event) => {
|
|
237
299
|
const name = definition.name;
|
|
238
300
|
if ((await claude().session.surfaces()).length === 0) {
|
|
239
|
-
showLine().wait(`Waiting for consent: run cmod install ${name}`);
|
|
240
301
|
claude().ui.log(`${name} waits for consent to run ${event.install}. Run cmod install ${name} in a terminal.`);
|
|
241
302
|
return;
|
|
242
303
|
}
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
304
|
+
installing('Waiting for your answer');
|
|
305
|
+
const updates = await updatesToShow().catch((error) => {
|
|
306
|
+
toastFailure({ id: 'cmod:updates', message: 'Could not read whether its marketplace updates automatically', error });
|
|
307
|
+
return undefined;
|
|
308
|
+
});
|
|
309
|
+
const isAccepted = await installer.consent({ ...event, updates });
|
|
246
310
|
await installer.close();
|
|
311
|
+
if (isAccepted) {
|
|
312
|
+
consented = event.sha256;
|
|
313
|
+
return install(consented);
|
|
314
|
+
}
|
|
247
315
|
phase = 'declined';
|
|
248
|
-
|
|
249
|
-
claude().ui.log(`${name} is not installed. Run cmod install ${name} to install it.`);
|
|
316
|
+
runtime?.toaster.toast('Not installed', { id: installId, description: 'Install it whenever you like.', duration: Infinity, action: { label: 'Install', onClick: () => install().catch(report) } });
|
|
250
317
|
};
|
|
251
|
-
const install = async (consent) => {
|
|
318
|
+
const install = async (consent, isQuiet = false) => {
|
|
252
319
|
if (plugin === undefined)
|
|
253
320
|
return;
|
|
254
321
|
if (plugin.name === cmodPluginName)
|
|
@@ -257,31 +324,34 @@ export function createLifecycle(definition, checksPermissions = () => true, opti
|
|
|
257
324
|
const found = await cmodVersion(claude());
|
|
258
325
|
if (!fits(found))
|
|
259
326
|
return waitForcmod(found);
|
|
260
|
-
|
|
261
|
-
|
|
327
|
+
if (!isQuiet)
|
|
328
|
+
installing();
|
|
262
329
|
let outcome;
|
|
330
|
+
let retries;
|
|
263
331
|
const argv = ['cmod', 'setup', plugin.root, '--events', ...(consent === undefined ? [] : ['--consent', consent])];
|
|
264
332
|
const { code, lastError } = await readLines(claude().process.spawn({ argv }), (text) => {
|
|
265
333
|
const event = parseEvent(text);
|
|
266
|
-
if (event.
|
|
267
|
-
|
|
268
|
-
if (event.
|
|
334
|
+
if (event.type === 'progress' && !isQuiet)
|
|
335
|
+
installing(progressOf(event));
|
|
336
|
+
if (event.type === 'retries')
|
|
337
|
+
retries = event.when;
|
|
338
|
+
if (event.type === 'done' || event.type === 'failed' || event.type === 'needs-consent')
|
|
269
339
|
outcome = event;
|
|
270
340
|
});
|
|
271
|
-
if (outcome?.
|
|
341
|
+
if (outcome?.type === 'done') {
|
|
342
|
+
stopRetrying();
|
|
272
343
|
for (const item of plugin.steps.permissions ?? [])
|
|
273
344
|
granted.add(item);
|
|
274
345
|
return finish();
|
|
275
346
|
}
|
|
276
|
-
if (outcome?.
|
|
347
|
+
if (outcome?.type === 'needs-consent')
|
|
277
348
|
return askConsent(outcome);
|
|
278
|
-
|
|
279
|
-
fail(reason, `Fix the cause, then run: cmod install ${definition.name}`);
|
|
349
|
+
return failInstall(outcome?.type === 'failed' ? outcome.message : `cmod setup ${formatExit(code, lastError)}`, retries);
|
|
280
350
|
};
|
|
281
351
|
const fits = (found) => found !== undefined && isAtLeast(found, oldestCmodFor(plugin?.steps ?? {}));
|
|
282
352
|
const waitForcmod = (found) => {
|
|
283
353
|
phase = 'waiting';
|
|
284
|
-
|
|
354
|
+
installing('Waiting for cmod');
|
|
285
355
|
let latest = found;
|
|
286
356
|
let check;
|
|
287
357
|
const stop = () => {
|
|
@@ -303,37 +373,76 @@ export function createLifecycle(definition, checksPermissions = () => true, opti
|
|
|
303
373
|
report(error);
|
|
304
374
|
});
|
|
305
375
|
});
|
|
306
|
-
const longWait = claude().clock.after(cmodWaitMs, () =>
|
|
376
|
+
const longWait = claude().clock.after(cmodWaitMs, () => installing(latest === undefined
|
|
307
377
|
? "Still waiting for cmod to download cmod. See cmod's own line."
|
|
308
378
|
: `PATH finds cmod ${latest}, and ${definition.name} needs cmod ${oldestCmodFor(plugin?.steps ?? {})} or later. Run npm i -g @cmodjs/cli, or put ~/.local/bin ahead of the old cmod on PATH.`));
|
|
309
379
|
};
|
|
310
380
|
const bootstrap = async (root) => {
|
|
311
|
-
|
|
381
|
+
installing();
|
|
312
382
|
phase = 'installing';
|
|
313
383
|
const { code, lastError } = await readLines(claude().process.spawn({ argv: ['sh', '-c', './setup/bootstrap.sh'], cwd: root }), (text) => {
|
|
314
384
|
const event = parseEvent(text);
|
|
315
|
-
if (event.
|
|
316
|
-
|
|
385
|
+
if (event.type === 'progress')
|
|
386
|
+
installing(progressOf(event));
|
|
317
387
|
});
|
|
318
388
|
if (code === 0)
|
|
319
389
|
return finish();
|
|
320
|
-
fail(`bootstrap ${formatExit(code, lastError)}`,
|
|
390
|
+
fail(`bootstrap ${formatExit(code, lastError)}`, undefined, () => bootstrap(root));
|
|
391
|
+
};
|
|
392
|
+
const begin = async () => {
|
|
393
|
+
if (readStarted === undefined)
|
|
394
|
+
return;
|
|
395
|
+
const started = await readStarted(claude());
|
|
396
|
+
plugin = started;
|
|
397
|
+
for (const item of started.granted)
|
|
398
|
+
granted.add(item);
|
|
399
|
+
consentMs = await stampOf(consentPath(started.store));
|
|
400
|
+
recordMs = await stampOf(recordPath(started.store, started.name));
|
|
401
|
+
if (started.isInstalled) {
|
|
402
|
+
shouldRecord = started.shouldRecord;
|
|
403
|
+
if (shouldRecord)
|
|
404
|
+
record();
|
|
405
|
+
activation = undefined;
|
|
406
|
+
return activate();
|
|
407
|
+
}
|
|
408
|
+
if (started.version === undefined)
|
|
409
|
+
return fail('no version to install', 'Its author adds "version" to .claude-plugin/plugin.json.');
|
|
410
|
+
void install().catch(report);
|
|
321
411
|
};
|
|
322
412
|
const report = (error) => {
|
|
323
413
|
failure = error;
|
|
324
414
|
if (phase !== 'failed')
|
|
325
|
-
fail(messageOf(error),
|
|
415
|
+
fail(messageOf(error), undefined, begin);
|
|
326
416
|
};
|
|
327
417
|
let consentMs = 0;
|
|
328
|
-
const
|
|
329
|
-
const stat = await claude().fs.stat(path).catch((
|
|
418
|
+
const stampOf = async (path) => {
|
|
419
|
+
const stat = await claude().fs.stat(path).catch(ifMissing(undefined));
|
|
330
420
|
return stat?.kind === 'file' ? stat.mtimeMs : 0;
|
|
331
421
|
};
|
|
422
|
+
let recordMs = 0;
|
|
423
|
+
let readStarted;
|
|
424
|
+
const recover = async () => {
|
|
425
|
+
if (plugin === undefined || readStarted === undefined || (phase !== 'failed' && phase !== 'declined'))
|
|
426
|
+
return false;
|
|
427
|
+
const modifiedMs = await stampOf(recordPath(plugin.store, plugin.name));
|
|
428
|
+
if (modifiedMs === recordMs)
|
|
429
|
+
return false;
|
|
430
|
+
recordMs = modifiedMs;
|
|
431
|
+
const current = await readStarted(claude());
|
|
432
|
+
if (!current.isInstalled)
|
|
433
|
+
return false;
|
|
434
|
+
plugin = current;
|
|
435
|
+
failure = undefined;
|
|
436
|
+
activation = undefined;
|
|
437
|
+
await refreshGrants();
|
|
438
|
+
await finish();
|
|
439
|
+
return true;
|
|
440
|
+
};
|
|
332
441
|
const refreshGrants = async () => {
|
|
333
442
|
if (plugin === undefined || plugin.name === cmodPluginName)
|
|
334
443
|
return;
|
|
335
444
|
const path = consentPath(plugin.store);
|
|
336
|
-
const modifiedMs = await
|
|
445
|
+
const modifiedMs = await stampOf(path);
|
|
337
446
|
if (modifiedMs === consentMs)
|
|
338
447
|
return;
|
|
339
448
|
consentMs = modifiedMs;
|
|
@@ -356,7 +465,7 @@ export function createLifecycle(definition, checksPermissions = () => true, opti
|
|
|
356
465
|
droppedParts.add(item);
|
|
357
466
|
const declared = plugin?.steps.permissions ?? [];
|
|
358
467
|
const fix = declared.includes(item) ? `Turn it on in /mods ${definition.name}.` : `It needs "permissions": { "${item}": true } in package.json "cmod".`;
|
|
359
|
-
|
|
468
|
+
runtime?.toaster.toast.warning(`Answered ${part} without your grant to "${permissionWords(item)}", so cmod dropped it`, { id: `dropped:${item}`, description: fix, duration: Infinity });
|
|
360
469
|
},
|
|
361
470
|
};
|
|
362
471
|
const checkedRoute = async (event, e, next) => {
|
|
@@ -371,13 +480,15 @@ export function createLifecycle(definition, checksPermissions = () => true, opti
|
|
|
371
480
|
if (activation !== undefined && phase !== 'active')
|
|
372
481
|
await activation;
|
|
373
482
|
};
|
|
374
|
-
const
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
limit.
|
|
378
|
-
|
|
483
|
+
const holdUntilStarted = () => runtime === undefined
|
|
484
|
+
? startSettled.then(whenActive)
|
|
485
|
+
: new Promise((resolve) => {
|
|
486
|
+
const limit = claude().clock.after(startHoldMs, resolve);
|
|
487
|
+
void startSettled.then(whenActive).then(() => {
|
|
488
|
+
limit.cancel();
|
|
489
|
+
resolve();
|
|
490
|
+
});
|
|
379
491
|
});
|
|
380
|
-
});
|
|
381
492
|
return {
|
|
382
493
|
get phase() {
|
|
383
494
|
return phase;
|
|
@@ -388,38 +499,26 @@ export function createLifecycle(definition, checksPermissions = () => true, opti
|
|
|
388
499
|
get failure() {
|
|
389
500
|
return failure;
|
|
390
501
|
},
|
|
502
|
+
get toasts() {
|
|
503
|
+
return runtime?.toaster.shown ?? [];
|
|
504
|
+
},
|
|
391
505
|
async start(claudeCalls, read) {
|
|
392
506
|
if (runtime !== undefined)
|
|
393
507
|
return;
|
|
394
508
|
programs = locatingPrograms(claudeCalls);
|
|
395
|
-
runtime = { claude: programs.claude,
|
|
509
|
+
runtime = { claude: programs.claude, toaster: createToaster(claudeCalls, definition.name, () => plugin?.version) };
|
|
510
|
+
readStarted = read;
|
|
396
511
|
try {
|
|
397
512
|
await programs.refresh();
|
|
398
|
-
|
|
399
|
-
report(error);
|
|
400
|
-
return undefined;
|
|
401
|
-
});
|
|
402
|
-
if (started === undefined)
|
|
403
|
-
return;
|
|
404
|
-
plugin = started;
|
|
405
|
-
for (const item of started.granted)
|
|
406
|
-
granted.add(item);
|
|
407
|
-
consentMs = await consentStamp(consentPath(started.store));
|
|
408
|
-
if (started.isInstalled) {
|
|
409
|
-
shouldRecord = started.shouldRecord;
|
|
410
|
-
if (shouldRecord)
|
|
411
|
-
record();
|
|
412
|
-
return await activate().catch(report);
|
|
413
|
-
}
|
|
414
|
-
if (started.version === undefined)
|
|
415
|
-
return fail('no version to install', 'Add "version" to .claude-plugin/plugin.json, then run /reload-plugins.');
|
|
416
|
-
void install().catch(report);
|
|
513
|
+
await begin().catch(report);
|
|
417
514
|
}
|
|
418
515
|
finally {
|
|
419
516
|
settleStart();
|
|
420
517
|
}
|
|
421
518
|
},
|
|
422
519
|
async route(event, e, next) {
|
|
520
|
+
if (imports !== undefined)
|
|
521
|
+
bind(imports);
|
|
423
522
|
if (event === 'ui.render' && installer.draws(e))
|
|
424
523
|
return installer.draw(e);
|
|
425
524
|
if (event === 'ui.close')
|
|
@@ -432,12 +531,17 @@ export function createLifecycle(definition, checksPermissions = () => true, opti
|
|
|
432
531
|
}
|
|
433
532
|
if (shouldRecord && event === 'classic.UserPromptSubmit')
|
|
434
533
|
record();
|
|
435
|
-
|
|
534
|
+
if (event === 'classic.UserPromptSubmit' || event === 'classic.SessionStart')
|
|
535
|
+
await recover().catch((error) => claude().ui.log(`${definition.name} stays stopped: ${messageOf(error)}`, { to: 'debug' }));
|
|
536
|
+
const isCallToThisMod = event === 'cmod.call' && e.to === definition.name;
|
|
537
|
+
if (event === 'cmod.call' && !isCallToThisMod)
|
|
538
|
+
return next(e);
|
|
539
|
+
await (event === 'classic.SessionStart' || isCallToThisMod ? holdUntilStarted() : whenActive());
|
|
436
540
|
if (event === 'classic.SessionStart' && phase !== 'active')
|
|
437
541
|
missedSessionStart = e;
|
|
438
|
-
if (
|
|
542
|
+
if (isCallToThisMod && phase !== 'active') {
|
|
439
543
|
const isStopped = phase === 'declined' || phase === 'failed';
|
|
440
|
-
return { deny: isStopped ?
|
|
544
|
+
return { deny: isStopped ? `${definition.name} is not set up. Its toast says why.` : `${definition.name} is installing. Try again when it's ready.` };
|
|
441
545
|
}
|
|
442
546
|
const call = e;
|
|
443
547
|
if (event === 'cmod.call' && call.to === definition.name && mod !== undefined) {
|
|
@@ -448,21 +552,22 @@ export function createLifecycle(definition, checksPermissions = () => true, opti
|
|
|
448
552
|
}
|
|
449
553
|
if (event === 'classic.UserPromptSubmit' || event === 'classic.SessionStart')
|
|
450
554
|
await refreshGrants().catch((error) => claude().ui.log(`${definition.name} keeps its grants from before: ${messageOf(error)}`, { to: 'debug' }));
|
|
451
|
-
if (checksAnswers(event) && phase === 'active')
|
|
555
|
+
if (checksAnswers(event) && phase === 'active' && definition.name !== cmodPluginName)
|
|
452
556
|
return checkedRoute(event, e, next);
|
|
453
557
|
const answer = router.dispatch(event, e, next);
|
|
454
558
|
const render = e;
|
|
455
|
-
if (event !== 'ui.render' || render.component !== 'AbovePrompt' || runtime === undefined
|
|
559
|
+
if (event !== 'ui.render' || render.component !== 'AbovePrompt' || runtime === undefined)
|
|
456
560
|
return answer;
|
|
457
|
-
return runtime.
|
|
561
|
+
return runtime.toaster.draw((await answer), runtime.claude.ui.resolve(render), render.props, render.requestId);
|
|
458
562
|
},
|
|
459
563
|
};
|
|
460
564
|
}
|
|
461
565
|
async function createMod(definition, runtime) {
|
|
462
|
-
const { router,
|
|
463
|
-
const
|
|
566
|
+
const { router, toaster } = runtime;
|
|
567
|
+
const report = (failure) => void toaster.fail(failure);
|
|
568
|
+
const claude = withBareRefusals(checkingKeys(runtime.claude, toaster.toast, runtime.steps.keys ?? {}));
|
|
464
569
|
const added = [];
|
|
465
|
-
const
|
|
570
|
+
const hooksAdded = [];
|
|
466
571
|
const announce = (feature) => {
|
|
467
572
|
added.push(feature);
|
|
468
573
|
};
|
|
@@ -478,19 +583,24 @@ async function createMod(definition, runtime) {
|
|
|
478
583
|
configRoot,
|
|
479
584
|
projectRoot: () => modState.root,
|
|
480
585
|
freeFolders: () => [modState.root, runtime.dataFolder],
|
|
586
|
+
freeAddresses: () => programAddresses(mod),
|
|
481
587
|
};
|
|
482
588
|
const checked = checkingGrants(claude, grants);
|
|
589
|
+
const places = { home, configRoot, projectRoot: () => modState.root };
|
|
483
590
|
const files = createModFiles({
|
|
484
591
|
name: definition.name,
|
|
485
592
|
claude,
|
|
486
|
-
places
|
|
593
|
+
places,
|
|
487
594
|
checkWrite: (path) => checkWrite(grants, `metadata.update(${path})`, path),
|
|
595
|
+
report,
|
|
488
596
|
});
|
|
489
597
|
const pages = new Map();
|
|
490
598
|
const settingsPages = {
|
|
491
599
|
page(page) {
|
|
492
600
|
if (pages.has(page.id))
|
|
493
601
|
throw new Error(`${definition.name}: the settings page "${page.id}" is already added. Give each page its own id.`);
|
|
602
|
+
if (page.id === changelogPage)
|
|
603
|
+
throw new Error(`${definition.name}: the settings page id "${changelogPage}" opens the mod's CHANGELOG.md in /mods. Give the page another id.`);
|
|
494
604
|
pages.set(page.id, { title: page.title, handle: area.ui.pane(page) });
|
|
495
605
|
},
|
|
496
606
|
async open(pageId) {
|
|
@@ -507,87 +617,162 @@ async function createMod(definition, runtime) {
|
|
|
507
617
|
let cwd = startCwd;
|
|
508
618
|
let loadedCwd = startCwd;
|
|
509
619
|
let staleCwd;
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
620
|
+
let forkedFrom;
|
|
621
|
+
let parentAsked;
|
|
622
|
+
let hasEarlierPrompts = true;
|
|
623
|
+
let agentType;
|
|
624
|
+
const area = createUi({ name: definition.name, claude, router, toaster, report, announce, mod: () => mod });
|
|
625
|
+
const askParent = async () => {
|
|
626
|
+
if (definition.name === cmodPluginName)
|
|
627
|
+
return undefined;
|
|
628
|
+
try {
|
|
629
|
+
const parent = await claude.cmod.call({ to: cmodPluginName, method: 'forkParent', input: null });
|
|
630
|
+
return typeof parent === 'string' ? parent : undefined;
|
|
631
|
+
}
|
|
632
|
+
catch (error) {
|
|
633
|
+
claude.ui.log(`${definition.name} starts without the session values of the conversation it was forked from: ${messageOf(error)}`, { to: 'debug' });
|
|
634
|
+
return undefined;
|
|
635
|
+
}
|
|
636
|
+
};
|
|
637
|
+
const forkParent = () => (parentAsked ??= askParent().then((parent) => {
|
|
638
|
+
forkedFrom = parent;
|
|
639
|
+
return parent;
|
|
640
|
+
}));
|
|
641
|
+
const modState = createState({ name: definition.name, initial: definition.state ?? {}, session, root, claude, report, changed: area.changed, forkParent });
|
|
642
|
+
const modOptions = createOptions({ name: definition.name, declared: definition.options ?? {}, fromClaude: runtime.options, claude, report, changed: modState.changed });
|
|
513
643
|
const held = runtime.checksPermissions() ? new Map() : undefined;
|
|
514
|
-
const
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
644
|
+
const expand = (path) => (homePrefix.test(path) ? resolvedPath(path, places) : path);
|
|
645
|
+
const settingsFile = (source) => ({
|
|
646
|
+
read: () => checked.settings.read({ source }),
|
|
647
|
+
async update(values) {
|
|
648
|
+
await checkSettings(grants, source, Object.keys(values));
|
|
649
|
+
await claude.cmod.call({ to: cmodPluginName, method: 'updateSettings', input: { mod: definition.name, source, project: modState.root, values } });
|
|
519
650
|
},
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
ui: area.ui,
|
|
546
|
-
process: {
|
|
547
|
-
run: (argv, init) => checked.process.run(argv, init),
|
|
548
|
-
spawn: (argv, init) => checked.process.spawn({ ...init, argv }),
|
|
651
|
+
});
|
|
652
|
+
const client = createSessionsClient({ name: definition.name, claude: checked, report: (id, message, error) => report({ id, message, error }), isGranted: (permission) => flag(permission) });
|
|
653
|
+
let followed;
|
|
654
|
+
const followFolders = (project, folder) => {
|
|
655
|
+
if (followed === undefined)
|
|
656
|
+
return;
|
|
657
|
+
followed = { ...followed, project, cwd: folder };
|
|
658
|
+
client.follow(followed);
|
|
659
|
+
};
|
|
660
|
+
const imports = {
|
|
661
|
+
sessions: client.sessions,
|
|
662
|
+
settings: {
|
|
663
|
+
list: async () => (await checked.config.list()).map(({ key, label, description, kind, value, options, provider, isLocked }) => ({
|
|
664
|
+
key,
|
|
665
|
+
label,
|
|
666
|
+
kind,
|
|
667
|
+
value,
|
|
668
|
+
isLocked,
|
|
669
|
+
...(description === undefined ? {} : { description }),
|
|
670
|
+
...(options === undefined ? {} : { options }),
|
|
671
|
+
...(provider.plugin === 'engine' ? {} : { plugin: provider.plugin }),
|
|
672
|
+
})),
|
|
673
|
+
user: settingsFile('user'),
|
|
674
|
+
project: settingsFile('project'),
|
|
675
|
+
local: settingsFile('local'),
|
|
549
676
|
},
|
|
677
|
+
model: { complete: (completion, options) => checked.model.complete(completion, options) },
|
|
550
678
|
fs: {
|
|
551
|
-
read: (path) => checked.fs.read(path),
|
|
552
|
-
write: (path, text) => checked.fs.write(path, text),
|
|
553
|
-
|
|
554
|
-
exists: (path) => checked.fs.exists(path),
|
|
555
|
-
stat: (path, options) => checked.fs.stat(path, options),
|
|
679
|
+
read: (path) => checked.fs.read(expand(path)),
|
|
680
|
+
write: (path, text) => checked.fs.write(expand(path), text),
|
|
681
|
+
exists: (path) => checked.fs.exists(expand(path)),
|
|
556
682
|
find: (glob) => files.find(glob),
|
|
683
|
+
list: async (path) => (await checked.fs.list(path === undefined ? undefined : expand(path))).map(({ name, kind, size, mtimeMs, isLink }) => ({ name, isFile: kind === 'file', isFolder: kind === 'dir', isLink, size, modifiedAt: mtimeMs })),
|
|
684
|
+
async stat(path, options) {
|
|
685
|
+
const { kind, size, mtimeMs, isLink, realPath } = await checked.fs.stat(expand(path), options);
|
|
686
|
+
return { isFile: kind === 'file', isFolder: kind === 'dir', isLink, size, modifiedAt: mtimeMs, ...(realPath === undefined ? {} : { realPath }) };
|
|
687
|
+
},
|
|
557
688
|
},
|
|
558
689
|
metadata: {
|
|
559
690
|
read: (path) => files.read(path),
|
|
560
691
|
update: (path, change) => files.update(path, change),
|
|
561
692
|
},
|
|
693
|
+
process: {
|
|
694
|
+
run: (argv, init) => checked.process.run(argv, init),
|
|
695
|
+
spawn: (argv, init) => checked.process.spawn({ ...init, argv }),
|
|
696
|
+
},
|
|
562
697
|
http: { fetch: (url, init) => checked.http.fetch(url, init) },
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
698
|
+
};
|
|
699
|
+
const allowed = (permission) => ({ allows: (value) => isCovered(granted, itemOf(permission, value), home) });
|
|
700
|
+
const flag = (permission) => isCovered(granted, permission, home);
|
|
701
|
+
const timer = (ms, subject, start, listener) => {
|
|
702
|
+
let isRunning = false;
|
|
703
|
+
const started = start(checkedMs(definition.name, `mod.${subject}`, ms), () => {
|
|
704
|
+
if (isRunning)
|
|
705
|
+
return;
|
|
706
|
+
isRunning = true;
|
|
707
|
+
void Promise.resolve()
|
|
708
|
+
.then(() => {
|
|
709
|
+
bind(imports);
|
|
710
|
+
return listener();
|
|
711
|
+
})
|
|
712
|
+
.catch((error) => report({ id: `${subject}:${ms}`, message: `The ${subject} ${ms} ms timer failed`, error }))
|
|
713
|
+
.finally(() => {
|
|
714
|
+
isRunning = false;
|
|
715
|
+
});
|
|
716
|
+
});
|
|
717
|
+
return { dispose: () => started.cancel() };
|
|
718
|
+
};
|
|
719
|
+
const mod = {
|
|
720
|
+
name: definition.name,
|
|
721
|
+
folder: claude.plugin.root,
|
|
722
|
+
dataFolder: runtime.dataFolder,
|
|
723
|
+
state: modState.state,
|
|
724
|
+
get options() {
|
|
725
|
+
return modOptions.values;
|
|
576
726
|
},
|
|
577
|
-
agent: { spawn: (args) => checked.agent.spawn(args) },
|
|
578
|
-
model: { complete: (completion, options) => checked.model.complete(completion, options) },
|
|
579
727
|
permissions: {
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
728
|
+
get conversation() {
|
|
729
|
+
return flag('conversation');
|
|
730
|
+
},
|
|
731
|
+
get prompt() {
|
|
732
|
+
return flag('prompt');
|
|
733
|
+
},
|
|
734
|
+
get model() {
|
|
735
|
+
return flag('model');
|
|
736
|
+
},
|
|
737
|
+
get tasks() {
|
|
738
|
+
return flag('tasks');
|
|
739
|
+
},
|
|
740
|
+
get tools() {
|
|
741
|
+
return flag('tools');
|
|
742
|
+
},
|
|
743
|
+
get approve() {
|
|
744
|
+
return flag('approve');
|
|
745
|
+
},
|
|
746
|
+
network: allowed('network'),
|
|
747
|
+
run: allowed('run'),
|
|
748
|
+
files: allowed('files'),
|
|
749
|
+
config: allowed('config'),
|
|
585
750
|
},
|
|
586
|
-
|
|
587
|
-
|
|
751
|
+
on(hook, matcherOrListener, listenerOrOptions, maybeOptions) {
|
|
752
|
+
const hasMatcher = typeof listenerOrOptions === 'function';
|
|
753
|
+
const listener = (hasMatcher ? listenerOrOptions : matcherOrListener);
|
|
754
|
+
const matcher = hasMatcher ? matcherOrListener : undefined;
|
|
755
|
+
const options = (hasMatcher ? maybeOptions : listenerOrOptions);
|
|
756
|
+
if (!hooks.includes(hook))
|
|
757
|
+
throw new Error(`${definition.name}: mod.on takes one of Claude Code's hooks, such as PreToolUse, not ${String(hook)}.`);
|
|
758
|
+
const matched = matcher === undefined ? listener : (input) => (matches(input, matcher) ? listener(input) : undefined);
|
|
759
|
+
const bounded = options?.timeoutMs === undefined ? matched : timedHook(hook, matched, checkedMs(definition.name, `the ${hook} hook's timeoutMs`, options.timeoutMs), claude, toaster.toast);
|
|
760
|
+
if (!hooksAdded.includes(hook))
|
|
761
|
+
hooksAdded.push(hook);
|
|
762
|
+
if (hook === PreToolUse)
|
|
763
|
+
return router.add('tool.call', preToolUseHook(definition.name, bounded, claude, agents, held));
|
|
764
|
+
const classic = hook;
|
|
765
|
+
return router.add(`classic.${classic}`, classicHook(definition.name, classic, bounded, claude, agents, report));
|
|
588
766
|
},
|
|
767
|
+
every: (ms, listener) => timer(ms, 'every', claude.clock.every, listener),
|
|
768
|
+
after: (ms, listener) => timer(ms, 'after', claude.clock.after, listener),
|
|
769
|
+
use: (job) => job({ mod, announce, reserveName }),
|
|
770
|
+
log: (text) => claude.ui.log(`${definition.name}: ${text}`, { to: 'debug' }),
|
|
771
|
+
ui: { ...area.ui, notice: (callId, text) => claude.ui.notice(callId, text) },
|
|
772
|
+
settings: settingsPages,
|
|
589
773
|
dependencies: dependencyCalls(claude, (call, task) => beforeDeadline(claude, { ms: dependencyCallMs }, call, task)),
|
|
590
774
|
};
|
|
775
|
+
bind(imports);
|
|
591
776
|
const followSession = async (isAfterCd = false) => {
|
|
592
777
|
try {
|
|
593
778
|
const [nextRoot, reportedCwd] = await Promise.all([claude.session.root(), claude.session.cwd()]);
|
|
@@ -601,9 +786,11 @@ async function createMod(definition, runtime) {
|
|
|
601
786
|
if (!hasMovedRoot && nextCwd === oldCwd)
|
|
602
787
|
return;
|
|
603
788
|
cwd = nextCwd;
|
|
789
|
+
followFolders(nextRoot, nextCwd);
|
|
604
790
|
await modState.moveTo(nextRoot).catch((error) => {
|
|
605
791
|
if (cwd === nextCwd)
|
|
606
792
|
cwd = loadedCwd;
|
|
793
|
+
followFolders(modState.root, cwd);
|
|
607
794
|
throw error;
|
|
608
795
|
});
|
|
609
796
|
if (hasMovedRoot)
|
|
@@ -617,18 +804,35 @@ async function createMod(definition, runtime) {
|
|
|
617
804
|
await router.dispatch('classic.CwdChanged', moved, async () => ({}));
|
|
618
805
|
}
|
|
619
806
|
catch (error) {
|
|
620
|
-
|
|
807
|
+
report({ id: 'cmod:follow', message: `Keeps the state of ${modState.root} until the next prompt or folder move`, error });
|
|
621
808
|
}
|
|
622
809
|
};
|
|
810
|
+
const followCurrent = async (title) => {
|
|
811
|
+
const [id, model, usage] = await Promise.all([claude.session.id(), claude.session.model(), claude.session.usage()]);
|
|
812
|
+
followed = { id, title: title ?? '', project: modState.root, cwd, model, startedAt: new Date(usage.startedAt).toISOString(), ...(forkedFrom === undefined ? {} : { forkedFrom }), ...(agentType === undefined ? {} : { agentType }) };
|
|
813
|
+
client.follow(followed);
|
|
814
|
+
};
|
|
623
815
|
router.add('classic.SessionStart', async (e, next) => {
|
|
624
|
-
|
|
625
|
-
|
|
816
|
+
agentType = e.agent_id === undefined ? e.agent_type : agentType;
|
|
817
|
+
const previous = followed?.id;
|
|
818
|
+
if (e.source === 'fork' && previous !== undefined && previous !== e.session_id)
|
|
819
|
+
forkedFrom = previous;
|
|
820
|
+
else if (e.session_id !== previous) {
|
|
821
|
+
forkedFrom = undefined;
|
|
822
|
+
parentAsked = undefined;
|
|
823
|
+
}
|
|
824
|
+
hasEarlierPrompts = e.source !== 'startup' && e.source !== 'clear';
|
|
825
|
+
await modState.switchSession(e.session_id, e.source, e.transcript_path).catch((error) => {
|
|
826
|
+
report({ id: 'cmod:session', message: `Kept its session values from before the ${e.source}`, error });
|
|
626
827
|
});
|
|
828
|
+
await followCurrent(e.session_title).catch((error) => report({ id: 'cmod:current', message: 'Kept the details of the session from before', error }));
|
|
627
829
|
return next(e);
|
|
628
830
|
});
|
|
629
831
|
router.add('classic.UserPromptSubmit', async (e, next) => {
|
|
630
832
|
staleCwd = undefined;
|
|
631
833
|
await followSession();
|
|
834
|
+
const isPerson = e.source === undefined || e.source === 'user' || e.source === 'sdk';
|
|
835
|
+
await followCurrent(e.session_title || (isPerson && !hasEarlierPrompts && client.sessions.current.title === '' ? firstLine(e.prompt) : undefined)).catch((error) => report({ id: 'cmod:current', message: 'Kept the details of the session from before', error }));
|
|
632
836
|
return next(e);
|
|
633
837
|
});
|
|
634
838
|
for (const event of ['classic.PostToolUse', 'classic.PostToolUseFailure']) {
|
|
@@ -640,56 +844,75 @@ async function createMod(definition, runtime) {
|
|
|
640
844
|
}
|
|
641
845
|
router.add('command.run', async (e, next) => {
|
|
642
846
|
const answer = await next(e);
|
|
643
|
-
if (e.command === 'cd')
|
|
847
|
+
if (e.command === 'cd') {
|
|
644
848
|
await followSession(true);
|
|
849
|
+
conversations?.moved();
|
|
850
|
+
}
|
|
645
851
|
return answer;
|
|
646
852
|
});
|
|
647
853
|
router.add('skill.prompt', userSkillHook(claude));
|
|
648
854
|
const api = Object.fromEntries(Object.entries(definition.api ?? {}).map(([method, run]) => [method, (input) => run(input, mod)]));
|
|
855
|
+
const conversations = definition.name === cmodPluginName ? serveConversations({ claude, router, store: runtime.store, project: () => modState.root, report: (id, message, error) => report({ id, message, error }) }) : undefined;
|
|
649
856
|
router.add('cmod.call', async (e, next) => {
|
|
650
857
|
if (e.to !== definition.name)
|
|
651
858
|
return next(e);
|
|
859
|
+
const call = e;
|
|
860
|
+
if (e.method === conversationMethods.changed && call.caller === cmodPluginName) {
|
|
861
|
+
if (!flag('conversation'))
|
|
862
|
+
return { value: null };
|
|
863
|
+
const { session: changedSession, change, isInProject } = e.input;
|
|
864
|
+
client.changed(changedSession, change, isInProject !== false);
|
|
865
|
+
return { value: null };
|
|
866
|
+
}
|
|
867
|
+
if (e.method === conversationMethods.before && call.caller === cmodPluginName) {
|
|
868
|
+
const { session: asking, event, change, isInProject } = e.input;
|
|
869
|
+
return { value: (await client.answerBefore(asking, event, change, isInProject !== false)) ?? null };
|
|
870
|
+
}
|
|
652
871
|
if (e.method === settingsPagesMethod)
|
|
653
872
|
return { value: [...pages].map(([id, { title }]) => ({ id, title })) };
|
|
654
873
|
if (e.method === openPageMethod) {
|
|
655
874
|
await settingsPages.open(String(e.input));
|
|
656
875
|
return { value: null };
|
|
657
876
|
}
|
|
658
|
-
|
|
877
|
+
const served = await conversations?.answer(call);
|
|
878
|
+
return served ?? answerCall(definition.name, api, e);
|
|
659
879
|
});
|
|
660
880
|
const agents = toolCalls(claude, router);
|
|
881
|
+
keepEngine(mod, { claude: checked, router, toolCalls: agents, report });
|
|
661
882
|
const names = new Set();
|
|
662
|
-
const reserveName = (
|
|
663
|
-
const key = `${
|
|
883
|
+
const reserveName = (type, name, taken) => {
|
|
884
|
+
const key = `${type}:${name}`;
|
|
664
885
|
if (names.has(key))
|
|
665
886
|
throw new Error(taken);
|
|
666
887
|
names.add(key);
|
|
888
|
+
return { dispose: () => void names.delete(key) };
|
|
667
889
|
};
|
|
668
890
|
await failsAs('its state did not load', () => modState.load());
|
|
669
891
|
await failsAs('its options did not load', () => modOptions.load(root));
|
|
670
892
|
const missing = modOptions.missing;
|
|
671
893
|
if (missing.length > 0)
|
|
672
894
|
throw new MissingOptions(missing, missing.map((key) => definition.options?.[key]?.title ?? key));
|
|
895
|
+
await failsAs('the details of its session did not load', () => followCurrent(undefined));
|
|
673
896
|
await failsAs('its setup function threw', () => definition.setup(mod));
|
|
674
897
|
const permissionHooks = permissionEvents.filter((event) => router.has(event));
|
|
675
898
|
if (permissionHooks.length > 0 && !runtime.checksPermissions()) {
|
|
676
899
|
throw new Error(`it decides permissions on ${listed(permissionHooks)}, so hooks/register.ts must call registerPermissionCheck(addHook) after registerMod`);
|
|
677
900
|
}
|
|
678
|
-
if (held !== undefined &&
|
|
901
|
+
if (held !== undefined && hooksAdded.includes(PreToolUse))
|
|
679
902
|
router.add('tool.check', heldDecisionHook(held));
|
|
680
903
|
await failsAs('its open panes did not load', () => area.restorePanes());
|
|
681
|
-
if (
|
|
682
|
-
added.push(`${
|
|
683
|
-
return { mod, added };
|
|
904
|
+
if (hooksAdded.length > 0)
|
|
905
|
+
added.push(`${hooksAdded.length === 1 ? 'a hook' : 'hooks'} on ${listed(hooksAdded)}`);
|
|
906
|
+
return { mod, added, imports };
|
|
684
907
|
}
|
|
685
|
-
function checkingKeys(claude,
|
|
908
|
+
function checkingKeys(claude, toast, keys) {
|
|
686
909
|
const bound = Object.entries(keys);
|
|
687
910
|
if (bound.length === 0)
|
|
688
911
|
return claude;
|
|
689
912
|
const register = (command) => {
|
|
690
913
|
for (const [key, commandName] of bound) {
|
|
691
914
|
if (commandName === command.name && command.immediate !== true) {
|
|
692
|
-
|
|
915
|
+
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 });
|
|
693
916
|
}
|
|
694
917
|
}
|
|
695
918
|
return claude.command.register(command);
|
|
@@ -702,10 +925,10 @@ function checkedMs(name, subject, ms) {
|
|
|
702
925
|
throw new Error(`${name}: ${subject} is ${ms}. Give a whole number of milliseconds above 0 and at most ${longestTimerMs}.`);
|
|
703
926
|
return ms;
|
|
704
927
|
}
|
|
705
|
-
function timedHook(
|
|
928
|
+
function timedHook(event, hook, ms, claude, toast) {
|
|
706
929
|
return (input) => new Promise((resolve, reject) => {
|
|
707
930
|
const timer = claude.clock.after(ms, () => {
|
|
708
|
-
|
|
931
|
+
toast.warning(`The ${event} hook passed its ${ms / 1000} s timeout`, { id: `timeout:${event}` });
|
|
709
932
|
resolve(undefined);
|
|
710
933
|
});
|
|
711
934
|
Promise.resolve()
|
|
@@ -727,27 +950,6 @@ async function failsAs(subject, step) {
|
|
|
727
950
|
throw new Error(`${subject}: ${messageOf(error)}`, { cause: error });
|
|
728
951
|
}
|
|
729
952
|
}
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
let lastError = '';
|
|
733
|
-
let piece = await stream.next();
|
|
734
|
-
while (piece.done !== true) {
|
|
735
|
-
if (piece.value.stream === 'stderr') {
|
|
736
|
-
lastError = lastLineOf(piece.value.text) ?? lastError;
|
|
737
|
-
}
|
|
738
|
-
else {
|
|
739
|
-
const lines = `${pending}${piece.value.text}`.split('\n');
|
|
740
|
-
pending = lines.pop() ?? '';
|
|
741
|
-
for (const text of lines)
|
|
742
|
-
onLine(text);
|
|
743
|
-
}
|
|
744
|
-
piece = await stream.next();
|
|
745
|
-
}
|
|
746
|
-
if (pending !== '')
|
|
747
|
-
onLine(pending);
|
|
748
|
-
return { code: piece.value.code, lastError };
|
|
749
|
-
}
|
|
750
|
-
function lastLineOf(text) {
|
|
751
|
-
const trimmed = text.trim();
|
|
752
|
-
return trimmed === '' ? undefined : trimmed.slice(trimmed.lastIndexOf('\n') + 1).trim();
|
|
953
|
+
function progressOf({ done, total, label }) {
|
|
954
|
+
return Box({ gap: 2, children: [Progress({ value: total > 0 ? (done / total) * 100 : 0 }), Text({ dimColor: true, wrap: 'truncate-end', children: label })] });
|
|
753
955
|
}
|