@cmodjs/core 1.5.1 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (130) hide show
  1. package/docs/commands.md +5 -5
  2. package/docs/index.md +12 -11
  3. package/docs/jobs.md +12 -16
  4. package/docs/mod.md +117 -39
  5. package/docs/permissions.md +17 -21
  6. package/docs/session.md +171 -82
  7. package/docs/state.md +5 -5
  8. package/docs/testing.md +57 -41
  9. package/docs/ui.md +9 -9
  10. package/jobs/permissions/decide-permission.d.ts.map +1 -1
  11. package/jobs/permissions/decide-permission.js +1 -1
  12. package/jobs/prompt.d.ts +1 -3
  13. package/jobs/prompt.d.ts.map +1 -1
  14. package/jobs/prompt.js +2 -21
  15. package/jobs/slash-command.d.ts +1 -1
  16. package/jobs/slash-command.d.ts.map +1 -1
  17. package/jobs/slash-command.js +1 -1
  18. package/jobs/status-line.d.ts +0 -1
  19. package/jobs/status-line.d.ts.map +1 -1
  20. package/jobs/status-line.js +3 -4
  21. package/mod.d.ts +53 -105
  22. package/mod.d.ts.map +1 -1
  23. package/mod.js +18 -80
  24. package/package.json +1 -1
  25. package/records.d.ts +3 -1
  26. package/records.d.ts.map +1 -1
  27. package/records.js +14 -5
  28. package/register.d.ts.map +1 -1
  29. package/register.js +3 -1
  30. package/runtime/before.d.ts +5 -1
  31. package/runtime/before.d.ts.map +1 -1
  32. package/runtime/before.js +81 -39
  33. package/runtime/claude.d.ts +6 -1
  34. package/runtime/claude.d.ts.map +1 -1
  35. package/runtime/conversations.d.ts.map +1 -1
  36. package/runtime/conversations.js +45 -26
  37. package/runtime/deadline.d.ts +17 -0
  38. package/runtime/deadline.d.ts.map +1 -1
  39. package/runtime/deadline.js +34 -0
  40. package/runtime/dependencies.d.ts +2 -0
  41. package/runtime/dependencies.d.ts.map +1 -1
  42. package/runtime/dependencies.js +12 -0
  43. package/runtime/grants.d.ts +1 -1
  44. package/runtime/grants.d.ts.map +1 -1
  45. package/runtime/grants.js +19 -12
  46. package/runtime/hooks.d.ts +28 -11
  47. package/runtime/hooks.d.ts.map +1 -1
  48. package/runtime/hooks.js +20 -123
  49. package/runtime/lifecycle.d.ts +2 -1
  50. package/runtime/lifecycle.d.ts.map +1 -1
  51. package/runtime/lifecycle.js +67 -50
  52. package/runtime/sessions.d.ts +31 -5
  53. package/runtime/sessions.d.ts.map +1 -1
  54. package/runtime/sessions.js +342 -81
  55. package/runtime/tool-calls.d.ts +0 -1
  56. package/runtime/tool-calls.d.ts.map +1 -1
  57. package/runtime/tool-calls.js +0 -19
  58. package/session/context/turn/message/instruction-file.d.ts +9 -0
  59. package/session/context/turn/message/instruction-file.d.ts.map +1 -0
  60. package/session/context/turn/message/instruction-file.js +4 -0
  61. package/session/context/turn/message/message.d.ts +5 -4
  62. package/session/context/turn/message/message.d.ts.map +1 -1
  63. package/session/context/turn/message/message.js +7 -6
  64. package/session/context/turn/message/sender.d.ts +2 -1
  65. package/session/context/turn/message/sender.d.ts.map +1 -1
  66. package/session/context/turn/message/sender.js +4 -1
  67. package/session/context/turn/message/tool-call.d.ts +5 -0
  68. package/session/context/turn/message/tool-call.d.ts.map +1 -1
  69. package/session/context/turn/message/tool-result.d.ts +6 -1
  70. package/session/context/turn/message/tool-result.d.ts.map +1 -1
  71. package/session/context/turn/turn.d.ts +7 -0
  72. package/session/context/turn/turn.d.ts.map +1 -1
  73. package/session/conversation.d.ts +10 -3
  74. package/session/conversation.d.ts.map +1 -1
  75. package/session/conversation.js +14 -5
  76. package/session/goal/goal.d.ts +1 -1
  77. package/session/goal/goal.d.ts.map +1 -1
  78. package/session/goal/task.d.ts +2 -2
  79. package/session/goal/task.d.ts.map +1 -1
  80. package/session/session.d.ts +361 -26
  81. package/session/session.d.ts.map +1 -1
  82. package/session/session.js +40 -0
  83. package/src/jobs/permissions/decide-permission.ts +2 -1
  84. package/src/jobs/prompt.ts +3 -20
  85. package/src/jobs/slash-command.ts +2 -2
  86. package/src/jobs/status-line.ts +4 -7
  87. package/src/mod.ts +52 -147
  88. package/src/records.ts +15 -5
  89. package/src/register.ts +3 -1
  90. package/src/runtime/before.ts +90 -37
  91. package/src/runtime/claude.ts +5 -1
  92. package/src/runtime/conversations.ts +41 -25
  93. package/src/runtime/deadline.ts +38 -0
  94. package/src/runtime/dependencies.ts +12 -0
  95. package/src/runtime/grants.ts +15 -10
  96. package/src/runtime/hooks.ts +48 -116
  97. package/src/runtime/lifecycle.ts +66 -51
  98. package/src/runtime/sessions.ts +366 -90
  99. package/src/runtime/tool-calls.ts +0 -19
  100. package/src/session/context/turn/message/instruction-file.ts +13 -0
  101. package/src/session/context/turn/message/message.ts +11 -9
  102. package/src/session/context/turn/message/sender.ts +5 -2
  103. package/src/session/context/turn/message/tool-call.ts +3 -0
  104. package/src/session/context/turn/message/tool-result.ts +6 -1
  105. package/src/session/context/turn/turn.ts +4 -0
  106. package/src/session/conversation.ts +21 -8
  107. package/src/session/goal/goal.ts +1 -1
  108. package/src/session/goal/task.ts +2 -2
  109. package/src/session/session.ts +227 -24
  110. package/src/testing/claude-code.ts +23 -0
  111. package/src/testing/fake-claude.ts +5 -1
  112. package/src/testing/fake-sessions.ts +38 -24
  113. package/src/testing.ts +84 -16
  114. package/testing/claude-code.d.ts +12 -0
  115. package/testing/claude-code.d.ts.map +1 -0
  116. package/testing/claude-code.js +10 -0
  117. package/testing/fake-claude.d.ts +4 -0
  118. package/testing/fake-claude.d.ts.map +1 -1
  119. package/testing/fake-claude.js +4 -1
  120. package/testing/fake-sessions.d.ts +8 -6
  121. package/testing/fake-sessions.d.ts.map +1 -1
  122. package/testing/fake-sessions.js +35 -20
  123. package/testing.d.ts +38 -9
  124. package/testing.d.ts.map +1 -1
  125. package/testing.js +54 -5
  126. package/docs/hooks.md +0 -259
  127. package/session/context/turn/message/document.d.ts +0 -7
  128. package/session/context/turn/message/document.d.ts.map +0 -1
  129. package/session/context/turn/message/document.js +0 -1
  130. package/src/session/context/turn/message/document.ts +0 -8
package/runtime/grants.js CHANGED
@@ -1,4 +1,4 @@
1
- import { isObject, permissionWords } from '../records.js';
1
+ import { isObject, isSettingCovered, permissionWords, settingPaths } from '../records.js';
2
2
  import { relativePath } from '../utils/paths.js';
3
3
  import { cmodPluginName } from './dependencies.js';
4
4
  import { actPermission, conversationMethods, isAct, toolPermission } from './sessions.js';
@@ -34,9 +34,9 @@ export async function checkGrant(grants, call, item) {
34
34
  export async function checkWrite(grants, call, path) {
35
35
  await checkGrant(grants, call, writeItem(grants, path));
36
36
  }
37
- export async function checkSettings(grants, source, keys) {
38
- for (const key of keys)
39
- await checkGrant(grants, `settings.${source}.update(${key})`, `config:${key}`);
37
+ export async function checkSettings(grants, source, values) {
38
+ for (const path of settingPaths(values).map((each) => each.join('.')))
39
+ await checkGrant(grants, `settings.${source}.update(${path})`, `config:${path}`);
40
40
  }
41
41
  export function checkingGrants(claude, grants) {
42
42
  const gate = (call, item) => checkGrant(grants, call, item);
@@ -72,10 +72,6 @@ export function checkingGrants(claude, grants) {
72
72
  },
73
73
  session: {
74
74
  ...claude.session,
75
- messages: (async (...args) => {
76
- await gate('session.messages', 'conversation');
77
- return claude.session.messages(...args);
78
- }),
79
75
  append: async (args) => {
80
76
  await gate('send', 'prompt');
81
77
  return claude.session.append(args);
@@ -91,13 +87,24 @@ export function checkingGrants(claude, grants) {
91
87
  return claude.prompt.submit(args);
92
88
  },
93
89
  },
90
+ command: {
91
+ ...claude.command,
92
+ run: async (args) => {
93
+ await gate(`commands.run(${args.command})`, 'prompt');
94
+ return claude.command.run(args);
95
+ },
96
+ },
94
97
  cmod: {
95
98
  async call(args) {
96
99
  const named = args.to === cmodPluginName ? conversationCalls[args.method] : undefined;
97
100
  const given = args.method === conversationMethods.act && isObject(args.input) ? String(args.input['action']) : undefined;
98
101
  const action = given !== undefined && isAct(given) ? given : undefined;
99
- if (named !== undefined)
100
- await gate(action ?? named, action === undefined ? 'conversation' : actPermission(action, isObject(args.input) ? args.input['input'] : undefined));
102
+ const isThisSession = isObject(args.input) && args.input['session'] === (await claude.session.id());
103
+ const isDropping = (args.method === conversationMethods.subscribe || args.method === conversationMethods.watchBefore) && isObject(args.input) && Array.isArray(args.input['subscriptions']) && args.input['subscriptions'].length === 0;
104
+ if (named !== undefined && action !== undefined)
105
+ await gate(action, actPermission(action, isObject(args.input) ? args.input['input'] : undefined));
106
+ else if (named !== undefined && !isThisSession && !isDropping)
107
+ await gate(named, 'conversation');
101
108
  return claude.cmod.call(args);
102
109
  },
103
110
  },
@@ -147,6 +154,8 @@ export function isCovered(granted, item, home) {
147
154
  if (split === -1)
148
155
  return false;
149
156
  const name = item.slice(0, split);
157
+ if (name === 'config')
158
+ return isSettingCovered(granted, item.slice(split + 1));
150
159
  const target = expanded(item.slice(split + 1), home);
151
160
  const values = [...granted].filter((each) => each.startsWith(`${name}:`)).map((each) => expanded(each.slice(name.length + 1), home));
152
161
  if (name === 'run')
@@ -189,10 +198,8 @@ function declaration(item, home) {
189
198
  const classicParts = {
190
199
  additionalContext: 'prompt',
191
200
  initialUserMessage: 'prompt',
192
- suppressOriginalPrompt: 'prompt',
193
201
  updatedToolOutput: 'prompt',
194
202
  updatedMCPToolOutput: 'prompt',
195
- updatedInput: 'tools',
196
203
  retry: 'approve',
197
204
  };
198
205
  const blocksKeepWorking = ['classic.Stop', 'classic.PostToolUse'];
@@ -1,19 +1,36 @@
1
- import type { Hook, HookListener } from '../mod.js';
1
+ import type { ClassicHookEvent, ClassicHookInputs, PermissionRequestDecision } from 'claude-code';
2
2
  import type { Claude } from './claude.js';
3
3
  import type { RoutedHook } from './router.js';
4
4
  import type { Failure } from './toaster.js';
5
- import { type ToolCalls } from './tool-calls.js';
6
- export type RoutedEvent = `classic.${Exclude<Hook, 'PreToolUse'>}` | 'tool.check' | 'tool.call' | 'prompt.submit' | 'prompt.context' | 'prompt.attachment' | 'command.run' | 'session.measure' | 'skill.prompt' | 'ui.render' | 'ui.press' | 'ui.scroll' | 'ui.close' | 'cmod.call' | 'session.append' | 'session.compact' | 'session.receive' | 'agent.spawn';
5
+ import { type CallBase } from '../utils/call-effects.js';
6
+ export type Hook = Exclude<ClassicHookEvent, 'PreToolUse'>;
7
+ export type HookAnswer = {
8
+ decision?: 'block';
9
+ reason?: string;
10
+ hookSpecificOutput?: {
11
+ hookEventName?: Hook;
12
+ additionalContext?: string;
13
+ permissionDecision?: 'deny' | 'ask';
14
+ permissionDecisionReason?: string;
15
+ sessionTitle?: string;
16
+ initialUserMessage?: string;
17
+ watchPaths?: string[];
18
+ reloadSkills?: boolean;
19
+ decision?: PermissionRequestDecision;
20
+ retry?: boolean;
21
+ displayContent?: string;
22
+ worktreePath?: string;
23
+ };
24
+ };
25
+ export type HookListener<H extends Hook> = (input: ClassicHookInputs[H]) => HookAnswer | void | Promise<HookAnswer | void>;
26
+ export type RoutedEvent = `classic.${Hook}` | 'tool.check' | 'tool.call' | 'prompt.submit' | 'prompt.context' | 'prompt.attachment' | 'command.run' | 'session.measure' | 'skill.prompt' | 'ui.render' | 'ui.press' | 'ui.scroll' | 'ui.close' | 'cmod.call' | 'session.append' | 'session.compact' | 'session.receive' | 'agent.spawn' | 'turn.start';
7
27
  export declare const permissionEvents: readonly RoutedEvent[];
8
- export declare function classicHook<E extends Exclude<Hook, 'PreToolUse'>>(name: string, event: E, hook: HookListener<E>, claude: Claude, calls: ToolCalls, report: (failure: Failure) => void): RoutedHook<RoutedEvent>;
9
- type HeldDecision = {
10
- readonly decision: 'allow' | 'ask';
11
- readonly reason?: string;
28
+ type CallFiles = {
29
+ readonly read: string[];
30
+ readonly changed: string[];
12
31
  };
13
- export type HeldDecisions = Map<string, HeldDecision>;
14
- export declare function preToolUseHook(name: string, hook: HookListener<'PreToolUse'>, claude: Claude, calls: ToolCalls, held: HeldDecisions | undefined): RoutedHook<'tool.call'>;
15
- export declare function heldDecisionHook(held: HeldDecisions): RoutedHook<'tool.check'>;
32
+ export declare function classicHook<H extends Hook>(name: string, event: H, hook: HookListener<H>, report: (failure: Failure) => void): RoutedHook<RoutedEvent>;
16
33
  export declare function userSkillHook(claude: Claude): RoutedHook<'skill.prompt'>;
17
- export declare const strictness: readonly ['allow', 'ask', 'deny'];
34
+ export declare function callFiles(name: string, claude: Claude, use: CallBase, cwd: string): Promise<CallFiles>;
18
35
  export {};
19
36
  //# sourceMappingURL=hooks.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"hooks.d.ts","sourceRoot":"","sources":["../src/runtime/hooks.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,IAAI,EAAyB,YAAY,EAAE,MAAM,WAAW,CAAA;AAE1E,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AACzC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AAC7C,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAA;AAC3C,OAAO,EAA6B,KAAK,SAAS,EAAE,MAAM,iBAAiB,CAAA;AAM3E,MAAM,MAAM,WAAW,GACnB,WAAW,OAAO,CAAC,IAAI,EAAE,YAAY,CAAC,EAAE,GACxC,YAAY,GACZ,WAAW,GACX,eAAe,GACf,gBAAgB,GAChB,mBAAmB,GACnB,aAAa,GACb,iBAAiB,GACjB,cAAc,GACd,WAAW,GACX,UAAU,GACV,WAAW,GACX,UAAU,GACV,WAAW,GACX,gBAAgB,GAChB,iBAAiB,GACjB,iBAAiB,GACjB,aAAa,CAAA;AAEjB,eAAO,MAAM,gBAAgB,EAAE,SAAS,WAAW,EAAgD,CAAA;AA4CnG,wBAAgB,WAAW,CAAC,CAAC,SAAS,OAAO,CAAC,IAAI,EAAE,YAAY,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,EAAE,IAAI,EAAE,YAAY,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,EAAE,MAAM,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,IAAI,GAAG,UAAU,CAAC,WAAW,CAAC,CAuB/M;AAED,KAAK,YAAY,GAAG;IAAE,QAAQ,CAAC,QAAQ,EAAE,OAAO,GAAG,KAAK,CAAC;IAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,CAAA;AAEpF,MAAM,MAAM,aAAa,GAAG,GAAG,CAAC,MAAM,EAAE,YAAY,CAAC,CAAA;AAErD,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,YAAY,CAAC,YAAY,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,EAAE,IAAI,EAAE,aAAa,GAAG,SAAS,GAAG,UAAU,CAAC,WAAW,CAAC,CA6BzK;AAED,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,aAAa,GAAG,UAAU,CAAC,YAAY,CAAC,CAM9E;AAYD,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,GAAG,UAAU,CAAC,cAAc,CAAC,CAexE;AAgED,eAAO,MAAM,UAAU,YAAI,OAAO,EAAE,KAAK,EAAE,MAAM,CAAU,CAAA"}
1
+ {"version":3,"file":"hooks.d.ts","sourceRoot":"","sources":["../src/runtime/hooks.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,yBAAyB,EAAE,MAAM,aAAa,CAAA;AAEjG,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AACzC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AAC7C,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAA;AAC3C,OAAO,EAAe,KAAK,QAAQ,EAAmB,MAAM,0BAA0B,CAAA;AAKtF,MAAM,MAAM,IAAI,GAAG,OAAO,CAAC,gBAAgB,EAAE,YAAY,CAAC,CAAA;AAE1D,MAAM,MAAM,UAAU,GAAG;IACvB,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,kBAAkB,CAAC,EAAE;QACnB,aAAa,CAAC,EAAE,IAAI,CAAA;QACpB,iBAAiB,CAAC,EAAE,MAAM,CAAA;QAC1B,kBAAkB,CAAC,EAAE,MAAM,GAAG,KAAK,CAAA;QACnC,wBAAwB,CAAC,EAAE,MAAM,CAAA;QACjC,YAAY,CAAC,EAAE,MAAM,CAAA;QACrB,kBAAkB,CAAC,EAAE,MAAM,CAAA;QAC3B,UAAU,CAAC,EAAE,MAAM,EAAE,CAAA;QACrB,YAAY,CAAC,EAAE,OAAO,CAAA;QACtB,QAAQ,CAAC,EAAE,yBAAyB,CAAA;QACpC,KAAK,CAAC,EAAE,OAAO,CAAA;QACf,cAAc,CAAC,EAAE,MAAM,CAAA;QACvB,YAAY,CAAC,EAAE,MAAM,CAAA;KACtB,CAAA;CACF,CAAA;AAED,MAAM,MAAM,YAAY,CAAC,CAAC,SAAS,IAAI,IAAI,CAAC,KAAK,EAAE,iBAAiB,CAAC,CAAC,CAAC,KAAK,UAAU,GAAG,IAAI,GAAG,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC,CAAA;AAE1H,MAAM,MAAM,WAAW,GACnB,WAAW,IAAI,EAAE,GACjB,YAAY,GACZ,WAAW,GACX,eAAe,GACf,gBAAgB,GAChB,mBAAmB,GACnB,aAAa,GACb,iBAAiB,GACjB,cAAc,GACd,WAAW,GACX,UAAU,GACV,WAAW,GACX,UAAU,GACV,WAAW,GACX,gBAAgB,GAChB,iBAAiB,GACjB,iBAAiB,GACjB,aAAa,GACb,YAAY,CAAA;AAEhB,eAAO,MAAM,gBAAgB,EAAE,SAAS,WAAW,EAAgD,CAAA;AAInG,KAAK,SAAS,GAAG;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,CAAC;IAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,EAAE,CAAA;CAAE,CAAA;AAuCxE,wBAAgB,WAAW,CAAC,CAAC,SAAS,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,EAAE,IAAI,EAAE,YAAY,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,IAAI,GAAG,UAAU,CAAC,WAAW,CAAC,CAgBtJ;AAMD,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,GAAG,UAAU,CAAC,cAAc,CAAC,CAexE;AAED,wBAAsB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,QAAQ,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,CAAC,CAgB5G"}
package/runtime/hooks.js CHANGED
@@ -1,26 +1,24 @@
1
1
  import { configFolders } from '../records.js';
2
- import { reservedKeys, toolInputOf } from './tool-calls.js';
3
- import { callEffects, inputOf } from '../utils/call-effects.js';
2
+ import { callEffects } from '../utils/call-effects.js';
4
3
  import { dynamicPattern } from '../utils/parse-shell.js';
5
4
  import { isMissing } from '../utils/paths.js';
6
5
  import { messageOf } from '../utils/text.js';
7
6
  export const permissionEvents = ['tool.check', 'classic.PermissionRequest'];
8
- const flags = ['suppressOriginalPrompt', 'reloadSkills', 'retry'];
7
+ const flags = ['reloadSkills', 'retry'];
9
8
  const readFields = {
10
9
  Setup: ['additionalContext'],
11
10
  SessionStart: ['additionalContext', 'initialUserMessage', 'sessionTitle', 'watchPaths', 'reloadSkills'],
12
11
  SessionEnd: [],
13
- UserPromptSubmit: ['additionalContext', 'sessionTitle', 'suppressOriginalPrompt'],
14
- UserPromptExpansion: ['additionalContext', 'suppressOriginalPrompt'],
12
+ UserPromptSubmit: ['additionalContext', 'sessionTitle'],
13
+ UserPromptExpansion: ['additionalContext'],
15
14
  InstructionsLoaded: [],
16
15
  ConfigChange: [],
17
16
  DirectoryAdded: [],
18
17
  PreModelSwitch: ['permissionDecision', 'permissionDecisionReason'],
19
18
  PostModelSwitch: ['additionalContext'],
20
- PreToolUse: ['additionalContext', 'permissionDecision', 'permissionDecisionReason', 'updatedInput'],
21
19
  PermissionRequest: ['decision'],
22
20
  PermissionDenied: ['retry'],
23
- PostToolUse: ['additionalContext', 'updatedToolOutput', 'updatedMCPToolOutput'],
21
+ PostToolUse: ['additionalContext'],
24
22
  PostToolUseFailure: ['additionalContext'],
25
23
  PostToolBatch: ['additionalContext'],
26
24
  MessageDisplay: ['displayContent'],
@@ -41,27 +39,17 @@ const readFields = {
41
39
  WorktreeCreate: ['worktreePath'],
42
40
  WorktreeRemove: [],
43
41
  };
44
- export function classicHook(name, event, hook, claude, calls, report) {
45
- const hookInputOf = async (e) => {
46
- if (event !== 'PostToolUse' && event !== 'PostToolUseFailure')
47
- return e;
48
- const input = e;
49
- const cwd = calls.cwdOf(input.tool_use_id) ?? input.cwd;
50
- return { ...input, files: await callFiles(name, claude, { tool: input.tool_name, input: inputOf(input.tool_input) }, cwd) };
51
- };
52
- const resultOf = async (e) => {
53
- const answer = await hook((await hookInputOf(e)));
54
- if (answer === undefined)
55
- return undefined;
56
- if (answer.systemMessage !== undefined)
57
- claude.ui.log(answer.systemMessage);
58
- return classicResult(event, answer);
59
- };
42
+ export function classicHook(name, event, hook, report) {
60
43
  const routed = async (e, next) => {
61
- const ours = await resultOf(e).catch((error) => {
44
+ const ours = await Promise.resolve()
45
+ .then(async () => {
46
+ const answer = await hook(e);
47
+ return answer === undefined ? undefined : classicResult(event, answer);
48
+ })
49
+ .catch((error) => {
62
50
  if (event === 'PermissionRequest')
63
- return { decision: { behavior: 'deny', message: `${name}: the ${event} hook failed: ${messageOf(error)}` } };
64
- report({ id: `hook:${event}`, message: `The ${event} hook failed`, error });
51
+ return { decision: { behavior: 'deny', message: `${name}: the ${event} listener failed: ${messageOf(error)}` } };
52
+ report({ id: `hook:${event}`, message: `The ${event} listener failed`, error });
65
53
  return undefined;
66
54
  });
67
55
  if (ours === undefined)
@@ -70,57 +58,6 @@ export function classicHook(name, event, hook, claude, calls, report) {
70
58
  };
71
59
  return routed;
72
60
  }
73
- export function preToolUseHook(name, hook, claude, calls, held) {
74
- return async (e, next) => {
75
- let ours;
76
- try {
77
- const answer = await hook(await preToolUseInput(name, e, claude, calls.agentOf));
78
- if (answer?.systemMessage !== undefined)
79
- claude.ui.log(answer.systemMessage);
80
- ours = answer === undefined ? {} : classicResult('PreToolUse', answer);
81
- }
82
- catch (error) {
83
- return { deny: `${name}: the PreToolUse hook failed: ${messageOf(error)}` };
84
- }
85
- if (typeof ours['deny'] === 'string')
86
- return { deny: ours['deny'] };
87
- const decided = heldDecisionOf(ours);
88
- if (decided !== undefined && held === undefined) {
89
- return { deny: `${name}: the PreToolUse hook answered permissionDecision "${decided.decision}", so hooks/register.ts must call registerPermissionCheck(addHook) after registerMod.` };
90
- }
91
- const updated = ours['updatedInput'];
92
- const reserved = Object.fromEntries(Object.entries(e).filter(([key]) => reservedKeys.includes(key)));
93
- const earlier = held?.get(e.tool_use_id);
94
- if (decided !== undefined)
95
- held?.set(e.tool_use_id, earlier !== undefined && strictness.indexOf(earlier.decision) >= strictness.indexOf(decided.decision) ? earlier : decided);
96
- let result;
97
- try {
98
- result = await next(updated === undefined ? e : { ...reserved, ...updated });
99
- }
100
- finally {
101
- if (decided !== undefined)
102
- held?.delete(e.tool_use_id);
103
- }
104
- const context = ours['additionalContext'];
105
- if (context === undefined || result.deny !== undefined)
106
- return result;
107
- return { ...result, context: [...(result.context ?? []), ...context] };
108
- };
109
- }
110
- export function heldDecisionHook(held) {
111
- return async (e, next) => {
112
- const below = await next(e);
113
- const ours = e.tool_use_id === undefined ? undefined : held.get(e.tool_use_id);
114
- return ours === undefined || below.decision === 'deny' ? below : ours;
115
- };
116
- }
117
- function heldDecisionOf(ours) {
118
- if (ours['allow'] !== undefined)
119
- return { decision: 'allow' };
120
- if (typeof ours['ask'] !== 'string')
121
- return undefined;
122
- return ours['ask'] === '' ? { decision: 'ask' } : { decision: 'ask', reason: ours['ask'] };
123
- }
124
61
  const frontmatter = /^---\r?\n[\s\S]*?\r?\n---[ \t]*(?:\r?\n|$)/;
125
62
  const baseDirectoryLine = /^Base directory for this skill: [^\n]*\n\n/;
126
63
  export function userSkillHook(claude) {
@@ -143,23 +80,7 @@ export function userSkillHook(claude) {
143
80
  return next(e);
144
81
  };
145
82
  }
146
- async function preToolUseInput(name, envelope, claude, agentOf) {
147
- const { tool, tool_use_id } = envelope;
148
- const [session_id, cwd, { agentId, agentType }] = await Promise.all([claude.session.id(), claude.session.cwd(), agentOf(tool_use_id)]);
149
- const tool_input = toolInputOf(envelope);
150
- return {
151
- session_id,
152
- cwd,
153
- hook_event_name: 'PreToolUse',
154
- tool_name: tool,
155
- tool_input,
156
- tool_use_id,
157
- ...(agentId === undefined ? {} : { agent_id: agentId }),
158
- ...(agentType === undefined ? {} : { agent_type: agentType }),
159
- files: await callFiles(name, claude, { tool, input: tool_input }, cwd),
160
- };
161
- }
162
- async function callFiles(name, claude, use, cwd) {
83
+ export async function callFiles(name, claude, use, cwd) {
163
84
  try {
164
85
  const [home, configRoot] = await Promise.all([claude.env.home(), claude.env.configHome()]);
165
86
  if (home === undefined)
@@ -181,42 +102,18 @@ async function callFiles(name, claude, use, cwd) {
181
102
  }
182
103
  function classicResult(event, answer) {
183
104
  const result = {};
184
- const unread = (field) => new Error(`it answered ${field}, which ${event} does not read. Remove it from the answer.`);
185
- if (event === 'PreToolUse') {
186
- if (answer.continue === false || answer.stopReason !== undefined)
187
- throw unread('continue or stopReason');
188
- if (answer.decision === 'block')
189
- result['deny'] = answer.reason ?? '';
190
- }
191
- else {
192
- if (answer.continue === false)
193
- result['preventContinuation'] = true;
194
- if (answer.stopReason !== undefined)
195
- result['stopReason'] = answer.stopReason;
196
- if (answer.decision === 'block')
197
- result['block'] = answer.reason ?? '';
198
- }
105
+ if (answer.decision === 'block')
106
+ result['block'] = answer.reason ?? '';
199
107
  const specific = answer.hookSpecificOutput ?? {};
200
- if (specific.hookEventName !== undefined && specific.hookEventName !== event) {
201
- throw new Error(`it answered hookSpecificOutput.hookEventName "${specific.hookEventName}". Set it to "${event}" or leave it out.`);
202
- }
203
108
  for (const [field, value] of Object.entries(specific)) {
204
- if (field === 'hookEventName' || value === undefined || (value === false && flags.includes(field)))
109
+ if (field === 'hookEventName' || (value === false && flags.includes(field)))
205
110
  continue;
206
111
  if (!readFields[event].includes(field))
207
- throw unread(`hookSpecificOutput.${field}`);
208
- if (field === 'additionalContext')
209
- result['additionalContext'] = [value];
210
- else if (field === 'permissionDecision' && event === 'PreToolUse') {
211
- const reason = specific.permissionDecisionReason ?? '';
212
- result[value] = value === 'allow' ? true : reason;
213
- }
214
- else if (field !== 'permissionDecisionReason' || event !== 'PreToolUse')
215
- result[field] = value;
112
+ throw new Error(`it answered ${field}, which ${event} does not read.`);
113
+ result[field] = field === 'additionalContext' ? [value] : value;
216
114
  }
217
115
  return result;
218
116
  }
219
- export const strictness = ['allow', 'ask', 'deny'];
220
117
  function mergeClassic(below, ours) {
221
118
  const merged = { ...below, ...ours };
222
119
  const context = [...(below['additionalContext'] ?? []), ...(ours['additionalContext'] ?? [])];
@@ -1,5 +1,5 @@
1
1
  import type { Args, EventResult, Frozen, PluginOptions } from 'claude-code';
2
- import { type Mod, type ModDefinition } from '../mod.js';
2
+ import type { Mod, ModDefinition } from '../mod.js';
3
3
  import type { Options } from '../options.js';
4
4
  import { type Steps } from '../records.js';
5
5
  import type { Claude } from './claude.js';
@@ -24,6 +24,7 @@ export type Lifecycle<State extends object> = {
24
24
  readonly toasts: readonly ShownToast[];
25
25
  start(claude: Claude, read: (claude: Claude) => Promise<Plugin>): Promise<void>;
26
26
  route<N extends RoutedEvent>(event: N, e: Frozen<Args<N>>, next: RouterNext<N>): Promise<EventResult<N>>;
27
+ raise(method: string, payload: Readonly<Record<string, unknown>>): Promise<unknown>;
27
28
  };
28
29
  export declare function readPlugin(claude: Claude): Promise<Plugin>;
29
30
  export declare function createLifecycle<State extends object, Declared extends Options = Options>(definition: ModDefinition<State, string, Declared>, checksPermissions?: () => boolean, options?: PluginOptions, asksPerson?: boolean): Lifecycle<State>;
@@ -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,EAAc,aAAa,EAAwB,MAAM,aAAa,CAAA;AAE7G,OAAO,EAAkI,KAAK,GAAG,EAAE,KAAK,aAAa,EAAkD,MAAM,WAAW,CAAA;AACxO,OAAO,KAAK,EAAE,OAAO,EAAgB,MAAM,eAAe,CAAA;AAC1D,OAAO,EAA2T,KAAK,KAAK,EAAE,MAAM,eAAe,CAAA;AAGnW,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AAOzC,OAAO,EAAsG,KAAK,WAAW,EAAE,MAAM,YAAY,CAAA;AAOjJ,OAAO,EAA6B,KAAK,UAAU,EAAE,MAAM,aAAa,CAAA;AAMxE,OAAO,EAA+B,KAAK,UAAU,EAAgB,MAAM,cAAc,CAAA;AAMzF,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;IAC9B,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAA;IACrB,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAA;CACpC,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,QAAQ,CAAC,MAAM,EAAE,SAAS,UAAU,EAAE,CAAA;IACtC,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;AAwCD,wBAAsB,UAAU,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAwBhE;AAkCD,wBAAgB,eAAe,CAAC,KAAK,SAAS,MAAM,EAAE,QAAQ,SAAS,OAAO,GAAG,OAAO,EACtF,UAAU,EAAE,aAAa,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,CAAC,EAClD,iBAAiB,GAAE,MAAM,OAAoB,EAC7C,OAAO,GAAE,aAAkB,EAC3B,UAAU,UAAO,GAChB,SAAS,CAAC,KAAK,CAAC,CAkclB"}
1
+ {"version":3,"file":"lifecycle.d.ts","sourceRoot":"","sources":["../src/runtime/lifecycle.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,WAAW,EAAE,MAAM,EAAc,aAAa,EAAwB,MAAM,aAAa,CAAA;AAE7G,OAAO,KAAK,EAA4C,GAAG,EAAE,aAAa,EAAmF,MAAM,WAAW,CAAA;AAC9K,OAAO,KAAK,EAAE,OAAO,EAAgB,MAAM,eAAe,CAAA;AAC1D,OAAO,EAA2T,KAAK,KAAK,EAAE,MAAM,eAAe,CAAA;AAGnW,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AAOzC,OAAO,EAA8E,KAAK,WAAW,EAAE,MAAM,YAAY,CAAA;AAOzH,OAAO,EAA6B,KAAK,UAAU,EAAE,MAAM,aAAa,CAAA;AAMxE,OAAO,EAA+B,KAAK,UAAU,EAAgB,MAAM,cAAc,CAAA;AAMzF,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;IAC9B,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAA;IACrB,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAA;CACpC,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,QAAQ,CAAC,MAAM,EAAE,SAAS,UAAU,EAAE,CAAA;IACtC,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;IACxG,KAAK,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,GAAG,OAAO,CAAC,OAAO,CAAC,CAAA;CACpF,CAAA;AAyCD,wBAAsB,UAAU,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAwBhE;AAkCD,wBAAgB,eAAe,CAAC,KAAK,SAAS,MAAM,EAAE,QAAQ,SAAS,OAAO,GAAG,OAAO,EACtF,UAAU,EAAE,aAAa,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,CAAC,EAClD,iBAAiB,GAAE,MAAM,OAAoB,EAC7C,OAAO,GAAE,aAAkB,EAC3B,UAAU,UAAO,GAChB,SAAS,CAAC,KAAK,CAAC,CAyclB"}
@@ -1,4 +1,3 @@
1
- import { hooks, PreToolUse } from '../mod.js';
2
1
  import { changelogPage, consentPath, dataFolder, finishStepsMethod, isAtLeast, marketplaceOf, oldestCmodFor, openPageMethod, parseConsent, pendingStepsMethod, parseEvent, permissionWords, readRecord, readSteps, recordPath, scriptsSha256, settingsPagesMethod, storeFolder, updatesToTurnOn } from '../records.js';
3
2
  import { homePrefix, ifMissing, relativePath } from '../utils/paths.js';
4
3
  import { firstLine, formatExit, listed, messageOf } from '../utils/text.js';
@@ -6,9 +5,9 @@ import { beforeDeadline } from './deadline.js';
6
5
  import { announcedOf, announcementMs, announcementOf } from './announce.js';
7
6
  import { bind } from './bound.js';
8
7
  import { serveConversations } from './conversations.js';
9
- import { answerCall, cmodPluginName, dependencyCalls, withBareRefusals } from './dependencies.js';
8
+ import { answerCall, answeringOwnCalls, cmodPluginName, dependencyCalls, withBareRefusals } from './dependencies.js';
10
9
  import { keepEngine } from './engine.js';
11
- import { classicHook, heldDecisionHook, permissionEvents, preToolUseHook, userSkillHook } from './hooks.js';
10
+ import { classicHook, permissionEvents, userSkillHook } from './hooks.js';
12
11
  import { checkedAnswer, checkingGrants, checkSettings, checksAnswers, checkWrite, isCovered, itemOf, passedDown, programAddresses } from './grants.js';
13
12
  import { cmodError, lastLineOf, locatingPrograms, readLines } from './programs.js';
14
13
  import { createModFiles, resolvedPath } from './metadata.js';
@@ -103,6 +102,7 @@ export function createLifecycle(definition, checksPermissions = () => true, opti
103
102
  let activation;
104
103
  let mod;
105
104
  let imports;
105
+ let raise;
106
106
  let failure;
107
107
  let pluginOptions = options;
108
108
  const installer = createInstaller(definition.name, () => claude());
@@ -261,6 +261,7 @@ export function createLifecycle(definition, checksPermissions = () => true, opti
261
261
  const { added } = active;
262
262
  mod = active.mod;
263
263
  imports = active.imports;
264
+ raise = active.raise;
264
265
  router = activeRouter;
265
266
  phase = 'active';
266
267
  runtime.claude.ui.invalidate('ui.render');
@@ -502,6 +503,12 @@ export function createLifecycle(definition, checksPermissions = () => true, opti
502
503
  get toasts() {
503
504
  return runtime?.toaster.shown ?? [];
504
505
  },
506
+ async raise(method, payload) {
507
+ if (raise === undefined || imports === undefined)
508
+ throw new Error(`${definition.name} has not started, so ${method} has no listener yet.`);
509
+ bind(imports);
510
+ return raise(method, payload);
511
+ },
505
512
  async start(claudeCalls, read) {
506
513
  if (runtime !== undefined)
507
514
  return;
@@ -565,9 +572,8 @@ export function createLifecycle(definition, checksPermissions = () => true, opti
565
572
  async function createMod(definition, runtime) {
566
573
  const { router, toaster } = runtime;
567
574
  const report = (failure) => void toaster.fail(failure);
568
- const claude = withBareRefusals(checkingKeys(runtime.claude, toaster.toast, runtime.steps.keys ?? {}));
575
+ const claude = answeringOwnCalls(withBareRefusals(checkingKeys(runtime.claude, toaster.toast, runtime.steps.keys ?? {})), router);
569
576
  const added = [];
570
- const hooksAdded = [];
571
577
  const announce = (feature) => {
572
578
  added.push(feature);
573
579
  };
@@ -640,17 +646,26 @@ async function createMod(definition, runtime) {
640
646
  }));
641
647
  const modState = createState({ name: definition.name, initial: definition.state ?? {}, session, root, claude, report, changed: area.changed, forkParent });
642
648
  const modOptions = createOptions({ name: definition.name, declared: definition.options ?? {}, fromClaude: runtime.options, claude, report, changed: modState.changed });
643
- const held = runtime.checksPermissions() ? new Map() : undefined;
649
+ const hookOn = (hook, listener) => router.add(`classic.${hook}`, classicHook(definition.name, hook, listener, report));
644
650
  const expand = (path) => (homePrefix.test(path) ? resolvedPath(path, places) : path);
645
651
  const settingsFile = (source) => ({
646
652
  read: () => checked.settings.read({ source }),
647
653
  async update(values) {
648
- await checkSettings(grants, source, Object.keys(values));
654
+ await checkSettings(grants, source, values);
649
655
  await claude.cmod.call({ to: cmodPluginName, method: 'updateSettings', input: { mod: definition.name, source, project: modState.root, values } });
650
656
  },
651
657
  });
652
- const client = createSessionsClient({ name: definition.name, claude: checked, report: (id, message, error) => report({ id, message, error }), isGranted: (permission) => flag(permission) });
658
+ const client = createSessionsClient({
659
+ name: definition.name,
660
+ claude: checked,
661
+ report: (id, message, error) => report({ id, message, error }),
662
+ isGranted: (permission) => flag(permission),
663
+ hookOn: (hook, listener) => hookOn(hook, listener),
664
+ routeOn: (event, hook) => router.add(event, hook),
665
+ });
653
666
  let followed;
667
+ let isSetUp = false;
668
+ const watches = new Set();
654
669
  const followFolders = (project, folder) => {
655
670
  if (followed === undefined)
656
671
  return;
@@ -659,7 +674,16 @@ async function createMod(definition, runtime) {
659
674
  };
660
675
  const imports = {
661
676
  sessions: client.sessions,
662
- settings: {
677
+ commands: {
678
+ list: async () => (await checked.command.list()).map(({ name, description, source, plugin }) => ({ name, description, source, ...(plugin === undefined ? {} : { plugin }) })),
679
+ run: async (command, args) => {
680
+ const { text, context } = await checked.command.run({ command, ...(args === undefined ? {} : { args }) });
681
+ return { ...(text === undefined ? {} : { text }), ...(context === undefined || context.length === 0 ? {} : { note: context.join('\n') }) };
682
+ },
683
+ beforeRun: (...args) => client.listen('beforeRun', args),
684
+ onRun: (...args) => client.listen('onRun', args),
685
+ },
686
+ config: {
663
687
  list: async () => (await checked.config.list()).map(({ key, label, description, kind, value, options, provider, isLocked }) => ({
664
688
  key,
665
689
  label,
@@ -670,9 +694,22 @@ async function createMod(definition, runtime) {
670
694
  ...(options === undefined ? {} : { options }),
671
695
  ...(provider.plugin === 'engine' ? {} : { plugin: provider.plugin }),
672
696
  })),
697
+ },
698
+ settings: {
673
699
  user: settingsFile('user'),
674
700
  project: settingsFile('project'),
675
701
  local: settingsFile('local'),
702
+ beforeFileChange: (matcherOrListener, maybeListener) => {
703
+ const listener = (maybeListener ?? matcherOrListener);
704
+ const matcher = maybeListener === undefined ? undefined : matcherOrListener;
705
+ return hookOn('ConfigChange', async ({ source, file_path }) => {
706
+ const change = { source: source === 'skills' ? 'skills' : source.replace('_settings', ''), ...(file_path === undefined ? {} : { path: file_path }) };
707
+ if (matcher !== undefined && !matches(change, matcher))
708
+ return undefined;
709
+ const answer = await listener(change);
710
+ return answer === undefined ? undefined : { decision: 'block', reason: answer.refuse };
711
+ });
712
+ },
676
713
  },
677
714
  model: { complete: (completion, options) => checked.model.complete(completion, options) },
678
715
  fs: {
@@ -685,6 +722,22 @@ async function createMod(definition, runtime) {
685
722
  const { kind, size, mtimeMs, isLink, realPath } = await checked.fs.stat(expand(path), options);
686
723
  return { isFile: kind === 'file', isFolder: kind === 'dir', isLink, size, modifiedAt: mtimeMs, ...(realPath === undefined ? {} : { realPath }) };
687
724
  },
725
+ onChange({ path }, listener) {
726
+ if (isSetUp)
727
+ throw new Error(`${definition.name}: fs.onChange works only in setup, because Claude Code reads the paths it watches when a session starts.`);
728
+ const paths = [path].flat().map((each) => resolvedPath(each, places));
729
+ const watch = { paths, listener };
730
+ watches.add(watch);
731
+ if (watches.size === 1) {
732
+ hookOn('SessionStart', () => ({ hookSpecificOutput: { watchPaths: [...new Set([...watches].flatMap((each) => each.paths))] } }));
733
+ hookOn('FileChanged', ({ file_path, event }) => {
734
+ for (const each of watches)
735
+ if (each.paths.some((watched) => relativePath(watched, file_path) !== undefined))
736
+ void Promise.resolve(each.listener({ path: file_path, type: event })).catch((error) => report({ id: 'fs:onChange', message: 'The fs.onChange listener failed', error }));
737
+ });
738
+ }
739
+ return { dispose: () => void watches.delete(watch) };
740
+ },
688
741
  },
689
742
  metadata: {
690
743
  read: (path) => files.read(path),
@@ -748,26 +801,10 @@ async function createMod(definition, runtime) {
748
801
  files: allowed('files'),
749
802
  config: allowed('config'),
750
803
  },
751
- on(hook, matcherOrListener, listenerOrOptions, maybeOptions) {
752
- const hasMatcher = typeof listenerOrOptions === 'function';
753
- const listener = (hasMatcher ? listenerOrOptions : matcherOrListener);
754
- const matcher = hasMatcher ? matcherOrListener : undefined;
755
- const options = (hasMatcher ? maybeOptions : listenerOrOptions);
756
- if (!hooks.includes(hook))
757
- throw new Error(`${definition.name}: mod.on takes one of Claude Code's hooks, such as PreToolUse, not ${String(hook)}.`);
758
- const matched = matcher === undefined ? listener : (input) => (matches(input, matcher) ? listener(input) : undefined);
759
- const bounded = options?.timeoutMs === undefined ? matched : timedHook(hook, matched, checkedMs(definition.name, `the ${hook} hook's timeoutMs`, options.timeoutMs), claude, toaster.toast);
760
- if (!hooksAdded.includes(hook))
761
- hooksAdded.push(hook);
762
- if (hook === PreToolUse)
763
- return router.add('tool.call', preToolUseHook(definition.name, bounded, claude, agents, held));
764
- const classic = hook;
765
- return router.add(`classic.${classic}`, classicHook(definition.name, classic, bounded, claude, agents, report));
766
- },
767
804
  every: (ms, listener) => timer(ms, 'every', claude.clock.every, listener),
768
805
  after: (ms, listener) => timer(ms, 'after', claude.clock.after, listener),
769
806
  use: (job) => job({ mod, announce, reserveName }),
770
- log: (text) => claude.ui.log(`${definition.name}: ${text}`, { to: 'debug' }),
807
+ log: (text, options) => claude.ui.log(`${definition.name}: ${text}`, { to: options?.to ?? 'debug' }),
771
808
  ui: { ...area.ui, notice: (callId, text) => claude.ui.notice(callId, text) },
772
809
  settings: settingsPages,
773
810
  dependencies: dependencyCalls(claude, (call, task) => beforeDeadline(claude, { ms: dependencyCallMs }, call, task)),
@@ -858,9 +895,9 @@ async function createMod(definition, runtime) {
858
895
  return next(e);
859
896
  const call = e;
860
897
  if (e.method === conversationMethods.changed && call.caller === cmodPluginName) {
861
- if (!flag('conversation'))
862
- return { value: null };
863
898
  const { session: changedSession, change, isInProject } = e.input;
899
+ if (changedSession !== client.sessions.current.id && !flag('conversation'))
900
+ return { value: null };
864
901
  client.changed(changedSession, change, isInProject !== false);
865
902
  return { value: null };
866
903
  }
@@ -894,16 +931,13 @@ async function createMod(definition, runtime) {
894
931
  throw new MissingOptions(missing, missing.map((key) => definition.options?.[key]?.title ?? key));
895
932
  await failsAs('the details of its session did not load', () => followCurrent(undefined));
896
933
  await failsAs('its setup function threw', () => definition.setup(mod));
934
+ isSetUp = true;
897
935
  const permissionHooks = permissionEvents.filter((event) => router.has(event));
898
936
  if (permissionHooks.length > 0 && !runtime.checksPermissions()) {
899
937
  throw new Error(`it decides permissions on ${listed(permissionHooks)}, so hooks/register.ts must call registerPermissionCheck(addHook) after registerMod`);
900
938
  }
901
- if (held !== undefined && hooksAdded.includes(PreToolUse))
902
- router.add('tool.check', heldDecisionHook(held));
903
939
  await failsAs('its open panes did not load', () => area.restorePanes());
904
- if (hooksAdded.length > 0)
905
- added.push(`${hooksAdded.length === 1 ? 'a hook' : 'hooks'} on ${listed(hooksAdded)}`);
906
- return { mod, added, imports };
940
+ return { mod, added, imports, raise: client.raise };
907
941
  }
908
942
  function checkingKeys(claude, toast, keys) {
909
943
  const bound = Object.entries(keys);
@@ -925,23 +959,6 @@ function checkedMs(name, subject, ms) {
925
959
  throw new Error(`${name}: ${subject} is ${ms}. Give a whole number of milliseconds above 0 and at most ${longestTimerMs}.`);
926
960
  return ms;
927
961
  }
928
- function timedHook(event, hook, ms, claude, toast) {
929
- return (input) => new Promise((resolve, reject) => {
930
- const timer = claude.clock.after(ms, () => {
931
- toast.warning(`The ${event} hook passed its ${ms / 1000} s timeout`, { id: `timeout:${event}` });
932
- resolve(undefined);
933
- });
934
- Promise.resolve()
935
- .then(() => hook(input))
936
- .then((answer) => {
937
- timer.cancel();
938
- resolve(answer);
939
- }, (error) => {
940
- timer.cancel();
941
- reject(error);
942
- });
943
- });
944
- }
945
962
  async function failsAs(subject, step) {
946
963
  try {
947
964
  await step();