@cmodjs/core 0.3.0 → 1.0.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 +50 -10
- package/docs/dependencies.md +12 -12
- package/docs/hooks.md +39 -27
- package/docs/index.md +29 -25
- package/docs/install-steps.md +9 -7
- package/docs/jobs.md +59 -111
- package/docs/mod.md +200 -174
- package/docs/options.md +2 -2
- package/docs/permissions.md +27 -16
- package/docs/session.md +248 -0
- package/docs/state.md +11 -7
- package/docs/testing.md +65 -22
- package/docs/ui.md +108 -76
- package/jobs/check.d.ts.map +1 -1
- package/jobs/check.js +5 -2
- package/jobs/context.d.ts +7 -8
- package/jobs/context.d.ts.map +1 -1
- package/jobs/context.js +23 -73
- package/jobs/permissions/decide-permission.d.ts +5 -3
- package/jobs/permissions/decide-permission.d.ts.map +1 -1
- package/jobs/permissions/decide-permission.js +6 -4
- package/jobs/permissions/find-project-scope.d.ts +1 -1
- package/jobs/permissions/find-project-scope.d.ts.map +1 -1
- package/jobs/permissions/find-project-scope.js +8 -11
- package/jobs/permissions/match-target.d.ts +2 -2
- package/jobs/permissions/match-target.d.ts.map +1 -1
- package/jobs/permissions.d.ts +1 -1
- package/jobs/permissions.d.ts.map +1 -1
- package/jobs/permissions.js +6 -4
- package/jobs/program.d.ts.map +1 -1
- package/jobs/program.js +8 -3
- package/jobs/prompt.d.ts +4 -2
- package/jobs/prompt.d.ts.map +1 -1
- package/jobs/prompt.js +16 -15
- package/jobs/schedule.d.ts +2 -2
- package/jobs/schedule.d.ts.map +1 -1
- package/jobs/schedule.js +9 -9
- package/jobs/slash-command.d.ts +4 -4
- package/jobs/slash-command.d.ts.map +1 -1
- package/jobs/slash-command.js +14 -9
- package/jobs/status-line.d.ts +1 -11
- package/jobs/status-line.d.ts.map +1 -1
- package/jobs/status-line.js +8 -12
- package/jobs/tool.d.ts +2 -2
- package/jobs/tool.d.ts.map +1 -1
- package/jobs/tool.js +15 -9
- package/mod.d.ts +154 -75
- package/mod.d.ts.map +1 -1
- package/mod.js +94 -3
- package/options.d.ts +2 -2
- package/options.js +19 -19
- package/package.json +3 -3
- package/records.d.ts +10 -8
- package/records.d.ts.map +1 -1
- package/records.js +28 -17
- package/register.d.ts.map +1 -1
- package/register.js +17 -5
- package/runtime/announce.d.ts +18 -0
- package/runtime/announce.d.ts.map +1 -0
- package/runtime/announce.js +66 -0
- package/runtime/bound.d.ts +4 -0
- package/runtime/bound.d.ts.map +1 -0
- package/runtime/bound.js +9 -0
- package/runtime/claude.d.ts +3 -2
- package/runtime/claude.d.ts.map +1 -1
- package/runtime/conversations.d.ts +16 -0
- package/runtime/conversations.d.ts.map +1 -0
- package/runtime/conversations.js +291 -0
- package/runtime/deadline.d.ts +0 -1
- package/runtime/deadline.d.ts.map +1 -1
- package/runtime/deadline.js +1 -1
- package/runtime/dependencies.d.ts +2 -0
- package/runtime/dependencies.d.ts.map +1 -1
- package/runtime/dependencies.js +9 -0
- package/runtime/engine.d.ts +17 -0
- package/runtime/engine.d.ts.map +1 -0
- package/runtime/engine.js +10 -0
- package/runtime/grants.d.ts +4 -0
- package/runtime/grants.d.ts.map +1 -1
- package/runtime/grants.js +63 -26
- package/runtime/hooks.d.ts +5 -4
- package/runtime/hooks.d.ts.map +1 -1
- package/runtime/hooks.js +10 -5
- package/runtime/installer.d.ts.map +1 -1
- package/runtime/installer.js +11 -9
- package/runtime/lifecycle.d.ts +3 -1
- package/runtime/lifecycle.d.ts.map +1 -1
- package/runtime/lifecycle.js +316 -185
- package/runtime/metadata.d.ts +3 -1
- package/runtime/metadata.d.ts.map +1 -1
- package/runtime/metadata.js +21 -11
- package/runtime/options.d.ts +3 -1
- package/runtime/options.d.ts.map +1 -1
- package/runtime/options.js +4 -4
- package/runtime/programs.d.ts +8 -0
- package/runtime/programs.d.ts.map +1 -1
- package/runtime/programs.js +39 -1
- package/runtime/router.d.ts +2 -1
- package/runtime/router.d.ts.map +1 -1
- package/runtime/router.js +11 -3
- package/runtime/sessions.d.ts +36 -0
- package/runtime/sessions.d.ts.map +1 -0
- package/runtime/sessions.js +235 -0
- package/runtime/state.d.ts +6 -2
- package/runtime/state.d.ts.map +1 -1
- package/runtime/state.js +46 -13
- package/runtime/toaster.d.ts +31 -0
- package/runtime/toaster.d.ts.map +1 -0
- package/runtime/toaster.js +176 -0
- package/runtime/ui.d.ts +6 -17
- package/runtime/ui.d.ts.map +1 -1
- package/runtime/ui.js +36 -112
- package/session/context/context.d.ts +10 -0
- package/session/context/context.d.ts.map +1 -0
- package/session/context/context.js +2 -0
- package/session/context/turn/message/document.d.ts +7 -0
- package/session/context/turn/message/document.d.ts.map +1 -0
- package/session/context/turn/message/document.js +1 -0
- package/session/context/turn/message/message.d.ts +54 -0
- package/session/context/turn/message/message.d.ts.map +1 -0
- package/session/context/turn/message/message.js +137 -0
- package/session/context/turn/message/note.d.ts +8 -0
- package/session/context/turn/message/note.d.ts.map +1 -0
- package/session/context/turn/message/note.js +1 -0
- package/session/context/turn/message/notice.d.ts +6 -0
- package/session/context/turn/message/notice.d.ts.map +1 -0
- package/session/context/turn/message/notice.js +1 -0
- package/session/context/turn/message/prompt.d.ts +8 -0
- package/session/context/turn/message/prompt.d.ts.map +1 -0
- package/session/context/turn/message/prompt.js +1 -0
- package/session/context/turn/message/reply.d.ts +6 -0
- package/session/context/turn/message/reply.d.ts.map +1 -0
- package/session/context/turn/message/reply.js +1 -0
- package/session/context/turn/message/sender.d.ts +14 -0
- package/session/context/turn/message/sender.d.ts.map +1 -0
- package/session/context/turn/message/sender.js +30 -0
- package/session/context/turn/message/skill.d.ts +7 -0
- package/session/context/turn/message/skill.d.ts.map +1 -0
- package/session/context/turn/message/skill.js +1 -0
- package/session/context/turn/message/tool-call.d.ts +9 -0
- package/session/context/turn/message/tool-call.d.ts.map +1 -0
- package/session/context/turn/message/tool-call.js +1 -0
- package/session/context/turn/message/tool-result.d.ts +8 -0
- package/session/context/turn/message/tool-result.d.ts.map +1 -0
- package/session/context/turn/message/tool-result.js +1 -0
- package/session/context/turn/turn.d.ts +12 -0
- package/session/context/turn/turn.d.ts.map +1 -0
- package/session/context/turn/turn.js +2 -0
- package/session/conversation.d.ts +64 -0
- package/session/conversation.d.ts.map +1 -0
- package/session/conversation.js +123 -0
- package/session/goal/goal.d.ts +10 -0
- package/session/goal/goal.d.ts.map +1 -0
- package/session/goal/goal.js +1 -0
- package/session/goal/task.d.ts +16 -0
- package/session/goal/task.d.ts.map +1 -0
- package/session/goal/task.js +3 -0
- package/session/session.d.ts +99 -0
- package/session/session.d.ts.map +1 -0
- package/session/session.js +1 -0
- package/src/jobs/check.ts +7 -4
- package/src/jobs/context.ts +24 -82
- package/src/jobs/permissions/decide-permission.ts +10 -8
- package/src/jobs/permissions/find-project-scope.ts +8 -14
- package/src/jobs/permissions/match-target.ts +3 -3
- package/src/jobs/permissions.ts +8 -6
- package/src/jobs/program.ts +7 -3
- package/src/jobs/prompt.ts +21 -24
- package/src/jobs/schedule.ts +14 -14
- package/src/jobs/slash-command.ts +20 -14
- package/src/jobs/status-line.ts +9 -21
- package/src/jobs/tool.ts +17 -11
- package/src/mod.ts +209 -97
- package/src/options.ts +22 -22
- package/src/records.ts +35 -23
- package/src/register.ts +17 -5
- package/src/runtime/announce.ts +74 -0
- package/src/runtime/bound.ts +12 -0
- package/src/runtime/claude.ts +4 -2
- package/src/runtime/conversations.ts +302 -0
- package/src/runtime/deadline.ts +2 -2
- package/src/runtime/dependencies.ts +12 -0
- package/src/runtime/engine.ts +22 -0
- package/src/runtime/grants.ts +66 -24
- package/src/runtime/hooks.ts +19 -10
- package/src/runtime/installer.ts +10 -9
- package/src/runtime/lifecycle.ts +312 -180
- package/src/runtime/metadata.ts +33 -11
- package/src/runtime/options.ts +19 -4
- package/src/runtime/programs.ts +40 -1
- package/src/runtime/router.ts +11 -4
- package/src/runtime/sessions.ts +242 -0
- package/src/runtime/state.ts +50 -14
- package/src/runtime/toaster.ts +218 -0
- package/src/runtime/ui.ts +42 -139
- package/src/session/context/context.ts +12 -0
- package/src/session/context/turn/message/document.ts +8 -0
- package/src/session/context/turn/message/message.ts +165 -0
- package/src/session/context/turn/message/note.ts +9 -0
- package/src/session/context/turn/message/notice.ts +7 -0
- package/src/session/context/turn/message/prompt.ts +9 -0
- package/src/session/context/turn/message/reply.ts +7 -0
- package/src/session/context/turn/message/sender.ts +38 -0
- package/src/session/context/turn/message/skill.ts +8 -0
- package/src/session/context/turn/message/tool-call.ts +10 -0
- package/src/session/context/turn/message/tool-result.ts +9 -0
- package/src/session/context/turn/turn.ts +14 -0
- package/src/session/conversation.ts +169 -0
- package/src/session/goal/goal.ts +9 -0
- package/src/session/goal/task.ts +19 -0
- package/src/session/session.ts +94 -0
- package/src/testing/fake-claude.ts +22 -16
- package/src/testing/fake-elements.ts +3 -2
- package/src/testing/fake-files.ts +1 -1
- package/src/testing/fake-sessions.ts +203 -0
- package/src/testing.ts +112 -33
- package/src/ui/components.ts +3 -3
- package/src/ui/elements.ts +22 -8
- package/src/ui/markdown.ts +22 -26
- package/src/ui/places.ts +49 -0
- package/src/utils/call-effects.ts +3 -3
- package/src/utils/changelog.ts +32 -0
- package/src/utils/matcher.ts +37 -0
- package/src/utils/metadata.ts +17 -15
- package/src/utils/parse-shell.ts +9 -8
- package/src/utils/paths.ts +15 -1
- package/src/utils/text.ts +4 -0
- package/testing/fake-claude.d.ts +4 -1
- package/testing/fake-claude.d.ts.map +1 -1
- package/testing/fake-claude.js +18 -11
- package/testing/fake-elements.js +4 -2
- package/testing/fake-files.js +1 -1
- package/testing/fake-sessions.d.ts +74 -0
- package/testing/fake-sessions.d.ts.map +1 -0
- package/testing/fake-sessions.js +170 -0
- package/testing.d.ts +33 -13
- package/testing.d.ts.map +1 -1
- package/testing.js +92 -21
- package/ui/components.d.ts +2 -3
- package/ui/components.d.ts.map +1 -1
- package/ui/components.js +3 -3
- package/ui/elements.d.ts.map +1 -1
- package/ui/elements.js +19 -9
- package/ui/markdown.d.ts +35 -33
- package/ui/markdown.d.ts.map +1 -1
- package/ui/markdown.js +14 -16
- package/ui/places.d.ts +43 -0
- package/ui/places.d.ts.map +1 -0
- package/ui/places.js +31 -0
- package/utils/call-effects.d.ts +1 -1
- package/utils/call-effects.d.ts.map +1 -1
- package/utils/call-effects.js +2 -2
- package/utils/changelog.d.ts +9 -0
- package/utils/changelog.d.ts.map +1 -0
- package/utils/changelog.js +29 -0
- package/utils/matcher.d.ts +13 -0
- package/utils/matcher.d.ts.map +1 -0
- package/utils/matcher.js +32 -0
- package/utils/metadata.d.ts +3 -3
- package/utils/metadata.js +18 -14
- package/utils/parse-shell.js +9 -7
- package/utils/paths.d.ts +2 -0
- package/utils/paths.d.ts.map +1 -1
- package/utils/paths.js +13 -1
- package/utils/text.d.ts +1 -0
- package/utils/text.d.ts.map +1 -1
- package/utils/text.js +3 -0
- package/src/ui/slots.ts +0 -36
- package/ui/slots.d.ts +0 -208
- package/ui/slots.d.ts.map +0 -1
- package/ui/slots.js +0 -19
package/docs/jobs.md
CHANGED
|
@@ -27,18 +27,20 @@ Each job announces itself. The first time a version starts, Claude Mod Manager (
|
|
|
27
27
|
|
|
28
28
|
## Deadlines
|
|
29
29
|
|
|
30
|
-
|
|
30
|
+
A job gives each function it calls a deadline. The whole function must answer within it, every call it makes included, or the job goes on without its answer and names what passed the deadline, as in `the open_ticket tool passed the 30 s deadline of tool`.
|
|
31
31
|
|
|
32
|
-
| Job and function | Deadline
|
|
32
|
+
| Job and function | Deadline | Past the deadline |
|
|
33
33
|
| --- | --- | --- |
|
|
34
|
-
| `slashCommand`'s `reply` | 30 s |
|
|
35
|
-
| `tool`'s `execute` | 30 s |
|
|
36
|
-
| `prompt`'s `prompt` | 5 s |
|
|
37
|
-
| `statusLine`'s `text` | 2 s |
|
|
38
|
-
| `permissions`' `when` | 2 s | 2 s
|
|
39
|
-
| `check`'s `run` |
|
|
34
|
+
| `slashCommand`'s `reply` | 30 s | The command shows `/<name> failed: /<name> passed the 30 s deadline of slashCommand`. |
|
|
35
|
+
| `tool`'s `execute` | 30 s | The call is denied with `The <name> tool failed: the <name> tool passed the 30 s deadline of tool`. |
|
|
36
|
+
| `prompt`'s `prompt` | 5 s | The prompt adds nothing, and the reason goes to Claude Code's debug log. |
|
|
37
|
+
| `statusLine`'s `text` | 2 s | The status line keeps its last text, and the reason goes to Claude Code's debug log. |
|
|
38
|
+
| `permissions`' `when` | 2 s | The rule matches, and its reason adds `Its when check failed: it passed the 2 s deadline of permissions`. |
|
|
39
|
+
| `check`'s `run` | `timeoutMs`, 60 s by default, 10 minutes at most | The call's result says the command passed the deadline. |
|
|
40
40
|
|
|
41
|
-
|
|
41
|
+
A deadline stops waiting for the function, not the work it started: a program the function spawned runs on until it exits or the mod stops. A function that waits on something slow keeps it short, such as a `process.run` with its own `timeoutMs`.
|
|
42
|
+
|
|
43
|
+
Outside a job, in a listener or a render, a `mod.dependencies` call fails after 30 s, and the other calls have only Claude Code's own limits ([mod.md](mod.md)).
|
|
42
44
|
|
|
43
45
|
## slashCommand
|
|
44
46
|
|
|
@@ -48,17 +50,17 @@ slashCommand<State>(options: {
|
|
|
48
50
|
readonly description: string
|
|
49
51
|
readonly argumentHint?: string
|
|
50
52
|
readonly immediate?: true
|
|
51
|
-
readonly reply: (input: { args: string; positionals: string[] }, mod: Mod<State>) =>
|
|
52
|
-
}): Job<
|
|
53
|
+
readonly reply: (input: { args: string; positionals: string[] }, mod: Mod<State>) => CommandReply | void | Promise<CommandReply | void>
|
|
54
|
+
}): Job<Disposable, State>
|
|
53
55
|
|
|
54
|
-
type
|
|
56
|
+
type CommandReply = string | { text?: string; context?: string } | undefined
|
|
55
57
|
```
|
|
56
58
|
|
|
57
59
|
- `name` is the command without the slash: 1 to 64 letters, digits, `_`, or `-`. The person types `/<name>`. Another name throws `<mod>: "<name>" is not a slash command name.`
|
|
58
60
|
- `description` is the line the typeahead and `/help` show.
|
|
59
61
|
- `argumentHint` is drawn dim after the name, such as `[path]`.
|
|
60
62
|
- `immediate: true` runs the command at once while Claude is still working. Without it, the command waits for the turn to end.
|
|
61
|
-
- A command lasts for the session. Adding a command with a name the session already has replaces it, and Claude Code has no call that removes one, so a command whose source is gone answers that it no longer applies.
|
|
63
|
+
- A command lasts for the session. Adding a command with a name the session already has replaces it, and Claude Code has no call that removes one, so a command whose source is gone answers that it no longer applies. `dispose()` on the handle stops the mod answering the command.
|
|
62
64
|
- A key the mod binds in its `package.json` `"cmod": { "keys": { "shift+tab": "/mode" } }` runs the command ([install-steps.md](install-steps.md#key-bindings)). The command of a key takes `immediate: true`, so the key works while Claude works, and a `reply` that returns `undefined`, so a press adds no row to the conversation. It changes `mod.state`, and a pane or band that shows the state is how the person sees the press. cmod logs `<mod>: <key> runs /<name>, which waits for Claude's turn to end and adds a row to the conversation. Give /<name> immediate: true.` for a key whose command lacks it.
|
|
63
65
|
|
|
64
66
|
```ts
|
|
@@ -101,10 +103,10 @@ tool<Schema, State>(options: {
|
|
|
101
103
|
readonly description: string
|
|
102
104
|
readonly inputSchema?: Schema
|
|
103
105
|
readonly execute: (input: FromSchema<Schema>, mod: Mod<State>) => unknown
|
|
104
|
-
}): Job<{ readonly name: string }, State>
|
|
106
|
+
}): Job<Disposable & { readonly name: string }, State>
|
|
105
107
|
```
|
|
106
108
|
|
|
107
|
-
- `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.
|
|
109
|
+
- `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. `dispose()` on the handle stops the mod answering the tool.
|
|
108
110
|
- `description` tells Claude what the tool does.
|
|
109
111
|
- `inputSchema` is a JSON Schema with `type: 'object'`. It types `execute`'s `input`, so write it inline. cmod checks each call's input against it, draft 2020-12, and denies an input that does not fit with the list of its errors. Its properties cannot be named `tool`, `tool_use_id`, `consent`, or `agentId`, which Claude Code keeps for itself.
|
|
110
112
|
- `execute` returns the result Claude reads. A `string` and an array of content blocks go to Claude as they are. Any other value goes as JSON. `undefined` returns nothing.
|
|
@@ -175,7 +177,7 @@ permissions<State>(rules: PermissionRules<State> | ((state: State) => Permission
|
|
|
175
177
|
type PermissionRules<State> = { deny?: Rule<State>[]; ask?: Rule<State>[] }
|
|
176
178
|
|
|
177
179
|
type Rule<State> = Target & {
|
|
178
|
-
when?: (call: <the call of the target's key
|
|
180
|
+
when?: (call: <the call of the target's key> & { folder: string }, mod: Mod<State>) => boolean | Promise<boolean>
|
|
179
181
|
reason?: string
|
|
180
182
|
}
|
|
181
183
|
```
|
|
@@ -189,16 +191,16 @@ type Rule<State> = Target & {
|
|
|
189
191
|
- `permissions({})` throws `permissions: give it a deny or an ask rule, or remove it from setup.`
|
|
190
192
|
- 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.
|
|
191
193
|
|
|
192
|
-
`when` gets the call, typed by the target's key. `jobs/permissions.js` exports each type, and `
|
|
194
|
+
`when` gets the call, typed by the target's key. `jobs/permissions.js` exports each type, and `Call` is any of them:
|
|
193
195
|
|
|
194
|
-
| Key | Call type | Fields beyond `tool`, `input`, `agentId`, and `
|
|
196
|
+
| Key | Call type | Fields beyond `tool`, `input`, `agentId`, `agentType`, and `folder` |
|
|
195
197
|
| --- | --- | --- |
|
|
196
198
|
| `command` | `CommandCall` | `commands`, each `[program, ...arguments]`, and `isFullyParsed` |
|
|
197
199
|
| `read`, `write` | `FileCall` | `path`, and for an `Edit` or a `Write`, `content`, the new text, and `previousContent`, the text before |
|
|
198
200
|
| `fetch` | `FetchCall` | `url` |
|
|
199
|
-
| `subagent`, `tool` | `
|
|
201
|
+
| `subagent`, `tool` | `Call` | none |
|
|
200
202
|
|
|
201
|
-
`tool` is the tool's name, such as `Bash`, and `input` is its input as Claude Code sent it, such as `{ command, description }` for `Bash`, so a rule reads any field cmod does not parse. `agentId` and `agentType` name the subagent that makes the call. In the main conversation `agentId` is absent, and `agentType` is the agent the session started as, when it started as one. `jobs/permissions.js` also exports `Target`, `Rule`, and `PermissionRules`.
|
|
203
|
+
`tool` is the tool's name, such as `Bash`, and `input` is its input as Claude Code sent it, such as `{ command, description }` for `Bash`, so a rule reads any field cmod does not parse. `agentId` and `agentType` name the subagent that makes the call. In the main conversation `agentId` is absent, and `agentType` is the agent the session started as, when it started as one. `folder` is the root of the git work tree that holds the call's file or folder, or that folder itself outside one, so `process.run(['git', 'status'], { cwd: call.folder })` asks the repository the call works in. `jobs/permissions.js` also exports `Target`, `Rule`, and `PermissionRules`.
|
|
202
204
|
|
|
203
205
|
```ts
|
|
204
206
|
import { defineMod } from '../node_modules/@cmodjs/core/mod.js'
|
|
@@ -256,7 +258,7 @@ It needs `"prompt": true`. Without the grant it adds nothing and logs once which
|
|
|
256
258
|
```ts
|
|
257
259
|
prompt<State>(options: {
|
|
258
260
|
name: string
|
|
259
|
-
prompt: string | ((input: { userPrompt?: string; call?:
|
|
261
|
+
prompt: string | ((input: { userPrompt?: string; call?: Call & { folder: string } }, mod: Mod<State>) => string | undefined | Promise<string | undefined>)
|
|
260
262
|
when?: RegExp | ((userPrompt: string) => boolean)
|
|
261
263
|
after?: Target | Target[]
|
|
262
264
|
}): Job<void, State>
|
|
@@ -268,16 +270,16 @@ prompt<State>(options: {
|
|
|
268
270
|
| --- | --- |
|
|
269
271
|
| neither | At the start of each conversation, as a block of context named `name`. A block of that name already in the context wins. Then again whenever `mod.state` changes and the text differs from what Claude last got. |
|
|
270
272
|
| `when` | With each prompt the person sends whose text matches the `RegExp`, or for which the function returns true. `input.userPrompt` is the prompt. |
|
|
271
|
-
| `after` | After each call that matches a target and succeeds, with the call's result. `input.call` is the matched call. |
|
|
273
|
+
| `after` | After each call that matches a target and succeeds, with the call's result. `input.call` is the matched call, with its `folder` as `permissions` gives it. |
|
|
272
274
|
|
|
273
275
|
- With `when` or `after`, the text goes to Claude under the heading `# <name>`.
|
|
274
|
-
- With neither, the text follows the mod's state. After a change to `mod.state`, cmod calls `prompt` again, and when the text differs from what Claude last got, it adds `# <name>` and the new text to the conversation as a
|
|
276
|
+
- With neither, the text follows the mod's state. After a change to `mod.state`, cmod calls `prompt` again, and when the text differs from what Claude last got, it adds `# <name>` and the new text to the conversation as a `Note` Claude reads on its next call to the model, as `sessions.current.send` does ([session.md](session.md#send-to-a-conversation)). Several changes in a row give one note with the last text. A note waits until the conversation has started, so a state change before the first prompt changes only the first block.
|
|
275
277
|
- A `prompt` function that returns `undefined` or `''` adds nothing. One that throws adds nothing, and the error goes to Claude Code's debug log.
|
|
276
278
|
- `name` cannot be blank. `when` and `after` together throw: add a second `prompt` for the other trigger. Two prompts with one name in a mod throw.
|
|
277
279
|
- A `when` prompt runs on `prompt.submit` and adds its text to the event it passes down. In a test, `fire('prompt.submit', …)` returns that text in the answer's `context`. [testing.md](testing.md#test-a-prompt-with-when) shows how.
|
|
278
280
|
|
|
279
281
|
```ts
|
|
280
|
-
import { defineMod } from '../node_modules/@cmodjs/core/mod.js'
|
|
282
|
+
import { defineMod, fs, sessions } from '../node_modules/@cmodjs/core/mod.js'
|
|
281
283
|
import { prompt } from '../node_modules/@cmodjs/core/jobs/prompt.js'
|
|
282
284
|
|
|
283
285
|
export const conventions = defineMod({
|
|
@@ -287,7 +289,7 @@ export const conventions = defineMod({
|
|
|
287
289
|
mod.use(prompt({ name: 'Commit messages', prompt: 'Write commit messages in the imperative mood.' }))
|
|
288
290
|
mod.use(prompt({ name: 'Mode', prompt: (_input, mod) => `You are in ${mod.state.session.mode} mode.` }))
|
|
289
291
|
mod.use(prompt({ name: 'Migrations', when: /\bmigrat/i, prompt: 'Every migration has a down step.' }))
|
|
290
|
-
mod.use(prompt({ name: 'Schema', after: { write: 'db/schema.sql' }, prompt: async (
|
|
292
|
+
mod.use(prompt({ name: 'Schema', after: { write: 'db/schema.sql' }, prompt: async () => `The schema is now:\n${await fs.read(`${sessions.current.project}/db/schema.sql`)}` }))
|
|
291
293
|
},
|
|
292
294
|
})
|
|
293
295
|
```
|
|
@@ -297,9 +299,9 @@ export const conventions = defineMod({
|
|
|
297
299
|
It needs `"prompt": true`, because each cron starts a turn ([permissions.md](permissions.md)).
|
|
298
300
|
|
|
299
301
|
```ts
|
|
300
|
-
schedule<State>(entry:
|
|
302
|
+
schedule<State>(entry: ScheduledPrompt | ((state: State) => ScheduledPrompt | undefined)): Job<void, State>
|
|
301
303
|
|
|
302
|
-
type
|
|
304
|
+
type ScheduledPrompt = { readonly cron: string; readonly prompt: string }
|
|
303
305
|
```
|
|
304
306
|
|
|
305
307
|
`schedule` gives Claude a prompt on a schedule, through the cron jobs Claude Code keeps for `/loop`. `cron` is a standard 5-field cron in local time, such as `*/10 * * * *` for every 10 minutes. At each fire Claude Code shows one `Running scheduled task` row and Claude answers the prompt.
|
|
@@ -368,13 +370,13 @@ type Program = {
|
|
|
368
370
|
`program` runs a background program, such as a preview server, and hands the mod its address.
|
|
369
371
|
|
|
370
372
|
- `command` is the program on `PATH` and its arguments. `environment` sets variables for it. An empty `command` throws.
|
|
371
|
-
- The program's first line on standard output must be its address: `127.0.0.1:<port>`, or `unix:<absolute socket path>`. `ready()` then resolves `{ url: 'http://127.0.0.1:<port>' }`, or `{ url: 'http://localhost', socketPath }`. Pass both to `
|
|
373
|
+
- The program's first line on standard output must be its address: `127.0.0.1:<port>`, or `unix:<absolute socket path>`. `ready()` then resolves `{ url: 'http://127.0.0.1:<port>' }`, or `{ url: 'http://localhost', socketPath }`. Pass both to `http.fetch`, which reaches that address with no `network` permission while the program runs.
|
|
372
374
|
- In an interactive session the program starts with the mod. Otherwise it starts at the first `ready()`.
|
|
373
375
|
- When the program exits, cmod starts it again after 1, 2, 4, 8, and 16 seconds. After 5 failed tries, or a first line that is not an address, the state is `fatal`, `ready()` rejects, and cmod logs `<mod>: the <program> program stopped: <reason>`.
|
|
374
376
|
- A program that ships with the mod gets onto `PATH` through the `program` key of the mod's `package.json` ([install-steps.md](install-steps.md)). A program an install step fetched with `cmod download` is not on `PATH`, so `command` names it by its full path, `${mod.dataFolder}/bin/<program>`.
|
|
375
377
|
|
|
376
378
|
```ts
|
|
377
|
-
import { defineMod } from '../node_modules/@cmodjs/core/mod.js'
|
|
379
|
+
import { defineMod, http } from '../node_modules/@cmodjs/core/mod.js'
|
|
378
380
|
import { program } from '../node_modules/@cmodjs/core/jobs/program.js'
|
|
379
381
|
import { slashCommand } from '../node_modules/@cmodjs/core/jobs/slash-command.js'
|
|
380
382
|
|
|
@@ -386,9 +388,9 @@ export const preview = defineMod({
|
|
|
386
388
|
slashCommand({
|
|
387
389
|
name: 'preview-status',
|
|
388
390
|
description: 'Ask the preview server for its status',
|
|
389
|
-
reply: async (
|
|
391
|
+
reply: async () => {
|
|
390
392
|
const { url, socketPath } = await server.ready()
|
|
391
|
-
const response = await
|
|
393
|
+
const response = await http.fetch(`${url}/status`, socketPath === undefined ? {} : { socketPath })
|
|
392
394
|
return response.text
|
|
393
395
|
},
|
|
394
396
|
}),
|
|
@@ -405,96 +407,42 @@ A `Job` is a function. `mod.use` calls it with a `JobContext` and returns what i
|
|
|
405
407
|
type JobContext<State> = {
|
|
406
408
|
readonly mod: Mod<State>
|
|
407
409
|
announce(feature: string): void
|
|
408
|
-
reserveName(
|
|
409
|
-
readonly toolCalls: ToolCalls
|
|
410
|
+
reserveName(type: string, name: string, taken: string): Disposable
|
|
410
411
|
}
|
|
411
412
|
```
|
|
412
413
|
|
|
413
|
-
A job
|
|
414
|
-
|
|
415
|
-
- `mod.claude` holds Claude Code's own calls. [The claude members](#the-claude-members) lists each one. Each call checks the permission it needs, as `mod`'s own members do ([permissions.md](permissions.md)).
|
|
416
|
-
- `mod.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.
|
|
417
|
-
- `mod.claude.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.scroll`, `ui.close`, or `cmod.call`. The handler calls `next(e)` to pass the event on. cmod checks what it answers against the person's grants, part by part ([permissions.md](permissions.md#when-a-hook-answer-has-no-grant)). A job with a handler on `tool.check` or `classic.PermissionRequest` needs `registerPermissionCheck` in `hooks/register.ts` ([mod.md](mod.md)).
|
|
418
|
-
|
|
419
|
-
The context adds what only a job needs:
|
|
420
|
-
|
|
414
|
+
- `mod` is the mod that uses the job. A job builds on `mod.on`, `mod.every`, `mod.ui`, and the imports from `mod.js`, as `setup` does ([mod.md](mod.md)).
|
|
421
415
|
- `announce` adds a phrase to the list cmod logs when the mod first starts.
|
|
422
|
-
- `reserveName` throws `taken` when the mod already reserved that `
|
|
423
|
-
- `toolCalls.agentOf(toolUseId)` resolves `{ agentId?, agentType? }`, the subagent behind a call.
|
|
424
|
-
- `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.
|
|
416
|
+
- `reserveName` throws `taken` when the mod already reserved that `type` and `name`, so a job refuses a duplicate. `dispose()` on what it returns frees the name again.
|
|
425
417
|
|
|
426
|
-
|
|
427
|
-
import { defineMod, type Job } from '../node_modules/@cmodjs/core/mod.js'
|
|
418
|
+
A job that hands back a feature the mod turns off returns a `Disposable`, as `slashCommand` and `tool` do:
|
|
428
419
|
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
420
|
+
```ts
|
|
421
|
+
import { defineMod, sessions, Stop, ToolCall, type Disposable, type Job } from '../node_modules/@cmodjs/core/mod.js'
|
|
422
|
+
|
|
423
|
+
const editCounter = (tool: string): Job<Disposable & { readonly count: () => number }> => ({ mod, announce, reserveName }) => {
|
|
424
|
+
const reserved = reserveName('editCounter', tool, `${mod.name}: edits of ${tool} are already counted.`)
|
|
425
|
+
let edits = 0
|
|
426
|
+
const listening = sessions.current.on(ToolCall, { name: tool }, () => void (edits += 1))
|
|
427
|
+
announce(`a count of ${tool} calls`)
|
|
428
|
+
return {
|
|
429
|
+
count: () => edits,
|
|
430
|
+
dispose() {
|
|
431
|
+
listening.dispose()
|
|
432
|
+
reserved.dispose()
|
|
433
|
+
},
|
|
434
|
+
}
|
|
432
435
|
}
|
|
433
436
|
|
|
434
|
-
export const
|
|
435
|
-
name: '
|
|
437
|
+
export const counter = defineMod({
|
|
438
|
+
name: 'counter',
|
|
436
439
|
setup(mod) {
|
|
437
|
-
const
|
|
438
|
-
mod.on(
|
|
439
|
-
mod.ui.toast(`Turn ended at ${new Date(await started.now()).toISOString()}`)
|
|
440
|
-
})
|
|
440
|
+
const writes = mod.use(editCounter('Write'))
|
|
441
|
+
mod.on(Stop, () => mod.ui.toast(`${writes.count()} files written so far`))
|
|
441
442
|
},
|
|
442
443
|
})
|
|
443
444
|
```
|
|
444
445
|
|
|
445
|
-
### The claude members
|
|
446
|
-
|
|
447
|
-
`mod.js` exports the types `Claude`, `RoutedEvent`, `RoutedHook`, and `ToolCalls`, so a job of your own names them in its own helpers, such as `(claude: Claude, calls: ToolCalls): RoutedHook<'command.run'> => …`.
|
|
448
|
-
|
|
449
|
-
Each member calls the member of the same name in Claude Code's hooks API, `$`, with the same arguments. The `env` members read `$.env.get` instead. `mod.claude.on` adds a handler, as [Write a job of your own](#write-a-job-of-your-own) shows.
|
|
450
|
-
|
|
451
|
-
A member that does what a permission covers checks it first and rejects with the fix: `process` needs `run`, `http.fetch` needs `network`, `fs.write` outside the project and the data folder needs `files` or, for Claude Code's own settings files, `config`, `fs.read` of a transcript needs `conversation`, `session.messages` needs `conversation`, `session.append` and `prompt.submit` need `prompt`, `model.complete` needs `model`, `agent.spawn` needs `agents`, and `tool.call` needs `tools`, except `CronCreate`, which needs `prompt`, and `CronList` and `CronDelete`, which need nothing ([permissions.md](permissions.md)).
|
|
452
|
-
|
|
453
|
-
| Member | What it does |
|
|
454
|
-
| --- | --- |
|
|
455
|
-
| `plugin.name` | The plugin's name. |
|
|
456
|
-
| `plugin.root` | The plugin's own folder, absolute, where the files the mod ships live. |
|
|
457
|
-
| `ui.toast(text, options?)` | Shows `text` for a few seconds, 4000 ms unless `options.timeoutMs` says otherwise. |
|
|
458
|
-
| `ui.status(text)` | Pins `text` as the plugin's one status line under the prompt. `undefined` removes it. |
|
|
459
|
-
| `ui.log(text, options?)` | Adds a dim line to the transcript that Claude does not read, or with `{ to: 'debug' }` a line to the debug log only. |
|
|
460
|
-
| `ui.notice(toolUseId, text)` | Shows `text` as a line under the open permission dialog of that call. `undefined` removes it. |
|
|
461
|
-
| `ui.invalidate(event)` | Draws `ui.render` again, or drops the cached answers of `prompt.context` and the other cached events. |
|
|
462
|
-
| `ui.ask(question, options?)` | Asks the person in Claude Code's question dialog, and resolves the label they chose. It rejects when they dismiss it, and in a `-p` run. |
|
|
463
|
-
| `ui.open(pane)` | Opens a pane, `{ id, title?, focus?, closeOnEscape?, holdToasts?, rows?, columns? }`, and resolves `{ isPlaced }`. |
|
|
464
|
-
| `ui.close(pane)` | Closes the open pane with that `{ id }`. |
|
|
465
|
-
| `ui.panes()` | The plugin's open panes: each one's id, title, and whether it is shown, holds the keyboard, and is placed. |
|
|
466
|
-
| `ui.scroll(args)` | What `mod.ui.scroll` calls. |
|
|
467
|
-
| `ui.resolve(e)` | The element table of the surface a `ui.render` event draws on. |
|
|
468
|
-
| `process.run(argv, init?)` | Runs a program to its end. `mod.process.run` is built on it. |
|
|
469
|
-
| `process.spawn(request)` | Starts a program and streams its output. `mod.process.spawn` is built on it. |
|
|
470
|
-
| `fs.read`, `fs.write`, `fs.list`, `fs.exists`, `fs.stat` | What `mod.fs` calls. A relative path is under the session's working folder. |
|
|
471
|
-
| `http.fetch(url, init?)` | What `mod.http.fetch` calls. |
|
|
472
|
-
| `settings.read(args?)` | Reads Claude Code's own `settings.json` files. Without `{ source }` it answers every source merged. `user`, `project`, `local`, `flag`, and `policy` each answer one. |
|
|
473
|
-
| `config.list()` | Lists the rows `/config` shows, such as `<mod>.<option>`. A mod sets an option through `cmod option <mod> <key> <value>`, as `/mods` does. |
|
|
474
|
-
| `store.get`, `store.set`, `store.delete`, `store.keys` | The plugin's key-value store, where cmod also keeps `mod.state`. |
|
|
475
|
-
| `clock.now()` | Resolves milliseconds since the epoch. |
|
|
476
|
-
| `clock.after(ms, fn)` | Calls `fn` once after `ms` milliseconds, and returns `{ cancel() }`. |
|
|
477
|
-
| `clock.every(ms, fn)` | Calls `fn` every `ms` milliseconds, at least 1, and returns `{ cancel() }`. |
|
|
478
|
-
| `session.id()` | The session's id, the transcript file's name. |
|
|
479
|
-
| `session.root()` | The session's project root. A shell `cd` does not move it. |
|
|
480
|
-
| `session.cwd()` | The folder the session runs in. |
|
|
481
|
-
| `session.model()` | The main conversation's model, as `/model` shows it. |
|
|
482
|
-
| `session.usage()` | When the session began, the context window's fill, the rate-limit windows, and the cost, as the status line has them. |
|
|
483
|
-
| `session.surfaces()` | Every surface the session draws on, `terminal` first. Empty in a plain `-p` run. |
|
|
484
|
-
| `session.messages(args?)` | What `mod.session.messages` calls. |
|
|
485
|
-
| `session.append({ message, agentId? })` | Adds a message Claude reads to the conversation, or to a subagent's, and resolves `{ message, uuid }` or `{ deny }`. `mod.session.append` is built on it. |
|
|
486
|
-
| `prompt.submit({ text, asUser? })` | Sends a prompt Claude answers as a turn once the session is idle. `mod.session.submit` is built on it. |
|
|
487
|
-
| `model.complete(request, options?)` | What `mod.model.complete` calls. |
|
|
488
|
-
| `command.register(spec)` | Adds a slash command. `slashCommand` is built on it. |
|
|
489
|
-
| `tool.register(spec)` | Adds a tool. `tool` is built on it. |
|
|
490
|
-
| `tool.call({ tool, ...input })` | Calls a tool the way Claude does, through the permission check, and resolves `{ result, text, isError? }` or `{ deny }`. `schedule` is built on it, calling `CronCreate` and `CronDelete`. |
|
|
491
|
-
| `agent.list()` | The session's subagents and teammates so far. |
|
|
492
|
-
| `agent.spawn(args)` | What `mod.agent.spawn` calls. |
|
|
493
|
-
| `env.home()` | The `HOME` variable, or `undefined` when it is unset. |
|
|
494
|
-
| `env.dataHome()` | The `XDG_DATA_HOME` variable, or `undefined`. |
|
|
495
|
-
| `env.configHome()` | The `CLAUDE_CONFIG_DIR` variable, or `undefined`. |
|
|
496
|
-
| `cmod.call({ to, method, input })` | Calls `method` of the mod named `to`. `mod.dependencies` is built on it. |
|
|
497
|
-
|
|
498
446
|
### The building blocks
|
|
499
447
|
|
|
500
448
|
The built-in jobs are written from these building blocks, which a job of your own may import:
|
|
@@ -502,9 +450,9 @@ The built-in jobs are written from these building blocks, which a job of your ow
|
|
|
502
450
|
| Import | Export | What it does |
|
|
503
451
|
| --- | --- | --- |
|
|
504
452
|
| `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. |
|
|
505
|
-
| `jobs/context.js` | `
|
|
506
|
-
| `jobs/context.js` | `
|
|
507
|
-
| `jobs/context.js` | `workspaceReader(job)` | A function that reads the session's
|
|
453
|
+
| `jobs/context.js` | `inFolder(call, folder, workspace)` | Adds the `folder` a rule's `when` gets: the work tree that holds the call's path or folder, or the folder itself. |
|
|
454
|
+
| `jobs/context.js` | `withinDeadline(mod, deadline, subject, run)` | Runs `run`, and rejects with `<subject> passed the <n> s deadline of <job>` when it has not answered by `deadline`, `{ ms, job? }`. |
|
|
455
|
+
| `jobs/context.js` | `workspaceReader(job)` | A function that reads the session's project, working folder, home, file access, and project scope. |
|
|
508
456
|
| `jobs/permissions/decide-permission.js` | `decidePermission`, `stricterVerdict`, and the types `Rule`, `PermissionRules`, `Decision`, `Verdict` | Decides a call against permission rules. |
|
|
509
457
|
| `jobs/permissions/find-project-scope.js` | `findProjectScope`, and the types `ProjectScope`, `Workspace` | Finds the repository a project plugin belongs to. |
|
|
510
458
|
|