@cmodjs/core 0.1.8 → 0.1.10
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/hooks.md +62 -6
- package/docs/index.md +7 -4
- package/docs/install-steps.md +3 -3
- package/docs/jobs.md +5 -4
- package/docs/mod.md +57 -7
- package/docs/testing.md +25 -9
- package/docs/ui.md +134 -5
- package/jobs/check.js +2 -2
- package/jobs/context.d.ts.map +1 -1
- package/jobs/context.js +2 -0
- package/jobs/permissions/decide-permission.js +2 -2
- package/jobs/permissions/find-project-scope.d.ts +1 -1
- package/jobs/permissions/find-project-scope.d.ts.map +1 -1
- package/jobs/permissions/find-project-scope.js +9 -9
- package/jobs/permissions/match-target.d.ts.map +1 -1
- package/jobs/permissions/match-target.js +4 -3
- package/mod.d.ts +16 -6
- package/mod.d.ts.map +1 -1
- package/package.json +2 -1
- package/path.d.ts +2 -0
- package/path.d.ts.map +1 -0
- package/path.js +1 -0
- package/register.d.ts.map +1 -1
- package/register.js +8 -2
- package/runtime/claude.d.ts +4 -1
- package/runtime/claude.d.ts.map +1 -1
- package/runtime/hooks.d.ts +10 -2
- package/runtime/hooks.d.ts.map +1 -1
- package/runtime/hooks.js +56 -18
- package/runtime/lifecycle.d.ts.map +1 -1
- package/runtime/lifecycle.js +12 -2
- package/runtime/ui.d.ts.map +1 -1
- package/runtime/ui.js +42 -26
- package/shell.d.ts +2 -0
- package/shell.d.ts.map +1 -0
- package/shell.js +1 -0
- package/src/jobs/check.ts +2 -2
- package/src/jobs/context.ts +2 -0
- package/src/jobs/permissions/decide-permission.ts +2 -2
- package/src/jobs/permissions/find-project-scope.ts +11 -10
- package/src/jobs/permissions/match-target.ts +4 -3
- package/src/mod.ts +24 -4
- package/src/path.ts +1 -0
- package/src/register.ts +8 -2
- package/src/runtime/claude.ts +11 -1
- package/src/runtime/hooks.ts +62 -23
- package/src/runtime/lifecycle.ts +11 -4
- package/src/runtime/ui.ts +39 -26
- package/src/shell.ts +1 -0
- package/src/testing/fake-claude.ts +14 -5
- package/src/testing/fake-elements.ts +5 -7
- package/src/testing/fake-files.ts +5 -5
- package/src/testing.ts +83 -11
- package/src/ui/components.ts +70 -0
- package/src/ui/define-pane.ts +5 -1
- package/src/utils/call-effects.ts +12 -11
- package/src/utils/parse-shell.ts +28 -36
- package/src/utils/paths.ts +4 -19
- package/src/vendor.ts +1 -0
- package/testing/fake-claude.d.ts +5 -0
- package/testing/fake-claude.d.ts.map +1 -1
- package/testing/fake-claude.js +13 -3
- package/testing/fake-elements.d.ts +5 -8
- package/testing/fake-elements.d.ts.map +1 -1
- package/testing/fake-elements.js +6 -6
- package/testing/fake-files.js +5 -5
- package/testing.d.ts +4 -2
- package/testing.d.ts.map +1 -1
- package/testing.js +65 -9
- package/ui/components.d.ts +48 -0
- package/ui/components.d.ts.map +1 -0
- package/ui/components.js +56 -0
- package/ui/define-pane.d.ts +5 -1
- package/ui/define-pane.d.ts.map +1 -1
- package/utils/call-effects.d.ts.map +1 -1
- package/utils/call-effects.js +12 -11
- package/utils/parse-shell.d.ts +5 -9
- package/utils/parse-shell.d.ts.map +1 -1
- package/utils/parse-shell.js +5 -10
- package/utils/paths.d.ts +0 -3
- package/utils/paths.d.ts.map +1 -1
- package/utils/paths.js +3 -18
- package/vendor.d.ts +1 -0
- package/vendor.d.ts.map +1 -1
- package/vendor.js +209 -12
package/runtime/hooks.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { configFolders } from '../records.js';
|
|
2
|
-
import { toolInputOf } from './tool-calls.js';
|
|
2
|
+
import { reservedKeys, toolInputOf } from './tool-calls.js';
|
|
3
3
|
import { callEffects } from '../utils/call-effects.js';
|
|
4
4
|
import { dynamicPattern } from '../utils/parse-shell.js';
|
|
5
5
|
import { messageOf } from '../utils/text.js';
|
|
6
|
-
export const permissionEvents = ['tool.check', 'classic.
|
|
6
|
+
export const permissionEvents = ['tool.check', 'classic.PermissionRequest'];
|
|
7
7
|
const flags = ['suppressOriginalPrompt', 'reloadSkills', 'retry'];
|
|
8
8
|
const readFields = {
|
|
9
9
|
SessionStart: ['additionalContext', 'initialUserMessage', 'sessionTitle', 'watchPaths', 'reloadSkills'],
|
|
@@ -23,11 +23,10 @@ const readFields = {
|
|
|
23
23
|
Stop: ['additionalContext'],
|
|
24
24
|
StopFailure: [],
|
|
25
25
|
CwdChanged: [],
|
|
26
|
+
FileChanged: [],
|
|
26
27
|
};
|
|
27
28
|
export function classicHook(name, event, hook, claude, calls) {
|
|
28
29
|
const inputOf = async (e) => {
|
|
29
|
-
if (event === 'PreToolUse')
|
|
30
|
-
return preToolUseInput(name, e, claude, calls.agentOf);
|
|
31
30
|
if (event !== 'PostToolUse' && event !== 'PostToolUseFailure')
|
|
32
31
|
return e;
|
|
33
32
|
const input = e;
|
|
@@ -45,8 +44,6 @@ export function classicHook(name, event, hook, claude, calls) {
|
|
|
45
44
|
const routed = async (e, next) => {
|
|
46
45
|
const ours = await resultOf(e).catch((error) => {
|
|
47
46
|
const reason = `${name}: the ${event} hook failed: ${messageOf(error)}`;
|
|
48
|
-
if (event === 'PreToolUse')
|
|
49
|
-
return { deny: reason };
|
|
50
47
|
if (event === 'PermissionRequest')
|
|
51
48
|
return { decision: { behavior: 'deny', message: reason } };
|
|
52
49
|
claude.ui.log(reason);
|
|
@@ -54,10 +51,61 @@ export function classicHook(name, event, hook, claude, calls) {
|
|
|
54
51
|
});
|
|
55
52
|
if (ours === undefined)
|
|
56
53
|
return next(e);
|
|
57
|
-
return mergeClassic(
|
|
54
|
+
return mergeClassic((await next(e)), ours);
|
|
58
55
|
};
|
|
59
56
|
return routed;
|
|
60
57
|
}
|
|
58
|
+
export function preToolUseHook(name, hook, claude, calls, held) {
|
|
59
|
+
return async (e, next) => {
|
|
60
|
+
let ours;
|
|
61
|
+
try {
|
|
62
|
+
const answer = await hook(await preToolUseInput(name, e, claude, calls.agentOf));
|
|
63
|
+
if (answer?.systemMessage !== undefined)
|
|
64
|
+
claude.ui.log(answer.systemMessage);
|
|
65
|
+
ours = answer === undefined ? {} : classicResult('PreToolUse', answer);
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
return { deny: `${name}: the PreToolUse hook failed: ${messageOf(error)}` };
|
|
69
|
+
}
|
|
70
|
+
if (typeof ours['deny'] === 'string')
|
|
71
|
+
return { deny: ours['deny'] };
|
|
72
|
+
const decided = heldDecisionOf(ours);
|
|
73
|
+
if (decided !== undefined && held === undefined) {
|
|
74
|
+
return { deny: `${name}: the PreToolUse hook answered permissionDecision "${decided.decision}", so hooks/register.ts must call registerPermissionCheck(addHook) after registerMod.` };
|
|
75
|
+
}
|
|
76
|
+
const updated = ours['updatedInput'];
|
|
77
|
+
const reserved = Object.fromEntries(Object.entries(e).filter(([key]) => reservedKeys.includes(key)));
|
|
78
|
+
const earlier = held?.get(e.tool_use_id);
|
|
79
|
+
if (decided !== undefined)
|
|
80
|
+
held?.set(e.tool_use_id, earlier !== undefined && strictness.indexOf(earlier.decision) >= strictness.indexOf(decided.decision) ? earlier : decided);
|
|
81
|
+
let result;
|
|
82
|
+
try {
|
|
83
|
+
result = await next(updated === undefined ? e : { ...reserved, ...updated });
|
|
84
|
+
}
|
|
85
|
+
finally {
|
|
86
|
+
if (decided !== undefined)
|
|
87
|
+
held?.delete(e.tool_use_id);
|
|
88
|
+
}
|
|
89
|
+
const context = ours['additionalContext'];
|
|
90
|
+
if (context === undefined || result.deny !== undefined)
|
|
91
|
+
return result;
|
|
92
|
+
return { ...result, context: [...(result.context ?? []), ...context] };
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
export function heldDecisionHook(held) {
|
|
96
|
+
return async (e, next) => {
|
|
97
|
+
const below = await next(e);
|
|
98
|
+
const ours = e.tool_use_id === undefined ? undefined : held.get(e.tool_use_id);
|
|
99
|
+
return ours === undefined || below.decision === 'deny' ? below : ours;
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
function heldDecisionOf(ours) {
|
|
103
|
+
if (ours['allow'] !== undefined)
|
|
104
|
+
return { decision: 'allow' };
|
|
105
|
+
if (typeof ours['ask'] !== 'string')
|
|
106
|
+
return undefined;
|
|
107
|
+
return ours['ask'] === '' ? { decision: 'ask' } : { decision: 'ask', reason: ours['ask'] };
|
|
108
|
+
}
|
|
61
109
|
const frontmatter = /^---\r?\n[\s\S]*?\r?\n---[ \t]*(?:\r?\n|$)/;
|
|
62
110
|
const baseDirectoryLine = /^Base directory for this skill: [^\n]*\n\n/;
|
|
63
111
|
export function userSkillHook(claude) {
|
|
@@ -149,20 +197,10 @@ function classicResult(event, answer) {
|
|
|
149
197
|
return result;
|
|
150
198
|
}
|
|
151
199
|
export const strictness = ['allow', 'ask', 'deny'];
|
|
152
|
-
function
|
|
153
|
-
return strictness.findLastIndex((decision) => result[decision] !== undefined);
|
|
154
|
-
}
|
|
155
|
-
function mergeClassic(event, below, ours) {
|
|
200
|
+
function mergeClassic(below, ours) {
|
|
156
201
|
const merged = { ...below, ...ours };
|
|
157
202
|
const context = [...(below['additionalContext'] ?? []), ...(ours['additionalContext'] ?? [])];
|
|
158
203
|
if (context.length > 0)
|
|
159
204
|
merged['additionalContext'] = context;
|
|
160
|
-
if (event !== 'PreToolUse')
|
|
161
|
-
return merged;
|
|
162
|
-
const strictest = strictness[Math.max(strictnessOf(below), strictnessOf(ours))];
|
|
163
|
-
for (const decision of strictness)
|
|
164
|
-
delete merged[decision];
|
|
165
|
-
if (strictest !== undefined)
|
|
166
|
-
merged[strictest] = ours[strictest] ?? below[strictest];
|
|
167
205
|
return merged;
|
|
168
206
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lifecycle.d.ts","sourceRoot":"","sources":["../src/runtime/lifecycle.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,WAAW,EAAE,MAAM,EAAiE,MAAM,aAAa,CAAA;AAC3H,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"lifecycle.d.ts","sourceRoot":"","sources":["../src/runtime/lifecycle.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,WAAW,EAAE,MAAM,EAAiE,MAAM,aAAa,CAAA;AAC3H,OAAO,KAAK,EAAa,GAAG,EAAE,aAAa,EAAqB,MAAM,WAAW,CAAA;AAIjF,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AAGzC,OAAO,EAAsG,KAAK,WAAW,EAAE,MAAM,YAAY,CAAA;AACjJ,OAAO,EAA6B,KAAK,UAAU,EAAE,MAAM,aAAa,CAAA;AAKxE,MAAM,MAAM,MAAM,GAAG;IACnB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,SAAS,CAAA;IACpC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,WAAW,EAAE,OAAO,CAAA;IAC7B,QAAQ,CAAC,YAAY,EAAE,OAAO,CAAA;CAC/B,CAAA;AAED,MAAM,MAAM,KAAK,GAAG,UAAU,GAAG,YAAY,GAAG,SAAS,GAAG,UAAU,GAAG,QAAQ,GAAG,OAAO,GAAG,QAAQ,CAAA;AAEtG,MAAM,MAAM,SAAS,CAAC,KAAK,SAAS,MAAM,IAAI;IAC5C,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAA;IACrB,QAAQ,CAAC,GAAG,EAAE,GAAG,CAAC,KAAK,CAAC,GAAG,SAAS,CAAA;IACpC,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAA;IACzB,KAAK,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAC/E,KAAK,CAAC,CAAC,SAAS,WAAW,EAAE,KAAK,EAAE,CAAC,EAAE,CAAC,EAAE,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,UAAU,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,CAAA;CACzG,CAAA;AAiCD,wBAAsB,UAAU,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAyBhE;AAkCD,wBAAgB,eAAe,CAAC,KAAK,SAAS,MAAM,EAAE,UAAU,EAAE,aAAa,CAAC,KAAK,CAAC,EAAE,iBAAiB,GAAE,MAAM,OAAoB,GAAG,SAAS,CAAC,KAAK,CAAC,CAkOvJ"}
|
package/runtime/lifecycle.js
CHANGED
|
@@ -3,7 +3,7 @@ import { relativePath } from '../utils/paths.js';
|
|
|
3
3
|
import { formatExit, listed, messageOf } from '../utils/text.js';
|
|
4
4
|
import { beforeDeadline } from './deadline.js';
|
|
5
5
|
import { answerCall, dependencyCalls, notInstalled } from './dependencies.js';
|
|
6
|
-
import { classicHook, permissionEvents, userSkillHook } from './hooks.js';
|
|
6
|
+
import { classicHook, heldDecisionHook, permissionEvents, preToolUseHook, userSkillHook } from './hooks.js';
|
|
7
7
|
import { createRouter } from './router.js';
|
|
8
8
|
import { createState } from './state.js';
|
|
9
9
|
import { toolCalls } from './tool-calls.js';
|
|
@@ -332,6 +332,7 @@ async function createMod(definition, runtime) {
|
|
|
332
332
|
let staleCwd;
|
|
333
333
|
const area = createUi({ name: definition.name, claude, router, progress, announce, mod: () => mod });
|
|
334
334
|
const modState = createState({ name: definition.name, initial: definition.state ?? {}, session, root, claude, changed: area.changed });
|
|
335
|
+
const held = runtime.checksPermissions() ? new Map() : undefined;
|
|
335
336
|
const mod = {
|
|
336
337
|
name: definition.name,
|
|
337
338
|
state: modState.state,
|
|
@@ -339,7 +340,10 @@ async function createMod(definition, runtime) {
|
|
|
339
340
|
on(event, hook) {
|
|
340
341
|
if (!hookEvents.includes(event))
|
|
341
342
|
hookEvents.push(event);
|
|
342
|
-
|
|
343
|
+
if (event === 'PreToolUse')
|
|
344
|
+
router.add('tool.call', preToolUseHook(definition.name, hook, claude, agents, held));
|
|
345
|
+
else
|
|
346
|
+
router.add(`classic.${event}`, classicHook(definition.name, event, hook, claude, agents));
|
|
343
347
|
},
|
|
344
348
|
use: (job) => job({ mod, claude, on: (event, hook) => router.add(event, hook), announce, reserveName, toolCalls: agents }),
|
|
345
349
|
ui: area.ui,
|
|
@@ -351,9 +355,13 @@ async function createMod(definition, runtime) {
|
|
|
351
355
|
read: (path) => claude.fs.read(path),
|
|
352
356
|
write: (path, text) => claude.fs.write(path, text),
|
|
353
357
|
list: (path) => claude.fs.list(path),
|
|
358
|
+
exists: (path) => claude.fs.exists(path),
|
|
359
|
+
stat: (path, options) => claude.fs.stat(path, options),
|
|
354
360
|
},
|
|
355
361
|
http: { fetch: (url, init) => claude.http.fetch(url, init) },
|
|
356
362
|
settings: { read: (args) => claude.settings.read(args) },
|
|
363
|
+
session: { messages: claude.session.messages },
|
|
364
|
+
agent: { spawn: (args) => claude.agent.spawn(args) },
|
|
357
365
|
get projectRoot() {
|
|
358
366
|
return modState.root;
|
|
359
367
|
},
|
|
@@ -433,6 +441,8 @@ async function createMod(definition, runtime) {
|
|
|
433
441
|
if (permissionHooks.length > 0 && !runtime.checksPermissions()) {
|
|
434
442
|
throw new Error(`it decides permissions on ${listed(permissionHooks)}, so hooks/register.ts must call registerPermissionCheck(addHook) after registerMod`);
|
|
435
443
|
}
|
|
444
|
+
if (held !== undefined && hookEvents.includes('PreToolUse'))
|
|
445
|
+
router.add('tool.check', heldDecisionHook(held));
|
|
436
446
|
await failsAs('its open panes did not load', () => area.restorePanes());
|
|
437
447
|
if (hookEvents.length > 0)
|
|
438
448
|
added.push(`${hookEvents.length === 1 ? 'a hook' : 'hooks'} on ${listed(hookEvents)}`);
|
package/runtime/ui.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ui.d.ts","sourceRoot":"","sources":["../src/runtime/ui.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAA+B,aAAa,EAAS,MAAM,aAAa,CAAA;AAClG,OAAO,KAAK,EAAE,GAAG,EAAc,YAAY,EAAE,MAAM,WAAW,CAAA;
|
|
1
|
+
{"version":3,"file":"ui.d.ts","sourceRoot":"","sources":["../src/runtime/ui.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAA+B,aAAa,EAAS,MAAM,aAAa,CAAA;AAClG,OAAO,KAAK,EAAE,GAAG,EAAc,YAAY,EAAE,MAAM,WAAW,CAAA;AAO9D,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AACzC,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AAEzC,MAAM,MAAM,YAAY,GAAG;IACzB,MAAM,CAAC,IAAI,EAAE,YAAY,GAAG,IAAI,CAAA;IAChC,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,IAAI,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,IAAI,CAAA;IACvC,GAAG,IAAI,IAAI,CAAA;CACZ,CAAA;AAED,MAAM,MAAM,QAAQ,GAAG;IACrB,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAA;IACzB,KAAK,CAAC,KAAK,EAAE,MAAM,GAAG,YAAY,CAAA;IAClC,IAAI,CAAC,KAAK,EAAE,aAAa,EAAE,KAAK,EAAE,YAAY,EAAE,OAAO,EAAE,MAAM,GAAG,SAAS,GAAG,aAAa,CAAA;CAC5F,CAAA;AAkBD,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,QAAQ,CAqDvD;AAsGD,MAAM,MAAM,SAAS,CAAC,KAAK,SAAS,MAAM,IAAI;IAC5C,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAA;IAC3B,QAAQ,CAAC,QAAQ,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAA;IAC5C,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,CAAC,KAAK,CAAC,CAAA;CAC/B,CAAA;AAED,MAAM,MAAM,MAAM,CAAC,KAAK,SAAS,MAAM,IAAI;IACzC,QAAQ,CAAC,EAAE,EAAE,GAAG,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,CAAA;IAC7B,OAAO,IAAI,IAAI,CAAA;IACf,YAAY,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;CAC9B,CAAA;AAED,wBAAgB,QAAQ,CAAC,KAAK,SAAS,MAAM,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,QAAQ,EAAE,GAAG,EAAE,EAAE,SAAS,CAAC,KAAK,CAAC,GAAG,MAAM,CAAC,KAAK,CAAC,CAmNjI"}
|
package/runtime/ui.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ProgressBar } from '../ui/components.js';
|
|
1
2
|
import { Box, drawWith, Text } from '../ui/elements.js';
|
|
2
3
|
import { messageOf } from '../utils/text.js';
|
|
3
4
|
const spinner = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
@@ -77,23 +78,8 @@ function drawLine(line, frame, columns) {
|
|
|
77
78
|
if (line.step === undefined)
|
|
78
79
|
return Text({ wrap: 'truncate-end', children: [glyph, ' ', title, Text({ dimColor: true, children: '…' })] });
|
|
79
80
|
const { done, total, label = '' } = line.step;
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
const filled = total > 0 ? Math.round(Math.min(done / total, 1) * width) : 0;
|
|
83
|
-
return Text({
|
|
84
|
-
wrap: 'truncate-end',
|
|
85
|
-
children: [
|
|
86
|
-
glyph,
|
|
87
|
-
' ',
|
|
88
|
-
title,
|
|
89
|
-
' ',
|
|
90
|
-
Text({ color: 'claude', children: '█'.repeat(filled) }),
|
|
91
|
-
Text({ color: 'subtle', children: '░'.repeat(width - filled) }),
|
|
92
|
-
' ',
|
|
93
|
-
Text({ dimColor: true, children: count }),
|
|
94
|
-
...(label === '' ? [] : [' ', label]),
|
|
95
|
-
],
|
|
96
|
-
});
|
|
81
|
+
const width = barWidth(columns, `${line.title}${done}/${total}${label}`.length);
|
|
82
|
+
return Text({ wrap: 'truncate-end', children: [glyph, ' ', title, ' ', ProgressBar({ done, total, width }), ...(label === '' ? [] : [' ', label])] });
|
|
97
83
|
}
|
|
98
84
|
function barWidth(columns, textLength) {
|
|
99
85
|
if (columns === undefined)
|
|
@@ -165,6 +151,7 @@ function drawPieces(pieces, place) {
|
|
|
165
151
|
}
|
|
166
152
|
export function createUi({ name, claude, router, progress, announce, mod }) {
|
|
167
153
|
const panes = new Map();
|
|
154
|
+
const paneLogs = new Map();
|
|
168
155
|
const openPanes = new Set();
|
|
169
156
|
const requestedSizes = new Map();
|
|
170
157
|
const renders = new Set();
|
|
@@ -231,9 +218,10 @@ export function createUi({ name, claude, router, progress, announce, mod }) {
|
|
|
231
218
|
return drawWith(table, () => drawPieces(drawn, e.props.isFirstOfReply ? 'opensReply' : 'inReply'));
|
|
232
219
|
});
|
|
233
220
|
};
|
|
234
|
-
const openAt = (pane, size) => {
|
|
221
|
+
const openAt = (pane, size, focus) => {
|
|
235
222
|
requestedSizes.set(pane.id, size);
|
|
236
|
-
|
|
223
|
+
const { id, title, closeOnEscape, holdToasts } = pane;
|
|
224
|
+
return claude.ui.open({ id, title, ...size, ...(closeOnEscape === true ? { closeOnEscape } : {}), ...(holdToasts === true ? { holdToasts } : {}), ...(focus === true ? { focus } : {}) });
|
|
237
225
|
};
|
|
238
226
|
const resize = async (pane) => {
|
|
239
227
|
try {
|
|
@@ -257,27 +245,54 @@ export function createUi({ name, claude, router, progress, announce, mod }) {
|
|
|
257
245
|
return next(e);
|
|
258
246
|
openPanes.add(pane.id);
|
|
259
247
|
const table = claude.ui.resolve(e);
|
|
260
|
-
|
|
248
|
+
try {
|
|
249
|
+
return drawWith(table, () => pane.render(mod(), e.props), markdownIn(table));
|
|
250
|
+
}
|
|
251
|
+
catch (error) {
|
|
252
|
+
paneLogs.get(pane.id)?.(`threw: ${messageOf(error)}`);
|
|
253
|
+
return drawWith(table, () => Text({ color: 'error', children: `The ${pane.title} pane could not draw: ${messageOf(error)}` }));
|
|
254
|
+
}
|
|
261
255
|
});
|
|
262
|
-
router.add('ui.
|
|
256
|
+
router.add('ui.scroll', async (e, next) => {
|
|
257
|
+
const pane = panes.get(e.requestId);
|
|
258
|
+
const moved = await next(e);
|
|
259
|
+
if (pane?.onScroll === undefined || e.component !== 'Pane' || moved.deny !== undefined)
|
|
260
|
+
return moved;
|
|
261
|
+
await handle(pane, 'onScroll', () => pane.onScroll?.(mod(), e));
|
|
262
|
+
return moved;
|
|
263
|
+
});
|
|
264
|
+
router.add('ui.close', async (e, next) => {
|
|
263
265
|
openPanes.delete(e.id);
|
|
264
|
-
|
|
266
|
+
const closed = await next(e);
|
|
267
|
+
const pane = panes.get(e.id);
|
|
268
|
+
if (pane?.onClose !== undefined)
|
|
269
|
+
await handle(pane, 'onClose', () => pane.onClose?.(mod(), e));
|
|
270
|
+
return closed;
|
|
265
271
|
});
|
|
266
272
|
};
|
|
273
|
+
const handle = async (pane, handler, run) => {
|
|
274
|
+
try {
|
|
275
|
+
await run();
|
|
276
|
+
}
|
|
277
|
+
catch (error) {
|
|
278
|
+
claude.ui.log(`${name}: the ${pane.title} pane's ${handler} threw: ${messageOf(error)}`);
|
|
279
|
+
}
|
|
280
|
+
};
|
|
267
281
|
return {
|
|
268
282
|
ui: {
|
|
269
283
|
pane(pane) {
|
|
270
284
|
if (panes.has(pane.id))
|
|
271
285
|
throw new Error(`${name}: the pane "${pane.id}" is already added. Give each pane its own id.`);
|
|
272
286
|
panes.set(pane.id, pane);
|
|
287
|
+
paneLogs.set(pane.id, logOnce(`${pane.title} pane`));
|
|
273
288
|
announce(`the ${pane.title} pane`);
|
|
274
289
|
route();
|
|
275
290
|
const handle = {
|
|
276
291
|
get isOpen() {
|
|
277
292
|
return openPanes.has(pane.id);
|
|
278
293
|
},
|
|
279
|
-
async open() {
|
|
280
|
-
const { isPlaced } = await openAt(pane, paneSize(pane, mod().state));
|
|
294
|
+
async open(options) {
|
|
295
|
+
const { isPlaced } = await openAt(pane, paneSize(pane, mod().state), options?.focus);
|
|
281
296
|
if (isPlaced)
|
|
282
297
|
openPanes.add(pane.id);
|
|
283
298
|
},
|
|
@@ -285,7 +300,7 @@ export function createUi({ name, claude, router, progress, announce, mod }) {
|
|
|
285
300
|
await claude.ui.close({ id: pane.id });
|
|
286
301
|
openPanes.delete(pane.id);
|
|
287
302
|
},
|
|
288
|
-
toggle: () => (openPanes.has(pane.id) ? handle.close() : handle.open()),
|
|
303
|
+
toggle: (options) => (openPanes.has(pane.id) ? handle.close() : handle.open(options)),
|
|
289
304
|
};
|
|
290
305
|
return handle;
|
|
291
306
|
},
|
|
@@ -318,7 +333,7 @@ export function createUi({ name, claude, router, progress, announce, mod }) {
|
|
|
318
333
|
return drawWith(table, () => drawPieces([drawn], e.props.isFirstOfReply ? 'opensReply' : 'inReply'));
|
|
319
334
|
});
|
|
320
335
|
},
|
|
321
|
-
toast: (text) => claude.ui.toast(text),
|
|
336
|
+
toast: (text, options) => claude.ui.toast(text, options),
|
|
322
337
|
async progress(title, task) {
|
|
323
338
|
const line = progress.start(title);
|
|
324
339
|
try {
|
|
@@ -329,6 +344,7 @@ export function createUi({ name, claude, router, progress, announce, mod }) {
|
|
|
329
344
|
}
|
|
330
345
|
},
|
|
331
346
|
ask: (question, options) => claude.ui.ask(question, options),
|
|
347
|
+
scroll: (args) => claude.ui.scroll(args),
|
|
332
348
|
},
|
|
333
349
|
changed() {
|
|
334
350
|
if (panes.size === 0 && renders.size === 0)
|
package/shell.d.ts
ADDED
package/shell.d.ts.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"shell.d.ts","sourceRoot":"","sources":["src/shell.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,KAAK,WAAW,EAAE,KAAK,YAAY,EAAE,MAAM,wBAAwB,CAAA"}
|
package/shell.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { parseShell } from './utils/parse-shell.js';
|
package/src/jobs/check.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Job } from '../mod.js'
|
|
2
2
|
import { longestMs, type Deadline } from '../runtime/deadline.js'
|
|
3
3
|
import { callEffects, type ToolUse } from '../utils/call-effects.js'
|
|
4
|
-
import {
|
|
4
|
+
import { dirname } from '../vendor.js'
|
|
5
5
|
import { listed, messageOf } from '../utils/text.js'
|
|
6
6
|
import { afterCall, modOf, targetWords } from './context.js'
|
|
7
7
|
import type { Workspace } from './permissions/find-project-scope.js'
|
|
@@ -52,6 +52,6 @@ async function triggeredRun(after: readonly Target[], run: readonly string[], us
|
|
|
52
52
|
const existing: string[] = []
|
|
53
53
|
for (const path of paths) if (await workspace.fs.exists(path)) existing.push(path)
|
|
54
54
|
const last = existing.at(-1)
|
|
55
|
-
if (last !== undefined) return { command: [...run, ...existing], folder:
|
|
55
|
+
if (last !== undefined) return { command: [...run, ...existing], folder: dirname(last) }
|
|
56
56
|
return commandFolder === undefined ? undefined : { command: [...run], folder: commandFolder }
|
|
57
57
|
}
|
package/src/jobs/context.ts
CHANGED
|
@@ -77,6 +77,8 @@ export function modWithin<State extends object>({ mod, claude }: JobContext<Stat
|
|
|
77
77
|
read: (path) => within('mod.fs.read', mod.fs.read(path)),
|
|
78
78
|
write: (path, text) => within('mod.fs.write', mod.fs.write(path, text)),
|
|
79
79
|
list: (path) => within('mod.fs.list', mod.fs.list(path)),
|
|
80
|
+
exists: (path) => within('mod.fs.exists', mod.fs.exists(path)),
|
|
81
|
+
stat: (path, options) => within('mod.fs.stat', mod.fs.stat(path, options)),
|
|
80
82
|
},
|
|
81
83
|
http: { fetch: (url, init) => within('mod.http.fetch', mod.http.fetch(url, init)) },
|
|
82
84
|
dependencies: dependencyCalls(claude, within),
|
|
@@ -3,7 +3,7 @@ import { messageOf } from '../../utils/text.js'
|
|
|
3
3
|
import { callBaseOf, callEffects, type CallEffects, type ToolCall, type ToolUse } 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
|
-
import {
|
|
6
|
+
import { resolve } from '../../vendor.js'
|
|
7
7
|
|
|
8
8
|
export type Rule<Mod> = {
|
|
9
9
|
[Key in TargetKey]: TargetOf<Key> & {
|
|
@@ -71,7 +71,7 @@ async function unparsedMatches(target: Target, use: ToolUse, effects: CallEffect
|
|
|
71
71
|
const named = patterns.map((pattern) => pattern.slice(pattern.lastIndexOf('/') + 1)).filter((segment) => segment !== '' && !/[*?[\]{}]/.test(segment) && line.includes(segment.toLowerCase()))
|
|
72
72
|
const folder = named.length === 0 ? undefined : await folderInScope(shell, workspace)
|
|
73
73
|
if (folder === undefined) return []
|
|
74
|
-
return named.map((segment) => ({ call: { ...base, path:
|
|
74
|
+
return named.map((segment) => ({ call: { ...base, path: resolve(folder, segment) }, access: undefined, folder }))
|
|
75
75
|
}
|
|
76
76
|
|
|
77
77
|
function containsWord(line: string, word: string): boolean {
|
|
@@ -1,14 +1,15 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { FileSystem } from '../../utils/paths.js'
|
|
2
|
+
import { basename, dirname, resolve } from '../../vendor.js'
|
|
2
3
|
|
|
3
4
|
export type ProjectScope = { workTreeOf: (path: string) => Promise<string | undefined> }
|
|
4
5
|
|
|
5
6
|
export type Workspace = { projectRoot: string; cwd: string; home: string; fs: FileSystem; scope: ProjectScope | undefined }
|
|
6
7
|
|
|
7
8
|
export async function findProjectScope(pluginRoot: string, home: string, fs: FileSystem): Promise<ProjectScope | undefined> {
|
|
8
|
-
const skills =
|
|
9
|
-
const claude =
|
|
10
|
-
const root =
|
|
11
|
-
if (
|
|
9
|
+
const skills = dirname(pluginRoot)
|
|
10
|
+
const claude = dirname(skills)
|
|
11
|
+
const root = dirname(claude)
|
|
12
|
+
if (basename(skills) !== 'skills' || basename(claude) !== '.claude' || root === resolve('/', home)) return undefined
|
|
12
13
|
const repositories = new Map<string, string>()
|
|
13
14
|
const repositoryOf = async (workTree: string) => {
|
|
14
15
|
const known = repositories.get(workTree)
|
|
@@ -21,7 +22,7 @@ export async function findProjectScope(pluginRoot: string, home: string, fs: Fil
|
|
|
21
22
|
if (project === undefined) return undefined
|
|
22
23
|
return {
|
|
23
24
|
async workTreeOf(path) {
|
|
24
|
-
for (let folder = path; ; folder =
|
|
25
|
+
for (let folder = path; ; folder = dirname(folder)) {
|
|
25
26
|
const repository = await repositoryOf(folder)
|
|
26
27
|
if (repository !== undefined) return repository === project ? folder : undefined
|
|
27
28
|
if (folder === '/') return undefined
|
|
@@ -31,16 +32,16 @@ export async function findProjectScope(pluginRoot: string, home: string, fs: Fil
|
|
|
31
32
|
}
|
|
32
33
|
|
|
33
34
|
async function readRepository(workTree: string, fs: FileSystem): Promise<string | undefined> {
|
|
34
|
-
const gitPath =
|
|
35
|
+
const gitPath = resolve(workTree, '.git')
|
|
35
36
|
const git = await fs.stat(gitPath).catch(() => undefined)
|
|
36
37
|
if (git === undefined) return undefined
|
|
37
38
|
if (git.kind === 'dir') return git.realPath ?? gitPath
|
|
38
39
|
try {
|
|
39
40
|
const gitDirectory = /^gitdir:\s*(.+)$/m.exec(await fs.read(gitPath))?.[1]?.trim()
|
|
40
41
|
if (gitDirectory === undefined) throw new Error(`${gitPath} has no gitdir line.`)
|
|
41
|
-
const linkedDirectory =
|
|
42
|
-
const commonDirectory = await fs.read(
|
|
43
|
-
(text) =>
|
|
42
|
+
const linkedDirectory = resolve(workTree, gitDirectory)
|
|
43
|
+
const commonDirectory = await fs.read(resolve(linkedDirectory, 'commondir')).then(
|
|
44
|
+
(text) => resolve(linkedDirectory, text.trim()),
|
|
44
45
|
() => linkedDirectory,
|
|
45
46
|
)
|
|
46
47
|
return (await fs.stat(commonDirectory)).realPath ?? commonDirectory
|
|
@@ -2,7 +2,8 @@ import { matchesGlob } from '../../utils/matches-glob.js'
|
|
|
2
2
|
import { parseShell } from '../../utils/parse-shell.js'
|
|
3
3
|
import { callBaseOf, type CallBase, type CallEffects, type CommandCall, type FetchCall, type FileAccess, type FileCall, type ToolCall, type ToolUse } from '../../utils/call-effects.js'
|
|
4
4
|
import type { Workspace } from './find-project-scope.js'
|
|
5
|
-
import {
|
|
5
|
+
import { realPathOf, relativePath } from '../../utils/paths.js'
|
|
6
|
+
import { basename } from '../../vendor.js'
|
|
6
7
|
|
|
7
8
|
export type TargetCalls = { command: CommandCall; read: FileCall; write: FileCall; fetch: FetchCall; subagent: CallBase; tool: CallBase }
|
|
8
9
|
export type TargetKey = keyof TargetCalls
|
|
@@ -56,11 +57,11 @@ export async function matchTarget(target: Target, use: ToolUse, effects: CallEff
|
|
|
56
57
|
function commandPattern(pattern: string): [string, ...string[]] {
|
|
57
58
|
const command = parseShell(pattern).commands[0]
|
|
58
59
|
if (command === undefined) throw new Error(`The command pattern '${pattern}' names no program.`)
|
|
59
|
-
return command
|
|
60
|
+
return command.argv
|
|
60
61
|
}
|
|
61
62
|
|
|
62
63
|
function commandMatches(pattern: [string, ...string[]], argv: [string, ...string[]]): boolean {
|
|
63
|
-
if (
|
|
64
|
+
if (basename(argv[0]) !== basename(pattern[0])) return false
|
|
64
65
|
const wanted = splitArguments(pattern.slice(1))
|
|
65
66
|
const actual = splitArguments(argv.slice(1))
|
|
66
67
|
let next = 0
|
package/src/mod.ts
CHANGED
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
import type {
|
|
2
|
+
AgentSpawnArgs,
|
|
3
|
+
AgentSpawnResult,
|
|
2
4
|
AskOptions,
|
|
3
5
|
ClassicHookInputs,
|
|
4
6
|
CmodDependencies,
|
|
5
7
|
FsEntry,
|
|
8
|
+
FsStat,
|
|
9
|
+
FsStatOptions,
|
|
6
10
|
HookStream,
|
|
7
11
|
HttpInit,
|
|
8
12
|
HttpResponse,
|
|
13
|
+
PaneOpenArgs,
|
|
9
14
|
PermissionRequestDecision,
|
|
10
15
|
ProcessRunInit,
|
|
11
16
|
ProcessRunResult,
|
|
@@ -15,6 +20,9 @@ import type {
|
|
|
15
20
|
RenderElement,
|
|
16
21
|
Settings,
|
|
17
22
|
SettingsReadArgs,
|
|
23
|
+
ToastOptions,
|
|
24
|
+
UiScrollArgs,
|
|
25
|
+
UiScrollResult,
|
|
18
26
|
} from 'claude-code'
|
|
19
27
|
import type { Claude } from './runtime/claude.js'
|
|
20
28
|
import type { RoutedEvent } from './runtime/hooks.js'
|
|
@@ -30,9 +38,9 @@ export type ProgressStep = {
|
|
|
30
38
|
}
|
|
31
39
|
|
|
32
40
|
export type PaneHandle = {
|
|
33
|
-
open(): Promise<void>
|
|
41
|
+
open(options?: Readonly<Pick<PaneOpenArgs, 'focus'>>): Promise<void>
|
|
34
42
|
close(): Promise<void>
|
|
35
|
-
toggle(): Promise<void>
|
|
43
|
+
toggle(options?: Readonly<Pick<PaneOpenArgs, 'focus'>>): Promise<void>
|
|
36
44
|
readonly isOpen: boolean
|
|
37
45
|
}
|
|
38
46
|
|
|
@@ -59,6 +67,7 @@ export type ModEvent =
|
|
|
59
67
|
| 'Stop'
|
|
60
68
|
| 'StopFailure'
|
|
61
69
|
| 'CwdChanged'
|
|
70
|
+
| 'FileChanged'
|
|
62
71
|
|
|
63
72
|
export type HookAnswer = {
|
|
64
73
|
continue?: boolean
|
|
@@ -104,7 +113,9 @@ type HookInputs = Omit<ClassicHookInputs, 'PreToolUse' | 'PostToolUse' | 'PostTo
|
|
|
104
113
|
CwdChanged: Omit<ClassicHookInputs['CwdChanged'], 'transcript_path'>
|
|
105
114
|
}
|
|
106
115
|
|
|
107
|
-
export type
|
|
116
|
+
export type HookInput<E extends ModEvent> = HookInputs[E]
|
|
117
|
+
|
|
118
|
+
export type ModHook<E extends ModEvent> = (input: HookInput<E>) => HookAnswer | void | Promise<HookAnswer | void>
|
|
108
119
|
|
|
109
120
|
export type Mod<State extends object = Record<never, never>> = {
|
|
110
121
|
readonly name: string
|
|
@@ -115,9 +126,10 @@ export type Mod<State extends object = Record<never, never>> = {
|
|
|
115
126
|
readonly ui: {
|
|
116
127
|
pane(pane: Pane<State>): PaneHandle
|
|
117
128
|
render<S extends Slot>(slot: S, Component: (props: SlotProps<S>) => RenderElement): void
|
|
118
|
-
toast(text: string): void
|
|
129
|
+
toast(text: string, options?: ToastOptions): void
|
|
119
130
|
progress<T>(title: string, task: (report: (step: ProgressStep) => void) => Promise<T>): Promise<T>
|
|
120
131
|
ask(question: string, options?: readonly string[] | AskOptions): Promise<string>
|
|
132
|
+
scroll(args: UiScrollArgs): Promise<UiScrollResult>
|
|
121
133
|
}
|
|
122
134
|
readonly process: {
|
|
123
135
|
run(argv: readonly string[], init?: ProcessRunInit): Promise<ProcessRunResult>
|
|
@@ -127,6 +139,8 @@ export type Mod<State extends object = Record<never, never>> = {
|
|
|
127
139
|
read(path: string): Promise<string>
|
|
128
140
|
write(path: string, text: string): Promise<void>
|
|
129
141
|
list(path?: string): Promise<FsEntry[]>
|
|
142
|
+
exists(path: string): Promise<boolean>
|
|
143
|
+
stat(path: string, options?: FsStatOptions): Promise<FsStat>
|
|
130
144
|
}
|
|
131
145
|
readonly http: {
|
|
132
146
|
fetch(url: string, init?: HttpInit): Promise<HttpResponse>
|
|
@@ -134,6 +148,12 @@ export type Mod<State extends object = Record<never, never>> = {
|
|
|
134
148
|
readonly settings: {
|
|
135
149
|
read(args?: SettingsReadArgs): Promise<Settings>
|
|
136
150
|
}
|
|
151
|
+
readonly session: {
|
|
152
|
+
messages: Claude['session']['messages']
|
|
153
|
+
}
|
|
154
|
+
readonly agent: {
|
|
155
|
+
spawn(args: AgentSpawnArgs): Promise<AgentSpawnResult>
|
|
156
|
+
}
|
|
137
157
|
readonly projectRoot: string
|
|
138
158
|
readonly cwd: string
|
|
139
159
|
readonly dependencies: CmodDependencies
|
package/src/path.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { basename, dirname, extname, isAbsolute, join, normalize, relative, resolve } from './vendor.js'
|
package/src/register.ts
CHANGED
|
@@ -21,6 +21,7 @@ async function startMod($: EngineInterface, eventInput: Frozen<Args<'session.sta
|
|
|
21
21
|
open: (pane) => $.ui.open(pane),
|
|
22
22
|
close: (pane) => $.ui.close(pane),
|
|
23
23
|
panes: () => $.ui.panes(),
|
|
24
|
+
scroll: (args) => $.ui.scroll(args),
|
|
24
25
|
resolve: (render) => $.ui.resolve(render),
|
|
25
26
|
},
|
|
26
27
|
process: {
|
|
@@ -54,10 +55,14 @@ async function startMod($: EngineInterface, eventInput: Frozen<Args<'session.sta
|
|
|
54
55
|
model: () => $.session.model(),
|
|
55
56
|
usage: () => $.session.usage(),
|
|
56
57
|
surfaces: () => $.session.surfaces(),
|
|
58
|
+
messages: ((args?: Parameters<Claude['session']['messages']>[0]) => (args === undefined ? $.session.messages() : $.session.messages(args))) as Claude['session']['messages'],
|
|
57
59
|
},
|
|
58
60
|
command: { register: (command) => $.command.register(command) },
|
|
59
61
|
tool: { register: (tool) => $.tool.register(tool) },
|
|
60
|
-
agent: {
|
|
62
|
+
agent: {
|
|
63
|
+
list: () => $.agent.list(),
|
|
64
|
+
spawn: (args) => $.agent.spawn(args),
|
|
65
|
+
},
|
|
61
66
|
env: {
|
|
62
67
|
home: () => $.env.get('HOME'),
|
|
63
68
|
dataHome: () => $.env.get('XDG_DATA_HOME'),
|
|
@@ -91,6 +96,7 @@ export function registerMod<State extends object>(addHook: On, definition: ModDe
|
|
|
91
96
|
addHook('classic.PreCompact', routeToMod)
|
|
92
97
|
addHook('classic.Stop', routeToMod)
|
|
93
98
|
addHook('classic.StopFailure', routeToMod)
|
|
99
|
+
addHook('classic.FileChanged', routeToMod)
|
|
94
100
|
addHook('tool.call', routeToMod)
|
|
95
101
|
addHook('prompt.submit', routeToMod)
|
|
96
102
|
addHook('prompt.context', routeToMod)
|
|
@@ -99,6 +105,7 @@ export function registerMod<State extends object>(addHook: On, definition: ModDe
|
|
|
99
105
|
addHook('skill.prompt', routeToMod)
|
|
100
106
|
addHook('ui.render', routeToMod)
|
|
101
107
|
addHook('ui.press', routeToMod)
|
|
108
|
+
addHook('ui.scroll', routeToMod)
|
|
102
109
|
addHook('ui.close', routeToMod)
|
|
103
110
|
addHook('cmod.call', routeToMod)
|
|
104
111
|
}
|
|
@@ -106,6 +113,5 @@ export function registerMod<State extends object>(addHook: On, definition: ModDe
|
|
|
106
113
|
export function registerPermissionCheck(addHook: On): void {
|
|
107
114
|
checksPermissions = true
|
|
108
115
|
addHook('tool.check', routeToMod)
|
|
109
|
-
addHook('classic.PreToolUse', routeToMod)
|
|
110
116
|
addHook('classic.PermissionRequest', routeToMod)
|
|
111
117
|
}
|
package/src/runtime/claude.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
AgentInfo,
|
|
3
|
+
AgentSpawnArgs,
|
|
4
|
+
AgentSpawnResult,
|
|
3
5
|
Args,
|
|
6
|
+
EngineInterface,
|
|
4
7
|
AskOptions,
|
|
5
8
|
CommandSpec,
|
|
6
9
|
ElementTable,
|
|
@@ -29,6 +32,8 @@ import type {
|
|
|
29
32
|
UiLogOptions,
|
|
30
33
|
UiOpenResult,
|
|
31
34
|
UiPane,
|
|
35
|
+
UiScrollArgs,
|
|
36
|
+
UiScrollResult,
|
|
32
37
|
} from 'claude-code'
|
|
33
38
|
|
|
34
39
|
export type Claude = {
|
|
@@ -43,6 +48,7 @@ export type Claude = {
|
|
|
43
48
|
open(pane: PaneOpenArgs): Promise<UiOpenResult>
|
|
44
49
|
close(pane: PaneCloseArgs): Promise<void>
|
|
45
50
|
panes(): Promise<readonly UiPane[]>
|
|
51
|
+
scroll(args: UiScrollArgs): Promise<UiScrollResult>
|
|
46
52
|
resolve(e: ResolveInput): ElementTable<RenderSurface>
|
|
47
53
|
}
|
|
48
54
|
readonly process: {
|
|
@@ -76,10 +82,14 @@ export type Claude = {
|
|
|
76
82
|
model(): Promise<string>
|
|
77
83
|
usage(): Promise<SessionUsage>
|
|
78
84
|
surfaces(): Promise<readonly RenderSurface[]>
|
|
85
|
+
messages: EngineInterface['session']['messages']
|
|
79
86
|
}
|
|
80
87
|
readonly command: { register(command: CommandSpec): Promise<{ command: string }> }
|
|
81
88
|
readonly tool: { register(tool: ToolSpec): Promise<{ tool: string }> }
|
|
82
|
-
readonly agent: {
|
|
89
|
+
readonly agent: {
|
|
90
|
+
list(): Promise<AgentInfo[]>
|
|
91
|
+
spawn(args: AgentSpawnArgs): Promise<AgentSpawnResult>
|
|
92
|
+
}
|
|
83
93
|
readonly env: {
|
|
84
94
|
home(): Promise<string | undefined>
|
|
85
95
|
dataHome(): Promise<string | undefined>
|