@cmodjs/core 0.1.6 → 0.1.8

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/state.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # State
2
2
 
3
- `mod.state` holds the mod's values. Claude Mod Manager (CMod) saves them, redraws the mod's panes and slot renders when one changes, and lets a person change the starting values without touching the mod's code.
3
+ `mod.state` holds the mod's values. Claude Mod Manager (cmod) saves them, redraws the mod's panes and slot renders when one changes, and lets a person change the starting values without touching the mod's code.
4
4
 
5
5
  ## Declare the state
6
6
 
@@ -13,7 +13,7 @@
13
13
  | `project` | This project, in every conversation. | Yes |
14
14
  | `global` | Every project. | Yes |
15
15
 
16
- CMod keeps each `session` value for the 20 sessions that wrote it last, and each `project` value for the 20 projects that wrote it last. A session or project past those 20 starts that value over from its starting value.
16
+ cmod keeps each `session` value for the 20 sessions that wrote it last, and each `project` value for the 20 projects that wrote it last. A session or project past those 20 starts that value over from its starting value.
17
17
 
18
18
  Put values the mod works out again on every prompt, such as a git status, in `memory`.
19
19
 
@@ -56,7 +56,7 @@ A group other than `memory`, `session`, `project`, and `global` throws `<mod>: s
56
56
 
57
57
  ## Change a value
58
58
 
59
- Assign to a key. CMod saves the new value and redraws the mod.
59
+ Assign to a key. cmod saves the new value and redraws the mod.
60
60
 
61
61
  - A key the state does not declare throws `<mod>: mod.state.<group>.<key> is not declared. Add it to state.<group> in defineMod with its starting value.`
62
62
  - An object or an array in the state is frozen. Changing it in place throws `<mod>: mod.state.<path> cannot change in place.` Assign a new value instead:
@@ -75,8 +75,8 @@ export const reads = defineMod({
75
75
  })
76
76
  ```
77
77
 
78
- - Keep only JSON data in a saved group. A value CMod cannot save logs `<mod> could not keep state.<group>.<key>: <error>. Keep only JSON data in mod.state.`
79
- - CMod saves values one at a time in the order they were assigned.
78
+ - Keep only JSON data in a saved group. A value cmod cannot save logs `<mod> could not keep state.<group>.<key>: <error>. Keep only JSON data in mod.state.`
79
+ - cmod saves values one at a time in the order they were assigned.
80
80
 
81
81
  ## Where the values come from
82
82
 
@@ -87,7 +87,7 @@ When the mod starts, each value is, in order of precedence:
87
87
  3. The value in the person's own `state.json`.
88
88
  4. The starting value in `defineMod`.
89
89
 
90
- After a `/cd` to another project, CMod loads that project's `project` values and reads its `state.json` again.
90
+ After a `/cd` to another project, cmod loads that project's `project` values and reads its `state.json` again.
91
91
 
92
92
  ## Let a person change the starting values
93
93
 
@@ -111,7 +111,7 @@ A `state.json` in either folder lists only the values to change:
111
111
  - A project `state.json` cannot set `global` values, because one repository cannot change a value for every project.
112
112
  - A file cannot set `memory` values.
113
113
 
114
- CMod ignores each entry it cannot use and logs one line naming the file, the entry, and the fix:
114
+ cmod ignores each entry it cannot use and logs one line naming the file, the entry, and the fix:
115
115
 
116
116
  - a file that is not JSON, or not a JSON object
117
117
  - a group the mod does not declare
@@ -131,7 +131,7 @@ A person replaces the text of a Skill the mod ships by putting a file at the sam
131
131
 
132
132
  - Claude reads that file in place of the mod's from the next time the Skill loads, and updates of the mod keep it.
133
133
  - The project file wins over the person's file.
134
- - CMod drops the file's frontmatter, so the mod's own name and description stay.
134
+ - cmod drops the file's frontmatter, so the mod's own name and description stay.
135
135
  - Only a Skill the mod ships in `skills/<skill>/` is replaced, and only when it loads as `<mod>:<skill>`.
136
136
  - Deleting the file brings the mod's text back.
137
137
 
package/docs/ui.md CHANGED
@@ -10,7 +10,7 @@ mod.ui.progress<T>(title: string, task: (report: (step: ProgressStep) => void) =
10
10
  mod.ui.ask(question: string, options?: readonly string[] | AskOptions): Promise<string>
11
11
  ```
12
12
 
13
- Claude Mod Manager (CMod) draws the mod again whenever a value in `mod.state` changes. A render reads `mod.state` and returns what to show. It never draws by hand.
13
+ Claude Mod Manager (cmod) draws the mod again whenever a value in `mod.state` changes. A render reads `mod.state` and returns what to show. It never draws by hand.
14
14
 
15
15
  ## Elements
16
16
 
@@ -46,7 +46,7 @@ export function Count({ label, count }: { readonly label: string; readonly count
46
46
 
47
47
  An element called outside a render throws `<Element> was called outside a render. Use it inside a pane's render or a slot's component.` So build elements inside a pane's `render` or a slot's component, never at module load or in a hook.
48
48
 
49
- `drawWith(table, draw, markdown?)` is the function CMod draws a render with. A mod never calls it.
49
+ `drawWith(table, draw, markdown?)` is the function cmod draws a render with. A mod never calls it.
50
50
 
51
51
  ## Panes
52
52
 
@@ -68,7 +68,7 @@ type Pane<State> = {
68
68
 
69
69
  - `id` is 1 to 64 letters, digits, `_`, or `-`. Another id throws `definePane: "<id>" is not a pane id.`
70
70
  - `title` labels the pane's tab while more than one pane is open.
71
- - `columns` and `rows` ask for a size. A number must be a whole number above 0, or `definePane` throws. A function reads the state and returns the size, or `undefined` for Claude Code's default. When the state changes the size, CMod resizes an open pane. A function that returns a bad size keeps an open pane at its size and logs the pane's title to the debug log. `open`, and `toggle` on a closed pane, reject with that size instead.
71
+ - `columns` and `rows` ask for a size. A number must be a whole number above 0, or `definePane` throws. A function reads the state and returns the size, or `undefined` for Claude Code's default. When the state changes the size, cmod resizes an open pane. A function that returns a bad size keeps an open pane at its size and logs the pane's title to the debug log. `open`, and `toggle` on a closed pane, reject with that size instead.
72
72
  - `render` draws the pane's body. `props` holds `title`, `isFocused`, `bodyColumns`, `placement` (`'dock'` or `'inline'`), `scroll`, the body's window over a taller drawing, and `view`, which transcript is on screen beside the pane.
73
73
 
74
74
  Put each pane in its own file in `src/panes/`.
@@ -106,13 +106,13 @@ type PaneHandle = {
106
106
  ```
107
107
 
108
108
  - `open` asks Claude Code to show the pane. `isOpen` turns true once Claude Code places it. Claude Code may hold a pane back, and then `isOpen` stays false.
109
- - `mod.ui.pane` only adds the pane. CMod opens it only when the mod calls `open` or `toggle`, and Claude Code shows a pane only once the plugin has opened it. So give the person a way in, such as a slash command whose `reply` calls `pane.toggle()`.
109
+ - `mod.ui.pane` only adds the pane. cmod opens it only when the mod calls `open` or `toggle`, and Claude Code shows a pane only once the plugin has opened it. So give the person a way in, such as a slash command whose `reply` calls `pane.toggle()`.
110
110
  - A pane opened from what the person did, such as a slash command they typed or a `Button` they pressed, shows at any terminal width. A pane opened from anything else, such as `setup`, a timer, or `SessionStart`, shows only on a terminal 144 columns wide, or 110 for a pane the person opened before. Below that it waits undrawn until the person opens it or the terminal widens.
111
111
  - `close` closes it. The person closes a pane too, and `isOpen` follows.
112
112
  - `toggle` closes an open pane and opens a closed one.
113
113
  - When the mod starts, `isOpen` is true for each of its panes Claude Code already shows.
114
114
  - Adding two panes with one id throws `<mod>: the pane "<id>" is already added. Give each pane its own id.`
115
- - The line CMod logs when the mod first starts names the pane by its `title`, as in `the Notes pane`.
115
+ - The line cmod logs when the mod first starts names the pane by its `title`, as in `the Notes pane`.
116
116
 
117
117
  ```tsx
118
118
  import { defineMod } from '../node_modules/@cmodjs/core/mod.js'
@@ -157,7 +157,7 @@ export const notes = defineMod({
157
157
  - `Default` draws what Claude Code, and the renders of other mods, would draw. Pass it props to change what it draws, such as `<Default hint="…" />`. Return `<Default />` to change nothing.
158
158
  - A render adds to what it wraps. In `AbovePrompt`, drawing `<Default />` and a line under it keeps the lines of Claude Code and other mods.
159
159
  - A render must draw the same for the same props. One that calls `Default` a different number of times on a second draw falls back to Claude Code's drawing.
160
- - A render that throws falls back to Claude Code's drawing. CMod logs `<mod>: the <Slot> render threw, so Claude Code draws its own: <error>` once.
160
+ - A render that throws falls back to Claude Code's drawing. cmod logs `<mod>: the <Slot> render threw, so Claude Code draws its own: <error>` once.
161
161
  - A mod renders each slot once. A second `mod.ui.render` of one slot throws `<mod>: a render of <Slot> is already added. Render each slot once.` For a markdown slot the error names it as `a render of markdown <Kind>`, such as `a render of markdown CodeBlock`.
162
162
  - `slots.ToolUse` changes a call's own row. A group of calls shows the `slots.ToolGroup` row, which Claude Code builds from the stored message, so a mod that hides a tool's input also sets `isExpanded` on `slots.ToolGroup` to unfold the group into `ToolUse` rows.
163
163
  - No slot covers the permission dialog.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cmodjs/core",
3
- "version": "0.1.6",
3
+ "version": "0.1.8",
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/records.js CHANGED
@@ -6,7 +6,7 @@ export function storeFolder(env) {
6
6
  if (dataHome)
7
7
  return `${dataHome}/cmod`;
8
8
  if (!home)
9
- throw new Error('Neither XDG_DATA_HOME nor HOME is set, so the CMod store has no folder. Set HOME.');
9
+ throw new Error('Neither XDG_DATA_HOME nor HOME is set, so the cmod store has no folder. Set HOME.');
10
10
  return `${home}/.local/share/cmod`;
11
11
  }
12
12
  export function recordPath(store, name) {
@@ -56,7 +56,7 @@ function parseRecord(text, path) {
56
56
  throw new Error(`${path} is not JSON (${messageOf(error)}). ${fix}`);
57
57
  }
58
58
  if (!isObject(value))
59
- throw new Error(`${path} is not a CMod install record. ${fix}`);
59
+ throw new Error(`${path} is not a cmod install record. ${fix}`);
60
60
  for (const key of ['name', 'version', 'root', 'installedAt', 'scriptsSha256']) {
61
61
  if (typeof value[key] !== 'string')
62
62
  throw new Error(`${path} has no "${key}" text. ${fix}`);
@@ -102,7 +102,7 @@ function readCommand(steps, key) {
102
102
  throw new Error(`package.json "cmod.${key}" holds a line break or a tab. Write one command, such as "./setup/${key}.sh".`);
103
103
  const atRoot = scriptPaths(command).find((path) => folderOf(path) === '.');
104
104
  if (atRoot !== undefined)
105
- throw new Error(`package.json "cmod.${key}" names ${atRoot}, a script at the plugin root. Move it into a folder, such as ./setup/${key}.sh: CMod asks consent for the whole folder of each script.`);
105
+ throw new Error(`package.json "cmod.${key}" names ${atRoot}, a script at the plugin root. Move it into a folder, such as ./setup/${key}.sh: cmod asks consent for the whole folder of each script.`);
106
106
  return { [key]: command };
107
107
  }
108
108
  export function scriptPaths(command) {
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 = () => {
@@ -127,7 +127,7 @@ export function createLifecycle(definition) {
127
127
  };
128
128
  const record = () => {
129
129
  recording ??= recordThroughCmod()
130
- .catch((error) => claude().ui.log(`${definition.name} has no CMod record yet: ${messageOf(error)}`, { to: 'debug' }))
130
+ .catch((error) => claude().ui.log(`${definition.name} has no cmod record yet: ${messageOf(error)}`, { to: 'debug' }))
131
131
  .finally(() => {
132
132
  recording = undefined;
133
133
  });
@@ -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
  });
@@ -186,7 +186,7 @@ export function createLifecycle(definition) {
186
186
  return bootstrap(plugin.root);
187
187
  phase = 'installing';
188
188
  if ((await cmodVersion(claude())) === undefined)
189
- return waitForCMod();
189
+ return waitForcmod();
190
190
  endLine();
191
191
  const progress = showLine();
192
192
  let outcome;
@@ -205,9 +205,9 @@ export function createLifecycle(definition) {
205
205
  const reason = outcome?.kind === 'failed' ? outcome.message : `cmod setup ${formatExit(code, lastError)}`;
206
206
  fail(reason, `Fix the cause, then run: cmod install ${definition.name}`);
207
207
  };
208
- const waitForCMod = () => {
208
+ const waitForcmod = () => {
209
209
  phase = 'waiting';
210
- showLine().wait('Waiting for CMod');
210
+ showLine().wait('Waiting for cmod');
211
211
  let check;
212
212
  const stop = () => {
213
213
  timer.cancel();
@@ -227,7 +227,7 @@ export function createLifecycle(definition) {
227
227
  report(error);
228
228
  });
229
229
  });
230
- const longWait = claude().clock.after(cmodWaitMs, () => showLine().wait("Still waiting for CMod to download cmod. See CMod's own line."));
230
+ const longWait = claude().clock.after(cmodWaitMs, () => showLine().wait("Still waiting for cmod to download cmod. See cmod's own line."));
231
231
  };
232
232
  const bootstrap = async (root) => {
233
233
  const progress = showLine();
@@ -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 {
package/src/records.ts CHANGED
@@ -28,7 +28,7 @@ export function storeFolder(env: Record<string, string | undefined>): string {
28
28
  const dataHome = env['XDG_DATA_HOME']
29
29
  const home = env['HOME']
30
30
  if (dataHome) return `${dataHome}/cmod`
31
- if (!home) throw new Error('Neither XDG_DATA_HOME nor HOME is set, so the CMod store has no folder. Set HOME.')
31
+ if (!home) throw new Error('Neither XDG_DATA_HOME nor HOME is set, so the cmod store has no folder. Set HOME.')
32
32
  return `${home}/.local/share/cmod`
33
33
  }
34
34
 
@@ -81,7 +81,7 @@ function parseRecord(text: string, path: string): InstallRecord {
81
81
  } catch (error) {
82
82
  throw new Error(`${path} is not JSON (${messageOf(error)}). ${fix}`)
83
83
  }
84
- if (!isObject(value)) throw new Error(`${path} is not a CMod install record. ${fix}`)
84
+ if (!isObject(value)) throw new Error(`${path} is not a cmod install record. ${fix}`)
85
85
  for (const key of ['name', 'version', 'root', 'installedAt', 'scriptsSha256'] as const) {
86
86
  if (typeof value[key] !== 'string') throw new Error(`${path} has no "${key}" text. ${fix}`)
87
87
  }
@@ -117,7 +117,7 @@ function readCommand(steps: Record<string, unknown>, key: 'install' | 'uninstall
117
117
  if (typeof command !== 'string' || command.trim() === '') throw new Error(`package.json "cmod.${key}" must be a command, such as "./setup/${key}.sh".`)
118
118
  if (/[\n\t]/.test(command)) throw new Error(`package.json "cmod.${key}" holds a line break or a tab. Write one command, such as "./setup/${key}.sh".`)
119
119
  const atRoot = scriptPaths(command).find((path) => folderOf(path) === '.')
120
- if (atRoot !== undefined) throw new Error(`package.json "cmod.${key}" names ${atRoot}, a script at the plugin root. Move it into a folder, such as ./setup/${key}.sh: CMod asks consent for the whole folder of each script.`)
120
+ if (atRoot !== undefined) throw new Error(`package.json "cmod.${key}" names ${atRoot}, a script at the plugin root. Move it into a folder, such as ./setup/${key}.sh: cmod asks consent for the whole folder of each script.`)
121
121
  return { [key]: command }
122
122
  }
123
123
 
@@ -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]