@cmodjs/core 2.1.0 → 3.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 (70) hide show
  1. package/docs/commands.md +1 -1
  2. package/docs/mod.md +2 -0
  3. package/docs/session.md +9 -1
  4. package/docs/testing.md +4 -2
  5. package/jobs/context.d.ts.map +1 -1
  6. package/jobs/context.js +2 -1
  7. package/jobs/permissions.d.ts.map +1 -1
  8. package/jobs/permissions.js +1 -1
  9. package/jobs/prompt.d.ts.map +1 -1
  10. package/jobs/prompt.js +2 -1
  11. package/jobs/slash-command.d.ts.map +1 -1
  12. package/jobs/slash-command.js +4 -3
  13. package/package.json +1 -1
  14. package/records.d.ts +1 -1
  15. package/records.js +1 -1
  16. package/register.d.ts.map +1 -1
  17. package/register.js +58 -51
  18. package/runtime/before.d.ts +1 -1
  19. package/runtime/before.d.ts.map +1 -1
  20. package/runtime/before.js +6 -5
  21. package/runtime/conversations.d.ts.map +1 -1
  22. package/runtime/conversations.js +46 -19
  23. package/runtime/hooks.d.ts +2 -1
  24. package/runtime/hooks.d.ts.map +1 -1
  25. package/runtime/hooks.js +7 -0
  26. package/runtime/lifecycle.d.ts +2 -1
  27. package/runtime/lifecycle.d.ts.map +1 -1
  28. package/runtime/lifecycle.js +31 -9
  29. package/runtime/permission-check.d.ts +3 -0
  30. package/runtime/permission-check.d.ts.map +1 -1
  31. package/runtime/permission-check.js +19 -6
  32. package/runtime/router.d.ts +3 -1
  33. package/runtime/router.d.ts.map +1 -1
  34. package/runtime/router.js +19 -11
  35. package/runtime/sessions.d.ts +13 -4
  36. package/runtime/sessions.d.ts.map +1 -1
  37. package/runtime/sessions.js +67 -26
  38. package/runtime/toaster.d.ts.map +1 -1
  39. package/runtime/toaster.js +3 -0
  40. package/session/context/turn/message/message.d.ts.map +1 -1
  41. package/session/context/turn/message/message.js +30 -2
  42. package/session/context/turn/message/note.d.ts +7 -0
  43. package/session/context/turn/message/note.d.ts.map +1 -1
  44. package/session/context/turn/message/note.js +12 -0
  45. package/session/session.d.ts +3 -1
  46. package/session/session.d.ts.map +1 -1
  47. package/src/jobs/context.ts +2 -1
  48. package/src/jobs/permissions.ts +8 -4
  49. package/src/jobs/prompt.ts +2 -1
  50. package/src/jobs/slash-command.ts +4 -3
  51. package/src/records.ts +1 -1
  52. package/src/register.ts +63 -52
  53. package/src/runtime/before.ts +7 -6
  54. package/src/runtime/conversations.ts +54 -27
  55. package/src/runtime/hooks.ts +9 -1
  56. package/src/runtime/lifecycle.ts +37 -13
  57. package/src/runtime/permission-check.ts +44 -22
  58. package/src/runtime/router.ts +23 -13
  59. package/src/runtime/sessions.ts +91 -37
  60. package/src/runtime/toaster.ts +2 -0
  61. package/src/session/context/turn/message/message.ts +27 -2
  62. package/src/session/context/turn/message/note.ts +16 -0
  63. package/src/session/session.ts +1 -2
  64. package/src/testing/fake-claude.ts +4 -3
  65. package/src/testing.ts +6 -5
  66. package/testing/fake-claude.d.ts +1 -0
  67. package/testing/fake-claude.d.ts.map +1 -1
  68. package/testing/fake-claude.js +3 -2
  69. package/testing.d.ts.map +1 -1
  70. package/testing.js +5 -4
package/docs/commands.md CHANGED
@@ -205,7 +205,7 @@ Reads every conversation Claude Code saved, as the sessions, contexts, turns, ta
205
205
  - `state` prints what a running conversation resumes from.
206
206
  - `follow` prints each message, turn, context, task, and goal change of the sessions named as Claude Code saves it, until stopped with Ctrl+C. `--running` follows every running session of the project too, and each one that starts later. The cmod plugin runs it to hear other running sessions.
207
207
  - `--json` prints JSON instead of one line each.
208
- - `index` reads every saved conversation into the index at `~/.local/share/cmod/history-6.sqlite`, which holds where each message is in its transcript, not a copy of it, and each conversation's turns, contexts, and tasks. While a mod other than cmod holds the `conversation` permission, the cmod plugin runs it in the background at each session's start, and a second run while one is running exits at once. The index forgets a conversation whose transcript is deleted.
208
+ - `index` reads every saved conversation into the index at `~/.local/share/cmod/history-7.sqlite`, which holds where each message is in its transcript, not a copy of it, and each conversation's turns, contexts, and tasks. While a mod other than cmod holds the `conversation` permission, the cmod plugin runs it in the background at each session's start, and a second run while one is running exits at once. The index forgets a conversation whose transcript is deleted.
209
209
 
210
210
  ## Run in an install script
211
211
 
package/docs/mod.md CHANGED
@@ -135,6 +135,8 @@ export function register(addHook: On, options: PluginOptions): void {
135
135
 
136
136
  A mod that answers, with a `before` method or the `permissions` job, also declares `"approve": true` in its `package.json` `"cmod".permissions`, so the person grants it at install. `cmod check` adds the line when it is missing, and without the grant cmod drops every allow or ask the mod answers, so Claude Code decides those calls itself ([permissions.md](permissions.md)). A mod without `registerPermissionCheck` has no part in Claude Code's permission check, so the plugin directory reads it as one that never answers a permission. Without it, each of those methods and `mod.use(permissions(…))` throws `<mod>: <method> hears Claude Code's permission check, so hooks/register.ts must call registerPermissionCheck(addHook) after registerMod.` where it is called, so a mod that calls one in `setup` does not start. `cmod check` fails a mod whose `src/` uses one while `register.ts` does not call `registerPermissionCheck`.
137
137
 
138
+ Where an organization manages Claude Code, Claude Code passes a mod the person installed over on its classic hooks, the notes it adds to a prompt, its Skill and instruction-file text, and compactions, and runs those only for a plugin the organization lists in managed settings. There, the `on` and `before` methods built on them hear nothing until the organization lists the mod and the cmod plugin. `claude plugin validate` names each such hook.
139
+
138
140
  Keep `register.ts` this small. Claude Code checks a hooks module before it loads it, and refuses some shapes, such as a `$` passed to a function in another module. The plugin directory reads the bundled module too, and flags it when the name of `register`'s first parameter is declared anywhere else in the bundle. `@cmodjs/core` declares `on` as a key, so name that parameter `addHook`. Declare `register` as a function, as here: `cmod publish` bundles the hooks module into one file, and Claude Code refuses a bundled `register` that is not one.
139
141
 
140
142
  ## mod: this mod
package/docs/session.md CHANGED
@@ -165,7 +165,7 @@ sessions.current.onTurnEnd(async (turn, { signal }) => {
165
165
  }, { timeoutMs: 30_000 })
166
166
  ```
167
167
 
168
- An `on` listener that passes its time stops quietly. A `before` listener that passes its time shows the error toast, and what was about to happen goes on as it was. With `onFailure: 'refuse'`, a `before` listener that throws or passes its time refuses instead, with the error as the reason, for a gate that must hold even when it breaks.
168
+ An `on` listener that passes its time stops quietly. A `before` listener that passes its time shows the error toast, and what was about to happen goes on as it was. With `onFailure: 'refuse'`, a `before` listener that throws or passes its time refuses instead, with the error as the reason, for a gate that must hold even when it breaks. The gate holds when Claude Code stops the mod's hook too, as it does when the listener's own code runs past 10 seconds without waiting on a call, and the toast `Claude Code stopped its <hook> hook` says so. On another session, it holds when that session does not answer within 5 seconds, or within what is left of the 8 seconds cmod waits on other sessions for one event, for what the listener's matcher and part cover. A `beforePermissionRequest` listener always refuses when it fails.
169
169
 
170
170
  ### Change what happens
171
171
 
@@ -197,6 +197,8 @@ sessions.current.beforeTurnEnd(async () => ((await testsPass()) ? undefined : {
197
197
 
198
198
  An answer that changes text needs the `prompt` permission, a `beforeToolCall` answer needs `tools`, and a `beforeTaskStart` answer needs `tasks`. Without it the answer changes nothing, and the toast `The <method> listener failed` names the permission. When several listeners answer, each sees what the one before it changed, and the first `refuse` or `result` decides.
199
199
 
200
+ Claude reads each mod's `note` inside the mod's own tag, such as `<tracer_mod>…</tracer_mod>` for the mod `tracer`. Every note Claude received becomes a `Note` whose `from` is `{ type: 'mod', name }`, so `onNote({ from: { type: 'mod', name: 'tracer' } })` hears each note of tracer's that Claude got, and never one Claude Code dropped.
201
+
200
202
  ### The session's own events
201
203
 
202
204
  `sessions.current` is a `CurrentSession`, which also hears what happens to the session itself. Each event has an `on` method, and one a mod can change also has a `before` method. A `before` method's answer reaches Claude Code, and an `on` listener never makes Claude Code wait. These methods hear the main agent, never a subagent.
@@ -222,6 +224,12 @@ An answer that changes text needs the `prompt` permission, a `beforeToolCall` an
222
224
 
223
225
  The four `PermissionCheck` and `PermissionRequest` methods work only in a mod whose `hooks/register.ts` calls `registerPermissionCheck` ([mod.md](mod.md#register-the-mod-with-claude-code)). Without it they throw where they are called.
224
226
 
227
+ At a compaction, Claude reads the `note` at the start of the window the compaction opens, and `title`, `reloadSkills`, and `firstPrompt` apply to the whole session. The payload carries `model` for the main agent and `contextTokenCount` as the compaction left it. `sessions.current.beforeSessionStart` hears the main agent's compactions. `sessions.current.goal.beforeSessionStart` also hears every subagent's, with the subagent in `task`, and its note reaches that subagent's window:
228
+
229
+ ```ts
230
+ sessions.current.goal.beforeSessionStart({ source: 'compact' }, ({ task }) => ({ note: primerFor(task) }))
231
+ ```
232
+
225
233
  Every payload also carries where it happened: `session`, `cwd`, and `agentType?` and `permissionMode?` when Claude Code reports them. A matcher keeps the payloads it matches:
226
234
 
227
235
  ```ts
package/docs/testing.md CHANGED
@@ -97,6 +97,8 @@ Starts the mod: loads its state, runs `setup`, and adds its listeners, panes, an
97
97
  - `raise.beforeTaskStart({ title, agentType?, model? })` asks about a task about to start.
98
98
  - `raise.beforeTurnEnd({ answer?, failure? })` asks about the open turn ending. It rejects when no turn is open: add a `Prompt` first.
99
99
  - `raise.beforeContextEnd()` asks about a compaction of the open context. `{ task }` asks about the compaction of a task's context, a task `tested.startTask` started.
100
+ - `raise.beforeSessionStart({ source: 'compact' })` asks about the main agent's compaction. `{ source: 'compact', task }` asks about a subagent's, which only `sessions.current.goal.beforeSessionStart` hears.
101
+ - A `note` that `raise` returns is the mod's own text. What Claude Code receives, as `claudeCodeOf(tested).fire` shows it, holds the note inside the mod's tag, such as `<guard_mod>…</guard_mod>`.
100
102
 
101
103
  ```ts
102
104
  test('a force push is refused', async () => {
@@ -279,7 +281,7 @@ type Fakes = {
279
281
  prompt: { submit? }
280
282
  model: { complete? }
281
283
  agent: { list?, spawn? }
282
- clock: { after?, every? }
284
+ clock: { now?, after?, every? }
283
285
  cmod: { call? }
284
286
  tool: { call? }
285
287
  }
@@ -292,7 +294,7 @@ Set a fake to answer a call:
292
294
  - `files` already fills the `fs` fakes: a write is read back, and `list` lists the given files. The fake file system resolves a relative path against `/`, not against the session's working folder, so a mod under test reads `${sessions.current.project}/<file>` rather than a relative path. `fs.list()` with no path rejects.
293
295
  - `ui.open` places every pane by default. A fake that answers `{ isPlaced: false }` holds a pane back.
294
296
  - `ui.scroll` answers `{}` by default, as when the window moved. A fake that answers `{ deny }` refuses the scroll.
295
- - `clock.after(ms, fn)` and `clock.every(ms, fn)` each return `{ cancel() }`, and so must a fake of either. `clock.after` runs a real timer by default. `clock.every` never fires unless a fake keeps `fn` for the test to call, so a test of `mod.every` keeps it and calls it once per tick.
297
+ - `clock.now()` answers the real time unless a fake gives its own, so a test of a deadline can move time. `clock.after(ms, fn)` and `clock.every(ms, fn)` each return `{ cancel() }`, and so must a fake of either. `clock.after` runs a real timer by default. `clock.every` never fires unless a fake keeps `fn` for the test to call, so a test of `mod.every` keeps it and calls it once per tick.
296
298
  - `session.append` and `prompt.submit` take every note and prompt by default, into `shown.notes` and `shown.prompts`. A fake that answers `{ deny }` or `{ drop }` refuses one.
297
299
  - `model.complete` has no default: set it to the answer the model gives, such as `async () => ({ isAnswered: true, text: 'no', usage })`.
298
300
  - `settings.read` answers `{}` by default, as Claude Code does with no `settings.json` files.
@@ -1 +1 @@
1
- {"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../src/jobs/context.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,WAAW,CAAA;AAC3C,OAAO,EAAkB,KAAK,QAAQ,EAAE,MAAM,wBAAwB,CAAA;AAItE,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,0BAA0B,CAAA;AACpD,OAAO,EAAoB,KAAK,SAAS,EAAE,MAAM,qCAAqC,CAAA;AACtF,OAAO,EAAY,KAAK,MAAM,EAAE,MAAM,+BAA+B,CAAA;AAKrE,wBAAgB,eAAe,CAAC,EAAE,GAAG,EAAE,EAAE,UAAU,GAAG,MAAM,OAAO,CAAC,SAAS,CAAC,CAmB7E;AAED,wBAAgB,SAAS,CAAC,KAAK,SAAS,MAAM,EAAE,GAAG,EAAE,UAAU,CAAC,KAAK,CAAC,EAAE,YAAY,EAAE,CAAC,IAAI,EAAE,IAAI,EAAE,SAAS,EAAE,SAAS,KAAK,OAAO,CAAC,SAAS,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,SAAS,MAAM,EAAE,GAAG,IAAI,CAW3M;AAED,wBAAsB,QAAQ,CAAC,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,GAAG;IAAE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC,CAE5H;AAED,wBAAgB,cAAc,CAAC,KAAK,EAAE,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC,GAAG,OAAO,CAAC,KAAK,CAAC,CAEzI;AAED,wBAAgB,WAAW,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CASlD"}
1
+ {"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../src/jobs/context.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,WAAW,CAAA;AAC3C,OAAO,EAAkB,KAAK,QAAQ,EAAE,MAAM,wBAAwB,CAAA;AAKtE,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,0BAA0B,CAAA;AACpD,OAAO,EAAoB,KAAK,SAAS,EAAE,MAAM,qCAAqC,CAAA;AACtF,OAAO,EAAY,KAAK,MAAM,EAAE,MAAM,+BAA+B,CAAA;AAKrE,wBAAgB,eAAe,CAAC,EAAE,GAAG,EAAE,EAAE,UAAU,GAAG,MAAM,OAAO,CAAC,SAAS,CAAC,CAmB7E;AAED,wBAAgB,SAAS,CAAC,KAAK,SAAS,MAAM,EAAE,GAAG,EAAE,UAAU,CAAC,KAAK,CAAC,EAAE,YAAY,EAAE,CAAC,IAAI,EAAE,IAAI,EAAE,SAAS,EAAE,SAAS,KAAK,OAAO,CAAC,SAAS,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,SAAS,MAAM,EAAE,GAAG,IAAI,CAW3M;AAED,wBAAsB,QAAQ,CAAC,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,GAAG;IAAE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC,CAE5H;AAED,wBAAgB,cAAc,CAAC,KAAK,EAAE,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC,GAAG,OAAO,CAAC,KAAK,CAAC,CAEzI;AAED,wBAAgB,WAAW,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CASlD"}
package/jobs/context.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { beforeDeadline } from '../runtime/deadline.js';
2
2
  import { engineOf } from '../runtime/engine.js';
3
3
  import { toolInputOf } from '../runtime/tool-calls.js';
4
+ import { modNote } from '../session/context/turn/message/note.js';
4
5
  import { listed } from '../utils/text.js';
5
6
  import { findProjectScope } from './permissions/find-project-scope.js';
6
7
  import { targetOf } from './permissions/match-target.js';
@@ -34,7 +35,7 @@ export function afterCall(job, contextAfter, failed) {
34
35
  if (result.deny !== undefined || result.isError === true)
35
36
  return result;
36
37
  const added = await before.then(([use, workspace]) => contextAfter(use, workspace)).catch(failed);
37
- return added.length === 0 ? result : { ...result, context: [...(result.context ?? []), ...added] };
38
+ return added.length === 0 ? result : { ...result, context: [...(result.context ?? []), ...added.map((note) => modNote(job.mod.name, note))] };
38
39
  });
39
40
  }
40
41
  export async function inFolder(call, folder, workspace) {
@@ -1 +1 @@
1
- {"version":3,"file":"permissions.d.ts","sourceRoot":"","sources":["../src/jobs/permissions.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,GAAG,EAAE,GAAG,EAAE,MAAM,WAAW,CAAA;AAKzC,OAAO,EAAqC,KAAK,eAAe,IAAI,QAAQ,EAAE,KAAK,IAAI,IAAI,OAAO,EAAgB,MAAM,oCAAoC,CAAA;AAK5J,YAAY,EAAE,WAAW,EAAE,SAAS,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,0BAA0B,CAAA;AACtF,YAAY,EAAE,MAAM,EAAE,MAAM,+BAA+B,CAAA;AAC3D,MAAM,MAAM,IAAI,CAAC,KAAK,SAAS,MAAM,GAAG,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,IAAI,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,CAAA;AACnF,MAAM,MAAM,eAAe,CAAC,KAAK,SAAS,MAAM,GAAG,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,IAAI,QAAQ,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,CAAA;AAI/F,wBAAgB,WAAW,CAAC,KAAK,SAAS,MAAM,GAAG,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,EAAE,KAAK,EAAE,eAAe,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,KAAK,KAAK,eAAe,CAAC,KAAK,CAAC,CAAC,GAAG,GAAG,CAAC,IAAI,EAAE,KAAK,CAAC,CA+BrK"}
1
+ {"version":3,"file":"permissions.d.ts","sourceRoot":"","sources":["../src/jobs/permissions.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,GAAG,EAAE,GAAG,EAAE,MAAM,WAAW,CAAA;AAKzC,OAAO,EAAqC,KAAK,eAAe,IAAI,QAAQ,EAAE,KAAK,IAAI,IAAI,OAAO,EAAgB,MAAM,oCAAoC,CAAA;AAK5J,YAAY,EAAE,WAAW,EAAE,SAAS,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,0BAA0B,CAAA;AACtF,YAAY,EAAE,MAAM,EAAE,MAAM,+BAA+B,CAAA;AAC3D,MAAM,MAAM,IAAI,CAAC,KAAK,SAAS,MAAM,GAAG,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,IAAI,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,CAAA;AACnF,MAAM,MAAM,eAAe,CAAC,KAAK,SAAS,MAAM,GAAG,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,IAAI,QAAQ,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,CAAA;AAI/F,wBAAgB,WAAW,CAAC,KAAK,SAAS,MAAM,GAAG,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,EAAE,KAAK,EAAE,eAAe,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,KAAK,KAAK,eAAe,CAAC,KAAK,CAAC,CAAC,GAAG,GAAG,CAAC,IAAI,EAAE,KAAK,CAAC,CAmCrK"}
@@ -35,7 +35,7 @@ export function permissions(rules) {
35
35
  router.add('tool.check', async (e, next) => {
36
36
  const [below, ours] = await Promise.all([next(e), verdictOf(e)]);
37
37
  return ours === undefined ? below : stricterVerdict(below, ours);
38
- });
38
+ }, (_e, error) => ({ decision: 'deny', reason: `Claude Code stopped the permissions job of ${job.mod.name}, so it denies the call: ${error.message}` }));
39
39
  job.announce(count === undefined ? 'permission rules from its state' : `${count} permission rule${count === 1 ? '' : 's'}`);
40
40
  };
41
41
  }
@@ -1 +1 @@
1
- {"version":3,"file":"prompt.d.ts","sourceRoot":"","sources":["../src/jobs/prompt.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,GAAG,EAAc,GAAG,EAAE,MAAM,WAAW,CAAA;AAKrD,OAAO,EAAe,KAAK,IAAI,EAAE,MAAM,0BAA0B,CAAA;AACjE,OAAO,EAAyB,KAAK,MAAM,EAAE,MAAM,+BAA+B,CAAA;AAGlF,KAAK,WAAW,GAAG;IAAE,IAAI,CAAC,EAAE,IAAI,GAAG;QAAE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAA;CAAE,CAAA;AAEhE,KAAK,aAAa,CAAC,KAAK,SAAS,MAAM,IAAI;IACzC,IAAI,EAAE,MAAM,CAAA;IACZ,MAAM,EAAE,MAAM,GAAG,CAAC,CAAC,KAAK,EAAE,WAAW,EAAE,GAAG,EAAE,GAAG,CAAC,KAAK,CAAC,KAAK,MAAM,GAAG,SAAS,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAAC,CAAA;IAC5G,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAA;CAC1B,CAAA;AAID,wBAAgB,MAAM,CAAC,KAAK,SAAS,MAAM,GAAG,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,EAAE,aAAa,CAAC,KAAK,CAAC,GAAG,GAAG,CAAC,IAAI,EAAE,KAAK,CAAC,CAgCzI"}
1
+ {"version":3,"file":"prompt.d.ts","sourceRoot":"","sources":["../src/jobs/prompt.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,GAAG,EAAc,GAAG,EAAE,MAAM,WAAW,CAAA;AAMrD,OAAO,EAAe,KAAK,IAAI,EAAE,MAAM,0BAA0B,CAAA;AACjE,OAAO,EAAyB,KAAK,MAAM,EAAE,MAAM,+BAA+B,CAAA;AAGlF,KAAK,WAAW,GAAG;IAAE,IAAI,CAAC,EAAE,IAAI,GAAG;QAAE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAA;CAAE,CAAA;AAEhE,KAAK,aAAa,CAAC,KAAK,SAAS,MAAM,IAAI;IACzC,IAAI,EAAE,MAAM,CAAA;IACZ,MAAM,EAAE,MAAM,GAAG,CAAC,CAAC,KAAK,EAAE,WAAW,EAAE,GAAG,EAAE,GAAG,CAAC,KAAK,CAAC,KAAK,MAAM,GAAG,SAAS,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAAC,CAAA;IAC5G,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAA;CAC1B,CAAA;AAID,wBAAgB,MAAM,CAAC,KAAK,SAAS,MAAM,GAAG,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,EAAE,aAAa,CAAC,KAAK,CAAC,GAAG,GAAG,CAAC,IAAI,EAAE,KAAK,CAAC,CAgCzI"}
package/jobs/prompt.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { engineOf } from '../runtime/engine.js';
2
2
  import { onStateChange } from '../runtime/state.js';
3
+ import { modNote } from '../session/context/turn/message/note.js';
3
4
  import { listed, messageOf } from '../utils/text.js';
4
5
  import { callEffects } from '../utils/call-effects.js';
5
6
  import { matchTarget, targetOf } from './permissions/match-target.js';
@@ -68,7 +69,7 @@ function addKeptCurrent(job, name, textFor, log) {
68
69
  if (value === undefined || value === given)
69
70
  return;
70
71
  given = value;
71
- await claude.session.append({ message: { type: 'user', content: [{ type: 'text', text: `# ${name}\n${value}` }] } }).then((added) => {
72
+ await claude.session.append({ message: { type: 'user', content: [{ type: 'text', text: modNote(job.mod.name, `# ${name}\n${value}`) }] } }).then((added) => {
72
73
  if (added.deny !== undefined)
73
74
  log(`Claude Code refused the new text: ${added.deny}`);
74
75
  });
@@ -1 +1 @@
1
- {"version":3,"file":"slash-command.d.ts","sourceRoot":"","sources":["../src/jobs/slash-command.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,GAAG,EAAE,GAAG,EAAE,MAAM,WAAW,CAAA;AAOrD,MAAM,MAAM,YAAY,GAAG,MAAM,GAAG;IAAE,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,SAAS,CAAA;AAMhF,wBAAgB,YAAY,CAAC,KAAK,SAAS,MAAM,GAAG,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,EAAE,OAAO,EAAE;IACjF,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAA;IAC5B,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAA;IAC9B,QAAQ,CAAC,SAAS,CAAC,EAAE,IAAI,CAAA;IACzB,QAAQ,CAAC,KAAK,EAAE,CAAC,KAAK,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,MAAM,EAAE,CAAA;KAAE,EAAE,GAAG,EAAE,GAAG,CAAC,KAAK,CAAC,KAAK,YAAY,GAAG,IAAI,GAAG,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC,CAAA;CACxI,GAAG,GAAG,CAAC,UAAU,EAAE,KAAK,CAAC,CAkCzB"}
1
+ {"version":3,"file":"slash-command.d.ts","sourceRoot":"","sources":["../src/jobs/slash-command.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,GAAG,EAAE,GAAG,EAAE,MAAM,WAAW,CAAA;AAQrD,MAAM,MAAM,YAAY,GAAG,MAAM,GAAG;IAAE,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,SAAS,CAAA;AAMhF,wBAAgB,YAAY,CAAC,KAAK,SAAS,MAAM,GAAG,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,EAAE,OAAO,EAAE;IACjF,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAA;IAC5B,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAA;IAC9B,QAAQ,CAAC,SAAS,CAAC,EAAE,IAAI,CAAA;IACzB,QAAQ,CAAC,KAAK,EAAE,CAAC,KAAK,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,MAAM,EAAE,CAAA;KAAE,EAAE,GAAG,EAAE,GAAG,CAAC,KAAK,CAAC,KAAK,YAAY,GAAG,IAAI,GAAG,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC,CAAA;CACxI,GAAG,GAAG,CAAC,UAAU,EAAE,KAAK,CAAC,CAkCzB"}
@@ -1,5 +1,6 @@
1
1
  import { parse } from '../vendor.js';
2
2
  import { engineOf } from '../runtime/engine.js';
3
+ import { modNote } from '../session/context/turn/message/note.js';
3
4
  import { messageOf } from '../utils/text.js';
4
5
  import { withinDeadline } from './context.js';
5
6
  const deadline = { ms: 30000, job: 'slashCommand' };
@@ -23,7 +24,7 @@ export function slashCommand(options) {
23
24
  if (e.command !== registered)
24
25
  return next(e);
25
26
  try {
26
- return answerOf(await withinDeadline(mod, deadline, `/${name}`, () => reply({ args: e.args, positionals: splitWords(e.args) }, mod)));
27
+ return answerOf(mod.name, await withinDeadline(mod, deadline, `/${name}`, () => reply({ args: e.args, positionals: splitWords(e.args) }, mod)));
27
28
  }
28
29
  catch (error) {
29
30
  return { text: `/${name} failed: ${oneLine(error)}` };
@@ -37,12 +38,12 @@ export function slashCommand(options) {
37
38
  };
38
39
  };
39
40
  }
40
- function answerOf(reply) {
41
+ function answerOf(name, reply) {
41
42
  if (reply === undefined)
42
43
  return {};
43
44
  if (typeof reply === 'string')
44
45
  return { text: reply };
45
- return { ...(reply.text === undefined ? {} : { text: reply.text }), ...(reply.note === undefined ? {} : { context: [reply.note] }) };
46
+ return { ...(reply.text === undefined ? {} : { text: reply.text }), ...(reply.note === undefined ? {} : { context: [modNote(name, reply.note)] }) };
46
47
  }
47
48
  function splitWords(args) {
48
49
  return parse(args, (key) => `$${key}`).flatMap((entry) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cmodjs/core",
3
- "version": "2.1.0",
3
+ "version": "3.0.0",
4
4
  "description": "Build Claude Code mods: hooks, panes, permissions, checks, and an install that just works.",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/records.d.ts CHANGED
@@ -74,7 +74,7 @@ export declare function marketplaceOf(root: string, name: string): string | unde
74
74
  export declare function updatesToTurnOn(marketplace: string | undefined, settings: unknown, known: unknown): string | undefined;
75
75
  export declare function consentPath(store: string): string;
76
76
  export declare function parseConsent(value: unknown, path: string): Record<string, string[]>;
77
- export declare const coreVersion = "2.1.0";
77
+ export declare const coreVersion = "3.0.0";
78
78
  export declare function oldestCmodFor(steps: Steps): string;
79
79
  export declare function keyWords(keys: Readonly<Record<string, string>> | undefined): string;
80
80
  export declare function scriptPaths(command: string): string[];
package/records.js CHANGED
@@ -227,7 +227,7 @@ function readKeys(keys) {
227
227
  }
228
228
  return { keys: commands };
229
229
  }
230
- export const coreVersion = '2.1.0';
230
+ export const coreVersion = '3.0.0';
231
231
  export function oldestCmodFor(steps) {
232
232
  if (steps.permissions?.some((item) => item.startsWith('config:')) === true)
233
233
  return '1.6.0';
package/register.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"register.d.ts","sourceRoot":"","sources":["src/register.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAoD,EAAE,EAAE,aAAa,EAAgB,MAAM,aAAa,CAAA;AACpH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,UAAU,CAAA;AAC7C,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAA;AAa3C,wBAAgB,aAAa,IAAI,aAAa,CAAC,MAAM,CAAC,GAAG,SAAS,CAEjE;AAoFD,wBAAgB,WAAW,CAAC,KAAK,SAAS,MAAM,EAAE,QAAQ,SAAS,OAAO,EAAE,OAAO,EAAE,EAAE,EAAE,UAAU,EAAE,aAAa,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,OAAO,EAAE,aAAa,GAAG,IAAI,CAqDzK;AAED,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,EAAE,GAAG,IAAI,CAIzD"}
1
+ {"version":3,"file":"register.d.ts","sourceRoot":"","sources":["src/register.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAA4D,EAAE,EAAE,aAAa,EAAgB,MAAM,aAAa,CAAA;AAC5H,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,UAAU,CAAA;AAC7C,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAA;AAa3C,wBAAgB,aAAa,IAAI,aAAa,CAAC,MAAM,CAAC,GAAG,SAAS,CAEjE;AA+FD,wBAAgB,WAAW,CAAC,KAAK,SAAS,MAAM,EAAE,QAAQ,SAAS,OAAO,EAAE,OAAO,EAAE,EAAE,EAAE,UAAU,EAAE,aAAa,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,OAAO,EAAE,aAAa,GAAG,IAAI,CAqDzK;AAED,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,EAAE,GAAG,IAAI,CAIzD"}
package/register.js CHANGED
@@ -82,67 +82,74 @@ async function startMod($, eventInput, passOn) {
82
82
  function routeToMod(_$, eventInput, passOn) {
83
83
  return lifecycle.route(passOn.event, eventInput, (passed) => passOn(passed));
84
84
  }
85
+ function routeFailure(_$, eventInput, passOn) {
86
+ return lifecycle.caught(passOn.event, eventInput, (passed) => passOn(passed), passOn.error);
87
+ }
88
+ const withCaller = (eventInput, passOn) => ({ ...eventInput, caller: passOn.origin.plugin });
89
+ const withoutCaller = (passOn) => (passed) => passOn(Object.fromEntries(Object.entries(passed).filter(([key]) => key !== 'caller')));
85
90
  function routeCallToMod(_$, eventInput, passOn) {
86
- const called = { ...eventInput, caller: passOn.origin.plugin };
87
- return lifecycle.route('cmod.call', called, (passed) => passOn(Object.fromEntries(Object.entries(passed).filter(([key]) => key !== 'caller'))));
91
+ return lifecycle.route('cmod.call', withCaller(eventInput, passOn), withoutCaller(passOn));
92
+ }
93
+ function routeCallFailure(_$, eventInput, passOn) {
94
+ return lifecycle.caught('cmod.call', withCaller(eventInput, passOn), withoutCaller(passOn), passOn.error);
88
95
  }
89
96
  export function registerMod(addHook, definition, options) {
90
97
  permissionCheck = undefined;
91
98
  registered = definition;
92
99
  lifecycle = createLifecycle(definition, () => permissionCheck, options);
93
100
  addHook('session.start', startMod);
94
- addHook('classic.Setup', routeToMod);
95
- addHook('classic.SessionStart', routeToMod);
96
- addHook('classic.SessionEnd', routeToMod);
97
- addHook('classic.UserPromptSubmit', routeToMod);
98
- addHook('classic.UserPromptExpansion', routeToMod);
99
- addHook('classic.InstructionsLoaded', routeToMod);
100
- addHook('classic.ConfigChange', routeToMod);
101
- addHook('classic.DirectoryAdded', routeToMod);
102
- addHook('classic.PreModelSwitch', routeToMod);
103
- addHook('classic.PostModelSwitch', routeToMod);
104
- addHook('classic.PermissionDenied', routeToMod);
105
- addHook('classic.PostToolUse', routeToMod);
106
- addHook('classic.PostToolUseFailure', routeToMod);
107
- addHook('classic.PostToolBatch', routeToMod);
108
- addHook('classic.MessageDisplay', routeToMod);
109
- addHook('classic.SubagentStart', routeToMod);
110
- addHook('classic.SubagentStop', routeToMod);
111
- addHook('classic.TaskCreated', routeToMod);
112
- addHook('classic.TaskCompleted', routeToMod);
113
- addHook('classic.TeammateIdle', routeToMod);
114
- addHook('classic.Elicitation', routeToMod);
115
- addHook('classic.ElicitationResult', routeToMod);
116
- addHook('classic.Notification', routeToMod);
117
- addHook('classic.PreCompact', routeToMod);
118
- addHook('classic.PostCompact', routeToMod);
119
- addHook('classic.Stop', routeToMod);
120
- addHook('classic.StopFailure', routeToMod);
121
- addHook('classic.FileChanged', routeToMod);
122
- addHook('classic.WorktreeCreate', routeToMod);
123
- addHook('classic.WorktreeRemove', routeToMod);
124
- addHook('tool.call', routeToMod);
125
- addHook('prompt.submit', routeToMod);
126
- addHook('prompt.context', routeToMod);
127
- addHook('command.run', routeToMod);
128
- addHook('session.measure', routeToMod);
129
- addHook('skill.prompt', routeToMod);
130
- addHook('ui.render', routeToMod);
131
- addHook('ui.press', routeToMod);
132
- addHook('ui.scroll', routeToMod);
133
- addHook('ui.close', routeToMod);
134
- addHook('cmod.call', routeCallToMod);
101
+ addHook('classic.Setup', routeToMod).catch(routeFailure);
102
+ addHook('classic.SessionStart', routeToMod).catch(routeFailure);
103
+ addHook('classic.SessionEnd', routeToMod).catch(routeFailure);
104
+ addHook('classic.UserPromptSubmit', routeToMod).catch(routeFailure);
105
+ addHook('classic.UserPromptExpansion', routeToMod).catch(routeFailure);
106
+ addHook('classic.InstructionsLoaded', routeToMod).catch(routeFailure);
107
+ addHook('classic.ConfigChange', routeToMod).catch(routeFailure);
108
+ addHook('classic.DirectoryAdded', routeToMod).catch(routeFailure);
109
+ addHook('classic.PreModelSwitch', routeToMod).catch(routeFailure);
110
+ addHook('classic.PostModelSwitch', routeToMod).catch(routeFailure);
111
+ addHook('classic.PermissionDenied', routeToMod).catch(routeFailure);
112
+ addHook('classic.PostToolUse', routeToMod).catch(routeFailure);
113
+ addHook('classic.PostToolUseFailure', routeToMod).catch(routeFailure);
114
+ addHook('classic.PostToolBatch', routeToMod).catch(routeFailure);
115
+ addHook('classic.MessageDisplay', routeToMod).catch(routeFailure);
116
+ addHook('classic.SubagentStart', routeToMod).catch(routeFailure);
117
+ addHook('classic.SubagentStop', routeToMod).catch(routeFailure);
118
+ addHook('classic.TaskCreated', routeToMod).catch(routeFailure);
119
+ addHook('classic.TaskCompleted', routeToMod).catch(routeFailure);
120
+ addHook('classic.TeammateIdle', routeToMod).catch(routeFailure);
121
+ addHook('classic.Elicitation', routeToMod).catch(routeFailure);
122
+ addHook('classic.ElicitationResult', routeToMod).catch(routeFailure);
123
+ addHook('classic.Notification', routeToMod).catch(routeFailure);
124
+ addHook('classic.PreCompact', routeToMod).catch(routeFailure);
125
+ addHook('classic.PostCompact', routeToMod).catch(routeFailure);
126
+ addHook('classic.Stop', routeToMod).catch(routeFailure);
127
+ addHook('classic.StopFailure', routeToMod).catch(routeFailure);
128
+ addHook('classic.FileChanged', routeToMod).catch(routeFailure);
129
+ addHook('classic.WorktreeCreate', routeToMod).catch(routeFailure);
130
+ addHook('classic.WorktreeRemove', routeToMod).catch(routeFailure);
131
+ addHook('tool.call', routeToMod).catch(routeFailure);
132
+ addHook('prompt.submit', routeToMod).catch(routeFailure);
133
+ addHook('prompt.context', routeToMod).catch(routeFailure);
134
+ addHook('command.run', routeToMod).catch(routeFailure);
135
+ addHook('session.measure', routeToMod).catch(routeFailure);
136
+ addHook('skill.prompt', routeToMod).catch(routeFailure);
137
+ addHook('ui.render', routeToMod).catch(routeFailure);
138
+ addHook('ui.press', routeToMod).catch(routeFailure);
139
+ addHook('ui.scroll', routeToMod).catch(routeFailure);
140
+ addHook('ui.close', routeToMod).catch(routeFailure);
141
+ addHook('cmod.call', routeCallToMod).catch(routeCallFailure);
142
+ addHook('session.compact', routeToMod).catch(routeFailure);
135
143
  if (definition.name !== cmodPluginName)
136
144
  return;
137
- addHook('session.append', routeToMod);
138
- addHook('session.compact', routeToMod);
139
- addHook('session.receive', routeToMod);
140
- addHook('agent.spawn', routeToMod);
141
- addHook('prompt.attachment', routeToMod);
142
- addHook('turn.start', routeToMod);
145
+ addHook('session.append', routeToMod).catch(routeFailure);
146
+ addHook('session.receive', routeToMod).catch(routeFailure);
147
+ addHook('agent.spawn', routeToMod).catch(routeFailure);
148
+ addHook('prompt.attachment', routeToMod).catch(routeFailure);
149
+ addHook('turn.start', routeToMod).catch(routeFailure);
143
150
  }
144
151
  export function registerPermissionCheck(addHook) {
145
152
  permissionCheck = permissionRoutes;
146
- addHook('tool.check', routeToMod);
147
- addHook('classic.PermissionRequest', routeToMod);
153
+ addHook('tool.check', routeToMod).catch(routeFailure);
154
+ addHook('classic.PermissionRequest', routeToMod).catch(routeFailure);
148
155
  }
@@ -22,7 +22,7 @@ export type Answers = {
22
22
  export type BeforeHooks = {
23
23
  readonly place: () => Promise<Place>;
24
24
  readonly wants: (event: BeforeEvent) => Promise<boolean>;
25
- readonly decide: Decide;
25
+ readonly fold: () => Decide;
26
26
  };
27
27
  export declare function relayOf(e: Frozen<Args<'session.receive'>>): string | undefined;
28
28
  export declare function installBefore({ claude, router, hooks }: {
@@ -1 +1 @@
1
- {"version":3,"file":"before.d.ts","sourceRoot":"","sources":["../src/runtime/before.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"before.d.ts","sourceRoot":"","sources":["../src/runtime/before.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AAC/C,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,WAAW,CAAA;AAa3C,OAAO,KAAK,EAAE,WAAW,EAAE,iBAAiB,EAAE,WAAW,EAAE,aAAa,EAAY,MAAM,4BAA4B,CAAA;AAEtH,OAAO,EAAiB,KAAK,WAAW,EAAE,KAAK,YAAY,EAAE,MAAM,uBAAuB,CAAA;AAC1F,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AAEzC,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AAIzC,MAAM,MAAM,KAAK,GAAG,IAAI,CAAC,iBAAiB,EAAE,SAAS,GAAG,SAAS,GAAG,iBAAiB,GAAG,MAAM,GAAG,OAAO,GAAG,WAAW,GAAG,WAAW,CAAC,CAAA;AAErI,MAAM,MAAM,MAAM,GAAG,CAAC,KAAK,EAAE,WAAW,EAAE,MAAM,EAAE,aAAa,KAAK,OAAO,CAAC,OAAO,CAAC,OAAO,CAAC,GAAG,SAAS,CAAC,CAAA;AAEzG,MAAM,MAAM,OAAO,GAAG;IACpB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAA;IACxB,QAAQ,CAAC,OAAO,EAAE,IAAI,CAAA;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,KAAK,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAA;IACjD,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,KAAK,EAAE;QAAE,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAA;CAC3C,CAAA;AAED,MAAM,MAAM,WAAW,GAAG;IAAE,QAAQ,CAAC,KAAK,EAAE,MAAM,OAAO,CAAC,KAAK,CAAC,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,CAAC,KAAK,EAAE,WAAW,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,MAAM,CAAA;CAAE,CAAA;AAazJ,wBAAgB,OAAO,CAAC,CAAC,EAAE,MAAM,CAAC,IAAI,CAAC,iBAAiB,CAAC,CAAC,GAAG,MAAM,GAAG,SAAS,CAG9E;AAED,wBAAgB,aAAa,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,EAAE;IAAE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,WAAW,CAAA;CAAE,GAAG,UAAU,CAiKtJ;AAED,wBAAgB,cAAc,CAAC,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,WAAW,GAAG,SAAS,EAAE,QAAQ,EAAE,MAAM,GAAG,WAAW,CAEzG;AAED,wBAAgB,UAAU,CAAC,MAAM,EAAE,aAAa,EAAE,MAAM,EAAE,OAAO,CAAC,OAAO,CAAC,GAAG,aAAa,CAMzF;AAED,eAAO,MAAM,QAAQ,WAAY,IAAI,GAAG,QAAQ,SAAS,YAAY,KAAG,MAAsG,CAAA;AAE9K,eAAO,MAAM,gBAAgB,EAAE,QAAQ,CAAC,MAAM,CAAC,WAAW,EAAE,SAAS,CAAC,MAAM,OAAO,CAAC,EAAE,CAAC,CAYtF,CAAA;AAED,wBAAgB,UAAU,CAAC,KAAK,EAAE,WAAW,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,EAAE,SAAS,CAAC,CAAC,MAAM,EAAE,aAAa,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC,EAAE,EAAE,MAAM,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,GAAG,OAAO,CAAC,OAAO,CAAC,OAAO,CAAC,GAAG,SAAS,CAAC,CAE/M;AAED,wBAAgB,WAAW,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,MAAM,EAAE,EAAE,MAAM,EAAE,OAAO,GAAG,OAAO,CAM9F;AAED,wBAAsB,WAAW,CAAC,MAAM,EAAE,MAAM,SAAS,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,MAAM,EAAE,EAAE,IAAI,EAAE,SAAS,MAAM,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,CAAC,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC,EAAE,EAAE,MAAM,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,EAAE,OAAO,GAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,KAAK,MAAuB,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAiBlV"}
package/runtime/before.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { ContextEnd } from '../session/context/context.js';
2
2
  import { InstructionFile, isRulePath } from '../session/context/turn/message/instruction-file.js';
3
3
  import { instructionPathOf, Message, messagesOf, noFiles, rowOfAppend, withTexts } from '../session/context/turn/message/message.js';
4
- import { Note } from '../session/context/turn/message/note.js';
4
+ import { Note, untagged } from '../session/context/turn/message/note.js';
5
5
  import { Notice } from '../session/context/turn/message/notice.js';
6
6
  import { Prompt } from '../session/context/turn/message/prompt.js';
7
7
  import { Reply } from '../session/context/turn/message/reply.js';
@@ -26,9 +26,9 @@ export function relayOf(e) {
26
26
  return isFromCmod ? relayTag.exec(e.text)?.[1] : undefined;
27
27
  }
28
28
  export function installBefore({ claude, router, hooks }) {
29
- const { place, wants, decide } = hooks;
29
+ const { place, wants, fold } = hooks;
30
30
  const now = () => new Date().toISOString();
31
- const asked = async (event, change) => (await decide(event, change)) ?? {};
31
+ const asked = async (event, change, decide = fold()) => (await decide(event, change)) ?? {};
32
32
  const message = (where, unplaced, task, isInTurn = true) => {
33
33
  const turn = isInTurn ? (task === undefined ? where.turn : where.taskTurns[task]) : undefined;
34
34
  const context = task === undefined ? where.context : (where.tasks[task]?.context ?? where.context);
@@ -50,7 +50,7 @@ export function installBefore({ claude, router, hooks }) {
50
50
  const answer = await asked(ToolResult, message(where, result, task));
51
51
  const below = await next(e);
52
52
  const rewritten = answer.output === undefined || isFailure ? {} : name.startsWith('mcp__') ? { updatedMCPToolOutput: answer.output } : { updatedToolOutput: answer.output };
53
- const failed = answer.isError === true && !isFailure ? { block: answer.note ?? `${name} failed.` } : {};
53
+ const failed = answer.isError === true && !isFailure ? { block: answer.note === undefined ? `${name} failed.` : untagged(answer.note) } : {};
54
54
  return { ...withNote(below, answer.isError === true ? undefined : answer.note), ...rewritten, ...failed };
55
55
  };
56
56
  const added = [
@@ -118,10 +118,11 @@ export function installBefore({ claude, router, hooks }) {
118
118
  return next(e);
119
119
  const where = await place();
120
120
  const texts = new Map();
121
+ const decide = fold();
121
122
  for (const [part, unplaced] of found.entries()) {
122
123
  if (!wanted.has(unplaced.type))
123
124
  continue;
124
- const answer = await asked(unplaced.type, message(where, unplaced, e.agentId));
125
+ const answer = await asked(unplaced.type, message(where, unplaced, e.agentId), decide);
125
126
  if (answer.text !== undefined)
126
127
  texts.set(part, answer.text);
127
128
  }
@@ -1 +1 @@
1
- {"version":3,"file":"conversations.d.ts","sourceRoot":"","sources":["../src/runtime/conversations.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,WAAW,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AAY5D,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AAGzC,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AA2BzC,MAAM,MAAM,aAAa,GAAG;IAC1B,MAAM,CAAC,CAAC,EAAE,MAAM,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC,GAAG;QAAE,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,WAAW,CAAC,WAAW,CAAC,GAAG,SAAS,CAAC,CAAA;IAClH,KAAK,IAAI,IAAI,CAAA;CACd,CAAA;AAED,wBAAgB,kBAAkB,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,EAAE;IAAE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,MAAM,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,KAAK,IAAI,CAAA;CAAE,GAAG,aAAa,CA6iBlR"}
1
+ {"version":3,"file":"conversations.d.ts","sourceRoot":"","sources":["../src/runtime/conversations.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,WAAW,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AAY5D,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AAGzC,OAAO,KAAK,EAAc,MAAM,EAAE,MAAM,aAAa,CAAA;AA8BrD,MAAM,MAAM,aAAa,GAAG;IAC1B,MAAM,CAAC,CAAC,EAAE,MAAM,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC,GAAG;QAAE,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,WAAW,CAAC,WAAW,CAAC,GAAG,SAAS,CAAC,CAAA;IAClH,KAAK,IAAI,IAAI,CAAA;CACd,CAAA;AAED,wBAAgB,kBAAkB,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,EAAE;IAAE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,MAAM,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,KAAK,IAAI,CAAA;CAAE,GAAG,aAAa,CAqkBlR"}
@@ -1,6 +1,6 @@
1
1
  import { consentPath, dataFolder, isObject, parseConsent } from '../records.js';
2
2
  import { Message, rowOfAppend } from '../session/context/turn/message/message.js';
3
- import { Note } from '../session/context/turn/message/note.js';
3
+ import { modNote, Note } from '../session/context/turn/message/note.js';
4
4
  import { Person } from '../session/context/turn/message/sender.js';
5
5
  import { createConversation, emptyConversation } from '../session/conversation.js';
6
6
  import { Session, UsageChange } from '../session/session.js';
@@ -8,12 +8,14 @@ import { GoalChange } from '../session/goal/goal.js';
8
8
  import { receivedMatcher } from '../utils/matcher.js';
9
9
  import { ifMissing } from '../utils/paths.js';
10
10
  import { formatExit, messageOf } from '../utils/text.js';
11
- import { foldBefore, installBefore, relayOf } from './before.js';
11
+ import { beforeAnswerKeys, foldBefore, installBefore, relayOf } from './before.js';
12
12
  import { cmodPluginName } from './dependencies.js';
13
13
  import { readLines, runCmod } from './programs.js';
14
- import { actPermission, conversationMethods, everySession, isAct, isHeardBy, resultOf, spawnTask, usageOf } from './sessions.js';
14
+ import { PassedTimeout } from './deadline.js';
15
+ import { actPermission, conversationMethods, everySession, isAct, isHeardBy, isInScope, resultOf, spawnTask, usageOf } from './sessions.js';
15
16
  const bufferedTaskRows = 200;
16
17
  const beforeDeadlineMs = 5000;
18
+ const awayFoldMs = 8000;
17
19
  const livenessMs = 5000;
18
20
  export function serveConversations({ claude, router, store, project, report }) {
19
21
  const folder = dataFolder(store, cmodPluginName);
@@ -161,7 +163,7 @@ export function serveConversations({ claude, router, store, project, report }) {
161
163
  reject(error);
162
164
  },
163
165
  };
164
- const timer = deadlineMs === undefined ? watchLife(settle, to) : claude.clock.after(deadlineMs, () => settle.reject(new Error(`session ${to} did not answer within ${deadlineMs / 1000} s`)));
166
+ const timer = deadlineMs === undefined ? watchLife(settle, to) : claude.clock.after(deadlineMs, () => settle.reject(new PassedTimeout(`session ${to} did not answer within ${deadlineMs / 1000} s`)));
165
167
  asked.set(id, settle);
166
168
  post(to, { ...relay, id, from: session }).catch((error) => settle.reject(error instanceof Error ? error : new Error(messageOf(error))));
167
169
  });
@@ -210,13 +212,26 @@ export function serveConversations({ claude, router, store, project, report }) {
210
212
  throw new Error(`${mod} needs the conversation permission to change what happens in another session. Turn it on in /mods ${mod}.`);
211
213
  return claude.cmod.call({ to: mod, method: conversationMethods.before, input: { session: from, event, change, isInProject } });
212
214
  };
213
- const askAway = async (watcher, event, change) => {
215
+ const isUnderHere = (task, ancestor) => {
216
+ for (let at = task, steps = 0; at !== undefined && steps < 64; at = conversation.state.tasks[at]?.parent, steps += 1)
217
+ if (at === ancestor)
218
+ return true;
219
+ return false;
220
+ };
221
+ const refuses = (watcher, event, change) => beforeAnswerKeys[event].includes('refuse') && (watcher.refusing ?? []).some(({ event: heard, matcher, scope }) => isInScope(scope ?? {}, session, change, isUnderHere) && isHeardBy(heard, matcher === undefined ? undefined : receivedMatcher(matcher), change));
222
+ const askAway = async (watcher, event, change, endsAt) => {
214
223
  try {
215
- return await ask(watcher.from, { kind: 'before', mod: watcher.mod, event, change, project: project() }, beforeDeadlineMs);
224
+ const leftMs = Math.min(beforeDeadlineMs, endsAt - (await claude.clock.now()));
225
+ if (leftMs <= 0)
226
+ throw new PassedTimeout(`the ${awayFoldMs / 1000} s cmod waits on other sessions ran out before session ${watcher.from} was asked`);
227
+ return await ask(watcher.from, { kind: 'before', mod: watcher.mod, event, change, project: project() }, leftMs);
216
228
  }
217
229
  catch (error) {
218
- if (await isRunning(watcher.from))
230
+ if (await isRunning(watcher.from)) {
231
+ if (error instanceof PassedTimeout && refuses(watcher, event, change))
232
+ return { refuse: `${watcher.mod}: ${error.message}` };
219
233
  throw error;
234
+ }
220
235
  watchers = watchers.filter((each) => each.from !== watcher.from);
221
236
  claude.ui.log(`cmod: session ${watcher.from} ended without clearing its before listeners, so they are cleared now`, { to: 'debug' });
222
237
  await claude.fs.write(`${watches}/${watcher.from}.json`, '[]');
@@ -235,7 +250,15 @@ export function serveConversations({ claude, router, store, project, report }) {
235
250
  await Promise.all([starting, watching]);
236
251
  return deciders(event).length > 0;
237
252
  },
238
- decide: (event, change) => foldBefore(event, change, deciders(event).map((watcher) => (latest) => (watcher.from === session ? askHere(watcher.mod, event, latest) : askAway(watcher, event, latest))), (error) => report(`before:${event}`, `Passed the ${event} on without a mod's answer`, error)),
253
+ fold: () => {
254
+ let endsAt;
255
+ return (event, change) => foldBefore(event, change, deciders(event).map((watcher) => async (latest) => {
256
+ if (watcher.from === session)
257
+ return askHere(watcher.mod, event, latest);
258
+ endsAt ??= claude.clock.now().then((now) => now + awayFoldMs);
259
+ return askAway(watcher, event, latest, await endsAt);
260
+ }), (error) => report(`before:${event}`, `Passed the ${event} on without a mod's answer`, error));
261
+ },
239
262
  },
240
263
  });
241
264
  const start = (id, source) => {
@@ -285,15 +308,16 @@ export function serveConversations({ claude, router, store, project, report }) {
285
308
  await publishWatches(true).catch((error) => report('before', 'Left its before listeners registered', error));
286
309
  return next(e);
287
310
  });
288
- router.add('session.append', async (e, next) => {
311
+ const recordAppend = async (e, next) => {
289
312
  const kept = await next(e);
290
313
  if (kept.deny !== undefined || session === '')
291
314
  return kept;
292
315
  const cwd = e.message.type === 'assistant' ? await claude.session.cwd() : '';
293
316
  void take(rowOfAppend({ uuid: kept.uuid, door: e.door, origin: e.origin, message: kept.message }, new Date().toISOString(), cwd), e.agentId).catch((error) => report('append', 'Missed a message', error));
294
317
  return kept;
295
- });
296
- router.add('session.compact', async (e, next) => {
318
+ };
319
+ router.add('session.append', recordAppend, (e, _error, next) => recordAppend(e, next));
320
+ const recordCompaction = async (e, next) => {
297
321
  const compacted = await next(e);
298
322
  if (e.trigger === 'precompute' || compacted.skip !== undefined || session === '')
299
323
  return compacted;
@@ -301,14 +325,15 @@ export function serveConversations({ claude, router, store, project, report }) {
301
325
  const boundary = { id: `compact:${e.agentId ?? session}:${at}`, at, type: 'system', name: 'compact_boundary', isMeta: false, isSummary: false, content: [] };
302
326
  void take(boundary, e.agentId).catch((error) => report('compact', 'Missed a compaction', error));
303
327
  return compacted;
304
- });
328
+ };
329
+ router.add('session.compact', recordCompaction, (e, _error, next) => recordCompaction(e, next));
305
330
  router.add('session.measure', async (e, next) => {
306
331
  const measured = await next(e);
307
332
  if (session !== '')
308
333
  dispatch(session, [{ type: UsageChange, usage: await usageOf(claude) }]);
309
334
  return measured;
310
335
  });
311
- router.add('agent.spawn', async (e, next) => {
336
+ const recordSpawn = async (e, next) => {
312
337
  const spawned = await next(e);
313
338
  if (spawned.deny !== undefined || spawned.agentId === undefined)
314
339
  return spawned;
@@ -321,7 +346,8 @@ export function serveConversations({ claude, router, store, project, report }) {
321
346
  for (const row of waiting)
322
347
  dispatch(session, conversation.add(row, id));
323
348
  return spawned;
324
- });
349
+ };
350
+ router.add('agent.spawn', recordSpawn, (e, _error, next) => recordSpawn(e, next));
325
351
  router.add('session.receive', async (e, next) => {
326
352
  const sent = relayOf(e);
327
353
  if (sent === undefined)
@@ -369,7 +395,7 @@ export function serveConversations({ claude, router, store, project, report }) {
369
395
  if (action === 'abort')
370
396
  return abortTurn(input, task);
371
397
  if (task !== undefined)
372
- return performInTask(action, input, task);
398
+ return performInTask(caller, action, input, task);
373
399
  if (action === 'usage')
374
400
  return usageOf(claude);
375
401
  if (action === 'surfaces')
@@ -378,7 +404,7 @@ export function serveConversations({ claude, router, store, project, report }) {
378
404
  const { type, text, as } = input;
379
405
  if (as === Session)
380
406
  throw new Error(`session ${session} cannot send as itself to itself.`);
381
- const delivered = type === Note ? await claude.session.append({ message: { type: 'user', content: [{ type: 'text', text }] } }) : await claude.prompt.submit({ text, ...(as === Person ? { asUser: true } : {}) });
407
+ const delivered = type === Note ? await claude.session.append({ message: { type: 'user', content: [{ type: 'text', text: modNote(caller, text) }] } }) : await claude.prompt.submit({ text, ...(as === Person ? { asUser: true } : {}) });
382
408
  if ('deny' in delivered && typeof delivered.deny === 'string')
383
409
  throw new Error(`Claude Code refused the note: ${delivered.deny}`);
384
410
  if ('drop' in delivered && typeof delivered.drop === 'string')
@@ -401,7 +427,7 @@ export function serveConversations({ claude, router, store, project, report }) {
401
427
  await claude.turn.abort({ turnId });
402
428
  return null;
403
429
  };
404
- const performInTask = async (action, input, task) => {
430
+ const performInTask = async (caller, action, input, task) => {
405
431
  const running = conversation.state.tasks[task];
406
432
  if (action === 'usage')
407
433
  return json(['history', 'usage', session, '--task', task]);
@@ -411,7 +437,8 @@ export function serveConversations({ claude, router, store, project, report }) {
411
437
  throw new Error(`cmod: ${action} runs only on a session's own main loop.`);
412
438
  if (running === undefined || running.closedAt !== undefined)
413
439
  throw ended(task);
414
- const sent = await claude.session.send({ to: { agentId: task }, text: input.text });
440
+ const { type, text } = input;
441
+ const sent = await claude.session.send({ to: { agentId: task }, text: type === Note ? modNote(caller, text) : text });
415
442
  if (sent.isDelivered !== true)
416
443
  throw ended(task);
417
444
  return null;
@@ -554,7 +581,7 @@ export function serveConversations({ claude, router, store, project, report }) {
554
581
  throw new Error('cmod hears no caller for this before listener.');
555
582
  const target = String(request['session']);
556
583
  const list = Array.isArray(request['subscriptions']) ? request['subscriptions'] : [];
557
- ownWatches.set(`${caller}\n${target}`, { mod: caller, target, project: project(), events: [...new Set(list.map(({ event }) => event))] });
584
+ ownWatches.set(`${caller}\n${target}`, { mod: caller, target, project: project(), events: [...new Set(list.map(({ event }) => event))], refusing: list.filter((each) => each.onFailure === 'refuse') });
558
585
  await publishWatches();
559
586
  return null;
560
587
  },