@cmodjs/core 2.0.0 → 2.0.1

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 (42) hide show
  1. package/docs/commands.md +3 -3
  2. package/docs/dependencies.md +1 -1
  3. package/package.json +1 -1
  4. package/records.d.ts +1 -1
  5. package/records.js +1 -1
  6. package/register.d.ts.map +1 -1
  7. package/register.js +5 -4
  8. package/runtime/before.d.ts +6 -3
  9. package/runtime/before.d.ts.map +1 -1
  10. package/runtime/before.js +59 -37
  11. package/runtime/conversations.d.ts.map +1 -1
  12. package/runtime/conversations.js +38 -13
  13. package/runtime/hooks.d.ts +9 -22
  14. package/runtime/hooks.d.ts.map +1 -1
  15. package/runtime/hooks.js +4 -58
  16. package/runtime/lifecycle.d.ts +2 -1
  17. package/runtime/lifecycle.d.ts.map +1 -1
  18. package/runtime/lifecycle.js +34 -17
  19. package/runtime/permission-check.d.ts +15 -0
  20. package/runtime/permission-check.d.ts.map +1 -0
  21. package/runtime/permission-check.js +37 -0
  22. package/runtime/sessions.d.ts +12 -8
  23. package/runtime/sessions.d.ts.map +1 -1
  24. package/runtime/sessions.js +41 -80
  25. package/runtime/state.d.ts.map +1 -1
  26. package/runtime/state.js +14 -11
  27. package/session/context/turn/message/message.d.ts +1 -0
  28. package/session/context/turn/message/message.d.ts.map +1 -1
  29. package/session/context/turn/message/message.js +5 -2
  30. package/src/records.ts +1 -1
  31. package/src/register.ts +5 -4
  32. package/src/runtime/before.ts +60 -39
  33. package/src/runtime/conversations.ts +35 -13
  34. package/src/runtime/hooks.ts +13 -84
  35. package/src/runtime/lifecycle.ts +49 -31
  36. package/src/runtime/permission-check.ts +47 -0
  37. package/src/runtime/sessions.ts +51 -84
  38. package/src/runtime/state.ts +15 -8
  39. package/src/session/context/turn/message/message.ts +6 -2
  40. package/src/testing.ts +4 -3
  41. package/testing.d.ts.map +1 -1
  42. package/testing.js +4 -3
package/docs/commands.md CHANGED
@@ -98,7 +98,7 @@ Releases the mod at `path` (default: the current folder) at the version in its `
98
98
  4. Commits the release as the `release` branch, and builds the release archive from that commit.
99
99
  5. Builds the program `cli/` declares, and writes `SHA256SUMS` for every file of the release.
100
100
  6. Writes `.claude-plugin/marketplace.json`, listing the archive and the cmod plugin. The cmod entry points at the `release` branch of `heyJordanParker/cmod`, so the mod's `"cmod"` dependency resolves in this marketplace and follows each cmod release with no new publish of the mod.
101
- 7. Commits that file, tags `v<version>`, pushes the tag and the `release` branch, creates the GitHub release with every file, then pushes the branch that holds `marketplace.json`. So Claude Code never updates anyone to a release whose files are not up yet. Its notes are the version's section of the committed `CHANGELOG.md`, above GitHub's compare link ([mod.md](mod.md#release-notes)). `--dry-run` prints them.
101
+ 7. Commits that file, tags `v<version>`, tags the release commit `<name>--v<version>`, which Claude Code installs a version range from when a marketplace lists the mod from its git repository, pushes both tags and the `release` branch, creates the GitHub release with every file, then pushes the branch that holds `marketplace.json`. So Claude Code never updates anyone to a release whose files are not up yet. Its notes are the version's section of the committed `CHANGELOG.md`, above GitHub's compare link ([mod.md](mod.md#release-notes)). `--dry-run` prints them.
102
102
  8. Prints the link to paste as the Repository when you submit the mod at [claude.ai/directory/manage](https://claude.ai/directory/manage), such as `https://github.com/owner/greeter/tree/release`. The portal reads the branch from the link, so the directory follows `release`.
103
103
 
104
104
  The release keeps `package.json`, so Claude Code still installs the mod's packages for its install and uninstall steps. It keeps `bun.lock` too, unless a `"files"` list leaves it out. `main` keeps the source only.
@@ -114,7 +114,7 @@ It needs, and refuses to start without:
114
114
  - a `package.json` with no dependency on a `file:` or `link:` path, which no user has
115
115
  - a `"files"` list, when `package.json` has one, whose every path matches a committed file
116
116
  - a `"program"` in the `cmod` key of `package.json` that names the program `cli/` declares, when `cli/` declares one
117
- - for a real publish, a working tree with no uncommitted changes, a `v<version>` tag that does not exist yet, a `git push` to `origin` that succeeds, and the `gh` command, which creates the GitHub release
117
+ - for a real publish, a working tree with no uncommitted changes, no `v<version>` or `<name>--v<version>` tag yet, a `git push` to `origin` that succeeds, and the `gh` command, which creates the GitHub release
118
118
 
119
119
  `--dry-run` builds everything into a temporary folder, and pushes nothing. It writes `marketplace.json` there too, so the working tree stays as it was. It allows uncommitted changes and an existing tag, and builds from the last commit. It prints the folder that holds the release, so you can read what the directory will read.
120
120
 
@@ -259,7 +259,7 @@ Changes Claude Code settings for a set-up mod, as its `settings.user.update`, `s
259
259
  cmod rules [--project <folder>] [--json]
260
260
  ```
261
261
 
262
- Links the `rules/` folder of each mod that is on and has the `prompt` permission into Claude Code's rules folder, and removes the link of each that is off, lacks the permission, or is removed ([mod.md](mod.md#rules)). A mod the person installed links at `<config>/rules/<mod>`, and one turned on only for `--project`, the current folder when it is left out, links at `<project>/.claude/rules/<mod>`. It prints the rules it linked. With `--json` it prints `{ linked, placed, removed }`, where `placed` and `removed` name the mods whose link this run made or took away. `cmod install`, `update`, `remove`, `enable`, `disable`, `permission`, `setup`, `teardown`, `link`, and `unlink` run it after they succeed, and the cmod plugin runs it at each session's start and each prompt.
262
+ Links the `rules/` folder of each mod that is on and has the `prompt` permission into Claude Code's rules folder, and removes the link of each that is off, lacks the permission, or is removed ([mod.md](mod.md#rules)). A mod the person installed links at `<config>/rules/<mod>`, and one turned on only for `--project`, the current folder when it is left out, links at `<project>/.claude/rules/<mod>`. It prints the rules it linked. With `--json` it prints `{ linked, placed, removed, watched }`, where `placed` and `removed` name the mods whose link this run made or took away, and `watched` lists the files and folders whose change can change the next run. `cmod install`, `update`, `remove`, `enable`, `disable`, `permission`, `setup`, `teardown`, `link`, and `unlink` run it after they succeed. The cmod plugin runs it when a session starts and before a prompt, each time only when a `watched` path changed since its last run.
263
263
 
264
264
  ### cmod option
265
265
 
@@ -58,7 +58,7 @@ A provider that changes or removes a method breaks every mod that calls it, and
58
58
  "dependencies": [{ "name": "cmod", "version": "^1.0.0" }, { "name": "tracer", "version": "^1.0" }]
59
59
  ```
60
60
 
61
- Claude Code checks the range against the `version` in the provider's `plugin.json` when it loads your mod ([Claude Code's plugin dependencies](https://code.claude.com/docs/en/plugins/dependencies)). cmod is a provider too: a mod built on `@cmodjs/core` 1.0 calls methods only the 1.0 cmod plugin has, so `cmod new` writes the `^1.0.0` range.
61
+ Claude Code checks the range against the provider's `version` when it loads your mod. Where a marketplace lists the provider from its git repository, as every mod's marketplace lists cmod, Claude Code also installs it at the highest `<name>--v<version>` tag inside the range, which `cmod publish` pushes for every release ([Claude Code's plugin dependencies](https://code.claude.com/docs/en/plugins/dependencies)). cmod is a provider too: a mod built on `@cmodjs/core` 1.0 calls methods only the 1.0 cmod plugin has, so `cmod new` writes the `^1.0.0` range.
62
62
 
63
63
  Claude Code then lays tracer's types file beside the mod's own types, so the call is typed:
64
64
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cmodjs/core",
3
- "version": "2.0.0",
3
+ "version": "2.0.1",
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.0.0";
77
+ export declare const coreVersion = "2.0.1";
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.0.0';
230
+ export const coreVersion = '2.0.1';
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;AAY3C,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,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"}
package/register.js CHANGED
@@ -1,7 +1,8 @@
1
1
  import { cmodPluginName } from './runtime/dependencies.js';
2
2
  import { createLifecycle, readPlugin } from './runtime/lifecycle.js';
3
+ import { permissionRoutes } from './runtime/permission-check.js';
3
4
  let lifecycle;
4
- let checksPermissions = false;
5
+ let permissionCheck;
5
6
  let registered;
6
7
  export function registeredMod() {
7
8
  return registered;
@@ -86,9 +87,9 @@ function routeCallToMod(_$, eventInput, passOn) {
86
87
  return lifecycle.route('cmod.call', called, (passed) => passOn(Object.fromEntries(Object.entries(passed).filter(([key]) => key !== 'caller'))));
87
88
  }
88
89
  export function registerMod(addHook, definition, options) {
89
- checksPermissions = false;
90
+ permissionCheck = undefined;
90
91
  registered = definition;
91
- lifecycle = createLifecycle(definition, () => checksPermissions, options);
92
+ lifecycle = createLifecycle(definition, () => permissionCheck, options);
92
93
  addHook('session.start', startMod);
93
94
  addHook('classic.Setup', routeToMod);
94
95
  addHook('classic.SessionStart', routeToMod);
@@ -141,7 +142,7 @@ export function registerMod(addHook, definition, options) {
141
142
  addHook('turn.start', routeToMod);
142
143
  }
143
144
  export function registerPermissionCheck(addHook) {
144
- checksPermissions = true;
145
+ permissionCheck = permissionRoutes;
145
146
  addHook('tool.check', routeToMod);
146
147
  addHook('classic.PermissionRequest', routeToMod);
147
148
  }
@@ -1,7 +1,7 @@
1
1
  import type { Args, Frozen } from 'claude-code';
2
2
  import type { Disposable } from '../mod.js';
3
3
  import type { ConversationState, SessionChange } from '../session/conversation.js';
4
- import type { BeforeEvent } from '../session/session.js';
4
+ import { type BeforeEvent, type SessionEvent } from '../session/session.js';
5
5
  import type { Claude } from './claude.js';
6
6
  import type { Router } from './router.js';
7
7
  export type Place = Pick<ConversationState, 'session' | 'context' | 'contextOpenedAt' | 'turn' | 'tasks' | 'taskTurns' | 'openCalls'>;
@@ -31,6 +31,9 @@ export declare function installBefore({ claude, router, hooks }: {
31
31
  readonly hooks: BeforeHooks;
32
32
  }): Disposable;
33
33
  export declare function withAnswer(change: SessionChange, answer: Partial<Answers>): SessionChange;
34
- export declare function foldAnswers(event: BeforeEvent, change: SessionChange, askers: readonly ((change: SessionChange) => Promise<unknown>)[], report: (error: unknown) => void): Promise<Partial<Answers> | undefined>;
35
- export declare function answerOf(event: BeforeEvent, answer: unknown): Partial<Answers> | undefined;
34
+ export declare const methodOf: (prefix: 'on' | 'before', event: SessionEvent) => string;
35
+ export declare const beforeAnswerKeys: Readonly<Record<BeforeEvent, readonly (keyof Answers)[]>>;
36
+ export declare function foldBefore(event: BeforeEvent, change: SessionChange, askers: readonly ((change: SessionChange) => Promise<unknown>)[], report: (error: unknown) => void): Promise<Partial<Answers> | undefined>;
37
+ export declare function takenAnswer(method: string, takes: readonly string[], answer: unknown): unknown;
38
+ export declare function foldAnswers<Change, Answer extends object>(method: string, takes: readonly string[], ends: readonly string[], change: Change, askers: readonly ((change: Change) => Promise<unknown>)[], report: (error: unknown) => void, applied?: (change: Change, answer: Answer) => Change): Promise<Answer | undefined>;
36
39
  //# sourceMappingURL=before.d.ts.map
@@ -1 +1 @@
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,iBAAiB,EAAE,aAAa,EAAY,MAAM,4BAA4B,CAAA;AAE5F,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAA;AACxD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AAEzC,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AAGzC,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,MAAM,EAAE,MAAM,CAAA;CAAE,CAAA;AAarJ,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,CAqJtJ;AAED,wBAAgB,UAAU,CAAC,MAAM,EAAE,aAAa,EAAE,MAAM,EAAE,OAAO,CAAC,OAAO,CAAC,GAAG,aAAa,CAMzF;AAED,wBAAsB,WAAW,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,CAgBtN;AAED,wBAAgB,QAAQ,CAAC,KAAK,EAAE,WAAW,EAAE,MAAM,EAAE,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,GAAG,SAAS,CAmB1F"}
1
+ {"version":3,"file":"before.d.ts","sourceRoot":"","sources":["../src/runtime/before.ts"],"names":[],"mappings":"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"}
package/runtime/before.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { ContextEnd } from '../session/context/context.js';
2
2
  import { InstructionFile, isRulePath } from '../session/context/turn/message/instruction-file.js';
3
- import { Message, messagesOf, noFiles, rowOfAppend, withTexts } from '../session/context/turn/message/message.js';
3
+ import { instructionPathOf, Message, messagesOf, noFiles, rowOfAppend, withTexts } from '../session/context/turn/message/message.js';
4
4
  import { Note } 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';
@@ -11,8 +11,10 @@ import { ToolCall } from '../session/context/turn/message/tool-call.js';
11
11
  import { ToolResult } from '../session/context/turn/message/tool-result.js';
12
12
  import { TurnEnd } from '../session/context/turn/turn.js';
13
13
  import { Task, TaskStart } from '../session/goal/task.js';
14
+ import { sessionEvents } from '../session/session.js';
14
15
  import { cmodPluginName } from './dependencies.js';
15
16
  import { toolInputOf } from './tool-calls.js';
17
+ import { resolve } from '../vendor.js';
16
18
  const appended = [Note, Reply, Notice];
17
19
  const withNote = (below, note) => {
18
20
  const fields = (below ?? {});
@@ -35,6 +37,7 @@ export function installBefore({ claude, router, hooks }) {
35
37
  };
36
38
  const titles = new Map();
37
39
  const spawned = new Map();
40
+ const agentFolders = new Map();
38
41
  const toolResultHook = (isFailure) => async (e, next) => {
39
42
  if (!(await wants(ToolResult)))
40
43
  return next(e);
@@ -99,7 +102,7 @@ export function installBefore({ claude, router, hooks }) {
99
102
  if (!(await wants(InstructionFile)) || e.type !== 'nested_memory')
100
103
  return next(e);
101
104
  const where = await place();
102
- const path = /^Contents of (.+?) \(/.exec(e.text)?.[1] ?? '';
105
+ const path = instructionPathOf(e.text);
103
106
  const answer = await asked(InstructionFile, message(where, { id: '', at: now(), type: InstructionFile, path, text: e.text, isRule: isRulePath(path) }, e.agentId));
104
107
  if (answer.refuse !== undefined)
105
108
  return { text: null };
@@ -129,7 +132,8 @@ export function installBefore({ claude, router, hooks }) {
129
132
  return next(e);
130
133
  const where = await place();
131
134
  const input = toolInputOf(e);
132
- const answer = await asked(ToolCall, message(where, { id: e.tool_use_id, at: now(), type: ToolCall, callId: e.tool_use_id, name: e.tool, input, cwd: await claude.session.cwd(), files: noFiles }, e.agentId));
135
+ const cwd = (e.agentId === undefined ? undefined : agentFolders.get(e.agentId)) ?? (await claude.session.cwd());
136
+ const answer = await asked(ToolCall, message(where, { id: e.tool_use_id, at: now(), type: ToolCall, callId: e.tool_use_id, name: e.tool, input, cwd, files: noFiles }, e.agentId));
133
137
  if (answer.refuse !== undefined)
134
138
  return { deny: answer.refuse };
135
139
  if (answer.result !== undefined)
@@ -141,8 +145,15 @@ export function installBefore({ claude, router, hooks }) {
141
145
  }),
142
146
  router.add('agent.spawn', async (e, next) => {
143
147
  spawned.set(e.tool_use_id, e.prompt);
148
+ const start = async (args) => {
149
+ const started = await next(args);
150
+ const folder = args.isTeammate === true ? undefined : (args.cwd ?? (args.parentAgentId === undefined ? undefined : agentFolders.get(args.parentAgentId)));
151
+ if (started.agentId !== undefined && folder !== undefined)
152
+ agentFolders.set(started.agentId, resolve(await claude.session.cwd(), folder));
153
+ return started;
154
+ };
144
155
  if (!(await wants(TaskStart)))
145
- return next(e);
156
+ return start(e);
146
157
  const where = await place();
147
158
  const task = { id: '', title: e.description, agentType: e.subagentType, model: e.model ?? e.parentModel, openedBy: e.tool_use_id, openedAt: now(), ...(e.parentAgentId === undefined ? {} : { parent: e.parentAgentId }) };
148
159
  const answer = await asked(TaskStart, { type: TaskStart, task: { ...task, context: where.context } });
@@ -150,7 +161,7 @@ export function installBefore({ claude, router, hooks }) {
150
161
  spawned.delete(e.tool_use_id);
151
162
  return { deny: answer.refuse };
152
163
  }
153
- return next(answer.owner === undefined ? e : { ...e, model: answer.owner.model });
164
+ return start(answer.owner === undefined ? e : { ...e, model: answer.owner.model });
154
165
  }),
155
166
  router.add('classic.SubagentStart', async (e, next) => {
156
167
  const where = await place();
@@ -168,14 +179,19 @@ export function installBefore({ claude, router, hooks }) {
168
179
  return withNote(await next(e), answer.note);
169
180
  }),
170
181
  router.add('classic.SubagentStop', async (e, next) => {
182
+ const stopped = (answer) => {
183
+ if (answer.block === undefined)
184
+ agentFolders.delete(e.agent_id);
185
+ return answer;
186
+ };
171
187
  if (!(await wants(TurnEnd)))
172
- return next(e);
188
+ return stopped(await next(e));
173
189
  const turn = (await place()).taskTurns[e.agent_id];
174
190
  if (turn === undefined)
175
- return next(e);
191
+ return stopped(await next(e));
176
192
  const answer = await asked(TurnEnd, { type: TurnEnd, turn: { ...turn, closedAt: now() } });
177
193
  const below = await next(e);
178
- return answer.refuse === undefined ? below : { ...below, block: answer.refuse };
194
+ return stopped(answer.refuse === undefined ? below : { ...below, block: answer.refuse });
179
195
  }),
180
196
  router.add('classic.Stop', async (e, next) => {
181
197
  if (!(await wants(TurnEnd)))
@@ -212,19 +228,47 @@ export function withAnswer(change, answer) {
212
228
  return { ...change, task: { ...change.task, model: answer.owner.model } };
213
229
  return change;
214
230
  }
215
- export async function foldAnswers(event, change, askers, report) {
231
+ export const methodOf = (prefix, event) => `${prefix}${Object.entries(sessionEvents).find(([, each]) => each === event)?.[0] ?? event}`;
232
+ export const beforeAnswerKeys = {
233
+ prompt: ['text', 'note', 'title', 'refuse'],
234
+ note: ['text'],
235
+ skill: ['text'],
236
+ instructionFile: ['text', 'refuse'],
237
+ reply: ['text'],
238
+ toolCall: ['input', 'result', 'refuse'],
239
+ toolResult: ['output', 'isError', 'note'],
240
+ notice: ['text'],
241
+ taskStart: ['owner', 'refuse'],
242
+ turnEnd: ['refuse'],
243
+ contextEnd: ['refuse'],
244
+ };
245
+ export function foldBefore(event, change, askers, report) {
246
+ return foldAnswers(methodOf('before', event), beforeAnswerKeys[event], ['refuse', 'result'], change, askers, report, withAnswer);
247
+ }
248
+ export function takenAnswer(method, takes, answer) {
249
+ if (answer === undefined || answer === null || answer === false)
250
+ return undefined;
251
+ if (typeof answer !== 'object' || Array.isArray(answer))
252
+ throw new Error(`it answered ${JSON.stringify(answer)}, but ${method} takes an object or nothing: ${takes.map((key) => `{ ${key} }`).join(', ') || 'no answer'}.`);
253
+ const unread = Object.keys(answer).filter((key) => !takes.includes(key));
254
+ if (unread.length > 0)
255
+ throw new Error(`it answered ${unread.join(' and ')}, which ${method} does not take. It takes ${takes.map((key) => `{ ${key} }`).join(', ') || 'no answer'}.`);
256
+ return answer;
257
+ }
258
+ export async function foldAnswers(method, takes, ends, change, askers, report, applied = (same) => same) {
216
259
  let latest = change;
217
260
  let result;
218
261
  for (const ask of askers) {
219
262
  try {
220
- const answer = answerOf(event, await ask(latest));
221
- if (answer === undefined)
263
+ const answer = takenAnswer(method, takes, await ask(latest));
264
+ if (typeof answer !== 'object' || answer === null)
222
265
  continue;
223
- const notes = [result?.note, answer.note].filter((note) => note !== undefined);
224
- result = { ...result, ...answer, ...(notes.length === 0 ? {} : { note: notes.join('\n\n') }) };
225
- if (answer.refuse !== undefined || answer.result !== undefined)
266
+ const given = answer;
267
+ const notes = [result?.['note'], given['note']].filter((note) => typeof note === 'string');
268
+ result = { ...result, ...given, ...(notes.length === 0 ? {} : { note: notes.join('\n\n') }) };
269
+ if (ends.some((key) => given[key] !== undefined))
226
270
  return result;
227
- latest = withAnswer(latest, answer);
271
+ latest = applied(latest, answer);
228
272
  }
229
273
  catch (error) {
230
274
  report(error);
@@ -232,25 +276,3 @@ export async function foldAnswers(event, change, askers, report) {
232
276
  }
233
277
  return result;
234
278
  }
235
- export function answerOf(event, answer) {
236
- if (typeof answer !== 'object' || answer === null)
237
- return undefined;
238
- const given = answer;
239
- const allowed = {
240
- prompt: ['text', 'note', 'title', 'refuse'],
241
- note: ['text'],
242
- skill: ['text'],
243
- instructionFile: ['text', 'refuse'],
244
- reply: ['text'],
245
- toolCall: ['input', 'result', 'refuse'],
246
- toolResult: ['output', 'isError', 'note'],
247
- notice: ['text'],
248
- taskStart: ['owner', 'refuse'],
249
- turnEnd: ['refuse'],
250
- contextEnd: ['refuse'],
251
- };
252
- const unread = Object.keys(given).filter((key) => !allowed[event].includes(key));
253
- if (unread.length > 0)
254
- throw new Error(`a before(${event}) listener answered ${unread.join(' and ')}, which ${event} does not take. It takes ${allowed[event].map((key) => `{ ${key} }`).join(', ')}.`);
255
- return given;
256
- }
@@ -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,CAuhBlR"}
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"}
@@ -8,7 +8,7 @@ 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 { foldAnswers, installBefore, relayOf } from './before.js';
11
+ import { foldBefore, installBefore, relayOf } from './before.js';
12
12
  import { cmodPluginName } from './dependencies.js';
13
13
  import { readLines, runCmod } from './programs.js';
14
14
  import { actPermission, conversationMethods, everySession, isAct, isHeardBy, resultOf, spawnTask, usageOf } from './sessions.js';
@@ -50,7 +50,7 @@ export function serveConversations({ claude, router, store, project, report }) {
50
50
  router.add('classic.Stop', async (e, next) => endTurn({ reason: 'answer', ...(e.last_assistant_message === undefined ? {} : { answer: e.last_assistant_message }) })(await next(e)));
51
51
  router.add('classic.StopFailure', async (e, next) => endTurn({ reason: 'error', failure: { type: String(e.error), text: e.error_details ?? e.last_assistant_message ?? String(e.error) } })(await next(e)));
52
52
  router.add('classic.SubagentStop', async (e, next) => endTurn({ reason: 'answer', ...(e.last_assistant_message === undefined ? {} : { answer: e.last_assistant_message }) }, e.agent_id)(await next(e)));
53
- const json = async (args) => JSON.parse(await runCmod(claude, [...args, '--json', '--project', project()], session === '' ? await claude.session.id() : session));
53
+ const json = async (args) => JSON.parse(await runCmod(claude, [...args, '--json', '--project', project()], await claude.session.id()));
54
54
  const isGranted = async (caller, item) => {
55
55
  if (caller === cmodPluginName)
56
56
  return true;
@@ -116,16 +116,39 @@ export function serveConversations({ claude, router, store, project, report }) {
116
116
  if (sent.isDelivered !== true)
117
117
  throw ended(to);
118
118
  };
119
+ const waitingOn = new Map();
120
+ let liveness;
121
+ let isChecking = false;
122
+ const checkLiveness = async () => {
123
+ if (isChecking)
124
+ return;
125
+ isChecking = true;
126
+ try {
127
+ const waiting = [...waitingOn];
128
+ const running = new Set((await json(['history', 'sessions', '--running', '--all'])).map((each) => each.id));
129
+ for (const [settle, to] of waiting)
130
+ if (!running.has(to))
131
+ settle.reject(ended(to));
132
+ }
133
+ finally {
134
+ isChecking = false;
135
+ }
136
+ };
137
+ const watchLife = (settle, to) => {
138
+ waitingOn.set(settle, to);
139
+ liveness ??= claude.clock.every(livenessMs, () => void checkLiveness().catch((error) => report('liveness', 'Could not check which sessions still run', error)));
140
+ return {
141
+ cancel() {
142
+ waitingOn.delete(settle);
143
+ if (waitingOn.size > 0)
144
+ return;
145
+ liveness?.cancel();
146
+ liveness = undefined;
147
+ },
148
+ };
149
+ };
119
150
  const ask = (to, relay, deadlineMs) => new Promise((resolve, reject) => {
120
151
  const id = `${session}:${(asks += 1)}`;
121
- const timer = deadlineMs === undefined
122
- ? claude.clock.every(livenessMs, () => {
123
- void isRunning(to).then((isLive) => {
124
- if (!isLive)
125
- settle.reject(ended(to));
126
- });
127
- })
128
- : claude.clock.after(deadlineMs, () => settle.reject(new Error(`session ${to} did not answer within ${deadlineMs / 1000} s`)));
129
152
  const settle = {
130
153
  resolve(value) {
131
154
  if (asked.delete(id))
@@ -138,6 +161,7 @@ export function serveConversations({ claude, router, store, project, report }) {
138
161
  reject(error);
139
162
  },
140
163
  };
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`)));
141
165
  asked.set(id, settle);
142
166
  post(to, { ...relay, id, from: session }).catch((error) => settle.reject(error instanceof Error ? error : new Error(messageOf(error))));
143
167
  });
@@ -211,7 +235,7 @@ export function serveConversations({ claude, router, store, project, report }) {
211
235
  await Promise.all([starting, watching]);
212
236
  return deciders(event).length > 0;
213
237
  },
214
- decide: (event, change) => foldAnswers(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)),
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)),
215
239
  },
216
240
  });
217
241
  const start = (id, source) => {
@@ -542,7 +566,7 @@ export function serveConversations({ claude, router, store, project, report }) {
542
566
  const task = typeof request['task'] === 'string' ? request['task'] : undefined;
543
567
  if (!isAct(action))
544
568
  throw new Error(`cmod takes no action named ${action}.`);
545
- if (target === session)
569
+ if (target === (await claude.session.id()))
546
570
  return perform(caller, action, request['input'], task);
547
571
  return actElsewhere(caller, target, action, request['input'], task);
548
572
  },
@@ -560,7 +584,8 @@ export function serveConversations({ claude, router, store, project, report }) {
560
584
  if (method === undefined)
561
585
  return undefined;
562
586
  const input = isObject(e.input) ? e.input : {};
563
- if (e.method !== conversationMethods.act && input['session'] !== session && !(await isGranted(e.caller, 'conversation')))
587
+ const isDropping = (e.method === conversationMethods.subscribe || e.method === conversationMethods.watchBefore) && Array.isArray(input['subscriptions']) && input['subscriptions'].length === 0;
588
+ if (e.method !== conversationMethods.act && !isDropping && input['session'] !== (await claude.session.id()) && !(await isGranted(e.caller, 'conversation')))
564
589
  return { deny: `${e.caller ?? 'A mod'} reads your conversations without your grant. Turn it on in /mods ${e.caller ?? ''}.`.trim() };
565
590
  try {
566
591
  return { value: await method(input, e.caller) };
@@ -1,36 +1,23 @@
1
- import type { ClassicHookEvent, ClassicHookInputs, PermissionRequestDecision } from 'claude-code';
1
+ import type { ClassicHookEvent, ClassicHookInputs } 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
5
  import { type CallBase } from '../utils/call-effects.js';
6
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>;
7
+ export type HookFields = Readonly<Record<string, unknown>>;
8
+ export type HookListener<H extends Hook> = (input: ClassicHookInputs[H]) => HookFields | undefined | Promise<HookFields | undefined>;
26
9
  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';
27
10
  export declare const permissionEvents: readonly RoutedEvent[];
28
11
  type CallFiles = {
29
12
  readonly read: string[];
30
13
  readonly changed: string[];
31
14
  };
32
- export declare function classicHook<H extends Hook>(name: string, event: H, hook: HookListener<H>, report: (failure: Failure) => void): RoutedHook<RoutedEvent>;
15
+ export declare function classicHook<H extends Hook>(event: H, hook: HookListener<H>, report: (failure: Failure) => void): RoutedHook<RoutedEvent>;
33
16
  export declare function userSkillHook(claude: Claude): RoutedHook<'skill.prompt'>;
34
- export declare function callFiles(name: string, claude: Claude, use: CallBase, cwd: string): Promise<CallFiles>;
17
+ export type HomeFolders = {
18
+ readonly home: string | undefined;
19
+ readonly configRoot: string | undefined;
20
+ };
21
+ export declare function callFiles(name: string, claude: Claude, folders: Promise<HomeFolders>, use: CallBase, cwd: string): Promise<CallFiles>;
35
22
  export {};
36
23
  //# sourceMappingURL=hooks.d.ts.map
@@ -1 +1 @@
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"}
1
+ {"version":3,"file":"hooks.d.ts","sourceRoot":"","sources":["../src/runtime/hooks.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAA;AAEtE,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,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAA;AAE1D,MAAM,MAAM,YAAY,CAAC,CAAC,SAAS,IAAI,IAAI,CAAC,KAAK,EAAE,iBAAiB,CAAC,CAAC,CAAC,KAAK,UAAU,GAAG,SAAS,GAAG,OAAO,CAAC,UAAU,GAAG,SAAS,CAAC,CAAA;AAEpI,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;AAEnG,KAAK,SAAS,GAAG;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,CAAC;IAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,EAAE,CAAA;CAAE,CAAA;AAExE,wBAAgB,WAAW,CAAC,CAAC,SAAS,IAAI,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,CAYxI;AAMD,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,GAAG,UAAU,CAAC,cAAc,CAAC,CAexE;AAED,MAAM,MAAM,WAAW,GAAG;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,CAAC;IAAC,QAAQ,CAAC,UAAU,EAAE,MAAM,GAAG,SAAS,CAAA;CAAE,CAAA;AAExG,wBAAsB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,CAAC,WAAW,CAAC,EAAE,GAAG,EAAE,QAAQ,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,CAAC,CAgB3I"}
package/runtime/hooks.js CHANGED
@@ -4,51 +4,11 @@ import { dynamicPattern } from '../utils/parse-shell.js';
4
4
  import { isMissing } from '../utils/paths.js';
5
5
  import { messageOf } from '../utils/text.js';
6
6
  export const permissionEvents = ['tool.check', 'classic.PermissionRequest'];
7
- const flags = ['reloadSkills', 'retry'];
8
- const readFields = {
9
- Setup: ['additionalContext'],
10
- SessionStart: ['additionalContext', 'initialUserMessage', 'sessionTitle', 'watchPaths', 'reloadSkills'],
11
- SessionEnd: [],
12
- UserPromptSubmit: ['additionalContext', 'sessionTitle'],
13
- UserPromptExpansion: ['additionalContext'],
14
- InstructionsLoaded: [],
15
- ConfigChange: [],
16
- DirectoryAdded: [],
17
- PreModelSwitch: ['permissionDecision', 'permissionDecisionReason'],
18
- PostModelSwitch: ['additionalContext'],
19
- PermissionRequest: ['decision'],
20
- PermissionDenied: ['retry'],
21
- PostToolUse: ['additionalContext'],
22
- PostToolUseFailure: ['additionalContext'],
23
- PostToolBatch: ['additionalContext'],
24
- MessageDisplay: ['displayContent'],
25
- SubagentStart: ['additionalContext'],
26
- SubagentStop: ['additionalContext'],
27
- TaskCreated: [],
28
- TaskCompleted: [],
29
- TeammateIdle: [],
30
- Elicitation: [],
31
- ElicitationResult: [],
32
- Notification: [],
33
- PreCompact: [],
34
- PostCompact: [],
35
- Stop: ['additionalContext'],
36
- StopFailure: [],
37
- CwdChanged: [],
38
- FileChanged: [],
39
- WorktreeCreate: ['worktreePath'],
40
- WorktreeRemove: [],
41
- };
42
- export function classicHook(name, event, hook, report) {
7
+ export function classicHook(event, hook, report) {
43
8
  const routed = async (e, next) => {
44
9
  const ours = await Promise.resolve()
45
- .then(async () => {
46
- const answer = await hook(e);
47
- return answer === undefined ? undefined : classicResult(event, answer);
48
- })
10
+ .then(() => hook(e))
49
11
  .catch((error) => {
50
- if (event === 'PermissionRequest')
51
- return { decision: { behavior: 'deny', message: `${name}: the ${event} listener failed: ${messageOf(error)}` } };
52
12
  report({ id: `hook:${event}`, message: `The ${event} listener failed`, error });
53
13
  return undefined;
54
14
  });
@@ -80,9 +40,9 @@ export function userSkillHook(claude) {
80
40
  return next(e);
81
41
  };
82
42
  }
83
- export async function callFiles(name, claude, use, cwd) {
43
+ export async function callFiles(name, claude, folders, use, cwd) {
84
44
  try {
85
- const [home, configRoot] = await Promise.all([claude.env.home(), claude.env.configHome()]);
45
+ const { home, configRoot } = await folders;
86
46
  if (home === undefined)
87
47
  throw new Error('HOME is not set, so ~ in a path has no meaning.');
88
48
  const { shell, reads, writes } = callEffects(use, { cwd, home, ...(configRoot === undefined ? {} : { configRoot }), fs: claude.fs });
@@ -100,20 +60,6 @@ export async function callFiles(name, claude, use, cwd) {
100
60
  return { read: [], changed: [] };
101
61
  }
102
62
  }
103
- function classicResult(event, answer) {
104
- const result = {};
105
- if (answer.decision === 'block')
106
- result['block'] = answer.reason ?? '';
107
- const specific = answer.hookSpecificOutput ?? {};
108
- for (const [field, value] of Object.entries(specific)) {
109
- if (field === 'hookEventName' || (value === false && flags.includes(field)))
110
- continue;
111
- if (!readFields[event].includes(field))
112
- throw new Error(`it answered ${field}, which ${event} does not read.`);
113
- result[field] = field === 'additionalContext' ? [value] : value;
114
- }
115
- return result;
116
- }
117
63
  function mergeClassic(below, ours) {
118
64
  const merged = { ...below, ...ours };
119
65
  const context = [...(below['additionalContext'] ?? []), ...(ours['additionalContext'] ?? [])];
@@ -4,6 +4,7 @@ import type { Options } from '../options.js';
4
4
  import { type Steps } from '../records.js';
5
5
  import type { Claude } from './claude.js';
6
6
  import { type RoutedEvent } from './hooks.js';
7
+ import type { PermissionRoutes } from './permission-check.js';
7
8
  import { type RouterNext } from './router.js';
8
9
  import { type ShownToast } from './toaster.js';
9
10
  export type Plugin = {
@@ -27,5 +28,5 @@ export type Lifecycle<State extends object> = {
27
28
  raise(method: string, payload: Readonly<Record<string, unknown>>): Promise<unknown>;
28
29
  };
29
30
  export declare function readPlugin(claude: Claude): Promise<Plugin>;
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>;
31
+ export declare function createLifecycle<State extends object, Declared extends Options = Options>(definition: ModDefinition<State, string, Declared>, permissionRoutes?: () => PermissionRoutes | undefined, options?: PluginOptions, asksPerson?: boolean): Lifecycle<State>;
31
32
  //# sourceMappingURL=lifecycle.d.ts.map
@@ -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,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
+ {"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;AACzH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAA;AAO7D,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;AA2CD,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,gBAAgB,GAAE,MAAM,gBAAgB,GAAG,SAA2B,EACtE,OAAO,GAAE,aAAkB,EAC3B,UAAU,UAAO,GAChB,SAAS,CAAC,KAAK,CAAC,CAsdlB"}