@cmodjs/core 0.1.10 → 0.1.12
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 +16 -13
- package/docs/dependencies.md +8 -0
- package/docs/hooks.md +10 -1
- package/docs/index.md +4 -3
- package/docs/install-steps.md +19 -3
- package/docs/jobs.md +23 -7
- package/docs/mod.md +58 -3
- package/docs/testing.md +27 -3
- package/jobs/context.d.ts +2 -2
- package/jobs/context.d.ts.map +1 -1
- package/jobs/context.js +2 -2
- package/jobs/permissions/decide-permission.d.ts +2 -2
- package/jobs/permissions/decide-permission.d.ts.map +1 -1
- package/jobs/permissions/match-target.d.ts +3 -2
- package/jobs/permissions/match-target.d.ts.map +1 -1
- package/jobs/permissions/match-target.js +24 -15
- package/jobs/permissions.d.ts +1 -1
- package/jobs/permissions.d.ts.map +1 -1
- package/jobs/permissions.js +18 -6
- package/jobs/prompt.d.ts.map +1 -1
- package/jobs/prompt.js +34 -3
- package/mod.d.ts +11 -2
- package/mod.d.ts.map +1 -1
- package/package.json +1 -1
- package/records.d.ts +4 -0
- package/records.d.ts.map +1 -1
- package/records.js +27 -6
- package/register.d.ts.map +1 -1
- package/register.js +3 -0
- package/runtime/claude.d.ts +7 -0
- package/runtime/claude.d.ts.map +1 -1
- package/runtime/hooks.js +4 -4
- package/runtime/lifecycle.d.ts +1 -0
- package/runtime/lifecycle.d.ts.map +1 -1
- package/runtime/lifecycle.js +80 -10
- package/runtime/state.d.ts +2 -1
- package/runtime/state.d.ts.map +1 -1
- package/runtime/state.js +12 -1
- package/src/jobs/check.ts +2 -2
- package/src/jobs/context.ts +5 -5
- package/src/jobs/permissions/decide-permission.ts +4 -4
- package/src/jobs/permissions/match-target.ts +24 -16
- package/src/jobs/permissions.ts +18 -7
- package/src/jobs/prompt.ts +31 -3
- package/src/mod.ts +15 -1
- package/src/records.ts +28 -8
- package/src/register.ts +3 -0
- package/src/runtime/claude.ts +3 -0
- package/src/runtime/hooks.ts +5 -5
- package/src/runtime/lifecycle.ts +85 -10
- package/src/runtime/state.ts +13 -1
- package/src/testing/fake-claude.ts +19 -3
- package/src/testing/fake-elements.ts +6 -1
- package/src/testing.ts +1 -0
- package/src/utils/call-effects.ts +13 -11
- package/testing/fake-claude.d.ts +9 -0
- package/testing/fake-claude.d.ts.map +1 -1
- package/testing/fake-claude.js +13 -2
- package/testing/fake-elements.js +6 -1
- package/testing.d.ts.map +1 -1
- package/testing.js +1 -0
- package/utils/call-effects.d.ts +4 -8
- package/utils/call-effects.d.ts.map +1 -1
- package/utils/call-effects.js +6 -3
package/runtime/lifecycle.js
CHANGED
|
@@ -30,12 +30,12 @@ export async function readPlugin(claude) {
|
|
|
30
30
|
const manifest = (await readJson(read, `${root}/.claude-plugin/plugin.json`));
|
|
31
31
|
const version = typeof manifest?.version === 'string' ? manifest.version : undefined;
|
|
32
32
|
const store = storeFolder({ HOME: await claude.env.home(), XDG_DATA_HOME: await claude.env.dataHome() });
|
|
33
|
-
const plugin = { name, root, version, store };
|
|
34
33
|
if (name === cmodPluginName) {
|
|
35
34
|
const installed = await cmodVersion(claude);
|
|
36
|
-
return {
|
|
35
|
+
return { name, root, version, store, keys: {}, isInstalled: version !== undefined && installed !== undefined && isAtLeast(installed, version), shouldRecord: false };
|
|
37
36
|
}
|
|
38
37
|
const steps = readSteps(await readJson(read, `${root}/package.json`)) ?? {};
|
|
38
|
+
const plugin = { name, root, version, store, keys: steps.keys ?? {} };
|
|
39
39
|
const record = await readRecord(read, store, name);
|
|
40
40
|
if (Object.keys(steps).length === 0)
|
|
41
41
|
return { ...plugin, isInstalled: true, shouldRecord: record?.version !== version };
|
|
@@ -136,7 +136,7 @@ export function createLifecycle(definition, checksPermissions = () => true) {
|
|
|
136
136
|
if (runtime === undefined || plugin === undefined)
|
|
137
137
|
return;
|
|
138
138
|
const activeRouter = createRouter();
|
|
139
|
-
const active = await createMod(definition, { ...runtime, router: activeRouter, dataFolder: dataFolder(plugin.store, plugin.name), checksPermissions }).catch((error) => {
|
|
139
|
+
const active = await createMod(definition, { ...runtime, router: activeRouter, dataFolder: dataFolder(plugin.store, plugin.name), keys: plugin.keys, checksPermissions }).catch((error) => {
|
|
140
140
|
fail(messageOf(error), 'Fix it, then run /reload-plugins.');
|
|
141
141
|
throw error;
|
|
142
142
|
});
|
|
@@ -171,8 +171,12 @@ export function createLifecycle(definition, checksPermissions = () => true) {
|
|
|
171
171
|
return;
|
|
172
172
|
}
|
|
173
173
|
showLine().wait('Waiting for your answer');
|
|
174
|
-
const
|
|
175
|
-
|
|
174
|
+
const actions = [
|
|
175
|
+
...(event.install === '' ? [] : [`runs ${event.install} to install`]),
|
|
176
|
+
...(event.uninstall === '' ? [] : [`runs ${event.uninstall} when you remove it`]),
|
|
177
|
+
...(event.keys === '' ? [] : [`binds ${event.keys}`]),
|
|
178
|
+
];
|
|
179
|
+
const answer = await claude().ui.ask(`${name} ${listed(actions)}. Install it now?`, { options: ['Install', 'Not now'], header: 'Install' });
|
|
176
180
|
if (answer === 'Install')
|
|
177
181
|
return install(event.sha256);
|
|
178
182
|
phase = 'declined';
|
|
@@ -320,7 +324,8 @@ export function createLifecycle(definition, checksPermissions = () => true) {
|
|
|
320
324
|
};
|
|
321
325
|
}
|
|
322
326
|
async function createMod(definition, runtime) {
|
|
323
|
-
const {
|
|
327
|
+
const { router, progress } = runtime;
|
|
328
|
+
const claude = checkingKeys(runtime.claude, definition.name, runtime.keys);
|
|
324
329
|
const added = [];
|
|
325
330
|
const hookEvents = [];
|
|
326
331
|
const announce = (feature) => {
|
|
@@ -337,15 +342,30 @@ async function createMod(definition, runtime) {
|
|
|
337
342
|
name: definition.name,
|
|
338
343
|
state: modState.state,
|
|
339
344
|
dataFolder: runtime.dataFolder,
|
|
340
|
-
on(event, hook) {
|
|
345
|
+
on(event, hook, options) {
|
|
346
|
+
const bounded = options?.timeoutMs === undefined ? hook : timedHook(definition.name, event, hook, checkedMs(definition.name, `the ${event} hook's timeoutMs`, options.timeoutMs), claude);
|
|
341
347
|
if (!hookEvents.includes(event))
|
|
342
348
|
hookEvents.push(event);
|
|
343
349
|
if (event === 'PreToolUse')
|
|
344
|
-
router.add('tool.call', preToolUseHook(definition.name,
|
|
350
|
+
router.add('tool.call', preToolUseHook(definition.name, bounded, claude, agents, held));
|
|
345
351
|
else
|
|
346
|
-
router.add(`classic.${event}`, classicHook(definition.name, event,
|
|
352
|
+
router.add(`classic.${event}`, classicHook(definition.name, event, bounded, claude, agents));
|
|
347
353
|
},
|
|
348
354
|
use: (job) => job({ mod, claude, on: (event, hook) => router.add(event, hook), announce, reserveName, toolCalls: agents }),
|
|
355
|
+
every(ms, hook) {
|
|
356
|
+
let isRunning = false;
|
|
357
|
+
return claude.clock.every(checkedMs(definition.name, 'mod.every', ms), () => {
|
|
358
|
+
if (isRunning)
|
|
359
|
+
return;
|
|
360
|
+
isRunning = true;
|
|
361
|
+
void Promise.resolve()
|
|
362
|
+
.then(hook)
|
|
363
|
+
.catch((error) => claude.ui.log(`${definition.name}: the every ${ms} ms hook failed: ${messageOf(error)}`))
|
|
364
|
+
.finally(() => {
|
|
365
|
+
isRunning = false;
|
|
366
|
+
});
|
|
367
|
+
});
|
|
368
|
+
},
|
|
349
369
|
ui: area.ui,
|
|
350
370
|
process: {
|
|
351
371
|
run: (argv, init) => claude.process.run(argv, init),
|
|
@@ -360,8 +380,21 @@ async function createMod(definition, runtime) {
|
|
|
360
380
|
},
|
|
361
381
|
http: { fetch: (url, init) => claude.http.fetch(url, init) },
|
|
362
382
|
settings: { read: (args) => claude.settings.read(args) },
|
|
363
|
-
session: {
|
|
383
|
+
session: {
|
|
384
|
+
messages: claude.session.messages,
|
|
385
|
+
async append(text) {
|
|
386
|
+
const added = await claude.session.append({ message: { type: 'user', content: [{ type: 'text', text }] } });
|
|
387
|
+
if (added.deny !== undefined)
|
|
388
|
+
throw new Error(`${definition.name}: Claude Code refused the note: ${added.deny}`);
|
|
389
|
+
},
|
|
390
|
+
async submit(text) {
|
|
391
|
+
const submitted = await claude.prompt.submit({ text });
|
|
392
|
+
if ('drop' in submitted && typeof submitted.drop === 'string')
|
|
393
|
+
throw new Error(`${definition.name}: Claude Code dropped the prompt: ${submitted.drop}`);
|
|
394
|
+
},
|
|
395
|
+
},
|
|
364
396
|
agent: { spawn: (args) => claude.agent.spawn(args) },
|
|
397
|
+
model: { complete: (request, options) => claude.model.complete(request, options) },
|
|
365
398
|
get projectRoot() {
|
|
366
399
|
return modState.root;
|
|
367
400
|
},
|
|
@@ -448,6 +481,43 @@ async function createMod(definition, runtime) {
|
|
|
448
481
|
added.push(`${hookEvents.length === 1 ? 'a hook' : 'hooks'} on ${listed(hookEvents)}`);
|
|
449
482
|
return { mod, added };
|
|
450
483
|
}
|
|
484
|
+
function checkingKeys(claude, name, keys) {
|
|
485
|
+
const bound = Object.entries(keys);
|
|
486
|
+
if (bound.length === 0)
|
|
487
|
+
return claude;
|
|
488
|
+
const register = (command) => {
|
|
489
|
+
for (const [key, commandName] of bound) {
|
|
490
|
+
if (commandName === command.name && command.immediate !== true) {
|
|
491
|
+
claude.ui.log(`${name}: ${key} runs /${command.name}, which waits for Claude's turn to end and adds a row to the conversation. Give /${command.name} immediate: true.`);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
return claude.command.register(command);
|
|
495
|
+
};
|
|
496
|
+
return { ...claude, command: { ...claude.command, register } };
|
|
497
|
+
}
|
|
498
|
+
const longestTimerMs = 2_147_483_647;
|
|
499
|
+
function checkedMs(name, subject, ms) {
|
|
500
|
+
if (!Number.isInteger(ms) || ms <= 0 || ms > longestTimerMs)
|
|
501
|
+
throw new Error(`${name}: ${subject} is ${ms}. Give a whole number of milliseconds above 0 and at most ${longestTimerMs}.`);
|
|
502
|
+
return ms;
|
|
503
|
+
}
|
|
504
|
+
function timedHook(name, event, hook, ms, claude) {
|
|
505
|
+
return (input) => new Promise((resolve, reject) => {
|
|
506
|
+
const timer = claude.clock.after(ms, () => {
|
|
507
|
+
claude.ui.log(`${name}: the ${event} hook passed its ${ms / 1000} s timeout`);
|
|
508
|
+
resolve(undefined);
|
|
509
|
+
});
|
|
510
|
+
Promise.resolve()
|
|
511
|
+
.then(() => hook(input))
|
|
512
|
+
.then((answer) => {
|
|
513
|
+
timer.cancel();
|
|
514
|
+
resolve(answer);
|
|
515
|
+
}, (error) => {
|
|
516
|
+
timer.cancel();
|
|
517
|
+
reject(error);
|
|
518
|
+
});
|
|
519
|
+
});
|
|
520
|
+
}
|
|
451
521
|
async function failsAs(subject, step) {
|
|
452
522
|
try {
|
|
453
523
|
await step();
|
package/runtime/state.d.ts
CHANGED
|
@@ -15,5 +15,6 @@ export type ModState<State extends object> = {
|
|
|
15
15
|
moveTo(root: string): Promise<void>;
|
|
16
16
|
switchSession(session: string, source: ClassicHookInputs['SessionStart']['source']): Promise<void>;
|
|
17
17
|
};
|
|
18
|
-
export declare function
|
|
18
|
+
export declare function onStateChange(state: object, listener: () => void): void;
|
|
19
|
+
export declare function createState<State extends object>({ name, initial, session, root, claude, changed: redraw }: StateOptions): ModState<State>;
|
|
19
20
|
//# sourceMappingURL=state.d.ts.map
|
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;AAGpD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AAQzC,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,OAAO,EAAE,MAAM,IAAI,CAAA;CAC7B,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,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,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;CACnG,CAAA;
|
|
1
|
+
{"version":3,"file":"state.d.ts","sourceRoot":"","sources":["../src/runtime/state.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAA;AAGpD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AAQzC,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,OAAO,EAAE,MAAM,IAAI,CAAA;CAC7B,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,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,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;CACnG,CAAA;AAUD,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,IAAI,GAAG,IAAI,CAIvE;AAED,wBAAgB,WAAW,CAAC,KAAK,SAAS,MAAM,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,EAAE,YAAY,GAAG,QAAQ,CAAC,KAAK,CAAC,CAyI1I"}
|
package/runtime/state.js
CHANGED
|
@@ -3,7 +3,18 @@ import { messageOf } from '../utils/text.js';
|
|
|
3
3
|
const lifetimes = ['memory', 'session', 'project', 'global'];
|
|
4
4
|
const savedLifetimes = ['session', 'project', 'global'];
|
|
5
5
|
const keptPerValue = 20;
|
|
6
|
-
|
|
6
|
+
const listeners = new WeakMap();
|
|
7
|
+
export function onStateChange(state, listener) {
|
|
8
|
+
const own = listeners.get(state) ?? new Set();
|
|
9
|
+
own.add(listener);
|
|
10
|
+
listeners.set(state, own);
|
|
11
|
+
}
|
|
12
|
+
export function createState({ name, initial, session, root, claude, changed: redraw }) {
|
|
13
|
+
const changed = () => {
|
|
14
|
+
redraw();
|
|
15
|
+
for (const listener of listeners.get(state) ?? [])
|
|
16
|
+
listener();
|
|
17
|
+
};
|
|
7
18
|
const declared = declaredGroups(name, initial);
|
|
8
19
|
const current = copied(declared);
|
|
9
20
|
let defaults = declared;
|
package/src/jobs/check.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Job } from '../mod.js'
|
|
2
2
|
import { longestMs, type Deadline } from '../runtime/deadline.js'
|
|
3
|
-
import { callEffects, type
|
|
3
|
+
import { callEffects, type ToolCall } from '../utils/call-effects.js'
|
|
4
4
|
import { dirname } from '../vendor.js'
|
|
5
5
|
import { listed, messageOf } from '../utils/text.js'
|
|
6
6
|
import { afterCall, modOf, targetWords } from './context.js'
|
|
@@ -39,7 +39,7 @@ export function check<State extends object = Record<never, never>>(options: { re
|
|
|
39
39
|
}
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
-
async function triggeredRun(after: readonly Target[], run: readonly string[], use:
|
|
42
|
+
async function triggeredRun(after: readonly Target[], run: readonly string[], use: ToolCall, workspace: Workspace): Promise<{ command: string[]; folder: string } | undefined> {
|
|
43
43
|
const effects = callEffects(use, workspace)
|
|
44
44
|
const paths = new Set<string>()
|
|
45
45
|
let commandFolder: string | undefined
|
package/src/jobs/context.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { beforeDeadline, type Deadline } from '../runtime/deadline.js'
|
|
|
5
5
|
import { dependencyCalls } from '../runtime/dependencies.js'
|
|
6
6
|
import { toolInputOf } from '../runtime/tool-calls.js'
|
|
7
7
|
import { listed } from '../utils/text.js'
|
|
8
|
-
import type { ToolCall
|
|
8
|
+
import type { ToolCall } from '../utils/call-effects.js'
|
|
9
9
|
import { findProjectScope, type Workspace } from './permissions/find-project-scope.js'
|
|
10
10
|
import { targetOf, type Target } from './permissions/match-target.js'
|
|
11
11
|
import type { FileSystem } from '../utils/paths.js'
|
|
@@ -36,7 +36,7 @@ async function readSession(claude: Claude): Promise<Session> {
|
|
|
36
36
|
|
|
37
37
|
export function afterCall<State extends object>(
|
|
38
38
|
job: JobContext<State>,
|
|
39
|
-
contextAfter: (
|
|
39
|
+
contextAfter: (call: ToolCall, workspace: Workspace) => Promise<readonly string[]>,
|
|
40
40
|
failed: (error: unknown) => readonly string[],
|
|
41
41
|
): void {
|
|
42
42
|
const readWorkspace = workspaceReader(job)
|
|
@@ -50,7 +50,7 @@ export function afterCall<State extends object>(
|
|
|
50
50
|
})
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
-
async function useOf({ toolCalls }: JobContext, e: Frozen<Args<'tool.call'>>): Promise<
|
|
53
|
+
async function useOf({ toolCalls }: JobContext, e: Frozen<Args<'tool.call'>>): Promise<ToolCall> {
|
|
54
54
|
return { tool: e.tool, input: toolInputOf(e), ...(await toolCalls.agentOf(e.tool_use_id)) }
|
|
55
55
|
}
|
|
56
56
|
|
|
@@ -109,8 +109,8 @@ function spawnWithin(claude: Claude, deadline: Deadline, started: Promise<Stream
|
|
|
109
109
|
}
|
|
110
110
|
|
|
111
111
|
export function targetWords(target: Target): string {
|
|
112
|
-
const { key, patterns } = targetOf(target)
|
|
113
|
-
const named = listed(patterns)
|
|
112
|
+
const { key, patterns, exclusions } = targetOf(target)
|
|
113
|
+
const named = exclusions.length === 0 ? listed(patterns) : `${listed(patterns)} except ${listed(exclusions)}`
|
|
114
114
|
if (key === 'command') return patterns.includes('*') ? 'every shell command' : named
|
|
115
115
|
if (key === 'write') return `edits to ${named}`
|
|
116
116
|
if (key === 'read') return `reads of ${named}`
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { strictness } from '../../runtime/hooks.js'
|
|
2
2
|
import { messageOf } from '../../utils/text.js'
|
|
3
|
-
import { callBaseOf, callEffects, type CallEffects, type ToolCall
|
|
3
|
+
import { callBaseOf, callEffects, type CallEffects, type ToolCall } from '../../utils/call-effects.js'
|
|
4
4
|
import type { Workspace } from './find-project-scope.js'
|
|
5
5
|
import { folderInScope, matchTarget, targetOf, type MatchedCall, type Target, type TargetCalls, type TargetKey, type TargetOf } from './match-target.js'
|
|
6
6
|
import { resolve } from '../../vendor.js'
|
|
@@ -22,7 +22,7 @@ export function stricterVerdict<Result extends Verdict>(first: Result, second: R
|
|
|
22
22
|
return strictness.indexOf(second.decision) > strictness.indexOf(first.decision) ? second : first
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
export async function decidePermission<Mod>(rules: PermissionRules<Mod>, use:
|
|
25
|
+
export async function decidePermission<Mod>(rules: PermissionRules<Mod>, use: ToolCall, workspace: Workspace, modOf: (call: ToolCall, folder: string) => Mod): Promise<Verdict | undefined> {
|
|
26
26
|
try {
|
|
27
27
|
const effects = callEffects(use, workspace)
|
|
28
28
|
for (const decision of ['deny', 'ask'] as const) {
|
|
@@ -39,7 +39,7 @@ export async function decidePermission<Mod>(rules: PermissionRules<Mod>, use: To
|
|
|
39
39
|
}
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
-
async function ruleOutcome<Mod>(rule: Rule<Mod>, use:
|
|
42
|
+
async function ruleOutcome<Mod>(rule: Rule<Mod>, use: ToolCall, effects: CallEffects, workspace: Workspace, modOf: (call: ToolCall, folder: string) => Mod): Promise<{ failure: string | undefined } | undefined> {
|
|
43
43
|
const matched = await matchTarget(rule, use, effects, workspace)
|
|
44
44
|
const calls = matched.length > 0 ? matched : await unparsedMatches(rule, use, effects, workspace)
|
|
45
45
|
if (calls.length === 0) return undefined
|
|
@@ -56,7 +56,7 @@ async function ruleOutcome<Mod>(rule: Rule<Mod>, use: ToolUse, effects: CallEffe
|
|
|
56
56
|
return undefined
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
-
async function unparsedMatches(target: Target, use:
|
|
59
|
+
async function unparsedMatches(target: Target, use: ToolCall, effects: CallEffects, workspace: Workspace): Promise<MatchedCall[]> {
|
|
60
60
|
const shell = effects.shell
|
|
61
61
|
if (shell === undefined || shell.isFullyParsed) return []
|
|
62
62
|
const { key, patterns } = targetOf(target)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { matchesGlob } from '../../utils/matches-glob.js'
|
|
2
2
|
import { parseShell } from '../../utils/parse-shell.js'
|
|
3
|
-
import { callBaseOf, type CallBase, type CallEffects, type CommandCall, type FetchCall, type FileAccess, type FileCall, type ToolCall
|
|
3
|
+
import { callBaseOf, type CallBase, type CallEffects, type CommandCall, type FetchCall, type FileAccess, type FileCall, type ToolCall } from '../../utils/call-effects.js'
|
|
4
4
|
import type { Workspace } from './find-project-scope.js'
|
|
5
5
|
import { realPathOf, relativePath } from '../../utils/paths.js'
|
|
6
6
|
import { basename } from '../../vendor.js'
|
|
@@ -13,45 +13,53 @@ export type MatchedCall = { call: ToolCall; access: FileAccess | undefined; fold
|
|
|
13
13
|
|
|
14
14
|
const targetKeys: readonly TargetKey[] = ['command', 'read', 'write', 'fetch', 'subagent', 'tool']
|
|
15
15
|
|
|
16
|
-
export function targetOf(target: Target): { key: TargetKey; patterns: string[] } {
|
|
16
|
+
export function targetOf(target: Target): { key: TargetKey; patterns: string[]; exclusions: string[] } {
|
|
17
17
|
const keys = targetKeys.filter((key) => key in target)
|
|
18
18
|
const key = keys[0]
|
|
19
19
|
const value: unknown = key === undefined ? undefined : (target as Partial<Record<TargetKey, unknown>>)[key]
|
|
20
20
|
if (key === undefined || keys.length > 1 || !(typeof value === 'string' || (Array.isArray(value) && value.every((pattern) => typeof pattern === 'string')))) {
|
|
21
21
|
throw new Error(`A rule names exactly one of ${targetKeys.join(', ')} with a pattern or a list of patterns; this one names ${keys.join(' and ') || 'none'}.`)
|
|
22
22
|
}
|
|
23
|
-
|
|
23
|
+
const all = typeof value === 'string' ? [value] : value
|
|
24
|
+
const patterns = all.filter((pattern) => !pattern.startsWith('!'))
|
|
25
|
+
if (patterns.length === 0) throw new Error(`A ${key} target needs a pattern without "!" for its "!" patterns to leave calls out of, such as ['**', ${all.map((pattern) => `'${pattern}'`).join(', ')}].`)
|
|
26
|
+
return { key, patterns, exclusions: all.filter((pattern) => pattern.startsWith('!')).map((pattern) => pattern.slice(1)) }
|
|
24
27
|
}
|
|
25
28
|
|
|
26
|
-
export async function matchTarget(target: Target,
|
|
27
|
-
const { key, patterns } = targetOf(target)
|
|
28
|
-
const base = callBaseOf(
|
|
29
|
-
const only = (
|
|
29
|
+
export async function matchTarget(target: Target, call: CallBase, effects: CallEffects, workspace: Workspace): Promise<MatchedCall[]> {
|
|
30
|
+
const { key, patterns, exclusions } = targetOf(target)
|
|
31
|
+
const base = callBaseOf(call)
|
|
32
|
+
const only = (matched: ToolCall, folder = workspace.cwd): MatchedCall[] => [{ call: matched, access: undefined, folder }]
|
|
33
|
+
const isNamed = (value: string) => matchesAny(value, patterns) && !matchesAny(value, exclusions)
|
|
30
34
|
if (key === 'read' || key === 'write') {
|
|
31
35
|
const matched: MatchedCall[] = []
|
|
32
36
|
for (const access of key === 'read' ? effects.reads : effects.writes) {
|
|
33
37
|
const path = await matchedPath(patterns, access.path, workspace)
|
|
34
|
-
|
|
38
|
+
const isExcluded = path !== undefined && exclusions.length > 0 && (await matchedPath(exclusions, access.path, workspace)) !== undefined
|
|
39
|
+
if (path !== undefined && !isExcluded) matched.push({ call: { ...base, path }, access, folder: workspace.cwd })
|
|
35
40
|
}
|
|
36
41
|
return matched
|
|
37
42
|
}
|
|
38
43
|
if (key === 'command') {
|
|
39
44
|
const shell = effects.shell
|
|
40
45
|
if (shell === undefined) return []
|
|
41
|
-
const
|
|
42
|
-
|
|
46
|
+
const commandCall = { ...base, commands: shell.commands.map((command) => command.argv), isFullyParsed: shell.isFullyParsed }
|
|
47
|
+
const isEvery = patterns.includes('*')
|
|
48
|
+
if (isEvery && exclusions.length === 0) {
|
|
43
49
|
const folder = await folderInScope(shell, workspace)
|
|
44
|
-
return folder === undefined ? [] : only(
|
|
50
|
+
return folder === undefined ? [] : only(commandCall, folder)
|
|
45
51
|
}
|
|
46
|
-
const wanted = patterns.map(commandPattern)
|
|
52
|
+
const wanted = patterns.filter((pattern) => pattern !== '*').map(commandPattern)
|
|
53
|
+
const unwanted = exclusions.map(commandPattern)
|
|
47
54
|
for (const command of shell.commands) {
|
|
48
|
-
|
|
55
|
+
const isWanted = isEvery || wanted.some((pattern) => commandMatches(pattern, command.argv))
|
|
56
|
+
if (isWanted && !unwanted.some((pattern) => commandMatches(pattern, command.argv)) && (await isInScope(command.folder, workspace))) return only(commandCall, command.folder)
|
|
49
57
|
}
|
|
50
58
|
return []
|
|
51
59
|
}
|
|
52
|
-
if (key === 'fetch') return effects.urls.filter(
|
|
53
|
-
if (key === 'subagent') return effects.subagent !== undefined &&
|
|
54
|
-
return
|
|
60
|
+
if (key === 'fetch') return effects.urls.filter(isNamed).map((url) => ({ call: { ...base, url }, access: undefined, folder: workspace.cwd }))
|
|
61
|
+
if (key === 'subagent') return effects.subagent !== undefined && isNamed(effects.subagent) ? only(base) : []
|
|
62
|
+
return isNamed(call.tool) ? only(base) : []
|
|
55
63
|
}
|
|
56
64
|
|
|
57
65
|
function commandPattern(pattern: string): [string, ...string[]] {
|
package/src/jobs/permissions.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
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
|
-
import
|
|
4
|
+
import { inputOf } from '../utils/call-effects.js'
|
|
5
5
|
import { decidePermission, stricterVerdict, type PermissionRules as RulesFor, type Rule as RuleFor, type Verdict } from './permissions/decide-permission.js'
|
|
6
6
|
import { targetOf } from './permissions/match-target.js'
|
|
7
7
|
import { messageOf } from '../utils/text.js'
|
|
@@ -14,19 +14,24 @@ export type PermissionRules<State extends object = Record<never, never>> = Rules
|
|
|
14
14
|
|
|
15
15
|
const deadline: Deadline = { ms: 2000, job: 'permissions' }
|
|
16
16
|
|
|
17
|
-
export function permissions<State extends object = Record<never, never>>(rules: PermissionRules<State>): Job<void, State> {
|
|
18
|
-
const count =
|
|
17
|
+
export function permissions<State extends object = Record<never, never>>(rules: PermissionRules<State> | ((state: State) => PermissionRules<State>)): Job<void, State> {
|
|
18
|
+
const count = typeof rules === 'function' ? undefined : checkedCount(rules)
|
|
19
19
|
if (count === 0) throw new Error('permissions: give it a deny or an ask rule, or remove it from setup.')
|
|
20
|
-
for (const rule of [...(rules.deny ?? []), ...(rules.ask ?? [])]) targetOf(rule)
|
|
21
20
|
|
|
22
21
|
return (job) => {
|
|
23
22
|
const readWorkspace = workspaceReader(job)
|
|
24
23
|
|
|
25
24
|
const verdictOf = async (e: Frozen<Args<'tool.check'>>): Promise<Verdict | undefined> => {
|
|
25
|
+
let current: PermissionRules<State>
|
|
26
|
+
try {
|
|
27
|
+
current = typeof rules === 'function' ? rules(job.mod.state as State) : rules
|
|
28
|
+
checkedCount(current)
|
|
29
|
+
} catch (error) {
|
|
30
|
+
return { decision: 'deny', reason: `The permission rules of ${job.mod.name} failed, so it denies the call: ${messageOf(error)}` }
|
|
31
|
+
}
|
|
26
32
|
try {
|
|
27
33
|
const [agent, workspace] = await Promise.all([job.toolCalls.agentOf(e.tool_use_id), readWorkspace()])
|
|
28
|
-
|
|
29
|
-
return await decidePermission(rules, use, workspace, (call, folder) => modOf(job, { call, folder }, workspace, deadline))
|
|
34
|
+
return await decidePermission(current, { tool: e.tool, input: inputOf(e.input), ...agent }, workspace, (call, folder) => modOf(job, { call, folder }, workspace, deadline))
|
|
30
35
|
} catch (error) {
|
|
31
36
|
return { decision: 'deny', reason: `The permissions job could not read the session, so it denies the call: ${messageOf(error)}` }
|
|
32
37
|
}
|
|
@@ -36,6 +41,12 @@ export function permissions<State extends object = Record<never, never>>(rules:
|
|
|
36
41
|
const [below, ours] = await Promise.all([next(e), verdictOf(e)])
|
|
37
42
|
return ours === undefined ? below : stricterVerdict<Verdict>(below, ours)
|
|
38
43
|
})
|
|
39
|
-
job.announce(`${count} permission rule${count === 1 ? '' : 's'}`)
|
|
44
|
+
job.announce(count === undefined ? 'permission rules from its state' : `${count} permission rule${count === 1 ? '' : 's'}`)
|
|
40
45
|
}
|
|
41
46
|
}
|
|
47
|
+
|
|
48
|
+
function checkedCount<Mod>(rules: RulesFor<Mod>): number {
|
|
49
|
+
const all = [...(rules.deny ?? []), ...(rules.ask ?? [])]
|
|
50
|
+
for (const rule of all) targetOf(rule)
|
|
51
|
+
return all.length
|
|
52
|
+
}
|
package/src/jobs/prompt.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Job, JobContext, Mod } from '../mod.js'
|
|
2
2
|
import { longestMs, type Deadline } from '../runtime/deadline.js'
|
|
3
|
+
import { onStateChange } from '../runtime/state.js'
|
|
3
4
|
import { listed, messageOf } from '../utils/text.js'
|
|
4
5
|
import { callEffects, type ToolCall } from '../utils/call-effects.js'
|
|
5
6
|
import { matchTarget, targetOf, type Target } from './permissions/match-target.js'
|
|
@@ -48,21 +49,48 @@ export function prompt<State extends object = Record<never, never>>({ name, prom
|
|
|
48
49
|
addAfterCalls(job, targets, headedText, log)
|
|
49
50
|
job.announce(`the ${name} prompt after ${listed(targets.map(targetWords))}`)
|
|
50
51
|
} else {
|
|
51
|
-
|
|
52
|
+
addKeptCurrent(job, name, () => textFor({}, modWithin(job, deadline)), log)
|
|
52
53
|
job.announce(`the ${name} prompt`)
|
|
53
54
|
}
|
|
54
55
|
}
|
|
55
56
|
}
|
|
56
57
|
|
|
57
|
-
function
|
|
58
|
+
function addKeptCurrent<State extends object>(job: JobContext<State>, name: string, textFor: () => Promise<string | undefined>, log: (reason: string) => void): void {
|
|
59
|
+
let given: string | undefined
|
|
60
|
+
let hasStarted = false
|
|
61
|
+
let isQueued = false
|
|
62
|
+
let updates = Promise.resolve()
|
|
63
|
+
|
|
64
|
+
job.on('classic.SessionStart', (e, next) => {
|
|
65
|
+
hasStarted = e.source === 'resume' || e.source === 'compact'
|
|
66
|
+
if (!hasStarted) given = undefined
|
|
67
|
+
return next(e)
|
|
68
|
+
})
|
|
58
69
|
job.on('prompt.context', async (e, next) => {
|
|
59
70
|
const below = await next(e)
|
|
71
|
+
hasStarted = true
|
|
60
72
|
if (below.blocks.some((block) => block.name === name)) {
|
|
61
73
|
log(`a block named "${name}" is already in the context`)
|
|
62
74
|
return below
|
|
63
75
|
}
|
|
76
|
+
given = await textFor()
|
|
77
|
+
return given === undefined ? below : { ...below, blocks: [...below.blocks, { name, text: given }] }
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
const update = async () => {
|
|
81
|
+
isQueued = false
|
|
82
|
+
if (!hasStarted) return
|
|
64
83
|
const value = await textFor()
|
|
65
|
-
|
|
84
|
+
if (value === undefined || value === given) return
|
|
85
|
+
given = value
|
|
86
|
+
await job.claude.session.append({ message: { type: 'user', content: [{ type: 'text', text: `# ${name}\n${value}` }] } }).then((added) => {
|
|
87
|
+
if (added.deny !== undefined) log(`Claude Code refused the new text: ${added.deny}`)
|
|
88
|
+
})
|
|
89
|
+
}
|
|
90
|
+
onStateChange(job.mod.state, () => {
|
|
91
|
+
if (isQueued) return
|
|
92
|
+
isQueued = true
|
|
93
|
+
updates = updates.then(update).catch((error: unknown) => log(messageOf(error)))
|
|
66
94
|
})
|
|
67
95
|
}
|
|
68
96
|
|
package/src/mod.ts
CHANGED
|
@@ -10,6 +10,9 @@ import type {
|
|
|
10
10
|
HookStream,
|
|
11
11
|
HttpInit,
|
|
12
12
|
HttpResponse,
|
|
13
|
+
ModelCompleteOptions,
|
|
14
|
+
ModelCompleteRequest,
|
|
15
|
+
ModelCompleteResult,
|
|
13
16
|
PaneOpenArgs,
|
|
14
17
|
PermissionRequestDecision,
|
|
15
18
|
ProcessRunInit,
|
|
@@ -20,6 +23,7 @@ import type {
|
|
|
20
23
|
RenderElement,
|
|
21
24
|
Settings,
|
|
22
25
|
SettingsReadArgs,
|
|
26
|
+
Timer,
|
|
23
27
|
ToastOptions,
|
|
24
28
|
UiScrollArgs,
|
|
25
29
|
UiScrollResult,
|
|
@@ -117,12 +121,17 @@ export type HookInput<E extends ModEvent> = HookInputs[E]
|
|
|
117
121
|
|
|
118
122
|
export type ModHook<E extends ModEvent> = (input: HookInput<E>) => HookAnswer | void | Promise<HookAnswer | void>
|
|
119
123
|
|
|
124
|
+
export type HookOptions = {
|
|
125
|
+
readonly timeoutMs?: number
|
|
126
|
+
}
|
|
127
|
+
|
|
120
128
|
export type Mod<State extends object = Record<never, never>> = {
|
|
121
129
|
readonly name: string
|
|
122
130
|
readonly state: Readonly<State>
|
|
123
131
|
readonly dataFolder: string
|
|
124
|
-
on<E extends ModEvent>(event: E, hook: ModHook<E
|
|
132
|
+
on<E extends ModEvent>(event: E, hook: ModHook<E>, options?: HookOptions): void
|
|
125
133
|
use<Handle>(job: Job<Handle, State>): Handle
|
|
134
|
+
every(ms: number, hook: () => unknown): Timer
|
|
126
135
|
readonly ui: {
|
|
127
136
|
pane(pane: Pane<State>): PaneHandle
|
|
128
137
|
render<S extends Slot>(slot: S, Component: (props: SlotProps<S>) => RenderElement): void
|
|
@@ -150,10 +159,15 @@ export type Mod<State extends object = Record<never, never>> = {
|
|
|
150
159
|
}
|
|
151
160
|
readonly session: {
|
|
152
161
|
messages: Claude['session']['messages']
|
|
162
|
+
append(text: string): Promise<void>
|
|
163
|
+
submit(text: string): Promise<void>
|
|
153
164
|
}
|
|
154
165
|
readonly agent: {
|
|
155
166
|
spawn(args: AgentSpawnArgs): Promise<AgentSpawnResult>
|
|
156
167
|
}
|
|
168
|
+
readonly model: {
|
|
169
|
+
complete(request: ModelCompleteRequest, options?: ModelCompleteOptions): Promise<ModelCompleteResult>
|
|
170
|
+
}
|
|
157
171
|
readonly projectRoot: string
|
|
158
172
|
readonly cwd: string
|
|
159
173
|
readonly dependencies: CmodDependencies
|
package/src/records.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { messageOf } from './utils/text.js'
|
|
1
|
+
import { listed, messageOf } from './utils/text.js'
|
|
2
2
|
|
|
3
3
|
export type InstallRecord = {
|
|
4
4
|
name: string
|
|
@@ -8,16 +8,17 @@ export type InstallRecord = {
|
|
|
8
8
|
scriptsSha256: string
|
|
9
9
|
uninstall: string | null
|
|
10
10
|
program: string | null
|
|
11
|
+
keys: Readonly<Record<string, string>>
|
|
11
12
|
}
|
|
12
13
|
|
|
13
|
-
export type Steps = { install?: string; uninstall?: string; program?: string }
|
|
14
|
+
export type Steps = { install?: string; uninstall?: string; program?: string; keys?: Readonly<Record<string, string>> }
|
|
14
15
|
|
|
15
16
|
export type ReadFile = (path: string) => Promise<string | undefined>
|
|
16
17
|
|
|
17
18
|
export type RunnerEvent =
|
|
18
19
|
| { kind: 'progress'; done: number; total: number; label: string }
|
|
19
20
|
| { kind: 'log'; text: string }
|
|
20
|
-
| { kind: 'needs-consent'; sha256: string; install: string; uninstall: string }
|
|
21
|
+
| { kind: 'needs-consent'; sha256: string; install: string; uninstall: string; keys: string }
|
|
21
22
|
| { kind: 'done'; name: string; version?: string }
|
|
22
23
|
| { kind: 'missing'; name: string }
|
|
23
24
|
| { kind: 'failed'; code: number; message: string }
|
|
@@ -89,6 +90,8 @@ function parseRecord(text: string, path: string): InstallRecord {
|
|
|
89
90
|
if (uninstall !== null && typeof uninstall !== 'string') throw new Error(`${path} has an "uninstall" that is neither a path nor null. ${fix}`)
|
|
90
91
|
const program = value['program']
|
|
91
92
|
if (program !== null && typeof program !== 'string') throw new Error(`${path} has a "program" that is neither a command name nor null. ${fix}`)
|
|
93
|
+
const keys = value['keys'] ?? {}
|
|
94
|
+
if (!isObject(keys) || Object.values(keys).some((command) => typeof command !== 'string')) throw new Error(`${path} has "keys" that are not key bindings. ${fix}`)
|
|
92
95
|
return {
|
|
93
96
|
name: value['name'] as string,
|
|
94
97
|
version: value['version'] as string,
|
|
@@ -97,6 +100,7 @@ function parseRecord(text: string, path: string): InstallRecord {
|
|
|
97
100
|
scriptsSha256: value['scriptsSha256'] as string,
|
|
98
101
|
uninstall,
|
|
99
102
|
program,
|
|
103
|
+
keys: keys as Record<string, string>,
|
|
100
104
|
}
|
|
101
105
|
}
|
|
102
106
|
|
|
@@ -108,7 +112,23 @@ export function readSteps(pkg: unknown): Steps | undefined {
|
|
|
108
112
|
if (!isObject(steps)) throw new Error('package.json has a "cmod" key that is not an object. Write "cmod": { "install": "./setup/install.sh", "uninstall": "./setup/uninstall.sh" }.')
|
|
109
113
|
const program = steps['program']
|
|
110
114
|
if (program !== undefined && (typeof program !== 'string' || !pluginName.test(program))) throw new Error('package.json "cmod.program" must be the program\'s command, such as "hello".')
|
|
111
|
-
return { ...readCommand(steps, 'install'), ...readCommand(steps, 'uninstall'), ...(program === undefined ? {} : { program }) }
|
|
115
|
+
return { ...readCommand(steps, 'install'), ...readCommand(steps, 'uninstall'), ...(program === undefined ? {} : { program }), ...readKeys(steps['keys']) }
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function readKeys(keys: unknown): Steps {
|
|
119
|
+
if (keys === undefined) return {}
|
|
120
|
+
if (!isObject(keys) || Object.keys(keys).length === 0) throw new Error('package.json "cmod.keys" must bind each key to a command, such as "keys": { "shift+tab": "/mode" }.')
|
|
121
|
+
const commands: Record<string, string> = {}
|
|
122
|
+
for (const [key, value] of Object.entries(keys)) {
|
|
123
|
+
const command = typeof value === 'string' ? /^\/([A-Za-z0-9][A-Za-z0-9._:-]*)$/.exec(value)?.[1] : undefined
|
|
124
|
+
if (key.trim() === '' || command === undefined) throw new Error(`package.json "cmod.keys" binds "${key}" to ${JSON.stringify(value)}. Bind each key to one of the mod's commands, such as "shift+tab": "/mode".`)
|
|
125
|
+
commands[key] = command
|
|
126
|
+
}
|
|
127
|
+
return { keys: commands }
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function keyWords(keys: Readonly<Record<string, string>> | undefined): string {
|
|
131
|
+
return listed(Object.entries(keys ?? {}).map(([key, command]) => `${key} to /${command}`))
|
|
112
132
|
}
|
|
113
133
|
|
|
114
134
|
function readCommand(steps: Record<string, unknown>, key: 'install' | 'uninstall'): Steps {
|
|
@@ -145,7 +165,7 @@ export async function scriptsSha256(steps: Steps, files: { read: ReadFile; list:
|
|
|
145
165
|
if (scriptFolders.length === 0) throw new Error(`package.json "cmod.${key}" runs "${command}", which names no script file in the mod, so consent cannot cover what it runs. Put the commands in a script, such as ./setup/${key}.sh.`)
|
|
146
166
|
for (const folder of scriptFolders) folders.add(folder)
|
|
147
167
|
}
|
|
148
|
-
let text = [steps.install ?? '', steps.uninstall ?? '', steps.program ?? ''].join('\0')
|
|
168
|
+
let text = [steps.install ?? '', steps.uninstall ?? '', steps.program ?? '', ...(steps.keys === undefined ? [] : [JSON.stringify(Object.entries(steps.keys).sort())])].join('\0')
|
|
149
169
|
for (const folder of [...folders].sort()) {
|
|
150
170
|
for (const name of (await files.list(folder)).sort()) {
|
|
151
171
|
const path = `${folder}/${name}`
|
|
@@ -165,7 +185,7 @@ export function formatEvent(event: RunnerEvent): string {
|
|
|
165
185
|
case 'log':
|
|
166
186
|
return `log ${event.text}`
|
|
167
187
|
case 'needs-consent':
|
|
168
|
-
return `needs-consent ${event.sha256}\t${event.install}\t${event.uninstall}`
|
|
188
|
+
return `needs-consent ${event.sha256}\t${event.install}\t${event.uninstall}\t${event.keys}`
|
|
169
189
|
case 'done':
|
|
170
190
|
return event.version === undefined ? `done ${event.name}` : `done ${event.name} ${event.version}`
|
|
171
191
|
case 'missing':
|
|
@@ -178,8 +198,8 @@ export function formatEvent(event: RunnerEvent): string {
|
|
|
178
198
|
export function parseEvent(line: string): RunnerEvent {
|
|
179
199
|
const progress = /^progress (\d+) (\d+)(?: (.*))?$/.exec(line)
|
|
180
200
|
if (progress !== null) return { kind: 'progress', done: Number(progress[1]), total: Number(progress[2]), label: progress[3] ?? '' }
|
|
181
|
-
const consent = /^needs-consent (\S+)\t([^\t]*)\t(.*)
|
|
182
|
-
if (consent !== null) return { kind: 'needs-consent', sha256: consent[1] as string, install: consent[2] as string, uninstall: consent[3] as string }
|
|
201
|
+
const consent = /^needs-consent (\S+)\t([^\t]*)\t([^\t]*)(?:\t(.*))?$/.exec(line)
|
|
202
|
+
if (consent !== null) return { kind: 'needs-consent', sha256: consent[1] as string, install: consent[2] as string, uninstall: consent[3] as string, keys: consent[4] ?? '' }
|
|
183
203
|
const done = /^done (\S+)(?: (\S+))?$/.exec(line)
|
|
184
204
|
if (done !== null) return { kind: 'done', name: done[1] as string, ...(done[2] === undefined ? {} : { version: done[2] }) }
|
|
185
205
|
const missing = /^missing (\S+)$/.exec(line)
|
package/src/register.ts
CHANGED
|
@@ -56,7 +56,10 @@ async function startMod($: EngineInterface, eventInput: Frozen<Args<'session.sta
|
|
|
56
56
|
usage: () => $.session.usage(),
|
|
57
57
|
surfaces: () => $.session.surfaces(),
|
|
58
58
|
messages: ((args?: Parameters<Claude['session']['messages']>[0]) => (args === undefined ? $.session.messages() : $.session.messages(args))) as Claude['session']['messages'],
|
|
59
|
+
append: (args) => $.session.append(args),
|
|
59
60
|
},
|
|
61
|
+
prompt: { submit: (args) => $.prompt.submit(args) },
|
|
62
|
+
model: { complete: (request, options) => $.model.complete(request, options) },
|
|
60
63
|
command: { register: (command) => $.command.register(command) },
|
|
61
64
|
tool: { register: (tool) => $.tool.register(tool) },
|
|
62
65
|
agent: {
|
package/src/runtime/claude.ts
CHANGED
|
@@ -83,7 +83,10 @@ export type Claude = {
|
|
|
83
83
|
usage(): Promise<SessionUsage>
|
|
84
84
|
surfaces(): Promise<readonly RenderSurface[]>
|
|
85
85
|
messages: EngineInterface['session']['messages']
|
|
86
|
+
append: EngineInterface['session']['append']
|
|
86
87
|
}
|
|
88
|
+
readonly prompt: { submit: EngineInterface['prompt']['submit'] }
|
|
89
|
+
readonly model: { complete: EngineInterface['model']['complete'] }
|
|
87
90
|
readonly command: { register(command: CommandSpec): Promise<{ command: string }> }
|
|
88
91
|
readonly tool: { register(tool: ToolSpec): Promise<{ tool: string }> }
|
|
89
92
|
readonly agent: {
|