@cmodjs/core 0.1.7 → 0.1.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/docs/commands.md +12 -12
- package/docs/dependencies.md +1 -1
- package/docs/hooks.md +7 -7
- package/docs/index.md +1 -1
- package/docs/install-steps.md +17 -17
- package/docs/jobs.md +17 -17
- package/docs/mod.md +5 -5
- package/docs/state.md +8 -8
- package/docs/ui.md +6 -6
- package/package.json +1 -1
- package/records.js +3 -3
- package/runtime/lifecycle.js +5 -5
- package/src/records.ts +3 -3
- package/src/runtime/lifecycle.ts +5 -5
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
# @cmodjs/core
|
|
6
6
|
|
|
7
|
-
The library every Claude Mod Manager mod imports. Claude Mod Manager (
|
|
7
|
+
The library every Claude Mod Manager mod imports. Claude Mod Manager (cmod) installs, tries, builds, checks, and publishes Claude Code mods: plugins with hooks, panes, slash commands, tools, permission rules, and an install step that runs with your consent.
|
|
8
8
|
|
|
9
9
|
```sh
|
|
10
10
|
npm i -g @cmodjs/cli
|
|
@@ -17,4 +17,4 @@ cmod new my-mod
|
|
|
17
17
|
|
|
18
18
|
This package ships its docs in `docs/`, so a mod's `node_modules/@cmodjs/core/docs/index.md` is the map of the docs for the version it installed. The same docs are on GitHub at [core/docs](https://github.com/heyJordanParker/cmod/blob/main/core/docs/index.md).
|
|
19
19
|
|
|
20
|
-
The
|
|
20
|
+
The cmod plugin and the `cmod` source live at [github.com/heyJordanParker/cmod](https://github.com/heyJordanParker/cmod).
|
package/docs/commands.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# cmod commands
|
|
2
2
|
|
|
3
|
-
The `cmod` command of Claude Mod Manager (
|
|
3
|
+
The `cmod` command of Claude Mod Manager (cmod) builds, checks, and publishes mods, and installs them. `npm i -g @cmodjs/cli` puts it on `PATH`. `cmod <command> --help` prints a command's arguments, and `cmod --version` prints the version.
|
|
4
4
|
|
|
5
5
|
Each command below restates what `cmod <command> --help` prints, for a mod author. The help text is the full word.
|
|
6
6
|
|
|
@@ -25,7 +25,7 @@ Creates a mod in `./<name>`: a `defineMod` with one hook, one pane, and one rend
|
|
|
25
25
|
cmod link [path] [--yes]
|
|
26
26
|
```
|
|
27
27
|
|
|
28
|
-
Loads the checkout at `path` (default: the current folder) in every new Claude Code session, in place of the installed mod. Installs the
|
|
28
|
+
Loads the checkout at `path` (default: the current folder) in every new Claude Code session, in place of the installed mod. Installs the cmod plugin when Claude Code lacks it, unless the checkout is cmod itself. Then installs the checkout's packages, builds the program `cli/` declares into `~/.local/bin`, writes the folder into `CLAUDE_CODE_PLUGIN_DIRS` in the `env` block of Claude Code's `settings.json`, and runs the checkout's install step. A session that is already running loads it after `/reload-plugins`.
|
|
29
29
|
|
|
30
30
|
- `--yes` approves the mod's install and uninstall commands without asking.
|
|
31
31
|
|
|
@@ -57,7 +57,7 @@ Runs every check this machine can run on the mod at `path` (default: the current
|
|
|
57
57
|
10. Runs, with `claude plugin test`, the test files that import `claude-code/testing`. It skips this when none does.
|
|
58
58
|
11. Checks its name against other plugins. Only a project plugin, in `.claude/skills/<name>/` of a repository, runs this check: it fails when an installed or linked plugin has the same name and hides the project plugin. Any other mod passes it at once.
|
|
59
59
|
|
|
60
|
-
`cmod check` fetches `tsc` 7.0.2 and `oxlint` 1.86.0 into the
|
|
60
|
+
`cmod check` fetches `tsc` 7.0.2 and `oxlint` 1.86.0 into the cmod store the first time. It ends with a count of passed, failed, and skipped checks, and exits 1 when one failed.
|
|
61
61
|
|
|
62
62
|
### cmod try
|
|
63
63
|
|
|
@@ -65,7 +65,7 @@ Runs every check this machine can run on the mod at `path` (default: the current
|
|
|
65
65
|
cmod try <owner/repo | path> [--yes] [-- claude arguments]
|
|
66
66
|
```
|
|
67
67
|
|
|
68
|
-
Starts one throwaway Claude Code session with a mod set up. Installs the mod's packages, installs the
|
|
68
|
+
Starts one throwaway Claude Code session with a mod set up. Installs the mod's packages, installs the cmod plugin when Claude Code lacks it, and runs the mod's install step after asking consent. Then starts one Claude Code session with the mod loaded through `--plugin-dir`.
|
|
69
69
|
|
|
70
70
|
- Unlike `cmod link`, it builds no program from `cli/`. A mod whose `package.json` `cmod.program` names a program downloads it from the GitHub release of the mod's version, so it fails until `cmod publish` has released that version.
|
|
71
71
|
|
|
@@ -87,7 +87,7 @@ Releases the mod at `path` (default: the current folder) at the version in its `
|
|
|
87
87
|
3. Checks the release with `claude plugin validate --strict`, and stops with the validator's message when it fails.
|
|
88
88
|
4. Commits the release as the `release` branch, and builds the release archive from that commit.
|
|
89
89
|
5. Builds the program `cli/` declares, and writes `SHA256SUMS` for every file of the release.
|
|
90
|
-
6. Writes `.claude-plugin/marketplace.json`, listing the archive and the
|
|
90
|
+
6. Writes `.claude-plugin/marketplace.json`, listing the archive and the cmod plugin.
|
|
91
91
|
7. Commits that file, tags `v<version>`, pushes the tag and the `release` branch, and creates the GitHub release.
|
|
92
92
|
8. Prints the link to paste as the Repository when you submit the mod at [claude.ai/directory/manage](https://claude.ai/directory/manage), such as `https://github.com/owner/greeter/tree/release`. The portal reads the branch from the link, so the directory follows `release`.
|
|
93
93
|
|
|
@@ -103,7 +103,7 @@ It needs, and refuses to start without:
|
|
|
103
103
|
- a `package.json` with no dependency on a `file:` or `link:` path, which no user has
|
|
104
104
|
- a `"files"` list, when `package.json` has one, whose every path matches a committed file
|
|
105
105
|
- a `"program"` in the `cmod` key of `package.json` that names the program `cli/` declares, when `cli/` declares one
|
|
106
|
-
- the network, to hash the
|
|
106
|
+
- the network, to hash the cmod plugin release it lists, unless the mod is cmod itself
|
|
107
107
|
- for a real publish, a working tree with no uncommitted changes, a `v<version>` tag that does not exist yet, a `git push` to `origin` that succeeds, and the `gh` command, which creates the GitHub release
|
|
108
108
|
|
|
109
109
|
`--dry-run` builds and writes everything, and pushes nothing. It allows uncommitted changes and an existing tag, and builds from the last commit. It prints the folder that holds the release, so you can read what the directory will read.
|
|
@@ -117,7 +117,7 @@ cmod install <owner/repo | path> [name] [--yes]
|
|
|
117
117
|
cmod install <name | name@marketplace> [--yes]
|
|
118
118
|
```
|
|
119
119
|
|
|
120
|
-
Adds the plugin's marketplace to Claude Code and installs the plugin through Claude Code. A mod also gets the
|
|
120
|
+
Adds the plugin's marketplace to Claude Code and installs the plugin through Claude Code. A mod also gets the cmod plugin when Claude Code lacks it, and its install step runs. Any other plugin installs through Claude Code alone, and cmod keeps no record of it.
|
|
121
121
|
|
|
122
122
|
- When the marketplace lists several plugins, name the one to install after it.
|
|
123
123
|
- Given `name@marketplace` of a marketplace Claude Code has added, it installs that plugin.
|
|
@@ -141,7 +141,7 @@ Updates the named plugin, or every plugin Claude Code has installed, through Cla
|
|
|
141
141
|
cmod remove <name>
|
|
142
142
|
```
|
|
143
143
|
|
|
144
|
-
Uninstalls the plugin through Claude Code. When
|
|
144
|
+
Uninstalls the plugin through Claude Code. When cmod set the plugin up as a mod, it then runs the uninstall step cmod saved and removes what cmod set up.
|
|
145
145
|
|
|
146
146
|
### cmod list
|
|
147
147
|
|
|
@@ -149,9 +149,9 @@ Uninstalls the plugin through Claude Code. When CMod set the plugin up as a mod,
|
|
|
149
149
|
cmod list
|
|
150
150
|
```
|
|
151
151
|
|
|
152
|
-
Lists every plugin Claude Code has installed or linked, with its version. A mod shows whether
|
|
152
|
+
Lists every plugin Claude Code has installed or linked, with its version. A mod shows whether cmod set it up, and any other plugin shows whether Claude Code enabled it. It also lists every mod Claude Code removed whose uninstall step has not run yet.
|
|
153
153
|
|
|
154
|
-
- The `cmod` commands count a plugin as a mod only when its `package.json` has a `cmod` step key or
|
|
154
|
+
- The `cmod` commands count a plugin as a mod only when its `package.json` has a `cmod` step key or cmod holds an install record for it. A mod made with `cmod new` has neither until its first Claude Code session sets it up, so until then `cmod list` shows it as `enabled`, like any other plugin.
|
|
155
155
|
|
|
156
156
|
## Run in an install script
|
|
157
157
|
|
|
@@ -163,9 +163,9 @@ cmod download <program> <machine> <url> <sha256> [<machine> <url> <sha256> …]
|
|
|
163
163
|
|
|
164
164
|
Downloads a program for an install script: fetches the download for this machine, checks its SHA-256, unpacks it, and moves `<program>` to `$CMOD_DATA/bin/<program>`. [install-steps.md](install-steps.md) explains it.
|
|
165
165
|
|
|
166
|
-
## Run by
|
|
166
|
+
## Run by cmod
|
|
167
167
|
|
|
168
|
-
|
|
168
|
+
cmod runs these two itself. A person runs them to see a step's whole log or to finish a step.
|
|
169
169
|
|
|
170
170
|
### cmod setup
|
|
171
171
|
|
package/docs/dependencies.md
CHANGED
|
@@ -24,7 +24,7 @@ export const tracer = defineMod({
|
|
|
24
24
|
|
|
25
25
|
## Type the methods: CmodDependencies
|
|
26
26
|
|
|
27
|
-
The provider types its `api` in `types/index.d.ts`, and its `.claude-plugin/plugin.json` names that file as `"types": "./types/index.d.ts"`. The file adds the mod to `CmodDependencies`, which the Claude Mod Manager (
|
|
27
|
+
The provider types its `api` in `types/index.d.ts`, and its `.claude-plugin/plugin.json` names that file as `"types": "./types/index.d.ts"`. The file adds the mod to `CmodDependencies`, which the Claude Mod Manager (cmod) plugin declares on the `claude-code` module:
|
|
28
28
|
|
|
29
29
|
```ts
|
|
30
30
|
export type TracerSignature = { name: string; line: number }
|
package/docs/hooks.md
CHANGED
|
@@ -39,7 +39,7 @@ export const safeDelete = defineMod({
|
|
|
39
39
|
| `SessionEnd` | none |
|
|
40
40
|
| `UserPromptSubmit` | none |
|
|
41
41
|
| `InstructionsLoaded` | none |
|
|
42
|
-
| `PreToolUse` | Claude Mod Manager (
|
|
42
|
+
| `PreToolUse` | Claude Mod Manager (cmod) builds the input: `session_id`, `cwd`, `hook_event_name`, `tool_name`, `tool_input`, `tool_use_id`, `agent_id` inside a subagent, `agent_type` inside a subagent or when the session started as an agent, and `files`. It has no `transcript_path` and no `permission_mode`. |
|
|
43
43
|
| `PermissionRequest` | none |
|
|
44
44
|
| `PermissionDenied` | none |
|
|
45
45
|
| `PostToolUse` | adds `files` |
|
|
@@ -53,17 +53,17 @@ export const safeDelete = defineMod({
|
|
|
53
53
|
| `StopFailure` | none |
|
|
54
54
|
| `CwdChanged` | no `transcript_path` |
|
|
55
55
|
|
|
56
|
-
|
|
56
|
+
cmod raises `CwdChanged` itself, never Claude Code. It raises it when the working folder changes: after `/cd`, after a Bash or PowerShell call, and at a prompt. `mod.cwd` and `mod.projectRoot` already hold the new folder when the hook runs.
|
|
57
57
|
|
|
58
58
|
## files: what a tool call reads and changes
|
|
59
59
|
|
|
60
|
-
The input of `PreToolUse`, `PostToolUse`, and `PostToolUseFailure` has `files: { read: string[]; changed: string[] }`, absolute paths that
|
|
60
|
+
The input of `PreToolUse`, `PostToolUse`, and `PostToolUseFailure` has `files: { read: string[]; changed: string[] }`, absolute paths that cmod works out from the call:
|
|
61
61
|
|
|
62
62
|
- `Read` reads its `file_path`. `Edit`, `Write`, and `NotebookEdit` change their path.
|
|
63
63
|
- `Grep` counts its `path`, or the working folder when it names none, and `Glob` counts its `pattern` joined to its `path`. Since `read` lists only files, a `Grep` of a folder and every `Glob` usually leave `read` empty.
|
|
64
64
|
- A `Bash` command reads and changes the files its commands name, such as `cat a.ts > b.ts`. A path that holds a shell expansion, such as `$HOME`, is left out.
|
|
65
65
|
- `read` lists only paths that are files now. `changed` lists every path the call writes, whether it exists or not.
|
|
66
|
-
- When
|
|
66
|
+
- When cmod cannot work the files out, both lists are empty, and the reason goes to Claude Code's debug log.
|
|
67
67
|
|
|
68
68
|
## HookAnswer: what a hook can answer
|
|
69
69
|
|
|
@@ -99,7 +99,7 @@ type HookAnswer = {
|
|
|
99
99
|
- `continue: false` stops the session after the event, and `stopReason` is the text Claude Code shows. `PreToolUse` takes neither.
|
|
100
100
|
- `decision: 'block'` with a `reason` blocks the event. On `PreToolUse` it denies the call with the reason. On `Stop` it keeps Claude going with the reason.
|
|
101
101
|
- `systemMessage` shows the text to the person as a log line.
|
|
102
|
-
-
|
|
102
|
+
- cmod ignores `suppressOutput`.
|
|
103
103
|
- `hookSpecificOutput.hookEventName` is optional. When set, it must be the event's own name.
|
|
104
104
|
|
|
105
105
|
Each event reads only some `hookSpecificOutput` fields:
|
|
@@ -147,11 +147,11 @@ A hook that throws, rejects, or answers a field its event does not read fails:
|
|
|
147
147
|
|
|
148
148
|
- On `PreToolUse` the call is denied with `<mod>: the PreToolUse hook failed: <error>`.
|
|
149
149
|
- On `PermissionRequest` the request is denied with `<mod>: the PermissionRequest hook failed: <error>`.
|
|
150
|
-
- On every other event
|
|
150
|
+
- On every other event cmod logs `<mod>: the <event> hook failed: <error>`, and the event goes on as if the hook answered nothing.
|
|
151
151
|
|
|
152
152
|
## The first SessionStart
|
|
153
153
|
|
|
154
|
-
A mod that is still installing when the session starts misses Claude Code's `SessionStart`.
|
|
154
|
+
A mod that is still installing when the session starts misses Claude Code's `SessionStart`. cmod holds `SessionStart` up to 9 seconds for the mod to start. When the mod starts later, cmod runs its `SessionStart` hooks then, and Claude Code no longer reads their answer.
|
|
155
155
|
|
|
156
156
|
## Test a hook
|
|
157
157
|
|
package/docs/index.md
CHANGED
|
@@ -4,7 +4,7 @@ These are the docs of the `@cmodjs/core` version installed beside them. Read the
|
|
|
4
4
|
|
|
5
5
|
## What a mod is
|
|
6
6
|
|
|
7
|
-
A mod is a Claude Code plugin built with `@cmodjs/core`. Claude Mod Manager (
|
|
7
|
+
A mod is a Claude Code plugin built with `@cmodjs/core`. Claude Mod Manager (cmod) installs mods, runs their install and uninstall steps with the person's consent, and builds and publishes new ones. The `cmod` command does that work in a terminal.
|
|
8
8
|
|
|
9
9
|
`cmod new my-mod` writes this layout:
|
|
10
10
|
|
package/docs/install-steps.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Install steps
|
|
2
2
|
|
|
3
|
-
A mod that changes the machine, such as by adding a shell alias or downloading a program, names its install and uninstall steps in its `package.json`. The cmod program of Claude Mod Manager (
|
|
3
|
+
A mod that changes the machine, such as by adding a shell alias or downloading a program, names its install and uninstall steps in its `package.json`. The cmod program of Claude Mod Manager (cmod) runs them with the person's consent, and runs the uninstall step after the person removes the mod. The library never runs them.
|
|
4
4
|
|
|
5
5
|
## The cmod key
|
|
6
6
|
|
|
@@ -18,20 +18,20 @@ Each key is optional.
|
|
|
18
18
|
|
|
19
19
|
- `install` is one shell command that sets the mod up.
|
|
20
20
|
- `uninstall` is one shell command that undoes it.
|
|
21
|
-
- `program` is the name of a program
|
|
21
|
+
- `program` is the name of a program cmod downloads from the mod's GitHub release into `~/.local/bin`. [Ship a program](#ship-a-program) explains it.
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
cmod refuses a `cmod` key that breaks these rules, with the fix in the message:
|
|
24
24
|
|
|
25
25
|
- `cmod` is an object.
|
|
26
26
|
- `install` and `uninstall` are each one command on one line, with no tab.
|
|
27
|
-
- Each names at least one script file inside the mod, and no script at the mod's root.
|
|
27
|
+
- Each names at least one script file inside the mod, and no script at the mod's root. cmod asks consent for the whole folder of each script, so put the scripts in a folder, such as `setup/`.
|
|
28
28
|
- A symbolic link in a script folder points at a file.
|
|
29
29
|
- `program` is a command name: letters, digits, `.`, `_`, and `-`, starting with a letter or digit.
|
|
30
30
|
- `.claude-plugin/plugin.json` has a `version`.
|
|
31
31
|
|
|
32
32
|
## Consent
|
|
33
33
|
|
|
34
|
-
Before a step runs for the first time,
|
|
34
|
+
Before a step runs for the first time, cmod shows the person the commands and asks to run them. cmod hashes the commands and every file in each script folder, and remembers the hash the person approved. A change to any of those files asks again.
|
|
35
35
|
|
|
36
36
|
- In Claude Code, the mod asks `<mod> runs <install> to install, and <uninstall> when you remove it. Run it now?` with `Install` and `Not now`. A progress line above the prompt shows the install.
|
|
37
37
|
- In a session with no screen, such as `claude -p`, the mod waits and logs `<mod> waits for consent to run <install>. Run cmod install <mod> in a terminal.`
|
|
@@ -42,14 +42,14 @@ The mod starts only once its install step has run ([mod.md](mod.md)).
|
|
|
42
42
|
|
|
43
43
|
## How a step runs
|
|
44
44
|
|
|
45
|
-
|
|
45
|
+
cmod runs a step with `sh -c "<command>"` in the mod's folder. So `./setup/install.sh` runs the script itself, and the script must be executable: `chmod +x setup/*.sh`.
|
|
46
46
|
|
|
47
47
|
The step gets these environment variables:
|
|
48
48
|
|
|
49
49
|
| Variable | Value |
|
|
50
50
|
| --- | --- |
|
|
51
51
|
| `CMOD_PLUGIN_ROOT` | The mod's folder |
|
|
52
|
-
| `CMOD_DATA` | The mod's data folder, which
|
|
52
|
+
| `CMOD_DATA` | The mod's data folder, which cmod creates first. It is the folder `mod.dataFolder` names. |
|
|
53
53
|
| `CMOD_VERSION` | The mod's version from `plugin.json` |
|
|
54
54
|
| `PATH` | The person's `PATH`, with the cmod program's own folder first, so the step can run `cmod download` |
|
|
55
55
|
|
|
@@ -63,18 +63,18 @@ The install step runs again on every new version of the mod and whenever its scr
|
|
|
63
63
|
|
|
64
64
|
## When the install step fails
|
|
65
65
|
|
|
66
|
-
- When the first install of a mod fails or is stopped,
|
|
67
|
-
- When the install step of an upgrade fails or is stopped,
|
|
66
|
+
- When the first install of a mod fails or is stopped, cmod runs the uninstall step to undo it. So the uninstall step must work on a partial install.
|
|
67
|
+
- When the install step of an upgrade fails or is stopped, cmod keeps the version it set up before.
|
|
68
68
|
- The failure shows on the mod's progress line with `Fix the cause, then run: cmod install <mod>`.
|
|
69
69
|
|
|
70
70
|
## The uninstall step
|
|
71
71
|
|
|
72
|
-
When the install step succeeds,
|
|
72
|
+
When the install step succeeds, cmod saves a copy of every script folder the uninstall step names. It runs the uninstall step from that copy, so it runs after the mod's own folder is gone.
|
|
73
73
|
|
|
74
|
-
- After the person removes the mod with `/plugin uninstall`, the
|
|
74
|
+
- After the person removes the mod with `/plugin uninstall`, the cmod plugin runs the uninstall step at the next session start or prompt.
|
|
75
75
|
- `cmod remove <mod>` uninstalls the mod and runs the step at once.
|
|
76
76
|
- In the copy, `CMOD_PLUGIN_ROOT` is the copy's folder, and `CMOD_DATA` and `CMOD_VERSION` are the same as at install.
|
|
77
|
-
- After the step,
|
|
77
|
+
- After the step, cmod deletes the mod's install record, its program, its data folder, and the approvals of its scripts. It keeps the mod's config folders, because the files there are the person's.
|
|
78
78
|
- A failed uninstall step keeps the record. `cmod teardown <mod>` runs it again.
|
|
79
79
|
|
|
80
80
|
## Example
|
|
@@ -102,7 +102,7 @@ cat "$kept" > "$HOME/.zshrc"
|
|
|
102
102
|
rm "$kept"
|
|
103
103
|
```
|
|
104
104
|
|
|
105
|
-
|
|
105
|
+
cmod deletes `$CMOD_DATA` itself after the uninstall step.
|
|
106
106
|
|
|
107
107
|
## Download a program in an install step
|
|
108
108
|
|
|
@@ -122,7 +122,7 @@ cmod download mermaid-ascii \
|
|
|
122
122
|
|
|
123
123
|
## Ship a program
|
|
124
124
|
|
|
125
|
-
A mod that builds its own command-line program keeps its source in `cli/`, and
|
|
125
|
+
A mod that builds its own command-line program keeps its source in `cli/`, and cmod builds it, releases it, and puts it on the person's `PATH`.
|
|
126
126
|
|
|
127
127
|
1. `cli/package.json` declares the build:
|
|
128
128
|
|
|
@@ -144,8 +144,8 @@ Then:
|
|
|
144
144
|
|
|
145
145
|
- `cmod link` builds the program and links this machine's build to `~/.local/bin/<program>`.
|
|
146
146
|
- `cmod publish` builds every machine's file and attaches each to the GitHub release `v<version>`, with a `SHA256SUMS` file. It refuses a `cli/` program that the `package.json` `program` key does not name.
|
|
147
|
-
- On install,
|
|
148
|
-
-
|
|
147
|
+
- On install, cmod downloads `<repository>/releases/download/v<version>/<program>-<machine>`, checks it against `SHA256SUMS`, runs `<program> --version`, and links it to `~/.local/bin/<program>`.
|
|
148
|
+
- cmod refuses to replace a `~/.local/bin/<program>` it did not make, and a program `PATH` already finds somewhere else.
|
|
149
149
|
- `cmod unlink` and the uninstall remove the program.
|
|
150
150
|
|
|
151
|
-
The
|
|
151
|
+
The cmod repository ships the `cmod` program this way. Its `package.json` holds `"cmod": { "program": "cmod" }`, and its `cli/package.json` holds `"cmod": { "build": "bun run build", "output": "dist" }`.
|
package/docs/jobs.md
CHANGED
|
@@ -22,7 +22,7 @@ A job's bad options throw at one of two times. `check`, `permissions`, and `prom
|
|
|
22
22
|
| `statusLine` | `jobs/status-line.js` | A status line |
|
|
23
23
|
| `program` | `jobs/program.js` | A background program the mod talks to over HTTP |
|
|
24
24
|
|
|
25
|
-
Each job announces itself. The first time a version starts, Claude Mod Manager (
|
|
25
|
+
Each job announces itself. The first time a version starts, Claude Mod Manager (cmod) logs what the mod added, such as `tasks added /tasks, the open_ticket tool and 2 permission rules.`
|
|
26
26
|
|
|
27
27
|
## Deadlines
|
|
28
28
|
|
|
@@ -57,11 +57,11 @@ type Reply = string | { text?: string; context?: string } | undefined
|
|
|
57
57
|
- `description` is the line the typeahead and `/help` show.
|
|
58
58
|
- `argumentHint` is drawn dim after the name, such as `[path]`.
|
|
59
59
|
- `immediate: true` runs the command at once while Claude is still working. Without it, the command waits for the turn to end.
|
|
60
|
-
- `reply` gets `args` and `positionals`. `args` is everything after the name as Claude Code passes it, and `''` for a bare `/<name>`.
|
|
60
|
+
- `reply` gets `args` and `positionals`. `args` is everything after the name as Claude Code passes it, and `''` for a bare `/<name>`. cmod hands it on unchanged. `positionals` is that text split into words the way a shell splits them, so extra spaces never reach it. `tested.type('/todo milk')` drops the spaces after the name.
|
|
61
61
|
- A `string` reply, or `text`, shows as the command's output. `context` is text Claude reads after the output, and the person never sees. `undefined` shows nothing.
|
|
62
62
|
- A `reply` that throws shows `/<name> failed: <error>`.
|
|
63
63
|
- Two commands with one name in a mod throw `<mod>: the slash command /<name> is already added. Give each slashCommand its own name.`
|
|
64
|
-
- When Claude Code refuses to add the command,
|
|
64
|
+
- When Claude Code refuses to add the command, cmod logs `<mod>: /<name> is not added: <error>`.
|
|
65
65
|
|
|
66
66
|
```ts
|
|
67
67
|
import { defineMod } from '../node_modules/@cmodjs/core/mod.js'
|
|
@@ -99,7 +99,7 @@ tool<Schema, State>(options: {
|
|
|
99
99
|
|
|
100
100
|
- `name` is 1 to 64 letters, digits, `_`, or `-`. Claude sees the tool as `mcp__<plugin name>__<name>`, and the handle's `name` is that full name.
|
|
101
101
|
- `description` tells Claude what the tool does.
|
|
102
|
-
- `inputSchema` is a JSON Schema with `type: 'object'`. It types `execute`'s `input`, so write it inline.
|
|
102
|
+
- `inputSchema` is a JSON Schema with `type: 'object'`. It types `execute`'s `input`, so write it inline. cmod checks each call's input against it, draft 2020-12, and denies an input that does not fit with the list of its errors. Its properties cannot be named `tool`, `tool_use_id`, `consent`, or `agentId`, which Claude Code keeps for itself.
|
|
103
103
|
- `execute` returns the result Claude reads. A `string` and an array of content blocks go to Claude as they are. Any other value goes as JSON. `undefined` returns nothing.
|
|
104
104
|
- An input that does not fit the schema is denied with one line per error under a heading, such as `The open_ticket tool input is not valid:` followed by `- #: Instance does not have required property "title".` The text after `- ` is the location in the input, `#` for its root, and the validator's message.
|
|
105
105
|
- An `execute` that throws denies the call with `The <name> tool failed: <error>`.
|
|
@@ -147,13 +147,13 @@ export const tickets = defineMod({
|
|
|
147
147
|
|
|
148
148
|
### Command patterns
|
|
149
149
|
|
|
150
|
-
A `command` pattern is not a glob.
|
|
150
|
+
A `command` pattern is not a glob. cmod splits it into words the way it splits a Bash command, and matches each command of the call:
|
|
151
151
|
|
|
152
152
|
- `'*'` alone matches every shell command. A `*` inside a pattern is a plain character.
|
|
153
153
|
- The first word is the program, matched by its name, so `/bin/rm` counts as `rm`.
|
|
154
154
|
- Every other word that does not start with `-` must appear in the command in the pattern's order, and other words may sit between them: `git push` matches `git push origin main`.
|
|
155
155
|
- Every flag, a word that starts with `-`, must appear somewhere in the command, in any order. `--force` also matches `--force=yes`.
|
|
156
|
-
-
|
|
156
|
+
- cmod splits a cluster of short flags into single flags, in the pattern and in the command alike, so `rm -rf` matches `rm -rf`, `rm -fr`, and `rm -r -f`. A letter that takes a value keeps the rest of the cluster as its value. These programs keep their flags as written: `find`, `java`, `javac`, `go`, `gcc`, `g++`, `clang`, `clang++`, `swift`, `swiftc`, `xcodebuild`, `xcrun`, `ffmpeg`, `ffprobe`, `openssl`, `plutil`, `defaults`, `security`, and `codesign`.
|
|
157
157
|
- Words and flags match exactly, letter case included.
|
|
158
158
|
- Each command of a pipeline or a `&&` chain counts, and so does the command a wrapper runs: `sudo rm -rf /` holds the commands `sudo` and `rm -rf /`. The wrappers are `sudo`, `env`, `doas`, `timeout`, `flock`, `stdbuf`, `watch`, `nohup`, `nice`, `time`, `exec`, `command`, and `xargs`.
|
|
159
159
|
|
|
@@ -168,11 +168,11 @@ type Rule<State> = Target & {
|
|
|
168
168
|
}
|
|
169
169
|
```
|
|
170
170
|
|
|
171
|
-
- Before each call,
|
|
171
|
+
- Before each call, cmod tries every `deny` rule, then every `ask` rule. The first rule that matches decides, and its `reason` goes with the decision. No match leaves the call to Claude Code.
|
|
172
172
|
- The strictest answer wins: a mod's `ask` or `deny` beats Claude Code's `allow`, and Claude Code's `deny` beats a mod's `ask`.
|
|
173
173
|
- `when` narrows a rule. The rule matches only when `when` resolves true. A `when` that throws makes the rule match, and the reason adds `Its when check failed: <error>`.
|
|
174
|
-
- When
|
|
175
|
-
- For a PowerShell command, or a Bash command
|
|
174
|
+
- When cmod cannot read the call or the session, it denies the call with the reason.
|
|
175
|
+
- For a PowerShell command, or a Bash command cmod cannot fully parse, a `command` rule matches when every word of its pattern appears in the command line. A `read` or `write` rule matches when the command line holds the last segment of its pattern, after the last `/`, and only when that segment holds none of `*?[]{}`. So `{ write: 'Domain.md' }` matches such a command that names `Domain.md`, and `{ write: '**/.env*' }` never matches one.
|
|
176
176
|
- `permissions({})` throws `permissions: give it a deny or an ask rule, or remove it from setup.`
|
|
177
177
|
- A mod that uses `permissions` calls `registerPermissionCheck(addHook)` after `registerMod` in `hooks/register.ts` ([mod.md](mod.md)). Without it the mod does not start.
|
|
178
178
|
|
|
@@ -215,8 +215,8 @@ export const guard = defineMod({
|
|
|
215
215
|
check<State>(options: { readonly after: Target | Target[]; readonly run: readonly string[]; readonly timeoutMs?: number }): Job<void, State>
|
|
216
216
|
```
|
|
217
217
|
|
|
218
|
-
- After each call that matches `after` and succeeds,
|
|
219
|
-
- For a `read` or `write` target,
|
|
218
|
+
- After each call that matches `after` and succeeds, cmod runs `run`. A call that was denied or failed runs nothing.
|
|
219
|
+
- For a `read` or `write` target, cmod adds the matched files that still exist to the end of `run`, as absolute paths. For any other target it runs `run` as it is.
|
|
220
220
|
- A mod installed for the person passes no folder, so Claude Code runs the command in the session's working folder. A project plugin runs it at the root of the work tree that holds the last matched file, or the matched command's folder.
|
|
221
221
|
- A command that exits with another code than 0 adds `<command> exited with <code>:` and its output to the call's result, so Claude reads the failure right after its own call.
|
|
222
222
|
- `timeoutMs` is 60 seconds by default and 10 minutes at most.
|
|
@@ -284,7 +284,7 @@ type Usage = { model: string; context: { tokens?: number; window: number; percen
|
|
|
284
284
|
```
|
|
285
285
|
|
|
286
286
|
- `text` returns the status line's text, or `undefined` to clear it.
|
|
287
|
-
-
|
|
287
|
+
- cmod calls `text` when the mod starts, every `interval` milliseconds (10000 by default), and whenever Claude Code measures the session. It sends the text only when it changed.
|
|
288
288
|
- A `text` that throws keeps the last text, and the error goes to Claude Code's debug log.
|
|
289
289
|
- A mod has one status line. A second `statusLine` throws: join the texts in one.
|
|
290
290
|
- An `interval` below 1 throws.
|
|
@@ -317,7 +317,7 @@ type Program = {
|
|
|
317
317
|
- `command` is the program on `PATH` and its arguments. `environment` sets variables for it. An empty `command` throws.
|
|
318
318
|
- The program's first line on standard output must be its address: `127.0.0.1:<port>`, or `unix:<absolute socket path>`. `ready()` then resolves `{ url: 'http://127.0.0.1:<port>' }`, or `{ url: 'http://localhost', socketPath }`. Pass both to `mod.http.fetch`.
|
|
319
319
|
- In an interactive session the program starts with the mod. Otherwise it starts at the first `ready()`.
|
|
320
|
-
- When the program exits,
|
|
320
|
+
- When the program exits, cmod starts it again after 1, 2, 4, 8, and 16 seconds. After 5 failed tries, or a first line that is not an address, the state is `fatal`, `ready()` rejects, and cmod logs `<mod>: the <program> program stopped: <reason>`.
|
|
321
321
|
- A program that ships with the mod gets onto `PATH` through the `program` key of the mod's `package.json` ([install-steps.md](install-steps.md)). A program an install step fetched with `cmod download` is not on `PATH`, so `command` names it by its full path, `${mod.dataFolder}/bin/<program>`.
|
|
322
322
|
|
|
323
323
|
```ts
|
|
@@ -360,12 +360,12 @@ type JobContext<State> = {
|
|
|
360
360
|
```
|
|
361
361
|
|
|
362
362
|
- `claude` reaches Claude Code itself. [The claude members](#the-claude-members) lists each one.
|
|
363
|
-
- `claude.store` is where
|
|
363
|
+
- `claude.store` is where cmod saves `mod.state`, under keys that start with `<mod>.`, such as `<mod>.<key>`. A job of your own never writes those keys, or it changes the mod's state behind cmod's back.
|
|
364
364
|
- `on` adds a Claude Code hook-module handler, `(e, next) => …`, on a `classic.<ModEvent>` event or on `tool.check`, `tool.call`, `prompt.submit`, `prompt.context`, `command.run`, `session.measure`, `skill.prompt`, `ui.render`, `ui.press`, `ui.close`, or `cmod.call`. The handler calls `next(e)` to pass the event on. A job with a handler on `tool.check`, `classic.PreToolUse`, or `classic.PermissionRequest` needs `registerPermissionCheck` in `hooks/register.ts` ([mod.md](mod.md)).
|
|
365
|
-
- `announce` adds a phrase to the list
|
|
365
|
+
- `announce` adds a phrase to the list cmod logs when the mod first starts.
|
|
366
366
|
- `reserveName` throws `taken` when the mod already reserved that `kind` and `name`. A job calls it to refuse a duplicate.
|
|
367
367
|
- `toolCalls.agentOf(toolUseId)` resolves `{ agentId?, agentType? }`, the subagent behind a call.
|
|
368
|
-
- `toolCalls.cwdOf(toolUseId)` returns the working folder when the call ran, or `undefined` once its `PostToolUse` or `PostToolUseFailure` has run.
|
|
368
|
+
- `toolCalls.cwdOf(toolUseId)` returns the working folder when the call ran, or `undefined` once its `PostToolUse` or `PostToolUseFailure` has run. cmod keeps the folders of the last 100 calls.
|
|
369
369
|
|
|
370
370
|
```ts
|
|
371
371
|
import { defineMod, type Job } from '../node_modules/@cmodjs/core/mod.js'
|
|
@@ -413,7 +413,7 @@ Each member calls the member of the same name in Claude Code's hooks API, `$`, w
|
|
|
413
413
|
| `fs.stat(path, options?)` | Resolves `{ kind, size, mtimeMs, isLink }`, and `realPath` with `{ resolve: true }`. It rejects for a missing path. |
|
|
414
414
|
| `http.fetch(url, init?)` | What `mod.http.fetch` calls. |
|
|
415
415
|
| `settings.read(args?)` | What `mod.settings.read` calls. |
|
|
416
|
-
| `store.get`, `store.set`, `store.delete`, `store.keys` | The plugin's key-value store, where
|
|
416
|
+
| `store.get`, `store.set`, `store.delete`, `store.keys` | The plugin's key-value store, where cmod also keeps `mod.state`. |
|
|
417
417
|
| `clock.now()` | Resolves milliseconds since the epoch. |
|
|
418
418
|
| `clock.after(ms, fn)` | Calls `fn` once after `ms` milliseconds, and returns `{ cancel() }`. |
|
|
419
419
|
| `clock.every(ms, fn)` | Calls `fn` every `ms` milliseconds, at least 1, and returns `{ cancel() }`. |
|
package/docs/mod.md
CHANGED
|
@@ -17,7 +17,7 @@ type ModDefinition<State, Name> = {
|
|
|
17
17
|
}
|
|
18
18
|
```
|
|
19
19
|
|
|
20
|
-
- `name` is the mod's name. Use the `name` in `.claude-plugin/plugin.json`. Claude Mod Manager (
|
|
20
|
+
- `name` is the mod's name. Use the `name` in `.claude-plugin/plugin.json`. Claude Mod Manager (cmod) keys the saved state and the `state.json` files by this name, and the data folder and the Skill overrides by the plugin's name, so the two must match. `defineMod` throws `defineMod: the mod needs a name, such as the name in .claude-plugin/plugin.json.` for a blank name.
|
|
21
21
|
- `state` holds the starting values, grouped by how long they last. [state.md](state.md) explains the groups.
|
|
22
22
|
- `api` holds the methods other mods call. [dependencies.md](dependencies.md) explains it.
|
|
23
23
|
- `setup` runs once when the mod starts. It adds hooks, panes, slot renders, and jobs. It may be `async`.
|
|
@@ -41,9 +41,9 @@ export const greeter = defineMod({
|
|
|
41
41
|
|
|
42
42
|
## When setup runs
|
|
43
43
|
|
|
44
|
-
Claude Code loads the mod at the start of each session.
|
|
44
|
+
Claude Code loads the mod at the start of each session. cmod then checks that the mod is set up. A mod with an install step that has not run waits for the person's consent and the install first ([install-steps.md](install-steps.md)). Then cmod loads the saved state, runs `setup`, and marks as open each of the mod's panes that Claude Code already shows.
|
|
45
45
|
|
|
46
|
-
The first time a version starts,
|
|
46
|
+
The first time a version starts, cmod shows the toast `<name> is ready` and logs `<name> added <what setup added>`, such as `the my-mod pane, a render of AbovePrompt and a hook on UserPromptSubmit`.
|
|
47
47
|
|
|
48
48
|
When a step fails, the mod does not start. Its progress line above the prompt names the failure and the fix:
|
|
49
49
|
|
|
@@ -115,7 +115,7 @@ type Mod<State> = {
|
|
|
115
115
|
| --- | --- | --- |
|
|
116
116
|
| `name` | The mod's name from `defineMod`. | |
|
|
117
117
|
| `state` | The mod's values. Assign to a key to change it. | [state.md](state.md) |
|
|
118
|
-
| `dataFolder` | The mod's own folder in the
|
|
118
|
+
| `dataFolder` | The mod's own folder in the cmod store: `$XDG_DATA_HOME/cmod/data/<plugin name>`, or `~/.local/share/cmod/data/<plugin name>` when `XDG_DATA_HOME` is not set. The install step gets the same folder as `CMOD_DATA`. cmod deletes it when the mod is removed. | [install-steps.md](install-steps.md) |
|
|
119
119
|
| `projectRoot` | The project the session works in. It follows `/cd`. | |
|
|
120
120
|
| `cwd` | The session's working folder. It follows a `cd` in a Bash or PowerShell call, and `/cd`. | |
|
|
121
121
|
| `on` | Adds a hook on a Claude Code event. | [hooks.md](hooks.md) |
|
|
@@ -145,7 +145,7 @@ Both run a program without a shell. `argv[0]` is the program and the rest are it
|
|
|
145
145
|
- `spawn` streams `{ stream: 'stdout' | 'stderr', text }` pieces as the program writes them. Its `result` resolves `{ code, signal }` once the stream is read to its end. Its first pull rejects when the program cannot start.
|
|
146
146
|
- The `for await` loop over a `spawn` is the program's life. Leaving the loop, or calling `return()` on the stream, kills the program, and so does unloading the mod. Returning from the hook does not.
|
|
147
147
|
|
|
148
|
-
Inside a job,
|
|
148
|
+
Inside a job, cmod adds the job's deadline to each call ([jobs.md](jobs.md)).
|
|
149
149
|
|
|
150
150
|
```ts
|
|
151
151
|
import { defineMod } from '../node_modules/@cmodjs/core/mod.js'
|
package/docs/state.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# State
|
|
2
2
|
|
|
3
|
-
`mod.state` holds the mod's values. Claude Mod Manager (
|
|
3
|
+
`mod.state` holds the mod's values. Claude Mod Manager (cmod) saves them, redraws the mod's panes and slot renders when one changes, and lets a person change the starting values without touching the mod's code.
|
|
4
4
|
|
|
5
5
|
## Declare the state
|
|
6
6
|
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
| `project` | This project, in every conversation. | Yes |
|
|
14
14
|
| `global` | Every project. | Yes |
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
cmod keeps each `session` value for the 20 sessions that wrote it last, and each `project` value for the 20 projects that wrote it last. A session or project past those 20 starts that value over from its starting value.
|
|
17
17
|
|
|
18
18
|
Put values the mod works out again on every prompt, such as a git status, in `memory`.
|
|
19
19
|
|
|
@@ -56,7 +56,7 @@ A group other than `memory`, `session`, `project`, and `global` throws `<mod>: s
|
|
|
56
56
|
|
|
57
57
|
## Change a value
|
|
58
58
|
|
|
59
|
-
Assign to a key.
|
|
59
|
+
Assign to a key. cmod saves the new value and redraws the mod.
|
|
60
60
|
|
|
61
61
|
- A key the state does not declare throws `<mod>: mod.state.<group>.<key> is not declared. Add it to state.<group> in defineMod with its starting value.`
|
|
62
62
|
- An object or an array in the state is frozen. Changing it in place throws `<mod>: mod.state.<path> cannot change in place.` Assign a new value instead:
|
|
@@ -75,8 +75,8 @@ export const reads = defineMod({
|
|
|
75
75
|
})
|
|
76
76
|
```
|
|
77
77
|
|
|
78
|
-
- Keep only JSON data in a saved group. A value
|
|
79
|
-
-
|
|
78
|
+
- Keep only JSON data in a saved group. A value cmod cannot save logs `<mod> could not keep state.<group>.<key>: <error>. Keep only JSON data in mod.state.`
|
|
79
|
+
- cmod saves values one at a time in the order they were assigned.
|
|
80
80
|
|
|
81
81
|
## Where the values come from
|
|
82
82
|
|
|
@@ -87,7 +87,7 @@ When the mod starts, each value is, in order of precedence:
|
|
|
87
87
|
3. The value in the person's own `state.json`.
|
|
88
88
|
4. The starting value in `defineMod`.
|
|
89
89
|
|
|
90
|
-
After a `/cd` to another project,
|
|
90
|
+
After a `/cd` to another project, cmod loads that project's `project` values and reads its `state.json` again.
|
|
91
91
|
|
|
92
92
|
## Let a person change the starting values
|
|
93
93
|
|
|
@@ -111,7 +111,7 @@ A `state.json` in either folder lists only the values to change:
|
|
|
111
111
|
- A project `state.json` cannot set `global` values, because one repository cannot change a value for every project.
|
|
112
112
|
- A file cannot set `memory` values.
|
|
113
113
|
|
|
114
|
-
|
|
114
|
+
cmod ignores each entry it cannot use and logs one line naming the file, the entry, and the fix:
|
|
115
115
|
|
|
116
116
|
- a file that is not JSON, or not a JSON object
|
|
117
117
|
- a group the mod does not declare
|
|
@@ -131,7 +131,7 @@ A person replaces the text of a Skill the mod ships by putting a file at the sam
|
|
|
131
131
|
|
|
132
132
|
- Claude reads that file in place of the mod's from the next time the Skill loads, and updates of the mod keep it.
|
|
133
133
|
- The project file wins over the person's file.
|
|
134
|
-
-
|
|
134
|
+
- cmod drops the file's frontmatter, so the mod's own name and description stay.
|
|
135
135
|
- Only a Skill the mod ships in `skills/<skill>/` is replaced, and only when it loads as `<mod>:<skill>`.
|
|
136
136
|
- Deleting the file brings the mod's text back.
|
|
137
137
|
|
package/docs/ui.md
CHANGED
|
@@ -10,7 +10,7 @@ mod.ui.progress<T>(title: string, task: (report: (step: ProgressStep) => void) =
|
|
|
10
10
|
mod.ui.ask(question: string, options?: readonly string[] | AskOptions): Promise<string>
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
-
Claude Mod Manager (
|
|
13
|
+
Claude Mod Manager (cmod) draws the mod again whenever a value in `mod.state` changes. A render reads `mod.state` and returns what to show. It never draws by hand.
|
|
14
14
|
|
|
15
15
|
## Elements
|
|
16
16
|
|
|
@@ -46,7 +46,7 @@ export function Count({ label, count }: { readonly label: string; readonly count
|
|
|
46
46
|
|
|
47
47
|
An element called outside a render throws `<Element> was called outside a render. Use it inside a pane's render or a slot's component.` So build elements inside a pane's `render` or a slot's component, never at module load or in a hook.
|
|
48
48
|
|
|
49
|
-
`drawWith(table, draw, markdown?)` is the function
|
|
49
|
+
`drawWith(table, draw, markdown?)` is the function cmod draws a render with. A mod never calls it.
|
|
50
50
|
|
|
51
51
|
## Panes
|
|
52
52
|
|
|
@@ -68,7 +68,7 @@ type Pane<State> = {
|
|
|
68
68
|
|
|
69
69
|
- `id` is 1 to 64 letters, digits, `_`, or `-`. Another id throws `definePane: "<id>" is not a pane id.`
|
|
70
70
|
- `title` labels the pane's tab while more than one pane is open.
|
|
71
|
-
- `columns` and `rows` ask for a size. A number must be a whole number above 0, or `definePane` throws. A function reads the state and returns the size, or `undefined` for Claude Code's default. When the state changes the size,
|
|
71
|
+
- `columns` and `rows` ask for a size. A number must be a whole number above 0, or `definePane` throws. A function reads the state and returns the size, or `undefined` for Claude Code's default. When the state changes the size, cmod resizes an open pane. A function that returns a bad size keeps an open pane at its size and logs the pane's title to the debug log. `open`, and `toggle` on a closed pane, reject with that size instead.
|
|
72
72
|
- `render` draws the pane's body. `props` holds `title`, `isFocused`, `bodyColumns`, `placement` (`'dock'` or `'inline'`), `scroll`, the body's window over a taller drawing, and `view`, which transcript is on screen beside the pane.
|
|
73
73
|
|
|
74
74
|
Put each pane in its own file in `src/panes/`.
|
|
@@ -106,13 +106,13 @@ type PaneHandle = {
|
|
|
106
106
|
```
|
|
107
107
|
|
|
108
108
|
- `open` asks Claude Code to show the pane. `isOpen` turns true once Claude Code places it. Claude Code may hold a pane back, and then `isOpen` stays false.
|
|
109
|
-
- `mod.ui.pane` only adds the pane.
|
|
109
|
+
- `mod.ui.pane` only adds the pane. cmod opens it only when the mod calls `open` or `toggle`, and Claude Code shows a pane only once the plugin has opened it. So give the person a way in, such as a slash command whose `reply` calls `pane.toggle()`.
|
|
110
110
|
- A pane opened from what the person did, such as a slash command they typed or a `Button` they pressed, shows at any terminal width. A pane opened from anything else, such as `setup`, a timer, or `SessionStart`, shows only on a terminal 144 columns wide, or 110 for a pane the person opened before. Below that it waits undrawn until the person opens it or the terminal widens.
|
|
111
111
|
- `close` closes it. The person closes a pane too, and `isOpen` follows.
|
|
112
112
|
- `toggle` closes an open pane and opens a closed one.
|
|
113
113
|
- When the mod starts, `isOpen` is true for each of its panes Claude Code already shows.
|
|
114
114
|
- Adding two panes with one id throws `<mod>: the pane "<id>" is already added. Give each pane its own id.`
|
|
115
|
-
- The line
|
|
115
|
+
- The line cmod logs when the mod first starts names the pane by its `title`, as in `the Notes pane`.
|
|
116
116
|
|
|
117
117
|
```tsx
|
|
118
118
|
import { defineMod } from '../node_modules/@cmodjs/core/mod.js'
|
|
@@ -157,7 +157,7 @@ export const notes = defineMod({
|
|
|
157
157
|
- `Default` draws what Claude Code, and the renders of other mods, would draw. Pass it props to change what it draws, such as `<Default hint="…" />`. Return `<Default />` to change nothing.
|
|
158
158
|
- A render adds to what it wraps. In `AbovePrompt`, drawing `<Default />` and a line under it keeps the lines of Claude Code and other mods.
|
|
159
159
|
- A render must draw the same for the same props. One that calls `Default` a different number of times on a second draw falls back to Claude Code's drawing.
|
|
160
|
-
- A render that throws falls back to Claude Code's drawing.
|
|
160
|
+
- A render that throws falls back to Claude Code's drawing. cmod logs `<mod>: the <Slot> render threw, so Claude Code draws its own: <error>` once.
|
|
161
161
|
- A mod renders each slot once. A second `mod.ui.render` of one slot throws `<mod>: a render of <Slot> is already added. Render each slot once.` For a markdown slot the error names it as `a render of markdown <Kind>`, such as `a render of markdown CodeBlock`.
|
|
162
162
|
- `slots.ToolUse` changes a call's own row. A group of calls shows the `slots.ToolGroup` row, which Claude Code builds from the stored message, so a mod that hides a tool's input also sets `isExpanded` on `slots.ToolGroup` to unfold the group into `ToolUse` rows.
|
|
163
163
|
- No slot covers the permission dialog.
|
package/package.json
CHANGED
package/records.js
CHANGED
|
@@ -6,7 +6,7 @@ export function storeFolder(env) {
|
|
|
6
6
|
if (dataHome)
|
|
7
7
|
return `${dataHome}/cmod`;
|
|
8
8
|
if (!home)
|
|
9
|
-
throw new Error('Neither XDG_DATA_HOME nor HOME is set, so the
|
|
9
|
+
throw new Error('Neither XDG_DATA_HOME nor HOME is set, so the cmod store has no folder. Set HOME.');
|
|
10
10
|
return `${home}/.local/share/cmod`;
|
|
11
11
|
}
|
|
12
12
|
export function recordPath(store, name) {
|
|
@@ -56,7 +56,7 @@ function parseRecord(text, path) {
|
|
|
56
56
|
throw new Error(`${path} is not JSON (${messageOf(error)}). ${fix}`);
|
|
57
57
|
}
|
|
58
58
|
if (!isObject(value))
|
|
59
|
-
throw new Error(`${path} is not a
|
|
59
|
+
throw new Error(`${path} is not a cmod install record. ${fix}`);
|
|
60
60
|
for (const key of ['name', 'version', 'root', 'installedAt', 'scriptsSha256']) {
|
|
61
61
|
if (typeof value[key] !== 'string')
|
|
62
62
|
throw new Error(`${path} has no "${key}" text. ${fix}`);
|
|
@@ -102,7 +102,7 @@ function readCommand(steps, key) {
|
|
|
102
102
|
throw new Error(`package.json "cmod.${key}" holds a line break or a tab. Write one command, such as "./setup/${key}.sh".`);
|
|
103
103
|
const atRoot = scriptPaths(command).find((path) => folderOf(path) === '.');
|
|
104
104
|
if (atRoot !== undefined)
|
|
105
|
-
throw new Error(`package.json "cmod.${key}" names ${atRoot}, a script at the plugin root. Move it into a folder, such as ./setup/${key}.sh:
|
|
105
|
+
throw new Error(`package.json "cmod.${key}" names ${atRoot}, a script at the plugin root. Move it into a folder, such as ./setup/${key}.sh: cmod asks consent for the whole folder of each script.`);
|
|
106
106
|
return { [key]: command };
|
|
107
107
|
}
|
|
108
108
|
export function scriptPaths(command) {
|
package/runtime/lifecycle.js
CHANGED
|
@@ -127,7 +127,7 @@ export function createLifecycle(definition, checksPermissions = () => true) {
|
|
|
127
127
|
};
|
|
128
128
|
const record = () => {
|
|
129
129
|
recording ??= recordThroughCmod()
|
|
130
|
-
.catch((error) => claude().ui.log(`${definition.name} has no
|
|
130
|
+
.catch((error) => claude().ui.log(`${definition.name} has no cmod record yet: ${messageOf(error)}`, { to: 'debug' }))
|
|
131
131
|
.finally(() => {
|
|
132
132
|
recording = undefined;
|
|
133
133
|
});
|
|
@@ -186,7 +186,7 @@ export function createLifecycle(definition, checksPermissions = () => true) {
|
|
|
186
186
|
return bootstrap(plugin.root);
|
|
187
187
|
phase = 'installing';
|
|
188
188
|
if ((await cmodVersion(claude())) === undefined)
|
|
189
|
-
return
|
|
189
|
+
return waitForcmod();
|
|
190
190
|
endLine();
|
|
191
191
|
const progress = showLine();
|
|
192
192
|
let outcome;
|
|
@@ -205,9 +205,9 @@ export function createLifecycle(definition, checksPermissions = () => true) {
|
|
|
205
205
|
const reason = outcome?.kind === 'failed' ? outcome.message : `cmod setup ${formatExit(code, lastError)}`;
|
|
206
206
|
fail(reason, `Fix the cause, then run: cmod install ${definition.name}`);
|
|
207
207
|
};
|
|
208
|
-
const
|
|
208
|
+
const waitForcmod = () => {
|
|
209
209
|
phase = 'waiting';
|
|
210
|
-
showLine().wait('Waiting for
|
|
210
|
+
showLine().wait('Waiting for cmod');
|
|
211
211
|
let check;
|
|
212
212
|
const stop = () => {
|
|
213
213
|
timer.cancel();
|
|
@@ -227,7 +227,7 @@ export function createLifecycle(definition, checksPermissions = () => true) {
|
|
|
227
227
|
report(error);
|
|
228
228
|
});
|
|
229
229
|
});
|
|
230
|
-
const longWait = claude().clock.after(cmodWaitMs, () => showLine().wait("Still waiting for
|
|
230
|
+
const longWait = claude().clock.after(cmodWaitMs, () => showLine().wait("Still waiting for cmod to download cmod. See cmod's own line."));
|
|
231
231
|
};
|
|
232
232
|
const bootstrap = async (root) => {
|
|
233
233
|
const progress = showLine();
|
package/src/records.ts
CHANGED
|
@@ -28,7 +28,7 @@ export function storeFolder(env: Record<string, string | undefined>): string {
|
|
|
28
28
|
const dataHome = env['XDG_DATA_HOME']
|
|
29
29
|
const home = env['HOME']
|
|
30
30
|
if (dataHome) return `${dataHome}/cmod`
|
|
31
|
-
if (!home) throw new Error('Neither XDG_DATA_HOME nor HOME is set, so the
|
|
31
|
+
if (!home) throw new Error('Neither XDG_DATA_HOME nor HOME is set, so the cmod store has no folder. Set HOME.')
|
|
32
32
|
return `${home}/.local/share/cmod`
|
|
33
33
|
}
|
|
34
34
|
|
|
@@ -81,7 +81,7 @@ function parseRecord(text: string, path: string): InstallRecord {
|
|
|
81
81
|
} catch (error) {
|
|
82
82
|
throw new Error(`${path} is not JSON (${messageOf(error)}). ${fix}`)
|
|
83
83
|
}
|
|
84
|
-
if (!isObject(value)) throw new Error(`${path} is not a
|
|
84
|
+
if (!isObject(value)) throw new Error(`${path} is not a cmod install record. ${fix}`)
|
|
85
85
|
for (const key of ['name', 'version', 'root', 'installedAt', 'scriptsSha256'] as const) {
|
|
86
86
|
if (typeof value[key] !== 'string') throw new Error(`${path} has no "${key}" text. ${fix}`)
|
|
87
87
|
}
|
|
@@ -117,7 +117,7 @@ function readCommand(steps: Record<string, unknown>, key: 'install' | 'uninstall
|
|
|
117
117
|
if (typeof command !== 'string' || command.trim() === '') throw new Error(`package.json "cmod.${key}" must be a command, such as "./setup/${key}.sh".`)
|
|
118
118
|
if (/[\n\t]/.test(command)) throw new Error(`package.json "cmod.${key}" holds a line break or a tab. Write one command, such as "./setup/${key}.sh".`)
|
|
119
119
|
const atRoot = scriptPaths(command).find((path) => folderOf(path) === '.')
|
|
120
|
-
if (atRoot !== undefined) throw new Error(`package.json "cmod.${key}" names ${atRoot}, a script at the plugin root. Move it into a folder, such as ./setup/${key}.sh:
|
|
120
|
+
if (atRoot !== undefined) throw new Error(`package.json "cmod.${key}" names ${atRoot}, a script at the plugin root. Move it into a folder, such as ./setup/${key}.sh: cmod asks consent for the whole folder of each script.`)
|
|
121
121
|
return { [key]: command }
|
|
122
122
|
}
|
|
123
123
|
|
package/src/runtime/lifecycle.ts
CHANGED
|
@@ -172,7 +172,7 @@ export function createLifecycle<State extends object>(definition: ModDefinition<
|
|
|
172
172
|
|
|
173
173
|
const record = () => {
|
|
174
174
|
recording ??= recordThroughCmod()
|
|
175
|
-
.catch((error: unknown) => claude().ui.log(`${definition.name} has no
|
|
175
|
+
.catch((error: unknown) => claude().ui.log(`${definition.name} has no cmod record yet: ${messageOf(error)}`, { to: 'debug' }))
|
|
176
176
|
.finally(() => {
|
|
177
177
|
recording = undefined
|
|
178
178
|
})
|
|
@@ -227,7 +227,7 @@ export function createLifecycle<State extends object>(definition: ModDefinition<
|
|
|
227
227
|
if (plugin === undefined) return
|
|
228
228
|
if (plugin.name === cmodPluginName) return bootstrap(plugin.root)
|
|
229
229
|
phase = 'installing'
|
|
230
|
-
if ((await cmodVersion(claude())) === undefined) return
|
|
230
|
+
if ((await cmodVersion(claude())) === undefined) return waitForcmod()
|
|
231
231
|
endLine()
|
|
232
232
|
const progress = showLine()
|
|
233
233
|
let outcome: RunnerEvent | undefined
|
|
@@ -243,9 +243,9 @@ export function createLifecycle<State extends object>(definition: ModDefinition<
|
|
|
243
243
|
fail(reason, `Fix the cause, then run: cmod install ${definition.name}`)
|
|
244
244
|
}
|
|
245
245
|
|
|
246
|
-
const
|
|
246
|
+
const waitForcmod = () => {
|
|
247
247
|
phase = 'waiting'
|
|
248
|
-
showLine().wait('Waiting for
|
|
248
|
+
showLine().wait('Waiting for cmod')
|
|
249
249
|
let check: Promise<void> | undefined
|
|
250
250
|
const stop = () => {
|
|
251
251
|
timer.cancel()
|
|
@@ -264,7 +264,7 @@ export function createLifecycle<State extends object>(definition: ModDefinition<
|
|
|
264
264
|
report(error)
|
|
265
265
|
})
|
|
266
266
|
})
|
|
267
|
-
const longWait = claude().clock.after(cmodWaitMs, () => showLine().wait("Still waiting for
|
|
267
|
+
const longWait = claude().clock.after(cmodWaitMs, () => showLine().wait("Still waiting for cmod to download cmod. See cmod's own line."))
|
|
268
268
|
}
|
|
269
269
|
|
|
270
270
|
const bootstrap = async (root: string) => {
|