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