@cmodjs/core 1.0.0 → 1.5.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 +22 -11
- package/docs/dependencies.md +6 -4
- package/docs/hooks.md +26 -3
- package/docs/index.md +1 -1
- package/docs/install-steps.md +5 -3
- package/docs/mod.md +29 -3
- package/docs/permissions.md +4 -4
- package/docs/session.md +126 -63
- package/docs/testing.md +7 -5
- package/docs/ui.md +9 -1
- package/jobs/program.d.ts.map +1 -1
- package/jobs/program.js +7 -2
- package/mod.d.ts +30 -6
- package/mod.d.ts.map +1 -1
- package/mod.js +34 -1
- package/package.json +1 -1
- package/records.d.ts +4 -1
- package/records.d.ts.map +1 -1
- package/records.js +6 -1
- package/register.d.ts.map +1 -1
- package/register.js +17 -0
- package/runtime/announce.d.ts.map +1 -1
- package/runtime/announce.js +2 -0
- package/runtime/before.d.ts +32 -0
- package/runtime/before.d.ts.map +1 -0
- package/runtime/before.js +214 -0
- package/runtime/claude.d.ts +2 -1
- package/runtime/claude.d.ts.map +1 -1
- package/runtime/conversations.d.ts +1 -0
- package/runtime/conversations.d.ts.map +1 -1
- package/runtime/conversations.js +333 -61
- package/runtime/dependencies.js +1 -1
- package/runtime/engine.d.ts +2 -6
- package/runtime/engine.d.ts.map +1 -1
- package/runtime/grants.d.ts.map +1 -1
- package/runtime/grants.js +8 -8
- package/runtime/hooks.d.ts +1 -1
- package/runtime/hooks.d.ts.map +1 -1
- package/runtime/hooks.js +19 -4
- package/runtime/installer.d.ts +1 -1
- package/runtime/installer.d.ts.map +1 -1
- package/runtime/installer.js +6 -5
- package/runtime/lifecycle.d.ts.map +1 -1
- package/runtime/lifecycle.js +130 -59
- package/runtime/metadata.d.ts.map +1 -1
- package/runtime/metadata.js +5 -4
- package/runtime/sessions.d.ts +26 -4
- package/runtime/sessions.d.ts.map +1 -1
- package/runtime/sessions.js +315 -117
- package/runtime/toaster.d.ts +5 -12
- package/runtime/toaster.d.ts.map +1 -1
- package/runtime/toaster.js +158 -25
- package/runtime/ui.d.ts.map +1 -1
- package/runtime/ui.js +26 -6
- package/session/context/context.d.ts +3 -1
- package/session/context/context.d.ts.map +1 -1
- package/session/context/turn/message/message.d.ts +1 -0
- package/session/context/turn/message/message.d.ts.map +1 -1
- package/session/context/turn/message/message.js +42 -1
- package/session/context/turn/turn.d.ts +2 -1
- package/session/context/turn/turn.d.ts.map +1 -1
- package/session/conversation.d.ts +20 -11
- package/session/conversation.d.ts.map +1 -1
- package/session/conversation.js +50 -27
- package/session/goal/goal.d.ts +3 -2
- package/session/goal/goal.d.ts.map +1 -1
- package/session/goal/task.d.ts +4 -4
- package/session/goal/task.d.ts.map +1 -1
- package/session/session.d.ts +80 -21
- package/session/session.d.ts.map +1 -1
- package/session/session.js +1 -0
- package/src/jobs/program.ts +7 -2
- package/src/mod.ts +48 -5
- package/src/records.ts +6 -1
- package/src/register.ts +17 -0
- package/src/runtime/announce.ts +2 -0
- package/src/runtime/before.ts +200 -0
- package/src/runtime/claude.ts +3 -0
- package/src/runtime/conversations.ts +337 -60
- package/src/runtime/dependencies.ts +1 -1
- package/src/runtime/engine.ts +2 -1
- package/src/runtime/grants.ts +8 -10
- package/src/runtime/hooks.ts +20 -4
- package/src/runtime/installer.ts +6 -5
- package/src/runtime/lifecycle.ts +136 -56
- package/src/runtime/metadata.ts +5 -4
- package/src/runtime/sessions.ts +316 -111
- package/src/runtime/toaster.ts +183 -45
- package/src/runtime/ui.ts +23 -4
- package/src/session/context/context.ts +3 -1
- package/src/session/context/turn/message/message.ts +39 -1
- package/src/session/context/turn/turn.ts +2 -1
- package/src/session/conversation.ts +56 -31
- package/src/session/goal/goal.ts +3 -2
- package/src/session/goal/task.ts +4 -4
- package/src/session/session.ts +62 -15
- package/src/testing/fake-claude.ts +4 -2
- package/src/testing/fake-sessions.ts +94 -20
- package/src/testing.ts +27 -3
- package/src/utils/call-effects.ts +2 -2
- package/src/utils/disposable.ts +3 -0
- package/src/utils/matcher.ts +1 -1
- package/src/utils/paths.ts +6 -3
- package/testing/fake-claude.d.ts +1 -0
- package/testing/fake-claude.d.ts.map +1 -1
- package/testing/fake-claude.js +3 -1
- package/testing/fake-sessions.d.ts +8 -1
- package/testing/fake-sessions.d.ts.map +1 -1
- package/testing/fake-sessions.js +97 -18
- package/testing.d.ts +1 -0
- package/testing.d.ts.map +1 -1
- package/testing.js +23 -2
- package/utils/call-effects.d.ts +1 -0
- package/utils/call-effects.d.ts.map +1 -1
- package/utils/call-effects.js +1 -1
- package/utils/disposable.d.ts +4 -0
- package/utils/disposable.d.ts.map +1 -0
- package/utils/disposable.js +1 -0
- package/utils/matcher.js +1 -1
- package/utils/paths.d.ts +2 -1
- package/utils/paths.d.ts.map +1 -1
- package/utils/paths.js +6 -3
package/runtime/grants.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { permissionWords } from '../records.js';
|
|
1
|
+
import { isObject, permissionWords } from '../records.js';
|
|
2
2
|
import { relativePath } from '../utils/paths.js';
|
|
3
3
|
import { cmodPluginName } from './dependencies.js';
|
|
4
|
-
import { conversationMethods } from './sessions.js';
|
|
4
|
+
import { actPermission, conversationMethods, isAct, toolPermission } from './sessions.js';
|
|
5
5
|
export class PermissionRefused extends Error {
|
|
6
6
|
}
|
|
7
7
|
const addressesOfPrograms = new WeakMap();
|
|
@@ -11,8 +11,6 @@ export function programAddresses(mod) {
|
|
|
11
11
|
return known;
|
|
12
12
|
}
|
|
13
13
|
const projectSettingsFiles = ['.claude/settings.json', '.claude/settings.local.json', '.mcp.json'];
|
|
14
|
-
const startsTurns = ['CronCreate'];
|
|
15
|
-
const ownSchedule = ['CronDelete', 'CronList'];
|
|
16
14
|
const conversationCalls = {
|
|
17
15
|
[conversationMethods.sessions]: 'sessions.list',
|
|
18
16
|
[conversationMethods.search]: 'sessions.search',
|
|
@@ -23,7 +21,8 @@ const conversationCalls = {
|
|
|
23
21
|
[conversationMethods.goal]: 'goal',
|
|
24
22
|
[conversationMethods.updateGoal]: 'goal.update',
|
|
25
23
|
[conversationMethods.subscribe]: 'on',
|
|
26
|
-
[conversationMethods.
|
|
24
|
+
[conversationMethods.watchBefore]: 'before',
|
|
25
|
+
[conversationMethods.act]: 'act',
|
|
27
26
|
};
|
|
28
27
|
export async function checkGrant(grants, call, item) {
|
|
29
28
|
if (item === undefined)
|
|
@@ -95,8 +94,10 @@ export function checkingGrants(claude, grants) {
|
|
|
95
94
|
cmod: {
|
|
96
95
|
async call(args) {
|
|
97
96
|
const named = args.to === cmodPluginName ? conversationCalls[args.method] : undefined;
|
|
97
|
+
const given = args.method === conversationMethods.act && isObject(args.input) ? String(args.input['action']) : undefined;
|
|
98
|
+
const action = given !== undefined && isAct(given) ? given : undefined;
|
|
98
99
|
if (named !== undefined)
|
|
99
|
-
await gate(named,
|
|
100
|
+
await gate(action ?? named, action === undefined ? 'conversation' : actPermission(action, isObject(args.input) ? args.input['input'] : undefined));
|
|
100
101
|
return claude.cmod.call(args);
|
|
101
102
|
},
|
|
102
103
|
},
|
|
@@ -116,8 +117,7 @@ export function checkingGrants(claude, grants) {
|
|
|
116
117
|
tool: {
|
|
117
118
|
...claude.tool,
|
|
118
119
|
call: async (input) => {
|
|
119
|
-
|
|
120
|
-
await gate(`tool.call(${input.tool})`, item);
|
|
120
|
+
await gate(`tool.call(${input.tool})`, toolPermission(input.tool));
|
|
121
121
|
return claude.tool.call(input);
|
|
122
122
|
},
|
|
123
123
|
},
|
package/runtime/hooks.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ import type { Claude } from './claude.js';
|
|
|
3
3
|
import type { RoutedHook } from './router.js';
|
|
4
4
|
import type { Failure } from './toaster.js';
|
|
5
5
|
import { type ToolCalls } from './tool-calls.js';
|
|
6
|
-
export type RoutedEvent = `classic.${Exclude<Hook, 'PreToolUse'>}` | 'tool.check' | 'tool.call' | 'prompt.submit' | 'prompt.context' | 'command.run' | 'session.measure' | 'skill.prompt' | 'ui.render' | 'ui.press' | 'ui.scroll' | 'ui.close' | 'cmod.call' | 'session.append' | 'session.compact' | 'session.receive' | 'agent.spawn';
|
|
6
|
+
export type RoutedEvent = `classic.${Exclude<Hook, 'PreToolUse'>}` | 'tool.check' | 'tool.call' | 'prompt.submit' | 'prompt.context' | 'prompt.attachment' | 'command.run' | 'session.measure' | 'skill.prompt' | 'ui.render' | 'ui.press' | 'ui.scroll' | 'ui.close' | 'cmod.call' | 'session.append' | 'session.compact' | 'session.receive' | 'agent.spawn';
|
|
7
7
|
export declare const permissionEvents: readonly RoutedEvent[];
|
|
8
8
|
export declare function classicHook<E extends Exclude<Hook, 'PreToolUse'>>(name: string, event: E, hook: HookListener<E>, claude: Claude, calls: ToolCalls, report: (failure: Failure) => void): RoutedHook<RoutedEvent>;
|
|
9
9
|
type HeldDecision = {
|
package/runtime/hooks.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"hooks.d.ts","sourceRoot":"","sources":["../src/runtime/hooks.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,IAAI,EAAyB,YAAY,EAAE,MAAM,WAAW,CAAA;AAE1E,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AACzC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AAC7C,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAA;AAC3C,OAAO,EAA6B,KAAK,SAAS,EAAE,MAAM,iBAAiB,CAAA;AAM3E,MAAM,MAAM,WAAW,GACnB,WAAW,OAAO,CAAC,IAAI,EAAE,YAAY,CAAC,EAAE,GACxC,YAAY,GACZ,WAAW,GACX,eAAe,GACf,gBAAgB,GAChB,aAAa,GACb,iBAAiB,GACjB,cAAc,GACd,WAAW,GACX,UAAU,GACV,WAAW,GACX,UAAU,GACV,WAAW,GACX,gBAAgB,GAChB,iBAAiB,GACjB,iBAAiB,GACjB,aAAa,CAAA;AAEjB,eAAO,MAAM,gBAAgB,EAAE,SAAS,WAAW,EAAgD,CAAA;
|
|
1
|
+
{"version":3,"file":"hooks.d.ts","sourceRoot":"","sources":["../src/runtime/hooks.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,IAAI,EAAyB,YAAY,EAAE,MAAM,WAAW,CAAA;AAE1E,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AACzC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AAC7C,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAA;AAC3C,OAAO,EAA6B,KAAK,SAAS,EAAE,MAAM,iBAAiB,CAAA;AAM3E,MAAM,MAAM,WAAW,GACnB,WAAW,OAAO,CAAC,IAAI,EAAE,YAAY,CAAC,EAAE,GACxC,YAAY,GACZ,WAAW,GACX,eAAe,GACf,gBAAgB,GAChB,mBAAmB,GACnB,aAAa,GACb,iBAAiB,GACjB,cAAc,GACd,WAAW,GACX,UAAU,GACV,WAAW,GACX,UAAU,GACV,WAAW,GACX,gBAAgB,GAChB,iBAAiB,GACjB,iBAAiB,GACjB,aAAa,CAAA;AAEjB,eAAO,MAAM,gBAAgB,EAAE,SAAS,WAAW,EAAgD,CAAA;AA4CnG,wBAAgB,WAAW,CAAC,CAAC,SAAS,OAAO,CAAC,IAAI,EAAE,YAAY,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,EAAE,IAAI,EAAE,YAAY,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,EAAE,MAAM,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,IAAI,GAAG,UAAU,CAAC,WAAW,CAAC,CAuB/M;AAED,KAAK,YAAY,GAAG;IAAE,QAAQ,CAAC,QAAQ,EAAE,OAAO,GAAG,KAAK,CAAC;IAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,CAAA;AAEpF,MAAM,MAAM,aAAa,GAAG,GAAG,CAAC,MAAM,EAAE,YAAY,CAAC,CAAA;AAErD,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,YAAY,CAAC,YAAY,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,EAAE,IAAI,EAAE,aAAa,GAAG,SAAS,GAAG,UAAU,CAAC,WAAW,CAAC,CA6BzK;AAED,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,aAAa,GAAG,UAAU,CAAC,YAAY,CAAC,CAM9E;AAYD,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,GAAG,UAAU,CAAC,cAAc,CAAC,CAexE;AAgED,eAAO,MAAM,UAAU,YAAI,OAAO,EAAE,KAAK,EAAE,MAAM,CAAU,CAAA"}
|
package/runtime/hooks.js
CHANGED
|
@@ -7,24 +7,39 @@ import { messageOf } from '../utils/text.js';
|
|
|
7
7
|
export const permissionEvents = ['tool.check', 'classic.PermissionRequest'];
|
|
8
8
|
const flags = ['suppressOriginalPrompt', 'reloadSkills', 'retry'];
|
|
9
9
|
const readFields = {
|
|
10
|
+
Setup: ['additionalContext'],
|
|
10
11
|
SessionStart: ['additionalContext', 'initialUserMessage', 'sessionTitle', 'watchPaths', 'reloadSkills'],
|
|
11
12
|
SessionEnd: [],
|
|
12
13
|
UserPromptSubmit: ['additionalContext', 'sessionTitle', 'suppressOriginalPrompt'],
|
|
14
|
+
UserPromptExpansion: ['additionalContext', 'suppressOriginalPrompt'],
|
|
13
15
|
InstructionsLoaded: [],
|
|
16
|
+
ConfigChange: [],
|
|
17
|
+
DirectoryAdded: [],
|
|
18
|
+
PreModelSwitch: ['permissionDecision', 'permissionDecisionReason'],
|
|
19
|
+
PostModelSwitch: ['additionalContext'],
|
|
14
20
|
PreToolUse: ['additionalContext', 'permissionDecision', 'permissionDecisionReason', 'updatedInput'],
|
|
15
21
|
PermissionRequest: ['decision'],
|
|
16
22
|
PermissionDenied: ['retry'],
|
|
17
23
|
PostToolUse: ['additionalContext', 'updatedToolOutput', 'updatedMCPToolOutput'],
|
|
18
24
|
PostToolUseFailure: ['additionalContext'],
|
|
19
25
|
PostToolBatch: ['additionalContext'],
|
|
26
|
+
MessageDisplay: ['displayContent'],
|
|
20
27
|
SubagentStart: ['additionalContext'],
|
|
21
28
|
SubagentStop: ['additionalContext'],
|
|
29
|
+
TaskCreated: [],
|
|
30
|
+
TaskCompleted: [],
|
|
31
|
+
TeammateIdle: [],
|
|
32
|
+
Elicitation: [],
|
|
33
|
+
ElicitationResult: [],
|
|
22
34
|
Notification: [],
|
|
23
35
|
PreCompact: [],
|
|
36
|
+
PostCompact: [],
|
|
24
37
|
Stop: ['additionalContext'],
|
|
25
38
|
StopFailure: [],
|
|
26
39
|
CwdChanged: [],
|
|
27
40
|
FileChanged: [],
|
|
41
|
+
WorktreeCreate: ['worktreePath'],
|
|
42
|
+
WorktreeRemove: [],
|
|
28
43
|
};
|
|
29
44
|
export function classicHook(name, event, hook, claude, calls, report) {
|
|
30
45
|
const hookInputOf = async (e) => {
|
|
@@ -146,10 +161,10 @@ async function preToolUseInput(name, envelope, claude, agentOf) {
|
|
|
146
161
|
}
|
|
147
162
|
async function callFiles(name, claude, use, cwd) {
|
|
148
163
|
try {
|
|
149
|
-
const home = await claude.env.home();
|
|
164
|
+
const [home, configRoot] = await Promise.all([claude.env.home(), claude.env.configHome()]);
|
|
150
165
|
if (home === undefined)
|
|
151
166
|
throw new Error('HOME is not set, so ~ in a path has no meaning.');
|
|
152
|
-
const { shell, reads, writes } = callEffects(use, { cwd, home, fs: claude.fs });
|
|
167
|
+
const { shell, reads, writes } = callEffects(use, { cwd, home, ...(configRoot === undefined ? {} : { configRoot }), fs: claude.fs });
|
|
153
168
|
const known = (accesses) => accesses.map(({ path }) => path).filter((path) => shell === undefined || !dynamicPattern.test(path));
|
|
154
169
|
const unchecked = (path) => (error) => {
|
|
155
170
|
if (!isMissing(error))
|
|
@@ -192,11 +207,11 @@ function classicResult(event, answer) {
|
|
|
192
207
|
throw unread(`hookSpecificOutput.${field}`);
|
|
193
208
|
if (field === 'additionalContext')
|
|
194
209
|
result['additionalContext'] = [value];
|
|
195
|
-
else if (field === 'permissionDecision') {
|
|
210
|
+
else if (field === 'permissionDecision' && event === 'PreToolUse') {
|
|
196
211
|
const reason = specific.permissionDecisionReason ?? '';
|
|
197
212
|
result[value] = value === 'allow' ? true : reason;
|
|
198
213
|
}
|
|
199
|
-
else if (field !== 'permissionDecisionReason')
|
|
214
|
+
else if (field !== 'permissionDecisionReason' || event !== 'PreToolUse')
|
|
200
215
|
result[field] = value;
|
|
201
216
|
}
|
|
202
217
|
return result;
|
package/runtime/installer.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ import type { Step } from '../installer.js';
|
|
|
3
3
|
import type { Mod } from '../mod.js';
|
|
4
4
|
import type { Option } from '../options.js';
|
|
5
5
|
import type { Claude } from './claude.js';
|
|
6
|
-
export declare const
|
|
6
|
+
export declare const installerPaneOf: (name: string) => string;
|
|
7
7
|
export type Consent = {
|
|
8
8
|
readonly install: string;
|
|
9
9
|
readonly uninstall: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"installer.d.ts","sourceRoot":"","sources":["../src/runtime/installer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,aAAa,EAAiB,MAAM,aAAa,CAAA;AAC7E,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,iBAAiB,CAAA;AAC3C,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,WAAW,CAAA;AACpC,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,eAAe,CAAA;AAI3C,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AAEzC,eAAO,MAAM,eAAe,
|
|
1
|
+
{"version":3,"file":"installer.d.ts","sourceRoot":"","sources":["../src/runtime/installer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,aAAa,EAAiB,MAAM,aAAa,CAAA;AAC7E,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,iBAAiB,CAAA;AAC3C,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,WAAW,CAAA;AACpC,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,eAAe,CAAA;AAI3C,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AAEzC,eAAO,MAAM,eAAe,SAAU,MAAM,KAAG,MAA0E,CAAA;AAEzH,MAAM,MAAM,OAAO,GAAG;IACpB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;IAC1B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,WAAW,EAAE,SAAS,MAAM,EAAE,CAAA;IACvC,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;CACtC,CAAA;AAOD,MAAM,MAAM,SAAS,GAAG;IACtB,KAAK,CAAC,CAAC,EAAE,MAAM,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC,GAAG,OAAO,CAAA;IAC5C,IAAI,CAAC,CAAC,EAAE,MAAM,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC,GAAG,aAAa,CAAA;IACjD,MAAM,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,OAAO,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAA;IAC3C,OAAO,CAAC,OAAO,EAAE,SAAS,CAAC,SAAS,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,EAAE,EAAE,IAAI,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,GAAG,OAAO,CAAC,OAAO,CAAC,CAAA;IAC7I,IAAI,CAAC,KAAK,SAAS,MAAM,EAAE,GAAG,EAAE,GAAG,CAAC,KAAK,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,KAAK,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAA;IAC9G,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;CACvB,CAAA;AAED,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,MAAM,GAAG,SAAS,CAwI7E"}
|
package/runtime/installer.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { permissionWords } from '../records.js';
|
|
2
2
|
import { Box, Button, drawWith, Input, Text } from '../ui/elements.js';
|
|
3
3
|
import { messageOf } from '../utils/text.js';
|
|
4
|
-
export const
|
|
4
|
+
export const installerPaneOf = (name) => `installer-${name.replace(/[^A-Za-z0-9_-]/g, '_')}`.slice(0, 64);
|
|
5
5
|
export function createInstaller(name, claude) {
|
|
6
|
+
const paneId = installerPaneOf(name);
|
|
6
7
|
let page;
|
|
7
8
|
let isOpen = false;
|
|
8
9
|
const show = async (build, cancelled) => {
|
|
@@ -18,7 +19,7 @@ export function createInstaller(name, claude) {
|
|
|
18
19
|
});
|
|
19
20
|
if (!isOpen) {
|
|
20
21
|
isOpen = true;
|
|
21
|
-
await claude().ui.open({ id:
|
|
22
|
+
await claude().ui.open({ id: paneId, title: `Install ${name}`, focus: true, holdToasts: true, closeOnEscape: true });
|
|
22
23
|
}
|
|
23
24
|
claude().ui.invalidate('ui.render');
|
|
24
25
|
return answer;
|
|
@@ -26,12 +27,12 @@ export function createInstaller(name, claude) {
|
|
|
26
27
|
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
28
|
const bullet = (text) => Text({ children: [Text({ color: 'claude', children: '◆ ' }), text] });
|
|
28
29
|
return {
|
|
29
|
-
draws: (e) => e.component === 'Pane' && e.requestId ===
|
|
30
|
+
draws: (e) => e.component === 'Pane' && e.requestId === paneId,
|
|
30
31
|
draw(e) {
|
|
31
32
|
return drawWith(claude().ui.resolve(e), () => page?.draw(e.props) ?? Text({ dimColor: true, children: `${name} is installing.` }));
|
|
32
33
|
},
|
|
33
34
|
closed(id) {
|
|
34
|
-
if (id !==
|
|
35
|
+
if (id !== paneId)
|
|
35
36
|
return;
|
|
36
37
|
isOpen = false;
|
|
37
38
|
page?.cancel();
|
|
@@ -114,7 +115,7 @@ export function createInstaller(name, claude) {
|
|
|
114
115
|
if (!isOpen)
|
|
115
116
|
return;
|
|
116
117
|
isOpen = false;
|
|
117
|
-
await claude().ui.close({ id:
|
|
118
|
+
await claude().ui.close({ id: paneId });
|
|
118
119
|
},
|
|
119
120
|
};
|
|
120
121
|
}
|
|
@@ -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,EAAc,aAAa,
|
|
1
|
+
{"version":3,"file":"lifecycle.d.ts","sourceRoot":"","sources":["../src/runtime/lifecycle.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,WAAW,EAAE,MAAM,EAAc,aAAa,EAAwB,MAAM,aAAa,CAAA;AAE7G,OAAO,EAAkI,KAAK,GAAG,EAAE,KAAK,aAAa,EAAkD,MAAM,WAAW,CAAA;AACxO,OAAO,KAAK,EAAE,OAAO,EAAgB,MAAM,eAAe,CAAA;AAC1D,OAAO,EAA2T,KAAK,KAAK,EAAE,MAAM,eAAe,CAAA;AAGnW,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AAOzC,OAAO,EAAsG,KAAK,WAAW,EAAE,MAAM,YAAY,CAAA;AAOjJ,OAAO,EAA6B,KAAK,UAAU,EAAE,MAAM,aAAa,CAAA;AAMxE,OAAO,EAA+B,KAAK,UAAU,EAAgB,MAAM,cAAc,CAAA;AAMzF,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,QAAQ,CAAC,MAAM,EAAE,SAAS,UAAU,EAAE,CAAA;IACtC,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;AAwCD,wBAAsB,UAAU,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAwBhE;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,CAkclB"}
|
package/runtime/lifecycle.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { hooks, PreToolUse } from '../mod.js';
|
|
2
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';
|
|
3
|
+
import { homePrefix, ifMissing, relativePath } from '../utils/paths.js';
|
|
4
4
|
import { firstLine, formatExit, listed, messageOf } from '../utils/text.js';
|
|
5
5
|
import { beforeDeadline } from './deadline.js';
|
|
6
6
|
import { announcedOf, announcementMs, announcementOf } from './announce.js';
|
|
7
7
|
import { bind } from './bound.js';
|
|
8
8
|
import { serveConversations } from './conversations.js';
|
|
9
|
-
import { answerCall, cmodPluginName, dependencyCalls,
|
|
9
|
+
import { answerCall, cmodPluginName, dependencyCalls, withBareRefusals } from './dependencies.js';
|
|
10
10
|
import { keepEngine } from './engine.js';
|
|
11
11
|
import { classicHook, heldDecisionHook, permissionEvents, preToolUseHook, userSkillHook } from './hooks.js';
|
|
12
12
|
import { checkedAnswer, checkingGrants, checkSettings, checksAnswers, checkWrite, isCovered, itemOf, passedDown, programAddresses } from './grants.js';
|
|
@@ -19,13 +19,14 @@ import { createRouter } from './router.js';
|
|
|
19
19
|
import { conversationMethods, createSessionsClient } from './sessions.js';
|
|
20
20
|
import { matches } from '../utils/matcher.js';
|
|
21
21
|
import { createState } from './state.js';
|
|
22
|
-
import { createToaster
|
|
22
|
+
import { createToaster } from './toaster.js';
|
|
23
23
|
import { toolCalls } from './tool-calls.js';
|
|
24
24
|
import { createUi } from './ui.js';
|
|
25
25
|
import { Progress } from '../ui/components.js';
|
|
26
26
|
import { Box, Text } from '../ui/elements.js';
|
|
27
27
|
const announcedKey = 'cmod:announced';
|
|
28
28
|
const installId = 'cmod:install';
|
|
29
|
+
const installRetrySeconds = [2, 4, 8, 16, 32, 64, 128];
|
|
29
30
|
const cmodCheckMs = 1000;
|
|
30
31
|
const cmodWaitMs = 60_000;
|
|
31
32
|
const startHoldMs = 10_000 - 1_000;
|
|
@@ -77,7 +78,7 @@ async function readJson(read, path) {
|
|
|
77
78
|
return JSON.parse(text);
|
|
78
79
|
}
|
|
79
80
|
catch (error) {
|
|
80
|
-
throw new Error(`${path} is not JSON (${messageOf(error)}). Fix
|
|
81
|
+
throw new Error(`${path} is not JSON (${messageOf(error)}). Fix it, then press Retry.`);
|
|
81
82
|
}
|
|
82
83
|
}
|
|
83
84
|
async function cmodVersion(claude) {
|
|
@@ -116,15 +117,52 @@ export function createLifecycle(definition, checksPermissions = () => true, opti
|
|
|
116
117
|
return runtime.claude;
|
|
117
118
|
};
|
|
118
119
|
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);
|
|
122
|
-
};
|
|
120
|
+
const toastFailure = (shown) => runtime?.toaster.fail(shown);
|
|
123
121
|
const fail = (reason, fix, retry) => {
|
|
124
122
|
phase = 'failed';
|
|
125
|
-
failure ??= new Error(`${definition.name}: ${reason}${/[.!?]$/.test(reason) ? '' : '.'} ${fix}`);
|
|
126
|
-
const
|
|
127
|
-
|
|
123
|
+
failure ??= new Error(`${definition.name}: ${reason}${/[.!?]$/.test(reason) ? '' : '.'}${fix === undefined ? '' : ` ${fix}`}`);
|
|
124
|
+
const again = retry === undefined ? {} : { retry: () => ((failure = undefined), stopRetrying(), retry().catch(report)) };
|
|
125
|
+
toastFailure({ id: installId, message: reason, ...(fix === undefined ? {} : { description: fix }), ...again });
|
|
126
|
+
};
|
|
127
|
+
let consented;
|
|
128
|
+
const installAgain = () => install(consented);
|
|
129
|
+
let retrying;
|
|
130
|
+
const retryLater = (next) => {
|
|
131
|
+
const seconds = installRetrySeconds[Math.min(next.tries, installRetrySeconds.length - 1)];
|
|
132
|
+
retrying?.timer.cancel();
|
|
133
|
+
retrying = { ...next, tries: next.tries + 1, timer: claude().clock.after(seconds * 1000, () => void retryInBackground().catch(report)) };
|
|
134
|
+
fail(next.reason, `Trying again in ${seconds} s.`, installAgain);
|
|
135
|
+
};
|
|
136
|
+
const stopRetrying = () => {
|
|
137
|
+
retrying?.timer.cancel();
|
|
138
|
+
retrying = undefined;
|
|
139
|
+
};
|
|
140
|
+
const checkoutState = async () => {
|
|
141
|
+
if (plugin === undefined)
|
|
142
|
+
return undefined;
|
|
143
|
+
const lines = [];
|
|
144
|
+
const { code } = await readLines(claude().process.spawn({ argv: ['sh', '-c', 'git rev-parse HEAD && git status --porcelain=v1 -uall && git diff HEAD'], cwd: plugin.root }), (text) => void lines.push(text));
|
|
145
|
+
return code === 0 ? lines.join('\n') : undefined;
|
|
146
|
+
};
|
|
147
|
+
const failInstall = async (reason, when) => {
|
|
148
|
+
if (when === undefined) {
|
|
149
|
+
stopRetrying();
|
|
150
|
+
return fail(reason, undefined, installAgain);
|
|
151
|
+
}
|
|
152
|
+
retryLater({ when, tries: retrying?.when === when ? retrying.tries : 0, checkout: when === 'changed' ? await checkoutState() : undefined, reason });
|
|
153
|
+
};
|
|
154
|
+
const retryInBackground = async () => {
|
|
155
|
+
const waiting = retrying;
|
|
156
|
+
if (phase !== 'failed' || waiting === undefined)
|
|
157
|
+
return;
|
|
158
|
+
if (await recover()) {
|
|
159
|
+
stopRetrying();
|
|
160
|
+
return whenActive();
|
|
161
|
+
}
|
|
162
|
+
if (waiting.when === 'changed' && waiting.checkout !== undefined && (await checkoutState()) === waiting.checkout)
|
|
163
|
+
return retryLater(waiting);
|
|
164
|
+
await install(consented, true);
|
|
165
|
+
await whenActive();
|
|
128
166
|
};
|
|
129
167
|
const finish = async () => {
|
|
130
168
|
await programs?.refresh();
|
|
@@ -215,7 +253,7 @@ export function createLifecycle(definition, checksPermissions = () => true, opti
|
|
|
215
253
|
const active = await createMod(definition, { ...runtime, router: activeRouter, dataFolder: dataFolder(plugin.store, plugin.name), store: plugin.store, steps: plugin.steps, granted, refreshGrants, checksPermissions, options: pluginOptions }).catch(async (error) => {
|
|
216
254
|
if (error instanceof MissingOptions && (await askOptions(error)))
|
|
217
255
|
return undefined;
|
|
218
|
-
fail(messageOf(error), error instanceof MissingOptions ? `Set ${error.keys.length === 1 ? 'it' : 'them'} in /config.` :
|
|
256
|
+
fail(messageOf(error), error instanceof MissingOptions ? `Set ${error.keys.length === 1 ? 'it' : 'them'} in /config.` : undefined, begin);
|
|
219
257
|
throw error;
|
|
220
258
|
});
|
|
221
259
|
if (active === undefined)
|
|
@@ -263,18 +301,21 @@ export function createLifecycle(definition, checksPermissions = () => true, opti
|
|
|
263
301
|
claude().ui.log(`${name} waits for consent to run ${event.install}. Run cmod install ${name} in a terminal.`);
|
|
264
302
|
return;
|
|
265
303
|
}
|
|
266
|
-
|
|
304
|
+
runtime?.toaster.toast.info('Installing', { id: installId, description: 'Waiting for your answer', duration: Infinity });
|
|
267
305
|
const updates = await updatesToShow().catch((error) => {
|
|
268
306
|
toastFailure({ id: 'cmod:updates', message: 'Could not read whether its marketplace updates automatically', error });
|
|
269
307
|
return undefined;
|
|
270
308
|
});
|
|
271
|
-
|
|
272
|
-
return install(event.sha256);
|
|
309
|
+
const isAccepted = await installer.consent({ ...event, updates });
|
|
273
310
|
await installer.close();
|
|
311
|
+
if (isAccepted) {
|
|
312
|
+
consented = event.sha256;
|
|
313
|
+
return install(consented);
|
|
314
|
+
}
|
|
274
315
|
phase = 'declined';
|
|
275
|
-
runtime?.toaster.toast('Not installed', { id: installId, description:
|
|
316
|
+
runtime?.toaster.toast('Not installed', { id: installId, description: 'Install it whenever you like.', duration: Infinity, action: { label: 'Install', onClick: () => install().catch(report) } });
|
|
276
317
|
};
|
|
277
|
-
const install = async (consent) => {
|
|
318
|
+
const install = async (consent, isQuiet = false) => {
|
|
278
319
|
if (plugin === undefined)
|
|
279
320
|
return;
|
|
280
321
|
if (plugin.name === cmodPluginName)
|
|
@@ -283,25 +324,29 @@ export function createLifecycle(definition, checksPermissions = () => true, opti
|
|
|
283
324
|
const found = await cmodVersion(claude());
|
|
284
325
|
if (!fits(found))
|
|
285
326
|
return waitForcmod(found);
|
|
286
|
-
|
|
327
|
+
if (!isQuiet)
|
|
328
|
+
installing();
|
|
287
329
|
let outcome;
|
|
330
|
+
let retries;
|
|
288
331
|
const argv = ['cmod', 'setup', plugin.root, '--events', ...(consent === undefined ? [] : ['--consent', consent])];
|
|
289
332
|
const { code, lastError } = await readLines(claude().process.spawn({ argv }), (text) => {
|
|
290
333
|
const event = parseEvent(text);
|
|
291
|
-
if (event.type === 'progress')
|
|
334
|
+
if (event.type === 'progress' && !isQuiet)
|
|
292
335
|
installing(progressOf(event));
|
|
336
|
+
if (event.type === 'retries')
|
|
337
|
+
retries = event.when;
|
|
293
338
|
if (event.type === 'done' || event.type === 'failed' || event.type === 'needs-consent')
|
|
294
339
|
outcome = event;
|
|
295
340
|
});
|
|
296
341
|
if (outcome?.type === 'done') {
|
|
342
|
+
stopRetrying();
|
|
297
343
|
for (const item of plugin.steps.permissions ?? [])
|
|
298
344
|
granted.add(item);
|
|
299
345
|
return finish();
|
|
300
346
|
}
|
|
301
347
|
if (outcome?.type === 'needs-consent')
|
|
302
348
|
return askConsent(outcome);
|
|
303
|
-
|
|
304
|
-
fail(reason, `Fix the cause, then retry, or run cmod install ${definition.name} in a terminal.`, install);
|
|
349
|
+
return failInstall(outcome?.type === 'failed' ? outcome.message : `cmod setup ${formatExit(code, lastError)}`, retries);
|
|
305
350
|
};
|
|
306
351
|
const fits = (found) => found !== undefined && isAtLeast(found, oldestCmodFor(plugin?.steps ?? {}));
|
|
307
352
|
const waitForcmod = (found) => {
|
|
@@ -342,12 +387,32 @@ export function createLifecycle(definition, checksPermissions = () => true, opti
|
|
|
342
387
|
});
|
|
343
388
|
if (code === 0)
|
|
344
389
|
return finish();
|
|
345
|
-
fail(`bootstrap ${formatExit(code, lastError)}`,
|
|
390
|
+
fail(`bootstrap ${formatExit(code, lastError)}`, undefined, () => bootstrap(root));
|
|
391
|
+
};
|
|
392
|
+
const begin = async () => {
|
|
393
|
+
if (readStarted === undefined)
|
|
394
|
+
return;
|
|
395
|
+
const started = await readStarted(claude());
|
|
396
|
+
plugin = started;
|
|
397
|
+
for (const item of started.granted)
|
|
398
|
+
granted.add(item);
|
|
399
|
+
consentMs = await stampOf(consentPath(started.store));
|
|
400
|
+
recordMs = await stampOf(recordPath(started.store, started.name));
|
|
401
|
+
if (started.isInstalled) {
|
|
402
|
+
shouldRecord = started.shouldRecord;
|
|
403
|
+
if (shouldRecord)
|
|
404
|
+
record();
|
|
405
|
+
activation = undefined;
|
|
406
|
+
return activate();
|
|
407
|
+
}
|
|
408
|
+
if (started.version === undefined)
|
|
409
|
+
return fail('no version to install', 'Its author adds "version" to .claude-plugin/plugin.json.');
|
|
410
|
+
void install().catch(report);
|
|
346
411
|
};
|
|
347
412
|
const report = (error) => {
|
|
348
413
|
failure = error;
|
|
349
414
|
if (phase !== 'failed')
|
|
350
|
-
fail(messageOf(error),
|
|
415
|
+
fail(messageOf(error), undefined, begin);
|
|
351
416
|
};
|
|
352
417
|
let consentMs = 0;
|
|
353
418
|
const stampOf = async (path) => {
|
|
@@ -358,19 +423,20 @@ export function createLifecycle(definition, checksPermissions = () => true, opti
|
|
|
358
423
|
let readStarted;
|
|
359
424
|
const recover = async () => {
|
|
360
425
|
if (plugin === undefined || readStarted === undefined || (phase !== 'failed' && phase !== 'declined'))
|
|
361
|
-
return;
|
|
426
|
+
return false;
|
|
362
427
|
const modifiedMs = await stampOf(recordPath(plugin.store, plugin.name));
|
|
363
428
|
if (modifiedMs === recordMs)
|
|
364
|
-
return;
|
|
429
|
+
return false;
|
|
365
430
|
recordMs = modifiedMs;
|
|
366
431
|
const current = await readStarted(claude());
|
|
367
432
|
if (!current.isInstalled)
|
|
368
|
-
return;
|
|
433
|
+
return false;
|
|
369
434
|
plugin = current;
|
|
370
435
|
failure = undefined;
|
|
371
436
|
activation = undefined;
|
|
372
437
|
await refreshGrants();
|
|
373
438
|
await finish();
|
|
439
|
+
return true;
|
|
374
440
|
};
|
|
375
441
|
const refreshGrants = async () => {
|
|
376
442
|
if (plugin === undefined || plugin.name === cmodPluginName)
|
|
@@ -440,30 +506,11 @@ export function createLifecycle(definition, checksPermissions = () => true, opti
|
|
|
440
506
|
if (runtime !== undefined)
|
|
441
507
|
return;
|
|
442
508
|
programs = locatingPrograms(claudeCalls);
|
|
443
|
-
runtime = { claude: programs.claude, toaster: createToaster(claudeCalls, definition.name) };
|
|
509
|
+
runtime = { claude: programs.claude, toaster: createToaster(claudeCalls, definition.name, () => plugin?.version) };
|
|
510
|
+
readStarted = read;
|
|
444
511
|
try {
|
|
445
512
|
await programs.refresh();
|
|
446
|
-
|
|
447
|
-
report(error);
|
|
448
|
-
return undefined;
|
|
449
|
-
});
|
|
450
|
-
if (started === undefined)
|
|
451
|
-
return;
|
|
452
|
-
plugin = started;
|
|
453
|
-
readStarted = read;
|
|
454
|
-
for (const item of started.granted)
|
|
455
|
-
granted.add(item);
|
|
456
|
-
consentMs = await stampOf(consentPath(started.store));
|
|
457
|
-
recordMs = await stampOf(recordPath(started.store, started.name));
|
|
458
|
-
if (started.isInstalled) {
|
|
459
|
-
shouldRecord = started.shouldRecord;
|
|
460
|
-
if (shouldRecord)
|
|
461
|
-
record();
|
|
462
|
-
return await activate().catch(report);
|
|
463
|
-
}
|
|
464
|
-
if (started.version === undefined)
|
|
465
|
-
return fail('no version to install', 'Add "version" to .claude-plugin/plugin.json, then run /reload-plugins.');
|
|
466
|
-
void install().catch(report);
|
|
513
|
+
await begin().catch(report);
|
|
467
514
|
}
|
|
468
515
|
finally {
|
|
469
516
|
settleStart();
|
|
@@ -487,12 +534,14 @@ export function createLifecycle(definition, checksPermissions = () => true, opti
|
|
|
487
534
|
if (event === 'classic.UserPromptSubmit' || event === 'classic.SessionStart')
|
|
488
535
|
await recover().catch((error) => claude().ui.log(`${definition.name} stays stopped: ${messageOf(error)}`, { to: 'debug' }));
|
|
489
536
|
const isCallToThisMod = event === 'cmod.call' && e.to === definition.name;
|
|
537
|
+
if (event === 'cmod.call' && !isCallToThisMod)
|
|
538
|
+
return next(e);
|
|
490
539
|
await (event === 'classic.SessionStart' || isCallToThisMod ? holdUntilStarted() : whenActive());
|
|
491
540
|
if (event === 'classic.SessionStart' && phase !== 'active')
|
|
492
541
|
missedSessionStart = e;
|
|
493
542
|
if (isCallToThisMod && phase !== 'active') {
|
|
494
543
|
const isStopped = phase === 'declined' || phase === 'failed';
|
|
495
|
-
return { deny: isStopped ?
|
|
544
|
+
return { deny: isStopped ? `${definition.name} is not set up. Its toast says why.` : `${definition.name} is installing. Try again when it's ready.` };
|
|
496
545
|
}
|
|
497
546
|
const call = e;
|
|
498
547
|
if (event === 'cmod.call' && call.to === definition.name && mod !== undefined) {
|
|
@@ -503,19 +552,19 @@ export function createLifecycle(definition, checksPermissions = () => true, opti
|
|
|
503
552
|
}
|
|
504
553
|
if (event === 'classic.UserPromptSubmit' || event === 'classic.SessionStart')
|
|
505
554
|
await refreshGrants().catch((error) => claude().ui.log(`${definition.name} keeps its grants from before: ${messageOf(error)}`, { to: 'debug' }));
|
|
506
|
-
if (checksAnswers(event) && phase === 'active')
|
|
555
|
+
if (checksAnswers(event) && phase === 'active' && definition.name !== cmodPluginName)
|
|
507
556
|
return checkedRoute(event, e, next);
|
|
508
557
|
const answer = router.dispatch(event, e, next);
|
|
509
558
|
const render = e;
|
|
510
559
|
if (event !== 'ui.render' || render.component !== 'AbovePrompt' || runtime === undefined)
|
|
511
560
|
return answer;
|
|
512
|
-
return runtime.toaster.draw((await answer), runtime.claude.ui.resolve(render), render.props);
|
|
561
|
+
return runtime.toaster.draw((await answer), runtime.claude.ui.resolve(render), render.props, render.requestId);
|
|
513
562
|
},
|
|
514
563
|
};
|
|
515
564
|
}
|
|
516
565
|
async function createMod(definition, runtime) {
|
|
517
566
|
const { router, toaster } = runtime;
|
|
518
|
-
const report = (failure) => void
|
|
567
|
+
const report = (failure) => void toaster.fail(failure);
|
|
519
568
|
const claude = withBareRefusals(checkingKeys(runtime.claude, toaster.toast, runtime.steps.keys ?? {}));
|
|
520
569
|
const added = [];
|
|
521
570
|
const hooksAdded = [];
|
|
@@ -571,6 +620,7 @@ async function createMod(definition, runtime) {
|
|
|
571
620
|
let forkedFrom;
|
|
572
621
|
let parentAsked;
|
|
573
622
|
let hasEarlierPrompts = true;
|
|
623
|
+
let agentType;
|
|
574
624
|
const area = createUi({ name: definition.name, claude, router, toaster, report, announce, mod: () => mod });
|
|
575
625
|
const askParent = async () => {
|
|
576
626
|
if (definition.name === cmodPluginName)
|
|
@@ -591,7 +641,7 @@ async function createMod(definition, runtime) {
|
|
|
591
641
|
const modState = createState({ name: definition.name, initial: definition.state ?? {}, session, root, claude, report, changed: area.changed, forkParent });
|
|
592
642
|
const modOptions = createOptions({ name: definition.name, declared: definition.options ?? {}, fromClaude: runtime.options, claude, report, changed: modState.changed });
|
|
593
643
|
const held = runtime.checksPermissions() ? new Map() : undefined;
|
|
594
|
-
const expand = (path) => (
|
|
644
|
+
const expand = (path) => (homePrefix.test(path) ? resolvedPath(path, places) : path);
|
|
595
645
|
const settingsFile = (source) => ({
|
|
596
646
|
read: () => checked.settings.read({ source }),
|
|
597
647
|
async update(values) {
|
|
@@ -599,7 +649,7 @@ async function createMod(definition, runtime) {
|
|
|
599
649
|
await claude.cmod.call({ to: cmodPluginName, method: 'updateSettings', input: { mod: definition.name, source, project: modState.root, values } });
|
|
600
650
|
},
|
|
601
651
|
});
|
|
602
|
-
const client = createSessionsClient({ name: definition.name, claude: checked, report: (id, message, error) => report({ id, message, error }) });
|
|
652
|
+
const client = createSessionsClient({ name: definition.name, claude: checked, report: (id, message, error) => report({ id, message, error }), isGranted: (permission) => flag(permission) });
|
|
603
653
|
let followed;
|
|
604
654
|
const followFolders = (project, folder) => {
|
|
605
655
|
if (followed === undefined)
|
|
@@ -609,7 +659,21 @@ async function createMod(definition, runtime) {
|
|
|
609
659
|
};
|
|
610
660
|
const imports = {
|
|
611
661
|
sessions: client.sessions,
|
|
612
|
-
settings: {
|
|
662
|
+
settings: {
|
|
663
|
+
list: async () => (await checked.config.list()).map(({ key, label, description, kind, value, options, provider, isLocked }) => ({
|
|
664
|
+
key,
|
|
665
|
+
label,
|
|
666
|
+
kind,
|
|
667
|
+
value,
|
|
668
|
+
isLocked,
|
|
669
|
+
...(description === undefined ? {} : { description }),
|
|
670
|
+
...(options === undefined ? {} : { options }),
|
|
671
|
+
...(provider.plugin === 'engine' ? {} : { plugin: provider.plugin }),
|
|
672
|
+
})),
|
|
673
|
+
user: settingsFile('user'),
|
|
674
|
+
project: settingsFile('project'),
|
|
675
|
+
local: settingsFile('local'),
|
|
676
|
+
},
|
|
613
677
|
model: { complete: (completion, options) => checked.model.complete(completion, options) },
|
|
614
678
|
fs: {
|
|
615
679
|
read: (path) => checked.fs.read(expand(path)),
|
|
@@ -745,10 +809,11 @@ async function createMod(definition, runtime) {
|
|
|
745
809
|
};
|
|
746
810
|
const followCurrent = async (title) => {
|
|
747
811
|
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 }) };
|
|
812
|
+
followed = { id, title: title ?? '', project: modState.root, cwd, model, startedAt: new Date(usage.startedAt).toISOString(), ...(forkedFrom === undefined ? {} : { forkedFrom }), ...(agentType === undefined ? {} : { agentType }) };
|
|
749
813
|
client.follow(followed);
|
|
750
814
|
};
|
|
751
815
|
router.add('classic.SessionStart', async (e, next) => {
|
|
816
|
+
agentType = e.agent_id === undefined ? e.agent_type : agentType;
|
|
752
817
|
const previous = followed?.id;
|
|
753
818
|
if (e.source === 'fork' && previous !== undefined && previous !== e.session_id)
|
|
754
819
|
forkedFrom = previous;
|
|
@@ -779,8 +844,10 @@ async function createMod(definition, runtime) {
|
|
|
779
844
|
}
|
|
780
845
|
router.add('command.run', async (e, next) => {
|
|
781
846
|
const answer = await next(e);
|
|
782
|
-
if (e.command === 'cd')
|
|
847
|
+
if (e.command === 'cd') {
|
|
783
848
|
await followSession(true);
|
|
849
|
+
conversations?.moved();
|
|
850
|
+
}
|
|
784
851
|
return answer;
|
|
785
852
|
});
|
|
786
853
|
router.add('skill.prompt', userSkillHook(claude));
|
|
@@ -793,10 +860,14 @@ async function createMod(definition, runtime) {
|
|
|
793
860
|
if (e.method === conversationMethods.changed && call.caller === cmodPluginName) {
|
|
794
861
|
if (!flag('conversation'))
|
|
795
862
|
return { value: null };
|
|
796
|
-
const { session: changedSession, change } = e.input;
|
|
797
|
-
client.changed(changedSession, change);
|
|
863
|
+
const { session: changedSession, change, isInProject } = e.input;
|
|
864
|
+
client.changed(changedSession, change, isInProject !== false);
|
|
798
865
|
return { value: null };
|
|
799
866
|
}
|
|
867
|
+
if (e.method === conversationMethods.before && call.caller === cmodPluginName) {
|
|
868
|
+
const { session: asking, event, change, isInProject } = e.input;
|
|
869
|
+
return { value: (await client.answerBefore(asking, event, change, isInProject !== false)) ?? null };
|
|
870
|
+
}
|
|
800
871
|
if (e.method === settingsPagesMethod)
|
|
801
872
|
return { value: [...pages].map(([id, { title }]) => ({ id, title })) };
|
|
802
873
|
if (e.method === openPageMethod) {
|
|
@@ -1 +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;AAKpD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AACzC,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAA;AAE3C,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,
|
|
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;AAKpD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AACzC,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAA;AAE3C,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,CASjE;AAED,wBAAgB,cAAc,CAAC,EAC7B,IAAI,EACJ,MAAM,EACN,MAAM,EACN,UAAU,EACV,MAAM,GACP,EAAE;IACD,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,UAAU,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;IACpD,QAAQ,CAAC,MAAM,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,IAAI,CAAA;CAC5C,GAAG,QAAQ,CA4HX"}
|
package/runtime/metadata.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { formatOf, MetadataError, readMetadata, sidecarSuffix, writeMetadata } from '../utils/metadata.js';
|
|
2
|
-
import { ifMissing, isMissing } from '../utils/paths.js';
|
|
2
|
+
import { expandHome, homePrefix, ifMissing, isMissing } from '../utils/paths.js';
|
|
3
3
|
import { messageOf } from '../utils/text.js';
|
|
4
4
|
import { picomatch, resolve } from '../vendor.js';
|
|
5
5
|
const skippedFolders = ['.git', 'node_modules'];
|
|
@@ -8,10 +8,11 @@ export function resolvedPath(path, places) {
|
|
|
8
8
|
const configured = /^~\/\.claude(?=\/|$)/.exec(path);
|
|
9
9
|
if (configured !== null && places.configRoot !== undefined)
|
|
10
10
|
return resolve(`${places.configRoot}${path.slice(configured[0].length)}`);
|
|
11
|
-
|
|
11
|
+
const prefix = homePrefix.exec(path)?.[0];
|
|
12
|
+
if (prefix !== undefined) {
|
|
12
13
|
if (places.home === undefined)
|
|
13
|
-
throw new Error(`${path} starts with
|
|
14
|
-
return resolve(
|
|
14
|
+
throw new Error(`${path} starts with ${prefix}, and HOME is not set. Start Claude Code with HOME set.`);
|
|
15
|
+
return resolve(expandHome(path, places.home, places.configRoot));
|
|
15
16
|
}
|
|
16
17
|
return path.startsWith('/') ? resolve(path) : resolve(places.projectRoot(), path);
|
|
17
18
|
}
|