@cmodjs/core 1.6.0 → 2.0.1
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 +7 -7
- package/docs/dependencies.md +1 -1
- package/docs/index.md +12 -11
- package/docs/jobs.md +12 -16
- package/docs/mod.md +111 -36
- package/docs/permissions.md +14 -18
- 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 +1 -1
- package/records.js +1 -1
- package/register.d.ts.map +1 -1
- package/register.js +8 -5
- package/runtime/before.d.ts +11 -4
- package/runtime/before.d.ts.map +1 -1
- package/runtime/before.js +131 -67
- 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 +82 -38
- 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.map +1 -1
- package/runtime/grants.js +13 -8
- package/runtime/hooks.d.ts +15 -11
- package/runtime/hooks.d.ts.map +1 -1
- package/runtime/hooks.js +8 -165
- package/runtime/lifecycle.d.ts +4 -2
- package/runtime/lifecycle.d.ts.map +1 -1
- package/runtime/lifecycle.js +96 -62
- package/runtime/permission-check.d.ts +15 -0
- package/runtime/permission-check.d.ts.map +1 -0
- package/runtime/permission-check.js +37 -0
- package/runtime/sessions.d.ts +35 -5
- package/runtime/sessions.d.ts.map +1 -1
- package/runtime/sessions.js +304 -82
- package/runtime/state.d.ts.map +1 -1
- package/runtime/state.js +14 -11
- 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 +6 -4
- package/session/context/turn/message/message.d.ts.map +1 -1
- package/session/context/turn/message/message.js +12 -8
- 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 +1 -1
- package/src/register.ts +8 -5
- package/src/runtime/before.ts +140 -66
- package/src/runtime/claude.ts +5 -1
- package/src/runtime/conversations.ts +75 -37
- package/src/runtime/deadline.ts +38 -0
- package/src/runtime/dependencies.ts +12 -0
- package/src/runtime/grants.ts +11 -7
- package/src/runtime/hooks.ts +24 -163
- package/src/runtime/lifecycle.ts +110 -77
- package/src/runtime/permission-check.ts +47 -0
- package/src/runtime/sessions.ts +334 -91
- package/src/runtime/state.ts +15 -8
- 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 +17 -11
- 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 +88 -19
- 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 +58 -8
- 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.
|
|
@@ -98,7 +98,7 @@ Releases the mod at `path` (default: the current folder) at the version in its `
|
|
|
98
98
|
4. Commits the release as the `release` branch, and builds the release archive from that commit.
|
|
99
99
|
5. Builds the program `cli/` declares, and writes `SHA256SUMS` for every file of the release.
|
|
100
100
|
6. Writes `.claude-plugin/marketplace.json`, listing the archive and the cmod plugin. The cmod entry points at the `release` branch of `heyJordanParker/cmod`, so the mod's `"cmod"` dependency resolves in this marketplace and follows each cmod release with no new publish of the mod.
|
|
101
|
-
7. Commits that file, tags `v<version>`,
|
|
101
|
+
7. Commits that file, tags `v<version>`, tags the release commit `<name>--v<version>`, which Claude Code installs a version range from when a marketplace lists the mod from its git repository, pushes both tags and the `release` branch, creates the GitHub release with every file, then pushes the branch that holds `marketplace.json`. So Claude Code never updates anyone to a release whose files are not up yet. Its notes are the version's section of the committed `CHANGELOG.md`, above GitHub's compare link ([mod.md](mod.md#release-notes)). `--dry-run` prints them.
|
|
102
102
|
8. Prints the link to paste as the Repository when you submit the mod at [claude.ai/directory/manage](https://claude.ai/directory/manage), such as `https://github.com/owner/greeter/tree/release`. The portal reads the branch from the link, so the directory follows `release`.
|
|
103
103
|
|
|
104
104
|
The release keeps `package.json`, so Claude Code still installs the mod's packages for its install and uninstall steps. It keeps `bun.lock` too, unless a `"files"` list leaves it out. `main` keeps the source only.
|
|
@@ -114,7 +114,7 @@ It needs, and refuses to start without:
|
|
|
114
114
|
- a `package.json` with no dependency on a `file:` or `link:` path, which no user has
|
|
115
115
|
- a `"files"` list, when `package.json` has one, whose every path matches a committed file
|
|
116
116
|
- a `"program"` in the `cmod` key of `package.json` that names the program `cli/` declares, when `cli/` declares one
|
|
117
|
-
- for a real publish, a working tree with no uncommitted changes,
|
|
117
|
+
- for a real publish, a working tree with no uncommitted changes, no `v<version>` or `<name>--v<version>` tag yet, a `git push` to `origin` that succeeds, and the `gh` command, which creates the GitHub release
|
|
118
118
|
|
|
119
119
|
`--dry-run` builds everything into a temporary folder, and pushes nothing. It writes `marketplace.json` there too, so the working tree stays as it was. It allows uncommitted changes and an existing tag, and builds from the last commit. It prints the folder that holds the release, so you can read what the directory will read.
|
|
120
120
|
|
|
@@ -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
|
|
|
@@ -259,7 +259,7 @@ Changes Claude Code settings for a set-up mod, as its `settings.user.update`, `s
|
|
|
259
259
|
cmod rules [--project <folder>] [--json]
|
|
260
260
|
```
|
|
261
261
|
|
|
262
|
-
Links the `rules/` folder of each mod that is on and has the `prompt` permission into Claude Code's rules folder, and removes the link of each that is off, lacks the permission, or is removed ([mod.md](mod.md#rules)). A mod the person installed links at `<config>/rules/<mod>`, and one turned on only for `--project`, the current folder when it is left out, links at `<project>/.claude/rules/<mod>`. It prints the rules it linked. With `--json` it prints `{ linked, placed, removed }`, where `placed` and `removed` name the mods whose link this run made or took away. `cmod install`, `update`, `remove`, `enable`, `disable`, `permission`, `setup`, `teardown`, `link`, and `unlink` run it after they succeed
|
|
262
|
+
Links the `rules/` folder of each mod that is on and has the `prompt` permission into Claude Code's rules folder, and removes the link of each that is off, lacks the permission, or is removed ([mod.md](mod.md#rules)). A mod the person installed links at `<config>/rules/<mod>`, and one turned on only for `--project`, the current folder when it is left out, links at `<project>/.claude/rules/<mod>`. It prints the rules it linked. With `--json` it prints `{ linked, placed, removed, watched }`, where `placed` and `removed` name the mods whose link this run made or took away, and `watched` lists the files and folders whose change can change the next run. `cmod install`, `update`, `remove`, `enable`, `disable`, `permission`, `setup`, `teardown`, `link`, and `unlink` run it after they succeed. The cmod plugin runs it when a session starts and before a prompt, each time only when a `watched` path changed since its last run.
|
|
263
263
|
|
|
264
264
|
### cmod option
|
|
265
265
|
|
package/docs/dependencies.md
CHANGED
|
@@ -58,7 +58,7 @@ A provider that changes or removes a method breaks every mod that calls it, and
|
|
|
58
58
|
"dependencies": [{ "name": "cmod", "version": "^1.0.0" }, { "name": "tracer", "version": "^1.0" }]
|
|
59
59
|
```
|
|
60
60
|
|
|
61
|
-
Claude Code checks the range against the `version`
|
|
61
|
+
Claude Code checks the range against the provider's `version` when it loads your mod. Where a marketplace lists the provider from its git repository, as every mod's marketplace lists cmod, Claude Code also installs it at the highest `<name>--v<version>` tag inside the range, which `cmod publish` pushes for every release ([Claude Code's plugin dependencies](https://code.claude.com/docs/en/plugins/dependencies)). cmod is a provider too: a mod built on `@cmodjs/core` 1.0 calls methods only the 1.0 cmod plugin has, so `cmod new` writes the `^1.0.0` range.
|
|
62
62
|
|
|
63
63
|
Claude Code then lays tracer's types file beside the mod's own types, so the call is typed:
|
|
64
64
|
|
package/docs/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,26 +250,43 @@ 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.list` answers every row of Claude Code's `/config` menu, its own and each plugin's, with the value each holds now:
|
|
265
|
-
|
|
266
|
-
```ts
|
|
267
|
-
type Setting = { key: string; label: string; description?: string; kind: 'boolean' | 'choice' | 'text' | 'number'; value: boolean | string | number | readonly string[]; options?: readonly string[]; plugin?: string; isLocked: boolean }
|
|
268
|
-
```
|
|
269
|
-
|
|
270
|
-
`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.
|
|
271
|
-
|
|
272
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`.
|
|
273
291
|
|
|
274
292
|
- `read` answers the values in that one file, such as `(await settings.user.read())['enabledPlugins']`.
|
|
@@ -285,6 +303,24 @@ A mod that points Claude Code at its local proxy sets one variable in `env`, and
|
|
|
285
303
|
await settings.user.update({ env: { ANTHROPIC_BASE_URL: 'http://127.0.0.1:8317' } })
|
|
286
304
|
```
|
|
287
305
|
|
|
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:
|
|
307
|
+
|
|
308
|
+
```ts
|
|
309
|
+
type SettingsFileChange = { source: 'user' | 'project' | 'local' | 'policy' | 'skills'; path?: string }
|
|
310
|
+
|
|
311
|
+
settings.beforeFileChange({ source: /^(user|project)$/ }, () => ({ refuse: 'Settings are locked while the deploy runs.' }))
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
## config: the /config menu
|
|
315
|
+
|
|
316
|
+
```ts
|
|
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 }
|
|
320
|
+
```
|
|
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
|
+
|
|
288
324
|
## model: one question outside the conversation
|
|
289
325
|
|
|
290
326
|
```ts
|
|
@@ -295,12 +331,12 @@ model.complete(request: ModelCompleteRequest, options?: { signal?: AbortSignal }
|
|
|
295
331
|
|
|
296
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'`.
|
|
297
333
|
|
|
298
|
-
|
|
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:
|
|
299
335
|
|
|
300
336
|
```ts
|
|
301
|
-
|
|
302
|
-
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${
|
|
303
|
-
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.' }
|
|
304
340
|
}, { timeoutMs: 9000 })
|
|
305
341
|
```
|
|
306
342
|
|
|
@@ -325,15 +361,15 @@ Both run a program without a shell. `argv[0]` is the program and the rest are it
|
|
|
325
361
|
A job's deadline bounds the whole callback that makes these calls ([jobs.md](jobs.md#deadlines)).
|
|
326
362
|
|
|
327
363
|
```ts
|
|
328
|
-
import { defineMod, process,
|
|
364
|
+
import { defineMod, process, sessions } from '../node_modules/@cmodjs/core/mod.js'
|
|
329
365
|
|
|
330
366
|
export const branchNote = defineMod({
|
|
331
367
|
name: 'branch-note',
|
|
332
|
-
setup(
|
|
333
|
-
|
|
368
|
+
setup() {
|
|
369
|
+
sessions.current.beforeSessionStart(async () => {
|
|
334
370
|
const { exitCode, stdout } = await process.run(['git', 'branch', '--show-current'], { cwd: sessions.current.project })
|
|
335
371
|
if (exitCode !== 0) return
|
|
336
|
-
return {
|
|
372
|
+
return { note: `The git branch is ${stdout.trim()}.` }
|
|
337
373
|
})
|
|
338
374
|
},
|
|
339
375
|
})
|
|
@@ -348,9 +384,11 @@ fs.list(path?: string): Promise<Entry[]>
|
|
|
348
384
|
fs.exists(path: string): Promise<boolean>
|
|
349
385
|
fs.stat(path: string, options?: { resolve: boolean }): Promise<Stat>
|
|
350
386
|
fs.find(glob: string | readonly string[]): Promise<readonly FileMatch[]>
|
|
387
|
+
fs.onChange({ path: string | readonly string[] }, listener: (change: FileChange) => unknown): Disposable
|
|
351
388
|
```
|
|
352
389
|
|
|
353
390
|
```ts
|
|
391
|
+
type FileChange = { path: string; type: 'change' | 'add' | 'unlink' }
|
|
354
392
|
type Entry = { name: string; isFile: boolean; isFolder: boolean; isLink: boolean; size: number; modifiedAt: number }
|
|
355
393
|
type Stat = { isFile: boolean; isFolder: boolean; isLink: boolean; size: number; modifiedAt: number; realPath?: string }
|
|
356
394
|
```
|
|
@@ -361,6 +399,11 @@ type Stat = { isFile: boolean; isFolder: boolean; isLink: boolean; size: number;
|
|
|
361
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.
|
|
362
400
|
- `exists` answers whether the path leads to a file or a folder.
|
|
363
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
|
+
```
|
|
364
407
|
|
|
365
408
|
A file the mod ships is under `mod.folder`. A file the mod keeps for itself belongs in `mod.dataFolder`.
|
|
366
409
|
|
|
@@ -443,12 +486,12 @@ messageOf(error: unknown): string
|
|
|
443
486
|
Returns an `Error`'s `message`, or the value as text. Use it in a `catch`, where the error is `unknown`.
|
|
444
487
|
|
|
445
488
|
```ts
|
|
446
|
-
import { defineMod, fs, messageOf,
|
|
489
|
+
import { defineMod, fs, messageOf, sessions } from '../node_modules/@cmodjs/core/mod.js'
|
|
447
490
|
|
|
448
491
|
export const notes = defineMod({
|
|
449
492
|
name: 'notes',
|
|
450
493
|
setup(mod) {
|
|
451
|
-
|
|
494
|
+
sessions.current.onTurnEnd(async () => {
|
|
452
495
|
await fs.write(`${mod.dataFolder}/last-stop.txt`, new Date().toISOString()).catch((error: unknown) => {
|
|
453
496
|
mod.ui.toast.error('Could not save the last stop', { description: messageOf(error) })
|
|
454
497
|
})
|
|
@@ -456,3 +499,35 @@ export const notes = defineMod({
|
|
|
456
499
|
},
|
|
457
500
|
})
|
|
458
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
|
+
```
|