@cmodjs/core 2.1.0 → 3.1.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 +2 -2
- package/docs/mod.md +3 -1
- package/docs/permissions.md +3 -1
- package/docs/session.md +9 -1
- package/docs/testing.md +4 -2
- package/jobs/context.d.ts.map +1 -1
- package/jobs/context.js +2 -1
- package/jobs/permissions.d.ts.map +1 -1
- package/jobs/permissions.js +1 -1
- package/jobs/prompt.d.ts.map +1 -1
- package/jobs/prompt.js +2 -1
- package/jobs/slash-command.d.ts.map +1 -1
- package/jobs/slash-command.js +4 -3
- package/package.json +1 -1
- package/records.d.ts +2 -1
- package/records.d.ts.map +1 -1
- package/records.js +4 -1
- package/register.d.ts.map +1 -1
- package/register.js +58 -51
- package/runtime/before.d.ts +1 -1
- package/runtime/before.d.ts.map +1 -1
- package/runtime/before.js +6 -5
- package/runtime/conversations.d.ts.map +1 -1
- package/runtime/conversations.js +46 -19
- package/runtime/hooks.d.ts +2 -1
- package/runtime/hooks.d.ts.map +1 -1
- package/runtime/hooks.js +7 -0
- package/runtime/lifecycle.d.ts +2 -1
- package/runtime/lifecycle.d.ts.map +1 -1
- package/runtime/lifecycle.js +31 -9
- package/runtime/permission-check.d.ts +3 -0
- package/runtime/permission-check.d.ts.map +1 -1
- package/runtime/permission-check.js +31 -10
- package/runtime/router.d.ts +3 -1
- package/runtime/router.d.ts.map +1 -1
- package/runtime/router.js +19 -11
- package/runtime/sessions.d.ts +13 -4
- package/runtime/sessions.d.ts.map +1 -1
- package/runtime/sessions.js +67 -26
- package/runtime/toaster.d.ts.map +1 -1
- package/runtime/toaster.js +3 -0
- package/session/context/turn/message/message.d.ts.map +1 -1
- package/session/context/turn/message/message.js +30 -2
- package/session/context/turn/message/note.d.ts +7 -0
- package/session/context/turn/message/note.d.ts.map +1 -1
- package/session/context/turn/message/note.js +12 -0
- package/session/session.d.ts +3 -1
- package/session/session.d.ts.map +1 -1
- package/src/jobs/context.ts +2 -1
- package/src/jobs/permissions.ts +8 -4
- package/src/jobs/prompt.ts +2 -1
- package/src/jobs/slash-command.ts +4 -3
- package/src/records.ts +5 -1
- package/src/register.ts +63 -52
- package/src/runtime/before.ts +7 -6
- package/src/runtime/conversations.ts +54 -27
- package/src/runtime/hooks.ts +9 -1
- package/src/runtime/lifecycle.ts +37 -13
- package/src/runtime/permission-check.ts +51 -21
- package/src/runtime/router.ts +23 -13
- package/src/runtime/sessions.ts +91 -37
- package/src/runtime/toaster.ts +2 -0
- package/src/session/context/turn/message/message.ts +27 -2
- package/src/session/context/turn/message/note.ts +16 -0
- package/src/session/session.ts +1 -2
- package/src/testing/fake-claude.ts +4 -3
- package/src/testing.ts +6 -5
- package/testing/fake-claude.d.ts +1 -0
- package/testing/fake-claude.d.ts.map +1 -1
- package/testing/fake-claude.js +3 -2
- package/testing.d.ts.map +1 -1
- package/testing.js +5 -4
package/runtime/conversations.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { consentPath, dataFolder, isObject, parseConsent } from '../records.js';
|
|
2
2
|
import { Message, rowOfAppend } from '../session/context/turn/message/message.js';
|
|
3
|
-
import { Note } from '../session/context/turn/message/note.js';
|
|
3
|
+
import { modNote, Note } from '../session/context/turn/message/note.js';
|
|
4
4
|
import { Person } from '../session/context/turn/message/sender.js';
|
|
5
5
|
import { createConversation, emptyConversation } from '../session/conversation.js';
|
|
6
6
|
import { Session, UsageChange } from '../session/session.js';
|
|
@@ -8,12 +8,14 @@ import { GoalChange } from '../session/goal/goal.js';
|
|
|
8
8
|
import { receivedMatcher } from '../utils/matcher.js';
|
|
9
9
|
import { ifMissing } from '../utils/paths.js';
|
|
10
10
|
import { formatExit, messageOf } from '../utils/text.js';
|
|
11
|
-
import { foldBefore, installBefore, relayOf } from './before.js';
|
|
11
|
+
import { beforeAnswerKeys, foldBefore, installBefore, relayOf } from './before.js';
|
|
12
12
|
import { cmodPluginName } from './dependencies.js';
|
|
13
13
|
import { readLines, runCmod } from './programs.js';
|
|
14
|
-
import {
|
|
14
|
+
import { PassedTimeout } from './deadline.js';
|
|
15
|
+
import { actPermission, conversationMethods, everySession, isAct, isHeardBy, isInScope, resultOf, spawnTask, usageOf } from './sessions.js';
|
|
15
16
|
const bufferedTaskRows = 200;
|
|
16
17
|
const beforeDeadlineMs = 5000;
|
|
18
|
+
const awayFoldMs = 8000;
|
|
17
19
|
const livenessMs = 5000;
|
|
18
20
|
export function serveConversations({ claude, router, store, project, report }) {
|
|
19
21
|
const folder = dataFolder(store, cmodPluginName);
|
|
@@ -161,7 +163,7 @@ export function serveConversations({ claude, router, store, project, report }) {
|
|
|
161
163
|
reject(error);
|
|
162
164
|
},
|
|
163
165
|
};
|
|
164
|
-
const timer = deadlineMs === undefined ? watchLife(settle, to) : claude.clock.after(deadlineMs, () => settle.reject(new
|
|
166
|
+
const timer = deadlineMs === undefined ? watchLife(settle, to) : claude.clock.after(deadlineMs, () => settle.reject(new PassedTimeout(`session ${to} did not answer within ${deadlineMs / 1000} s`)));
|
|
165
167
|
asked.set(id, settle);
|
|
166
168
|
post(to, { ...relay, id, from: session }).catch((error) => settle.reject(error instanceof Error ? error : new Error(messageOf(error))));
|
|
167
169
|
});
|
|
@@ -210,13 +212,26 @@ export function serveConversations({ claude, router, store, project, report }) {
|
|
|
210
212
|
throw new Error(`${mod} needs the conversation permission to change what happens in another session. Turn it on in /mods ${mod}.`);
|
|
211
213
|
return claude.cmod.call({ to: mod, method: conversationMethods.before, input: { session: from, event, change, isInProject } });
|
|
212
214
|
};
|
|
213
|
-
const
|
|
215
|
+
const isUnderHere = (task, ancestor) => {
|
|
216
|
+
for (let at = task, steps = 0; at !== undefined && steps < 64; at = conversation.state.tasks[at]?.parent, steps += 1)
|
|
217
|
+
if (at === ancestor)
|
|
218
|
+
return true;
|
|
219
|
+
return false;
|
|
220
|
+
};
|
|
221
|
+
const refuses = (watcher, event, change) => beforeAnswerKeys[event].includes('refuse') && (watcher.refusing ?? []).some(({ event: heard, matcher, scope }) => isInScope(scope ?? {}, session, change, isUnderHere) && isHeardBy(heard, matcher === undefined ? undefined : receivedMatcher(matcher), change));
|
|
222
|
+
const askAway = async (watcher, event, change, endsAt) => {
|
|
214
223
|
try {
|
|
215
|
-
|
|
224
|
+
const leftMs = Math.min(beforeDeadlineMs, endsAt - (await claude.clock.now()));
|
|
225
|
+
if (leftMs <= 0)
|
|
226
|
+
throw new PassedTimeout(`the ${awayFoldMs / 1000} s cmod waits on other sessions ran out before session ${watcher.from} was asked`);
|
|
227
|
+
return await ask(watcher.from, { kind: 'before', mod: watcher.mod, event, change, project: project() }, leftMs);
|
|
216
228
|
}
|
|
217
229
|
catch (error) {
|
|
218
|
-
if (await isRunning(watcher.from))
|
|
230
|
+
if (await isRunning(watcher.from)) {
|
|
231
|
+
if (error instanceof PassedTimeout && refuses(watcher, event, change))
|
|
232
|
+
return { refuse: `${watcher.mod}: ${error.message}` };
|
|
219
233
|
throw error;
|
|
234
|
+
}
|
|
220
235
|
watchers = watchers.filter((each) => each.from !== watcher.from);
|
|
221
236
|
claude.ui.log(`cmod: session ${watcher.from} ended without clearing its before listeners, so they are cleared now`, { to: 'debug' });
|
|
222
237
|
await claude.fs.write(`${watches}/${watcher.from}.json`, '[]');
|
|
@@ -235,7 +250,15 @@ export function serveConversations({ claude, router, store, project, report }) {
|
|
|
235
250
|
await Promise.all([starting, watching]);
|
|
236
251
|
return deciders(event).length > 0;
|
|
237
252
|
},
|
|
238
|
-
|
|
253
|
+
fold: () => {
|
|
254
|
+
let endsAt;
|
|
255
|
+
return (event, change) => foldBefore(event, change, deciders(event).map((watcher) => async (latest) => {
|
|
256
|
+
if (watcher.from === session)
|
|
257
|
+
return askHere(watcher.mod, event, latest);
|
|
258
|
+
endsAt ??= claude.clock.now().then((now) => now + awayFoldMs);
|
|
259
|
+
return askAway(watcher, event, latest, await endsAt);
|
|
260
|
+
}), (error) => report(`before:${event}`, `Passed the ${event} on without a mod's answer`, error));
|
|
261
|
+
},
|
|
239
262
|
},
|
|
240
263
|
});
|
|
241
264
|
const start = (id, source) => {
|
|
@@ -285,15 +308,16 @@ export function serveConversations({ claude, router, store, project, report }) {
|
|
|
285
308
|
await publishWatches(true).catch((error) => report('before', 'Left its before listeners registered', error));
|
|
286
309
|
return next(e);
|
|
287
310
|
});
|
|
288
|
-
|
|
311
|
+
const recordAppend = async (e, next) => {
|
|
289
312
|
const kept = await next(e);
|
|
290
313
|
if (kept.deny !== undefined || session === '')
|
|
291
314
|
return kept;
|
|
292
315
|
const cwd = e.message.type === 'assistant' ? await claude.session.cwd() : '';
|
|
293
316
|
void take(rowOfAppend({ uuid: kept.uuid, door: e.door, origin: e.origin, message: kept.message }, new Date().toISOString(), cwd), e.agentId).catch((error) => report('append', 'Missed a message', error));
|
|
294
317
|
return kept;
|
|
295
|
-
}
|
|
296
|
-
router.add('session.
|
|
318
|
+
};
|
|
319
|
+
router.add('session.append', recordAppend, (e, _error, next) => recordAppend(e, next));
|
|
320
|
+
const recordCompaction = async (e, next) => {
|
|
297
321
|
const compacted = await next(e);
|
|
298
322
|
if (e.trigger === 'precompute' || compacted.skip !== undefined || session === '')
|
|
299
323
|
return compacted;
|
|
@@ -301,14 +325,15 @@ export function serveConversations({ claude, router, store, project, report }) {
|
|
|
301
325
|
const boundary = { id: `compact:${e.agentId ?? session}:${at}`, at, type: 'system', name: 'compact_boundary', isMeta: false, isSummary: false, content: [] };
|
|
302
326
|
void take(boundary, e.agentId).catch((error) => report('compact', 'Missed a compaction', error));
|
|
303
327
|
return compacted;
|
|
304
|
-
}
|
|
328
|
+
};
|
|
329
|
+
router.add('session.compact', recordCompaction, (e, _error, next) => recordCompaction(e, next));
|
|
305
330
|
router.add('session.measure', async (e, next) => {
|
|
306
331
|
const measured = await next(e);
|
|
307
332
|
if (session !== '')
|
|
308
333
|
dispatch(session, [{ type: UsageChange, usage: await usageOf(claude) }]);
|
|
309
334
|
return measured;
|
|
310
335
|
});
|
|
311
|
-
|
|
336
|
+
const recordSpawn = async (e, next) => {
|
|
312
337
|
const spawned = await next(e);
|
|
313
338
|
if (spawned.deny !== undefined || spawned.agentId === undefined)
|
|
314
339
|
return spawned;
|
|
@@ -321,7 +346,8 @@ export function serveConversations({ claude, router, store, project, report }) {
|
|
|
321
346
|
for (const row of waiting)
|
|
322
347
|
dispatch(session, conversation.add(row, id));
|
|
323
348
|
return spawned;
|
|
324
|
-
}
|
|
349
|
+
};
|
|
350
|
+
router.add('agent.spawn', recordSpawn, (e, _error, next) => recordSpawn(e, next));
|
|
325
351
|
router.add('session.receive', async (e, next) => {
|
|
326
352
|
const sent = relayOf(e);
|
|
327
353
|
if (sent === undefined)
|
|
@@ -369,7 +395,7 @@ export function serveConversations({ claude, router, store, project, report }) {
|
|
|
369
395
|
if (action === 'abort')
|
|
370
396
|
return abortTurn(input, task);
|
|
371
397
|
if (task !== undefined)
|
|
372
|
-
return performInTask(action, input, task);
|
|
398
|
+
return performInTask(caller, action, input, task);
|
|
373
399
|
if (action === 'usage')
|
|
374
400
|
return usageOf(claude);
|
|
375
401
|
if (action === 'surfaces')
|
|
@@ -378,7 +404,7 @@ export function serveConversations({ claude, router, store, project, report }) {
|
|
|
378
404
|
const { type, text, as } = input;
|
|
379
405
|
if (as === Session)
|
|
380
406
|
throw new Error(`session ${session} cannot send as itself to itself.`);
|
|
381
|
-
const delivered = type === Note ? await claude.session.append({ message: { type: 'user', content: [{ type: 'text', text }] } }) : await claude.prompt.submit({ text, ...(as === Person ? { asUser: true } : {}) });
|
|
407
|
+
const delivered = type === Note ? await claude.session.append({ message: { type: 'user', content: [{ type: 'text', text: modNote(caller, text) }] } }) : await claude.prompt.submit({ text, ...(as === Person ? { asUser: true } : {}) });
|
|
382
408
|
if ('deny' in delivered && typeof delivered.deny === 'string')
|
|
383
409
|
throw new Error(`Claude Code refused the note: ${delivered.deny}`);
|
|
384
410
|
if ('drop' in delivered && typeof delivered.drop === 'string')
|
|
@@ -401,7 +427,7 @@ export function serveConversations({ claude, router, store, project, report }) {
|
|
|
401
427
|
await claude.turn.abort({ turnId });
|
|
402
428
|
return null;
|
|
403
429
|
};
|
|
404
|
-
const performInTask = async (action, input, task) => {
|
|
430
|
+
const performInTask = async (caller, action, input, task) => {
|
|
405
431
|
const running = conversation.state.tasks[task];
|
|
406
432
|
if (action === 'usage')
|
|
407
433
|
return json(['history', 'usage', session, '--task', task]);
|
|
@@ -411,7 +437,8 @@ export function serveConversations({ claude, router, store, project, report }) {
|
|
|
411
437
|
throw new Error(`cmod: ${action} runs only on a session's own main loop.`);
|
|
412
438
|
if (running === undefined || running.closedAt !== undefined)
|
|
413
439
|
throw ended(task);
|
|
414
|
-
const
|
|
440
|
+
const { type, text } = input;
|
|
441
|
+
const sent = await claude.session.send({ to: { agentId: task }, text: type === Note ? modNote(caller, text) : text });
|
|
415
442
|
if (sent.isDelivered !== true)
|
|
416
443
|
throw ended(task);
|
|
417
444
|
return null;
|
|
@@ -554,7 +581,7 @@ export function serveConversations({ claude, router, store, project, report }) {
|
|
|
554
581
|
throw new Error('cmod hears no caller for this before listener.');
|
|
555
582
|
const target = String(request['session']);
|
|
556
583
|
const list = Array.isArray(request['subscriptions']) ? request['subscriptions'] : [];
|
|
557
|
-
ownWatches.set(`${caller}\n${target}`, { mod: caller, target, project: project(), events: [...new Set(list.map(({ event }) => event))] });
|
|
584
|
+
ownWatches.set(`${caller}\n${target}`, { mod: caller, target, project: project(), events: [...new Set(list.map(({ event }) => event))], refusing: list.filter((each) => each.onFailure === 'refuse') });
|
|
558
585
|
await publishWatches();
|
|
559
586
|
return null;
|
|
560
587
|
},
|
package/runtime/hooks.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ClassicHookEvent, ClassicHookInputs } from 'claude-code';
|
|
2
2
|
import type { Claude } from './claude.js';
|
|
3
|
-
import type { RoutedHook } from './router.js';
|
|
3
|
+
import type { RoutedFailure, RoutedHook } from './router.js';
|
|
4
4
|
import type { Failure } from './toaster.js';
|
|
5
5
|
import { type CallBase } from '../utils/call-effects.js';
|
|
6
6
|
export type Hook = Exclude<ClassicHookEvent, 'PreToolUse'>;
|
|
@@ -12,6 +12,7 @@ type CallFiles = {
|
|
|
12
12
|
readonly changed: string[];
|
|
13
13
|
};
|
|
14
14
|
export declare function classicHook<H extends Hook>(event: H, hook: HookListener<H>, report: (failure: Failure) => void): RoutedHook<RoutedEvent>;
|
|
15
|
+
export declare function classicFailure<H extends Hook>(failed: (input: ClassicHookInputs[H], error: Error) => HookFields | undefined | Promise<HookFields | undefined>): RoutedFailure<RoutedEvent>;
|
|
15
16
|
export declare function userSkillHook(claude: Claude): RoutedHook<'skill.prompt'>;
|
|
16
17
|
export type HomeFolders = {
|
|
17
18
|
readonly home: string | undefined;
|
package/runtime/hooks.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"hooks.d.ts","sourceRoot":"","sources":["../src/runtime/hooks.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAA;AAEtE,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AACzC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;
|
|
1
|
+
{"version":3,"file":"hooks.d.ts","sourceRoot":"","sources":["../src/runtime/hooks.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAA;AAEtE,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AACzC,OAAO,KAAK,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AAC5D,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAA;AAC3C,OAAO,EAAe,KAAK,QAAQ,EAAmB,MAAM,0BAA0B,CAAA;AAKtF,MAAM,MAAM,IAAI,GAAG,OAAO,CAAC,gBAAgB,EAAE,YAAY,CAAC,CAAA;AAE1D,MAAM,MAAM,UAAU,GAAG,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAA;AAE1D,MAAM,MAAM,YAAY,CAAC,CAAC,SAAS,IAAI,IAAI,CAAC,KAAK,EAAE,iBAAiB,CAAC,CAAC,CAAC,KAAK,UAAU,GAAG,SAAS,GAAG,OAAO,CAAC,UAAU,GAAG,SAAS,CAAC,CAAA;AAEpI,MAAM,MAAM,WAAW,GACnB,WAAW,IAAI,EAAE,GACjB,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,GACb,YAAY,CAAA;AAEhB,KAAK,SAAS,GAAG;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,CAAC;IAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,EAAE,CAAA;CAAE,CAAA;AAExE,wBAAgB,WAAW,CAAC,CAAC,SAAS,IAAI,EAAE,KAAK,EAAE,CAAC,EAAE,IAAI,EAAE,YAAY,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,IAAI,GAAG,UAAU,CAAC,WAAW,CAAC,CAYxI;AAED,wBAAgB,cAAc,CAAC,CAAC,SAAS,IAAI,EAAE,MAAM,EAAE,CAAC,KAAK,EAAE,iBAAiB,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,KAAK,KAAK,UAAU,GAAG,SAAS,GAAG,OAAO,CAAC,UAAU,GAAG,SAAS,CAAC,GAAG,aAAa,CAAC,WAAW,CAAC,CAM1L;AAMD,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,GAAG,UAAU,CAAC,cAAc,CAAC,CAexE;AAED,MAAM,MAAM,WAAW,GAAG;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,CAAC;IAAC,QAAQ,CAAC,UAAU,EAAE,MAAM,GAAG,SAAS,CAAA;CAAE,CAAA;AAExG,wBAAsB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,CAAC,WAAW,CAAC,EAAE,GAAG,EAAE,QAAQ,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,CAAC,CAgB3I"}
|
package/runtime/hooks.js
CHANGED
|
@@ -17,6 +17,13 @@ export function classicHook(event, hook, report) {
|
|
|
17
17
|
};
|
|
18
18
|
return routed;
|
|
19
19
|
}
|
|
20
|
+
export function classicFailure(failed) {
|
|
21
|
+
const routed = async (e, error, next) => {
|
|
22
|
+
const ours = await failed(e, error);
|
|
23
|
+
return ours === undefined ? undefined : mergeClassic((await next(e)), ours);
|
|
24
|
+
};
|
|
25
|
+
return routed;
|
|
26
|
+
}
|
|
20
27
|
const frontmatter = /^---\r?\n[\s\S]*?\r?\n---[ \t]*(?:\r?\n|$)/;
|
|
21
28
|
const baseDirectoryLine = /^Base directory for this skill: [^\n]*\n\n/;
|
|
22
29
|
export function userSkillHook(claude) {
|
package/runtime/lifecycle.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Args, EventResult, Frozen, PluginOptions } from 'claude-code';
|
|
1
|
+
import type { Args, EventResult, Frozen, HookFailure, PluginOptions } from 'claude-code';
|
|
2
2
|
import type { Mod, ModDefinition } from '../mod.js';
|
|
3
3
|
import type { Options } from '../options.js';
|
|
4
4
|
import { type Steps } from '../records.js';
|
|
@@ -25,6 +25,7 @@ export type Lifecycle<State extends object> = {
|
|
|
25
25
|
readonly toasts: readonly ShownToast[];
|
|
26
26
|
start(claude: Claude, read: (claude: Claude) => Promise<Plugin>): Promise<void>;
|
|
27
27
|
route<N extends RoutedEvent>(event: N, e: Frozen<Args<N>>, next: RouterNext<N>): Promise<EventResult<N>>;
|
|
28
|
+
caught<N extends RoutedEvent>(event: N, e: Frozen<Args<N>>, next: RouterNext<N>, failure: HookFailure): Promise<EventResult<N>>;
|
|
28
29
|
raise(method: string, payload: Readonly<Record<string, unknown>>): Promise<unknown>;
|
|
29
30
|
};
|
|
30
31
|
export declare function readPlugin(claude: Claude): Promise<Plugin>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lifecycle.d.ts","sourceRoot":"","sources":["../src/runtime/lifecycle.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,
|
|
1
|
+
{"version":3,"file":"lifecycle.d.ts","sourceRoot":"","sources":["../src/runtime/lifecycle.ts"],"names":[],"mappings":"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"}
|
package/runtime/lifecycle.js
CHANGED
|
@@ -7,7 +7,7 @@ import { bind } from './bound.js';
|
|
|
7
7
|
import { serveConversations } from './conversations.js';
|
|
8
8
|
import { answerCall, answeringOwnCalls, cmodPluginName, dependencyCalls, withBareRefusals } from './dependencies.js';
|
|
9
9
|
import { keepEngine } from './engine.js';
|
|
10
|
-
import { classicHook, userSkillHook } from './hooks.js';
|
|
10
|
+
import { classicFailure, classicHook, userSkillHook } from './hooks.js';
|
|
11
11
|
import { checkedAnswer, checkingGrants, checkSettings, checksAnswers, checkWrite, isCovered, itemOf, passedDown, programAddresses } from './grants.js';
|
|
12
12
|
import { cmodError, lastLineOf, locatingPrograms, readLines } from './programs.js';
|
|
13
13
|
import { createModFiles, resolvedPath } from './metadata.js';
|
|
@@ -29,7 +29,8 @@ const installRetrySeconds = [2, 4, 8, 16, 32, 64, 128];
|
|
|
29
29
|
const cmodCheckMs = 1000;
|
|
30
30
|
const cmodWaitMs = 60_000;
|
|
31
31
|
const cmodSlowCheckMs = 10_000;
|
|
32
|
-
const
|
|
32
|
+
const hookMs = 10_000;
|
|
33
|
+
const startHoldMs = hookMs - 1_000;
|
|
33
34
|
const dependencyCallMs = 30_000;
|
|
34
35
|
const cannotStart = /failed to start: /;
|
|
35
36
|
const shellTools = ['Bash', 'PowerShell'];
|
|
@@ -582,6 +583,17 @@ export function createLifecycle(definition, permissionRoutes = () => undefined,
|
|
|
582
583
|
return answer;
|
|
583
584
|
return runtime.toaster.draw((await answer), runtime.claude.ui.resolve(render), render.props, render.requestId);
|
|
584
585
|
},
|
|
586
|
+
async caught(event, e, next, { kind, message }) {
|
|
587
|
+
const call = e;
|
|
588
|
+
const isAsk = event === 'cmod.call' && call.to === definition.name && beforeAskOf(call) !== undefined;
|
|
589
|
+
const reasons = { timeout: `it ran past the ${hookMs / 1000} s Claude Code gives a hook`, throw: message ?? 'it threw' };
|
|
590
|
+
const reason = reasons[kind];
|
|
591
|
+
if (reason === undefined)
|
|
592
|
+
return isAsk ? { value: null } : next(e);
|
|
593
|
+
const error = new Error(reason);
|
|
594
|
+
toastFailure({ id: `stopped:${event}`, message: `Claude Code stopped its ${event.replace(/^classic\./, '')} hook`, error });
|
|
595
|
+
return router.dispatchFailure(event, e, error, next);
|
|
596
|
+
},
|
|
585
597
|
};
|
|
586
598
|
}
|
|
587
599
|
async function createMod(definition, runtime) {
|
|
@@ -661,7 +673,7 @@ async function createMod(definition, runtime) {
|
|
|
661
673
|
}));
|
|
662
674
|
const modState = createState({ name: definition.name, initial: definition.state ?? {}, session, root, claude, report, changed: area.changed, forkParent });
|
|
663
675
|
const modOptions = createOptions({ name: definition.name, declared: definition.options ?? {}, fromClaude: runtime.options, claude, report, changed: modState.changed });
|
|
664
|
-
const hookOn = (hook, listener) => router.add(`classic.${hook}`, classicHook(hook, listener, report));
|
|
676
|
+
const hookOn = (hook, listener, failed) => router.add(`classic.${hook}`, classicHook(hook, listener, report), failed === undefined ? undefined : classicFailure(failed));
|
|
665
677
|
const expand = (path) => (homePrefix.test(path) ? resolvedPath(path, places) : path);
|
|
666
678
|
const settingsFile = (source) => ({
|
|
667
679
|
read: () => checked.settings.read({ source }),
|
|
@@ -675,8 +687,8 @@ async function createMod(definition, runtime) {
|
|
|
675
687
|
claude: checked,
|
|
676
688
|
report: (id, message, error) => report({ id, message, error }),
|
|
677
689
|
isGranted: (permission) => flag(permission),
|
|
678
|
-
hookOn: (hook, listener) => hookOn(hook, listener),
|
|
679
|
-
routeOn: (event, hook) => router.add(event, hook),
|
|
690
|
+
hookOn: (hook, listener, failed) => hookOn(hook, listener, failed),
|
|
691
|
+
routeOn: (event, hook, failed) => router.add(event, hook, failed),
|
|
680
692
|
permissionRoutes: runtime.permissionRoutes,
|
|
681
693
|
});
|
|
682
694
|
let followed;
|
|
@@ -918,10 +930,9 @@ async function createMod(definition, runtime) {
|
|
|
918
930
|
client.changed(changedSession, change, isInProject !== false);
|
|
919
931
|
return { value: null };
|
|
920
932
|
}
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
return { value: (await client.answerBefore(
|
|
924
|
-
}
|
|
933
|
+
const ask = beforeAskOf(call);
|
|
934
|
+
if (ask !== undefined)
|
|
935
|
+
return { value: (await client.answerBefore(ask.session, ask.event, ask.change, ask.isInProject)) ?? null };
|
|
925
936
|
if (e.method === settingsPagesMethod)
|
|
926
937
|
return { value: [...pages].map(([id, { title }]) => ({ id, title })) };
|
|
927
938
|
if (e.method === openPageMethod) {
|
|
@@ -930,6 +941,11 @@ async function createMod(definition, runtime) {
|
|
|
930
941
|
}
|
|
931
942
|
const served = await conversations?.answer(call);
|
|
932
943
|
return served ?? answerCall(definition.name, api, e);
|
|
944
|
+
}, (e, error) => {
|
|
945
|
+
if (e.to !== definition.name)
|
|
946
|
+
return undefined;
|
|
947
|
+
const ask = beforeAskOf(e);
|
|
948
|
+
return ask === undefined ? { deny: `${definition.name}: ${error.message}` } : { value: client.failedBefore(ask.session, ask.event, ask.change, ask.isInProject, error) ?? null };
|
|
933
949
|
});
|
|
934
950
|
const agents = toolCalls(claude, router);
|
|
935
951
|
keepEngine(mod, { claude: checked, router, toolCalls: agents, report, hasPermissionCheck: () => runtime.permissionRoutes() !== undefined });
|
|
@@ -972,6 +988,12 @@ function checkedMs(name, subject, ms) {
|
|
|
972
988
|
throw new Error(`${name}: ${subject} is ${ms}. Give a whole number of milliseconds above 0 and at most ${longestTimerMs}.`);
|
|
973
989
|
return ms;
|
|
974
990
|
}
|
|
991
|
+
function beforeAskOf(call) {
|
|
992
|
+
if (call.method !== conversationMethods.before || call.caller !== cmodPluginName)
|
|
993
|
+
return undefined;
|
|
994
|
+
const { session, event, change, isInProject } = call.input;
|
|
995
|
+
return { session, event, change, isInProject: isInProject !== false };
|
|
996
|
+
}
|
|
975
997
|
async function failsAs(subject, step) {
|
|
976
998
|
try {
|
|
977
999
|
await step();
|
|
@@ -3,6 +3,9 @@ import type { HookFields } from './hooks.js';
|
|
|
3
3
|
import { type HookOn, type RouteOn } from './sessions.js';
|
|
4
4
|
export type PermissionUse = {
|
|
5
5
|
readonly raise: (method: string, payload: HookFields) => Promise<HookFields | undefined>;
|
|
6
|
+
readonly refusalFor: (method: string, payload: HookFields, error: Error) => {
|
|
7
|
+
readonly refuse: string;
|
|
8
|
+
} | undefined;
|
|
6
9
|
readonly hookOn: HookOn;
|
|
7
10
|
readonly routeOn: RouteOn;
|
|
8
11
|
readonly current: () => {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"permission-check.d.ts","sourceRoot":"","sources":["../src/runtime/permission-check.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,WAAW,CAAA;AAC3C,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAA;AAC5C,OAAO,EAAuC,KAAK,MAAM,EAAE,KAAK,OAAO,EAAkB,MAAM,eAAe,CAAA;AAE9G,MAAM,MAAM,aAAa,GAAG;IAC1B,QAAQ,CAAC,KAAK,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,UAAU,KAAK,OAAO,CAAC,UAAU,GAAG,SAAS,CAAC,CAAA;IACxF,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAA;IACzB,QAAQ,CAAC,OAAO,EAAE,MAAM;QAAE,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;KAAE,CAAA;CACtE,CAAA;AAED,MAAM,MAAM,gBAAgB,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,aAAa,KAAK,UAAU,CAAA;
|
|
1
|
+
{"version":3,"file":"permission-check.d.ts","sourceRoot":"","sources":["../src/runtime/permission-check.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,WAAW,CAAA;AAC3C,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAA;AAC5C,OAAO,EAAuC,KAAK,MAAM,EAAE,KAAK,OAAO,EAAkB,MAAM,eAAe,CAAA;AAE9G,MAAM,MAAM,aAAa,GAAG;IAC1B,QAAQ,CAAC,KAAK,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,UAAU,KAAK,OAAO,CAAC,UAAU,GAAG,SAAS,CAAC,CAAA;IACxF,QAAQ,CAAC,UAAU,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,UAAU,EAAE,KAAK,EAAE,KAAK,KAAK;QAAE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;KAAE,GAAG,SAAS,CAAA;IACnH,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAA;IACzB,QAAQ,CAAC,OAAO,EAAE,MAAM;QAAE,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;KAAE,CAAA;CACtE,CAAA;AAED,MAAM,MAAM,gBAAgB,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,aAAa,KAAK,UAAU,CAAA;AAQjF,eAAO,MAAM,gBAAgB,EAAE,gBAwD9B,CAAA"}
|
|
@@ -1,12 +1,19 @@
|
|
|
1
1
|
import { defined, processPayloadOf, recordOf } from './sessions.js';
|
|
2
2
|
const verdictName = (decision) => (decision === 'deny' ? 'refuse' : decision);
|
|
3
|
-
export const permissionRoutes = (method, { raise, hookOn, routeOn, current }) => {
|
|
3
|
+
export const permissionRoutes = (method, { raise, refusalFor, hookOn, routeOn, current }) => {
|
|
4
4
|
const isBefore = method.startsWith('before');
|
|
5
5
|
if (method.endsWith('PermissionCheck')) {
|
|
6
|
+
const payloadOf = (e, below) => {
|
|
7
|
+
const { id, cwd } = current();
|
|
8
|
+
return { session: id, cwd, ...defined({ task: e.agentId, callId: e.tool_use_id, reason: below.reason, rule: below.rule, ceiling: e.ceiling === undefined ? undefined : verdictName(e.ceiling) }), name: e.tool, input: recordOf(e.input), decision: verdictName(below.decision) };
|
|
9
|
+
};
|
|
10
|
+
const failed = async (e, error, next) => {
|
|
11
|
+
const refusal = refusalFor(method, payloadOf(e, (await next(e))), error);
|
|
12
|
+
return refusal === undefined ? undefined : { decision: 'deny', reason: refusal.refuse };
|
|
13
|
+
};
|
|
6
14
|
return routeOn('tool.check', async (e, next) => {
|
|
7
15
|
const below = (await next(e));
|
|
8
|
-
const
|
|
9
|
-
const payload = { session: id, cwd, ...defined({ task: e.agentId, callId: e.tool_use_id, reason: below.reason, rule: below.rule, ceiling: e.ceiling === undefined ? undefined : verdictName(e.ceiling) }), name: e.tool, input: recordOf(e.input), decision: verdictName(below.decision) };
|
|
16
|
+
const payload = payloadOf(e, below);
|
|
10
17
|
if (!isBefore) {
|
|
11
18
|
void raise(method, payload);
|
|
12
19
|
return below;
|
|
@@ -16,11 +23,16 @@ export const permissionRoutes = (method, { raise, hookOn, routeOn, current }) =>
|
|
|
16
23
|
return below;
|
|
17
24
|
if (typeof answer['refuse'] === 'string')
|
|
18
25
|
return { decision: 'deny', reason: answer['refuse'] };
|
|
19
|
-
if (
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
26
|
+
if (!globalThis.cmodWithoutApprove) {
|
|
27
|
+
if (typeof answer['ask'] === 'string')
|
|
28
|
+
return { decision: 'ask', reason: answer['ask'] };
|
|
29
|
+
if (answer['allow'] === true)
|
|
30
|
+
return { decision: 'allow' };
|
|
31
|
+
}
|
|
32
|
+
return below;
|
|
33
|
+
}, isBefore ? failed : undefined);
|
|
23
34
|
}
|
|
35
|
+
const refused = (refuse, interrupt) => ({ decision: defined({ behavior: 'deny', message: refuse, interrupt }) });
|
|
24
36
|
return hookOn('PermissionRequest', async (input) => {
|
|
25
37
|
const payload = processPayloadOf('PermissionRequest', input);
|
|
26
38
|
if (!isBefore) {
|
|
@@ -31,7 +43,16 @@ export const permissionRoutes = (method, { raise, hookOn, routeOn, current }) =>
|
|
|
31
43
|
if (answer === undefined)
|
|
32
44
|
return undefined;
|
|
33
45
|
if (typeof answer['refuse'] === 'string')
|
|
34
|
-
return
|
|
35
|
-
|
|
36
|
-
|
|
46
|
+
return refused(answer['refuse'], answer['interrupt']);
|
|
47
|
+
if (!globalThis.cmodWithoutApprove) {
|
|
48
|
+
if (answer['allow'] === true)
|
|
49
|
+
return { decision: defined({ behavior: 'allow', updatedInput: answer['input'], updatedPermissions: answer['updates'] }) };
|
|
50
|
+
}
|
|
51
|
+
return undefined;
|
|
52
|
+
}, isBefore
|
|
53
|
+
? (input, error) => {
|
|
54
|
+
const refusal = refusalFor(method, processPayloadOf('PermissionRequest', input), error);
|
|
55
|
+
return refusal === undefined ? undefined : refused(refusal.refuse);
|
|
56
|
+
}
|
|
57
|
+
: undefined);
|
|
37
58
|
};
|
package/runtime/router.d.ts
CHANGED
|
@@ -3,9 +3,11 @@ import type { Disposable } from '../mod.js';
|
|
|
3
3
|
import type { RoutedEvent } from './hooks.js';
|
|
4
4
|
export type RouterNext<N extends RoutedEvent> = (e: Frozen<Args<N>>) => Promise<EventResult<N>>;
|
|
5
5
|
export type RoutedHook<N extends RoutedEvent> = (e: Frozen<Args<N>>, next: RouterNext<N>) => EventResult<N> | Promise<EventResult<N>>;
|
|
6
|
+
export type RoutedFailure<N extends RoutedEvent> = (e: Frozen<Args<N>>, error: Error, next: RouterNext<N>) => EventResult<N> | undefined | Promise<EventResult<N> | undefined>;
|
|
6
7
|
export type Router = {
|
|
7
|
-
add<N extends RoutedEvent>(event: N, hook: RoutedHook<N>): Disposable;
|
|
8
|
+
add<N extends RoutedEvent>(event: N, hook: RoutedHook<N>, failed?: RoutedFailure<N>): Disposable;
|
|
8
9
|
dispatch<N extends RoutedEvent>(event: N, e: Frozen<Args<N>>, next: RouterNext<N>): Promise<EventResult<N>>;
|
|
10
|
+
dispatchFailure<N extends RoutedEvent>(event: N, e: Frozen<Args<N>>, error: Error, next: RouterNext<N>): Promise<EventResult<N>>;
|
|
9
11
|
};
|
|
10
12
|
export declare function createRouter(): Router;
|
|
11
13
|
//# sourceMappingURL=router.d.ts.map
|
package/runtime/router.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"router.d.ts","sourceRoot":"","sources":["../src/runtime/router.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,WAAW,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AAC5D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,WAAW,CAAA;AAC3C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAA;AAE7C,MAAM,MAAM,UAAU,CAAC,CAAC,SAAS,WAAW,IAAI,CAAC,CAAC,EAAE,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,OAAO,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,CAAA;AAE/F,MAAM,MAAM,UAAU,CAAC,CAAC,SAAS,WAAW,IAAI,CAAC,CAAC,EAAE,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,UAAU,CAAC,CAAC,CAAC,KAAK,WAAW,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,CAAA;AAErI,MAAM,MAAM,MAAM,GAAG;IACnB,GAAG,CAAC,CAAC,SAAS,WAAW,EAAE,KAAK,EAAE,CAAC,EAAE,IAAI,EAAE,UAAU,CAAC,CAAC,CAAC,GAAG,UAAU,CAAA;
|
|
1
|
+
{"version":3,"file":"router.d.ts","sourceRoot":"","sources":["../src/runtime/router.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,WAAW,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AAC5D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,WAAW,CAAA;AAC3C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAA;AAE7C,MAAM,MAAM,UAAU,CAAC,CAAC,SAAS,WAAW,IAAI,CAAC,CAAC,EAAE,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,OAAO,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,CAAA;AAE/F,MAAM,MAAM,UAAU,CAAC,CAAC,SAAS,WAAW,IAAI,CAAC,CAAC,EAAE,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,UAAU,CAAC,CAAC,CAAC,KAAK,WAAW,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,CAAA;AAErI,MAAM,MAAM,aAAa,CAAC,CAAC,SAAS,WAAW,IAAI,CAAC,CAAC,EAAE,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,UAAU,CAAC,CAAC,CAAC,KAAK,WAAW,CAAC,CAAC,CAAC,GAAG,SAAS,GAAG,OAAO,CAAC,WAAW,CAAC,CAAC,CAAC,GAAG,SAAS,CAAC,CAAA;AAE9K,MAAM,MAAM,MAAM,GAAG;IACnB,GAAG,CAAC,CAAC,SAAS,WAAW,EAAE,KAAK,EAAE,CAAC,EAAE,IAAI,EAAE,UAAU,CAAC,CAAC,CAAC,EAAE,MAAM,CAAC,EAAE,aAAa,CAAC,CAAC,CAAC,GAAG,UAAU,CAAA;IAChG,QAAQ,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;IAC3G,eAAe,CAAC,CAAC,SAAS,WAAW,EAAE,KAAK,EAAE,CAAC,EAAE,CAAC,EAAE,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,UAAU,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,CAAA;CACjI,CAAA;AAID,wBAAgB,YAAY,IAAI,MAAM,CAgCrC"}
|
package/runtime/router.js
CHANGED
|
@@ -1,28 +1,36 @@
|
|
|
1
1
|
export function createRouter() {
|
|
2
|
-
const
|
|
2
|
+
const routes = new Map();
|
|
3
3
|
return {
|
|
4
|
-
add(event, hook) {
|
|
5
|
-
const
|
|
6
|
-
|
|
4
|
+
add(event, hook, failed) {
|
|
5
|
+
const route = { hook, ...(failed === undefined ? {} : { failed }) };
|
|
6
|
+
routes.set(event, [...(routes.get(event) ?? []), route]);
|
|
7
7
|
return {
|
|
8
8
|
dispose() {
|
|
9
|
-
const chain = (
|
|
9
|
+
const chain = (routes.get(event) ?? []).filter((each) => each !== route);
|
|
10
10
|
if (chain.length === 0)
|
|
11
|
-
|
|
11
|
+
routes.delete(event);
|
|
12
12
|
else
|
|
13
|
-
|
|
13
|
+
routes.set(event, chain);
|
|
14
14
|
},
|
|
15
15
|
};
|
|
16
16
|
},
|
|
17
17
|
dispatch(event, e, next) {
|
|
18
|
-
const chain =
|
|
18
|
+
const chain = routes.get(event);
|
|
19
19
|
const call = async (index, current) => {
|
|
20
|
-
const
|
|
21
|
-
if (
|
|
20
|
+
const route = chain?.[index];
|
|
21
|
+
if (route === undefined)
|
|
22
22
|
return next(current);
|
|
23
|
-
return hook(current, (passed) => call(index + 1, passed));
|
|
23
|
+
return route.hook(current, (passed) => call(index + 1, passed));
|
|
24
24
|
};
|
|
25
25
|
return call(0, e);
|
|
26
26
|
},
|
|
27
|
+
async dispatchFailure(event, e, error, next) {
|
|
28
|
+
for (const route of routes.get(event) ?? []) {
|
|
29
|
+
const answer = await route.failed?.(e, error, next);
|
|
30
|
+
if (answer !== undefined)
|
|
31
|
+
return answer;
|
|
32
|
+
}
|
|
33
|
+
return next(e);
|
|
34
|
+
},
|
|
27
35
|
};
|
|
28
36
|
}
|
package/runtime/sessions.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Args, CommandRunResult } from 'claude-code';
|
|
1
|
+
import type { Args, CommandRunResult, SessionCompactResult } from 'claude-code';
|
|
2
2
|
import type { Disposable } from '../mod.js';
|
|
3
3
|
import { ToolResult } from '../session/context/turn/message/tool-result.js';
|
|
4
4
|
import { type SessionChange, type SessionData, type TaskData } from '../session/conversation.js';
|
|
@@ -11,6 +11,8 @@ import type { PermissionRoutes } from './permission-check.js';
|
|
|
11
11
|
export type Subscription = {
|
|
12
12
|
readonly event: SessionEvent;
|
|
13
13
|
readonly matcher: SentMatcher | undefined;
|
|
14
|
+
readonly onFailure?: 'refuse';
|
|
15
|
+
readonly scope?: Scope;
|
|
14
16
|
};
|
|
15
17
|
export declare const everySession = "*";
|
|
16
18
|
export declare const conversationMethods: {
|
|
@@ -46,6 +48,9 @@ export type SessionsClient = {
|
|
|
46
48
|
readonly sessions: Sessions;
|
|
47
49
|
changed(session: string, change: SessionChange, isInProject: boolean): void;
|
|
48
50
|
answerBefore(session: string, event: BeforeEvent, change: SessionChange, isInProject: boolean): Promise<unknown>;
|
|
51
|
+
failedBefore(session: string, event: BeforeEvent, change: SessionChange, isInProject: boolean, error: Error): {
|
|
52
|
+
readonly refuse: string;
|
|
53
|
+
} | undefined;
|
|
49
54
|
raise(method: string, payload: Readonly<Record<string, unknown>>): Promise<unknown>;
|
|
50
55
|
listen(method: string, args: readonly unknown[]): Disposable;
|
|
51
56
|
/** An empty title keeps the title the session already has. */
|
|
@@ -56,7 +61,7 @@ export declare function payloadOf(change: SessionChange): unknown;
|
|
|
56
61
|
export declare function isHeardBy(event: SessionEvent, matcher: unknown, change: SessionChange): boolean;
|
|
57
62
|
export declare function isInScope(scope: Scope, session: string, change: SessionChange, isUnder: (task: string, ancestor: string) => boolean): boolean;
|
|
58
63
|
export declare function withoutPermissionCheck(name: string, hearer: string): string;
|
|
59
|
-
export type HookOn = (hook: string, listener: (input: unknown) => HookFields | undefined | Promise<HookFields | undefined
|
|
64
|
+
export type HookOn = (hook: string, listener: (input: unknown) => HookFields | undefined | Promise<HookFields | undefined>, failed?: (input: unknown, error: Error) => HookFields | undefined) => Disposable;
|
|
60
65
|
export declare function createSessionsClient({ name, claude, report, isGranted, hookOn, routeOn, permissionRoutes }: {
|
|
61
66
|
readonly name: string;
|
|
62
67
|
readonly claude: Claude;
|
|
@@ -74,8 +79,12 @@ export type ToolCheck = {
|
|
|
74
79
|
readonly ceiling?: 'allow' | 'ask' | 'deny';
|
|
75
80
|
};
|
|
76
81
|
export type RouteOn = {
|
|
77
|
-
(event: 'tool.check', hook: (e: ToolCheck, next: (e: ToolCheck) => Promise<unknown>) => Promise<unknown>)
|
|
78
|
-
|
|
82
|
+
(event: 'tool.check', hook: (e: ToolCheck, next: (e: ToolCheck) => Promise<unknown>) => Promise<unknown>, failed?: (e: ToolCheck, error: Error, next: (e: ToolCheck) => Promise<unknown>) => Promise<{
|
|
83
|
+
readonly decision: 'deny';
|
|
84
|
+
readonly reason: string;
|
|
85
|
+
} | undefined>): Disposable;
|
|
86
|
+
(event: 'command.run', hook: (e: Args<'command.run'>, next: (e: Args<'command.run'>) => Promise<CommandRunResult>) => Promise<CommandRunResult>, failed?: (e: Args<'command.run'>, error: Error) => Promise<CommandRunResult | undefined>): Disposable;
|
|
87
|
+
(event: 'session.compact', hook: (e: Args<'session.compact'>, next: (e: Args<'session.compact'>) => Promise<SessionCompactResult>) => Promise<SessionCompactResult>): Disposable;
|
|
79
88
|
};
|
|
80
89
|
export declare const recordOf: (value: unknown) => Readonly<Record<string, unknown>>;
|
|
81
90
|
export declare const defined: (fields: HookFields) => HookFields;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sessions.d.ts","sourceRoot":"","sources":["../src/runtime/sessions.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAA;
|
|
1
|
+
{"version":3,"file":"sessions.d.ts","sourceRoot":"","sources":["../src/runtime/sessions.ts"],"names":[],"mappings":"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"}
|