@cmodjs/core 2.0.0 → 2.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 +4 -4
- package/docs/dependencies.md +1 -1
- package/docs/jobs.md +1 -1
- package/docs/mod.md +2 -3
- package/docs/permissions.md +1 -1
- package/docs/session.md +3 -1
- package/docs/testing.md +2 -1
- package/jobs/permissions.d.ts.map +1 -1
- package/jobs/permissions.js +4 -1
- package/package.json +1 -1
- package/records.d.ts +1 -1
- package/records.js +1 -1
- package/register.d.ts.map +1 -1
- package/register.js +5 -4
- package/runtime/before.d.ts +8 -4
- package/runtime/before.d.ts.map +1 -1
- package/runtime/before.js +63 -39
- package/runtime/conversations.d.ts.map +1 -1
- package/runtime/conversations.js +38 -13
- package/runtime/engine.d.ts +1 -0
- package/runtime/engine.d.ts.map +1 -1
- package/runtime/hooks.d.ts +9 -23
- package/runtime/hooks.d.ts.map +1 -1
- package/runtime/hooks.js +4 -59
- package/runtime/lifecycle.d.ts +2 -1
- package/runtime/lifecycle.d.ts.map +1 -1
- package/runtime/lifecycle.js +36 -23
- package/runtime/permission-check.d.ts +15 -0
- package/runtime/permission-check.d.ts.map +1 -0
- package/runtime/permission-check.js +37 -0
- package/runtime/router.d.ts +0 -1
- package/runtime/router.d.ts.map +1 -1
- package/runtime/router.js +0 -1
- package/runtime/sessions.d.ts +13 -8
- package/runtime/sessions.d.ts.map +1 -1
- package/runtime/sessions.js +44 -80
- package/runtime/state.d.ts.map +1 -1
- package/runtime/state.js +14 -11
- 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 +5 -2
- package/src/jobs/permissions.ts +3 -1
- package/src/records.ts +1 -1
- package/src/register.ts +5 -4
- package/src/runtime/before.ts +66 -42
- package/src/runtime/conversations.ts +35 -13
- package/src/runtime/engine.ts +1 -0
- package/src/runtime/hooks.ts +13 -86
- package/src/runtime/lifecycle.ts +51 -37
- package/src/runtime/permission-check.ts +47 -0
- package/src/runtime/router.ts +0 -2
- package/src/runtime/sessions.ts +55 -84
- package/src/runtime/state.ts +15 -8
- package/src/session/context/turn/message/message.ts +6 -2
- package/src/testing.ts +17 -10
- package/testing.d.ts +7 -2
- package/testing.d.ts.map +1 -1
- package/testing.js +13 -7
package/runtime/sessions.js
CHANGED
|
@@ -10,7 +10,7 @@ import { GoalChange } from '../session/goal/goal.js';
|
|
|
10
10
|
import { TaskEnd, TaskStart } from '../session/goal/task.js';
|
|
11
11
|
import { beforeEvents, processEvents, Session, sessionEvents } from '../session/session.js';
|
|
12
12
|
import { matches, sentMatcher } from '../utils/matcher.js';
|
|
13
|
-
import { foldAnswers } from './before.js';
|
|
13
|
+
import { beforeAnswerKeys, foldAnswers, foldBefore, methodOf, takenAnswer } from './before.js';
|
|
14
14
|
import { cmodPluginName } from './dependencies.js';
|
|
15
15
|
import { messageOf } from '../utils/text.js';
|
|
16
16
|
import { listenerOf, PassedTimeout, withinTime } from './deadline.js';
|
|
@@ -36,7 +36,6 @@ const actPermissions = { send: 'prompt', startTask: 'tasks', usage: 'conversatio
|
|
|
36
36
|
const startsTurns = ['CronCreate'];
|
|
37
37
|
const ownSchedule = ['CronDelete', 'CronList'];
|
|
38
38
|
export const beforePermission = (event) => (event === 'toolCall' ? 'tools' : event === TaskStart ? 'tasks' : 'prompt');
|
|
39
|
-
const methodOf = (prefix, event) => `${prefix}${Object.entries(sessionEvents).find(([, each]) => each === event)?.[0] ?? event}`;
|
|
40
39
|
export const isAct = (action) => action === 'callTool' || action in actPermissions;
|
|
41
40
|
export function toolPermission(tool) {
|
|
42
41
|
return startsTurns.includes(tool) ? 'prompt' : ownSchedule.includes(tool) ? undefined : 'tools';
|
|
@@ -113,7 +112,10 @@ export function isInScope(scope, session, change, isUnder) {
|
|
|
113
112
|
return change.message.turn === scope.turn;
|
|
114
113
|
return (change.type === TurnStart || change.type === TurnEnd) && change.turn.prompt.id === scope.turn;
|
|
115
114
|
}
|
|
116
|
-
export function
|
|
115
|
+
export function withoutPermissionCheck(name, hearer) {
|
|
116
|
+
return `${name}: ${hearer} hears Claude Code's permission check, so hooks/register.ts must call registerPermissionCheck(addHook) after registerMod.`;
|
|
117
|
+
}
|
|
118
|
+
export function createSessionsClient({ name, claude, report, isGranted, hookOn, routeOn, permissionRoutes }) {
|
|
117
119
|
const call = (method, input) => claude.cmod.call({ to: cmodPluginName, method, input });
|
|
118
120
|
const listening = new Map();
|
|
119
121
|
const deciding = new Map();
|
|
@@ -176,6 +178,12 @@ export function createSessionsClient({ name, claude, report, isGranted, hookOn,
|
|
|
176
178
|
},
|
|
177
179
|
};
|
|
178
180
|
};
|
|
181
|
+
const invoke = (method, options, run, takes = []) => withinTime(claude.clock, method, options, run).catch((error) => {
|
|
182
|
+
if (error instanceof PassedTimeout && !method.startsWith('before'))
|
|
183
|
+
return undefined;
|
|
184
|
+
report(`session:${method}`, `The ${method} listener failed`, error);
|
|
185
|
+
return options.onFailure === 'refuse' && takes.includes('refuse') ? { refuse: `${name}: ${messageOf(error)}` } : undefined;
|
|
186
|
+
});
|
|
179
187
|
const combined = async (session, event, change, isInProject) => {
|
|
180
188
|
const askers = [...(deciding.get(session) ?? []), ...(isInProject ? (deciding.get(everySession) ?? []) : [])]
|
|
181
189
|
.filter((entry) => entry.event === event)
|
|
@@ -185,17 +193,12 @@ export function createSessionsClient({ name, claude, report, isGranted, hookOn,
|
|
|
185
193
|
if (entry.matcher !== undefined && !matches(payloadOf(latest), entry.matcher))
|
|
186
194
|
return undefined;
|
|
187
195
|
const payload = await payloadFor(session, latest);
|
|
188
|
-
const answer = await
|
|
189
|
-
if (entry.options.onFailure !== 'refuse')
|
|
190
|
-
throw error;
|
|
191
|
-
report(`session:${entry.method}`, `The ${entry.method} listener failed`, error);
|
|
192
|
-
return { refuse: `${name}: ${messageOf(error)}` };
|
|
193
|
-
});
|
|
196
|
+
const answer = await invoke(entry.method, entry.options, async (time) => takenAnswer(entry.method, beforeAnswerKeys[event], await entry.listener(payload, time)), beforeAnswerKeys[event]);
|
|
194
197
|
if (answer === undefined || answer === null || (typeof answer === 'object' && 'refuse' in answer) || isGranted(beforePermission(event)))
|
|
195
198
|
return answer;
|
|
196
199
|
throw new Error(`Its ${entry.method} answer changes nothing until ${beforePermission(event)} is turned on for ${name} in /mods.`);
|
|
197
200
|
});
|
|
198
|
-
return
|
|
201
|
+
return foldBefore(event, change, askers, (error) => report(`session:${methodOf('before', event)}`, `The ${methodOf('before', event)} listener failed`, error));
|
|
199
202
|
};
|
|
200
203
|
const eventMethods = (scopeOf, hasBefore, ended) => {
|
|
201
204
|
const method = (into, named, event) => (...args) => {
|
|
@@ -208,10 +211,12 @@ export function createSessionsClient({ name, claude, report, isGranted, hookOn,
|
|
|
208
211
|
...(hasBefore ? beforeEvents.map((named) => [`before${named}`, method(deciding, `before${named}`, sessionEvents[named])]) : []),
|
|
209
212
|
]);
|
|
210
213
|
};
|
|
214
|
+
let folders;
|
|
211
215
|
const withFiles = async (message) => {
|
|
212
216
|
if (message.type !== ToolCall && message.type !== ToolResult)
|
|
213
217
|
return message;
|
|
214
|
-
|
|
218
|
+
folders ??= Promise.all([claude.env.home(), claude.env.configHome()]).then(([home, configRoot]) => ({ home, configRoot }));
|
|
219
|
+
return { ...message, files: await callFiles(name, claude, folders, { tool: message.name, input: message.input }, message.cwd) };
|
|
215
220
|
};
|
|
216
221
|
const messagesFrom = async (method, input) => Promise.all((await call(method, input)).map(withFiles));
|
|
217
222
|
const searched = (request, matcher) => ({
|
|
@@ -405,39 +410,16 @@ export function createSessionsClient({ name, claude, report, isGranted, hookOn,
|
|
|
405
410
|
};
|
|
406
411
|
const processListeners = new Map();
|
|
407
412
|
const raise = async (method, payload) => {
|
|
408
|
-
const isBefore = method.startsWith('before');
|
|
409
413
|
const heard = [...(processListeners.get(method) ?? [])].filter(({ matcher }) => payload['task'] === undefined && (matcher === undefined || matches(payload, matcher)));
|
|
410
|
-
if (!
|
|
411
|
-
await Promise.all(heard.map(({ listener, options }) =>
|
|
412
|
-
if (!(error instanceof PassedTimeout))
|
|
413
|
-
report(`session:${method}`, `The ${method} listener failed`, error);
|
|
414
|
-
})));
|
|
414
|
+
if (!method.startsWith('before')) {
|
|
415
|
+
await Promise.all(heard.map(({ listener, options }) => invoke(method, options, (time) => listener(payload, time))));
|
|
415
416
|
return undefined;
|
|
416
417
|
}
|
|
418
|
+
const failing = method === 'beforePermissionRequest' ? { onFailure: 'refuse' } : {};
|
|
417
419
|
const takes = answerKeys[method.slice('before'.length)] ?? [];
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
try {
|
|
421
|
-
const answer = (await withinTime(claude.clock, method, options, (time) => listener(payload, time)));
|
|
422
|
-
if (answer === undefined || answer === null)
|
|
423
|
-
continue;
|
|
424
|
-
const unread = Object.keys(answer).filter((key) => !takes.includes(key));
|
|
425
|
-
if (unread.length > 0)
|
|
426
|
-
throw new Error(`it answered ${unread.join(' and ')}, which ${method} does not take. It takes ${takes.map((key) => `{ ${key} }`).join(', ') || 'no answer'}.`);
|
|
427
|
-
const notes = [merged?.['note'], answer['note']].filter((note) => typeof note === 'string');
|
|
428
|
-
merged = { ...merged, ...answer, ...(notes.length === 0 ? {} : { note: notes.join('\n\n') }) };
|
|
429
|
-
if (typeof answer['refuse'] === 'string')
|
|
430
|
-
return merged;
|
|
431
|
-
}
|
|
432
|
-
catch (error) {
|
|
433
|
-
report(`session:${method}`, `The ${method} listener failed`, error);
|
|
434
|
-
if (options.onFailure === 'refuse' || method === 'beforePermissionRequest')
|
|
435
|
-
return { refuse: `${name}: ${messageOf(error)}` };
|
|
436
|
-
}
|
|
437
|
-
}
|
|
438
|
-
return merged;
|
|
420
|
+
const askers = heard.map(({ listener, options }) => () => invoke(method, { ...options, ...failing }, async (time) => takenAnswer(method, takes, await listener(payload, time)), takes));
|
|
421
|
+
return foldAnswers(method, takes, ['refuse'], undefined, askers, (error) => report(`session:${method}`, `The ${method} listener failed`, error));
|
|
439
422
|
};
|
|
440
|
-
const permissionCheck = (e, below) => ({ session: current.id, cwd: current.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) });
|
|
441
423
|
const commandRun = async (e) => {
|
|
442
424
|
const source = (await claude.command.list()).find((command) => command.name === e.command)?.source;
|
|
443
425
|
return { session: current.id, cwd: current.cwd, name: e.command, args: e.args, ...defined({ source }), from: senderOf(e.origin, false) };
|
|
@@ -466,22 +448,11 @@ export function createSessionsClient({ name, claude, report, isGranted, hookOn,
|
|
|
466
448
|
if (hookOn === undefined || routeOn === undefined)
|
|
467
449
|
throw new Error(`${name}: ${method} works only in a mod's own session.`);
|
|
468
450
|
const isBefore = method.startsWith('before');
|
|
469
|
-
if (named === 'PermissionCheck') {
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
return below;
|
|
475
|
-
}
|
|
476
|
-
const answer = await raise(method, permissionCheck(e, below));
|
|
477
|
-
if (answer === undefined || below.decision === 'deny')
|
|
478
|
-
return below;
|
|
479
|
-
if (typeof answer['refuse'] === 'string')
|
|
480
|
-
return { decision: 'deny', reason: answer['refuse'] };
|
|
481
|
-
if (typeof answer['ask'] === 'string')
|
|
482
|
-
return { decision: 'ask', reason: answer['ask'] };
|
|
483
|
-
return answer['allow'] === true ? { decision: 'allow' } : below;
|
|
484
|
-
});
|
|
451
|
+
if (named === 'PermissionCheck' || named === 'PermissionRequest') {
|
|
452
|
+
const routes = permissionRoutes?.();
|
|
453
|
+
if (routes === undefined)
|
|
454
|
+
throw new Error(withoutPermissionCheck(name, method));
|
|
455
|
+
return routes(method, { raise, hookOn, routeOn, current: () => current });
|
|
485
456
|
}
|
|
486
457
|
const event = processEvents[named];
|
|
487
458
|
const hook = (isBefore ? event.before : undefined) ?? event.on;
|
|
@@ -493,7 +464,7 @@ export function createSessionsClient({ name, claude, report, isGranted, hookOn,
|
|
|
493
464
|
return undefined;
|
|
494
465
|
}
|
|
495
466
|
const answer = await raise(method, payload);
|
|
496
|
-
return answer === undefined ? undefined : hookAnswerOf(named,
|
|
467
|
+
return answer === undefined ? undefined : hookAnswerOf(named, answer);
|
|
497
468
|
});
|
|
498
469
|
};
|
|
499
470
|
const listen = (method, args) => {
|
|
@@ -529,16 +500,15 @@ export function createSessionsClient({ name, claude, report, isGranted, hookOn,
|
|
|
529
500
|
},
|
|
530
501
|
changed(session, change, isInProject) {
|
|
531
502
|
remember(session, change);
|
|
503
|
+
let payload;
|
|
532
504
|
for (const key of isInProject ? [session, everySession] : [session]) {
|
|
533
505
|
for (const { scope, event, method, matcher, listener, options } of listening.get(key) ?? []) {
|
|
534
506
|
if (!isHeardBy(event, matcher, change) || !isInScope(scope, session, change, isUnderIn(session)))
|
|
535
507
|
continue;
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
.
|
|
539
|
-
|
|
540
|
-
report(`session:${method}`, `The ${method} listener failed`, error);
|
|
541
|
-
});
|
|
508
|
+
payload ??= payloadFor(session, change);
|
|
509
|
+
void payload
|
|
510
|
+
.then((heard) => invoke(method, options, (time) => listener(heard, time)))
|
|
511
|
+
.catch((error) => report(`session:${method}`, `The ${method} listener failed`, error));
|
|
542
512
|
}
|
|
543
513
|
}
|
|
544
514
|
},
|
|
@@ -567,9 +537,8 @@ export function createSessionsClient({ name, claude, report, isGranted, hookOn,
|
|
|
567
537
|
},
|
|
568
538
|
};
|
|
569
539
|
}
|
|
570
|
-
const
|
|
571
|
-
const
|
|
572
|
-
const defined = (fields) => Object.fromEntries(Object.entries(fields).filter(([, value]) => value !== undefined));
|
|
540
|
+
export const recordOf = (value) => (typeof value === 'object' && value !== null && !Array.isArray(value) ? value : {});
|
|
541
|
+
export const defined = (fields) => Object.fromEntries(Object.entries(fields).filter(([, value]) => value !== undefined));
|
|
573
542
|
const placementOf = (input) => defined({ session: input['session_id'], task: input['agent_id'], agentType: input['agent_type'], cwd: input['cwd'], permissionMode: input['permission_mode'] });
|
|
574
543
|
const mcpServerOf = (server) => (server === undefined ? undefined : { name: String(recordOf(server)['name']), source: String(recordOf(server)['source']) });
|
|
575
544
|
const modelChangeOf = (input) => ({
|
|
@@ -653,30 +622,25 @@ export function processPayloadOf(named, input) {
|
|
|
653
622
|
const fields = recordOf(input);
|
|
654
623
|
return { ...placementOf(fields), ...defined(processFields[named](fields)) };
|
|
655
624
|
}
|
|
656
|
-
|
|
657
|
-
const output = (fields) => ({ hookSpecificOutput: { hookEventName: hook, ...defined(fields) } });
|
|
625
|
+
function hookAnswerOf(named, answer) {
|
|
658
626
|
const refuse = typeof answer['refuse'] === 'string' ? answer['refuse'] : undefined;
|
|
627
|
+
const note = typeof answer['note'] === 'string' ? [answer['note']] : undefined;
|
|
659
628
|
if (named === 'SessionStart')
|
|
660
|
-
return
|
|
629
|
+
return defined({ additionalContext: note, sessionTitle: answer['title'], reloadSkills: answer['reloadSkills'] === true ? true : undefined, initialUserMessage: answer['firstPrompt'] });
|
|
661
630
|
if (named === 'ModelChange') {
|
|
662
631
|
if (refuse !== undefined)
|
|
663
|
-
return
|
|
664
|
-
return typeof answer['ask'] === 'string' ?
|
|
632
|
+
return { permissionDecision: 'deny', permissionDecisionReason: refuse };
|
|
633
|
+
return typeof answer['ask'] === 'string' ? { permissionDecision: 'ask', permissionDecisionReason: answer['ask'] } : undefined;
|
|
665
634
|
}
|
|
666
635
|
if (named === 'WorktreeCreate')
|
|
667
|
-
return
|
|
668
|
-
if (named === 'PermissionRequest') {
|
|
669
|
-
if (refuse !== undefined)
|
|
670
|
-
return output({ decision: defined({ behavior: 'deny', message: refuse, interrupt: answer['interrupt'] }) });
|
|
671
|
-
return answer['allow'] === true ? output({ decision: defined({ behavior: 'allow', updatedInput: answer['input'], updatedPermissions: answer['updates'] }) }) : undefined;
|
|
672
|
-
}
|
|
636
|
+
return defined({ worktreePath: answer['result'] });
|
|
673
637
|
if (named === 'PermissionDenial')
|
|
674
|
-
return
|
|
638
|
+
return answer['retry'] === true ? { retry: true } : undefined;
|
|
675
639
|
if (named === 'ToolBatch')
|
|
676
|
-
return
|
|
640
|
+
return defined({ additionalContext: note });
|
|
677
641
|
if (named === 'ReplyDisplay')
|
|
678
|
-
return
|
|
679
|
-
return refuse === undefined ? undefined : {
|
|
642
|
+
return defined({ displayContent: answer['text'] });
|
|
643
|
+
return refuse === undefined ? undefined : { block: refuse };
|
|
680
644
|
}
|
|
681
645
|
export function resultOf(where, call, answered) {
|
|
682
646
|
const base = { id: '', type: ToolResult, at: new Date().toISOString(), session: where.session, context: where.context, ...(where.turn === undefined ? {} : { turn: where.turn.prompt.id }), callId: '', name: call.name, input: call.input, cwd: call.cwd, files: noFiles };
|
package/runtime/state.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"state.d.ts","sourceRoot":"","sources":["../src/runtime/state.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAA;AACpD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AACzC,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAA;AAQ3C,MAAM,MAAM,YAAY,GAAG;IACzB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,MAAM,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,IAAI,CAAA;IAC3C,QAAQ,CAAC,OAAO,EAAE,MAAM,IAAI,CAAA;IAC5B,QAAQ,CAAC,UAAU,EAAE,MAAM,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAAA;CACvD,CAAA;AAED,MAAM,MAAM,QAAQ,CAAC,KAAK,SAAS,MAAM,IAAI;IAC3C,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAA;IACrB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,OAAO,IAAI,IAAI,CAAA;IACf,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;IACrB,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IACnC,aAAa,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,iBAAiB,CAAC,cAAc,CAAC,CAAC,QAAQ,CAAC,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;CACvH,CAAA;AAQD,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,IAAI,GAAG,IAAI,CAIvE;AAED,wBAAsB,YAAY,CAAC,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAO9H;AAED,wBAAgB,WAAW,CAAC,KAAK,SAAS,MAAM,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,EAAE,YAAY,GAAG,QAAQ,CAAC,KAAK,CAAC,
|
|
1
|
+
{"version":3,"file":"state.d.ts","sourceRoot":"","sources":["../src/runtime/state.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAA;AACpD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AACzC,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAA;AAQ3C,MAAM,MAAM,YAAY,GAAG;IACzB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,MAAM,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,IAAI,CAAA;IAC3C,QAAQ,CAAC,OAAO,EAAE,MAAM,IAAI,CAAA;IAC5B,QAAQ,CAAC,UAAU,EAAE,MAAM,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAAA;CACvD,CAAA;AAED,MAAM,MAAM,QAAQ,CAAC,KAAK,SAAS,MAAM,IAAI;IAC3C,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAA;IACrB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,OAAO,IAAI,IAAI,CAAA;IACf,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;IACrB,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IACnC,aAAa,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,iBAAiB,CAAC,cAAc,CAAC,CAAC,QAAQ,CAAC,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;CACvH,CAAA;AAQD,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,IAAI,GAAG,IAAI,CAIvE;AAED,wBAAsB,YAAY,CAAC,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAO9H;AAED,wBAAgB,WAAW,CAAC,KAAK,SAAS,MAAM,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,EAAE,YAAY,GAAG,QAAQ,CAAC,KAAK,CAAC,CAoH9J"}
|
package/runtime/state.js
CHANGED
|
@@ -46,17 +46,18 @@ export function createState({ name, initial, session, root, claude, report, chan
|
|
|
46
46
|
const transcriptsKey = `${name}.$transcripts.`;
|
|
47
47
|
const forgetEndedSessions = async (current) => {
|
|
48
48
|
const stored = await claude.store.keys();
|
|
49
|
-
|
|
50
|
-
const owner = recorded.slice(transcriptsKey.length);
|
|
51
|
-
if (owner === current)
|
|
52
|
-
continue;
|
|
49
|
+
const ended = async (recorded) => {
|
|
53
50
|
const transcript = await claude.store.get(recorded);
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
51
|
+
return typeof transcript !== 'string' || !(await claude.fs.exists(transcript));
|
|
52
|
+
};
|
|
53
|
+
const owners = stored.filter((key) => key.startsWith(transcriptsKey) && key !== `${transcriptsKey}${current}`);
|
|
54
|
+
await Promise.all(owners.map(async (recorded) => {
|
|
55
|
+
if (!(await ended(recorded)))
|
|
56
|
+
return;
|
|
57
|
+
const owner = recorded.slice(transcriptsKey.length);
|
|
58
|
+
await Promise.all(stored.filter((key) => key.startsWith(`${name}.`) && key.endsWith(`.sessions.${owner}`)).map((value) => claude.store.delete(value)));
|
|
58
59
|
await claude.store.delete(recorded);
|
|
59
|
-
}
|
|
60
|
+
}));
|
|
60
61
|
};
|
|
61
62
|
const loadGroups = async (only, next) => {
|
|
62
63
|
const loaded = [];
|
|
@@ -114,8 +115,10 @@ export function createState({ name, initial, session, root, claude, report, chan
|
|
|
114
115
|
}),
|
|
115
116
|
moveTo: (nextRoot) => inTurn(async () => (nextRoot === projectRoot ? undefined : loadGroups(['project'], { root: nextRoot, session: sessionId }))),
|
|
116
117
|
switchSession: (nextSession, source, transcript) => inTurn(async () => {
|
|
117
|
-
|
|
118
|
-
|
|
118
|
+
if (Object.keys(declared.session).length > 0) {
|
|
119
|
+
await claude.store.set(`${transcriptsKey}${nextSession}`, transcript);
|
|
120
|
+
void forgetEndedSessions(nextSession).catch((error) => report({ id: 'state:sessions', message: 'Could not forget the session values of ended sessions', error }));
|
|
121
|
+
}
|
|
119
122
|
if (nextSession === sessionId)
|
|
120
123
|
return;
|
|
121
124
|
if (source === 'fork')
|
|
@@ -35,6 +35,7 @@ export type Row = {
|
|
|
35
35
|
};
|
|
36
36
|
export declare const messageTypes: readonly MessageType[];
|
|
37
37
|
export declare const skillPrefix = "Base directory for this skill:";
|
|
38
|
+
export declare function instructionPathOf(text: string): string;
|
|
38
39
|
export declare function rowOfLine(row: Readonly<Record<string, unknown>>): Row | undefined;
|
|
39
40
|
export declare function rowOfAppend(append: {
|
|
40
41
|
readonly uuid: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"message.d.ts","sourceRoot":"","sources":["../../../../src/session/context/turn/message/message.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"message.d.ts","sourceRoot":"","sources":["../../../../src/session/context/turn/message/message.ts"],"names":[],"mappings":"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"}
|
|
@@ -13,7 +13,10 @@ export const Message = 'message';
|
|
|
13
13
|
export const messageTypes = [Prompt, Note, Reply, ToolCall, ToolResult, Skill, InstructionFile, Notice];
|
|
14
14
|
export const skillPrefix = 'Base directory for this skill:';
|
|
15
15
|
const rowTypes = ['user', 'assistant', 'attachment', 'system'];
|
|
16
|
-
const memoryPath = /^Contents of (.+?) \(
|
|
16
|
+
const memoryPath = /^Contents of (.+?)(?: \([^)\n]*\))?:\r?$/m;
|
|
17
|
+
export function instructionPathOf(text) {
|
|
18
|
+
return memoryPath.exec(text)?.[1] ?? '';
|
|
19
|
+
}
|
|
17
20
|
export function rowOfLine(row) {
|
|
18
21
|
const { uuid, timestamp, type } = row;
|
|
19
22
|
if (typeof uuid !== 'string' || typeof timestamp !== 'string' || typeof type !== 'string' || !rowTypes.includes(type))
|
|
@@ -39,7 +42,7 @@ export function rowOfLine(row) {
|
|
|
39
42
|
export function rowOfAppend(append, at, cwd) {
|
|
40
43
|
const { message, origin } = append;
|
|
41
44
|
const text = textOf(message.content);
|
|
42
|
-
const path = message.type === 'attachment' ?
|
|
45
|
+
const path = message.type === 'attachment' ? instructionPathOf(text) || undefined : undefined;
|
|
43
46
|
const model = isObject(origin) && origin['kind'] === 'model' && typeof origin['model'] === 'string' ? origin['model'] : undefined;
|
|
44
47
|
return {
|
|
45
48
|
id: append.uuid,
|
package/src/jobs/permissions.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { Args, Frozen } from 'claude-code'
|
|
|
2
2
|
import type { Job, Mod } from '../mod.js'
|
|
3
3
|
import type { Deadline } from '../runtime/deadline.js'
|
|
4
4
|
import { engineOf } from '../runtime/engine.js'
|
|
5
|
+
import { withoutPermissionCheck } from '../runtime/sessions.js'
|
|
5
6
|
import { inputOf } from '../utils/call-effects.js'
|
|
6
7
|
import { decidePermission, stricterVerdict, type PermissionRules as RulesFor, type Rule as RuleFor, type Verdict } from './permissions/decide-permission.js'
|
|
7
8
|
import { targetOf } from './permissions/match-target.js'
|
|
@@ -20,7 +21,8 @@ export function permissions<State extends object = Record<never, never>>(rules:
|
|
|
20
21
|
if (count === 0) throw new Error('permissions: give it a deny or an ask rule, or remove it from setup.')
|
|
21
22
|
|
|
22
23
|
return (job) => {
|
|
23
|
-
const { router, toolCalls } = engineOf(job.mod)
|
|
24
|
+
const { router, toolCalls, hasPermissionCheck } = engineOf(job.mod)
|
|
25
|
+
if (!hasPermissionCheck()) throw new Error(withoutPermissionCheck(job.mod.name, 'the permissions job'))
|
|
24
26
|
const readWorkspace = workspaceReader(job)
|
|
25
27
|
|
|
26
28
|
const verdictOf = async (e: Frozen<Args<'tool.check'>>): Promise<Verdict | undefined> => {
|
package/src/records.ts
CHANGED
|
@@ -259,7 +259,7 @@ function readKeys(keys: unknown): Steps {
|
|
|
259
259
|
return { keys: commands }
|
|
260
260
|
}
|
|
261
261
|
|
|
262
|
-
export const coreVersion = '2.
|
|
262
|
+
export const coreVersion = '2.1.0'
|
|
263
263
|
|
|
264
264
|
export function oldestCmodFor(steps: Steps): string {
|
|
265
265
|
if (steps.permissions?.some((item) => item.startsWith('config:')) === true) return '1.6.0'
|
package/src/register.ts
CHANGED
|
@@ -5,10 +5,11 @@ import type { Claude } from './runtime/claude.js'
|
|
|
5
5
|
import type { RoutedEvent } from './runtime/hooks.js'
|
|
6
6
|
import { cmodPluginName } from './runtime/dependencies.js'
|
|
7
7
|
import { createLifecycle, readPlugin, type Lifecycle } from './runtime/lifecycle.js'
|
|
8
|
+
import { permissionRoutes, type PermissionRoutes } from './runtime/permission-check.js'
|
|
8
9
|
|
|
9
10
|
let lifecycle: Lifecycle<object>
|
|
10
11
|
|
|
11
|
-
let
|
|
12
|
+
let permissionCheck: PermissionRoutes | undefined
|
|
12
13
|
|
|
13
14
|
let registered: ModDefinition<object> | undefined
|
|
14
15
|
|
|
@@ -99,9 +100,9 @@ function routeCallToMod(_$: EngineInterface, eventInput: Frozen<Args<'cmod.call'
|
|
|
99
100
|
}
|
|
100
101
|
|
|
101
102
|
export function registerMod<State extends object, Declared extends Options>(addHook: On, definition: ModDefinition<State, string, Declared>, options: PluginOptions): void {
|
|
102
|
-
|
|
103
|
+
permissionCheck = undefined
|
|
103
104
|
registered = definition as ModDefinition<object>
|
|
104
|
-
lifecycle = createLifecycle(definition, () =>
|
|
105
|
+
lifecycle = createLifecycle(definition, () => permissionCheck, options)
|
|
105
106
|
addHook('session.start', startMod)
|
|
106
107
|
addHook('classic.Setup', routeToMod)
|
|
107
108
|
addHook('classic.SessionStart', routeToMod)
|
|
@@ -154,7 +155,7 @@ export function registerMod<State extends object, Declared extends Options>(addH
|
|
|
154
155
|
}
|
|
155
156
|
|
|
156
157
|
export function registerPermissionCheck(addHook: On): void {
|
|
157
|
-
|
|
158
|
+
permissionCheck = permissionRoutes
|
|
158
159
|
addHook('tool.check', routeToMod)
|
|
159
160
|
addHook('classic.PermissionRequest', routeToMod)
|
|
160
161
|
}
|
package/src/runtime/before.ts
CHANGED
|
@@ -2,7 +2,7 @@ import type { Args, Frozen } from 'claude-code'
|
|
|
2
2
|
import type { Disposable } from '../mod.js'
|
|
3
3
|
import { ContextEnd } from '../session/context/context.js'
|
|
4
4
|
import { InstructionFile, isRulePath } from '../session/context/turn/message/instruction-file.js'
|
|
5
|
-
import { Message, messagesOf, noFiles, rowOfAppend, withTexts, type Unplaced } from '../session/context/turn/message/message.js'
|
|
5
|
+
import { instructionPathOf, Message, messagesOf, noFiles, rowOfAppend, withTexts, type Unplaced } from '../session/context/turn/message/message.js'
|
|
6
6
|
import { Note } from '../session/context/turn/message/note.js'
|
|
7
7
|
import { Notice } from '../session/context/turn/message/notice.js'
|
|
8
8
|
import { Prompt } from '../session/context/turn/message/prompt.js'
|
|
@@ -12,13 +12,14 @@ import { Skill } from '../session/context/turn/message/skill.js'
|
|
|
12
12
|
import { ToolCall } from '../session/context/turn/message/tool-call.js'
|
|
13
13
|
import { ToolResult } from '../session/context/turn/message/tool-result.js'
|
|
14
14
|
import { TurnEnd } from '../session/context/turn/turn.js'
|
|
15
|
-
import type { ConversationState, SessionChange, TaskData } from '../session/conversation.js'
|
|
15
|
+
import type { ContextData, ConversationState, RunningTask, SessionChange, TaskData } from '../session/conversation.js'
|
|
16
16
|
import { Task, TaskStart } from '../session/goal/task.js'
|
|
17
|
-
import type
|
|
17
|
+
import { sessionEvents, type BeforeEvent, type SessionEvent } from '../session/session.js'
|
|
18
18
|
import type { Claude } from './claude.js'
|
|
19
19
|
import { cmodPluginName } from './dependencies.js'
|
|
20
20
|
import type { Router } from './router.js'
|
|
21
21
|
import { toolInputOf } from './tool-calls.js'
|
|
22
|
+
import { resolve } from '../vendor.js'
|
|
22
23
|
|
|
23
24
|
export type Place = Pick<ConversationState, 'session' | 'context' | 'contextOpenedAt' | 'turn' | 'tasks' | 'taskTurns' | 'openCalls'>
|
|
24
25
|
|
|
@@ -66,6 +67,7 @@ export function installBefore({ claude, router, hooks }: { readonly claude: Clau
|
|
|
66
67
|
}
|
|
67
68
|
const titles = new Map<string, string>()
|
|
68
69
|
const spawned = new Map<string, string>()
|
|
70
|
+
const agentFolders = new Map<string, string>()
|
|
69
71
|
const toolResultHook = (isFailure: boolean) => async (e: Readonly<Record<string, unknown>>, next: (e: never) => Promise<unknown>) => {
|
|
70
72
|
if (!(await wants(ToolResult))) return next(e as never)
|
|
71
73
|
const where = await place()
|
|
@@ -121,7 +123,7 @@ export function installBefore({ claude, router, hooks }: { readonly claude: Clau
|
|
|
121
123
|
router.add('prompt.attachment', async (e, next) => {
|
|
122
124
|
if (!(await wants(InstructionFile)) || e.type !== 'nested_memory') return next(e)
|
|
123
125
|
const where = await place()
|
|
124
|
-
const path =
|
|
126
|
+
const path = instructionPathOf(e.text)
|
|
125
127
|
const answer = await asked(InstructionFile, message(where, { id: '', at: now(), type: InstructionFile, path, text: e.text, isRule: isRulePath(path) }, e.agentId))
|
|
126
128
|
if (answer.refuse !== undefined) return { text: null }
|
|
127
129
|
return answer.text === undefined ? next(e) : { text: answer.text }
|
|
@@ -145,7 +147,8 @@ export function installBefore({ claude, router, hooks }: { readonly claude: Clau
|
|
|
145
147
|
if (!(await wants(ToolCall))) return next(e)
|
|
146
148
|
const where = await place()
|
|
147
149
|
const input = toolInputOf(e)
|
|
148
|
-
const
|
|
150
|
+
const cwd = (e.agentId === undefined ? undefined : agentFolders.get(e.agentId)) ?? (await claude.session.cwd())
|
|
151
|
+
const answer = await asked(ToolCall, message(where, { id: e.tool_use_id, at: now(), type: ToolCall, callId: e.tool_use_id, name: e.tool, input, cwd, files: noFiles }, e.agentId))
|
|
149
152
|
if (answer.refuse !== undefined) return { deny: answer.refuse }
|
|
150
153
|
if (answer.result !== undefined) return { result: answer.result }
|
|
151
154
|
if (answer.input === undefined) return next(e)
|
|
@@ -154,7 +157,13 @@ export function installBefore({ claude, router, hooks }: { readonly claude: Clau
|
|
|
154
157
|
}),
|
|
155
158
|
router.add('agent.spawn', async (e, next) => {
|
|
156
159
|
spawned.set(e.tool_use_id, e.prompt)
|
|
157
|
-
|
|
160
|
+
const start = async (args: typeof e) => {
|
|
161
|
+
const started = await next(args)
|
|
162
|
+
const folder = args.isTeammate === true ? undefined : (args.cwd ?? (args.parentAgentId === undefined ? undefined : agentFolders.get(args.parentAgentId)))
|
|
163
|
+
if (started.agentId !== undefined && folder !== undefined) agentFolders.set(started.agentId, resolve(await claude.session.cwd(), folder))
|
|
164
|
+
return started
|
|
165
|
+
}
|
|
166
|
+
if (!(await wants(TaskStart))) return start(e)
|
|
158
167
|
const where = await place()
|
|
159
168
|
const task: TaskData = { id: '', title: e.description, agentType: e.subagentType, model: e.model ?? e.parentModel, openedBy: e.tool_use_id, openedAt: now(), ...(e.parentAgentId === undefined ? {} : { parent: e.parentAgentId }) }
|
|
160
169
|
const answer = await asked(TaskStart, { type: TaskStart, task: { ...task, context: where.context } })
|
|
@@ -162,7 +171,7 @@ export function installBefore({ claude, router, hooks }: { readonly claude: Clau
|
|
|
162
171
|
spawned.delete(e.tool_use_id)
|
|
163
172
|
return { deny: answer.refuse }
|
|
164
173
|
}
|
|
165
|
-
return
|
|
174
|
+
return start(answer.owner === undefined ? e : { ...e, model: answer.owner.model })
|
|
166
175
|
}),
|
|
167
176
|
router.add('classic.SubagentStart', async (e, next) => {
|
|
168
177
|
const where = await place()
|
|
@@ -177,12 +186,16 @@ export function installBefore({ claude, router, hooks }: { readonly claude: Clau
|
|
|
177
186
|
return withNote(await next(e), answer.note)
|
|
178
187
|
}),
|
|
179
188
|
router.add('classic.SubagentStop', async (e, next) => {
|
|
180
|
-
|
|
189
|
+
const stopped = <Answer extends { readonly block?: string }>(answer: Answer) => {
|
|
190
|
+
if (answer.block === undefined) agentFolders.delete(e.agent_id)
|
|
191
|
+
return answer
|
|
192
|
+
}
|
|
193
|
+
if (!(await wants(TurnEnd))) return stopped(await next(e))
|
|
181
194
|
const turn = (await place()).taskTurns[e.agent_id]
|
|
182
|
-
if (turn === undefined) return next(e)
|
|
195
|
+
if (turn === undefined) return stopped(await next(e))
|
|
183
196
|
const answer = await asked(TurnEnd, { type: TurnEnd, turn: { ...turn, closedAt: now() } })
|
|
184
197
|
const below = await next(e)
|
|
185
|
-
return answer.refuse === undefined ? below : { ...below, block: answer.refuse }
|
|
198
|
+
return stopped(answer.refuse === undefined ? below : { ...below, block: answer.refuse })
|
|
186
199
|
}),
|
|
187
200
|
router.add('classic.Stop', async (e, next) => {
|
|
188
201
|
if (!(await wants(TurnEnd))) return next(e)
|
|
@@ -197,14 +210,17 @@ export function installBefore({ claude, router, hooks }: { readonly claude: Clau
|
|
|
197
210
|
const where = await place()
|
|
198
211
|
const task = e.agentId === undefined ? undefined : where.tasks[e.agentId]
|
|
199
212
|
if (e.agentId !== undefined && task === undefined) return next(e)
|
|
200
|
-
const
|
|
201
|
-
const answer = await asked(ContextEnd, { type: ContextEnd, context: { ...context, closedAt: now() } })
|
|
213
|
+
const answer = await asked(ContextEnd, { type: ContextEnd, context: closingContext(where, task, now()) })
|
|
202
214
|
return answer.refuse === undefined ? next(e) : { skip: answer.refuse }
|
|
203
215
|
}),
|
|
204
216
|
]
|
|
205
217
|
return { dispose: () => added.forEach((each) => each.dispose()) }
|
|
206
218
|
}
|
|
207
219
|
|
|
220
|
+
export function closingContext(where: Place, task: RunningTask | undefined, closedAt: string): ContextData {
|
|
221
|
+
return task === undefined ? { number: where.context, openedAt: where.contextOpenedAt, closedAt } : { number: task.context, openedAt: task.contextOpenedAt ?? task.openedAt, closedAt, task: task.id }
|
|
222
|
+
}
|
|
223
|
+
|
|
208
224
|
export function withAnswer(change: SessionChange, answer: Partial<Answers>): SessionChange {
|
|
209
225
|
if (change.type === Message && answer.output !== undefined && change.message.type === ToolResult) return { ...change, message: { ...change.message, output: answer.output } }
|
|
210
226
|
if (change.type === Message && answer.text !== undefined) return { ...change, message: { ...change.message, text: answer.text } as Message }
|
|
@@ -213,41 +229,49 @@ export function withAnswer(change: SessionChange, answer: Partial<Answers>): Ses
|
|
|
213
229
|
return change
|
|
214
230
|
}
|
|
215
231
|
|
|
216
|
-
export
|
|
232
|
+
export const methodOf = (prefix: 'on' | 'before', event: SessionEvent): string => `${prefix}${Object.entries(sessionEvents).find(([, each]) => each === event)?.[0] ?? event}`
|
|
233
|
+
|
|
234
|
+
export const beforeAnswerKeys: Readonly<Record<BeforeEvent, readonly (keyof Answers)[]>> = {
|
|
235
|
+
prompt: ['text', 'note', 'title', 'refuse'],
|
|
236
|
+
note: ['text'],
|
|
237
|
+
skill: ['text'],
|
|
238
|
+
instructionFile: ['text', 'refuse'],
|
|
239
|
+
reply: ['text'],
|
|
240
|
+
toolCall: ['input', 'result', 'refuse'],
|
|
241
|
+
toolResult: ['output', 'isError', 'note'],
|
|
242
|
+
notice: ['text'],
|
|
243
|
+
taskStart: ['owner', 'refuse'],
|
|
244
|
+
turnEnd: ['refuse'],
|
|
245
|
+
contextEnd: ['refuse'],
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export function foldBefore(event: BeforeEvent, change: SessionChange, askers: readonly ((change: SessionChange) => Promise<unknown>)[], report: (error: unknown) => void): Promise<Partial<Answers> | undefined> {
|
|
249
|
+
return foldAnswers(methodOf('before', event), beforeAnswerKeys[event], ['refuse', 'result'], change, askers, report, withAnswer)
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
export function takenAnswer(method: string, takes: readonly string[], answer: unknown): unknown {
|
|
253
|
+
if (answer === undefined || answer === null || answer === false) return undefined
|
|
254
|
+
if (typeof answer !== 'object' || Array.isArray(answer)) throw new Error(`it answered ${JSON.stringify(answer)}, but ${method} takes an object or nothing: ${takes.map((key) => `{ ${key} }`).join(', ') || 'no answer'}.`)
|
|
255
|
+
const unread = Object.keys(answer).filter((key) => !takes.includes(key))
|
|
256
|
+
if (unread.length > 0) throw new Error(`it answered ${unread.join(' and ')}, which ${method} does not take. It takes ${takes.map((key) => `{ ${key} }`).join(', ') || 'no answer'}.`)
|
|
257
|
+
return answer
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
export async function foldAnswers<Change, Answer extends object>(method: string, takes: readonly string[], ends: readonly string[], change: Change, askers: readonly ((change: Change) => Promise<unknown>)[], report: (error: unknown) => void, applied: (change: Change, answer: Answer) => Change = (same) => same): Promise<Answer | undefined> {
|
|
217
261
|
let latest = change
|
|
218
|
-
let result:
|
|
262
|
+
let result: Record<string, unknown> | undefined
|
|
219
263
|
for (const ask of askers) {
|
|
220
264
|
try {
|
|
221
|
-
const answer =
|
|
222
|
-
if (answer ===
|
|
223
|
-
const
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
265
|
+
const answer = takenAnswer(method, takes, await ask(latest))
|
|
266
|
+
if (typeof answer !== 'object' || answer === null) continue
|
|
267
|
+
const given = answer as Record<string, unknown>
|
|
268
|
+
const notes = [result?.['note'], given['note']].filter((note) => typeof note === 'string')
|
|
269
|
+
result = { ...result, ...given, ...(notes.length === 0 ? {} : { note: notes.join('\n\n') }) }
|
|
270
|
+
if (ends.some((key) => given[key] !== undefined)) return result as Answer
|
|
271
|
+
latest = applied(latest, answer as Answer)
|
|
227
272
|
} catch (error) {
|
|
228
273
|
report(error)
|
|
229
274
|
}
|
|
230
275
|
}
|
|
231
|
-
return result
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
export function answerOf(event: BeforeEvent, answer: unknown): Partial<Answers> | undefined {
|
|
235
|
-
if (typeof answer !== 'object' || answer === null) return undefined
|
|
236
|
-
const given = answer as Partial<Answers>
|
|
237
|
-
const allowed: Readonly<Record<BeforeEvent, readonly (keyof Answers)[]>> = {
|
|
238
|
-
prompt: ['text', 'note', 'title', 'refuse'],
|
|
239
|
-
note: ['text'],
|
|
240
|
-
skill: ['text'],
|
|
241
|
-
instructionFile: ['text', 'refuse'],
|
|
242
|
-
reply: ['text'],
|
|
243
|
-
toolCall: ['input', 'result', 'refuse'],
|
|
244
|
-
toolResult: ['output', 'isError', 'note'],
|
|
245
|
-
notice: ['text'],
|
|
246
|
-
taskStart: ['owner', 'refuse'],
|
|
247
|
-
turnEnd: ['refuse'],
|
|
248
|
-
contextEnd: ['refuse'],
|
|
249
|
-
}
|
|
250
|
-
const unread = Object.keys(given).filter((key) => !allowed[event].includes(key as keyof Answers))
|
|
251
|
-
if (unread.length > 0) throw new Error(`a before(${event}) listener answered ${unread.join(' and ')}, which ${event} does not take. It takes ${allowed[event].map((key) => `{ ${key} }`).join(', ')}.`)
|
|
252
|
-
return given
|
|
276
|
+
return result as Answer | undefined
|
|
253
277
|
}
|