@cmodjs/core 0.1.14 → 0.2.1
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 +32 -21
- package/docs/dependencies.md +8 -0
- package/docs/index.md +12 -4
- package/docs/install-steps.md +36 -4
- package/docs/jobs.md +25 -9
- package/docs/mod.md +103 -37
- package/docs/options.md +93 -0
- package/docs/permissions.md +88 -0
- package/docs/state.md +4 -31
- package/docs/testing.md +13 -2
- package/installer.d.ts +10 -0
- package/installer.d.ts.map +1 -0
- package/installer.js +8 -0
- package/jobs/context.d.ts +2 -2
- package/jobs/context.d.ts.map +1 -1
- package/jobs/context.js +5 -3
- package/jobs/permissions.js +1 -1
- package/jobs/program.d.ts.map +1 -1
- package/jobs/program.js +5 -1
- package/jobs/prompt.js +5 -5
- package/jobs/schedule.d.ts.map +1 -1
- package/jobs/schedule.js +2 -1
- package/jobs/slash-command.d.ts.map +1 -1
- package/jobs/slash-command.js +3 -2
- package/jobs/status-line.d.ts.map +1 -1
- package/jobs/status-line.js +3 -2
- package/jobs/tool.d.ts.map +1 -1
- package/jobs/tool.js +3 -2
- package/mod.d.ts +48 -10
- package/mod.d.ts.map +1 -1
- package/mod.js +3 -0
- package/options.d.ts +58 -0
- package/options.d.ts.map +1 -0
- package/options.js +66 -0
- package/package.json +1 -1
- package/records.d.ts +16 -1
- package/records.d.ts.map +1 -1
- package/records.js +119 -7
- package/register.d.ts +4 -2
- package/register.d.ts.map +1 -1
- package/register.js +8 -2
- package/runtime/claude.d.ts +5 -1
- package/runtime/claude.d.ts.map +1 -1
- package/runtime/grants.d.ts +28 -0
- package/runtime/grants.d.ts.map +1 -0
- package/runtime/grants.js +286 -0
- package/runtime/installer.d.ts +24 -0
- package/runtime/installer.d.ts.map +1 -0
- package/runtime/installer.js +118 -0
- package/runtime/lifecycle.d.ts +6 -3
- package/runtime/lifecycle.d.ts.map +1 -1
- package/runtime/lifecycle.js +226 -36
- package/runtime/metadata.d.ts +20 -0
- package/runtime/metadata.d.ts.map +1 -0
- package/runtime/metadata.js +143 -0
- package/runtime/options.d.ts +21 -0
- package/runtime/options.d.ts.map +1 -0
- package/runtime/options.js +100 -0
- package/runtime/state.d.ts +1 -0
- package/runtime/state.d.ts.map +1 -1
- package/runtime/state.js +5 -75
- package/src/installer.ts +17 -0
- package/src/jobs/context.ts +5 -3
- package/src/jobs/permissions.ts +1 -1
- package/src/jobs/program.ts +4 -1
- package/src/jobs/prompt.ts +5 -5
- package/src/jobs/schedule.ts +2 -1
- package/src/jobs/slash-command.ts +3 -2
- package/src/jobs/status-line.ts +3 -2
- package/src/jobs/tool.ts +3 -2
- package/src/mod.ts +46 -11
- package/src/options.ts +91 -0
- package/src/records.ts +131 -8
- package/src/register.ts +12 -3
- package/src/runtime/claude.ts +2 -0
- package/src/runtime/grants.ts +306 -0
- package/src/runtime/installer.ts +169 -0
- package/src/runtime/lifecycle.ts +227 -45
- package/src/runtime/metadata.ts +153 -0
- package/src/runtime/options.ts +102 -0
- package/src/runtime/state.ts +7 -70
- package/src/testing/fake-claude.ts +6 -1
- package/src/testing/fake-files.ts +9 -3
- package/src/testing.ts +40 -15
- package/src/utils/metadata.ts +242 -0
- package/testing/fake-claude.d.ts +4 -0
- package/testing/fake-claude.d.ts.map +1 -1
- package/testing/fake-claude.js +5 -1
- package/testing/fake-files.d.ts.map +1 -1
- package/testing/fake-files.js +9 -3
- package/testing.d.ts +6 -2
- package/testing.d.ts.map +1 -1
- package/testing.js +29 -13
- package/utils/metadata.d.ts +21 -0
- package/utils/metadata.d.ts.map +1 -0
- package/utils/metadata.js +250 -0
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { permissionWords } from '../records.js';
|
|
2
|
+
import { Box, Button, drawWith, Input, Text } from '../ui/elements.js';
|
|
3
|
+
import { messageOf } from '../utils/text.js';
|
|
4
|
+
export const installerPaneId = 'cmod-installer';
|
|
5
|
+
export function createInstaller(name, claude) {
|
|
6
|
+
let page;
|
|
7
|
+
let isOpen = false;
|
|
8
|
+
const show = async (build, cancelled) => {
|
|
9
|
+
page?.cancel();
|
|
10
|
+
const answer = new Promise((resolve) => {
|
|
11
|
+
const done = (value) => {
|
|
12
|
+
if (page === shown)
|
|
13
|
+
page = undefined;
|
|
14
|
+
resolve(value);
|
|
15
|
+
};
|
|
16
|
+
const shown = { ...build(done), cancel: () => done(cancelled) };
|
|
17
|
+
page = shown;
|
|
18
|
+
});
|
|
19
|
+
if (!isOpen) {
|
|
20
|
+
isOpen = true;
|
|
21
|
+
await claude().ui.open({ id: installerPaneId, title: `Install ${name}`, focus: true, holdToasts: true, closeOnEscape: true });
|
|
22
|
+
}
|
|
23
|
+
claude().ui.invalidate('ui.render');
|
|
24
|
+
return answer;
|
|
25
|
+
};
|
|
26
|
+
const frame = (title, body, actions) => Box({ flexDirection: 'column', gap: 1, paddingX: 1, children: [Text({ bold: true, children: title }), ...body, Box({ gap: 3, children: actions })] });
|
|
27
|
+
const bullet = (text) => Text({ children: [Text({ color: 'claude', children: '◆ ' }), text] });
|
|
28
|
+
return {
|
|
29
|
+
draws: (e) => e.component === 'Pane' && e.requestId === installerPaneId,
|
|
30
|
+
draw(e) {
|
|
31
|
+
return drawWith(claude().ui.resolve(e), () => page?.draw(e.props) ?? Text({ dimColor: true, children: `${name} is installing.` }));
|
|
32
|
+
},
|
|
33
|
+
closed(id) {
|
|
34
|
+
if (id !== installerPaneId)
|
|
35
|
+
return;
|
|
36
|
+
isOpen = false;
|
|
37
|
+
page?.cancel();
|
|
38
|
+
},
|
|
39
|
+
consent({ install, uninstall, keys, permissions, updates }) {
|
|
40
|
+
const changes = [
|
|
41
|
+
...(install === '' ? [] : [`Run ${install} now${uninstall === '' ? '' : `, and ${uninstall} when you remove it`}`]),
|
|
42
|
+
...(install === '' && uninstall !== '' ? [`Run ${uninstall} when you remove it`] : []),
|
|
43
|
+
...(keys === '' ? [] : [`Bind ${keys}`]),
|
|
44
|
+
...(updates === undefined ? [] : [`Update ${name} automatically from the ${updates} marketplace`]),
|
|
45
|
+
];
|
|
46
|
+
return show((answer) => ({
|
|
47
|
+
draw: () => frame(`${name} wants to:`, [
|
|
48
|
+
...(permissions.length === 0 ? [] : [Box({ flexDirection: 'column', children: permissions.map((item) => bullet(permissionWords(item))) })]),
|
|
49
|
+
...(changes.length === 0 ? [] : [Text({ children: permissions.length === 0 ? 'Change your computer:' : 'and change your computer:' }), Box({ flexDirection: 'column', children: changes.map(bullet) })]),
|
|
50
|
+
Text({ dimColor: true, children: 'You can turn each permission off later in /mods.' }),
|
|
51
|
+
], [Button({ key: 'not-now', hotkey: 'n', label: 'Not now', onPress: () => answer(false) }), Button({ key: 'accept', hotkey: 'a', label: 'Accept', onPress: () => answer(true) })]),
|
|
52
|
+
}), false);
|
|
53
|
+
},
|
|
54
|
+
options(missing, save) {
|
|
55
|
+
const saved = new Map();
|
|
56
|
+
const failures = new Map();
|
|
57
|
+
return show((answer) => ({
|
|
58
|
+
draw: () => frame(`${name} needs ${missing.length === 1 ? 'one setting' : `${missing.length} settings`}:`, missing.map(([key, option]) => {
|
|
59
|
+
const label = Text({ children: [Text({ bold: true, children: option.title }), option.description === '' ? '' : Text({ dimColor: true, children: ` ${option.description}` })] });
|
|
60
|
+
if (option.kind === 'secret')
|
|
61
|
+
return Box({ flexDirection: 'column', children: [label, Text({ dimColor: true, children: `Set it in /config, where Claude Code keeps it in your keychain, then press Next.` })] });
|
|
62
|
+
const failure = failures.get(key);
|
|
63
|
+
const field = Input({
|
|
64
|
+
key: `option:${key}`,
|
|
65
|
+
label: '› ',
|
|
66
|
+
placeholder: option.kind === 'choice' ? (option.choices ?? []).join(', ') : option.kind === 'toggle' ? 'true or false' : option.kind === 'list' ? 'comma-separated' : '',
|
|
67
|
+
value: saved.get(key) ?? '',
|
|
68
|
+
submitLabel: 'save',
|
|
69
|
+
onSubmit: (value) => void save(key, value)
|
|
70
|
+
.then((refused) => {
|
|
71
|
+
if (refused === undefined) {
|
|
72
|
+
saved.set(key, value);
|
|
73
|
+
failures.delete(key);
|
|
74
|
+
}
|
|
75
|
+
else
|
|
76
|
+
failures.set(key, refused);
|
|
77
|
+
})
|
|
78
|
+
.catch((error) => failures.set(key, messageOf(error)))
|
|
79
|
+
.finally(() => claude().ui.invalidate('ui.render')),
|
|
80
|
+
});
|
|
81
|
+
const status = failure !== undefined ? Text({ color: 'error', children: failure }) : saved.has(key) ? Text({ color: 'success', children: '✔ saved' }) : Text({ children: '' });
|
|
82
|
+
return Box({ flexDirection: 'column', children: [label, field, status] });
|
|
83
|
+
}), [Button({ key: 'not-now', hotkey: 'n', label: 'Not now', onPress: () => answer(false) }), Button({ key: 'next', hotkey: 'x', label: 'Next', onPress: () => answer(true) })]),
|
|
84
|
+
}), false);
|
|
85
|
+
},
|
|
86
|
+
step(mod, step, index, total) {
|
|
87
|
+
let notice;
|
|
88
|
+
return show((answer) => ({
|
|
89
|
+
draw: (props) => {
|
|
90
|
+
let body;
|
|
91
|
+
try {
|
|
92
|
+
body = step.render(mod, props);
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
body = Text({ color: 'error', children: `The ${step.title} step could not draw: ${messageOf(error)}` });
|
|
96
|
+
}
|
|
97
|
+
const next = async () => {
|
|
98
|
+
const isDone = await Promise.resolve(step.isDone(mod)).catch(() => false);
|
|
99
|
+
if (isDone)
|
|
100
|
+
answer(true);
|
|
101
|
+
else {
|
|
102
|
+
notice = `Finish ${step.title} first.`;
|
|
103
|
+
claude().ui.invalidate('ui.render');
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
return frame(`${step.title} (${index} of ${total})`, [body, ...(notice === undefined ? [] : [Text({ color: 'warning', children: notice })])], [Button({ key: 'not-now', hotkey: 'n', label: 'Not now', onPress: () => answer(false) }), Button({ key: 'next', hotkey: 'x', label: index === total ? 'Finish' : 'Next', onPress: () => void next() })]);
|
|
107
|
+
},
|
|
108
|
+
}), false);
|
|
109
|
+
},
|
|
110
|
+
async close() {
|
|
111
|
+
page = undefined;
|
|
112
|
+
if (!isOpen)
|
|
113
|
+
return;
|
|
114
|
+
isOpen = false;
|
|
115
|
+
await claude().ui.close({ id: installerPaneId });
|
|
116
|
+
},
|
|
117
|
+
};
|
|
118
|
+
}
|
package/runtime/lifecycle.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import type { Args, EventResult, Frozen } from 'claude-code';
|
|
1
|
+
import type { Args, EventResult, Frozen, PluginOptions } from 'claude-code';
|
|
2
2
|
import type { Mod, ModDefinition } from '../mod.js';
|
|
3
|
+
import type { Options } from '../options.js';
|
|
4
|
+
import { type Steps } from '../records.js';
|
|
3
5
|
import type { Claude } from './claude.js';
|
|
4
6
|
import { type RoutedEvent } from './hooks.js';
|
|
5
7
|
import { type RouterNext } from './router.js';
|
|
@@ -10,7 +12,8 @@ export type Plugin = {
|
|
|
10
12
|
readonly store: string;
|
|
11
13
|
readonly isInstalled: boolean;
|
|
12
14
|
readonly shouldRecord: boolean;
|
|
13
|
-
readonly
|
|
15
|
+
readonly steps: Steps;
|
|
16
|
+
readonly granted: readonly string[];
|
|
14
17
|
};
|
|
15
18
|
export type Phase = 'starting' | 'installing' | 'waiting' | 'declined' | 'failed' | 'ready' | 'active';
|
|
16
19
|
export type Lifecycle<State extends object> = {
|
|
@@ -21,5 +24,5 @@ export type Lifecycle<State extends object> = {
|
|
|
21
24
|
route<N extends RoutedEvent>(event: N, e: Frozen<Args<N>>, next: RouterNext<N>): Promise<EventResult<N>>;
|
|
22
25
|
};
|
|
23
26
|
export declare function readPlugin(claude: Claude): Promise<Plugin>;
|
|
24
|
-
export declare function createLifecycle<State extends object>(definition: ModDefinition<State>, checksPermissions?: () => boolean): Lifecycle<State>;
|
|
27
|
+
export declare function createLifecycle<State extends object, Declared extends Options = Options>(definition: ModDefinition<State, string, Declared>, checksPermissions?: () => boolean, options?: PluginOptions, asksPerson?: boolean): Lifecycle<State>;
|
|
25
28
|
//# sourceMappingURL=lifecycle.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lifecycle.d.ts","sourceRoot":"","sources":["../src/runtime/lifecycle.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,WAAW,EAAE,MAAM,
|
|
1
|
+
{"version":3,"file":"lifecycle.d.ts","sourceRoot":"","sources":["../src/runtime/lifecycle.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,WAAW,EAAE,MAAM,EAA0B,aAAa,EAAyC,MAAM,aAAa,CAAA;AAC1I,OAAO,KAAK,EAAa,GAAG,EAAE,aAAa,EAAiC,MAAM,WAAW,CAAA;AAC7F,OAAO,KAAK,EAAE,OAAO,EAAgB,MAAM,eAAe,CAAA;AAC1D,OAAO,EAAgS,KAAK,KAAK,EAAE,MAAM,eAAe,CAAA;AAGxU,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AAGzC,OAAO,EAAsG,KAAK,WAAW,EAAE,MAAM,YAAY,CAAA;AAMjJ,OAAO,EAA6B,KAAK,UAAU,EAAE,MAAM,aAAa,CAAA;AAKxE,MAAM,MAAM,MAAM,GAAG;IACnB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,SAAS,CAAA;IACpC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,WAAW,EAAE,OAAO,CAAA;IAC7B,QAAQ,CAAC,YAAY,EAAE,OAAO,CAAA;IAC9B,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAA;IACrB,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAA;CACpC,CAAA;AAED,MAAM,MAAM,KAAK,GAAG,UAAU,GAAG,YAAY,GAAG,SAAS,GAAG,UAAU,GAAG,QAAQ,GAAG,OAAO,GAAG,QAAQ,CAAA;AAEtG,MAAM,MAAM,SAAS,CAAC,KAAK,SAAS,MAAM,IAAI;IAC5C,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAA;IACrB,QAAQ,CAAC,GAAG,EAAE,GAAG,CAAC,KAAK,CAAC,GAAG,SAAS,CAAA;IACpC,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAA;IACzB,KAAK,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAC/E,KAAK,CAAC,CAAC,SAAS,WAAW,EAAE,KAAK,EAAE,CAAC,EAAE,CAAC,EAAE,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,UAAU,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,CAAA;CACzG,CAAA;AAqCD,wBAAsB,UAAU,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CA2BhE;AAkCD,wBAAgB,eAAe,CAAC,KAAK,SAAS,MAAM,EAAE,QAAQ,SAAS,OAAO,GAAG,OAAO,EACtF,UAAU,EAAE,aAAa,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,CAAC,EAClD,iBAAiB,GAAE,MAAM,OAAoB,EAC7C,OAAO,GAAE,aAAkB,EAC3B,UAAU,UAAO,GAChB,SAAS,CAAC,KAAK,CAAC,CA0VlB"}
|
package/runtime/lifecycle.js
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
|
-
import { dataFolder, isAtLeast, oldestCmodFor, parseEvent, readRecord, readSteps, scriptsSha256, storeFolder } from '../records.js';
|
|
1
|
+
import { consentPath, dataFolder, finishStepsMethod, isAtLeast, marketplaceOf, oldestCmodFor, openPageMethod, parseConsent, pendingStepsMethod, parseEvent, permissionWords, readRecord, readSteps, scriptsSha256, settingsPagesMethod, storeFolder, updatesToTurnOn } from '../records.js';
|
|
2
2
|
import { relativePath } from '../utils/paths.js';
|
|
3
3
|
import { formatExit, listed, messageOf } from '../utils/text.js';
|
|
4
4
|
import { beforeDeadline } from './deadline.js';
|
|
5
5
|
import { answerCall, dependencyCalls, notInstalled } from './dependencies.js';
|
|
6
6
|
import { classicHook, heldDecisionHook, permissionEvents, preToolUseHook, userSkillHook } from './hooks.js';
|
|
7
|
+
import { checkedAnswer, checkingGrants, checksAnswers, checkWrite, isCovered, itemOf, passedDown } from './grants.js';
|
|
8
|
+
import { createModFiles } from './metadata.js';
|
|
9
|
+
import { fitsOption } from '../options.js';
|
|
10
|
+
import { createInstaller } from './installer.js';
|
|
11
|
+
import { createOptions, MissingOptions } from './options.js';
|
|
7
12
|
import { createRouter } from './router.js';
|
|
8
13
|
import { createState } from './state.js';
|
|
9
14
|
import { toolCalls } from './tool-calls.js';
|
|
@@ -30,12 +35,14 @@ export async function readPlugin(claude) {
|
|
|
30
35
|
const manifest = (await readJson(read, `${root}/.claude-plugin/plugin.json`));
|
|
31
36
|
const version = typeof manifest?.version === 'string' ? manifest.version : undefined;
|
|
32
37
|
const store = storeFolder({ HOME: await claude.env.home(), XDG_DATA_HOME: await claude.env.dataHome() });
|
|
38
|
+
const steps = readSteps(await readJson(read, `${root}/package.json`)) ?? {};
|
|
39
|
+
const declared = steps.permissions ?? [];
|
|
33
40
|
if (name === cmodPluginName) {
|
|
34
41
|
const installed = await cmodVersion(claude);
|
|
35
|
-
return { name, root, version, store,
|
|
42
|
+
return { name, root, version, store, steps, granted: declared, isInstalled: version !== undefined && installed !== undefined && isAtLeast(installed, version), shouldRecord: false };
|
|
36
43
|
}
|
|
37
|
-
const
|
|
38
|
-
const plugin = { name, root, version, store,
|
|
44
|
+
const approved = parseConsent(await readJson(read, consentPath(store)), consentPath(store))[name] ?? [];
|
|
45
|
+
const plugin = { name, root, version, store, steps, granted: declared.filter((item) => approved.includes(item)) };
|
|
39
46
|
const record = await readRecord(read, store, name);
|
|
40
47
|
if (Object.keys(steps).length === 0)
|
|
41
48
|
return { ...plugin, isInstalled: true, shouldRecord: record?.version !== version };
|
|
@@ -79,15 +86,18 @@ async function cmodVersion(claude) {
|
|
|
79
86
|
throw new Error(`cmod --version ${formatExit(result.exitCode, lastLineOf(result.stderr) ?? '')}`);
|
|
80
87
|
return result.stdout.trim().split(/\s+/).at(-1);
|
|
81
88
|
}
|
|
82
|
-
export function createLifecycle(definition, checksPermissions = () => true) {
|
|
89
|
+
export function createLifecycle(definition, checksPermissions = () => true, options = {}, asksPerson = true) {
|
|
83
90
|
let router = createRouter();
|
|
84
91
|
let phase = 'starting';
|
|
85
92
|
let runtime;
|
|
86
93
|
let plugin;
|
|
94
|
+
const granted = new Set();
|
|
87
95
|
let line;
|
|
88
96
|
let activation;
|
|
89
97
|
let mod;
|
|
90
98
|
let failure;
|
|
99
|
+
let pluginOptions = options;
|
|
100
|
+
const installer = createInstaller(definition.name, () => claude());
|
|
91
101
|
let shouldRecord = false;
|
|
92
102
|
let recording;
|
|
93
103
|
let missedSessionStart;
|
|
@@ -95,7 +105,7 @@ export function createLifecycle(definition, checksPermissions = () => true) {
|
|
|
95
105
|
const startSettled = new Promise((resolve) => (settleStart = resolve));
|
|
96
106
|
const claude = () => {
|
|
97
107
|
if (runtime === undefined)
|
|
98
|
-
throw new Error(`${definition.name}: the lifecycle has not started. registerMod(addHook, mod) starts it at session.start.`);
|
|
108
|
+
throw new Error(`${definition.name}: the lifecycle has not started. registerMod(addHook, mod, options) starts it at session.start.`);
|
|
99
109
|
return runtime.claude;
|
|
100
110
|
};
|
|
101
111
|
const showLine = () => {
|
|
@@ -132,20 +142,71 @@ export function createLifecycle(definition, checksPermissions = () => true) {
|
|
|
132
142
|
recording = undefined;
|
|
133
143
|
});
|
|
134
144
|
};
|
|
145
|
+
const saveOption = async (key, text) => {
|
|
146
|
+
const option = definition.options?.[key];
|
|
147
|
+
if (option === undefined || plugin === undefined)
|
|
148
|
+
return `${definition.name} has no option ${key}.`;
|
|
149
|
+
const value = option.kind === 'number' ? Number(text) : option.kind === 'toggle' ? text.trim() === 'true' : option.kind === 'list' ? text.split(',').map((item) => item.trim()).filter((item) => item !== '') : text;
|
|
150
|
+
const misfit = option.kind === 'toggle' && !['true', 'false'].includes(text.trim()) ? 'takes true or false' : fitsOption(option, value);
|
|
151
|
+
if (misfit !== undefined)
|
|
152
|
+
return `${option.title} ${misfit}.`;
|
|
153
|
+
const saved = await claude().config.set({ key: `${plugin.name}.${key}`, value });
|
|
154
|
+
return saved.deny;
|
|
155
|
+
};
|
|
156
|
+
const askOptions = async (missing) => {
|
|
157
|
+
if (!asksPerson || plugin === undefined || (await claude().session.surfaces()).length === 0)
|
|
158
|
+
return false;
|
|
159
|
+
const declared = definition.options ?? {};
|
|
160
|
+
const asked = missing.keys.flatMap((key) => {
|
|
161
|
+
const option = declared[key];
|
|
162
|
+
return option === undefined ? [] : [[key, option]];
|
|
163
|
+
});
|
|
164
|
+
if (!(await installer.options(asked, saveOption)))
|
|
165
|
+
return false;
|
|
166
|
+
const prefix = `${plugin.name}.`;
|
|
167
|
+
const rows = await claude().config.list();
|
|
168
|
+
pluginOptions = { ...pluginOptions, ...Object.fromEntries(rows.filter((row) => row.key.startsWith(prefix)).map((row) => [row.key.slice(prefix.length), row.value])) };
|
|
169
|
+
return true;
|
|
170
|
+
};
|
|
171
|
+
const pendingSteps = async (active) => {
|
|
172
|
+
const pending = [];
|
|
173
|
+
for (const step of definition.installer ?? [])
|
|
174
|
+
if (!(await Promise.resolve(step.isDone(active)).catch(() => false)))
|
|
175
|
+
pending.push(step.title);
|
|
176
|
+
return pending;
|
|
177
|
+
};
|
|
178
|
+
const runSteps = async (active) => {
|
|
179
|
+
const steps = asksPerson ? (definition.installer ?? []) : [];
|
|
180
|
+
for (const [index, step] of steps.entries()) {
|
|
181
|
+
if (await Promise.resolve(step.isDone(active)).catch(() => false))
|
|
182
|
+
continue;
|
|
183
|
+
const isAnswered = (await claude().session.surfaces()).length > 0 && (await installer.step(active, step, index + 1, steps.length));
|
|
184
|
+
if (!isAnswered) {
|
|
185
|
+
claude().ui.log(`${definition.name} needs ${steps.length - index === 1 ? 'one more step' : `${steps.length - index} more steps`}, starting with ${step.title}. Run /mods ${definition.name} to finish.`);
|
|
186
|
+
break;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
await installer.close();
|
|
190
|
+
};
|
|
135
191
|
const activate = async () => {
|
|
136
192
|
if (runtime === undefined || plugin === undefined)
|
|
137
193
|
return;
|
|
138
194
|
const activeRouter = createRouter();
|
|
139
|
-
const active = await createMod(definition, { ...runtime, router: activeRouter, dataFolder: dataFolder(plugin.store, plugin.name),
|
|
140
|
-
|
|
195
|
+
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) => {
|
|
196
|
+
if (error instanceof MissingOptions && (await askOptions(error)))
|
|
197
|
+
return undefined;
|
|
198
|
+
fail(messageOf(error), error instanceof MissingOptions ? `Set ${error.keys.length === 1 ? 'it' : 'them'} in /config.` : 'Fix it, then run /reload-plugins.');
|
|
141
199
|
throw error;
|
|
142
200
|
});
|
|
201
|
+
if (active === undefined)
|
|
202
|
+
return activate();
|
|
143
203
|
const { added } = active;
|
|
144
204
|
mod = active.mod;
|
|
145
205
|
router = activeRouter;
|
|
146
206
|
phase = 'active';
|
|
147
207
|
runtime.claude.ui.invalidate('ui.render');
|
|
148
208
|
endLine();
|
|
209
|
+
void runSteps(active.mod).catch((error) => claude().ui.log(`${definition.name}: its installer stopped: ${messageOf(error)}`));
|
|
149
210
|
const missed = missedSessionStart;
|
|
150
211
|
missedSessionStart = undefined;
|
|
151
212
|
const lateAnswer = missed === undefined ? {} : await activeRouter.dispatch('classic.SessionStart', missed, async () => ({})).catch((error) => {
|
|
@@ -163,6 +224,12 @@ export function createLifecycle(definition, checksPermissions = () => true) {
|
|
|
163
224
|
if (added.length > 0)
|
|
164
225
|
runtime.claude.ui.log(`${definition.name} added ${listed(added)}.`);
|
|
165
226
|
};
|
|
227
|
+
const updatesToShow = async () => {
|
|
228
|
+
const [home, configHome] = await Promise.all([claude().env.home(), claude().env.configHome()]);
|
|
229
|
+
const configRoot = configHome ?? `${home ?? ''}/.claude`;
|
|
230
|
+
const known = await claude().fs.read(`${configRoot}/plugins/known_marketplaces.json`).then((text) => JSON.parse(text), () => undefined);
|
|
231
|
+
return updatesToTurnOn(marketplaceOf(claude().plugin.root, claude().plugin.name), await claude().settings.read({ source: 'user' }), known);
|
|
232
|
+
};
|
|
166
233
|
const askConsent = async (event) => {
|
|
167
234
|
const name = definition.name;
|
|
168
235
|
if ((await claude().session.surfaces()).length === 0) {
|
|
@@ -171,14 +238,9 @@ export function createLifecycle(definition, checksPermissions = () => true) {
|
|
|
171
238
|
return;
|
|
172
239
|
}
|
|
173
240
|
showLine().wait('Waiting for your answer');
|
|
174
|
-
|
|
175
|
-
...(event.install === '' ? [] : [`runs ${event.install} to install`]),
|
|
176
|
-
...(event.uninstall === '' ? [] : [`runs ${event.uninstall} when you remove it`]),
|
|
177
|
-
...(event.keys === '' ? [] : [`binds ${event.keys}`]),
|
|
178
|
-
];
|
|
179
|
-
const answer = await claude().ui.ask(`${name} ${listed(actions)}. Install it now?`, { options: ['Install', 'Not now'], header: 'Install' });
|
|
180
|
-
if (answer === 'Install')
|
|
241
|
+
if (await installer.consent({ ...event, updates: await updatesToShow().catch(() => undefined) }))
|
|
181
242
|
return install(event.sha256);
|
|
243
|
+
await installer.close();
|
|
182
244
|
phase = 'declined';
|
|
183
245
|
endLine();
|
|
184
246
|
claude().ui.log(`${name} is not installed. Run cmod install ${name} to install it.`);
|
|
@@ -203,14 +265,17 @@ export function createLifecycle(definition, checksPermissions = () => true) {
|
|
|
203
265
|
if (event.kind === 'done' || event.kind === 'failed' || event.kind === 'needs-consent')
|
|
204
266
|
outcome = event;
|
|
205
267
|
});
|
|
206
|
-
if (outcome?.kind === 'done')
|
|
268
|
+
if (outcome?.kind === 'done') {
|
|
269
|
+
for (const item of plugin.steps.permissions ?? [])
|
|
270
|
+
granted.add(item);
|
|
207
271
|
return finish();
|
|
272
|
+
}
|
|
208
273
|
if (outcome?.kind === 'needs-consent')
|
|
209
274
|
return askConsent(outcome);
|
|
210
275
|
const reason = outcome?.kind === 'failed' ? outcome.message : `cmod setup ${formatExit(code, lastError)}`;
|
|
211
276
|
fail(reason, `Fix the cause, then run: cmod install ${definition.name}`);
|
|
212
277
|
};
|
|
213
|
-
const fits = (found) => found !== undefined && isAtLeast(found, oldestCmodFor(plugin?.
|
|
278
|
+
const fits = (found) => found !== undefined && isAtLeast(found, oldestCmodFor(plugin?.steps ?? {}));
|
|
214
279
|
const waitForcmod = (found) => {
|
|
215
280
|
phase = 'waiting';
|
|
216
281
|
showLine().wait('Waiting for cmod');
|
|
@@ -237,7 +302,7 @@ export function createLifecycle(definition, checksPermissions = () => true) {
|
|
|
237
302
|
});
|
|
238
303
|
const longWait = claude().clock.after(cmodWaitMs, () => showLine().wait(latest === undefined
|
|
239
304
|
? "Still waiting for cmod to download cmod. See cmod's own line."
|
|
240
|
-
: `PATH finds cmod ${latest}, and ${definition.name} needs cmod ${oldestCmodFor(plugin?.
|
|
305
|
+
: `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.`));
|
|
241
306
|
};
|
|
242
307
|
const bootstrap = async (root) => {
|
|
243
308
|
const progress = showLine();
|
|
@@ -256,6 +321,47 @@ export function createLifecycle(definition, checksPermissions = () => true) {
|
|
|
256
321
|
if (phase !== 'failed')
|
|
257
322
|
fail(messageOf(error), `Run cmod install ${definition.name} in a terminal to see the whole log.`);
|
|
258
323
|
};
|
|
324
|
+
let consentMs = 0;
|
|
325
|
+
const consentStamp = async (path) => {
|
|
326
|
+
const stat = await claude().fs.stat(path).catch(() => undefined);
|
|
327
|
+
return stat?.kind === 'file' ? stat.mtimeMs : 0;
|
|
328
|
+
};
|
|
329
|
+
const refreshGrants = async () => {
|
|
330
|
+
if (plugin === undefined || plugin.name === cmodPluginName)
|
|
331
|
+
return;
|
|
332
|
+
const path = consentPath(plugin.store);
|
|
333
|
+
const modifiedMs = await consentStamp(path);
|
|
334
|
+
if (modifiedMs === consentMs)
|
|
335
|
+
return;
|
|
336
|
+
consentMs = modifiedMs;
|
|
337
|
+
const approved = modifiedMs === 0 ? [] : (parseConsent(JSON.parse(await claude().fs.read(path)), path)[plugin.name] ?? []);
|
|
338
|
+
granted.clear();
|
|
339
|
+
for (const item of plugin.steps.permissions ?? []) {
|
|
340
|
+
if (approved.includes(item))
|
|
341
|
+
granted.add(item);
|
|
342
|
+
}
|
|
343
|
+
};
|
|
344
|
+
const droppedParts = new Set();
|
|
345
|
+
const answerCheck = {
|
|
346
|
+
get plugin() {
|
|
347
|
+
return plugin?.name ?? definition.name;
|
|
348
|
+
},
|
|
349
|
+
isGranted: (item) => isCovered(granted, item, undefined),
|
|
350
|
+
dropped(item, part) {
|
|
351
|
+
if (droppedParts.has(item))
|
|
352
|
+
return;
|
|
353
|
+
droppedParts.add(item);
|
|
354
|
+
const declared = plugin?.steps.permissions ?? [];
|
|
355
|
+
const fix = declared.includes(item) ? `Turn it on in /mods ${definition.name}.` : `It needs "permissions": { "${item}": true } in package.json "cmod".`;
|
|
356
|
+
claude().ui.log(`${definition.name} answered ${part} without your grant to "${permissionWords(item)}", so cmod dropped it. ${fix}`);
|
|
357
|
+
},
|
|
358
|
+
};
|
|
359
|
+
const checkedRoute = async (event, e, next) => {
|
|
360
|
+
let below;
|
|
361
|
+
const passOn = (passed) => (below ??= next(passedDown(event, e, passed, answerCheck)));
|
|
362
|
+
const answer = await router.dispatch(event, e, passOn);
|
|
363
|
+
return (await checkedAnswer(event, e, answer, () => passOn(e), answerCheck));
|
|
364
|
+
};
|
|
259
365
|
const whenActive = async () => {
|
|
260
366
|
if (phase === 'ready')
|
|
261
367
|
activation ??= activate().catch(report);
|
|
@@ -291,6 +397,9 @@ export function createLifecycle(definition, checksPermissions = () => true) {
|
|
|
291
397
|
if (started === undefined)
|
|
292
398
|
return;
|
|
293
399
|
plugin = started;
|
|
400
|
+
for (const item of started.granted)
|
|
401
|
+
granted.add(item);
|
|
402
|
+
consentMs = await consentStamp(consentPath(started.store));
|
|
294
403
|
if (started.isInstalled) {
|
|
295
404
|
shouldRecord = started.shouldRecord;
|
|
296
405
|
if (shouldRecord)
|
|
@@ -306,6 +415,10 @@ export function createLifecycle(definition, checksPermissions = () => true) {
|
|
|
306
415
|
}
|
|
307
416
|
},
|
|
308
417
|
async route(event, e, next) {
|
|
418
|
+
if (event === 'ui.render' && installer.draws(e))
|
|
419
|
+
return installer.draw(e);
|
|
420
|
+
if (event === 'ui.close')
|
|
421
|
+
installer.closed(e.id);
|
|
309
422
|
if (event === 'classic.SessionStart')
|
|
310
423
|
missedSessionStart = undefined;
|
|
311
424
|
if (event === 'classic.SessionStart' && runtime === undefined) {
|
|
@@ -321,6 +434,17 @@ export function createLifecycle(definition, checksPermissions = () => true) {
|
|
|
321
434
|
const isStopped = phase === 'declined' || phase === 'failed';
|
|
322
435
|
return { deny: isStopped ? notInstalled(definition.name) : `${definition.name} is installing. Try again when it's ready.` };
|
|
323
436
|
}
|
|
437
|
+
const call = e;
|
|
438
|
+
if (event === 'cmod.call' && call.to === definition.name && mod !== undefined) {
|
|
439
|
+
if (call.method === pendingStepsMethod)
|
|
440
|
+
return { value: await pendingSteps(mod) };
|
|
441
|
+
if (call.method === finishStepsMethod)
|
|
442
|
+
return (await runSteps(mod), { value: null });
|
|
443
|
+
}
|
|
444
|
+
if (event === 'classic.UserPromptSubmit' || event === 'classic.SessionStart')
|
|
445
|
+
await refreshGrants().catch((error) => claude().ui.log(`${definition.name} keeps its grants from before: ${messageOf(error)}`, { to: 'debug' }));
|
|
446
|
+
if (checksAnswers(event) && phase === 'active')
|
|
447
|
+
return checkedRoute(event, e, next);
|
|
324
448
|
const answer = router.dispatch(event, e, next);
|
|
325
449
|
const render = e;
|
|
326
450
|
if (event !== 'ui.render' || render.component !== 'AbovePrompt' || runtime === undefined || !runtime.progress.isShown)
|
|
@@ -331,22 +455,63 @@ export function createLifecycle(definition, checksPermissions = () => true) {
|
|
|
331
455
|
}
|
|
332
456
|
async function createMod(definition, runtime) {
|
|
333
457
|
const { router, progress } = runtime;
|
|
334
|
-
const claude = checkingKeys(runtime.claude, definition.name, runtime.keys);
|
|
458
|
+
const claude = checkingKeys(runtime.claude, definition.name, runtime.steps.keys ?? {});
|
|
335
459
|
const added = [];
|
|
336
460
|
const hookEvents = [];
|
|
337
461
|
const announce = (feature) => {
|
|
338
462
|
added.push(feature);
|
|
339
463
|
};
|
|
340
|
-
const [session, root, startCwd] = await Promise.all([claude.session.id(), claude.session.root(), claude.session.cwd()]);
|
|
464
|
+
const [session, root, startCwd, home, configHome] = await Promise.all([claude.session.id(), claude.session.root(), claude.session.cwd(), claude.env.home(), claude.env.configHome()]);
|
|
465
|
+
const { granted } = runtime;
|
|
466
|
+
const configRoot = configHome ?? (home === undefined ? undefined : `${home}/.claude`);
|
|
467
|
+
const grants = {
|
|
468
|
+
name: definition.name,
|
|
469
|
+
declared: runtime.steps.permissions ?? [],
|
|
470
|
+
granted: () => granted,
|
|
471
|
+
refresh: runtime.refreshGrants,
|
|
472
|
+
home,
|
|
473
|
+
configRoot,
|
|
474
|
+
projectRoot: () => modState.root,
|
|
475
|
+
freeFolders: () => [modState.root, runtime.dataFolder],
|
|
476
|
+
};
|
|
477
|
+
const checked = checkingGrants(claude, grants);
|
|
478
|
+
const files = createModFiles({
|
|
479
|
+
name: definition.name,
|
|
480
|
+
claude,
|
|
481
|
+
places: { home, configRoot, projectRoot: () => modState.root },
|
|
482
|
+
checkWrite: (path) => checkWrite(grants, `metadata.update(${path})`, path),
|
|
483
|
+
});
|
|
484
|
+
const pages = new Map();
|
|
485
|
+
const settingsPages = {
|
|
486
|
+
page(page) {
|
|
487
|
+
if (pages.has(page.id))
|
|
488
|
+
throw new Error(`${definition.name}: the settings page "${page.id}" is already added. Give each page its own id.`);
|
|
489
|
+
pages.set(page.id, { title: page.title, handle: area.ui.pane(page) });
|
|
490
|
+
},
|
|
491
|
+
async open(pageId) {
|
|
492
|
+
if (pageId === undefined) {
|
|
493
|
+
await claude.cmod.call({ to: cmodPluginName, method: 'openSettings', input: { mod: definition.name } });
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
const page = pages.get(pageId);
|
|
497
|
+
if (page === undefined)
|
|
498
|
+
throw new Error(`${definition.name} has no settings page "${pageId}". It has: ${[...pages.keys()].join(', ') || 'none'}.`);
|
|
499
|
+
await page.handle.open({ focus: true });
|
|
500
|
+
},
|
|
501
|
+
};
|
|
341
502
|
let cwd = startCwd;
|
|
342
503
|
let loadedCwd = startCwd;
|
|
343
504
|
let staleCwd;
|
|
344
505
|
const area = createUi({ name: definition.name, claude, router, progress, announce, mod: () => mod });
|
|
345
506
|
const modState = createState({ name: definition.name, initial: definition.state ?? {}, session, root, claude, changed: area.changed });
|
|
507
|
+
const modOptions = createOptions({ name: definition.name, declared: definition.options ?? {}, fromClaude: runtime.options, claude, changed: modState.changed });
|
|
346
508
|
const held = runtime.checksPermissions() ? new Map() : undefined;
|
|
347
509
|
const mod = {
|
|
348
510
|
name: definition.name,
|
|
349
511
|
state: modState.state,
|
|
512
|
+
get options() {
|
|
513
|
+
return modOptions.values;
|
|
514
|
+
},
|
|
350
515
|
dataFolder: runtime.dataFolder,
|
|
351
516
|
on(event, hook, options) {
|
|
352
517
|
const bounded = options?.timeoutMs === undefined ? hook : timedHook(definition.name, event, hook, checkedMs(definition.name, `the ${event} hook's timeoutMs`, options.timeoutMs), claude);
|
|
@@ -357,7 +522,7 @@ async function createMod(definition, runtime) {
|
|
|
357
522
|
else
|
|
358
523
|
router.add(`classic.${event}`, classicHook(definition.name, event, bounded, claude, agents));
|
|
359
524
|
},
|
|
360
|
-
use: (job) => job({ mod,
|
|
525
|
+
use: (job) => job({ mod, announce, reserveName, toolCalls: agents }),
|
|
361
526
|
every(ms, hook) {
|
|
362
527
|
let isRunning = false;
|
|
363
528
|
return claude.clock.every(checkedMs(definition.name, 'mod.every', ms), () => {
|
|
@@ -374,33 +539,42 @@ async function createMod(definition, runtime) {
|
|
|
374
539
|
},
|
|
375
540
|
ui: area.ui,
|
|
376
541
|
process: {
|
|
377
|
-
run: (argv, init) =>
|
|
378
|
-
spawn: (argv, init) =>
|
|
542
|
+
run: (argv, init) => checked.process.run(argv, init),
|
|
543
|
+
spawn: (argv, init) => checked.process.spawn({ ...init, argv }),
|
|
379
544
|
},
|
|
380
545
|
fs: {
|
|
381
|
-
read: (path) =>
|
|
382
|
-
write: (path, text) =>
|
|
383
|
-
list: (path) =>
|
|
384
|
-
exists: (path) =>
|
|
385
|
-
stat: (path, options) =>
|
|
546
|
+
read: (path) => checked.fs.read(path),
|
|
547
|
+
write: (path, text) => checked.fs.write(path, text),
|
|
548
|
+
list: (path) => checked.fs.list(path),
|
|
549
|
+
exists: (path) => checked.fs.exists(path),
|
|
550
|
+
stat: (path, options) => checked.fs.stat(path, options),
|
|
551
|
+
find: (glob) => files.find(glob),
|
|
552
|
+
},
|
|
553
|
+
metadata: {
|
|
554
|
+
read: (path) => files.read(path),
|
|
555
|
+
update: (path, change) => files.update(path, change),
|
|
386
556
|
},
|
|
387
|
-
http: { fetch: (url, init) =>
|
|
388
|
-
settings:
|
|
557
|
+
http: { fetch: (url, init) => checked.http.fetch(url, init) },
|
|
558
|
+
settings: settingsPages,
|
|
389
559
|
session: {
|
|
390
|
-
messages:
|
|
560
|
+
messages: ((args) => (args === undefined ? checked.session.messages() : checked.session.messages({ agentId: args.agentId }))),
|
|
391
561
|
async append(text) {
|
|
392
|
-
const added = await
|
|
562
|
+
const added = await checked.session.append({ message: { type: 'user', content: [{ type: 'text', text }] } });
|
|
393
563
|
if (added.deny !== undefined)
|
|
394
564
|
throw new Error(`${definition.name}: Claude Code refused the note: ${added.deny}`);
|
|
395
565
|
},
|
|
396
566
|
async submit(text) {
|
|
397
|
-
const submitted = await
|
|
567
|
+
const submitted = await checked.prompt.submit({ text });
|
|
398
568
|
if ('drop' in submitted && typeof submitted.drop === 'string')
|
|
399
569
|
throw new Error(`${definition.name}: Claude Code dropped the prompt: ${submitted.drop}`);
|
|
400
570
|
},
|
|
401
571
|
},
|
|
402
|
-
agent: { spawn: (args) =>
|
|
403
|
-
model: { complete: (
|
|
572
|
+
agent: { spawn: (args) => checked.agent.spawn(args) },
|
|
573
|
+
model: { complete: (completion, options) => checked.model.complete(completion, options) },
|
|
574
|
+
permissions: {
|
|
575
|
+
has: (name, value) => isCovered(granted, itemOf(name, value), home),
|
|
576
|
+
},
|
|
577
|
+
claude: { ...checked, on: (event, hook) => router.add(event, hook) },
|
|
404
578
|
get projectRoot() {
|
|
405
579
|
return modState.root;
|
|
406
580
|
},
|
|
@@ -427,6 +601,8 @@ async function createMod(definition, runtime) {
|
|
|
427
601
|
cwd = loadedCwd;
|
|
428
602
|
throw error;
|
|
429
603
|
});
|
|
604
|
+
if (hasMovedRoot)
|
|
605
|
+
await modOptions.load(nextRoot);
|
|
430
606
|
loadedCwd = nextCwd;
|
|
431
607
|
if (!hasMovedRoot)
|
|
432
608
|
area.changed();
|
|
@@ -465,7 +641,17 @@ async function createMod(definition, runtime) {
|
|
|
465
641
|
});
|
|
466
642
|
router.add('skill.prompt', userSkillHook(claude));
|
|
467
643
|
const api = Object.fromEntries(Object.entries(definition.api ?? {}).map(([method, run]) => [method, (input) => run(input, mod)]));
|
|
468
|
-
router.add('cmod.call', (e, next) =>
|
|
644
|
+
router.add('cmod.call', async (e, next) => {
|
|
645
|
+
if (e.to !== definition.name)
|
|
646
|
+
return next(e);
|
|
647
|
+
if (e.method === settingsPagesMethod)
|
|
648
|
+
return { value: [...pages].map(([id, { title }]) => ({ id, title })) };
|
|
649
|
+
if (e.method === openPageMethod) {
|
|
650
|
+
await settingsPages.open(String(e.input));
|
|
651
|
+
return { value: null };
|
|
652
|
+
}
|
|
653
|
+
return answerCall(definition.name, api, e);
|
|
654
|
+
});
|
|
469
655
|
const agents = toolCalls(claude, router);
|
|
470
656
|
const names = new Set();
|
|
471
657
|
const reserveName = (kind, name, taken) => {
|
|
@@ -475,6 +661,10 @@ async function createMod(definition, runtime) {
|
|
|
475
661
|
names.add(key);
|
|
476
662
|
};
|
|
477
663
|
await failsAs('its state did not load', () => modState.load());
|
|
664
|
+
await failsAs('its options did not load', () => modOptions.load(root));
|
|
665
|
+
const missing = modOptions.missing;
|
|
666
|
+
if (missing.length > 0)
|
|
667
|
+
throw new MissingOptions(missing, missing.map((key) => definition.options?.[key]?.title ?? key));
|
|
478
668
|
await failsAs('its setup function threw', () => definition.setup(mod));
|
|
479
669
|
const permissionHooks = permissionEvents.filter((event) => router.has(event));
|
|
480
670
|
if (permissionHooks.length > 0 && !runtime.checksPermissions()) {
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { FileMatch, Metadata } from '../mod.js';
|
|
2
|
+
import type { Claude } from './claude.js';
|
|
3
|
+
export type Places = {
|
|
4
|
+
readonly home: string | undefined;
|
|
5
|
+
readonly configRoot: string | undefined;
|
|
6
|
+
readonly projectRoot: () => string;
|
|
7
|
+
};
|
|
8
|
+
export type ModFiles = {
|
|
9
|
+
find(glob: string | readonly string[]): Promise<readonly FileMatch[]>;
|
|
10
|
+
read(path: string): Promise<Metadata>;
|
|
11
|
+
update(path: string, change: (metadata: Record<string, string>) => void): Promise<void>;
|
|
12
|
+
};
|
|
13
|
+
export declare function resolvedPath(path: string, places: Places): string;
|
|
14
|
+
export declare function createModFiles({ name, claude, places, checkWrite }: {
|
|
15
|
+
readonly name: string;
|
|
16
|
+
readonly claude: Claude;
|
|
17
|
+
readonly places: Places;
|
|
18
|
+
readonly checkWrite: (path: string) => Promise<void>;
|
|
19
|
+
}): ModFiles;
|
|
20
|
+
//# sourceMappingURL=metadata.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"metadata.d.ts","sourceRoot":"","sources":["../src/runtime/metadata.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAA;AAIpD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AAEzC,MAAM,MAAM,MAAM,GAAG;IACnB,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,CAAA;IACjC,QAAQ,CAAC,UAAU,EAAE,MAAM,GAAG,SAAS,CAAA;IACvC,QAAQ,CAAC,WAAW,EAAE,MAAM,MAAM,CAAA;CACnC,CAAA;AAED,MAAM,MAAM,QAAQ,GAAG;IACrB,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,MAAM,EAAE,GAAG,OAAO,CAAC,SAAS,SAAS,EAAE,CAAC,CAAA;IACrE,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAA;IACrC,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;CACxF,CAAA;AAUD,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAQjE;AAED,wBAAgB,cAAc,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,EAAE;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,UAAU,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;CAAE,GAAG,QAAQ,CAoHhN"}
|