@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 +2 -2
- package/docs/hooks.md +2 -0
- package/docs/index.md +2 -2
- package/docs/jobs.md +3 -2
- package/docs/mod.md +23 -8
- package/package.json +2 -2
- package/register.d.ts +5 -0
- package/register.d.ts.map +1 -0
- package/{connect.js → register.js} +35 -30
- package/runtime/hooks.d.ts +1 -0
- package/runtime/hooks.d.ts.map +1 -1
- package/runtime/hooks.js +1 -0
- package/runtime/lifecycle.d.ts +1 -1
- package/runtime/lifecycle.d.ts.map +1 -1
- package/runtime/lifecycle.js +8 -4
- package/runtime/router.d.ts +1 -0
- package/runtime/router.d.ts.map +1 -1
- package/runtime/router.js +1 -0
- package/runtime/tool-calls.d.ts.map +1 -1
- package/runtime/tool-calls.js +6 -9
- package/src/{connect.ts → register.ts} +37 -30
- package/src/runtime/hooks.ts +2 -0
- package/src/runtime/lifecycle.ts +9 -4
- package/src/runtime/router.ts +2 -0
- package/src/runtime/tool-calls.ts +5 -8
- package/vendor.js +1 -3
- package/connect.d.ts +0 -4
- package/connect.d.ts.map +0 -1
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
|
|
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
|
-
| `
|
|
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
|
|
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. `
|
|
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
|
-
##
|
|
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 `
|
|
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 {
|
|
64
|
+
import { registerMod } from '../node_modules/@cmodjs/core/register.js'
|
|
64
65
|
import { greeter } from '../src/mod.js'
|
|
65
66
|
|
|
66
|
-
export function register(
|
|
67
|
-
|
|
67
|
+
export function register(addHook: On): void {
|
|
68
|
+
registerMod(addHook, greeter)
|
|
68
69
|
}
|
|
69
70
|
```
|
|
70
71
|
|
|
71
72
|
```ts
|
|
72
|
-
|
|
73
|
+
registerMod<State>(addHook: On, definition: ModDefinition<State>): void
|
|
74
|
+
registerPermissionCheck(addHook: On): void
|
|
73
75
|
```
|
|
74
76
|
|
|
75
|
-
`
|
|
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.
|
|
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
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|
}
|
package/runtime/hooks.d.ts
CHANGED
|
@@ -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'];
|
package/runtime/hooks.d.ts.map
CHANGED
|
@@ -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;
|
|
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'],
|
package/runtime/lifecycle.d.ts
CHANGED
|
@@ -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
|
|
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,
|
|
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"}
|
package/runtime/lifecycle.js
CHANGED
|
@@ -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.
|
|
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)}`);
|
package/runtime/router.d.ts
CHANGED
|
@@ -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;
|
package/runtime/router.d.ts.map
CHANGED
|
@@ -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,
|
|
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
|
@@ -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,
|
|
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"}
|
package/runtime/tool-calls.js
CHANGED
|
@@ -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
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
}
|
package/src/runtime/hooks.ts
CHANGED
|
@@ -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]
|
package/src/runtime/lifecycle.ts
CHANGED
|
@@ -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
|
|
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.
|
|
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 }
|
package/src/runtime/router.ts
CHANGED
|
@@ -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 (
|
|
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
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"}
|