@cmodjs/core 1.5.1 → 2.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 +5 -5
- package/docs/index.md +12 -11
- package/docs/jobs.md +12 -16
- package/docs/mod.md +117 -39
- package/docs/permissions.md +17 -21
- package/docs/session.md +171 -82
- package/docs/state.md +5 -5
- package/docs/testing.md +57 -41
- package/docs/ui.md +9 -9
- package/jobs/permissions/decide-permission.d.ts.map +1 -1
- package/jobs/permissions/decide-permission.js +1 -1
- package/jobs/prompt.d.ts +1 -3
- package/jobs/prompt.d.ts.map +1 -1
- package/jobs/prompt.js +2 -21
- package/jobs/slash-command.d.ts +1 -1
- package/jobs/slash-command.d.ts.map +1 -1
- package/jobs/slash-command.js +1 -1
- package/jobs/status-line.d.ts +0 -1
- package/jobs/status-line.d.ts.map +1 -1
- package/jobs/status-line.js +3 -4
- package/mod.d.ts +53 -105
- package/mod.d.ts.map +1 -1
- package/mod.js +18 -80
- package/package.json +1 -1
- package/records.d.ts +3 -1
- package/records.d.ts.map +1 -1
- package/records.js +14 -5
- package/register.d.ts.map +1 -1
- package/register.js +3 -1
- package/runtime/before.d.ts +5 -1
- package/runtime/before.d.ts.map +1 -1
- package/runtime/before.js +81 -39
- package/runtime/claude.d.ts +6 -1
- package/runtime/claude.d.ts.map +1 -1
- package/runtime/conversations.d.ts.map +1 -1
- package/runtime/conversations.js +45 -26
- package/runtime/deadline.d.ts +17 -0
- package/runtime/deadline.d.ts.map +1 -1
- package/runtime/deadline.js +34 -0
- package/runtime/dependencies.d.ts +2 -0
- package/runtime/dependencies.d.ts.map +1 -1
- package/runtime/dependencies.js +12 -0
- package/runtime/grants.d.ts +1 -1
- package/runtime/grants.d.ts.map +1 -1
- package/runtime/grants.js +19 -12
- package/runtime/hooks.d.ts +28 -11
- package/runtime/hooks.d.ts.map +1 -1
- package/runtime/hooks.js +20 -123
- package/runtime/lifecycle.d.ts +2 -1
- package/runtime/lifecycle.d.ts.map +1 -1
- package/runtime/lifecycle.js +67 -50
- package/runtime/sessions.d.ts +31 -5
- package/runtime/sessions.d.ts.map +1 -1
- package/runtime/sessions.js +342 -81
- package/runtime/tool-calls.d.ts +0 -1
- package/runtime/tool-calls.d.ts.map +1 -1
- package/runtime/tool-calls.js +0 -19
- package/session/context/turn/message/instruction-file.d.ts +9 -0
- package/session/context/turn/message/instruction-file.d.ts.map +1 -0
- package/session/context/turn/message/instruction-file.js +4 -0
- package/session/context/turn/message/message.d.ts +5 -4
- package/session/context/turn/message/message.d.ts.map +1 -1
- package/session/context/turn/message/message.js +7 -6
- package/session/context/turn/message/sender.d.ts +2 -1
- package/session/context/turn/message/sender.d.ts.map +1 -1
- package/session/context/turn/message/sender.js +4 -1
- package/session/context/turn/message/tool-call.d.ts +5 -0
- package/session/context/turn/message/tool-call.d.ts.map +1 -1
- package/session/context/turn/message/tool-result.d.ts +6 -1
- package/session/context/turn/message/tool-result.d.ts.map +1 -1
- package/session/context/turn/turn.d.ts +7 -0
- package/session/context/turn/turn.d.ts.map +1 -1
- package/session/conversation.d.ts +10 -3
- package/session/conversation.d.ts.map +1 -1
- package/session/conversation.js +14 -5
- package/session/goal/goal.d.ts +1 -1
- package/session/goal/goal.d.ts.map +1 -1
- package/session/goal/task.d.ts +2 -2
- package/session/goal/task.d.ts.map +1 -1
- package/session/session.d.ts +361 -26
- package/session/session.d.ts.map +1 -1
- package/session/session.js +40 -0
- package/src/jobs/permissions/decide-permission.ts +2 -1
- package/src/jobs/prompt.ts +3 -20
- package/src/jobs/slash-command.ts +2 -2
- package/src/jobs/status-line.ts +4 -7
- package/src/mod.ts +52 -147
- package/src/records.ts +15 -5
- package/src/register.ts +3 -1
- package/src/runtime/before.ts +90 -37
- package/src/runtime/claude.ts +5 -1
- package/src/runtime/conversations.ts +41 -25
- package/src/runtime/deadline.ts +38 -0
- package/src/runtime/dependencies.ts +12 -0
- package/src/runtime/grants.ts +15 -10
- package/src/runtime/hooks.ts +48 -116
- package/src/runtime/lifecycle.ts +66 -51
- package/src/runtime/sessions.ts +366 -90
- package/src/runtime/tool-calls.ts +0 -19
- package/src/session/context/turn/message/instruction-file.ts +13 -0
- package/src/session/context/turn/message/message.ts +11 -9
- package/src/session/context/turn/message/sender.ts +5 -2
- package/src/session/context/turn/message/tool-call.ts +3 -0
- package/src/session/context/turn/message/tool-result.ts +6 -1
- package/src/session/context/turn/turn.ts +4 -0
- package/src/session/conversation.ts +21 -8
- package/src/session/goal/goal.ts +1 -1
- package/src/session/goal/task.ts +2 -2
- package/src/session/session.ts +227 -24
- package/src/testing/claude-code.ts +23 -0
- package/src/testing/fake-claude.ts +5 -1
- package/src/testing/fake-sessions.ts +38 -24
- package/src/testing.ts +84 -16
- package/testing/claude-code.d.ts +12 -0
- package/testing/claude-code.d.ts.map +1 -0
- package/testing/claude-code.js +10 -0
- package/testing/fake-claude.d.ts +4 -0
- package/testing/fake-claude.d.ts.map +1 -1
- package/testing/fake-claude.js +4 -1
- package/testing/fake-sessions.d.ts +8 -6
- package/testing/fake-sessions.d.ts.map +1 -1
- package/testing/fake-sessions.js +35 -20
- package/testing.d.ts +38 -9
- package/testing.d.ts.map +1 -1
- package/testing.js +54 -5
- package/docs/hooks.md +0 -259
- package/session/context/turn/message/document.d.ts +0 -7
- package/session/context/turn/message/document.d.ts.map +0 -1
- package/session/context/turn/message/document.js +0 -1
- package/src/session/context/turn/message/document.ts +0 -8
package/docs/commands.md
CHANGED
|
@@ -12,7 +12,7 @@ Each command below restates what `cmod <command> --help` prints, for a mod autho
|
|
|
12
12
|
cmod new <name> [--project]
|
|
13
13
|
```
|
|
14
14
|
|
|
15
|
-
Creates a mod in `./<name>`: a `defineMod` with one
|
|
15
|
+
Creates a mod in `./<name>`: a `defineMod` with one listener, one pane, and one render of the band above the prompt, a Skill, a test, and a `CHANGELOG.md` ([mod.md](mod.md#release-notes)). Then installs its packages.
|
|
16
16
|
|
|
17
17
|
- `<name>` is lowercase letters, digits, and `-`, starting with a letter.
|
|
18
18
|
- `--project` creates a project-scope plugin in `./.claude/skills/<name>/` of the repository in the current folder, which loads for everyone who trusts the repository. Run it in the repository's root.
|
|
@@ -179,7 +179,7 @@ Lists every plugin Claude Code has installed or linked, with its version. A mod
|
|
|
179
179
|
|
|
180
180
|
```text
|
|
181
181
|
cmod history sessions [--all] [--since 2d] [--running] [--project <folder>] [--json]
|
|
182
|
-
cmod history search <text> [--all] [--since 2d] [--running] [--project <folder>] [--session <id> [--task <id>] [--turn <id>] [--context <n>] [--
|
|
182
|
+
cmod history search <text> [--all] [--since 2d] [--running] [--project <folder>] [--session <id> [--task <id>] [--turn <id>] [--context <n>] [--agent]] [--limit <n>] [--before <session>:<id>] [--json]
|
|
183
183
|
cmod history messages [<session>] [--task <id>] [--turn <id>] [--after <id>] [--last <n>] [--json]
|
|
184
184
|
cmod history turns [<session>] [--task <id>] [--context <n>] [--last <n>] [--json]
|
|
185
185
|
cmod history contexts [<session>] [--task <id>] [--json]
|
|
@@ -194,7 +194,7 @@ cmod history index
|
|
|
194
194
|
Reads every conversation Claude Code saved, as the sessions, contexts, turns, tasks, and messages `sessions` answers a mod ([session.md](session.md)), so a person or an agent reads them in a terminal without parsing a transcript.
|
|
195
195
|
|
|
196
196
|
- `sessions` lists the conversations of the current folder's project, or `--project`'s, newest first, each with its time, id, and title. `--all` lists every project's, `--since` keeps those active in the last `30m`, `6h`, `2d`, or `1w`, or since a date such as `2026-10-01`, and `--running` keeps those open in Claude Code now.
|
|
197
|
-
- `search` prints the newest 50 messages whose text or tool input contains the text, at least 3 characters, any case, each with its time and session. Each tool input value is searched in its first 1,024 characters, and tool results are not searched. `--all`, `--since`, and `--running` scope it as they scope `sessions`, `--limit` changes the 50, and `--before <session>:<id>` prints the page after the message it names, the last one of the page before. `--session` searches one conversation, and inside it `--task` searches a task and the tasks it started, `--turn` one turn, `--context` one context, and `--
|
|
197
|
+
- `search` prints the newest 50 messages whose text or tool input contains the text, at least 3 characters, any case, each with its time and session. Each tool input value is searched in its first 1,024 characters, and tool results are not searched. `--all`, `--since`, and `--running` scope it as they scope `sessions`, `--limit` changes the 50, and `--before <session>:<id>` prints the page after the message it names, the last one of the page before. `--session` searches one conversation, and inside it `--task` searches a task and the tasks it started, `--turn` one turn, `--context` one context, and `--agent` keeps the main agent's messages, or with `--task` that task's own, without the tasks it started.
|
|
198
198
|
- `messages` prints one conversation's messages in order: the session named, or the one the command runs in when Claude Code runs it. `--task` reads a task's messages instead, `--turn` keeps one turn's, `--after` starts after a message id, and `--last` keeps that many from the end.
|
|
199
199
|
- `turns` prints the conversation's turns, each opened by a prompt and closed when Claude finishes. `--context` keeps one context's, and `--last` keeps that many from the end.
|
|
200
200
|
- `contexts` prints the conversation's contexts, a new one after each compaction.
|
|
@@ -205,7 +205,7 @@ Reads every conversation Claude Code saved, as the sessions, contexts, turns, ta
|
|
|
205
205
|
- `state` prints what a running conversation resumes from.
|
|
206
206
|
- `follow` prints each message, turn, context, task, and goal change of the sessions named as Claude Code saves it, until stopped with Ctrl+C. `--running` follows every running session of the project too, and each one that starts later. The cmod plugin runs it to hear other running sessions.
|
|
207
207
|
- `--json` prints JSON instead of one line each.
|
|
208
|
-
- `index` reads every saved conversation into the index at `~/.local/share/cmod/history-
|
|
208
|
+
- `index` reads every saved conversation into the index at `~/.local/share/cmod/history-6.sqlite`, which holds where each message is in its transcript, not a copy of it, and each conversation's turns, contexts, and tasks. While a mod other than cmod holds the `conversation` permission, the cmod plugin runs it in the background at each session's start, and a second run while one is running exits at once. The index forgets a conversation whose transcript is deleted.
|
|
209
209
|
|
|
210
210
|
## Run in an install script
|
|
211
211
|
|
|
@@ -251,7 +251,7 @@ Turns one permission of a set-up mod on or off, such as `cmod permission ci-watc
|
|
|
251
251
|
cmod settings <mod> <user|project|local> <json> [--project <folder>]
|
|
252
252
|
```
|
|
253
253
|
|
|
254
|
-
Changes Claude Code settings for a set-up mod, as its `settings.user.update`, `settings.project.update`, and `settings.local.update` do ([mod.md](mod.md#settings-claude-code-s-settings-files)), such as `cmod settings
|
|
254
|
+
Changes Claude Code settings for a set-up mod, as its `settings.user.update`, `settings.project.update`, and `settings.local.update` do ([mod.md](mod.md#settings-claude-code-s-settings-files)), such as `cmod settings oneclaude user '{"env":{"ANTHROPIC_BASE_URL":"http://127.0.0.1:8317"}}'`. It merges the JSON object into the user, project, or local settings as a JSON Merge Patch: an object merges into the object already there, `null` removes the key, and an array or any other value replaces it. The project and local settings are those of `--project`, the current folder when it is left out, and `mcpServers` in the project settings goes to `.mcp.json`. Each value needs the mod's `config` permission for its key path or a path above it. cmod keeps every other key and the file's formatting, and `cmod teardown` takes out each value the mod set that is still the same, then each object that leaves empty.
|
|
255
255
|
|
|
256
256
|
### cmod rules
|
|
257
257
|
|
package/docs/index.md
CHANGED
|
@@ -16,7 +16,7 @@ my-mod/
|
|
|
16
16
|
├── .oxlintrc.json leaves .claude-plugin/types/ out of the lint
|
|
17
17
|
├── hooks/hooks.json names hooks/register.ts as the hooks module
|
|
18
18
|
├── hooks/register.ts registerMod(addHook, myMod, options), and nothing else
|
|
19
|
-
├── src/mod.tsx defineMod: the state, the setup, the
|
|
19
|
+
├── src/mod.tsx defineMod: the state, the setup, the listeners, the panes
|
|
20
20
|
├── src/state.ts the starting state
|
|
21
21
|
├── src/panes/prompts.tsx one definePane per file
|
|
22
22
|
├── src/components/prompt-count.tsx what the panes and renders draw with
|
|
@@ -57,7 +57,7 @@ A program the mod needs lives as source in `cli/`. `cmod publish` builds it and
|
|
|
57
57
|
| Question | File |
|
|
58
58
|
| --- | --- |
|
|
59
59
|
| How do I define a mod, and what can `mod` call? | [mod.md](mod.md) |
|
|
60
|
-
| How does a mod watch something outside Claude Code, tell Claude about it, or ask a model? | [mod.md](mod.md#mod-every-and-mod-after), [session.md](session.md#
|
|
60
|
+
| How does a mod watch something outside Claude Code, tell Claude about it, or ask a model? | [mod.md](mod.md#mod-every-and-mod-after), [session.md](session.md#act-on-a-session) |
|
|
61
61
|
| How does a mod give Claude a prompt on a schedule, like `/loop`? | [jobs.md](jobs.md#schedule) |
|
|
62
62
|
| How do I keep values across prompts, sessions, and projects? | [state.md](state.md) |
|
|
63
63
|
| How does a person set a token, a URL, or a setting for my mod, for themselves or their team? | [options.md](options.md) |
|
|
@@ -66,7 +66,8 @@ A program the mod needs lives as source in `cli/`. `cmod publish` builds it and
|
|
|
66
66
|
| How does the person finish a setup step only they can do, such as signing in? | [install-steps.md](install-steps.md#steps-in-claude-code) |
|
|
67
67
|
| How do I add my mod's own page to `/mods`, or change Claude Code's settings? | [mod.md](mod.md#mod-settings), [mod.md](mod.md#settings-claude-code-s-settings-files) |
|
|
68
68
|
| How does my mod read and follow this conversation, its tasks, or earlier ones, search them, or start a task? | [session.md](session.md) |
|
|
69
|
-
| How
|
|
69
|
+
| How does my mod hear a prompt, a tool call, a permission dialog, or a model switch, and change or refuse it? | [session.md](session.md#listen-to-a-record), [session.md](session.md#the-session-s-own-events) |
|
|
70
|
+
| How does my mod hear, change, or run any slash command, a built-in such as `/compact` included? | [mod.md](mod.md#commands-slash-commands) |
|
|
70
71
|
| How do I draw a pane, change a row Claude Code draws, or ask the person something? | [ui.md](ui.md) |
|
|
71
72
|
| How do I add a slash command, a tool, a permission rule, a check, a prompt, a schedule, a status line, or a background program? | [jobs.md](jobs.md) |
|
|
72
73
|
| How does one mod call another? | [dependencies.md](dependencies.md) |
|
|
@@ -81,20 +82,20 @@ A mod imports from the files below, and every other file in this package is inte
|
|
|
81
82
|
| Import | Exports | Docs |
|
|
82
83
|
| --- | --- | --- |
|
|
83
84
|
| `mod.js` | `defineMod`, `ModDefinition`, `Mod`, `Disposable`, `Permissions`, `Allowed`, `JobContext`, `Job`, `PaneHandle`, `longestMs`, `messageOf` | [mod.md](mod.md), [jobs.md](jobs.md) |
|
|
84
|
-
| `mod.js` | `settings`, `SettingsFile`, `model`, `fs`, `Entry`, `Stat`, `FileMatch`, `metadata`, `Metadata`, `process`, `http`, `Imports` | [mod.md](mod.md) |
|
|
85
|
-
| `mod.js` | `sessions`, `Sessions`, `Session`, `
|
|
86
|
-
| `mod.js` | `
|
|
87
|
-
| `mod.js` | `
|
|
88
|
-
| `mod.js` | `
|
|
85
|
+
| `mod.js` | `settings`, `SettingsFile`, `SettingsFileChange`, `SettingsFileChangeListener`, `config`, `ConfigRow`, `commands`, `CommandInfo`, `CommandRun`, `CommandRunAnswer`, `CommandRunResult`, `model`, `fs`, `Entry`, `Stat`, `FileMatch`, `FileChange`, `metadata`, `Metadata`, `process`, `http`, `Imports` | [mod.md](mod.md) |
|
|
86
|
+
| `mod.js` | `sessions`, `Sessions`, `Session`, `CurrentSession`, `TaskOwner`, `RecordPart`, `OnMethods`, `BeforeMethods`, `Listener`, `ListenerTime`, `ReadOptions`, `AnswerOptions`, `Refusal`, `SearchOptions`, `SessionScope`, `SessionSend`, `TaskStartArgs`, `ToolCallArgs`, `Usage`, `Surface`, `Matcher` | [session.md](session.md) |
|
|
87
|
+
| `mod.js` | `Placement`, `ProjectSetup`, `SessionStartAsked`, `SessionStarted`, `SessionEnded`, `ModelChange`, `CwdChange`, `DirectoryAdd`, `Worktree`, `Notification`, `Elicitation`, `ElicitationResponse`, `McpServer`, `PermissionUpdate`, `PermissionCheck`, `PermissionRequest`, `PermissionDenial`, `BatchedCall`, `ToolBatch`, `Todo`, `ReplyDisplay` | [session.md](session.md#the-session-s-own-events) |
|
|
88
|
+
| `mod.js` | `Message`, `MessageType`, `Prompt`, `Note`, `Reply`, `ToolCall`, `ToolResult`, `Skill`, `InstructionFile`, `Notice`, `Sender`, `SenderType`, `Person`, `Mod`, `Schedule`, `Channel`, `Sdk`, `Engine` | [session.md](session.md#message) |
|
|
89
|
+
| `mod.js` | `Context`, `Turn`, `Goal`, `Task` | [session.md](session.md#turn-context-goal-and-task) |
|
|
89
90
|
| `mod.js` | `Toast`, `ToastId`, `ToastType`, `ToastOptions` | [ui.md](ui.md#toasts) |
|
|
90
91
|
| `mod.js` | `Place`, `PlaceProps`, `ToolGroup`, `ToolProgress`, `CommandOutput`, `AskUserQuestion`, `Spinner`, `TurnDuration`, `SessionMode`, `PromptHint`, `AbovePrompt` | [ui.md](ui.md#places-change-a-row-claude-code-draws) |
|
|
91
92
|
| `mod.js` | `notInstalled` | [dependencies.md](dependencies.md) |
|
|
92
93
|
| `installer.js` | `defineStep`, `Step` | [install-steps.md](install-steps.md#steps-in-claude-code) |
|
|
93
94
|
| `register.js` | `registerMod`, `registerPermissionCheck`, `registeredMod` | [mod.md](mod.md) |
|
|
94
95
|
| `options.js` | `option`, `Option`, `Options`, `OptionValues`, `OptionValue`, `OptionType`, `UserConfigField`, `userConfigOf` | [options.md](options.md) |
|
|
95
|
-
| `shell.js` | `parseShell`, `ParsedShell`, `ShellCommand` | [
|
|
96
|
-
| `path.js` | `resolve`, `join`, `dirname`, `basename`, `extname`, `relative`, `isAbsolute`, `normalize` | [
|
|
97
|
-
| `testing.js` | `testMod`, `TestOptions`, `
|
|
96
|
+
| `shell.js` | `parseShell`, `ParsedShell`, `ShellCommand` | [mod.md](mod.md#shell-js-and-path-js-read-a-command-line) |
|
|
97
|
+
| `path.js` | `resolve`, `join`, `dirname`, `basename`, `extname`, `relative`, `isAbsolute`, `normalize` | [mod.md](mod.md#shell-js-and-path-js-read-a-command-line) |
|
|
98
|
+
| `testing.js` | `testMod`, `TestOptions`, `TestedMod`, `Raise`, `Fakes`, `Shown`, `ShownToast`, `TestCall`, `FakeSession`, `MessageDraft`, `RoutedEvent` | [testing.md](testing.md) |
|
|
98
99
|
| `ui/define-pane.js` | `definePane`, `Pane` | [ui.md](ui.md) |
|
|
99
100
|
| `ui/elements.js` | `Box`, `Text`, `Button`, `Link`, `Code`, `Markdown`, `Input`, `Select`, `Image`, `drawWith` | [ui.md](ui.md) |
|
|
100
101
|
| `ui/components.js` | `Tabs`, `Tab`, `Split`, `Panel`, `Toggle`, `Tooltip`, `Help`, `Dialog`, `Pagination`, `Progress` | [ui.md](ui.md) |
|
package/docs/jobs.md
CHANGED
|
@@ -258,25 +258,22 @@ It needs `"prompt": true`. Without the grant it adds nothing and logs once which
|
|
|
258
258
|
```ts
|
|
259
259
|
prompt<State>(options: {
|
|
260
260
|
name: string
|
|
261
|
-
prompt: string | ((input: {
|
|
262
|
-
when?: RegExp | ((userPrompt: string) => boolean)
|
|
261
|
+
prompt: string | ((input: { call?: Call & { folder: string } }, mod: Mod<State>) => string | undefined | Promise<string | undefined>)
|
|
263
262
|
after?: Target | Target[]
|
|
264
263
|
}): Job<void, State>
|
|
265
264
|
```
|
|
266
265
|
|
|
267
|
-
`prompt` adds text that Claude reads. `
|
|
266
|
+
`prompt` adds text that Claude reads. `after` picks when:
|
|
268
267
|
|
|
269
268
|
| Options | When Claude reads the text |
|
|
270
269
|
| --- | --- |
|
|
271
|
-
|
|
|
272
|
-
| `
|
|
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. |
|
|
270
|
+
| no `after` | 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. |
|
|
271
|
+
| `after` | After each call that matches a target and succeeds, with the call's result, under the heading `# <name>`. `input.call` is the matched call, with its `folder` as `permissions` gives it. |
|
|
274
272
|
|
|
275
|
-
-
|
|
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.
|
|
273
|
+
- Without `after`, 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#act-on-a-session)). 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.
|
|
277
274
|
- A `prompt` function that returns `undefined` or `''` adds nothing. One that throws adds nothing, and the error goes to Claude Code's debug log.
|
|
278
|
-
- `name` cannot be blank.
|
|
279
|
-
-
|
|
275
|
+
- `name` cannot be blank. Two prompts with one name in a mod throw.
|
|
276
|
+
- Text for the prompts that match a pattern is a `beforePrompt` note: `sessions.current.beforePrompt({ text: /\bmigrat/i }, () => ({ note: 'Every migration has a down step.' }))` ([session.md](session.md#change-what-happens)).
|
|
280
277
|
|
|
281
278
|
```ts
|
|
282
279
|
import { defineMod, fs, sessions } from '../node_modules/@cmodjs/core/mod.js'
|
|
@@ -288,7 +285,6 @@ export const conventions = defineMod({
|
|
|
288
285
|
setup(mod) {
|
|
289
286
|
mod.use(prompt({ name: 'Commit messages', prompt: 'Write commit messages in the imperative mood.' }))
|
|
290
287
|
mod.use(prompt({ name: 'Mode', prompt: (_input, mod) => `You are in ${mod.state.session.mode} mode.` }))
|
|
291
|
-
mod.use(prompt({ name: 'Migrations', when: /\bmigrat/i, prompt: 'Every migration has a down step.' }))
|
|
292
288
|
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`)}` }))
|
|
293
289
|
},
|
|
294
290
|
})
|
|
@@ -411,19 +407,19 @@ type JobContext<State> = {
|
|
|
411
407
|
}
|
|
412
408
|
```
|
|
413
409
|
|
|
414
|
-
- `mod` is the mod that uses the job. A job builds on `mod.
|
|
410
|
+
- `mod` is the mod that uses the job. A job builds on `mod.every`, `mod.ui`, and the imports from `mod.js`, such as the methods of `sessions`, as `setup` does ([mod.md](mod.md)).
|
|
415
411
|
- `announce` adds a phrase to the list cmod logs when the mod first starts.
|
|
416
412
|
- `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.
|
|
417
413
|
|
|
418
414
|
A job that hands back a feature the mod turns off returns a `Disposable`, as `slashCommand` and `tool` do:
|
|
419
415
|
|
|
420
416
|
```ts
|
|
421
|
-
import { defineMod, sessions,
|
|
417
|
+
import { defineMod, sessions, type Disposable, type Job } from '../node_modules/@cmodjs/core/mod.js'
|
|
422
418
|
|
|
423
419
|
const editCounter = (tool: string): Job<Disposable & { readonly count: () => number }> => ({ mod, announce, reserveName }) => {
|
|
424
420
|
const reserved = reserveName('editCounter', tool, `${mod.name}: edits of ${tool} are already counted.`)
|
|
425
421
|
let edits = 0
|
|
426
|
-
const listening = sessions.current.
|
|
422
|
+
const listening = sessions.current.onToolCall({ name: tool }, () => void (edits += 1))
|
|
427
423
|
announce(`a count of ${tool} calls`)
|
|
428
424
|
return {
|
|
429
425
|
count: () => edits,
|
|
@@ -438,7 +434,7 @@ export const counter = defineMod({
|
|
|
438
434
|
name: 'counter',
|
|
439
435
|
setup(mod) {
|
|
440
436
|
const writes = mod.use(editCounter('Write'))
|
|
441
|
-
|
|
437
|
+
sessions.current.onTurnEnd(() => mod.ui.toast(`${writes.count()} files written so far`))
|
|
442
438
|
},
|
|
443
439
|
})
|
|
444
440
|
```
|
|
@@ -460,4 +456,4 @@ A job of your own matches calls against targets through the `permissions`, `chec
|
|
|
460
456
|
|
|
461
457
|
## Test a job
|
|
462
458
|
|
|
463
|
-
`tested.type('/todo milk')` runs a slash command. `tested.callTool('open_ticket', { title: 'Crash' })` runs the permission rules, then the tool. `tested.fire('tool.check', …)` asks the permission rules about any call. `tested.
|
|
459
|
+
`tested.type('/todo milk')` runs a slash command. `tested.callTool('open_ticket', { title: 'Crash' })` runs the permission rules, then the tool. `tested.fire('tool.check', …)` asks the permission rules about any call. `tested.shown.notes` holds the notes a `prompt` sent after a state change. [testing.md](testing.md) covers each.
|
package/docs/mod.md
CHANGED
|
@@ -24,17 +24,17 @@ type ModDefinition<State, Name, Options> = {
|
|
|
24
24
|
- `options` declares what a person sets, such as a token, and types `mod.options`. [options.md](options.md) explains them.
|
|
25
25
|
- `api` holds the methods other mods call. [dependencies.md](dependencies.md) explains it.
|
|
26
26
|
- `installer` lists the steps the person finishes in Claude Code after the permissions and the options, such as signing in. [install-steps.md](install-steps.md#steps-in-claude-code) explains them.
|
|
27
|
-
- `setup` runs once when the mod starts. It adds
|
|
27
|
+
- `setup` runs once when the mod starts. It adds listeners, panes, renders, and jobs. It may be `async`.
|
|
28
28
|
|
|
29
29
|
```tsx
|
|
30
|
-
import { defineMod,
|
|
30
|
+
import { defineMod, sessions } from '../node_modules/@cmodjs/core/mod.js'
|
|
31
31
|
|
|
32
32
|
export const greeter = defineMod({
|
|
33
33
|
name: 'greeter',
|
|
34
34
|
state: { session: { isGreeted: false } },
|
|
35
35
|
|
|
36
36
|
setup(mod) {
|
|
37
|
-
|
|
37
|
+
sessions.current.onPrompt(() => {
|
|
38
38
|
if (mod.state.session.isGreeted) return
|
|
39
39
|
mod.state.session.isGreeted = true
|
|
40
40
|
mod.ui.toast(`Hello from ${mod.name}`)
|
|
@@ -47,7 +47,7 @@ export const greeter = defineMod({
|
|
|
47
47
|
|
|
48
48
|
Claude Code loads the mod at the start of each session. cmod then checks that the mod is set up. A mod the person has not accepted yet opens the installer pane, `Install <name>`, which lists its permissions and the changes it makes to the computer, and waits for Accept ([install-steps.md](install-steps.md)). An option with no default and no value is asked on the next page of the same pane. Then cmod loads the saved state, runs `setup`, marks as open each of the mod's panes that Claude Code already shows, and opens the pane again for each `installer` step the mod declares that is not done.
|
|
49
49
|
|
|
50
|
-
The first time a mod starts on a machine, cmod shows the toast `Installed` under its name. Opened, it lists the mod's `plugin.json` `description`, what setup added, such as `Adds the my-mod pane, a render of AbovePrompt and
|
|
50
|
+
The first time a mod starts on a machine, cmod shows the toast `Installed` under its name. Opened, it lists the mod's `plugin.json` `description`, what setup added, such as `Adds the my-mod pane, a render of AbovePrompt and /deploy.`, what its install step set up, and each permission the person allowed, with a Settings button for `/mods <name>`. The first time a new version starts, the toast is `Updated to <version>`: the first three changes of that version's `CHANGELOG.md` section, the version the machine ran before, and a Changelog button ([Release notes](#release-notes)). A mod with no changelog lists what it adds now and no longer adds instead. A new session, `/reload-plugins`, and a version the machine already ran show no toast.
|
|
51
51
|
|
|
52
52
|
When a step fails, or the person declines the install, the mod stays stopped. Once another session sets it up, the mod starts at the next prompt of every session it stopped in, with no `/reload-plugins`.
|
|
53
53
|
|
|
@@ -59,7 +59,7 @@ 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
|
|
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
63
|
|
|
64
64
|
Each of these has `Retry`, which starts the mod again, and `Report` and `Hide` ([ui.md](ui.md#toasts)).
|
|
65
65
|
|
|
@@ -123,9 +123,9 @@ registerPermissionCheck(addHook: On): void
|
|
|
123
123
|
registeredMod(): ModDefinition | undefined
|
|
124
124
|
```
|
|
125
125
|
|
|
126
|
-
`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.
|
|
126
|
+
`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
127
|
|
|
128
|
-
A mod that decides permissions calls `registerPermissionCheck` after `registerMod`. That covers
|
|
128
|
+
A mod that decides permissions calls `registerPermissionCheck` after `registerMod`. That covers `sessions.current.beforePermissionCheck`, `sessions.current.beforePermissionRequest`, and the `permissions` job:
|
|
129
129
|
|
|
130
130
|
```ts
|
|
131
131
|
export function register(addHook: On, options: PluginOptions): void {
|
|
@@ -136,11 +136,11 @@ export function register(addHook: On, options: PluginOptions): void {
|
|
|
136
136
|
|
|
137
137
|
Such a mod 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. A mod that decides permissions without it does not start, and its error toast names the line to add.
|
|
138
138
|
|
|
139
|
-
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
|
|
139
|
+
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
140
|
|
|
141
141
|
## mod: this mod
|
|
142
142
|
|
|
143
|
-
`setup` gets `mod`, of type `Mod<State, Options>`. Every
|
|
143
|
+
`setup` gets `mod`, of type `Mod<State, Options>`. Every listener, render, and job gets the same `mod`. `mod` holds what belongs to this mod: its name, its folders, its values, its grants, and what it adds to Claude Code.
|
|
144
144
|
|
|
145
145
|
```ts
|
|
146
146
|
type Mod<State, Options> = {
|
|
@@ -150,8 +150,6 @@ type Mod<State, Options> = {
|
|
|
150
150
|
readonly state: Readonly<State>
|
|
151
151
|
readonly options: OptionValues<Options>
|
|
152
152
|
readonly permissions: Permissions
|
|
153
|
-
on(hook, listener, options?): Disposable
|
|
154
|
-
on(hook, matcher, listener, options?): Disposable
|
|
155
153
|
every(ms, listener): Disposable
|
|
156
154
|
after(ms, listener): Disposable
|
|
157
155
|
use(job): Handle
|
|
@@ -170,7 +168,6 @@ type Mod<State, Options> = {
|
|
|
170
168
|
| `state` | The mod's values. Assign to a key to change it. | [state.md](state.md) |
|
|
171
169
|
| `options` | The values a person set for the mod's options, typed from their declaration. Read-only. | [options.md](options.md) |
|
|
172
170
|
| `permissions` | Says whether the person granted each permission. | [permissions.md](permissions.md) |
|
|
173
|
-
| `on` | Listens to a Claude Code hook, such as `PreToolUse`. | [hooks.md](hooks.md) |
|
|
174
171
|
| `every` and `after` | Run a listener on a timer while the session runs. | below |
|
|
175
172
|
| `use` | Adds a job, such as a slash command or a tool, and returns its handle. | [jobs.md](jobs.md) |
|
|
176
173
|
| `log` | Writes a line, under the mod's name, to Claude Code's debug log. | below |
|
|
@@ -181,13 +178,15 @@ type Mod<State, Options> = {
|
|
|
181
178
|
Everything that is Claude Code's or the computer's rather than the mod's is an import from `mod.js`, so a function in any file calls it without being handed `mod`:
|
|
182
179
|
|
|
183
180
|
```ts
|
|
184
|
-
import { fs, http, metadata, model, process, sessions, settings } from '../node_modules/@cmodjs/core/mod.js'
|
|
181
|
+
import { commands, config, fs, http, metadata, model, process, sessions, settings } from '../node_modules/@cmodjs/core/mod.js'
|
|
185
182
|
```
|
|
186
183
|
|
|
187
184
|
| Import | What it is | Docs |
|
|
188
185
|
| --- | --- | --- |
|
|
189
|
-
| `sessions` | The conversations: the one the mod runs in, every other one, and search across them. | [session.md](session.md) |
|
|
186
|
+
| `sessions` | The conversations: the one the mod runs in, every other one, and search across them. Everything that happens in a session, from a prompt to a permission dialog, is a method on it. | [session.md](session.md) |
|
|
187
|
+
| `commands` | Every slash command: lists them, runs one, and hears or changes each run. | below |
|
|
190
188
|
| `settings` | Claude Code's `settings.json` files: the person's, the project's, and the project's local one. | below |
|
|
189
|
+
| `config` | The rows of Claude Code's `/config` menu. | below |
|
|
191
190
|
| `model` | Asks a model one question, outside the conversation. | below |
|
|
192
191
|
| `fs` | Reads, writes, lists, and finds files. | below |
|
|
193
192
|
| `metadata` | Reads and changes the mod's keys in one file's metadata. | below |
|
|
@@ -198,14 +197,14 @@ Each import works once the mod has started, in `setup` and anything it adds. Eac
|
|
|
198
197
|
|
|
199
198
|
### Disposable
|
|
200
199
|
|
|
201
|
-
`mod.
|
|
200
|
+
`mod.every`, `mod.after`, `mod.ui.render`, and every `on` and `before` method each return a `Disposable`. `dispose()` takes back what the call added, so a mod turns a feature off and on while it runs. Everything a mod adds is taken back when the mod stops, so a mod that keeps a feature for the whole session ignores the `Disposable`.
|
|
202
201
|
|
|
203
202
|
```ts
|
|
204
203
|
type Disposable = { dispose(): void }
|
|
205
204
|
```
|
|
206
205
|
|
|
207
206
|
```ts
|
|
208
|
-
const watching =
|
|
207
|
+
const watching = sessions.current.onToolCall({ name: 'Edit' }, () => void (mod.state.session.edits += 1))
|
|
209
208
|
mod.use(slashCommand({ name: 'stop-counting', description: 'Stop counting edits', reply: () => watching.dispose() }))
|
|
210
209
|
```
|
|
211
210
|
|
|
@@ -230,11 +229,13 @@ mod.every(60_000, async () => {
|
|
|
230
229
|
### mod.log
|
|
231
230
|
|
|
232
231
|
```ts
|
|
233
|
-
mod.log(text: string): void
|
|
232
|
+
mod.log(text: string, options?: { to?: 'debug' | 'transcript' }): void
|
|
234
233
|
```
|
|
235
234
|
|
|
236
235
|
`log` writes `<mod>: <text>` to Claude Code's debug log, which `claude --debug` shows. It is for what helps the mod's author find a fault and the person never needs to see. Tell the person with a toast instead ([ui.md](ui.md#toasts)).
|
|
237
236
|
|
|
237
|
+
With `{ to: 'transcript' }`, the line shows dim in the conversation, where the person reads it and Claude never does, such as "Formatted 3 files" after a turn.
|
|
238
|
+
|
|
238
239
|
### mod.settings
|
|
239
240
|
|
|
240
241
|
```ts
|
|
@@ -249,39 +250,77 @@ mod.settings.page(definePane({ id: 'runs', title: 'Runs', render: (current) => <
|
|
|
249
250
|
mod.use(slashCommand({ name: 'ci-runs', description: 'Show the CI runs', reply: () => void mod.settings.open('runs') }))
|
|
250
251
|
```
|
|
251
252
|
|
|
253
|
+
## commands: slash commands
|
|
254
|
+
|
|
255
|
+
```ts
|
|
256
|
+
commands.list(): Promise<CommandInfo[]>
|
|
257
|
+
commands.run(name: string, args?: string): Promise<{ text?: string; note?: string }>
|
|
258
|
+
commands.beforeRun(matcher?, listener: (run: CommandRun) => Answer | void, options?): Disposable
|
|
259
|
+
commands.onRun(matcher?, listener: (run: CommandRun) => unknown, options?): Disposable
|
|
260
|
+
|
|
261
|
+
type CommandInfo = { name: string; description: string; source: 'builtin' | 'plugin' | 'user' | 'mcp'; plugin?: string }
|
|
262
|
+
type CommandRun = { name: string; args: string; source?: 'builtin' | 'plugin' | 'user' | 'mcp'; from: Sender; session: string; cwd: string }
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
- `list` answers every command the person can type: Claude Code's own, each plugin's, the person's and the project's, and each MCP server's prompts.
|
|
266
|
+
- `run` runs a command, a built-in such as `compact` included, the way the person typing `/<name> <args>` would. It resolves the text the command printed and the note it left Claude. It needs the `prompt` permission.
|
|
267
|
+
- `beforeRun` runs just before any command runs, typed or run by a mod, and its answer changes it: `{ args?, note? }` runs it with new arguments and adds a note Claude reads after its output, `{ result, exitCode? }` answers in its place without running it, and `{ refuse }` runs nothing and shows the reason as its output. An answer needs the `prompt` permission. `exitCode` is what a `claude -p "/lint"` run exits with.
|
|
268
|
+
- `onRun` hears each command once it ran.
|
|
269
|
+
- A command a project or a plugin writes as a Markdown prompt, such as `/review`, also reaches `beforePrompt` after `beforeRun`, as its text goes to Claude ([session.md](session.md#change-what-happens)). A built-in such as `/model` reaches only these two.
|
|
270
|
+
|
|
271
|
+
```ts
|
|
272
|
+
commands.beforeRun({ name: 'compact' }, () => (mod.state.session.isDeploying ? { refuse: 'Compact once the deploy finishes.' } : undefined))
|
|
273
|
+
commands.onRun({ name: 'model' }, (run) => mod.log(`model set to ${run.args}`))
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
A command of the mod's own is a `slashCommand` job ([jobs.md](jobs.md#slashcommand)).
|
|
277
|
+
|
|
252
278
|
## settings: Claude Code's settings files
|
|
253
279
|
|
|
254
280
|
```ts
|
|
255
|
-
settings.list(): Promise<Setting[]>
|
|
256
281
|
settings.user.read(): Promise<Settings>
|
|
257
282
|
settings.user.update(values: Readonly<Record<string, unknown>>): Promise<void>
|
|
258
283
|
settings.project.read(): Promise<Settings>
|
|
259
284
|
settings.project.update(values: Readonly<Record<string, unknown>>): Promise<void>
|
|
260
285
|
settings.local.read(): Promise<Settings>
|
|
261
286
|
settings.local.update(values: Readonly<Record<string, unknown>>): Promise<void>
|
|
287
|
+
settings.beforeFileChange(matcher?, listener: (change: SettingsFileChange) => { refuse: string } | void): Disposable
|
|
262
288
|
```
|
|
263
289
|
|
|
264
|
-
`settings.
|
|
290
|
+
`settings.user` is the person's own `~/.claude/settings.json`, `settings.project` is the project's `.claude/settings.json`, and `settings.local` is the project's `.claude/settings.local.json`.
|
|
291
|
+
|
|
292
|
+
- `read` answers the values in that one file, such as `(await settings.user.read())['enabledPlugins']`.
|
|
293
|
+
- `update` merges `values` into that file as a JSON Merge Patch ([RFC 7396](https://www.rfc-editor.org/rfc/rfc7396)). `mcpServers` in `settings.project` goes to the project's `.mcp.json`, where Claude Code reads a project's servers.
|
|
294
|
+
- An object merges into the object already at that key, at every depth, so every key it does not name stays as it was. `null` removes the key at its depth. An array or any other value replaces the value. To replace a whole object, read it and set each key you drop to `null`.
|
|
295
|
+
- The file's comments and formatting stay as they were.
|
|
296
|
+
- Each value needs a `config` permission for its key path or a path above it ([permissions.md](permissions.md)): `"config": ["env.ANTHROPIC_BASE_URL"]` covers that one value, and `"config": ["env"]` covers every key in `env`. `update` rejects with the line to add for a value no grant covers, and changes nothing.
|
|
297
|
+
- Removing the mod takes out each value it set that still equals what it set, then each object that leaves empty. A value the person changed since stays.
|
|
298
|
+
- `fs.write` to a Claude Code settings file rejects and names `update`, because a whole-file write loses the person's own edits and leaves the mod's values behind when it is removed.
|
|
299
|
+
|
|
300
|
+
A mod that points Claude Code at its local proxy sets one variable in `env`, and the person's other variables stay:
|
|
265
301
|
|
|
266
302
|
```ts
|
|
267
|
-
|
|
303
|
+
await settings.user.update({ env: { ANTHROPIC_BASE_URL: 'http://127.0.0.1:8317' } })
|
|
268
304
|
```
|
|
269
305
|
|
|
270
|
-
`
|
|
306
|
+
`beforeFileChange` hears each settings file Claude Code sees edited while the session runs, and a Skill file edited, and `{ refuse }` keeps Claude Code on the values it had. `source` is `'user'`, `'project'`, `'local'`, `'policy'`, or `'skills'`, and `path` is the file, when Claude Code names it:
|
|
271
307
|
|
|
272
|
-
|
|
308
|
+
```ts
|
|
309
|
+
type SettingsFileChange = { source: 'user' | 'project' | 'local' | 'policy' | 'skills'; path?: string }
|
|
273
310
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
- Removing the mod takes out each value it set that still equals what it set, so a value the person changed since stays.
|
|
279
|
-
- `fs.write` to a Claude Code settings file rejects and names `update`, because a whole-file write loses the person's own edits and leaves the mod's values behind when it is removed.
|
|
311
|
+
settings.beforeFileChange({ source: /^(user|project)$/ }, () => ({ refuse: 'Settings are locked while the deploy runs.' }))
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
## config: the /config menu
|
|
280
315
|
|
|
281
316
|
```ts
|
|
282
|
-
|
|
317
|
+
config.list(): Promise<ConfigRow[]>
|
|
318
|
+
|
|
319
|
+
type ConfigRow = { key: string; label: string; description?: string; kind: 'boolean' | 'choice' | 'text' | 'number'; value: boolean | string | number | readonly string[]; options?: readonly string[]; plugin?: string; isLocked: boolean }
|
|
283
320
|
```
|
|
284
321
|
|
|
322
|
+
`config.list` answers every row of Claude Code's `/config` menu, its own and each plugin's, with the value each holds now. `plugin` names the plugin a row belongs to, and Claude Code's own rows have none. `isLocked` is true when a managed setting or the organization's policy holds the value.
|
|
323
|
+
|
|
285
324
|
## model: one question outside the conversation
|
|
286
325
|
|
|
287
326
|
```ts
|
|
@@ -292,12 +331,12 @@ model.complete(request: ModelCompleteRequest, options?: { signal?: AbortSignal }
|
|
|
292
331
|
|
|
293
332
|
The call never rejects for what the model did. The result's `isAnswered` is true with the reply's `text`, or false with a `reason`, such as `'api-error'` or `'aborted'`.
|
|
294
333
|
|
|
295
|
-
|
|
334
|
+
Give the listener that waits on it time too, with its `timeoutMs` ([session.md](session.md#give-a-listener-time)), which also covers the work around the call:
|
|
296
335
|
|
|
297
336
|
```ts
|
|
298
|
-
|
|
299
|
-
const verdict = await model.complete({ model: 'haiku', prompt: `Does this reply ask the person to do work Claude could do itself? Answer yes or no.\n\n${
|
|
300
|
-
if (verdict.isAnswered && verdict.text.trim().toLowerCase().startsWith('yes')) return {
|
|
337
|
+
sessions.current.beforeTurnEnd(async (turn, { signal }) => {
|
|
338
|
+
const verdict = await model.complete({ model: 'haiku', prompt: `Does this reply ask the person to do work Claude could do itself? Answer yes or no.\n\n${turn.answer ?? ''}`, timeoutMs: 8000 }, { signal })
|
|
339
|
+
if (verdict.isAnswered && verdict.text.trim().toLowerCase().startsWith('yes')) return { refuse: 'Do that work yourself, then reply.' }
|
|
301
340
|
}, { timeoutMs: 9000 })
|
|
302
341
|
```
|
|
303
342
|
|
|
@@ -322,15 +361,15 @@ Both run a program without a shell. `argv[0]` is the program and the rest are it
|
|
|
322
361
|
A job's deadline bounds the whole callback that makes these calls ([jobs.md](jobs.md#deadlines)).
|
|
323
362
|
|
|
324
363
|
```ts
|
|
325
|
-
import { defineMod, process,
|
|
364
|
+
import { defineMod, process, sessions } from '../node_modules/@cmodjs/core/mod.js'
|
|
326
365
|
|
|
327
366
|
export const branchNote = defineMod({
|
|
328
367
|
name: 'branch-note',
|
|
329
|
-
setup(
|
|
330
|
-
|
|
368
|
+
setup() {
|
|
369
|
+
sessions.current.beforeSessionStart(async () => {
|
|
331
370
|
const { exitCode, stdout } = await process.run(['git', 'branch', '--show-current'], { cwd: sessions.current.project })
|
|
332
371
|
if (exitCode !== 0) return
|
|
333
|
-
return {
|
|
372
|
+
return { note: `The git branch is ${stdout.trim()}.` }
|
|
334
373
|
})
|
|
335
374
|
},
|
|
336
375
|
})
|
|
@@ -345,9 +384,11 @@ fs.list(path?: string): Promise<Entry[]>
|
|
|
345
384
|
fs.exists(path: string): Promise<boolean>
|
|
346
385
|
fs.stat(path: string, options?: { resolve: boolean }): Promise<Stat>
|
|
347
386
|
fs.find(glob: string | readonly string[]): Promise<readonly FileMatch[]>
|
|
387
|
+
fs.onChange({ path: string | readonly string[] }, listener: (change: FileChange) => unknown): Disposable
|
|
348
388
|
```
|
|
349
389
|
|
|
350
390
|
```ts
|
|
391
|
+
type FileChange = { path: string; type: 'change' | 'add' | 'unlink' }
|
|
351
392
|
type Entry = { name: string; isFile: boolean; isFolder: boolean; isLink: boolean; size: number; modifiedAt: number }
|
|
352
393
|
type Stat = { isFile: boolean; isFolder: boolean; isLink: boolean; size: number; modifiedAt: number; realPath?: string }
|
|
353
394
|
```
|
|
@@ -358,6 +399,11 @@ type Stat = { isFile: boolean; isFolder: boolean; isLink: boolean; size: number;
|
|
|
358
399
|
- `list` lists a folder, the working folder without a path. A symbolic link is neither a file nor a folder, with `isLink` true. `modifiedAt` is the time it last changed, in milliseconds since 1970.
|
|
359
400
|
- `exists` answers whether the path leads to a file or a folder.
|
|
360
401
|
- `stat` answers what the path leads to, following a symbolic link, and rejects a missing path. With `{ resolve: true }` it also answers `realPath`: the absolute path, every symbolic link followed.
|
|
402
|
+
- `onChange` hears each file Claude Code sees change, appear, or go at a path or under a folder. Claude Code reads the paths it watches when a session starts, so `onChange` works only in `setup`, and a call later throws. A relative path is under the project. `path` takes strings only, unlike other matchers, because the paths go to Claude Code as they are:
|
|
403
|
+
|
|
404
|
+
```ts
|
|
405
|
+
fs.onChange({ path: `${mod.dataFolder}/history` }, ({ path, type }) => mod.ui.toast(`${path}: ${type}`))
|
|
406
|
+
```
|
|
361
407
|
|
|
362
408
|
A file the mod ships is under `mod.folder`. A file the mod keeps for itself belongs in `mod.dataFolder`.
|
|
363
409
|
|
|
@@ -440,12 +486,12 @@ messageOf(error: unknown): string
|
|
|
440
486
|
Returns an `Error`'s `message`, or the value as text. Use it in a `catch`, where the error is `unknown`.
|
|
441
487
|
|
|
442
488
|
```ts
|
|
443
|
-
import { defineMod, fs, messageOf,
|
|
489
|
+
import { defineMod, fs, messageOf, sessions } from '../node_modules/@cmodjs/core/mod.js'
|
|
444
490
|
|
|
445
491
|
export const notes = defineMod({
|
|
446
492
|
name: 'notes',
|
|
447
493
|
setup(mod) {
|
|
448
|
-
|
|
494
|
+
sessions.current.onTurnEnd(async () => {
|
|
449
495
|
await fs.write(`${mod.dataFolder}/last-stop.txt`, new Date().toISOString()).catch((error: unknown) => {
|
|
450
496
|
mod.ui.toast.error('Could not save the last stop', { description: messageOf(error) })
|
|
451
497
|
})
|
|
@@ -453,3 +499,35 @@ export const notes = defineMod({
|
|
|
453
499
|
},
|
|
454
500
|
})
|
|
455
501
|
```
|
|
502
|
+
|
|
503
|
+
## shell.js and path.js: read a command line
|
|
504
|
+
|
|
505
|
+
`shell.js` reads a Bash line the way cmod does for a tool call's `files` ([session.md](session.md#files-what-a-tool-call-reads-and-changes)), and `path.js` is `node:path`'s POSIX functions, which a mod cannot import from Node:
|
|
506
|
+
|
|
507
|
+
```ts
|
|
508
|
+
parseShell(line: string): ParsedShell
|
|
509
|
+
|
|
510
|
+
type ParsedShell = { commands: ShellCommand[]; reads: string[]; writes: string[]; fetches: string[]; isFullyParsed: boolean }
|
|
511
|
+
type ShellCommand = { argv: [string, ...string[]]; folder: string; input?: number }
|
|
512
|
+
|
|
513
|
+
resolve(...paths): string join(...paths): string dirname(path): string basename(path, suffix?): string
|
|
514
|
+
extname(path): string relative(from, to): string isAbsolute(path): boolean normalize(path): string
|
|
515
|
+
```
|
|
516
|
+
|
|
517
|
+
- `commands` lists every command the line runs, those inside `sudo`, `bash -c`, `$(…)`, and heredocs included, with flags such as `-rf` split into `-r` and `-f`.
|
|
518
|
+
- `folder` is where a command runs, as the line wrote it: `''` for the folder the line starts in, `src` after `cd src`, `/tmp` after `cd /tmp`, and `~/p` after `cd ~/p`. `resolve(call.cwd, folder)` makes it absolute.
|
|
519
|
+
- `input` is the index in `commands` of the command whose output this one reads through `|` or `|&`, and is absent for a command that reads no pipe. `trace read a.ts | head -5` gives `head` the `input` `0`. A wrapper such as `timeout 5 trace read a.ts` writes the output of the command it runs, so `input` names `trace`, not `timeout`.
|
|
520
|
+
- `reads`, `writes`, and `fetches` are the paths and URLs the commands name, as written. A path that holds a shell expansion makes `isFullyParsed` false, and so does a line the parser cannot read.
|
|
521
|
+
- `resolve` has no working folder to start from in Claude Code, so pass an absolute folder first, such as `resolve(sessions.current.cwd, path)`.
|
|
522
|
+
|
|
523
|
+
```ts
|
|
524
|
+
import { basename, resolve } from '../node_modules/@cmodjs/core/path.js'
|
|
525
|
+
import { parseShell } from '../node_modules/@cmodjs/core/shell.js'
|
|
526
|
+
import { sessions } from '../node_modules/@cmodjs/core/mod.js'
|
|
527
|
+
|
|
528
|
+
sessions.current.onToolCall({ name: 'Bash' }, (call) => {
|
|
529
|
+
for (const { argv, folder } of parseShell(String(call.input['command'])).commands) {
|
|
530
|
+
if (basename(argv[0]) === 'trace') mod.ui.toast(`trace runs in ${resolve(call.cwd, folder)}`)
|
|
531
|
+
}
|
|
532
|
+
})
|
|
533
|
+
```
|