@cmodjs/core 2.0.0 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/docs/commands.md +4 -4
- package/docs/dependencies.md +1 -1
- package/docs/jobs.md +1 -1
- package/docs/mod.md +2 -3
- package/docs/permissions.md +1 -1
- package/docs/session.md +3 -1
- package/docs/testing.md +2 -1
- package/jobs/permissions.d.ts.map +1 -1
- package/jobs/permissions.js +4 -1
- package/package.json +1 -1
- package/records.d.ts +1 -1
- package/records.js +1 -1
- package/register.d.ts.map +1 -1
- package/register.js +5 -4
- package/runtime/before.d.ts +8 -4
- package/runtime/before.d.ts.map +1 -1
- package/runtime/before.js +63 -39
- package/runtime/conversations.d.ts.map +1 -1
- package/runtime/conversations.js +38 -13
- package/runtime/engine.d.ts +1 -0
- package/runtime/engine.d.ts.map +1 -1
- package/runtime/hooks.d.ts +9 -23
- package/runtime/hooks.d.ts.map +1 -1
- package/runtime/hooks.js +4 -59
- package/runtime/lifecycle.d.ts +2 -1
- package/runtime/lifecycle.d.ts.map +1 -1
- package/runtime/lifecycle.js +36 -23
- package/runtime/permission-check.d.ts +15 -0
- package/runtime/permission-check.d.ts.map +1 -0
- package/runtime/permission-check.js +37 -0
- package/runtime/router.d.ts +0 -1
- package/runtime/router.d.ts.map +1 -1
- package/runtime/router.js +0 -1
- package/runtime/sessions.d.ts +13 -8
- package/runtime/sessions.d.ts.map +1 -1
- package/runtime/sessions.js +44 -80
- package/runtime/state.d.ts.map +1 -1
- package/runtime/state.js +14 -11
- package/session/context/turn/message/message.d.ts +1 -0
- package/session/context/turn/message/message.d.ts.map +1 -1
- package/session/context/turn/message/message.js +5 -2
- package/src/jobs/permissions.ts +3 -1
- package/src/records.ts +1 -1
- package/src/register.ts +5 -4
- package/src/runtime/before.ts +66 -42
- package/src/runtime/conversations.ts +35 -13
- package/src/runtime/engine.ts +1 -0
- package/src/runtime/hooks.ts +13 -86
- package/src/runtime/lifecycle.ts +51 -37
- package/src/runtime/permission-check.ts +47 -0
- package/src/runtime/router.ts +0 -2
- package/src/runtime/sessions.ts +55 -84
- package/src/runtime/state.ts +15 -8
- package/src/session/context/turn/message/message.ts +6 -2
- package/src/testing.ts +17 -10
- package/testing.d.ts +7 -2
- package/testing.d.ts.map +1 -1
- package/testing.js +13 -7
package/docs/commands.md
CHANGED
|
@@ -48,7 +48,7 @@ Runs every check this machine can run on the mod at `path` (default: the current
|
|
|
48
48
|
|
|
49
49
|
1. Installs its packages.
|
|
50
50
|
2. Checks its layout.
|
|
51
|
-
3. Checks that each step runs a script, and prints the permissions the person grants at install and can grant later, as the person reads them ([permissions.md](permissions.md)). It
|
|
51
|
+
3. Checks that each step runs a script, and prints the permissions the person grants at install and can grant later, as the person reads them ([permissions.md](permissions.md)). It adds `"approve": true` when the mod answers the permission check, with a `beforePermissionCheck` or `beforePermissionRequest` listener or the `permissions` job, and `hooks/register.ts` calls `registerPermissionCheck`. It fails a mod whose `src/` hears permissions while `register.ts` does not call `registerPermissionCheck`.
|
|
52
52
|
4. Writes the options `defineMod` declares into `.claude-plugin/plugin.json` `userConfig`, keeping the file's own formatting, so Claude Code shows them in `/config` ([options.md](options.md)). It loads `hooks/register.ts` to read them, without starting the mod.
|
|
53
53
|
5. Checks its imports.
|
|
54
54
|
6. Looks for prebuilt binaries outside `cli/`.
|
|
@@ -98,7 +98,7 @@ Releases the mod at `path` (default: the current folder) at the version in its `
|
|
|
98
98
|
4. Commits the release as the `release` branch, and builds the release archive from that commit.
|
|
99
99
|
5. Builds the program `cli/` declares, and writes `SHA256SUMS` for every file of the release.
|
|
100
100
|
6. Writes `.claude-plugin/marketplace.json`, listing the archive and the cmod plugin. The cmod entry points at the `release` branch of `heyJordanParker/cmod`, so the mod's `"cmod"` dependency resolves in this marketplace and follows each cmod release with no new publish of the mod.
|
|
101
|
-
7. Commits that file, tags `v<version>`,
|
|
101
|
+
7. Commits that file, tags `v<version>`, tags the release commit `<name>--v<version>`, which Claude Code installs a version range from when a marketplace lists the mod from its git repository, pushes both tags and the `release` branch, creates the GitHub release with every file, then pushes the branch that holds `marketplace.json`. So Claude Code never updates anyone to a release whose files are not up yet. Its notes are the version's section of the committed `CHANGELOG.md`, above GitHub's compare link ([mod.md](mod.md#release-notes)). `--dry-run` prints them.
|
|
102
102
|
8. Prints the link to paste as the Repository when you submit the mod at [claude.ai/directory/manage](https://claude.ai/directory/manage), such as `https://github.com/owner/greeter/tree/release`. The portal reads the branch from the link, so the directory follows `release`.
|
|
103
103
|
|
|
104
104
|
The release keeps `package.json`, so Claude Code still installs the mod's packages for its install and uninstall steps. It keeps `bun.lock` too, unless a `"files"` list leaves it out. `main` keeps the source only.
|
|
@@ -114,7 +114,7 @@ It needs, and refuses to start without:
|
|
|
114
114
|
- a `package.json` with no dependency on a `file:` or `link:` path, which no user has
|
|
115
115
|
- a `"files"` list, when `package.json` has one, whose every path matches a committed file
|
|
116
116
|
- a `"program"` in the `cmod` key of `package.json` that names the program `cli/` declares, when `cli/` declares one
|
|
117
|
-
- for a real publish, a working tree with no uncommitted changes,
|
|
117
|
+
- for a real publish, a working tree with no uncommitted changes, no `v<version>` or `<name>--v<version>` tag yet, a `git push` to `origin` that succeeds, and the `gh` command, which creates the GitHub release
|
|
118
118
|
|
|
119
119
|
`--dry-run` builds everything into a temporary folder, and pushes nothing. It writes `marketplace.json` there too, so the working tree stays as it was. It allows uncommitted changes and an existing tag, and builds from the last commit. It prints the folder that holds the release, so you can read what the directory will read.
|
|
120
120
|
|
|
@@ -259,7 +259,7 @@ Changes Claude Code settings for a set-up mod, as its `settings.user.update`, `s
|
|
|
259
259
|
cmod rules [--project <folder>] [--json]
|
|
260
260
|
```
|
|
261
261
|
|
|
262
|
-
Links the `rules/` folder of each mod that is on and has the `prompt` permission into Claude Code's rules folder, and removes the link of each that is off, lacks the permission, or is removed ([mod.md](mod.md#rules)). A mod the person installed links at `<config>/rules/<mod>`, and one turned on only for `--project`, the current folder when it is left out, links at `<project>/.claude/rules/<mod>`. It prints the rules it linked. With `--json` it prints `{ linked, placed, removed }`, where `placed` and `removed` name the mods whose link this run made or took away. `cmod install`, `update`, `remove`, `enable`, `disable`, `permission`, `setup`, `teardown`, `link`, and `unlink` run it after they succeed
|
|
262
|
+
Links the `rules/` folder of each mod that is on and has the `prompt` permission into Claude Code's rules folder, and removes the link of each that is off, lacks the permission, or is removed ([mod.md](mod.md#rules)). A mod the person installed links at `<config>/rules/<mod>`, and one turned on only for `--project`, the current folder when it is left out, links at `<project>/.claude/rules/<mod>`. It prints the rules it linked. With `--json` it prints `{ linked, placed, removed, watched }`, where `placed` and `removed` name the mods whose link this run made or took away, and `watched` lists the files and folders whose change can change the next run. `cmod install`, `update`, `remove`, `enable`, `disable`, `permission`, `setup`, `teardown`, `link`, and `unlink` run it after they succeed. The cmod plugin runs it when a session starts and before a prompt, each time only when a `watched` path changed since its last run.
|
|
263
263
|
|
|
264
264
|
### cmod option
|
|
265
265
|
|
package/docs/dependencies.md
CHANGED
|
@@ -58,7 +58,7 @@ A provider that changes or removes a method breaks every mod that calls it, and
|
|
|
58
58
|
"dependencies": [{ "name": "cmod", "version": "^1.0.0" }, { "name": "tracer", "version": "^1.0" }]
|
|
59
59
|
```
|
|
60
60
|
|
|
61
|
-
Claude Code checks the range against the `version`
|
|
61
|
+
Claude Code checks the range against the provider's `version` when it loads your mod. Where a marketplace lists the provider from its git repository, as every mod's marketplace lists cmod, Claude Code also installs it at the highest `<name>--v<version>` tag inside the range, which `cmod publish` pushes for every release ([Claude Code's plugin dependencies](https://code.claude.com/docs/en/plugins/dependencies)). cmod is a provider too: a mod built on `@cmodjs/core` 1.0 calls methods only the 1.0 cmod plugin has, so `cmod new` writes the `^1.0.0` range.
|
|
62
62
|
|
|
63
63
|
Claude Code then lays tracer's types file beside the mod's own types, so the call is typed:
|
|
64
64
|
|
package/docs/jobs.md
CHANGED
|
@@ -189,7 +189,7 @@ type Rule<State> = Target & {
|
|
|
189
189
|
- When cmod cannot read the call or the session, it denies the call with the reason.
|
|
190
190
|
- 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. A `!` pattern leaves nothing out of such a line, so the rule errs toward matching.
|
|
191
191
|
- `permissions({})` throws `permissions: give it a deny or an ask rule, or remove it from setup.`
|
|
192
|
-
- A mod that uses `permissions` calls `registerPermissionCheck(addHook)` after `registerMod` in `hooks/register.ts` ([mod.md](mod.md)). Without it
|
|
192
|
+
- A mod that uses `permissions` calls `registerPermissionCheck(addHook)` after `registerMod` in `hooks/register.ts` ([mod.md](mod.md)). Without it `mod.use(permissions(…))` throws, so a mod that uses it in `setup` does not start.
|
|
193
193
|
|
|
194
194
|
`when` gets the call, typed by the target's key. `jobs/permissions.js` exports each type, and `Call` is any of them:
|
|
195
195
|
|
package/docs/mod.md
CHANGED
|
@@ -59,7 +59,6 @@ When a step fails, the mod does not start. An error toast above the prompt names
|
|
|
59
59
|
- `its setup function threw: <error>` when `setup` throws or rejects.
|
|
60
60
|
- `its open panes did not load: <error>` when Claude Code cannot list the open panes.
|
|
61
61
|
- `the details of its session did not load: <error>` when Claude Code cannot name the session's id, model, or start time.
|
|
62
|
-
- `it decides permissions on <events>, so hooks/register.ts must call registerPermissionCheck(addHook) after registerMod` when `setup` adds a `beforePermissionCheck` or `beforePermissionRequest` listener, or a job that decides permissions, and `register.ts` does not register the permission check.
|
|
63
62
|
|
|
64
63
|
Each of these has `Retry`, which starts the mod again, and `Report` and `Hide` ([ui.md](ui.md#toasts)).
|
|
65
64
|
|
|
@@ -125,7 +124,7 @@ registeredMod(): ModDefinition | undefined
|
|
|
125
124
|
|
|
126
125
|
`registerMod` registers one handler per Claude Code event the mod can use, and routes each event to the mod. `options` is what Claude Code passes `register`: the values of the mod's options ([options.md](options.md)). `registeredMod` returns the definition `registerMod` got, for `cmod check` to read the mod's options without starting it. `registerMod` leaves out Claude Code's permission check, the events `tool.check` and `PermissionRequest`.
|
|
127
126
|
|
|
128
|
-
A mod that
|
|
127
|
+
A mod that hears permissions calls `registerPermissionCheck` after `registerMod`. That covers `sessions.current.onPermissionCheck`, `beforePermissionCheck`, `onPermissionRequest`, `beforePermissionRequest`, and the `permissions` job:
|
|
129
128
|
|
|
130
129
|
```ts
|
|
131
130
|
export function register(addHook: On, options: PluginOptions): void {
|
|
@@ -134,7 +133,7 @@ export function register(addHook: On, options: PluginOptions): void {
|
|
|
134
133
|
}
|
|
135
134
|
```
|
|
136
135
|
|
|
137
|
-
|
|
136
|
+
A mod that answers, with a `before` method or the `permissions` job, also declares `"approve": true` in its `package.json` `"cmod".permissions`, so the person grants it at install. `cmod check` adds the line when it is missing, and without the grant cmod drops every allow or ask the mod answers, so Claude Code decides those calls itself ([permissions.md](permissions.md)). A mod without `registerPermissionCheck` has no part in Claude Code's permission check, so the plugin directory reads it as one that never answers a permission. Without it, each of those methods and `mod.use(permissions(…))` throws `<mod>: <method> hears Claude Code's permission check, so hooks/register.ts must call registerPermissionCheck(addHook) after registerMod.` where it is called, so a mod that calls one in `setup` does not start. `cmod check` fails a mod whose `src/` uses one while `register.ts` does not call `registerPermissionCheck`.
|
|
138
137
|
|
|
139
138
|
Keep `register.ts` this small. Claude Code checks a hooks module before it loads it, and refuses some shapes, such as a `$` passed to a function in another module. The plugin directory reads the bundled module too, and flags it when the name of `register`'s first parameter is declared anywhere else in the bundle. `@cmodjs/core` declares `on` as a key, so name that parameter `addHook`. Declare `register` as a function, as here: `cmod publish` bundles the hooks module into one file, and Claude Code refuses a bundled `register` that is not one.
|
|
140
139
|
|
package/docs/permissions.md
CHANGED
|
@@ -29,7 +29,7 @@ They sit in the `package.json` `cmod` key, beside `install` and `keys`, because
|
|
|
29
29
|
|
|
30
30
|
Reading files, drawing panes and renders, toasts, slash commands, tools of the mod's own, listeners on the mod's own session, and a `refuse` answer need no permission. Writing a Claude Code settings file with `fs.write` is refused with every permission, and names the `settings` call to use instead ([mod.md](mod.md#settings-claude-code-s-settings-files)).
|
|
31
31
|
|
|
32
|
-
`cmod check` adds `"approve": true` to `package.json` when `
|
|
32
|
+
`cmod check` adds `"approve": true` to `package.json` when the mod answers the permission check, with a `beforePermissionCheck` or `beforePermissionRequest` listener or the `permissions` job, and the line is missing, and says so. A mod that only listens with `onPermissionCheck` or `onPermissionRequest` needs no `approve`.
|
|
33
33
|
|
|
34
34
|
## What the person sees
|
|
35
35
|
|
package/docs/session.md
CHANGED
|
@@ -157,7 +157,7 @@ A listener that throws shows the error toast `The <method> listener failed`, suc
|
|
|
157
157
|
|
|
158
158
|
### Give a listener time
|
|
159
159
|
|
|
160
|
-
A listener gets 10 seconds. `timeoutMs` changes that, a whole number above 0 and at most `600000`. The second argument a listener gets is its `deadline`, the time it ends as `Date.now()` counts, and a `signal` that aborts then, so a slow call such as `http.fetch` can stop with it
|
|
160
|
+
A listener gets 10 seconds. `timeoutMs` changes that, a whole number above 0 and at most `600000`. The second argument a listener gets is its `deadline`, the time it ends as `Date.now()` counts, and a `signal` that aborts then, so a slow call such as `http.fetch` can stop with it. Time a listener spends waiting on a call, such as `process.run`, never counts against the 10 seconds Claude Code gives a hook's own code, so a listener that waits can use its whole `timeoutMs`:
|
|
161
161
|
|
|
162
162
|
```ts
|
|
163
163
|
sessions.current.onTurnEnd(async (turn, { signal }) => {
|
|
@@ -220,6 +220,8 @@ An answer that changes text needs the `prompt` permission, a `beforeToolCall` an
|
|
|
220
220
|
| `beforeTodoCreate`, `onTodoCreate`, `beforeTodoComplete`, `onTodoComplete` | a todo is added to or ticked on the checklist | `Todo`: `id`, `subject`, `description?`, `teammate?` | `{ refuse }` |
|
|
221
221
|
| `beforeReplyDisplay`, `onReplyDisplay` | a reply is about to show | `ReplyDisplay`: `replyId`, `index`, `isLast`, `text` | `{ text }`, what the person sees |
|
|
222
222
|
|
|
223
|
+
The four `PermissionCheck` and `PermissionRequest` methods work only in a mod whose `hooks/register.ts` calls `registerPermissionCheck` ([mod.md](mod.md#register-the-mod-with-claude-code)). Without it they throw where they are called.
|
|
224
|
+
|
|
223
225
|
Every payload also carries where it happened: `session`, `cwd`, and `agentType?` and `permissionMode?` when Claude Code reports them. A matcher keeps the payloads it matches:
|
|
224
226
|
|
|
225
227
|
```ts
|
package/docs/testing.md
CHANGED
|
@@ -93,9 +93,10 @@ Starts the mod: loads its state, runs `setup`, and adds its listeners, panes, an
|
|
|
93
93
|
|
|
94
94
|
- Each process method takes its payload without `session` and `cwd`, which `raise` fills: `raise.beforeModelChange({ model: 'haiku', previous: 'opus', source: 'command', … })`, `raise.onCwdChange({ previous: '/work/old' })`, `raise.beforeRun({ name: 'compact', args: '', from: { type: Person } })`.
|
|
95
95
|
- Each message `before` method takes a `MessageDraft` without its `type`, placed in the open turn: `raise.beforeToolCall({ name: 'Bash', input: { command: 'rm -rf build' } })` and `raise.beforePrompt({ text: 'Deploy to prod' })`. `{ task }` places it in a task.
|
|
96
|
+
- `raise.beforeToolResult({ callId, output })` passes `output` to the listener as given, so a test gives what the tool answered Claude Code: `{ stdout: 'ok', stderr: '' }` for Bash, or Edit's `{ filePath, structuredPatch }`. A `ToolResult` that `tested.add` adds holds the text Claude read, as a transcript does.
|
|
96
97
|
- `raise.beforeTaskStart({ title, agentType?, model? })` asks about a task about to start.
|
|
97
98
|
- `raise.beforeTurnEnd({ answer?, failure? })` asks about the open turn ending. It rejects when no turn is open: add a `Prompt` first.
|
|
98
|
-
- `raise.beforeContextEnd()` asks about a compaction of the open context.
|
|
99
|
+
- `raise.beforeContextEnd()` asks about a compaction of the open context. `{ task }` asks about the compaction of a task's context, a task `tested.startTask` started.
|
|
99
100
|
|
|
100
101
|
```ts
|
|
101
102
|
test('a force push is refused', async () => {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"permissions.d.ts","sourceRoot":"","sources":["../src/jobs/permissions.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,GAAG,EAAE,GAAG,EAAE,MAAM,WAAW,CAAA;
|
|
1
|
+
{"version":3,"file":"permissions.d.ts","sourceRoot":"","sources":["../src/jobs/permissions.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,GAAG,EAAE,GAAG,EAAE,MAAM,WAAW,CAAA;AAKzC,OAAO,EAAqC,KAAK,eAAe,IAAI,QAAQ,EAAE,KAAK,IAAI,IAAI,OAAO,EAAgB,MAAM,oCAAoC,CAAA;AAK5J,YAAY,EAAE,WAAW,EAAE,SAAS,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,0BAA0B,CAAA;AACtF,YAAY,EAAE,MAAM,EAAE,MAAM,+BAA+B,CAAA;AAC3D,MAAM,MAAM,IAAI,CAAC,KAAK,SAAS,MAAM,GAAG,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,IAAI,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,CAAA;AACnF,MAAM,MAAM,eAAe,CAAC,KAAK,SAAS,MAAM,GAAG,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,IAAI,QAAQ,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,CAAA;AAI/F,wBAAgB,WAAW,CAAC,KAAK,SAAS,MAAM,GAAG,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,EAAE,KAAK,EAAE,eAAe,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,KAAK,KAAK,eAAe,CAAC,KAAK,CAAC,CAAC,GAAG,GAAG,CAAC,IAAI,EAAE,KAAK,CAAC,CA+BrK"}
|
package/jobs/permissions.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { engineOf } from '../runtime/engine.js';
|
|
2
|
+
import { withoutPermissionCheck } from '../runtime/sessions.js';
|
|
2
3
|
import { inputOf } from '../utils/call-effects.js';
|
|
3
4
|
import { decidePermission, stricterVerdict } from './permissions/decide-permission.js';
|
|
4
5
|
import { targetOf } from './permissions/match-target.js';
|
|
@@ -10,7 +11,9 @@ export function permissions(rules) {
|
|
|
10
11
|
if (count === 0)
|
|
11
12
|
throw new Error('permissions: give it a deny or an ask rule, or remove it from setup.');
|
|
12
13
|
return (job) => {
|
|
13
|
-
const { router, toolCalls } = engineOf(job.mod);
|
|
14
|
+
const { router, toolCalls, hasPermissionCheck } = engineOf(job.mod);
|
|
15
|
+
if (!hasPermissionCheck())
|
|
16
|
+
throw new Error(withoutPermissionCheck(job.mod.name, 'the permissions job'));
|
|
14
17
|
const readWorkspace = workspaceReader(job);
|
|
15
18
|
const verdictOf = async (e) => {
|
|
16
19
|
let current;
|
package/package.json
CHANGED
package/records.d.ts
CHANGED
|
@@ -74,7 +74,7 @@ export declare function marketplaceOf(root: string, name: string): string | unde
|
|
|
74
74
|
export declare function updatesToTurnOn(marketplace: string | undefined, settings: unknown, known: unknown): string | undefined;
|
|
75
75
|
export declare function consentPath(store: string): string;
|
|
76
76
|
export declare function parseConsent(value: unknown, path: string): Record<string, string[]>;
|
|
77
|
-
export declare const coreVersion = "2.
|
|
77
|
+
export declare const coreVersion = "2.1.0";
|
|
78
78
|
export declare function oldestCmodFor(steps: Steps): string;
|
|
79
79
|
export declare function keyWords(keys: Readonly<Record<string, string>> | undefined): string;
|
|
80
80
|
export declare function scriptPaths(command: string): string[];
|
package/records.js
CHANGED
|
@@ -227,7 +227,7 @@ function readKeys(keys) {
|
|
|
227
227
|
}
|
|
228
228
|
return { keys: commands };
|
|
229
229
|
}
|
|
230
|
-
export const coreVersion = '2.
|
|
230
|
+
export const coreVersion = '2.1.0';
|
|
231
231
|
export function oldestCmodFor(steps) {
|
|
232
232
|
if (steps.permissions?.some((item) => item.startsWith('config:')) === true)
|
|
233
233
|
return '1.6.0';
|
package/register.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"register.d.ts","sourceRoot":"","sources":["src/register.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAoD,EAAE,EAAE,aAAa,EAAgB,MAAM,aAAa,CAAA;AACpH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,UAAU,CAAA;AAC7C,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAA;
|
|
1
|
+
{"version":3,"file":"register.d.ts","sourceRoot":"","sources":["src/register.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAoD,EAAE,EAAE,aAAa,EAAgB,MAAM,aAAa,CAAA;AACpH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,UAAU,CAAA;AAC7C,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAA;AAa3C,wBAAgB,aAAa,IAAI,aAAa,CAAC,MAAM,CAAC,GAAG,SAAS,CAEjE;AAoFD,wBAAgB,WAAW,CAAC,KAAK,SAAS,MAAM,EAAE,QAAQ,SAAS,OAAO,EAAE,OAAO,EAAE,EAAE,EAAE,UAAU,EAAE,aAAa,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,OAAO,EAAE,aAAa,GAAG,IAAI,CAqDzK;AAED,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,EAAE,GAAG,IAAI,CAIzD"}
|
package/register.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { cmodPluginName } from './runtime/dependencies.js';
|
|
2
2
|
import { createLifecycle, readPlugin } from './runtime/lifecycle.js';
|
|
3
|
+
import { permissionRoutes } from './runtime/permission-check.js';
|
|
3
4
|
let lifecycle;
|
|
4
|
-
let
|
|
5
|
+
let permissionCheck;
|
|
5
6
|
let registered;
|
|
6
7
|
export function registeredMod() {
|
|
7
8
|
return registered;
|
|
@@ -86,9 +87,9 @@ function routeCallToMod(_$, eventInput, passOn) {
|
|
|
86
87
|
return lifecycle.route('cmod.call', called, (passed) => passOn(Object.fromEntries(Object.entries(passed).filter(([key]) => key !== 'caller'))));
|
|
87
88
|
}
|
|
88
89
|
export function registerMod(addHook, definition, options) {
|
|
89
|
-
|
|
90
|
+
permissionCheck = undefined;
|
|
90
91
|
registered = definition;
|
|
91
|
-
lifecycle = createLifecycle(definition, () =>
|
|
92
|
+
lifecycle = createLifecycle(definition, () => permissionCheck, options);
|
|
92
93
|
addHook('session.start', startMod);
|
|
93
94
|
addHook('classic.Setup', routeToMod);
|
|
94
95
|
addHook('classic.SessionStart', routeToMod);
|
|
@@ -141,7 +142,7 @@ export function registerMod(addHook, definition, options) {
|
|
|
141
142
|
addHook('turn.start', routeToMod);
|
|
142
143
|
}
|
|
143
144
|
export function registerPermissionCheck(addHook) {
|
|
144
|
-
|
|
145
|
+
permissionCheck = permissionRoutes;
|
|
145
146
|
addHook('tool.check', routeToMod);
|
|
146
147
|
addHook('classic.PermissionRequest', routeToMod);
|
|
147
148
|
}
|
package/runtime/before.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Args, Frozen } from 'claude-code';
|
|
2
2
|
import type { Disposable } from '../mod.js';
|
|
3
|
-
import type { ConversationState, SessionChange } from '../session/conversation.js';
|
|
4
|
-
import type
|
|
3
|
+
import type { ContextData, ConversationState, RunningTask, SessionChange } from '../session/conversation.js';
|
|
4
|
+
import { type BeforeEvent, type SessionEvent } from '../session/session.js';
|
|
5
5
|
import type { Claude } from './claude.js';
|
|
6
6
|
import type { Router } from './router.js';
|
|
7
7
|
export type Place = Pick<ConversationState, 'session' | 'context' | 'contextOpenedAt' | 'turn' | 'tasks' | 'taskTurns' | 'openCalls'>;
|
|
@@ -30,7 +30,11 @@ export declare function installBefore({ claude, router, hooks }: {
|
|
|
30
30
|
readonly router: Router;
|
|
31
31
|
readonly hooks: BeforeHooks;
|
|
32
32
|
}): Disposable;
|
|
33
|
+
export declare function closingContext(where: Place, task: RunningTask | undefined, closedAt: string): ContextData;
|
|
33
34
|
export declare function withAnswer(change: SessionChange, answer: Partial<Answers>): SessionChange;
|
|
34
|
-
export declare
|
|
35
|
-
export declare
|
|
35
|
+
export declare const methodOf: (prefix: 'on' | 'before', event: SessionEvent) => string;
|
|
36
|
+
export declare const beforeAnswerKeys: Readonly<Record<BeforeEvent, readonly (keyof Answers)[]>>;
|
|
37
|
+
export declare function foldBefore(event: BeforeEvent, change: SessionChange, askers: readonly ((change: SessionChange) => Promise<unknown>)[], report: (error: unknown) => void): Promise<Partial<Answers> | undefined>;
|
|
38
|
+
export declare function takenAnswer(method: string, takes: readonly string[], answer: unknown): unknown;
|
|
39
|
+
export declare function foldAnswers<Change, Answer extends object>(method: string, takes: readonly string[], ends: readonly string[], change: Change, askers: readonly ((change: Change) => Promise<unknown>)[], report: (error: unknown) => void, applied?: (change: Change, answer: Answer) => Change): Promise<Answer | undefined>;
|
|
36
40
|
//# sourceMappingURL=before.d.ts.map
|
package/runtime/before.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"before.d.ts","sourceRoot":"","sources":["../src/runtime/before.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AAC/C,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,WAAW,CAAA;AAa3C,OAAO,KAAK,EAAE,iBAAiB,EAAE,aAAa,EAAY,MAAM,4BAA4B,CAAA;
|
|
1
|
+
{"version":3,"file":"before.d.ts","sourceRoot":"","sources":["../src/runtime/before.ts"],"names":[],"mappings":"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"}
|
package/runtime/before.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ContextEnd } from '../session/context/context.js';
|
|
2
2
|
import { InstructionFile, isRulePath } from '../session/context/turn/message/instruction-file.js';
|
|
3
|
-
import { Message, messagesOf, noFiles, rowOfAppend, withTexts } from '../session/context/turn/message/message.js';
|
|
3
|
+
import { instructionPathOf, Message, messagesOf, noFiles, rowOfAppend, withTexts } from '../session/context/turn/message/message.js';
|
|
4
4
|
import { Note } from '../session/context/turn/message/note.js';
|
|
5
5
|
import { Notice } from '../session/context/turn/message/notice.js';
|
|
6
6
|
import { Prompt } from '../session/context/turn/message/prompt.js';
|
|
@@ -11,8 +11,10 @@ import { ToolCall } from '../session/context/turn/message/tool-call.js';
|
|
|
11
11
|
import { ToolResult } from '../session/context/turn/message/tool-result.js';
|
|
12
12
|
import { TurnEnd } from '../session/context/turn/turn.js';
|
|
13
13
|
import { Task, TaskStart } from '../session/goal/task.js';
|
|
14
|
+
import { sessionEvents } from '../session/session.js';
|
|
14
15
|
import { cmodPluginName } from './dependencies.js';
|
|
15
16
|
import { toolInputOf } from './tool-calls.js';
|
|
17
|
+
import { resolve } from '../vendor.js';
|
|
16
18
|
const appended = [Note, Reply, Notice];
|
|
17
19
|
const withNote = (below, note) => {
|
|
18
20
|
const fields = (below ?? {});
|
|
@@ -35,6 +37,7 @@ export function installBefore({ claude, router, hooks }) {
|
|
|
35
37
|
};
|
|
36
38
|
const titles = new Map();
|
|
37
39
|
const spawned = new Map();
|
|
40
|
+
const agentFolders = new Map();
|
|
38
41
|
const toolResultHook = (isFailure) => async (e, next) => {
|
|
39
42
|
if (!(await wants(ToolResult)))
|
|
40
43
|
return next(e);
|
|
@@ -99,7 +102,7 @@ export function installBefore({ claude, router, hooks }) {
|
|
|
99
102
|
if (!(await wants(InstructionFile)) || e.type !== 'nested_memory')
|
|
100
103
|
return next(e);
|
|
101
104
|
const where = await place();
|
|
102
|
-
const path =
|
|
105
|
+
const path = instructionPathOf(e.text);
|
|
103
106
|
const answer = await asked(InstructionFile, message(where, { id: '', at: now(), type: InstructionFile, path, text: e.text, isRule: isRulePath(path) }, e.agentId));
|
|
104
107
|
if (answer.refuse !== undefined)
|
|
105
108
|
return { text: null };
|
|
@@ -129,7 +132,8 @@ export function installBefore({ claude, router, hooks }) {
|
|
|
129
132
|
return next(e);
|
|
130
133
|
const where = await place();
|
|
131
134
|
const input = toolInputOf(e);
|
|
132
|
-
const
|
|
135
|
+
const cwd = (e.agentId === undefined ? undefined : agentFolders.get(e.agentId)) ?? (await claude.session.cwd());
|
|
136
|
+
const answer = await asked(ToolCall, message(where, { id: e.tool_use_id, at: now(), type: ToolCall, callId: e.tool_use_id, name: e.tool, input, cwd, files: noFiles }, e.agentId));
|
|
133
137
|
if (answer.refuse !== undefined)
|
|
134
138
|
return { deny: answer.refuse };
|
|
135
139
|
if (answer.result !== undefined)
|
|
@@ -141,8 +145,15 @@ export function installBefore({ claude, router, hooks }) {
|
|
|
141
145
|
}),
|
|
142
146
|
router.add('agent.spawn', async (e, next) => {
|
|
143
147
|
spawned.set(e.tool_use_id, e.prompt);
|
|
148
|
+
const start = async (args) => {
|
|
149
|
+
const started = await next(args);
|
|
150
|
+
const folder = args.isTeammate === true ? undefined : (args.cwd ?? (args.parentAgentId === undefined ? undefined : agentFolders.get(args.parentAgentId)));
|
|
151
|
+
if (started.agentId !== undefined && folder !== undefined)
|
|
152
|
+
agentFolders.set(started.agentId, resolve(await claude.session.cwd(), folder));
|
|
153
|
+
return started;
|
|
154
|
+
};
|
|
144
155
|
if (!(await wants(TaskStart)))
|
|
145
|
-
return
|
|
156
|
+
return start(e);
|
|
146
157
|
const where = await place();
|
|
147
158
|
const task = { id: '', title: e.description, agentType: e.subagentType, model: e.model ?? e.parentModel, openedBy: e.tool_use_id, openedAt: now(), ...(e.parentAgentId === undefined ? {} : { parent: e.parentAgentId }) };
|
|
148
159
|
const answer = await asked(TaskStart, { type: TaskStart, task: { ...task, context: where.context } });
|
|
@@ -150,7 +161,7 @@ export function installBefore({ claude, router, hooks }) {
|
|
|
150
161
|
spawned.delete(e.tool_use_id);
|
|
151
162
|
return { deny: answer.refuse };
|
|
152
163
|
}
|
|
153
|
-
return
|
|
164
|
+
return start(answer.owner === undefined ? e : { ...e, model: answer.owner.model });
|
|
154
165
|
}),
|
|
155
166
|
router.add('classic.SubagentStart', async (e, next) => {
|
|
156
167
|
const where = await place();
|
|
@@ -168,14 +179,19 @@ export function installBefore({ claude, router, hooks }) {
|
|
|
168
179
|
return withNote(await next(e), answer.note);
|
|
169
180
|
}),
|
|
170
181
|
router.add('classic.SubagentStop', async (e, next) => {
|
|
182
|
+
const stopped = (answer) => {
|
|
183
|
+
if (answer.block === undefined)
|
|
184
|
+
agentFolders.delete(e.agent_id);
|
|
185
|
+
return answer;
|
|
186
|
+
};
|
|
171
187
|
if (!(await wants(TurnEnd)))
|
|
172
|
-
return next(e);
|
|
188
|
+
return stopped(await next(e));
|
|
173
189
|
const turn = (await place()).taskTurns[e.agent_id];
|
|
174
190
|
if (turn === undefined)
|
|
175
|
-
return next(e);
|
|
191
|
+
return stopped(await next(e));
|
|
176
192
|
const answer = await asked(TurnEnd, { type: TurnEnd, turn: { ...turn, closedAt: now() } });
|
|
177
193
|
const below = await next(e);
|
|
178
|
-
return answer.refuse === undefined ? below : { ...below, block: answer.refuse };
|
|
194
|
+
return stopped(answer.refuse === undefined ? below : { ...below, block: answer.refuse });
|
|
179
195
|
}),
|
|
180
196
|
router.add('classic.Stop', async (e, next) => {
|
|
181
197
|
if (!(await wants(TurnEnd)))
|
|
@@ -194,13 +210,15 @@ export function installBefore({ claude, router, hooks }) {
|
|
|
194
210
|
const task = e.agentId === undefined ? undefined : where.tasks[e.agentId];
|
|
195
211
|
if (e.agentId !== undefined && task === undefined)
|
|
196
212
|
return next(e);
|
|
197
|
-
const
|
|
198
|
-
const answer = await asked(ContextEnd, { type: ContextEnd, context: { ...context, closedAt: now() } });
|
|
213
|
+
const answer = await asked(ContextEnd, { type: ContextEnd, context: closingContext(where, task, now()) });
|
|
199
214
|
return answer.refuse === undefined ? next(e) : { skip: answer.refuse };
|
|
200
215
|
}),
|
|
201
216
|
];
|
|
202
217
|
return { dispose: () => added.forEach((each) => each.dispose()) };
|
|
203
218
|
}
|
|
219
|
+
export function closingContext(where, task, closedAt) {
|
|
220
|
+
return task === undefined ? { number: where.context, openedAt: where.contextOpenedAt, closedAt } : { number: task.context, openedAt: task.contextOpenedAt ?? task.openedAt, closedAt, task: task.id };
|
|
221
|
+
}
|
|
204
222
|
export function withAnswer(change, answer) {
|
|
205
223
|
if (change.type === Message && answer.output !== undefined && change.message.type === ToolResult)
|
|
206
224
|
return { ...change, message: { ...change.message, output: answer.output } };
|
|
@@ -212,19 +230,47 @@ export function withAnswer(change, answer) {
|
|
|
212
230
|
return { ...change, task: { ...change.task, model: answer.owner.model } };
|
|
213
231
|
return change;
|
|
214
232
|
}
|
|
215
|
-
export
|
|
233
|
+
export const methodOf = (prefix, event) => `${prefix}${Object.entries(sessionEvents).find(([, each]) => each === event)?.[0] ?? event}`;
|
|
234
|
+
export const beforeAnswerKeys = {
|
|
235
|
+
prompt: ['text', 'note', 'title', 'refuse'],
|
|
236
|
+
note: ['text'],
|
|
237
|
+
skill: ['text'],
|
|
238
|
+
instructionFile: ['text', 'refuse'],
|
|
239
|
+
reply: ['text'],
|
|
240
|
+
toolCall: ['input', 'result', 'refuse'],
|
|
241
|
+
toolResult: ['output', 'isError', 'note'],
|
|
242
|
+
notice: ['text'],
|
|
243
|
+
taskStart: ['owner', 'refuse'],
|
|
244
|
+
turnEnd: ['refuse'],
|
|
245
|
+
contextEnd: ['refuse'],
|
|
246
|
+
};
|
|
247
|
+
export function foldBefore(event, change, askers, report) {
|
|
248
|
+
return foldAnswers(methodOf('before', event), beforeAnswerKeys[event], ['refuse', 'result'], change, askers, report, withAnswer);
|
|
249
|
+
}
|
|
250
|
+
export function takenAnswer(method, takes, answer) {
|
|
251
|
+
if (answer === undefined || answer === null || answer === false)
|
|
252
|
+
return undefined;
|
|
253
|
+
if (typeof answer !== 'object' || Array.isArray(answer))
|
|
254
|
+
throw new Error(`it answered ${JSON.stringify(answer)}, but ${method} takes an object or nothing: ${takes.map((key) => `{ ${key} }`).join(', ') || 'no answer'}.`);
|
|
255
|
+
const unread = Object.keys(answer).filter((key) => !takes.includes(key));
|
|
256
|
+
if (unread.length > 0)
|
|
257
|
+
throw new Error(`it answered ${unread.join(' and ')}, which ${method} does not take. It takes ${takes.map((key) => `{ ${key} }`).join(', ') || 'no answer'}.`);
|
|
258
|
+
return answer;
|
|
259
|
+
}
|
|
260
|
+
export async function foldAnswers(method, takes, ends, change, askers, report, applied = (same) => same) {
|
|
216
261
|
let latest = change;
|
|
217
262
|
let result;
|
|
218
263
|
for (const ask of askers) {
|
|
219
264
|
try {
|
|
220
|
-
const answer =
|
|
221
|
-
if (answer ===
|
|
265
|
+
const answer = takenAnswer(method, takes, await ask(latest));
|
|
266
|
+
if (typeof answer !== 'object' || answer === null)
|
|
222
267
|
continue;
|
|
223
|
-
const
|
|
224
|
-
|
|
225
|
-
|
|
268
|
+
const given = answer;
|
|
269
|
+
const notes = [result?.['note'], given['note']].filter((note) => typeof note === 'string');
|
|
270
|
+
result = { ...result, ...given, ...(notes.length === 0 ? {} : { note: notes.join('\n\n') }) };
|
|
271
|
+
if (ends.some((key) => given[key] !== undefined))
|
|
226
272
|
return result;
|
|
227
|
-
latest =
|
|
273
|
+
latest = applied(latest, answer);
|
|
228
274
|
}
|
|
229
275
|
catch (error) {
|
|
230
276
|
report(error);
|
|
@@ -232,25 +278,3 @@ export async function foldAnswers(event, change, askers, report) {
|
|
|
232
278
|
}
|
|
233
279
|
return result;
|
|
234
280
|
}
|
|
235
|
-
export function answerOf(event, answer) {
|
|
236
|
-
if (typeof answer !== 'object' || answer === null)
|
|
237
|
-
return undefined;
|
|
238
|
-
const given = answer;
|
|
239
|
-
const allowed = {
|
|
240
|
-
prompt: ['text', 'note', 'title', 'refuse'],
|
|
241
|
-
note: ['text'],
|
|
242
|
-
skill: ['text'],
|
|
243
|
-
instructionFile: ['text', 'refuse'],
|
|
244
|
-
reply: ['text'],
|
|
245
|
-
toolCall: ['input', 'result', 'refuse'],
|
|
246
|
-
toolResult: ['output', 'isError', 'note'],
|
|
247
|
-
notice: ['text'],
|
|
248
|
-
taskStart: ['owner', 'refuse'],
|
|
249
|
-
turnEnd: ['refuse'],
|
|
250
|
-
contextEnd: ['refuse'],
|
|
251
|
-
};
|
|
252
|
-
const unread = Object.keys(given).filter((key) => !allowed[event].includes(key));
|
|
253
|
-
if (unread.length > 0)
|
|
254
|
-
throw new Error(`a before(${event}) listener answered ${unread.join(' and ')}, which ${event} does not take. It takes ${allowed[event].map((key) => `{ ${key} }`).join(', ')}.`);
|
|
255
|
-
return given;
|
|
256
|
-
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"conversations.d.ts","sourceRoot":"","sources":["../src/runtime/conversations.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,WAAW,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AAY5D,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AAGzC,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AA2BzC,MAAM,MAAM,aAAa,GAAG;IAC1B,MAAM,CAAC,CAAC,EAAE,MAAM,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC,GAAG;QAAE,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,WAAW,CAAC,WAAW,CAAC,GAAG,SAAS,CAAC,CAAA;IAClH,KAAK,IAAI,IAAI,CAAA;CACd,CAAA;AAED,wBAAgB,kBAAkB,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,EAAE;IAAE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,MAAM,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,KAAK,IAAI,CAAA;CAAE,GAAG,aAAa,
|
|
1
|
+
{"version":3,"file":"conversations.d.ts","sourceRoot":"","sources":["../src/runtime/conversations.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,WAAW,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AAY5D,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AAGzC,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AA2BzC,MAAM,MAAM,aAAa,GAAG;IAC1B,MAAM,CAAC,CAAC,EAAE,MAAM,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC,GAAG;QAAE,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,WAAW,CAAC,WAAW,CAAC,GAAG,SAAS,CAAC,CAAA;IAClH,KAAK,IAAI,IAAI,CAAA;CACd,CAAA;AAED,wBAAgB,kBAAkB,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,EAAE;IAAE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,MAAM,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,KAAK,IAAI,CAAA;CAAE,GAAG,aAAa,CA6iBlR"}
|
package/runtime/conversations.js
CHANGED
|
@@ -8,7 +8,7 @@ import { GoalChange } from '../session/goal/goal.js';
|
|
|
8
8
|
import { receivedMatcher } from '../utils/matcher.js';
|
|
9
9
|
import { ifMissing } from '../utils/paths.js';
|
|
10
10
|
import { formatExit, messageOf } from '../utils/text.js';
|
|
11
|
-
import {
|
|
11
|
+
import { foldBefore, installBefore, relayOf } from './before.js';
|
|
12
12
|
import { cmodPluginName } from './dependencies.js';
|
|
13
13
|
import { readLines, runCmod } from './programs.js';
|
|
14
14
|
import { actPermission, conversationMethods, everySession, isAct, isHeardBy, resultOf, spawnTask, usageOf } from './sessions.js';
|
|
@@ -50,7 +50,7 @@ export function serveConversations({ claude, router, store, project, report }) {
|
|
|
50
50
|
router.add('classic.Stop', async (e, next) => endTurn({ reason: 'answer', ...(e.last_assistant_message === undefined ? {} : { answer: e.last_assistant_message }) })(await next(e)));
|
|
51
51
|
router.add('classic.StopFailure', async (e, next) => endTurn({ reason: 'error', failure: { type: String(e.error), text: e.error_details ?? e.last_assistant_message ?? String(e.error) } })(await next(e)));
|
|
52
52
|
router.add('classic.SubagentStop', async (e, next) => endTurn({ reason: 'answer', ...(e.last_assistant_message === undefined ? {} : { answer: e.last_assistant_message }) }, e.agent_id)(await next(e)));
|
|
53
|
-
const json = async (args) => JSON.parse(await runCmod(claude, [...args, '--json', '--project', project()],
|
|
53
|
+
const json = async (args) => JSON.parse(await runCmod(claude, [...args, '--json', '--project', project()], await claude.session.id()));
|
|
54
54
|
const isGranted = async (caller, item) => {
|
|
55
55
|
if (caller === cmodPluginName)
|
|
56
56
|
return true;
|
|
@@ -116,16 +116,39 @@ export function serveConversations({ claude, router, store, project, report }) {
|
|
|
116
116
|
if (sent.isDelivered !== true)
|
|
117
117
|
throw ended(to);
|
|
118
118
|
};
|
|
119
|
+
const waitingOn = new Map();
|
|
120
|
+
let liveness;
|
|
121
|
+
let isChecking = false;
|
|
122
|
+
const checkLiveness = async () => {
|
|
123
|
+
if (isChecking)
|
|
124
|
+
return;
|
|
125
|
+
isChecking = true;
|
|
126
|
+
try {
|
|
127
|
+
const waiting = [...waitingOn];
|
|
128
|
+
const running = new Set((await json(['history', 'sessions', '--running', '--all'])).map((each) => each.id));
|
|
129
|
+
for (const [settle, to] of waiting)
|
|
130
|
+
if (!running.has(to))
|
|
131
|
+
settle.reject(ended(to));
|
|
132
|
+
}
|
|
133
|
+
finally {
|
|
134
|
+
isChecking = false;
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
const watchLife = (settle, to) => {
|
|
138
|
+
waitingOn.set(settle, to);
|
|
139
|
+
liveness ??= claude.clock.every(livenessMs, () => void checkLiveness().catch((error) => report('liveness', 'Could not check which sessions still run', error)));
|
|
140
|
+
return {
|
|
141
|
+
cancel() {
|
|
142
|
+
waitingOn.delete(settle);
|
|
143
|
+
if (waitingOn.size > 0)
|
|
144
|
+
return;
|
|
145
|
+
liveness?.cancel();
|
|
146
|
+
liveness = undefined;
|
|
147
|
+
},
|
|
148
|
+
};
|
|
149
|
+
};
|
|
119
150
|
const ask = (to, relay, deadlineMs) => new Promise((resolve, reject) => {
|
|
120
151
|
const id = `${session}:${(asks += 1)}`;
|
|
121
|
-
const timer = deadlineMs === undefined
|
|
122
|
-
? claude.clock.every(livenessMs, () => {
|
|
123
|
-
void isRunning(to).then((isLive) => {
|
|
124
|
-
if (!isLive)
|
|
125
|
-
settle.reject(ended(to));
|
|
126
|
-
});
|
|
127
|
-
})
|
|
128
|
-
: claude.clock.after(deadlineMs, () => settle.reject(new Error(`session ${to} did not answer within ${deadlineMs / 1000} s`)));
|
|
129
152
|
const settle = {
|
|
130
153
|
resolve(value) {
|
|
131
154
|
if (asked.delete(id))
|
|
@@ -138,6 +161,7 @@ export function serveConversations({ claude, router, store, project, report }) {
|
|
|
138
161
|
reject(error);
|
|
139
162
|
},
|
|
140
163
|
};
|
|
164
|
+
const timer = deadlineMs === undefined ? watchLife(settle, to) : claude.clock.after(deadlineMs, () => settle.reject(new Error(`session ${to} did not answer within ${deadlineMs / 1000} s`)));
|
|
141
165
|
asked.set(id, settle);
|
|
142
166
|
post(to, { ...relay, id, from: session }).catch((error) => settle.reject(error instanceof Error ? error : new Error(messageOf(error))));
|
|
143
167
|
});
|
|
@@ -211,7 +235,7 @@ export function serveConversations({ claude, router, store, project, report }) {
|
|
|
211
235
|
await Promise.all([starting, watching]);
|
|
212
236
|
return deciders(event).length > 0;
|
|
213
237
|
},
|
|
214
|
-
decide: (event, change) =>
|
|
238
|
+
decide: (event, change) => foldBefore(event, change, deciders(event).map((watcher) => (latest) => (watcher.from === session ? askHere(watcher.mod, event, latest) : askAway(watcher, event, latest))), (error) => report(`before:${event}`, `Passed the ${event} on without a mod's answer`, error)),
|
|
215
239
|
},
|
|
216
240
|
});
|
|
217
241
|
const start = (id, source) => {
|
|
@@ -542,7 +566,7 @@ export function serveConversations({ claude, router, store, project, report }) {
|
|
|
542
566
|
const task = typeof request['task'] === 'string' ? request['task'] : undefined;
|
|
543
567
|
if (!isAct(action))
|
|
544
568
|
throw new Error(`cmod takes no action named ${action}.`);
|
|
545
|
-
if (target === session)
|
|
569
|
+
if (target === (await claude.session.id()))
|
|
546
570
|
return perform(caller, action, request['input'], task);
|
|
547
571
|
return actElsewhere(caller, target, action, request['input'], task);
|
|
548
572
|
},
|
|
@@ -560,7 +584,8 @@ export function serveConversations({ claude, router, store, project, report }) {
|
|
|
560
584
|
if (method === undefined)
|
|
561
585
|
return undefined;
|
|
562
586
|
const input = isObject(e.input) ? e.input : {};
|
|
563
|
-
|
|
587
|
+
const isDropping = (e.method === conversationMethods.subscribe || e.method === conversationMethods.watchBefore) && Array.isArray(input['subscriptions']) && input['subscriptions'].length === 0;
|
|
588
|
+
if (e.method !== conversationMethods.act && !isDropping && input['session'] !== (await claude.session.id()) && !(await isGranted(e.caller, 'conversation')))
|
|
564
589
|
return { deny: `${e.caller ?? 'A mod'} reads your conversations without your grant. Turn it on in /mods ${e.caller ?? ''}.`.trim() };
|
|
565
590
|
try {
|
|
566
591
|
return { value: await method(input, e.caller) };
|
package/runtime/engine.d.ts
CHANGED
|
@@ -7,6 +7,7 @@ export type Engine = {
|
|
|
7
7
|
readonly router: Router;
|
|
8
8
|
readonly toolCalls: ToolCalls;
|
|
9
9
|
readonly report: (failure: Failure) => void;
|
|
10
|
+
readonly hasPermissionCheck: () => boolean;
|
|
10
11
|
};
|
|
11
12
|
export declare function keepEngine(mod: object, engine: Engine): void;
|
|
12
13
|
export declare function engineOf(mod: object): Engine;
|
package/runtime/engine.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../src/runtime/engine.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AACzC,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AACzC,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAA;AAC3C,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAA;AAEhD,MAAM,MAAM,MAAM,GAAG;IACnB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,SAAS,EAAE,SAAS,CAAA;IAC7B,QAAQ,CAAC,MAAM,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,IAAI,CAAA;
|
|
1
|
+
{"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../src/runtime/engine.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AACzC,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AACzC,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAA;AAC3C,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAA;AAEhD,MAAM,MAAM,MAAM,GAAG;IACnB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,SAAS,EAAE,SAAS,CAAA;IAC7B,QAAQ,CAAC,MAAM,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,IAAI,CAAA;IAC3C,QAAQ,CAAC,kBAAkB,EAAE,MAAM,OAAO,CAAA;CAC3C,CAAA;AAID,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI,CAE5D;AAED,wBAAgB,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAI5C"}
|