@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.
Files changed (86) hide show
  1. package/docs/commands.md +2 -2
  2. package/docs/hooks.md +62 -6
  3. package/docs/index.md +7 -4
  4. package/docs/install-steps.md +3 -3
  5. package/docs/jobs.md +5 -4
  6. package/docs/mod.md +57 -7
  7. package/docs/testing.md +25 -9
  8. package/docs/ui.md +134 -5
  9. package/jobs/check.js +2 -2
  10. package/jobs/context.d.ts.map +1 -1
  11. package/jobs/context.js +2 -0
  12. package/jobs/permissions/decide-permission.js +2 -2
  13. package/jobs/permissions/find-project-scope.d.ts +1 -1
  14. package/jobs/permissions/find-project-scope.d.ts.map +1 -1
  15. package/jobs/permissions/find-project-scope.js +9 -9
  16. package/jobs/permissions/match-target.d.ts.map +1 -1
  17. package/jobs/permissions/match-target.js +4 -3
  18. package/mod.d.ts +16 -6
  19. package/mod.d.ts.map +1 -1
  20. package/package.json +2 -1
  21. package/path.d.ts +2 -0
  22. package/path.d.ts.map +1 -0
  23. package/path.js +1 -0
  24. package/register.d.ts.map +1 -1
  25. package/register.js +8 -2
  26. package/runtime/claude.d.ts +4 -1
  27. package/runtime/claude.d.ts.map +1 -1
  28. package/runtime/hooks.d.ts +10 -2
  29. package/runtime/hooks.d.ts.map +1 -1
  30. package/runtime/hooks.js +56 -18
  31. package/runtime/lifecycle.d.ts.map +1 -1
  32. package/runtime/lifecycle.js +12 -2
  33. package/runtime/ui.d.ts.map +1 -1
  34. package/runtime/ui.js +42 -26
  35. package/shell.d.ts +2 -0
  36. package/shell.d.ts.map +1 -0
  37. package/shell.js +1 -0
  38. package/src/jobs/check.ts +2 -2
  39. package/src/jobs/context.ts +2 -0
  40. package/src/jobs/permissions/decide-permission.ts +2 -2
  41. package/src/jobs/permissions/find-project-scope.ts +11 -10
  42. package/src/jobs/permissions/match-target.ts +4 -3
  43. package/src/mod.ts +24 -4
  44. package/src/path.ts +1 -0
  45. package/src/register.ts +8 -2
  46. package/src/runtime/claude.ts +11 -1
  47. package/src/runtime/hooks.ts +62 -23
  48. package/src/runtime/lifecycle.ts +11 -4
  49. package/src/runtime/ui.ts +39 -26
  50. package/src/shell.ts +1 -0
  51. package/src/testing/fake-claude.ts +14 -5
  52. package/src/testing/fake-elements.ts +5 -7
  53. package/src/testing/fake-files.ts +5 -5
  54. package/src/testing.ts +83 -11
  55. package/src/ui/components.ts +70 -0
  56. package/src/ui/define-pane.ts +5 -1
  57. package/src/utils/call-effects.ts +12 -11
  58. package/src/utils/parse-shell.ts +28 -36
  59. package/src/utils/paths.ts +4 -19
  60. package/src/vendor.ts +1 -0
  61. package/testing/fake-claude.d.ts +5 -0
  62. package/testing/fake-claude.d.ts.map +1 -1
  63. package/testing/fake-claude.js +13 -3
  64. package/testing/fake-elements.d.ts +5 -8
  65. package/testing/fake-elements.d.ts.map +1 -1
  66. package/testing/fake-elements.js +6 -6
  67. package/testing/fake-files.js +5 -5
  68. package/testing.d.ts +4 -2
  69. package/testing.d.ts.map +1 -1
  70. package/testing.js +65 -9
  71. package/ui/components.d.ts +48 -0
  72. package/ui/components.d.ts.map +1 -0
  73. package/ui/components.js +56 -0
  74. package/ui/define-pane.d.ts +5 -1
  75. package/ui/define-pane.d.ts.map +1 -1
  76. package/utils/call-effects.d.ts.map +1 -1
  77. package/utils/call-effects.js +12 -11
  78. package/utils/parse-shell.d.ts +5 -9
  79. package/utils/parse-shell.d.ts.map +1 -1
  80. package/utils/parse-shell.js +5 -10
  81. package/utils/paths.d.ts +0 -3
  82. package/utils/paths.d.ts.map +1 -1
  83. package/utils/paths.js +3 -18
  84. package/vendor.d.ts +1 -0
  85. package/vendor.d.ts.map +1 -1
  86. 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.PreToolUse', 'classic.PermissionRequest'];
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(event, (await next(e)), ours);
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 strictnessOf(result) {
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,EAAE,GAAG,EAAE,aAAa,EAAqB,MAAM,WAAW,CAAA;AAItE,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AAGzC,OAAO,EAAgD,KAAK,WAAW,EAAE,MAAM,YAAY,CAAA;AAC3F,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"}
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"}
@@ -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
- router.add(`classic.${event}`, classicHook(definition.name, event, hook, claude, agents));
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)}`);
@@ -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;AAM9D,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;AAqHD,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,CAwLjI"}
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 count = `${done}/${total}`;
81
- const width = barWidth(columns, `${line.title}${count}${label}`.length);
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
- return claude.ui.open({ id: pane.id, title: pane.title, ...size });
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
- return drawWith(table, () => pane.render(mod(), e.props), markdownIn(table));
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.close', (e, next) => {
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
- return next(e);
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
@@ -0,0 +1,2 @@
1
+ export { parseShell, type ParsedShell, type ShellCommand } from './utils/parse-shell.js';
2
+ //# sourceMappingURL=shell.d.ts.map
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 { parentOf } from '../utils/paths.js'
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: parentOf(last) }
55
+ if (last !== undefined) return { command: [...run, ...existing], folder: dirname(last) }
56
56
  return commandFolder === undefined ? undefined : { command: [...run], folder: commandFolder }
57
57
  }
@@ -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 { resolvePath } from '../../utils/paths.js'
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: resolvePath(segment, folder) }, access: undefined, folder }))
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 { nameOf, parentOf, resolvePath, type FileSystem } from '../../utils/paths.js'
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 = parentOf(pluginRoot)
9
- const claude = parentOf(skills)
10
- const root = parentOf(claude)
11
- if (nameOf(skills) !== 'skills' || nameOf(claude) !== '.claude' || root === resolvePath(home, '/')) return undefined
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 = parentOf(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 = resolvePath('.git', workTree)
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 = resolvePath(gitDirectory, workTree)
42
- const commonDirectory = await fs.read(resolvePath('commondir', linkedDirectory)).then(
43
- (text) => resolvePath(text.trim(), linkedDirectory),
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 { nameOf, realPathOf, relativePath } from '../../utils/paths.js'
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 (nameOf(argv[0]) !== nameOf(pattern[0])) return false
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 ModHook<E extends ModEvent> = (input: HookInputs[E]) => HookAnswer | void | Promise<HookAnswer | void>
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: { list: () => $.agent.list() },
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
  }
@@ -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: { list(): Promise<AgentInfo[]> }
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>