@cmodjs/core 0.1.6 → 0.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/docs/commands.md CHANGED
@@ -82,8 +82,8 @@ cmod publish [path] [--dry-run]
82
82
 
83
83
  Releases the mod at `path` (default: the current folder) at the version in its `plugin.json`:
84
84
 
85
- 1. Builds the release from the committed files, leaving out `cli/`, `.github/`, and `.claude/`. A `"files"` list in `package.json` limits the release to the paths it lists, plus `.claude-plugin/`, `package.json`, the README, and the license, the way `npm publish` reads it. List every folder the hooks module imports from, such as `"files": ["hooks", "src", "skills"]`, because the bundle in step 2 is built from the release.
86
- 2. Bundles the hooks module that `hooks/hooks.json` names, with the mod's source and packages, into one readable `.js` file, and points `hooks/hooks.json` at it. Anthropic's plugin directory reads a repository without installing its packages, so it can follow a mod only when all its code is in one file.
85
+ 1. Builds the release from the committed files, leaving out `cli/`, `.github/`, and `.claude/`. A `"files"` list in `package.json` limits the release to the paths it lists, plus the folders of the install and uninstall steps, `.claude-plugin/`, `package.json`, the README, and the license, the way `npm publish` reads it. List every folder the hooks module imports from, such as `"files": ["hooks", "src", "skills"]`, because the bundle in step 2 is built from the release.
86
+ 2. Bundles the hooks module that `hooks/hooks.json` names, with the mod's source and packages, into one readable `.js` file, writes each control character in it as a `\u` escape, and points `hooks/hooks.json` at it. Anthropic's plugin directory reads a repository without installing its packages, so it can follow a mod only when all its code is in one file.
87
87
  3. Checks the release with `claude plugin validate --strict`, and stops with the validator's message when it fails.
88
88
  4. Commits the release as the `release` branch, and builds the release archive from that commit.
89
89
  5. Builds the program `cli/` declares, and writes `SHA256SUMS` for every file of the release.
package/docs/hooks.md CHANGED
@@ -12,6 +12,8 @@ type ModHook<E> = (input: HookInputs[E]) => HookAnswer | void | Promise<HookAnsw
12
12
 
13
13
  Add hooks in `setup`. Each `mod.on` adds one more hook. Several hooks on one event all run.
14
14
 
15
+ A hook on `PreToolUse` or `PermissionRequest` takes part in Claude Code's permission check, so the mod's `hooks/register.ts` calls `registerPermissionCheck(addHook)` after `registerMod` ([mod.md](mod.md)). Without it the mod does not start.
16
+
15
17
  ```ts
16
18
  import { defineMod } from '../node_modules/@cmodjs/core/mod.js'
17
19
 
package/docs/index.md CHANGED
@@ -15,7 +15,7 @@ my-mod/
15
15
  ├── .gitignore node_modules/ and .claude-plugin/types/
16
16
  ├── .oxlintrc.json leaves .claude-plugin/types/ out of the lint
17
17
  ├── hooks/hooks.json names hooks/register.ts as the hooks module
18
- ├── hooks/register.ts connect(on, myMod), and nothing else
18
+ ├── hooks/register.ts registerMod(addHook, myMod), and nothing else
19
19
  ├── src/mod.tsx defineMod: the state, the setup, the hooks, the panes
20
20
  ├── src/state.ts the starting state
21
21
  ├── src/panes/prompts.tsx one definePane per file
@@ -74,7 +74,7 @@ A mod imports from the files below, and every other file in this package is inte
74
74
  | `mod.js` | `Claude`, `RoutedEvent`, `RoutedHook`, `ToolCalls` | [jobs.md](jobs.md) |
75
75
  | `mod.js` | `ModEvent`, `ModHook`, `HookAnswer` | [hooks.md](hooks.md) |
76
76
  | `mod.js` | `notInstalled` | [dependencies.md](dependencies.md) |
77
- | `connect.js` | `connect` | [mod.md](mod.md) |
77
+ | `register.js` | `registerMod`, `registerPermissionCheck` | [mod.md](mod.md) |
78
78
  | `testing.js` | `testMod`, `TestOptions`, `TestInput`, `TestedMod`, `Fakes`, `Shown`, `TestCall` | [testing.md](testing.md) |
79
79
  | `ui/define-pane.js` | `definePane`, `Pane` | [ui.md](ui.md) |
80
80
  | `ui/elements.js` | `Box`, `Text`, `Button`, `Link`, `Code`, `Markdown`, `Input`, `Select`, `Image`, `drawWith` | [ui.md](ui.md) |
package/docs/jobs.md CHANGED
@@ -174,6 +174,7 @@ type Rule<State> = Target & {
174
174
  - When CMod cannot read the call or the session, it denies the call with the reason.
175
175
  - For a PowerShell command, or a Bash command CMod cannot fully parse, a `command` rule matches when every word of its pattern appears in the command line. A `read` or `write` rule matches when the command line holds the last segment of its pattern, after the last `/`, and only when that segment holds none of `*?[]{}`. So `{ write: 'Domain.md' }` matches such a command that names `Domain.md`, and `{ write: '**/.env*' }` never matches one.
176
176
  - `permissions({})` throws `permissions: give it a deny or an ask rule, or remove it from setup.`
177
+ - A mod that uses `permissions` calls `registerPermissionCheck(addHook)` after `registerMod` in `hooks/register.ts` ([mod.md](mod.md)). Without it the mod does not start.
177
178
 
178
179
  `when` gets the call, typed by the target's key. `jobs/permissions.js` exports each type:
179
180
 
@@ -360,11 +361,11 @@ type JobContext<State> = {
360
361
 
361
362
  - `claude` reaches Claude Code itself. [The claude members](#the-claude-members) lists each one.
362
363
  - `claude.store` is where CMod saves `mod.state`, under keys that start with `<mod>.`, such as `<mod>.<key>`. A job of your own never writes those keys, or it changes the mod's state behind CMod's back.
363
- - `on` adds a Claude Code hook-module handler, `(e, next) => …`, on a `classic.<ModEvent>` event or on `tool.check`, `tool.call`, `prompt.submit`, `prompt.context`, `command.run`, `session.measure`, `skill.prompt`, `ui.render`, `ui.press`, `ui.close`, or `cmod.call`. The handler calls `next(e)` to pass the event on.
364
+ - `on` adds a Claude Code hook-module handler, `(e, next) => …`, on a `classic.<ModEvent>` event or on `tool.check`, `tool.call`, `prompt.submit`, `prompt.context`, `command.run`, `session.measure`, `skill.prompt`, `ui.render`, `ui.press`, `ui.close`, or `cmod.call`. The handler calls `next(e)` to pass the event on. A job with a handler on `tool.check`, `classic.PreToolUse`, or `classic.PermissionRequest` needs `registerPermissionCheck` in `hooks/register.ts` ([mod.md](mod.md)).
364
365
  - `announce` adds a phrase to the list CMod logs when the mod first starts.
365
366
  - `reserveName` throws `taken` when the mod already reserved that `kind` and `name`. A job calls it to refuse a duplicate.
366
367
  - `toolCalls.agentOf(toolUseId)` resolves `{ agentId?, agentType? }`, the subagent behind a call.
367
- - `toolCalls.cwdOf(toolUseId)` returns the working folder at the call's `PreToolUse`, or `undefined` once its `PostToolUse` or `PostToolUseFailure` has run. CMod keeps the folders of the last 100 calls.
368
+ - `toolCalls.cwdOf(toolUseId)` returns the working folder when the call ran, or `undefined` once its `PostToolUse` or `PostToolUseFailure` has run. CMod keeps the folders of the last 100 calls.
368
369
 
369
370
  ```ts
370
371
  import { defineMod, type Job } from '../node_modules/@cmodjs/core/mod.js'
package/docs/mod.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Define a mod
2
2
 
3
- `defineMod` describes a mod: its name, its state, the methods it offers other mods, and the `setup` function that attaches everything to Claude Code. `connect` hands the definition to Claude Code.
3
+ `defineMod` describes a mod: its name, its state, the methods it offers other mods, and the `setup` function that attaches everything to Claude Code. `registerMod` registers the definition with Claude Code.
4
4
 
5
5
  ## defineMod
6
6
 
@@ -51,28 +51,43 @@ When a step fails, the mod does not start. Its progress line above the prompt na
51
51
  - `its state did not load: <error>` when reading the saved values or a `state.json` fails.
52
52
  - `its setup function threw: <error>` when `setup` throws or rejects.
53
53
  - `its open panes did not load: <error>` when Claude Code cannot list the open panes.
54
+ - `it decides permissions on <events>, so hooks/register.ts must call registerPermissionCheck(addHook) after registerMod` when `setup` adds a hook on `PreToolUse` or `PermissionRequest`, or a job that decides permissions, and `register.ts` does not register the permission check.
54
55
 
55
56
  Each of these says `Fix it, then run /reload-plugins.`
56
57
 
57
- ## Connect the mod to Claude Code
58
+ ## Register the mod with Claude Code
58
59
 
59
- `hooks/hooks.json` names `hooks/register.ts` as the plugin's hooks module. `register.ts` calls `connect` and does nothing else:
60
+ `hooks/hooks.json` names `hooks/register.ts` as the plugin's hooks module. `register.ts` calls `registerMod` and does nothing else:
60
61
 
61
62
  ```ts
62
63
  import type { On } from 'claude-code'
63
- import { connect } from '../node_modules/@cmodjs/core/connect.js'
64
+ import { registerMod } from '../node_modules/@cmodjs/core/register.js'
64
65
  import { greeter } from '../src/mod.js'
65
66
 
66
- export function register(on: On): void {
67
- connect(on, greeter)
67
+ export function register(addHook: On): void {
68
+ registerMod(addHook, greeter)
68
69
  }
69
70
  ```
70
71
 
71
72
  ```ts
72
- connect<State>(on: On, definition: ModDefinition<State>): void
73
+ registerMod<State>(addHook: On, definition: ModDefinition<State>): void
74
+ registerPermissionCheck(addHook: On): void
73
75
  ```
74
76
 
75
- `connect` registers one handler per Claude Code event the mod can use, and routes each event to the mod. 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. 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.
77
+ `registerMod` registers one handler per Claude Code event the mod can use, and routes each event to the mod. It leaves out Claude Code's permission check, the events `tool.check`, `PreToolUse`, and `PermissionRequest`.
78
+
79
+ A mod that decides permissions calls `registerPermissionCheck` after `registerMod`. That covers a hook on `PreToolUse` or `PermissionRequest`, the `permissions` job, and a job of your own on `tool.check`:
80
+
81
+ ```ts
82
+ export function register(addHook: On): void {
83
+ registerMod(addHook, guard)
84
+ registerPermissionCheck(addHook)
85
+ }
86
+ ```
87
+
88
+ A mod without it has no part in Claude Code's permission check, so the plugin directory reads it as one that never answers a permission. A mod that decides permissions without it does not start, and its progress line names the line to add.
89
+
90
+ 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 the method `mod.on`, 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.
76
91
 
77
92
  ## What `mod` can call
78
93
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cmodjs/core",
3
- "version": "0.1.6",
3
+ "version": "0.1.7",
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",
@@ -25,7 +25,7 @@
25
25
  "docs/"
26
26
  ],
27
27
  "scripts": {
28
- "build": "tsc -p tsconfig.build.json && bun build src/vendor.ts --outfile vendor.js --format esm --target browser && NODE_ENV=production bun build src/vendor-markdown.ts --outfile vendor-markdown.js --format esm --target browser --conditions worker",
28
+ "build": "tsc -p tsconfig.build.json && bun build src/vendor.ts --outfile vendor.js --format esm --target browser --define process=undefined && NODE_ENV=production bun build src/vendor-markdown.ts --outfile vendor-markdown.js --format esm --target browser --conditions worker",
29
29
  "check": "tsc -p tsconfig.json --noEmit && oxlint --deny-warnings",
30
30
  "test": "bun test"
31
31
  },
package/register.d.ts ADDED
@@ -0,0 +1,5 @@
1
+ import type { On } from 'claude-code';
2
+ import type { ModDefinition } from './mod.js';
3
+ export declare function registerMod<State extends object>(addHook: On, definition: ModDefinition<State>): void;
4
+ export declare function registerPermissionCheck(addHook: On): void;
5
+ //# sourceMappingURL=register.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"register.d.ts","sourceRoot":"","sources":["src/register.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAoD,EAAE,EAAE,MAAM,aAAa,CAAA;AACvF,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,UAAU,CAAA;AA0E7C,wBAAgB,WAAW,CAAC,KAAK,SAAS,MAAM,EAAE,OAAO,EAAE,EAAE,EAAE,UAAU,EAAE,aAAa,CAAC,KAAK,CAAC,GAAG,IAAI,CA4BrG;AAED,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,EAAE,GAAG,IAAI,CAKzD"}
@@ -1,5 +1,6 @@
1
1
  import { createLifecycle, readPlugin } from './runtime/lifecycle.js';
2
2
  let lifecycle;
3
+ let checksPermissions = false;
3
4
  async function startMod($, eventInput, passOn) {
4
5
  const claude = {
5
6
  plugin: { name: $.plugin.name, root: $.plugin.root },
@@ -63,34 +64,38 @@ async function startMod($, eventInput, passOn) {
63
64
  function routeToMod(_$, eventInput, passOn) {
64
65
  return lifecycle.route(passOn.event, eventInput, (passed) => passOn(passed));
65
66
  }
66
- export function connect(on, definition) {
67
- lifecycle = createLifecycle(definition);
68
- on('session.start', startMod);
69
- on('classic.SessionStart', routeToMod);
70
- on('classic.SessionEnd', routeToMod);
71
- on('classic.UserPromptSubmit', routeToMod);
72
- on('classic.InstructionsLoaded', routeToMod);
73
- on('classic.PreToolUse', routeToMod);
74
- on('classic.PermissionRequest', routeToMod);
75
- on('classic.PermissionDenied', routeToMod);
76
- on('classic.PostToolUse', routeToMod);
77
- on('classic.PostToolUseFailure', routeToMod);
78
- on('classic.PostToolBatch', routeToMod);
79
- on('classic.SubagentStart', routeToMod);
80
- on('classic.SubagentStop', routeToMod);
81
- on('classic.Notification', routeToMod);
82
- on('classic.PreCompact', routeToMod);
83
- on('classic.Stop', routeToMod);
84
- on('classic.StopFailure', routeToMod);
85
- on('tool.check', routeToMod);
86
- on('tool.call', routeToMod);
87
- on('prompt.submit', routeToMod);
88
- on('prompt.context', routeToMod);
89
- on('command.run', routeToMod);
90
- on('session.measure', routeToMod);
91
- on('skill.prompt', routeToMod);
92
- on('ui.render', routeToMod);
93
- on('ui.press', routeToMod);
94
- on('ui.close', routeToMod);
95
- on('cmod.call', routeToMod);
67
+ export function registerMod(addHook, definition) {
68
+ checksPermissions = false;
69
+ lifecycle = createLifecycle(definition, () => checksPermissions);
70
+ addHook('session.start', startMod);
71
+ addHook('classic.SessionStart', routeToMod);
72
+ addHook('classic.SessionEnd', routeToMod);
73
+ addHook('classic.UserPromptSubmit', routeToMod);
74
+ addHook('classic.InstructionsLoaded', routeToMod);
75
+ addHook('classic.PermissionDenied', routeToMod);
76
+ addHook('classic.PostToolUse', routeToMod);
77
+ addHook('classic.PostToolUseFailure', routeToMod);
78
+ addHook('classic.PostToolBatch', routeToMod);
79
+ addHook('classic.SubagentStart', routeToMod);
80
+ addHook('classic.SubagentStop', routeToMod);
81
+ addHook('classic.Notification', routeToMod);
82
+ addHook('classic.PreCompact', routeToMod);
83
+ addHook('classic.Stop', routeToMod);
84
+ addHook('classic.StopFailure', routeToMod);
85
+ addHook('tool.call', routeToMod);
86
+ addHook('prompt.submit', routeToMod);
87
+ addHook('prompt.context', routeToMod);
88
+ addHook('command.run', routeToMod);
89
+ addHook('session.measure', routeToMod);
90
+ addHook('skill.prompt', routeToMod);
91
+ addHook('ui.render', routeToMod);
92
+ addHook('ui.press', routeToMod);
93
+ addHook('ui.close', routeToMod);
94
+ addHook('cmod.call', routeToMod);
95
+ }
96
+ export function registerPermissionCheck(addHook) {
97
+ checksPermissions = true;
98
+ addHook('tool.check', routeToMod);
99
+ addHook('classic.PreToolUse', routeToMod);
100
+ addHook('classic.PermissionRequest', routeToMod);
96
101
  }
@@ -3,6 +3,7 @@ import type { Claude } from './claude.js';
3
3
  import type { RoutedHook } from './router.js';
4
4
  import { type ToolCalls } from './tool-calls.js';
5
5
  export type RoutedEvent = `classic.${ModEvent}` | 'tool.check' | 'tool.call' | 'prompt.submit' | 'prompt.context' | 'command.run' | 'session.measure' | 'skill.prompt' | 'ui.render' | 'ui.press' | 'ui.close' | 'cmod.call';
6
+ export declare const permissionEvents: readonly RoutedEvent[];
6
7
  export declare function classicHook<E extends ModEvent>(name: string, event: E, hook: ModHook<E>, claude: Claude, calls: ToolCalls): RoutedHook<RoutedEvent>;
7
8
  export declare function userSkillHook(claude: Claude): RoutedHook<'skill.prompt'>;
8
9
  export declare const strictness: readonly ['allow', 'ask', 'deny'];
@@ -1 +1 @@
1
- {"version":3,"file":"hooks.d.ts","sourceRoot":"","sources":["../src/runtime/hooks.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAc,QAAQ,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AAE9D,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AACzC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AAC7C,OAAO,EAAe,KAAK,SAAS,EAAE,MAAM,iBAAiB,CAAA;AAK7D,MAAM,MAAM,WAAW,GACnB,WAAW,QAAQ,EAAE,GACrB,YAAY,GACZ,WAAW,GACX,eAAe,GACf,gBAAgB,GAChB,aAAa,GACb,iBAAiB,GACjB,cAAc,GACd,WAAW,GACX,UAAU,GACV,UAAU,GACV,WAAW,CAAA;AA8Bf,wBAAgB,WAAW,CAAC,CAAC,SAAS,QAAQ,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,EAAE,IAAI,EAAE,OAAO,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,GAAG,UAAU,CAAC,WAAW,CAAC,CA0BnJ;AAMD,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,GAAG,UAAU,CAAC,cAAc,CAAC,CAexE;AA4DD,eAAO,MAAM,UAAU,YAAI,OAAO,EAAE,KAAK,EAAE,MAAM,CAAU,CAAA"}
1
+ {"version":3,"file":"hooks.d.ts","sourceRoot":"","sources":["../src/runtime/hooks.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAc,QAAQ,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AAE9D,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AACzC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AAC7C,OAAO,EAAe,KAAK,SAAS,EAAE,MAAM,iBAAiB,CAAA;AAK7D,MAAM,MAAM,WAAW,GACnB,WAAW,QAAQ,EAAE,GACrB,YAAY,GACZ,WAAW,GACX,eAAe,GACf,gBAAgB,GAChB,aAAa,GACb,iBAAiB,GACjB,cAAc,GACd,WAAW,GACX,UAAU,GACV,UAAU,GACV,WAAW,CAAA;AAEf,eAAO,MAAM,gBAAgB,EAAE,SAAS,WAAW,EAAsE,CAAA;AA8BzH,wBAAgB,WAAW,CAAC,CAAC,SAAS,QAAQ,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,EAAE,IAAI,EAAE,OAAO,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,GAAG,UAAU,CAAC,WAAW,CAAC,CA0BnJ;AAMD,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,GAAG,UAAU,CAAC,cAAc,CAAC,CAexE;AA4DD,eAAO,MAAM,UAAU,YAAI,OAAO,EAAE,KAAK,EAAE,MAAM,CAAU,CAAA"}
package/runtime/hooks.js CHANGED
@@ -3,6 +3,7 @@ import { toolInputOf } from './tool-calls.js';
3
3
  import { callEffects } from '../utils/call-effects.js';
4
4
  import { dynamicPattern } from '../utils/parse-shell.js';
5
5
  import { messageOf } from '../utils/text.js';
6
+ export const permissionEvents = ['tool.check', 'classic.PreToolUse', 'classic.PermissionRequest'];
6
7
  const flags = ['suppressOriginalPrompt', 'reloadSkills', 'retry'];
7
8
  const readFields = {
8
9
  SessionStart: ['additionalContext', 'initialUserMessage', 'sessionTitle', 'watchPaths', 'reloadSkills'],
@@ -20,5 +20,5 @@ export type Lifecycle<State extends object> = {
20
20
  route<N extends RoutedEvent>(event: N, e: Frozen<Args<N>>, next: RouterNext<N>): Promise<EventResult<N>>;
21
21
  };
22
22
  export declare function readPlugin(claude: Claude): Promise<Plugin>;
23
- export declare function createLifecycle<State extends object>(definition: ModDefinition<State>): Lifecycle<State>;
23
+ export declare function createLifecycle<State extends object>(definition: ModDefinition<State>, checksPermissions?: () => boolean): Lifecycle<State>;
24
24
  //# 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,EAAiE,MAAM,aAAa,CAAA;AAC3H,OAAO,KAAK,EAAE,GAAG,EAAE,aAAa,EAAqB,MAAM,WAAW,CAAA;AAItE,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AAGzC,OAAO,EAA8B,KAAK,WAAW,EAAE,MAAM,YAAY,CAAA;AACzE,OAAO,EAA6B,KAAK,UAAU,EAAE,MAAM,aAAa,CAAA;AAKxE,MAAM,MAAM,MAAM,GAAG;IACnB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,SAAS,CAAA;IACpC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,WAAW,EAAE,OAAO,CAAA;IAC7B,QAAQ,CAAC,YAAY,EAAE,OAAO,CAAA;CAC/B,CAAA;AAED,MAAM,MAAM,KAAK,GAAG,UAAU,GAAG,YAAY,GAAG,SAAS,GAAG,UAAU,GAAG,QAAQ,GAAG,OAAO,GAAG,QAAQ,CAAA;AAEtG,MAAM,MAAM,SAAS,CAAC,KAAK,SAAS,MAAM,IAAI;IAC5C,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAA;IACrB,QAAQ,CAAC,GAAG,EAAE,GAAG,CAAC,KAAK,CAAC,GAAG,SAAS,CAAA;IACpC,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAA;IACzB,KAAK,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAC/E,KAAK,CAAC,CAAC,SAAS,WAAW,EAAE,KAAK,EAAE,CAAC,EAAE,CAAC,EAAE,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,UAAU,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,CAAA;CACzG,CAAA;AAgCD,wBAAsB,UAAU,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAyBhE;AAkCD,wBAAgB,eAAe,CAAC,KAAK,SAAS,MAAM,EAAE,UAAU,EAAE,aAAa,CAAC,KAAK,CAAC,GAAG,SAAS,CAAC,KAAK,CAAC,CAkOxG"}
1
+ {"version":3,"file":"lifecycle.d.ts","sourceRoot":"","sources":["../src/runtime/lifecycle.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,WAAW,EAAE,MAAM,EAAiE,MAAM,aAAa,CAAA;AAC3H,OAAO,KAAK,EAAE,GAAG,EAAE,aAAa,EAAqB,MAAM,WAAW,CAAA;AAItE,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AAGzC,OAAO,EAAgD,KAAK,WAAW,EAAE,MAAM,YAAY,CAAA;AAC3F,OAAO,EAA6B,KAAK,UAAU,EAAE,MAAM,aAAa,CAAA;AAKxE,MAAM,MAAM,MAAM,GAAG;IACnB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,SAAS,CAAA;IACpC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,WAAW,EAAE,OAAO,CAAA;IAC7B,QAAQ,CAAC,YAAY,EAAE,OAAO,CAAA;CAC/B,CAAA;AAED,MAAM,MAAM,KAAK,GAAG,UAAU,GAAG,YAAY,GAAG,SAAS,GAAG,UAAU,GAAG,QAAQ,GAAG,OAAO,GAAG,QAAQ,CAAA;AAEtG,MAAM,MAAM,SAAS,CAAC,KAAK,SAAS,MAAM,IAAI;IAC5C,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAA;IACrB,QAAQ,CAAC,GAAG,EAAE,GAAG,CAAC,KAAK,CAAC,GAAG,SAAS,CAAA;IACpC,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAA;IACzB,KAAK,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAC/E,KAAK,CAAC,CAAC,SAAS,WAAW,EAAE,KAAK,EAAE,CAAC,EAAE,CAAC,EAAE,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,UAAU,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,CAAA;CACzG,CAAA;AAiCD,wBAAsB,UAAU,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAyBhE;AAkCD,wBAAgB,eAAe,CAAC,KAAK,SAAS,MAAM,EAAE,UAAU,EAAE,aAAa,CAAC,KAAK,CAAC,EAAE,iBAAiB,GAAE,MAAM,OAAoB,GAAG,SAAS,CAAC,KAAK,CAAC,CAkOvJ"}
@@ -3,7 +3,7 @@ import { relativePath } from '../utils/paths.js';
3
3
  import { formatExit, listed, messageOf } from '../utils/text.js';
4
4
  import { beforeDeadline } from './deadline.js';
5
5
  import { answerCall, dependencyCalls, notInstalled } from './dependencies.js';
6
- import { classicHook, userSkillHook } from './hooks.js';
6
+ import { classicHook, permissionEvents, userSkillHook } from './hooks.js';
7
7
  import { createRouter } from './router.js';
8
8
  import { createState } from './state.js';
9
9
  import { toolCalls } from './tool-calls.js';
@@ -79,7 +79,7 @@ async function cmodVersion(claude) {
79
79
  throw new Error(`cmod --version ${formatExit(result.exitCode, lastLineOf(result.stderr) ?? '')}`);
80
80
  return result.stdout.trim().split(/\s+/).at(-1);
81
81
  }
82
- export function createLifecycle(definition) {
82
+ export function createLifecycle(definition, checksPermissions = () => true) {
83
83
  let router = createRouter();
84
84
  let phase = 'starting';
85
85
  let runtime;
@@ -95,7 +95,7 @@ export function createLifecycle(definition) {
95
95
  const startSettled = new Promise((resolve) => (settleStart = resolve));
96
96
  const claude = () => {
97
97
  if (runtime === undefined)
98
- throw new Error(`${definition.name}: the lifecycle has not started. connect(on, mod) starts it at session.start.`);
98
+ throw new Error(`${definition.name}: the lifecycle has not started. registerMod(addHook, mod) starts it at session.start.`);
99
99
  return runtime.claude;
100
100
  };
101
101
  const showLine = () => {
@@ -136,7 +136,7 @@ export function createLifecycle(definition) {
136
136
  if (runtime === undefined || plugin === undefined)
137
137
  return;
138
138
  const activeRouter = createRouter();
139
- const active = await createMod(definition, { ...runtime, router: activeRouter, dataFolder: dataFolder(plugin.store, plugin.name) }).catch((error) => {
139
+ const active = await createMod(definition, { ...runtime, router: activeRouter, dataFolder: dataFolder(plugin.store, plugin.name), checksPermissions }).catch((error) => {
140
140
  fail(messageOf(error), 'Fix it, then run /reload-plugins.');
141
141
  throw error;
142
142
  });
@@ -429,6 +429,10 @@ async function createMod(definition, runtime) {
429
429
  };
430
430
  await failsAs('its state did not load', () => modState.load());
431
431
  await failsAs('its setup function threw', () => definition.setup(mod));
432
+ const permissionHooks = permissionEvents.filter((event) => router.has(event));
433
+ if (permissionHooks.length > 0 && !runtime.checksPermissions()) {
434
+ throw new Error(`it decides permissions on ${listed(permissionHooks)}, so hooks/register.ts must call registerPermissionCheck(addHook) after registerMod`);
435
+ }
432
436
  await failsAs('its open panes did not load', () => area.restorePanes());
433
437
  if (hookEvents.length > 0)
434
438
  added.push(`${hookEvents.length === 1 ? 'a hook' : 'hooks'} on ${listed(hookEvents)}`);
@@ -4,6 +4,7 @@ export type RouterNext<N extends RoutedEvent> = (e: Frozen<Args<N>>) => Promise<
4
4
  export type RoutedHook<N extends RoutedEvent> = (e: Frozen<Args<N>>, next: RouterNext<N>) => EventResult<N> | Promise<EventResult<N>>;
5
5
  export type Router = {
6
6
  add<N extends RoutedEvent>(event: N, hook: RoutedHook<N>): void;
7
+ has(event: RoutedEvent): boolean;
7
8
  dispatch<N extends RoutedEvent>(event: N, e: Frozen<Args<N>>, next: RouterNext<N>): Promise<EventResult<N>>;
8
9
  };
9
10
  export declare function createRouter(): Router;
@@ -1 +1 @@
1
- {"version":3,"file":"router.d.ts","sourceRoot":"","sources":["../src/runtime/router.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,WAAW,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AAC5D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAA;AAE7C,MAAM,MAAM,UAAU,CAAC,CAAC,SAAS,WAAW,IAAI,CAAC,CAAC,EAAE,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,OAAO,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,CAAA;AAE/F,MAAM,MAAM,UAAU,CAAC,CAAC,SAAS,WAAW,IAAI,CAAC,CAAC,EAAE,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,UAAU,CAAC,CAAC,CAAC,KAAK,WAAW,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,CAAA;AAErI,MAAM,MAAM,MAAM,GAAG;IACnB,GAAG,CAAC,CAAC,SAAS,WAAW,EAAE,KAAK,EAAE,CAAC,EAAE,IAAI,EAAE,UAAU,CAAC,CAAC,CAAC,GAAG,IAAI,CAAA;IAC/D,QAAQ,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;CAC5G,CAAA;AAID,wBAAgB,YAAY,IAAI,MAAM,CAmBrC"}
1
+ {"version":3,"file":"router.d.ts","sourceRoot":"","sources":["../src/runtime/router.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,WAAW,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AAC5D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAA;AAE7C,MAAM,MAAM,UAAU,CAAC,CAAC,SAAS,WAAW,IAAI,CAAC,CAAC,EAAE,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,OAAO,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,CAAA;AAE/F,MAAM,MAAM,UAAU,CAAC,CAAC,SAAS,WAAW,IAAI,CAAC,CAAC,EAAE,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,UAAU,CAAC,CAAC,CAAC,KAAK,WAAW,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,CAAA;AAErI,MAAM,MAAM,MAAM,GAAG;IACnB,GAAG,CAAC,CAAC,SAAS,WAAW,EAAE,KAAK,EAAE,CAAC,EAAE,IAAI,EAAE,UAAU,CAAC,CAAC,CAAC,GAAG,IAAI,CAAA;IAC/D,GAAG,CAAC,KAAK,EAAE,WAAW,GAAG,OAAO,CAAA;IAChC,QAAQ,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;CAC5G,CAAA;AAID,wBAAgB,YAAY,IAAI,MAAM,CAoBrC"}
package/runtime/router.js CHANGED
@@ -6,6 +6,7 @@ export function createRouter() {
6
6
  chain.push(hook);
7
7
  hooks.set(event, chain);
8
8
  },
9
+ has: (event) => hooks.has(event),
9
10
  dispatch(event, e, next) {
10
11
  const chain = hooks.get(event);
11
12
  const call = async (index, current) => {
@@ -1 +1 @@
1
- {"version":3,"file":"tool-calls.d.ts","sourceRoot":"","sources":["../src/runtime/tool-calls.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AACzC,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AAEzC,MAAM,MAAM,KAAK,GAAG;IAAE,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,CAAA;AAE5D,MAAM,MAAM,SAAS,GAAG;IACtB,OAAO,CAAC,SAAS,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAAC,KAAK,CAAC,CAAA;IACtD,KAAK,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAAA;CAC7C,CAAA;AAED,eAAO,MAAM,YAAY,EAAE,SAAS,MAAM,EAAkD,CAAA;AAE5F,wBAAgB,WAAW,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAErE;AAMD,wBAAgB,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,SAAS,CAiDnE"}
1
+ {"version":3,"file":"tool-calls.d.ts","sourceRoot":"","sources":["../src/runtime/tool-calls.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AACzC,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AAEzC,MAAM,MAAM,KAAK,GAAG;IAAE,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,CAAA;AAE5D,MAAM,MAAM,SAAS,GAAG;IACtB,OAAO,CAAC,SAAS,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAAC,KAAK,CAAC,CAAA;IACtD,KAAK,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAAA;CAC7C,CAAA;AAED,eAAO,MAAM,YAAY,EAAE,SAAS,MAAM,EAAkD,CAAA;AAE5F,wBAAgB,WAAW,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAErE;AAMD,wBAAgB,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,SAAS,CA8CnE"}
@@ -20,6 +20,12 @@ export function toolCalls(claude, router) {
20
20
  return next(e);
21
21
  });
22
22
  router.add('tool.call', async (e, next) => {
23
+ cwds.set(e.tool_use_id, await claude.session.cwd());
24
+ for (const oldest of cwds.keys()) {
25
+ if (cwds.size <= keptCalls)
26
+ break;
27
+ cwds.delete(oldest);
28
+ }
23
29
  if (e.agentId === undefined)
24
30
  return next(e);
25
31
  agentIds.set(e.tool_use_id, e.agentId);
@@ -30,15 +36,6 @@ export function toolCalls(claude, router) {
30
36
  agentIds.delete(e.tool_use_id);
31
37
  }
32
38
  });
33
- router.add('classic.PreToolUse', async (e, next) => {
34
- cwds.set(e.tool_use_id, await claude.session.cwd());
35
- for (const oldest of cwds.keys()) {
36
- if (cwds.size <= keptCalls)
37
- break;
38
- cwds.delete(oldest);
39
- }
40
- return next(e);
41
- });
42
39
  for (const event of ['classic.PostToolUse', 'classic.PostToolUseFailure']) {
43
40
  router.add(event, async (e, next) => {
44
41
  try {
@@ -6,6 +6,8 @@ import { createLifecycle, readPlugin, type Lifecycle } from './runtime/lifecycle
6
6
 
7
7
  let lifecycle: Lifecycle<object>
8
8
 
9
+ let checksPermissions = false
10
+
9
11
  async function startMod($: EngineInterface, eventInput: Frozen<Args<'session.start'>>, passOn: Next<'session.start'>): Promise<EventResult<'session.start'>> {
10
12
  const claude: Claude = {
11
13
  plugin: { name: $.plugin.name, root: $.plugin.root },
@@ -71,34 +73,39 @@ function routeToMod<N extends RoutedEvent>(_$: EngineInterface, eventInput: Froz
71
73
  return lifecycle.route(passOn.event, eventInput, (passed: unknown) => passOn(passed as Args<N>) as Promise<EventResult<N>>)
72
74
  }
73
75
 
74
- export function connect<State extends object>(on: On, definition: ModDefinition<State>): void {
75
- lifecycle = createLifecycle(definition)
76
- on('session.start', startMod)
77
- on('classic.SessionStart', routeToMod)
78
- on('classic.SessionEnd', routeToMod)
79
- on('classic.UserPromptSubmit', routeToMod)
80
- on('classic.InstructionsLoaded', routeToMod)
81
- on('classic.PreToolUse', routeToMod)
82
- on('classic.PermissionRequest', routeToMod)
83
- on('classic.PermissionDenied', routeToMod)
84
- on('classic.PostToolUse', routeToMod)
85
- on('classic.PostToolUseFailure', routeToMod)
86
- on('classic.PostToolBatch', routeToMod)
87
- on('classic.SubagentStart', routeToMod)
88
- on('classic.SubagentStop', routeToMod)
89
- on('classic.Notification', routeToMod)
90
- on('classic.PreCompact', routeToMod)
91
- on('classic.Stop', routeToMod)
92
- on('classic.StopFailure', routeToMod)
93
- on('tool.check', routeToMod)
94
- on('tool.call', routeToMod)
95
- on('prompt.submit', routeToMod)
96
- on('prompt.context', routeToMod)
97
- on('command.run', routeToMod)
98
- on('session.measure', routeToMod)
99
- on('skill.prompt', routeToMod)
100
- on('ui.render', routeToMod)
101
- on('ui.press', routeToMod)
102
- on('ui.close', routeToMod)
103
- on('cmod.call', routeToMod)
76
+ export function registerMod<State extends object>(addHook: On, definition: ModDefinition<State>): void {
77
+ checksPermissions = false
78
+ lifecycle = createLifecycle(definition, () => checksPermissions)
79
+ addHook('session.start', startMod)
80
+ addHook('classic.SessionStart', routeToMod)
81
+ addHook('classic.SessionEnd', routeToMod)
82
+ addHook('classic.UserPromptSubmit', routeToMod)
83
+ addHook('classic.InstructionsLoaded', routeToMod)
84
+ addHook('classic.PermissionDenied', routeToMod)
85
+ addHook('classic.PostToolUse', routeToMod)
86
+ addHook('classic.PostToolUseFailure', routeToMod)
87
+ addHook('classic.PostToolBatch', routeToMod)
88
+ addHook('classic.SubagentStart', routeToMod)
89
+ addHook('classic.SubagentStop', routeToMod)
90
+ addHook('classic.Notification', routeToMod)
91
+ addHook('classic.PreCompact', routeToMod)
92
+ addHook('classic.Stop', routeToMod)
93
+ addHook('classic.StopFailure', routeToMod)
94
+ addHook('tool.call', routeToMod)
95
+ addHook('prompt.submit', routeToMod)
96
+ addHook('prompt.context', routeToMod)
97
+ addHook('command.run', routeToMod)
98
+ addHook('session.measure', routeToMod)
99
+ addHook('skill.prompt', routeToMod)
100
+ addHook('ui.render', routeToMod)
101
+ addHook('ui.press', routeToMod)
102
+ addHook('ui.close', routeToMod)
103
+ addHook('cmod.call', routeToMod)
104
+ }
105
+
106
+ export function registerPermissionCheck(addHook: On): void {
107
+ checksPermissions = true
108
+ addHook('tool.check', routeToMod)
109
+ addHook('classic.PreToolUse', routeToMod)
110
+ addHook('classic.PermissionRequest', routeToMod)
104
111
  }
@@ -22,6 +22,8 @@ export type RoutedEvent =
22
22
  | 'ui.close'
23
23
  | 'cmod.call'
24
24
 
25
+ export const permissionEvents: readonly RoutedEvent[] = ['tool.check', 'classic.PreToolUse', 'classic.PermissionRequest']
26
+
25
27
  type ClassicFields = Record<string, unknown>
26
28
 
27
29
  type PreToolUseInput = Parameters<ModHook<'PreToolUse'>>[0]
@@ -6,7 +6,7 @@ import { formatExit, listed, messageOf } from '../utils/text.js'
6
6
  import type { Claude } from './claude.js'
7
7
  import { beforeDeadline } from './deadline.js'
8
8
  import { answerCall, dependencyCalls, notInstalled } from './dependencies.js'
9
- import { classicHook, userSkillHook, type RoutedEvent } from './hooks.js'
9
+ import { classicHook, permissionEvents, userSkillHook, type RoutedEvent } from './hooks.js'
10
10
  import { createRouter, type Router, type RouterNext } from './router.js'
11
11
  import { createState } from './state.js'
12
12
  import { toolCalls } from './tool-calls.js'
@@ -36,6 +36,7 @@ type ModRuntime = {
36
36
  readonly router: Router
37
37
  readonly progress: Progress
38
38
  readonly dataFolder: string
39
+ readonly checksPermissions: () => boolean
39
40
  }
40
41
 
41
42
  type ActiveMod<State extends object> = {
@@ -120,7 +121,7 @@ async function cmodVersion(claude: Claude): Promise<string | undefined> {
120
121
  return result.stdout.trim().split(/\s+/).at(-1)
121
122
  }
122
123
 
123
- export function createLifecycle<State extends object>(definition: ModDefinition<State>): Lifecycle<State> {
124
+ export function createLifecycle<State extends object>(definition: ModDefinition<State>, checksPermissions: () => boolean = () => true): Lifecycle<State> {
124
125
  let router: Router = createRouter()
125
126
  let phase: Phase = 'starting'
126
127
  let runtime: Pick<ModRuntime, 'claude' | 'progress'> | undefined
@@ -136,7 +137,7 @@ export function createLifecycle<State extends object>(definition: ModDefinition<
136
137
  const startSettled = new Promise<void>((resolve) => (settleStart = resolve))
137
138
 
138
139
  const claude = () => {
139
- if (runtime === undefined) throw new Error(`${definition.name}: the lifecycle has not started. connect(on, mod) starts it at session.start.`)
140
+ if (runtime === undefined) throw new Error(`${definition.name}: the lifecycle has not started. registerMod(addHook, mod) starts it at session.start.`)
140
141
  return runtime.claude
141
142
  }
142
143
 
@@ -180,7 +181,7 @@ export function createLifecycle<State extends object>(definition: ModDefinition<
180
181
  const activate = async () => {
181
182
  if (runtime === undefined || plugin === undefined) return
182
183
  const activeRouter = createRouter()
183
- const active = await createMod(definition, { ...runtime, router: activeRouter, dataFolder: dataFolder(plugin.store, plugin.name) }).catch((error: unknown) => {
184
+ const active = await createMod(definition, { ...runtime, router: activeRouter, dataFolder: dataFolder(plugin.store, plugin.name), checksPermissions }).catch((error: unknown) => {
184
185
  fail(messageOf(error), 'Fix it, then run /reload-plugins.')
185
186
  throw error
186
187
  })
@@ -447,6 +448,10 @@ async function createMod<State extends object>(definition: ModDefinition<State>,
447
448
  }
448
449
  await failsAs('its state did not load', () => modState.load())
449
450
  await failsAs('its setup function threw', () => definition.setup(mod))
451
+ const permissionHooks = permissionEvents.filter((event) => router.has(event))
452
+ if (permissionHooks.length > 0 && !runtime.checksPermissions()) {
453
+ throw new Error(`it decides permissions on ${listed(permissionHooks)}, so hooks/register.ts must call registerPermissionCheck(addHook) after registerMod`)
454
+ }
450
455
  await failsAs('its open panes did not load', () => area.restorePanes())
451
456
  if (hookEvents.length > 0) added.push(`${hookEvents.length === 1 ? 'a hook' : 'hooks'} on ${listed(hookEvents)}`)
452
457
  return { mod, added }
@@ -7,6 +7,7 @@ export type RoutedHook<N extends RoutedEvent> = (e: Frozen<Args<N>>, next: Route
7
7
 
8
8
  export type Router = {
9
9
  add<N extends RoutedEvent>(event: N, hook: RoutedHook<N>): void
10
+ has(event: RoutedEvent): boolean
10
11
  dispatch<N extends RoutedEvent>(event: N, e: Frozen<Args<N>>, next: RouterNext<N>): Promise<EventResult<N>>
11
12
  }
12
13
 
@@ -21,6 +22,7 @@ export function createRouter(): Router {
21
22
  chain.push(hook as unknown as ErasedHook)
22
23
  hooks.set(event, chain)
23
24
  },
25
+ has: (event) => hooks.has(event),
24
26
  dispatch<N extends RoutedEvent>(event: N, e: Frozen<Args<N>>, next: RouterNext<N>): Promise<EventResult<N>> {
25
27
  const chain = hooks.get(event)
26
28
  const call = async (index: number, current: unknown): Promise<unknown> => {
@@ -34,6 +34,11 @@ export function toolCalls(claude: Claude, router: Router): ToolCalls {
34
34
  return next(e)
35
35
  })
36
36
  router.add('tool.call', async (e, next) => {
37
+ cwds.set(e.tool_use_id, await claude.session.cwd())
38
+ for (const oldest of cwds.keys()) {
39
+ if (cwds.size <= keptCalls) break
40
+ cwds.delete(oldest)
41
+ }
37
42
  if (e.agentId === undefined) return next(e)
38
43
  agentIds.set(e.tool_use_id, e.agentId)
39
44
  try {
@@ -42,14 +47,6 @@ export function toolCalls(claude: Claude, router: Router): ToolCalls {
42
47
  agentIds.delete(e.tool_use_id)
43
48
  }
44
49
  })
45
- router.add('classic.PreToolUse', async (e, next) => {
46
- cwds.set(e.tool_use_id, await claude.session.cwd())
47
- for (const oldest of cwds.keys()) {
48
- if (cwds.size <= keptCalls) break
49
- cwds.delete(oldest)
50
- }
51
- return next(e)
52
- })
53
50
  for (const event of ['classic.PostToolUse', 'classic.PostToolUseFailure'] as const) {
54
51
  router.add(event, async (e, next) => {
55
52
  try {
package/vendor.js CHANGED
@@ -179,9 +179,7 @@ var require_utils = __commonJS((exports) => {
179
179
  const platform = navigator.platform.toLowerCase();
180
180
  return platform === "win32" || platform === "windows";
181
181
  }
182
- if (typeof process !== "undefined" && process.platform) {
183
- return process.platform === "win32";
184
- }
182
+ if (false) {}
185
183
  return false;
186
184
  };
187
185
  exports.removeBackslashes = (str) => {
package/connect.d.ts DELETED
@@ -1,4 +0,0 @@
1
- import type { On } from 'claude-code';
2
- import type { ModDefinition } from './mod.js';
3
- export declare function connect<State extends object>(on: On, definition: ModDefinition<State>): void;
4
- //# sourceMappingURL=connect.d.ts.map
package/connect.d.ts.map DELETED
@@ -1 +0,0 @@
1
- {"version":3,"file":"connect.d.ts","sourceRoot":"","sources":["src/connect.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAoD,EAAE,EAAE,MAAM,aAAa,CAAA;AACvF,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,UAAU,CAAA;AAwE7C,wBAAgB,OAAO,CAAC,KAAK,SAAS,MAAM,EAAE,EAAE,EAAE,EAAE,EAAE,UAAU,EAAE,aAAa,CAAC,KAAK,CAAC,GAAG,IAAI,CA8B5F"}