@cmodjs/core 0.1.4 → 0.1.6
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/README.md +4 -0
- package/connect.d.ts.map +1 -1
- package/connect.js +35 -37
- package/docs/commands.md +15 -6
- package/docs/index.md +3 -3
- package/docs/jobs.md +20 -20
- package/docs/mod.md +4 -4
- package/docs/testing.md +1 -1
- package/jobs/check.d.ts +2 -2
- package/jobs/check.d.ts.map +1 -1
- package/jobs/check.js +5 -5
- package/jobs/context.d.ts +14 -0
- package/jobs/context.d.ts.map +1 -0
- package/jobs/{part-context.js → context.js} +6 -6
- package/jobs/permissions.d.ts +2 -2
- package/jobs/permissions.d.ts.map +1 -1
- package/jobs/permissions.js +7 -7
- package/jobs/program.d.ts +2 -2
- package/jobs/program.d.ts.map +1 -1
- package/jobs/prompt.d.ts +2 -2
- package/jobs/prompt.d.ts.map +1 -1
- package/jobs/prompt.js +17 -17
- package/jobs/slash-command.d.ts +2 -2
- package/jobs/slash-command.d.ts.map +1 -1
- package/jobs/slash-command.js +5 -5
- package/jobs/status-line.d.ts +2 -2
- package/jobs/status-line.d.ts.map +1 -1
- package/jobs/status-line.js +5 -5
- package/jobs/tool.d.ts +2 -2
- package/jobs/tool.d.ts.map +1 -1
- package/jobs/tool.js +5 -5
- package/mod.d.ts +3 -3
- package/mod.d.ts.map +1 -1
- package/package.json +1 -1
- package/runtime/lifecycle.d.ts.map +1 -1
- package/runtime/lifecycle.js +8 -9
- package/runtime/tool-calls.d.ts +3 -2
- package/runtime/tool-calls.d.ts.map +1 -1
- package/runtime/tool-calls.js +6 -6
- package/src/connect.ts +36 -39
- package/src/jobs/check.ts +7 -7
- package/src/jobs/{part-context.ts → context.ts} +10 -10
- package/src/jobs/permissions.ts +9 -9
- package/src/jobs/program.ts +2 -2
- package/src/jobs/prompt.ts +19 -19
- package/src/jobs/slash-command.ts +7 -7
- package/src/jobs/status-line.ts +7 -7
- package/src/jobs/tool.ts +7 -7
- package/src/mod.ts +3 -3
- package/src/runtime/lifecycle.ts +9 -10
- package/src/runtime/tool-calls.ts +8 -7
- package/jobs/part-context.d.ts +0 -14
- package/jobs/part-context.d.ts.map +0 -1
package/README.md
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="https://raw.githubusercontent.com/heyJordanParker/cmod/main/.claude-plugin/icon.png" alt="Claude Mod Manager" width="128">
|
|
3
|
+
</p>
|
|
4
|
+
|
|
1
5
|
# @cmodjs/core
|
|
2
6
|
|
|
3
7
|
The library every Claude Mod Manager mod imports. Claude Mod Manager (CMod) installs, tries, builds, checks, and publishes Claude Code mods: plugins with hooks, panes, slash commands, tools, permission rules, and an install step that runs with your consent.
|
package/connect.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
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;
|
|
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"}
|
package/connect.js
CHANGED
|
@@ -1,14 +1,7 @@
|
|
|
1
1
|
import { createLifecycle, readPlugin } from './runtime/lifecycle.js';
|
|
2
2
|
let lifecycle;
|
|
3
|
-
async function
|
|
4
|
-
|
|
5
|
-
return next(e);
|
|
6
|
-
}
|
|
7
|
-
function route(_$, e, next) {
|
|
8
|
-
return lifecycle.route(next.event, e, (passed) => next(passed));
|
|
9
|
-
}
|
|
10
|
-
function claudeOf($) {
|
|
11
|
-
return {
|
|
3
|
+
async function startMod($, eventInput, passOn) {
|
|
4
|
+
const claude = {
|
|
12
5
|
plugin: { name: $.plugin.name, root: $.plugin.root },
|
|
13
6
|
ui: {
|
|
14
7
|
toast: (text, options) => $.ui.toast(text, options),
|
|
@@ -64,35 +57,40 @@ function claudeOf($) {
|
|
|
64
57
|
},
|
|
65
58
|
cmod: { call: (input) => $.cmod.call(input) },
|
|
66
59
|
};
|
|
60
|
+
await lifecycle.start(claude, readPlugin);
|
|
61
|
+
return passOn(eventInput);
|
|
62
|
+
}
|
|
63
|
+
function routeToMod(_$, eventInput, passOn) {
|
|
64
|
+
return lifecycle.route(passOn.event, eventInput, (passed) => passOn(passed));
|
|
67
65
|
}
|
|
68
66
|
export function connect(on, definition) {
|
|
69
67
|
lifecycle = createLifecycle(definition);
|
|
70
|
-
on('session.start',
|
|
71
|
-
on('classic.SessionStart',
|
|
72
|
-
on('classic.SessionEnd',
|
|
73
|
-
on('classic.UserPromptSubmit',
|
|
74
|
-
on('classic.InstructionsLoaded',
|
|
75
|
-
on('classic.PreToolUse',
|
|
76
|
-
on('classic.PermissionRequest',
|
|
77
|
-
on('classic.PermissionDenied',
|
|
78
|
-
on('classic.PostToolUse',
|
|
79
|
-
on('classic.PostToolUseFailure',
|
|
80
|
-
on('classic.PostToolBatch',
|
|
81
|
-
on('classic.SubagentStart',
|
|
82
|
-
on('classic.SubagentStop',
|
|
83
|
-
on('classic.Notification',
|
|
84
|
-
on('classic.PreCompact',
|
|
85
|
-
on('classic.Stop',
|
|
86
|
-
on('classic.StopFailure',
|
|
87
|
-
on('tool.check',
|
|
88
|
-
on('tool.call',
|
|
89
|
-
on('prompt.submit',
|
|
90
|
-
on('prompt.context',
|
|
91
|
-
on('command.run',
|
|
92
|
-
on('session.measure',
|
|
93
|
-
on('skill.prompt',
|
|
94
|
-
on('ui.render',
|
|
95
|
-
on('ui.press',
|
|
96
|
-
on('ui.close',
|
|
97
|
-
on('cmod.call',
|
|
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);
|
|
98
96
|
}
|
package/docs/commands.md
CHANGED
|
@@ -82,22 +82,31 @@ 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
|
|
86
|
-
2.
|
|
87
|
-
3.
|
|
88
|
-
4.
|
|
89
|
-
5.
|
|
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.
|
|
87
|
+
3. Checks the release with `claude plugin validate --strict`, and stops with the validator's message when it fails.
|
|
88
|
+
4. Commits the release as the `release` branch, and builds the release archive from that commit.
|
|
89
|
+
5. Builds the program `cli/` declares, and writes `SHA256SUMS` for every file of the release.
|
|
90
|
+
6. Writes `.claude-plugin/marketplace.json`, listing the archive and the CMod plugin.
|
|
91
|
+
7. Commits that file, tags `v<version>`, pushes the tag and the `release` branch, and creates the GitHub release.
|
|
92
|
+
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`.
|
|
93
|
+
|
|
94
|
+
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.
|
|
90
95
|
|
|
91
96
|
It needs, and refuses to start without:
|
|
92
97
|
|
|
93
98
|
- the mod's folder as the root of a git repository with at least one commit, because `git archive` builds from `HEAD`
|
|
99
|
+
- `node_modules` in the mod's folder, from `bun install`, when the hooks module imports a package
|
|
100
|
+
- a `register` declared as a function in the hooks module, as `cmod new` writes it, because the validator refuses a bundled `register` that is not one
|
|
101
|
+
- the `claude` command, which validates the release
|
|
94
102
|
- an `origin` remote on GitHub
|
|
95
103
|
- a `package.json` with no dependency on a `file:` or `link:` path, which no user has
|
|
104
|
+
- a `"files"` list, when `package.json` has one, whose every path matches a committed file
|
|
96
105
|
- a `"program"` in the `cmod` key of `package.json` that names the program `cli/` declares, when `cli/` declares one
|
|
97
106
|
- the network, to hash the CMod plugin release it lists, unless the mod is CMod itself
|
|
98
107
|
- for a real publish, a working tree with no uncommitted changes, a `v<version>` tag that does not exist yet, a `git push` to `origin` that succeeds, and the `gh` command, which creates the GitHub release
|
|
99
108
|
|
|
100
|
-
`--dry-run` builds and writes everything, and pushes nothing. It allows uncommitted changes and an existing tag, and builds from the last commit.
|
|
109
|
+
`--dry-run` builds and writes everything, and pushes nothing. 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.
|
|
101
110
|
|
|
102
111
|
## Install mods
|
|
103
112
|
|
package/docs/index.md
CHANGED
|
@@ -30,7 +30,7 @@ my-mod/
|
|
|
30
30
|
|
|
31
31
|
The template is a working example to replace. It counts the prompts of the session in `state.session.prompts`, shows the count in a pane titled with the mod's name and above the prompt, and tests both. Replace the counter, the pane, the component, the Skill, and the tests with the mod's own.
|
|
32
32
|
|
|
33
|
-
A mod imports `@cmodjs/core` by path, such as `../node_modules/@cmodjs/core/mod.js`, because Claude Code loads a plugin's own files only. Types such as `RenderElement` and `
|
|
33
|
+
A mod imports `@cmodjs/core` by path, such as `../node_modules/@cmodjs/core/mod.js`, because Claude Code loads a plugin's own files only. Types such as `RenderElement` and `On` come from the `claude-code` module.
|
|
34
34
|
|
|
35
35
|
## Type-check after the first session
|
|
36
36
|
|
|
@@ -70,7 +70,7 @@ A mod imports from the files below, and every other file in this package is inte
|
|
|
70
70
|
|
|
71
71
|
| Import | Exports | Docs |
|
|
72
72
|
| --- | --- | --- |
|
|
73
|
-
| `mod.js` | `defineMod`, `ModDefinition`, `Mod`, `
|
|
73
|
+
| `mod.js` | `defineMod`, `ModDefinition`, `Mod`, `JobContext`, `Job`, `ProgressStep`, `PaneHandle`, `longestMs`, `messageOf` | [mod.md](mod.md), [jobs.md](jobs.md), [ui.md](ui.md) |
|
|
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) |
|
|
@@ -87,7 +87,7 @@ A mod imports from the files below, and every other file in this package is inte
|
|
|
87
87
|
| `jobs/prompt.js` | `prompt` | [jobs.md](jobs.md) |
|
|
88
88
|
| `jobs/status-line.js` | `statusLine`, `Usage` | [jobs.md](jobs.md) |
|
|
89
89
|
| `jobs/program.js` | `program`, `Program` | [jobs.md](jobs.md) |
|
|
90
|
-
| `jobs/
|
|
90
|
+
| `jobs/context.js` | `afterCall`, `modWithin`, `modOf`, `workspaceReader` | [jobs.md](jobs.md) |
|
|
91
91
|
| `jobs/permissions/decide-permission.js` | `decidePermission`, `stricterVerdict`, `Rule`, `PermissionRules`, `Decision`, `Verdict` | [jobs.md](jobs.md) |
|
|
92
92
|
| `jobs/permissions/find-project-scope.js` | `findProjectScope`, `ProjectScope`, `Workspace` | [jobs.md](jobs.md) |
|
|
93
93
|
|
package/docs/jobs.md
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
# Jobs
|
|
2
2
|
|
|
3
|
-
A job is a ready-made
|
|
3
|
+
A job is a ready-made feature of a mod: a slash command, a tool, permission rules, a check, a prompt, a status line, or a background program. Add each in `setup` with `mod.use`.
|
|
4
4
|
|
|
5
5
|
```ts
|
|
6
|
-
mod.use<Handle>(
|
|
6
|
+
mod.use<Handle>(job: Job<Handle, State>): Handle
|
|
7
7
|
|
|
8
|
-
type
|
|
8
|
+
type Job<Handle, State> = (context: JobContext<State>) => Handle
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
Each job function, such as `slashCommand(options)`, returns a `
|
|
11
|
+
Each job function, such as `slashCommand(options)`, returns a `Job`. `mod.use` runs the job and returns its handle. A job that has nothing to hand back returns `void`.
|
|
12
12
|
|
|
13
|
-
A job's bad options throw at one of two times. `check`, `permissions`, and `prompt` check their options when the job function is called. `slashCommand`, `tool`, `statusLine`, and `program` check theirs
|
|
13
|
+
A job's bad options throw at one of two times. `check`, `permissions`, and `prompt` check their options when the job function is called. `slashCommand`, `tool`, `statusLine`, and `program` check theirs when the job runs, at `mod.use`, so the error surfaces as `its setup function threw: …`.
|
|
14
14
|
|
|
15
15
|
| Job | Import | Adds |
|
|
16
16
|
| --- | --- | --- |
|
|
@@ -48,7 +48,7 @@ slashCommand<State>(options: {
|
|
|
48
48
|
readonly argumentHint?: string
|
|
49
49
|
readonly immediate?: true
|
|
50
50
|
readonly reply: (input: { args: string; positionals: string[] }, mod: Mod<State>) => Reply | void | Promise<Reply | void>
|
|
51
|
-
}):
|
|
51
|
+
}): Job<void, State>
|
|
52
52
|
|
|
53
53
|
type Reply = string | { text?: string; context?: string } | undefined
|
|
54
54
|
```
|
|
@@ -94,7 +94,7 @@ tool<Schema, State>(options: {
|
|
|
94
94
|
readonly description: string
|
|
95
95
|
readonly inputSchema?: Schema
|
|
96
96
|
readonly execute: (input: FromSchema<Schema>, mod: Mod<State>) => unknown
|
|
97
|
-
}):
|
|
97
|
+
}): Job<{ readonly name: string }, State>
|
|
98
98
|
```
|
|
99
99
|
|
|
100
100
|
- `name` is 1 to 64 letters, digits, `_`, or `-`. Claude sees the tool as `mcp__<plugin name>__<name>`, and the handle's `name` is that full name.
|
|
@@ -160,7 +160,7 @@ A `command` pattern is not a glob. CMod splits it into words the way it splits a
|
|
|
160
160
|
## permissions
|
|
161
161
|
|
|
162
162
|
```ts
|
|
163
|
-
permissions<State>(rules: { deny?: Rule<State>[]; ask?: Rule<State>[] }):
|
|
163
|
+
permissions<State>(rules: { deny?: Rule<State>[]; ask?: Rule<State>[] }): Job<void, State>
|
|
164
164
|
|
|
165
165
|
type Rule<State> = Target & {
|
|
166
166
|
when?: (call: <the call of the target's key>, mod: Mod<State>) => boolean | Promise<boolean>
|
|
@@ -211,7 +211,7 @@ export const guard = defineMod({
|
|
|
211
211
|
## check
|
|
212
212
|
|
|
213
213
|
```ts
|
|
214
|
-
check<State>(options: { readonly after: Target | Target[]; readonly run: readonly string[]; readonly timeoutMs?: number }):
|
|
214
|
+
check<State>(options: { readonly after: Target | Target[]; readonly run: readonly string[]; readonly timeoutMs?: number }): Job<void, State>
|
|
215
215
|
```
|
|
216
216
|
|
|
217
217
|
- After each call that matches `after` and succeeds, CMod runs `run`. A call that was denied or failed runs nothing.
|
|
@@ -241,7 +241,7 @@ prompt<State>(options: {
|
|
|
241
241
|
prompt: string | ((input: { userPrompt?: string; call?: ToolCall }, mod: Mod<State>) => string | undefined | Promise<string | undefined>)
|
|
242
242
|
when?: RegExp | ((userPrompt: string) => boolean)
|
|
243
243
|
after?: Target | Target[]
|
|
244
|
-
}):
|
|
244
|
+
}): Job<void, State>
|
|
245
245
|
```
|
|
246
246
|
|
|
247
247
|
`prompt` adds text that Claude reads. `when` and `after` pick when:
|
|
@@ -277,7 +277,7 @@ export const conventions = defineMod({
|
|
|
277
277
|
statusLine<State>(options: {
|
|
278
278
|
readonly text: (usage: Usage, mod: Mod<State>) => string | undefined | Promise<string | undefined>
|
|
279
279
|
readonly interval?: number
|
|
280
|
-
}):
|
|
280
|
+
}): Job<void, State>
|
|
281
281
|
|
|
282
282
|
type Usage = { model: string; context: { tokens?: number; window: number; percent?: number }; cost?: { usd: number } }
|
|
283
283
|
```
|
|
@@ -303,7 +303,7 @@ export const usage = defineMod({
|
|
|
303
303
|
## program
|
|
304
304
|
|
|
305
305
|
```ts
|
|
306
|
-
program(options: { readonly command: readonly string[]; readonly environment?: Record<string, string> }):
|
|
306
|
+
program(options: { readonly command: readonly string[]; readonly environment?: Record<string, string> }): Job<Program>
|
|
307
307
|
|
|
308
308
|
type Program = {
|
|
309
309
|
readonly state: 'stopped' | 'starting' | 'running' | 'backoff' | 'fatal'
|
|
@@ -345,10 +345,10 @@ export const preview = defineMod({
|
|
|
345
345
|
|
|
346
346
|
## Write a job of your own
|
|
347
347
|
|
|
348
|
-
A `
|
|
348
|
+
A `Job` is a function. `mod.use` calls it with a `JobContext` and returns what it returns:
|
|
349
349
|
|
|
350
350
|
```ts
|
|
351
|
-
type
|
|
351
|
+
type JobContext<State> = {
|
|
352
352
|
readonly mod: Mod<State>
|
|
353
353
|
readonly claude: Claude
|
|
354
354
|
on<N extends RoutedEvent>(event: N, hook: RoutedHook<N>): void
|
|
@@ -367,9 +367,9 @@ type PartContext<State> = {
|
|
|
367
367
|
- `toolCalls.cwdOf(toolUseId)` returns the working folder at the call's `PreToolUse`, or `undefined` once its `PostToolUse` or `PostToolUseFailure` has run. CMod keeps the folders of the last 100 calls.
|
|
368
368
|
|
|
369
369
|
```ts
|
|
370
|
-
import { defineMod, type
|
|
370
|
+
import { defineMod, type Job } from '../node_modules/@cmodjs/core/mod.js'
|
|
371
371
|
|
|
372
|
-
const clock:
|
|
372
|
+
const clock: Job<{ now(): Promise<number> }> = ({ claude, announce }) => {
|
|
373
373
|
announce('a clock')
|
|
374
374
|
return { now: () => claude.clock.now() }
|
|
375
375
|
}
|
|
@@ -436,10 +436,10 @@ The built-in jobs are written from these building blocks, which a job of your ow
|
|
|
436
436
|
|
|
437
437
|
| Import | Export | What it does |
|
|
438
438
|
| --- | --- | --- |
|
|
439
|
-
| `jobs/
|
|
440
|
-
| `jobs/
|
|
441
|
-
| `jobs/
|
|
442
|
-
| `jobs/
|
|
439
|
+
| `jobs/context.js` | `afterCall(job, contextAfter, failed)` | Runs `contextAfter` after each call that succeeds, and adds the lines it returns to the call's result. |
|
|
440
|
+
| `jobs/context.js` | `modWithin(job, deadline, workTree?)` | A `mod` whose calls fail at `deadline`, `{ ms, job?, longestMs? }`. |
|
|
441
|
+
| `jobs/context.js` | `modOf(job, { call?, folder }, workspace, deadline)` | `modWithin`, with `mod.process` calls run in the call's work tree. |
|
|
442
|
+
| `jobs/context.js` | `workspaceReader(job)` | A function that reads the session's `projectRoot`, `cwd`, `home`, file access, and project scope. |
|
|
443
443
|
| `jobs/permissions/decide-permission.js` | `decidePermission`, `stricterVerdict`, and the types `Rule`, `PermissionRules`, `Decision`, `Verdict` | Decides a call against permission rules. |
|
|
444
444
|
| `jobs/permissions/find-project-scope.js` | `findProjectScope`, and the types `ProjectScope`, `Workspace` | Finds the repository a project plugin belongs to. |
|
|
445
445
|
|
package/docs/mod.md
CHANGED
|
@@ -59,11 +59,11 @@ Each of these says `Fix it, then run /reload-plugins.`
|
|
|
59
59
|
`hooks/hooks.json` names `hooks/register.ts` as the plugin's hooks module. `register.ts` calls `connect` and does nothing else:
|
|
60
60
|
|
|
61
61
|
```ts
|
|
62
|
-
import type {
|
|
62
|
+
import type { On } from 'claude-code'
|
|
63
63
|
import { connect } from '../node_modules/@cmodjs/core/connect.js'
|
|
64
64
|
import { greeter } from '../src/mod.js'
|
|
65
65
|
|
|
66
|
-
export
|
|
66
|
+
export function register(on: On): void {
|
|
67
67
|
connect(on, greeter)
|
|
68
68
|
}
|
|
69
69
|
```
|
|
@@ -72,7 +72,7 @@ export const register: Register = (on) => {
|
|
|
72
72
|
connect<State>(on: On, definition: ModDefinition<State>): void
|
|
73
73
|
```
|
|
74
74
|
|
|
75
|
-
`connect` registers one handler per Claude Code event the mod can use, and routes each event to the mod. Keep `register.ts` this small. Claude Code checks a hooks module before it loads it, and refuses some shapes, such as a `$` passed to a function in another module.
|
|
75
|
+
`connect` registers one handler per Claude Code event the mod can use, and routes each event to the mod. Keep `register.ts` this small. Claude Code checks a hooks module before it loads it, and refuses some shapes, such as a `$` passed to a function in another module. Declare `register` as a function, as here: `cmod publish` bundles the hooks module into one file, and Claude Code refuses a bundled `register` that is not one.
|
|
76
76
|
|
|
77
77
|
## What `mod` can call
|
|
78
78
|
|
|
@@ -86,7 +86,7 @@ type Mod<State> = {
|
|
|
86
86
|
readonly projectRoot: string
|
|
87
87
|
readonly cwd: string
|
|
88
88
|
on(event, hook): void
|
|
89
|
-
use(
|
|
89
|
+
use(job): Handle
|
|
90
90
|
readonly ui: { pane, render, toast, progress, ask }
|
|
91
91
|
readonly process: { run, spawn }
|
|
92
92
|
readonly fs: { read, write, list }
|
package/docs/testing.md
CHANGED
|
@@ -69,7 +69,7 @@ type TestedMod<State> = {
|
|
|
69
69
|
|
|
70
70
|
### start
|
|
71
71
|
|
|
72
|
-
Starts the mod: loads its state, runs `setup`, and adds its
|
|
72
|
+
Starts the mod: loads its state, runs `setup`, and adds its hooks, panes, and jobs. `fire`, `lines`, `type`, `callTool`, `press`, and `moveTo` start the mod first, so a test calls `start` only before it reads `state` or `shown` with nothing else to run. It rejects with the reason when the mod does not start.
|
|
73
73
|
|
|
74
74
|
### fire
|
|
75
75
|
|
package/jobs/check.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { Job } from '../mod.js';
|
|
2
2
|
import { type Target } from './permissions/match-target.js';
|
|
3
3
|
export declare function check<State extends object = Record<never, never>>(options: {
|
|
4
4
|
readonly after: Target | Target[];
|
|
5
5
|
readonly run: readonly string[];
|
|
6
6
|
readonly timeoutMs?: number;
|
|
7
|
-
}):
|
|
7
|
+
}): Job<void, State>;
|
|
8
8
|
//# sourceMappingURL=check.d.ts.map
|
package/jobs/check.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"check.d.ts","sourceRoot":"","sources":["../src/jobs/check.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,
|
|
1
|
+
{"version":3,"file":"check.d.ts","sourceRoot":"","sources":["../src/jobs/check.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,WAAW,CAAA;AAOpC,OAAO,EAAyB,KAAK,MAAM,EAAE,MAAM,+BAA+B,CAAA;AAIlF,wBAAgB,KAAK,CAAC,KAAK,SAAS,MAAM,GAAG,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,EAAE,OAAO,EAAE;IAAE,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IAAC,QAAQ,CAAC,GAAG,EAAE,SAAS,MAAM,EAAE,CAAC;IAAC,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,GAAG,CAAC,IAAI,EAAE,KAAK,CAAC,CA4BjM"}
|
package/jobs/check.js
CHANGED
|
@@ -2,7 +2,7 @@ import { longestMs } from '../runtime/deadline.js';
|
|
|
2
2
|
import { callEffects } from '../utils/call-effects.js';
|
|
3
3
|
import { parentOf } from '../utils/paths.js';
|
|
4
4
|
import { listed, messageOf } from '../utils/text.js';
|
|
5
|
-
import { afterCall, modOf, targetWords } from './
|
|
5
|
+
import { afterCall, modOf, targetWords } from './context.js';
|
|
6
6
|
import { matchTarget, targetOf } from './permissions/match-target.js';
|
|
7
7
|
const defaultMs = 60_000;
|
|
8
8
|
export function check(options) {
|
|
@@ -18,21 +18,21 @@ export function check(options) {
|
|
|
18
18
|
if (!(timeoutMs > 0 && timeoutMs <= longestMs))
|
|
19
19
|
throw new Error(`check: the ${named} has timeoutMs ${timeoutMs}. Set it above 0 and at most ${longestMs} (${longestMs / 60_000} minutes).`);
|
|
20
20
|
const deadline = { ms: timeoutMs, job: 'check' };
|
|
21
|
-
return (
|
|
22
|
-
afterCall(
|
|
21
|
+
return (job) => {
|
|
22
|
+
afterCall(job, async (use, workspace) => {
|
|
23
23
|
const triggered = await triggeredRun(targets, run, use, workspace);
|
|
24
24
|
if (triggered === undefined)
|
|
25
25
|
return [];
|
|
26
26
|
try {
|
|
27
27
|
const { command, folder } = triggered;
|
|
28
|
-
const { exitCode, stdout, stderr } = await modOf(
|
|
28
|
+
const { exitCode, stdout, stderr } = await modOf(job, { folder }, workspace, deadline).process.run(command);
|
|
29
29
|
return exitCode === 0 ? [] : [`${command.join(' ')} exited with ${exitCode}:\n${`${stdout}${stderr}`.trim()}`];
|
|
30
30
|
}
|
|
31
31
|
catch (error) {
|
|
32
32
|
return [`The ${named} failed: ${messageOf(error)}`];
|
|
33
33
|
}
|
|
34
34
|
}, (error) => [`The check job failed, so the ${named} did not run: ${messageOf(error)}`]);
|
|
35
|
-
|
|
35
|
+
job.announce(`a ${named}`);
|
|
36
36
|
};
|
|
37
37
|
}
|
|
38
38
|
async function triggeredRun(after, run, use, workspace) {
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { JobContext, Mod } from '../mod.js';
|
|
2
|
+
import { type Deadline } from '../runtime/deadline.js';
|
|
3
|
+
import type { ToolCall, ToolUse } from '../utils/call-effects.js';
|
|
4
|
+
import { type Workspace } from './permissions/find-project-scope.js';
|
|
5
|
+
import { type Target } from './permissions/match-target.js';
|
|
6
|
+
export declare function workspaceReader({ claude }: JobContext): () => Promise<Workspace>;
|
|
7
|
+
export declare function afterCall<State extends object>(job: JobContext<State>, contextAfter: (use: ToolUse, workspace: Workspace) => Promise<readonly string[]>, failed: (error: unknown) => readonly string[]): void;
|
|
8
|
+
export declare function modOf<State extends object>(job: JobContext<State>, { call, folder }: {
|
|
9
|
+
readonly call?: ToolCall;
|
|
10
|
+
readonly folder: string;
|
|
11
|
+
}, workspace: Workspace, deadline: Deadline): Mod<State>;
|
|
12
|
+
export declare function modWithin<State extends object>({ mod, claude }: JobContext<State>, deadline: Deadline, workTree?: () => Promise<string | undefined>): Mod<State>;
|
|
13
|
+
export declare function targetWords(target: Target): string;
|
|
14
|
+
//# sourceMappingURL=context.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../src/jobs/context.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,GAAG,EAAE,MAAM,WAAW,CAAA;AAEhD,OAAO,EAAkB,KAAK,QAAQ,EAAE,MAAM,wBAAwB,CAAA;AAItE,OAAO,KAAK,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,0BAA0B,CAAA;AACjE,OAAO,EAAoB,KAAK,SAAS,EAAE,MAAM,qCAAqC,CAAA;AACtF,OAAO,EAAY,KAAK,MAAM,EAAE,MAAM,+BAA+B,CAAA;AAOrE,wBAAgB,eAAe,CAAC,EAAE,MAAM,EAAE,EAAE,UAAU,GAAG,MAAM,OAAO,CAAC,SAAS,CAAC,CAOhF;AAaD,wBAAgB,SAAS,CAAC,KAAK,SAAS,MAAM,EAC5C,GAAG,EAAE,UAAU,CAAC,KAAK,CAAC,EACtB,YAAY,EAAE,CAAC,GAAG,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS,KAAK,OAAO,CAAC,SAAS,MAAM,EAAE,CAAC,EAChF,MAAM,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,SAAS,MAAM,EAAE,GAC5C,IAAI,CAUN;AAMD,wBAAgB,KAAK,CAAC,KAAK,SAAS,MAAM,EAAE,GAAG,EAAE,UAAU,CAAC,KAAK,CAAC,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE;IAAE,QAAQ,CAAC,IAAI,CAAC,EAAE,QAAQ,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CAAE,EAAE,SAAS,EAAE,SAAS,EAAE,QAAQ,EAAE,QAAQ,GAAG,GAAG,CAAC,KAAK,CAAC,CAEjM;AAED,wBAAgB,SAAS,CAAC,KAAK,SAAS,MAAM,EAAE,EAAE,GAAG,EAAE,MAAM,EAAE,EAAE,UAAU,CAAC,KAAK,CAAC,EAAE,QAAQ,EAAE,QAAQ,EAAE,QAAQ,GAAE,MAAM,OAAO,CAAC,MAAM,GAAG,SAAS,CAAyB,GAAG,GAAG,CAAC,KAAK,CAAC,CAwBvL;AAwBD,wBAAgB,WAAW,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CASlD"}
|
|
@@ -23,10 +23,10 @@ async function readSession(claude) {
|
|
|
23
23
|
throw new Error('HOME is not set, so ~ in a path has no meaning. Start Claude Code with HOME set.');
|
|
24
24
|
return { home, fs, scope: await findProjectScope(claude.plugin.root, home, fs) };
|
|
25
25
|
}
|
|
26
|
-
export function afterCall(
|
|
27
|
-
const readWorkspace = workspaceReader(
|
|
28
|
-
|
|
29
|
-
const before = Promise.all([useOf(
|
|
26
|
+
export function afterCall(job, contextAfter, failed) {
|
|
27
|
+
const readWorkspace = workspaceReader(job);
|
|
28
|
+
job.on('tool.call', async (e, next) => {
|
|
29
|
+
const before = Promise.all([useOf(job, e), readWorkspace()]);
|
|
30
30
|
await before.catch(() => undefined);
|
|
31
31
|
const result = await next(e);
|
|
32
32
|
if (result.deny !== undefined || result.isError === true)
|
|
@@ -38,8 +38,8 @@ export function afterCall(part, contextAfter, failed) {
|
|
|
38
38
|
async function useOf({ toolCalls }, e) {
|
|
39
39
|
return { tool: e.tool, input: toolInputOf(e), ...(await toolCalls.agentOf(e.tool_use_id)) };
|
|
40
40
|
}
|
|
41
|
-
export function modOf(
|
|
42
|
-
return modWithin(
|
|
41
|
+
export function modOf(job, { call, folder }, workspace, deadline) {
|
|
42
|
+
return modWithin(job, deadline, async () => workspace.scope?.workTreeOf(call !== undefined && 'path' in call ? call.path : folder));
|
|
43
43
|
}
|
|
44
44
|
export function modWithin({ mod, claude }, deadline, workTree = async () => undefined) {
|
|
45
45
|
const within = (call, task) => beforeDeadline(claude, deadline, call, task);
|
package/jobs/permissions.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { Job, Mod } from '../mod.js';
|
|
2
2
|
import { type PermissionRules as RulesFor, type Rule as RuleFor } from './permissions/decide-permission.js';
|
|
3
3
|
export type { CommandCall, FetchCall, FileCall, ToolCall } from '../utils/call-effects.js';
|
|
4
4
|
export type { Target } from './permissions/match-target.js';
|
|
5
5
|
export type Rule<State extends object = Record<never, never>> = RuleFor<Mod<State>>;
|
|
6
6
|
export type PermissionRules<State extends object = Record<never, never>> = RulesFor<Mod<State>>;
|
|
7
|
-
export declare function permissions<State extends object = Record<never, never>>(rules: PermissionRules<State>):
|
|
7
|
+
export declare function permissions<State extends object = Record<never, never>>(rules: PermissionRules<State>): Job<void, State>;
|
|
8
8
|
//# sourceMappingURL=permissions.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"permissions.d.ts","sourceRoot":"","sources":["../src/jobs/permissions.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,GAAG,EAAE,
|
|
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;AAGzC,OAAO,EAAqC,KAAK,eAAe,IAAI,QAAQ,EAAE,KAAK,IAAI,IAAI,OAAO,EAAgB,MAAM,oCAAoC,CAAA;AAK5J,YAAY,EAAE,WAAW,EAAE,SAAS,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,0BAA0B,CAAA;AAC1F,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,GAAG,CAAC,IAAI,EAAE,KAAK,CAAC,CAwBxH"}
|
package/jobs/permissions.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { decidePermission, stricterVerdict } from './permissions/decide-permission.js';
|
|
2
2
|
import { targetOf } from './permissions/match-target.js';
|
|
3
3
|
import { messageOf } from '../utils/text.js';
|
|
4
|
-
import { modOf, workspaceReader } from './
|
|
4
|
+
import { modOf, workspaceReader } from './context.js';
|
|
5
5
|
const deadline = { ms: 2000, job: 'permissions' };
|
|
6
6
|
export function permissions(rules) {
|
|
7
7
|
const count = (rules.deny?.length ?? 0) + (rules.ask?.length ?? 0);
|
|
@@ -9,22 +9,22 @@ export function permissions(rules) {
|
|
|
9
9
|
throw new Error('permissions: give it a deny or an ask rule, or remove it from setup.');
|
|
10
10
|
for (const rule of [...(rules.deny ?? []), ...(rules.ask ?? [])])
|
|
11
11
|
targetOf(rule);
|
|
12
|
-
return (
|
|
13
|
-
const readWorkspace = workspaceReader(
|
|
12
|
+
return (job) => {
|
|
13
|
+
const readWorkspace = workspaceReader(job);
|
|
14
14
|
const verdictOf = async (e) => {
|
|
15
15
|
try {
|
|
16
|
-
const [agent, workspace] = await Promise.all([
|
|
16
|
+
const [agent, workspace] = await Promise.all([job.toolCalls.agentOf(e.tool_use_id), readWorkspace()]);
|
|
17
17
|
const use = { tool: e.tool, input: e.input, ...agent };
|
|
18
|
-
return await decidePermission(rules, use, workspace, (call, folder) => modOf(
|
|
18
|
+
return await decidePermission(rules, use, workspace, (call, folder) => modOf(job, { call, folder }, workspace, deadline));
|
|
19
19
|
}
|
|
20
20
|
catch (error) {
|
|
21
21
|
return { decision: 'deny', reason: `The permissions job could not read the session, so it denies the call: ${messageOf(error)}` };
|
|
22
22
|
}
|
|
23
23
|
};
|
|
24
|
-
|
|
24
|
+
job.on('tool.check', async (e, next) => {
|
|
25
25
|
const [below, ours] = await Promise.all([next(e), verdictOf(e)]);
|
|
26
26
|
return ours === undefined ? below : stricterVerdict(below, ours);
|
|
27
27
|
});
|
|
28
|
-
|
|
28
|
+
job.announce(`${count} permission rule${count === 1 ? '' : 's'}`);
|
|
29
29
|
};
|
|
30
30
|
}
|
package/jobs/program.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { Job } from '../mod.js';
|
|
2
2
|
export type Program = {
|
|
3
3
|
readonly state: 'stopped' | 'starting' | 'running' | 'backoff' | 'fatal';
|
|
4
4
|
ready(): Promise<{
|
|
@@ -9,5 +9,5 @@ export type Program = {
|
|
|
9
9
|
export declare function program(options: {
|
|
10
10
|
readonly command: readonly string[];
|
|
11
11
|
readonly environment?: Record<string, string>;
|
|
12
|
-
}):
|
|
12
|
+
}): Job<Program>;
|
|
13
13
|
//# sourceMappingURL=program.d.ts.map
|
package/jobs/program.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"program.d.ts","sourceRoot":"","sources":["../src/jobs/program.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,
|
|
1
|
+
{"version":3,"file":"program.d.ts","sourceRoot":"","sources":["../src/jobs/program.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,WAAW,CAAA;AAGpC,MAAM,MAAM,OAAO,GAAG;IACpB,QAAQ,CAAC,KAAK,EAAE,SAAS,GAAG,UAAU,GAAG,SAAS,GAAG,SAAS,GAAG,OAAO,CAAA;IACxE,KAAK,IAAI,OAAO,CAAC;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,UAAU,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;CACvD,CAAA;AAMD,wBAAgB,OAAO,CAAC,OAAO,EAAE;IAAE,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;IAAC,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAAE,GAAG,GAAG,CAAC,OAAO,CAAC,CAyErI"}
|
package/jobs/prompt.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { Job, Mod } from '../mod.js';
|
|
2
2
|
import { type ToolCall } from '../utils/call-effects.js';
|
|
3
3
|
import { type Target } from './permissions/match-target.js';
|
|
4
4
|
type PromptInput = {
|
|
@@ -11,6 +11,6 @@ type PromptOptions<State extends object> = {
|
|
|
11
11
|
when?: RegExp | ((userPrompt: string) => boolean);
|
|
12
12
|
after?: Target | Target[];
|
|
13
13
|
};
|
|
14
|
-
export declare function prompt<State extends object = Record<never, never>>({ name, prompt: text, when, after }: PromptOptions<State>):
|
|
14
|
+
export declare function prompt<State extends object = Record<never, never>>({ name, prompt: text, when, after }: PromptOptions<State>): Job<void, State>;
|
|
15
15
|
export {};
|
|
16
16
|
//# sourceMappingURL=prompt.d.ts.map
|
package/jobs/prompt.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"prompt.d.ts","sourceRoot":"","sources":["../src/jobs/prompt.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,GAAG,
|
|
1
|
+
{"version":3,"file":"prompt.d.ts","sourceRoot":"","sources":["../src/jobs/prompt.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,GAAG,EAAc,GAAG,EAAE,MAAM,WAAW,CAAA;AAGrD,OAAO,EAAe,KAAK,QAAQ,EAAE,MAAM,0BAA0B,CAAA;AACrE,OAAO,EAAyB,KAAK,MAAM,EAAE,MAAM,+BAA+B,CAAA;AAGlF,KAAK,WAAW,GAAG;IAAE,UAAU,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,QAAQ,CAAA;CAAE,CAAA;AAE3D,KAAK,aAAa,CAAC,KAAK,SAAS,MAAM,IAAI;IACzC,IAAI,EAAE,MAAM,CAAA;IACZ,MAAM,EAAE,MAAM,GAAG,CAAC,CAAC,KAAK,EAAE,WAAW,EAAE,GAAG,EAAE,GAAG,CAAC,KAAK,CAAC,KAAK,MAAM,GAAG,SAAS,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAAC,CAAA;IAC5G,IAAI,CAAC,EAAE,MAAM,GAAG,CAAC,CAAC,UAAU,EAAE,MAAM,KAAK,OAAO,CAAC,CAAA;IACjD,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAA;CAC1B,CAAA;AAID,wBAAgB,MAAM,CAAC,KAAK,SAAS,MAAM,GAAG,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,EAAE,aAAa,CAAC,KAAK,CAAC,GAAG,GAAG,CAAC,IAAI,EAAE,KAAK,CAAC,CAoC/I"}
|
package/jobs/prompt.js
CHANGED
|
@@ -2,7 +2,7 @@ import { longestMs } from '../runtime/deadline.js';
|
|
|
2
2
|
import { listed, messageOf } from '../utils/text.js';
|
|
3
3
|
import { callEffects } from '../utils/call-effects.js';
|
|
4
4
|
import { matchTarget, targetOf } from './permissions/match-target.js';
|
|
5
|
-
import { afterCall, modOf, modWithin, targetWords } from './
|
|
5
|
+
import { afterCall, modOf, modWithin, targetWords } from './context.js';
|
|
6
6
|
const deadline = { ms: 5000, longestMs, job: 'prompt' };
|
|
7
7
|
export function prompt({ name, prompt: text, when, after }) {
|
|
8
8
|
if (name.trim() === '')
|
|
@@ -12,9 +12,9 @@ export function prompt({ name, prompt: text, when, after }) {
|
|
|
12
12
|
const targets = after === undefined ? [] : Array.isArray(after) ? after : [after];
|
|
13
13
|
for (const target of targets)
|
|
14
14
|
targetOf(target);
|
|
15
|
-
return (
|
|
16
|
-
|
|
17
|
-
const log = (reason) =>
|
|
15
|
+
return (job) => {
|
|
16
|
+
job.reserveName('prompt', name, `prompt: ${job.mod.name} adds the name "${name}" two times. Give each prompt its own name.`);
|
|
17
|
+
const log = (reason) => job.claude.ui.log(`prompt "${name}" added nothing: ${reason}`, { to: 'debug' });
|
|
18
18
|
const textFor = async (input, mod) => {
|
|
19
19
|
try {
|
|
20
20
|
const value = typeof text === 'string' ? text : await text(input, mod);
|
|
@@ -30,21 +30,21 @@ export function prompt({ name, prompt: text, when, after }) {
|
|
|
30
30
|
return value === undefined ? undefined : `# ${name}\n${value}`;
|
|
31
31
|
};
|
|
32
32
|
if (when !== undefined) {
|
|
33
|
-
addAfterUserPrompts(
|
|
34
|
-
|
|
33
|
+
addAfterUserPrompts(job, when, (userPrompt) => headedText({ userPrompt }, modWithin(job, deadline)), log);
|
|
34
|
+
job.announce(`the ${name} prompt after matching user prompts`);
|
|
35
35
|
}
|
|
36
36
|
else if (targets.length > 0) {
|
|
37
|
-
addAfterCalls(
|
|
38
|
-
|
|
37
|
+
addAfterCalls(job, targets, headedText, log);
|
|
38
|
+
job.announce(`the ${name} prompt after ${listed(targets.map(targetWords))}`);
|
|
39
39
|
}
|
|
40
40
|
else {
|
|
41
|
-
addOncePerConversation(
|
|
42
|
-
|
|
41
|
+
addOncePerConversation(job, name, () => textFor({}, modWithin(job, deadline)), log);
|
|
42
|
+
job.announce(`the ${name} prompt`);
|
|
43
43
|
}
|
|
44
44
|
};
|
|
45
45
|
}
|
|
46
|
-
function addOncePerConversation(
|
|
47
|
-
|
|
46
|
+
function addOncePerConversation(job, name, textFor, log) {
|
|
47
|
+
job.on('prompt.context', async (e, next) => {
|
|
48
48
|
const below = await next(e);
|
|
49
49
|
if (below.blocks.some((block) => block.name === name)) {
|
|
50
50
|
log(`a block named "${name}" is already in the context`);
|
|
@@ -54,8 +54,8 @@ function addOncePerConversation(part, name, textFor, log) {
|
|
|
54
54
|
return value === undefined ? below : { ...below, blocks: [...below.blocks, { name, text: value }] };
|
|
55
55
|
});
|
|
56
56
|
}
|
|
57
|
-
function addAfterUserPrompts(
|
|
58
|
-
|
|
57
|
+
function addAfterUserPrompts(job, when, textFor, log) {
|
|
58
|
+
job.on('prompt.submit', async (e, next) => {
|
|
59
59
|
let value;
|
|
60
60
|
try {
|
|
61
61
|
if (typeof when === 'function' ? when(e.text) : e.text.search(when) >= 0)
|
|
@@ -67,14 +67,14 @@ function addAfterUserPrompts(part, when, textFor, log) {
|
|
|
67
67
|
return next(value === undefined ? e : { ...e, context: [...(e.context ?? []), value] });
|
|
68
68
|
});
|
|
69
69
|
}
|
|
70
|
-
function addAfterCalls(
|
|
71
|
-
afterCall(
|
|
70
|
+
function addAfterCalls(job, targets, textFor, log) {
|
|
71
|
+
afterCall(job, async (use, workspace) => {
|
|
72
72
|
const effects = callEffects(use, workspace);
|
|
73
73
|
for (const target of targets) {
|
|
74
74
|
const [matched] = await matchTarget(target, use, effects, workspace);
|
|
75
75
|
if (matched === undefined)
|
|
76
76
|
continue;
|
|
77
|
-
const value = await textFor({ call: matched.call }, modOf(
|
|
77
|
+
const value = await textFor({ call: matched.call }, modOf(job, matched, workspace, deadline));
|
|
78
78
|
return value === undefined ? [] : [value];
|
|
79
79
|
}
|
|
80
80
|
return [];
|