@cmodjs/core 0.1.13 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/docs/commands.md +31 -19
- package/docs/index.md +14 -4
- package/docs/install-steps.md +30 -4
- package/docs/jobs.md +25 -9
- package/docs/mod.md +103 -37
- package/docs/options.md +93 -0
- package/docs/permissions.md +88 -0
- package/docs/state.md +4 -31
- package/docs/testing.md +13 -2
- package/installer.d.ts +10 -0
- package/installer.d.ts.map +1 -0
- package/installer.js +8 -0
- package/jobs/context.d.ts +2 -2
- package/jobs/context.d.ts.map +1 -1
- package/jobs/context.js +5 -3
- package/jobs/permissions.js +1 -1
- package/jobs/program.d.ts.map +1 -1
- package/jobs/program.js +5 -1
- package/jobs/prompt.js +5 -5
- package/jobs/schedule.d.ts.map +1 -1
- package/jobs/schedule.js +2 -1
- package/jobs/slash-command.d.ts.map +1 -1
- package/jobs/slash-command.js +3 -2
- package/jobs/status-line.d.ts.map +1 -1
- package/jobs/status-line.js +3 -2
- package/jobs/tool.d.ts.map +1 -1
- package/jobs/tool.js +3 -2
- package/mod.d.ts +48 -10
- package/mod.d.ts.map +1 -1
- package/mod.js +3 -0
- package/options.d.ts +58 -0
- package/options.d.ts.map +1 -0
- package/options.js +66 -0
- package/package.json +1 -1
- package/records.d.ts +14 -1
- package/records.d.ts.map +1 -1
- package/records.js +106 -7
- package/register.d.ts +4 -2
- package/register.d.ts.map +1 -1
- package/register.js +8 -2
- package/runtime/claude.d.ts +5 -1
- package/runtime/claude.d.ts.map +1 -1
- package/runtime/grants.d.ts +28 -0
- package/runtime/grants.d.ts.map +1 -0
- package/runtime/grants.js +286 -0
- package/runtime/installer.d.ts +23 -0
- package/runtime/installer.d.ts.map +1 -0
- package/runtime/installer.js +113 -0
- package/runtime/lifecycle.d.ts +6 -3
- package/runtime/lifecycle.d.ts.map +1 -1
- package/runtime/lifecycle.js +220 -36
- package/runtime/metadata.d.ts +20 -0
- package/runtime/metadata.d.ts.map +1 -0
- package/runtime/metadata.js +143 -0
- package/runtime/options.d.ts +21 -0
- package/runtime/options.d.ts.map +1 -0
- package/runtime/options.js +100 -0
- package/runtime/state.d.ts +1 -0
- package/runtime/state.d.ts.map +1 -1
- package/runtime/state.js +5 -75
- package/src/installer.ts +17 -0
- package/src/jobs/context.ts +5 -3
- package/src/jobs/permissions.ts +1 -1
- package/src/jobs/program.ts +4 -1
- package/src/jobs/prompt.ts +5 -5
- package/src/jobs/schedule.ts +2 -1
- package/src/jobs/slash-command.ts +3 -2
- package/src/jobs/status-line.ts +3 -2
- package/src/jobs/tool.ts +3 -2
- package/src/mod.ts +46 -11
- package/src/options.ts +91 -0
- package/src/records.ts +117 -8
- package/src/register.ts +12 -3
- package/src/runtime/claude.ts +2 -0
- package/src/runtime/grants.ts +306 -0
- package/src/runtime/installer.ts +163 -0
- package/src/runtime/lifecycle.ts +220 -45
- package/src/runtime/metadata.ts +153 -0
- package/src/runtime/options.ts +102 -0
- package/src/runtime/state.ts +7 -70
- package/src/testing/fake-claude.ts +6 -1
- package/src/testing/fake-files.ts +9 -3
- package/src/testing.ts +40 -15
- package/src/utils/metadata.ts +242 -0
- package/testing/fake-claude.d.ts +4 -0
- package/testing/fake-claude.d.ts.map +1 -1
- package/testing/fake-claude.js +5 -1
- package/testing/fake-files.d.ts.map +1 -1
- package/testing/fake-files.js +9 -3
- package/testing.d.ts +6 -2
- package/testing.d.ts.map +1 -1
- package/testing.js +29 -13
- package/utils/metadata.d.ts +21 -0
- package/utils/metadata.d.ts.map +1 -0
- package/utils/metadata.js +250 -0
package/docs/commands.md
CHANGED
|
@@ -22,12 +22,13 @@ Creates a mod in `./<name>`: a `defineMod` with one hook, one pane, and one rend
|
|
|
22
22
|
### cmod link
|
|
23
23
|
|
|
24
24
|
```text
|
|
25
|
-
cmod link [path] [--yes]
|
|
25
|
+
cmod link [path] [--yes] [--option key=value]...
|
|
26
26
|
```
|
|
27
27
|
|
|
28
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
|
+
- `--option key=value` sets one of the mod's options through `claude plugin configure`, once per option. It refuses a key the mod does not declare, and names the keys it does ([options.md](options.md)).
|
|
31
32
|
|
|
32
33
|
### cmod unlink
|
|
33
34
|
|
|
@@ -47,27 +48,29 @@ Runs every check this machine can run on the mod at `path` (default: the current
|
|
|
47
48
|
|
|
48
49
|
1. Installs its packages.
|
|
49
50
|
2. Checks its layout.
|
|
50
|
-
3. Checks that each step runs a script.
|
|
51
|
-
4.
|
|
52
|
-
5.
|
|
53
|
-
6.
|
|
54
|
-
7.
|
|
55
|
-
8.
|
|
56
|
-
9.
|
|
57
|
-
10.
|
|
58
|
-
11. Runs, with `
|
|
59
|
-
12.
|
|
51
|
+
3. Checks that each step runs a script, and prints the permissions the person grants at install and can grant later, as the person reads them ([permissions.md](permissions.md)). It fails a `hooks/register.ts` that calls `registerPermissionCheck` without `"approve": true`.
|
|
52
|
+
4. Writes the options `defineMod` declares into `.claude-plugin/plugin.json` `userConfig`, keeping the file's own formatting, so Claude Code shows them in `/config` ([options.md](options.md)). It loads `hooks/register.ts` to read them, without starting the mod.
|
|
53
|
+
5. Checks its imports.
|
|
54
|
+
6. Looks for prebuilt binaries outside `cli/`.
|
|
55
|
+
7. Bundles the hooks module as `cmod publish` does, and names each reason a bundle fails.
|
|
56
|
+
8. Validates it with Claude Code, `claude plugin validate --strict`.
|
|
57
|
+
9. Type-checks it with `tsc`, with `tsconfig.json`, and then with `tests/tsconfig.json` when it exists. `cmod new` writes both: the root config leaves Bun out of `src/`, which runs inside Claude Code, and the tests config adds `bun-types`, so a test can run a real program with `Bun.spawn` or read files with `node:fs`. When `.claude-plugin/types/` is missing, as in a fresh clone or in CI, Claude Code writes it first: cmod loads the mod in one `claude -p` run with a config folder of its own and no model to reach, so the run sends nothing.
|
|
58
|
+
10. Lints it with `oxlint`.
|
|
59
|
+
11. Runs, with `bun test`, every test file that does not import `claude-code/testing`.
|
|
60
|
+
12. Runs, with `claude plugin test`, the test files that import `claude-code/testing`. It skips this when none does.
|
|
61
|
+
13. 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.
|
|
60
62
|
|
|
61
63
|
`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.
|
|
62
64
|
|
|
63
65
|
### cmod try
|
|
64
66
|
|
|
65
67
|
```text
|
|
66
|
-
cmod try <owner/repo | path>... [--yes] [--home <folder>] [-- claude arguments]
|
|
68
|
+
cmod try <owner/repo | path>... [--yes] [--option key=value]... [--home <folder>] [-- claude arguments]
|
|
67
69
|
```
|
|
68
70
|
|
|
69
71
|
Starts one throwaway Claude Code session with mods set up, for trying a mod by hand or testing it with an agent. Installs each mod's packages, installs the cmod plugin when Claude Code lacks it, and runs each mod's install step after asking consent. Then starts one Claude Code session with every mod loaded through `--plugin-dir`.
|
|
70
72
|
|
|
73
|
+
- `--option key=value` sets one option of the mod. With several mods, write `--option <mod>.key=value`.
|
|
71
74
|
- The session runs with a new home folder, so it sees only the mods named here and changes none of your Claude Code settings, mods, key bindings, or history. The folder is deleted when the session ends.
|
|
72
75
|
- The session stays logged in as you, with no login screen:
|
|
73
76
|
- On macOS, Claude Code reads your login from the keychain, which `cmod try` links into the new home as `~/Library/Keychains`. Deleting the home removes only the link.
|
|
@@ -87,7 +90,7 @@ Starts one throwaway Claude Code session with mods set up, for trying a mod by h
|
|
|
87
90
|
cmod publish [path] [--dry-run]
|
|
88
91
|
```
|
|
89
92
|
|
|
90
|
-
Releases the mod at `path` (default: the current folder) at the version in its `plugin.json
|
|
93
|
+
Releases the mod at `path` (default: the current folder) at the version in its `plugin.json`. It refuses a `plugin.json` whose `userConfig` differs from the options `defineMod` declares, and says to run `cmod check` and commit `.claude-plugin/plugin.json`.
|
|
91
94
|
|
|
92
95
|
1. Builds the release from the committed files, leaving out `cli/`, `.github/`, and `.claude/`. A `"files"` list in `package.json` limits the release to the paths it lists, plus the folders of the install and uninstall steps, `.claude-plugin/`, `package.json`, `tsconfig.json`, the README, and the license, the way `npm publish` reads it. `tsconfig.json` holds the JSX settings the bundle in step 2 needs. List every folder the hooks module imports from, such as `"files": ["hooks", "src", "skills"]`, because the bundle in step 2 is built from the release.
|
|
93
96
|
2. Bundles the hooks module that `hooks/hooks.json` names, with the mod's source and packages, into one readable `.js` file, writes each control character in it as a `\u` escape, and points `hooks/hooks.json` at it. Anthropic's plugin directory reads a repository without installing its packages, so it can follow a mod only when all its code is in one file.
|
|
@@ -95,7 +98,7 @@ Releases the mod at `path` (default: the current folder) at the version in its `
|
|
|
95
98
|
4. Commits the release as the `release` branch, and builds the release archive from that commit.
|
|
96
99
|
5. Builds the program `cli/` declares, and writes `SHA256SUMS` for every file of the release.
|
|
97
100
|
6. Writes `.claude-plugin/marketplace.json`, listing the archive and the cmod plugin.
|
|
98
|
-
7. Commits that file, tags `v<version>`, pushes the tag and the `release` branch, and creates the GitHub release.
|
|
101
|
+
7. Commits that file, tags `v<version>`, pushes the tag and the `release` branch, and creates the GitHub release. Its notes open with `## Breaking changes`, listing each `BREAKING CHANGE:` footer of a commit since the last `v` tag and the subject of each commit typed with `!`, such as `feat!: mod.claude replaces job.claude`, so the people who use the mod know what to change before they update. `--dry-run` prints that list.
|
|
99
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`.
|
|
100
103
|
|
|
101
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.
|
|
@@ -120,8 +123,8 @@ It needs, and refuses to start without:
|
|
|
120
123
|
### cmod install
|
|
121
124
|
|
|
122
125
|
```text
|
|
123
|
-
cmod install <owner/repo | path> [name] [--yes]
|
|
124
|
-
cmod install <name | name@marketplace> [--yes]
|
|
126
|
+
cmod install <owner/repo | path> [name] [--yes] [--option key=value]...
|
|
127
|
+
cmod install <name | name@marketplace> [--yes] [--option key=value]...
|
|
125
128
|
```
|
|
126
129
|
|
|
127
130
|
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.
|
|
@@ -131,6 +134,7 @@ Adds the plugin's marketplace to Claude Code and installs the plugin through Cla
|
|
|
131
134
|
- Given the name of a plugin Claude Code already holds, it runs a mod's install step again.
|
|
132
135
|
- A path holds a `/`, such as `./my-mod`.
|
|
133
136
|
- `--yes` approves the mod's install and uninstall commands without asking.
|
|
137
|
+
- `--option key=value` sets one of the mod's options through `claude plugin configure`, once per option, so a scripted install needs no visit to `/config`. It refuses a key the mod does not declare, and names the keys it does ([options.md](options.md)).
|
|
134
138
|
|
|
135
139
|
### cmod update
|
|
136
140
|
|
|
@@ -172,7 +176,7 @@ Downloads a program for an install script: fetches the download for this machine
|
|
|
172
176
|
|
|
173
177
|
## Run by cmod
|
|
174
178
|
|
|
175
|
-
cmod runs these
|
|
179
|
+
cmod runs these itself. A person runs them to see a step's whole log, to finish a step, or to grant a permission.
|
|
176
180
|
|
|
177
181
|
### cmod setup
|
|
178
182
|
|
|
@@ -180,7 +184,7 @@ cmod runs these two itself. A person runs them to see a step's whole log or to f
|
|
|
180
184
|
cmod setup <plugin-root> [--events] [--consent <sha256>] [--yes]
|
|
181
185
|
```
|
|
182
186
|
|
|
183
|
-
Downloads the program the mod's `package.json` `cmod.program` names into `~/.local/bin`, runs the mod's install step, saves its uninstall step, and records the mod as set up. An unchanged mod runs nothing. `--events` prints one event per line for a program to read, and the mod's progress line in Claude Code reads them.
|
|
187
|
+
Downloads the program the mod's `package.json` `cmod.program` names into `~/.local/bin`, runs the mod's install step, saves its uninstall step, grants the permissions its `cmod.permissions` lists, and records the mod as set up. The consent covers the scripts, the program, the keys, and the permissions together, and an update asks again only for what changed. An unchanged mod runs nothing. `--events` prints one event per line for a program to read, and the mod's progress line in Claude Code reads them.
|
|
184
188
|
|
|
185
189
|
### cmod teardown
|
|
186
190
|
|
|
@@ -188,4 +192,12 @@ Downloads the program the mod's `package.json` `cmod.program` names into `~/.loc
|
|
|
188
192
|
cmod teardown <plugin-name> [--events]
|
|
189
193
|
```
|
|
190
194
|
|
|
191
|
-
Runs a removed mod's saved uninstall step, then deletes its install record, the saved step, every version of its program, and its data folder, and forgets the scripts the person approved for it. It keeps the mod's config folder, `~/.claude/cmods/<plugin-name>`. A failed uninstall step keeps the record, so `cmod teardown` can run again.
|
|
195
|
+
Runs a removed mod's saved uninstall step, then deletes its install record, the saved step, every version of its program, and its data folder, and forgets the scripts and the permissions the person approved for it. It keeps the mod's config folder, `~/.claude/cmods/<plugin-name>`. A failed uninstall step keeps the record, so `cmod teardown` can run again.
|
|
196
|
+
|
|
197
|
+
### cmod permission
|
|
198
|
+
|
|
199
|
+
```text
|
|
200
|
+
cmod permission <mod> <name> [value] on|off
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
Turns one permission of a set-up mod on or off, such as `cmod permission ci-watch model off` or `cmod permission ci-watch network api.github.com on`. A toggle in `/mods` runs it. It refuses a permission the mod's `package.json` `cmod.permissions` does not list, and the mod sees the change before its next call, with no reload ([permissions.md](permissions.md)).
|
package/docs/index.md
CHANGED
|
@@ -15,7 +15,7 @@ my-mod/
|
|
|
15
15
|
├── .gitignore node_modules/ and .claude-plugin/types/
|
|
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
|
-
├── hooks/register.ts registerMod(addHook, myMod), and nothing else
|
|
18
|
+
├── hooks/register.ts registerMod(addHook, myMod, options), and nothing else
|
|
19
19
|
├── src/mod.tsx defineMod: the state, the setup, the hooks, the panes
|
|
20
20
|
├── src/state.ts the starting state
|
|
21
21
|
├── src/panes/prompts.tsx one definePane per file
|
|
@@ -36,6 +36,8 @@ A mod imports `@cmodjs/core` by path, such as `../node_modules/@cmodjs/core/mod.
|
|
|
36
36
|
|
|
37
37
|
`.claude-plugin/types/` holds the `tsconfig.json` the mod's own extends, the `claude-code` module, and the `h` JSX global. Claude Code writes it when it loads the mod, and git ignores it. `cmod check` has Claude Code write it when it is missing, so a fresh clone and CI type-check too. Until it exists, `tsc` alone fails, so run `cmod check` once first.
|
|
38
38
|
|
|
39
|
+
Claude Code links the types of each plugin the mod depends on, such as `cmod`, into the folder where that plugin is installed. `cmod try` installs the cmod plugin into a home it deletes when the session ends, which leaves those links leading nowhere, so `tsc` alone fails with `Cannot find type definition file for 'cmod'`. `cmod check` deletes a types folder with such a link and has Claude Code write it again.
|
|
40
|
+
|
|
39
41
|
## Where a mod runs
|
|
40
42
|
|
|
41
43
|
Mod code runs inside Claude Code's hooks environment, not in Node or Bun. It has no Node or Bun modules, no DOM, and no `import()`. `mod.fs` reaches files, `mod.process` runs programs, and `mod.http` reaches the network ([mod.md](mod.md)). A job of your own also reaches the plugin's folder through `claude.plugin.root` and timers through `claude.clock` ([jobs.md](jobs.md)).
|
|
@@ -57,7 +59,12 @@ A program the mod needs lives as source in `cli/`. `cmod publish` builds it and
|
|
|
57
59
|
| How do I define a mod, and what can `mod` call? | [mod.md](mod.md) |
|
|
58
60
|
| How does a mod watch something outside Claude Code, tell Claude about it, or ask a model? | [mod.md](mod.md#mod-every) |
|
|
59
61
|
| How does a mod give Claude a prompt on a schedule, like `/loop`? | [jobs.md](jobs.md#schedule) |
|
|
60
|
-
| How do I keep values across prompts, sessions, and projects?
|
|
62
|
+
| How do I keep values across prompts, sessions, and projects? | [state.md](state.md) |
|
|
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) |
|
|
64
|
+
| Which hosts, programs, and files may my mod reach, and how does the person grant them? | [permissions.md](permissions.md) |
|
|
65
|
+
| How does my mod find files and keep its own keys on each one, such as on a Skill? | [mod.md](mod.md#mod-metadata) |
|
|
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
|
+
| How do I add my mod's own page to `/mods`? | [mod.md](mod.md#mod-settings) |
|
|
61
68
|
| How do I react to a Claude Code event, and what can a hook answer? | [hooks.md](hooks.md) |
|
|
62
69
|
| How do I draw a pane, change a row Claude Code draws, or ask the person something? | [ui.md](ui.md) |
|
|
63
70
|
| 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) |
|
|
@@ -73,10 +80,13 @@ A mod imports from the files below, and every other file in this package is inte
|
|
|
73
80
|
| Import | Exports | Docs |
|
|
74
81
|
| --- | --- | --- |
|
|
75
82
|
| `mod.js` | `defineMod`, `ModDefinition`, `Mod`, `JobContext`, `Job`, `ProgressStep`, `PaneHandle`, `longestMs`, `messageOf` | [mod.md](mod.md), [jobs.md](jobs.md), [ui.md](ui.md) |
|
|
76
|
-
| `mod.js` | `Claude`, `RoutedEvent`, `RoutedHook`, `ToolCalls` | [jobs.md](jobs.md) |
|
|
83
|
+
| `mod.js` | `Claude`, `ModClaude`, `RoutedEvent`, `RoutedHook`, `ToolCalls` | [jobs.md](jobs.md) |
|
|
84
|
+
| `mod.js` | `FileMatch`, `Metadata`, `ScrollArgs` | [mod.md](mod.md), [ui.md](ui.md) |
|
|
85
|
+
| `installer.js` | `defineStep`, `Step` | [install-steps.md](install-steps.md#steps-in-claude-code) |
|
|
77
86
|
| `mod.js` | `ModEvent`, `ModHook`, `HookOptions`, `HookInput`, `HookAnswer` | [hooks.md](hooks.md) |
|
|
78
87
|
| `mod.js` | `notInstalled` | [dependencies.md](dependencies.md) |
|
|
79
|
-
| `register.js` | `registerMod`, `registerPermissionCheck` | [mod.md](mod.md) |
|
|
88
|
+
| `register.js` | `registerMod`, `registerPermissionCheck`, `registeredMod` | [mod.md](mod.md) |
|
|
89
|
+
| `options.js` | `option`, `Option`, `Options`, `OptionValues`, `OptionValue`, `OptionKind`, `UserConfigField`, `userConfigOf` | [options.md](options.md) |
|
|
80
90
|
| `shell.js` | `parseShell`, `ParsedShell`, `ShellCommand` | [hooks.md](hooks.md) |
|
|
81
91
|
| `path.js` | `resolve`, `join`, `dirname`, `basename`, `extname`, `relative`, `isAbsolute`, `normalize` | [hooks.md](hooks.md) |
|
|
82
92
|
| `testing.js` | `testMod`, `TestOptions`, `TestInput`, `TestedMod`, `Fakes`, `Shown`, `TestCall` | [testing.md](testing.md) |
|
package/docs/install-steps.md
CHANGED
|
@@ -34,16 +34,42 @@ cmod refuses a `cmod` key that breaks these rules, with the fix in the message:
|
|
|
34
34
|
|
|
35
35
|
## Consent
|
|
36
36
|
|
|
37
|
-
Before a step runs for the first time, cmod shows the person the commands and the
|
|
37
|
+
Before a step runs for the first time, cmod shows the person the commands, the keys, and the permissions, and asks to run them. cmod hashes the commands, the keys, the permissions, and every file in each script folder, and remembers the hash the person approved with each permission. A change to any of those asks again, and an added permission is the only one it asks about ([permissions.md](permissions.md)).
|
|
38
38
|
|
|
39
|
-
- In Claude Code, the mod
|
|
39
|
+
- In Claude Code, the mod opens the installer pane, `Install <mod>`, focused. It lists `<mod> wants to:`, each permission as one sentence, then `and change your computer:` with `Run <install> now, and <uninstall> when you remove it` and `Bind shift+tab to /mode`, naming only what the mod has. `Accept` (a) runs the setup, and a progress line above the prompt shows the install. `Not now` (n) and Escape decline.
|
|
40
40
|
- 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.`
|
|
41
|
-
- In a terminal, `cmod install`, `cmod link`, `cmod try`, and `cmod update` print the commands, the program, and the
|
|
41
|
+
- In a terminal, `cmod install`, `cmod link`, `cmod try`, and `cmod update` print the commands, the program, the keys, and the permissions, and ask `Set up <mod>? [y/N]`. `--yes` approves without asking.
|
|
42
42
|
- `Not now` leaves the mod off and logs `<mod> is not installed. Run cmod install <mod> to install it.`
|
|
43
43
|
|
|
44
44
|
The mod starts only once its install step has run ([mod.md](mod.md)).
|
|
45
45
|
|
|
46
|
-
In Claude Code, the mod sets itself up through the `cmod` that PATH finds, and only through one that knows every step it has. A mod with `keys` waits for cmod 0.1.12 or later, because an older cmod skips the keys. While PATH finds an older one, the line above the prompt says `PATH finds cmod <version>, and <mod> needs cmod 0.1.12 or later. Run npm i -g @cmodjs/cli, or put ~/.local/bin ahead of the old cmod on PATH.` The mod sets up as soon as a new enough cmod answers, such as the one the cmod plugin installs into `~/.local/bin` at its first start.
|
|
46
|
+
In Claude Code, the mod sets itself up through the `cmod` that PATH finds, and only through one that knows every step it has. A mod with `keys` waits for cmod 0.1.12 or later, because an older cmod skips the keys, and a mod with `permissions` waits for cmod 0.2.0 or later, because an older cmod grants none of them. While PATH finds an older one, the line above the prompt says `PATH finds cmod <version>, and <mod> needs cmod 0.1.12 or later. Run npm i -g @cmodjs/cli, or put ~/.local/bin ahead of the old cmod on PATH.` The mod sets up as soon as a new enough cmod answers, such as the one the cmod plugin installs into `~/.local/bin` at its first start.
|
|
47
|
+
|
|
48
|
+
## Steps in Claude Code
|
|
49
|
+
|
|
50
|
+
Some setup only the person can do, such as signing in to a service. A mod lists those steps in `installer`, and the installer pane shows each one after the permissions and the options, once the mod has started:
|
|
51
|
+
|
|
52
|
+
```ts
|
|
53
|
+
import { defineStep } from '../node_modules/@cmodjs/core/installer.js'
|
|
54
|
+
|
|
55
|
+
const signIn = defineStep({
|
|
56
|
+
id: 'sign-in',
|
|
57
|
+
title: 'Sign in to GitHub',
|
|
58
|
+
render: () => <Text>Run gh auth login in a terminal, then press Next.</Text>,
|
|
59
|
+
isDone: async (mod) => (await mod.process.run(['gh', 'auth', 'status'])).exitCode === 0,
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
export const ciWatch = defineMod({ name: 'ci-watch', installer: [signIn], setup(mod) { … } })
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
```ts
|
|
66
|
+
defineStep<State>(step: { id: string; title: string; render(mod, props): RenderElement; isDone(mod): boolean | Promise<boolean> }): Step<State>
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
- `id` is 1 to 64 letters, digits, `_`, or `-`. `title` is the line above the step, with `(1 of 2)`. `defineStep` throws for either one missing.
|
|
70
|
+
- `render` draws the step's body, the way a pane's `render` does ([ui.md](ui.md)).
|
|
71
|
+
- `isDone` says whether the step is finished. A step that is done is skipped, so the pane opens only for what is left. `Next` checks `isDone`, and shows `Finish <title> first.` while it is false. The last step's button says `Finish`.
|
|
72
|
+
- `Not now` closes the pane and logs `<mod> needs one more step, starting with <title>. Run /mods <mod> to finish.` The mod keeps running. `/mods <mod>` shows `Needs setup: <title>` with a `Finish setup` button, which opens the pane on the first step left.
|
|
47
73
|
|
|
48
74
|
## Key bindings
|
|
49
75
|
|
package/docs/jobs.md
CHANGED
|
@@ -167,6 +167,8 @@ A `command` pattern is not a glob. cmod splits it into words the way it splits a
|
|
|
167
167
|
|
|
168
168
|
## permissions
|
|
169
169
|
|
|
170
|
+
A `deny` rule needs no permission. An `ask` rule takes effect only with `"approve": true` in `package.json` `"cmod".permissions` ([permissions.md](permissions.md)).
|
|
171
|
+
|
|
170
172
|
```ts
|
|
171
173
|
permissions<State>(rules: PermissionRules<State> | ((state: State) => PermissionRules<State>)): Job<void, State>
|
|
172
174
|
|
|
@@ -222,6 +224,8 @@ export const guard = defineMod({
|
|
|
222
224
|
|
|
223
225
|
## check
|
|
224
226
|
|
|
227
|
+
Its command needs `"run"` with the program's name, such as `"run": ["tsc"]` ([permissions.md](permissions.md)).
|
|
228
|
+
|
|
225
229
|
```ts
|
|
226
230
|
check<State>(options: { readonly after: Target | Target[]; readonly run: readonly string[]; readonly timeoutMs?: number }): Job<void, State>
|
|
227
231
|
```
|
|
@@ -247,6 +251,8 @@ export const lint = defineMod({
|
|
|
247
251
|
|
|
248
252
|
## prompt
|
|
249
253
|
|
|
254
|
+
It needs `"prompt": true`. Without the grant it adds nothing and logs once which line to add ([permissions.md](permissions.md)).
|
|
255
|
+
|
|
250
256
|
```ts
|
|
251
257
|
prompt<State>(options: {
|
|
252
258
|
name: string
|
|
@@ -288,6 +294,8 @@ export const conventions = defineMod({
|
|
|
288
294
|
|
|
289
295
|
## schedule
|
|
290
296
|
|
|
297
|
+
It needs `"prompt": true`, because each cron starts a turn ([permissions.md](permissions.md)).
|
|
298
|
+
|
|
291
299
|
```ts
|
|
292
300
|
schedule<State>(entry: Schedule | ((state: State) => Schedule | undefined)): Job<void, State>
|
|
293
301
|
|
|
@@ -346,6 +354,8 @@ export const usage = defineMod({
|
|
|
346
354
|
|
|
347
355
|
## program
|
|
348
356
|
|
|
357
|
+
It needs `"run"` with the program's name. Without the grant the program stops at once and names the line to add ([permissions.md](permissions.md)).
|
|
358
|
+
|
|
349
359
|
```ts
|
|
350
360
|
program(options: { readonly command: readonly string[]; readonly environment?: Record<string, string> }): Job<Program>
|
|
351
361
|
|
|
@@ -394,17 +404,20 @@ A `Job` is a function. `mod.use` calls it with a `JobContext` and returns what i
|
|
|
394
404
|
```ts
|
|
395
405
|
type JobContext<State> = {
|
|
396
406
|
readonly mod: Mod<State>
|
|
397
|
-
readonly claude: Claude
|
|
398
|
-
on<N extends RoutedEvent>(event: N, hook: RoutedHook<N>): void
|
|
399
407
|
announce(feature: string): void
|
|
400
408
|
reserveName(kind: string, name: string, taken: string): void
|
|
401
409
|
readonly toolCalls: ToolCalls
|
|
402
410
|
}
|
|
403
411
|
```
|
|
404
412
|
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
- `
|
|
413
|
+
A job reaches Claude Code through `mod.claude`, the same member every hook has:
|
|
414
|
+
|
|
415
|
+
- `mod.claude` holds Claude Code's own calls. [The claude members](#the-claude-members) lists each one. Each call checks the permission it needs, as `mod`'s own members do ([permissions.md](permissions.md)).
|
|
416
|
+
- `mod.claude.store` is where cmod saves `mod.state`, under keys that start with `<mod>.`, such as `<mod>.<key>`. A job of your own never writes those keys, or it changes the mod's state behind cmod's back.
|
|
417
|
+
- `mod.claude.on` adds a Claude Code hook-module handler, `(e, next) => …`, on a `classic.<ModEvent>` event or on `tool.check`, `tool.call`, `prompt.submit`, `prompt.context`, `command.run`, `session.measure`, `skill.prompt`, `ui.render`, `ui.press`, `ui.scroll`, `ui.close`, or `cmod.call`. The handler calls `next(e)` to pass the event on. cmod checks what it answers against the person's grants, part by part ([permissions.md](permissions.md#when-a-hook-answer-has-no-grant)). A job with a handler on `tool.check` or `classic.PermissionRequest` needs `registerPermissionCheck` in `hooks/register.ts` ([mod.md](mod.md)).
|
|
418
|
+
|
|
419
|
+
The context adds what only a job needs:
|
|
420
|
+
|
|
408
421
|
- `announce` adds a phrase to the list cmod logs when the mod first starts.
|
|
409
422
|
- `reserveName` throws `taken` when the mod already reserved that `kind` and `name`. A job calls it to refuse a duplicate.
|
|
410
423
|
- `toolCalls.agentOf(toolUseId)` resolves `{ agentId?, agentType? }`, the subagent behind a call.
|
|
@@ -413,9 +426,9 @@ type JobContext<State> = {
|
|
|
413
426
|
```ts
|
|
414
427
|
import { defineMod, type Job } from '../node_modules/@cmodjs/core/mod.js'
|
|
415
428
|
|
|
416
|
-
const clock: Job<{ now(): Promise<number> }> = ({
|
|
429
|
+
const clock: Job<{ now(): Promise<number> }> = ({ mod, announce }) => {
|
|
417
430
|
announce('a clock')
|
|
418
|
-
return { now: () => claude.clock.now() }
|
|
431
|
+
return { now: () => mod.claude.clock.now() }
|
|
419
432
|
}
|
|
420
433
|
|
|
421
434
|
export const timer = defineMod({
|
|
@@ -433,7 +446,9 @@ export const timer = defineMod({
|
|
|
433
446
|
|
|
434
447
|
`mod.js` exports the types `Claude`, `RoutedEvent`, `RoutedHook`, and `ToolCalls`, so a job of your own names them in its own helpers, such as `(claude: Claude, calls: ToolCalls): RoutedHook<'command.run'> => …`.
|
|
435
448
|
|
|
436
|
-
Each member calls the member of the same name in Claude Code's hooks API, `$`, with the same arguments. The `env` members read `$.env.get` instead.
|
|
449
|
+
Each member calls the member of the same name in Claude Code's hooks API, `$`, with the same arguments. The `env` members read `$.env.get` instead. `mod.claude.on` adds a handler, as [Write a job of your own](#write-a-job-of-your-own) shows.
|
|
450
|
+
|
|
451
|
+
A member that does what a permission covers checks it first and rejects with the fix: `process` needs `run`, `http.fetch` needs `network`, `fs.write` outside the project and the data folder needs `files` or, for Claude Code's own settings files, `config`, `fs.read` of a transcript needs `conversation`, `session.messages` needs `conversation`, `session.append` and `prompt.submit` need `prompt`, `model.complete` needs `model`, `agent.spawn` needs `agents`, `config.set` needs `config`, and `tool.call` needs `tools`, except `CronCreate`, which needs `prompt`, and `CronList` and `CronDelete`, which need nothing ([permissions.md](permissions.md)).
|
|
437
452
|
|
|
438
453
|
| Member | What it does |
|
|
439
454
|
| --- | --- |
|
|
@@ -454,7 +469,8 @@ Each member calls the member of the same name in Claude Code's hooks API, `$`, w
|
|
|
454
469
|
| `process.spawn(request)` | Starts a program and streams its output. `mod.process.spawn` is built on it. |
|
|
455
470
|
| `fs.read`, `fs.write`, `fs.list`, `fs.exists`, `fs.stat` | What `mod.fs` calls. A relative path is under the session's working folder. |
|
|
456
471
|
| `http.fetch(url, init?)` | What `mod.http.fetch` calls. |
|
|
457
|
-
| `settings.read(args?)` |
|
|
472
|
+
| `settings.read(args?)` | Reads Claude Code's own `settings.json` files. Without `{ source }` it answers every source merged. `user`, `project`, `local`, `flag`, and `policy` each answer one. |
|
|
473
|
+
| `config.list()`, `config.set({ key, value })` | Lists the rows `/config` shows, and changes one, such as `<mod>.<option>`. `set` needs `config`. |
|
|
458
474
|
| `store.get`, `store.set`, `store.delete`, `store.keys` | The plugin's key-value store, where cmod also keeps `mod.state`. |
|
|
459
475
|
| `clock.now()` | Resolves milliseconds since the epoch. |
|
|
460
476
|
| `clock.after(ms, fn)` | Calls `fn` once after `ms` milliseconds, and returns `{ cancel() }`. |
|
package/docs/mod.md
CHANGED
|
@@ -5,21 +5,25 @@
|
|
|
5
5
|
## defineMod
|
|
6
6
|
|
|
7
7
|
```ts
|
|
8
|
-
defineMod<State, Name>(definition: ModDefinition<State, Name>): ModDefinition<State, Name>
|
|
8
|
+
defineMod<State, Name, Options>(definition: ModDefinition<State, Name, Options>): ModDefinition<State, Name, Options>
|
|
9
9
|
```
|
|
10
10
|
|
|
11
11
|
```ts
|
|
12
|
-
type ModDefinition<State, Name> = {
|
|
12
|
+
type ModDefinition<State, Name, Options> = {
|
|
13
13
|
readonly name: Name
|
|
14
14
|
readonly state?: State
|
|
15
|
+
readonly options?: Options
|
|
15
16
|
readonly api?: { … }
|
|
16
|
-
|
|
17
|
+
readonly installer?: readonly Step<State>[]
|
|
18
|
+
setup(mod: Mod<State, Options>): void | Promise<void>
|
|
17
19
|
}
|
|
18
20
|
```
|
|
19
21
|
|
|
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
|
|
22
|
+
- `name` is the mod's name. Use the `name` in `.claude-plugin/plugin.json`. Claude Mod Manager (cmod) keys the saved state and the options 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
23
|
- `state` holds the starting values, grouped by how long they last. [state.md](state.md) explains the groups.
|
|
24
|
+
- `options` declares what a person sets, such as a token, and types `mod.options`. [options.md](options.md) explains them.
|
|
22
25
|
- `api` holds the methods other mods call. [dependencies.md](dependencies.md) explains it.
|
|
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.
|
|
23
27
|
- `setup` runs once when the mod starts. It adds hooks, panes, slot renders, and jobs. It may be `async`.
|
|
24
28
|
|
|
25
29
|
```tsx
|
|
@@ -41,14 +45,15 @@ export const greeter = defineMod({
|
|
|
41
45
|
|
|
42
46
|
## When setup runs
|
|
43
47
|
|
|
44
|
-
Claude Code loads the mod at the start of each session. cmod then checks that the mod is set up. A mod
|
|
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.
|
|
45
49
|
|
|
46
50
|
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
51
|
|
|
48
52
|
When a step fails, the mod does not start. Its progress line above the prompt names the failure and the fix:
|
|
49
53
|
|
|
50
54
|
- `<mod>: state.<group> is not a lifetime. …` or `<mod>: state.<group> is not an object of values. …` when the declared state is not valid ([state.md](state.md)).
|
|
51
|
-
- `its state did not load: <error>` when reading the saved values
|
|
55
|
+
- `its state did not load: <error>` when reading the saved values fails.
|
|
56
|
+
- `it needs <titles>` when the person chose Not now on an option with no default, or no one can answer, as in `claude -p` ([options.md](options.md)). This one says `Set it in /config.` instead, and Claude Code reloads the mod once the value is set.
|
|
52
57
|
- `its setup function threw: <error>` when `setup` throws or rejects.
|
|
53
58
|
- `its open panes did not load: <error>` when Claude Code cannot list the open panes.
|
|
54
59
|
- `it decides permissions on <events>, so hooks/register.ts must call registerPermissionCheck(addHook) after registerMod` when `setup` adds a hook on `PermissionRequest`, or a job that decides permissions, and `register.ts` does not register the permission check.
|
|
@@ -60,43 +65,45 @@ Each of these says `Fix it, then run /reload-plugins.`
|
|
|
60
65
|
`hooks/hooks.json` names `hooks/register.ts` as the plugin's hooks module. `register.ts` calls `registerMod` and does nothing else:
|
|
61
66
|
|
|
62
67
|
```ts
|
|
63
|
-
import type { On } from 'claude-code'
|
|
68
|
+
import type { On, PluginOptions } from 'claude-code'
|
|
64
69
|
import { registerMod } from '../node_modules/@cmodjs/core/register.js'
|
|
65
70
|
import { greeter } from '../src/mod.js'
|
|
66
71
|
|
|
67
|
-
export function register(addHook: On): void {
|
|
68
|
-
registerMod(addHook, greeter)
|
|
72
|
+
export function register(addHook: On, options: PluginOptions): void {
|
|
73
|
+
registerMod(addHook, greeter, options)
|
|
69
74
|
}
|
|
70
75
|
```
|
|
71
76
|
|
|
72
77
|
```ts
|
|
73
|
-
registerMod<State>(addHook: On, definition: ModDefinition<State
|
|
78
|
+
registerMod<State, Options>(addHook: On, definition: ModDefinition<State, string, Options>, options: PluginOptions): void
|
|
74
79
|
registerPermissionCheck(addHook: On): void
|
|
80
|
+
registeredMod(): ModDefinition | undefined
|
|
75
81
|
```
|
|
76
82
|
|
|
77
|
-
`registerMod` registers one handler per Claude Code event the mod can use, and routes each event to the mod. A `PreToolUse` hook runs on `tool.call`, which `registerMod` registers ([hooks.md](hooks.md)). It leaves out Claude Code's permission check, the events `tool.check` and `PermissionRequest`.
|
|
83
|
+
`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. A `PreToolUse` hook runs on `tool.call`, which `registerMod` registers ([hooks.md](hooks.md)). It leaves out Claude Code's permission check, the events `tool.check` and `PermissionRequest`.
|
|
78
84
|
|
|
79
85
|
A mod that decides permissions calls `registerPermissionCheck` after `registerMod`. That covers a `PreToolUse` hook that answers `permissionDecision: 'allow'` or `'ask'`, a hook on `PermissionRequest`, the `permissions` job, and a job of your own on `tool.check`:
|
|
80
86
|
|
|
81
87
|
```ts
|
|
82
|
-
export function register(addHook: On): void {
|
|
83
|
-
registerMod(addHook, guard)
|
|
88
|
+
export function register(addHook: On, options: PluginOptions): void {
|
|
89
|
+
registerMod(addHook, guard, options)
|
|
84
90
|
registerPermissionCheck(addHook)
|
|
85
91
|
}
|
|
86
92
|
```
|
|
87
93
|
|
|
88
|
-
A mod without
|
|
94
|
+
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 progress line names the line to add.
|
|
89
95
|
|
|
90
96
|
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 the method `mod.on`, 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.
|
|
91
97
|
|
|
92
98
|
## What `mod` can call
|
|
93
99
|
|
|
94
|
-
`setup` gets `mod`, of type `Mod<State>`. Every hook, render, and job gets the same `mod`.
|
|
100
|
+
`setup` gets `mod`, of type `Mod<State, Options>`. Every hook, render, and job gets the same `mod`.
|
|
95
101
|
|
|
96
102
|
```ts
|
|
97
|
-
type Mod<State> = {
|
|
103
|
+
type Mod<State, Options> = {
|
|
98
104
|
readonly name: string
|
|
99
105
|
readonly state: Readonly<State>
|
|
106
|
+
readonly options: OptionValues<Options>
|
|
100
107
|
readonly dataFolder: string
|
|
101
108
|
readonly projectRoot: string
|
|
102
109
|
readonly cwd: string
|
|
@@ -105,13 +112,16 @@ type Mod<State> = {
|
|
|
105
112
|
every(ms, hook): Timer
|
|
106
113
|
readonly ui: { pane, render, toast, progress, ask, scroll }
|
|
107
114
|
readonly process: { run, spawn }
|
|
108
|
-
readonly fs: { read, write, list, exists, stat }
|
|
115
|
+
readonly fs: { read, write, list, exists, stat, find }
|
|
116
|
+
readonly metadata: { read, update }
|
|
109
117
|
readonly http: { fetch }
|
|
110
|
-
readonly settings: {
|
|
118
|
+
readonly settings: { page, open }
|
|
111
119
|
readonly session: { messages, append, submit }
|
|
112
120
|
readonly agent: { spawn }
|
|
113
121
|
readonly model: { complete }
|
|
122
|
+
readonly permissions: { has }
|
|
114
123
|
readonly dependencies: CmodDependencies
|
|
124
|
+
readonly claude: ModClaude
|
|
115
125
|
}
|
|
116
126
|
```
|
|
117
127
|
|
|
@@ -119,6 +129,7 @@ type Mod<State> = {
|
|
|
119
129
|
| --- | --- | --- |
|
|
120
130
|
| `name` | The mod's name from `defineMod`. | |
|
|
121
131
|
| `state` | The mod's values. Assign to a key to change it. | [state.md](state.md) |
|
|
132
|
+
| `options` | The values a person set for the mod's options, typed from their declaration. Read-only. | [options.md](options.md) |
|
|
122
133
|
| `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) |
|
|
123
134
|
| `projectRoot` | The project the session works in. It follows `/cd`. | |
|
|
124
135
|
| `cwd` | The session's working folder. It follows a `cd` in a Bash or PowerShell call, and `/cd`. | |
|
|
@@ -127,13 +138,16 @@ type Mod<State> = {
|
|
|
127
138
|
| `every` | Runs a function every so many milliseconds while the session runs. | below |
|
|
128
139
|
| `ui` | Panes, slot renders, toasts, progress lines, questions, and scrolling. | [ui.md](ui.md) |
|
|
129
140
|
| `process` | Runs a program. | below |
|
|
130
|
-
| `fs` | Reads, writes,
|
|
141
|
+
| `fs` | Reads, writes, looks up, and finds files. | below |
|
|
142
|
+
| `metadata` | Reads and changes the mod's keys in one file's metadata. | below |
|
|
131
143
|
| `http` | Fetches a URL. | below |
|
|
132
|
-
| `settings` |
|
|
144
|
+
| `settings` | Adds the mod's own pages to `/mods`, and opens them. | below |
|
|
133
145
|
| `session` | Reads the conversation or a subagent's, adds a note Claude reads, and asks Claude for a turn. | below |
|
|
134
146
|
| `agent` | Starts a subagent. | below |
|
|
135
147
|
| `model` | Asks a model one question, outside the conversation. | below |
|
|
148
|
+
| `permissions` | Says whether the person has a permission turned on. `process`, `fs.write`, `metadata.update`, `http`, `session`, `agent`, `model`, and `claude` each check theirs. | [permissions.md](permissions.md) |
|
|
136
149
|
| `dependencies` | Calls the methods of other mods. | [dependencies.md](dependencies.md) |
|
|
150
|
+
| `claude` | Claude Code's own calls and events, for what `mod` has no member for. | [jobs.md](jobs.md#the-claude-members) |
|
|
137
151
|
|
|
138
152
|
### mod.every
|
|
139
153
|
|
|
@@ -195,6 +209,7 @@ mod.fs.write(path: string, text: string): Promise<void>
|
|
|
195
209
|
mod.fs.list(path?: string): Promise<FsEntry[]>
|
|
196
210
|
mod.fs.exists(path: string): Promise<boolean>
|
|
197
211
|
mod.fs.stat(path: string, options?: { resolve: boolean }): Promise<FsStat>
|
|
212
|
+
mod.fs.find(glob: string | readonly string[]): Promise<readonly FileMatch[]>
|
|
198
213
|
```
|
|
199
214
|
|
|
200
215
|
- A relative path is under the session's working folder. An absolute path is used as given. Text is UTF-8.
|
|
@@ -204,7 +219,64 @@ mod.fs.stat(path: string, options?: { resolve: boolean }): Promise<FsStat>
|
|
|
204
219
|
- `exists` answers whether the path leads to a file or a folder.
|
|
205
220
|
- `stat` answers `{ kind, size, mtimeMs, isLink }` of 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.
|
|
206
221
|
|
|
207
|
-
A file the mod ships is under the plugin's folder
|
|
222
|
+
A file the mod ships is under the plugin's folder, `mod.claude.plugin.root` ([jobs.md](jobs.md#the-claude-members)). A file the mod keeps for itself belongs in `mod.dataFolder`.
|
|
223
|
+
|
|
224
|
+
`find` answers every file a glob matches, each with its metadata, so a mod finds the Skills, agents, scripts, or notes it acts on in one call:
|
|
225
|
+
|
|
226
|
+
```ts
|
|
227
|
+
type FileMatch = { readonly path: string; readonly name?: string; readonly metadata: Readonly<Record<string, string>>; readonly error?: string }
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
- A glob starting with `~/.claude` starts at Claude Code's config folder, `CLAUDE_CONFIG_DIR` when it is set. One starting with `~/` starts at home, one starting with `/` is absolute, and any other starts at the project root. Several globs answer each file once.
|
|
231
|
+
- `path` is absolute. `name` is a Markdown file's frontmatter `name`. `metadata` holds the mod's own keys, bare, as `mod.metadata.read` answers them.
|
|
232
|
+
- `find` follows symbolic links into the folders they point at, once each. It skips `.git` and `node_modules` unless the glob names them.
|
|
233
|
+
- A file whose metadata cmod cannot read comes back with empty `metadata` and an `error` naming the line, and cmod logs it once.
|
|
234
|
+
- cmod keeps what it read of each file until the file's modified time or size changes.
|
|
235
|
+
|
|
236
|
+
```ts
|
|
237
|
+
const skills = await mod.fs.find(['~/.claude/skills/*/SKILL.md', '.claude/skills/*/SKILL.md'])
|
|
238
|
+
const planning = skills.filter((skill) => skill.metadata['modes']?.split(' ').includes('plan'))
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
### mod.metadata
|
|
242
|
+
|
|
243
|
+
```ts
|
|
244
|
+
mod.metadata.read(path: string): Promise<Readonly<Record<string, string>>>
|
|
245
|
+
mod.metadata.update(path: string, change: (metadata: Record<string, string>) => void): Promise<void>
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
Metadata is a file's own set of string keys, kept in the file, so it moves and is committed with the file. Each mod reads and writes its own keys only: the file stores them as `<mod>.<key>`, and the mod sees them bare. A value is one line of text, and a list is one space-separated string.
|
|
249
|
+
|
|
250
|
+
Where a file keeps its metadata depends on its kind:
|
|
251
|
+
|
|
252
|
+
```markdown
|
|
253
|
+
---
|
|
254
|
+
name: plan
|
|
255
|
+
metadata:
|
|
256
|
+
modes.modes: plan build
|
|
257
|
+
---
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
```sh
|
|
261
|
+
#!/bin/sh
|
|
262
|
+
# /// metadata
|
|
263
|
+
# modes.modes: plan build
|
|
264
|
+
# ///
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
- A Markdown file keeps it in its frontmatter, under `metadata:`.
|
|
268
|
+
- A `.sh`, `.py`, `.ts`, `.js`, `.yaml`, or `.toml` file keeps it in a `/// metadata` comment block after any shebang, in the file's own comment, `#` or `//`, the way PEP 723 keeps a Python script's dependencies.
|
|
269
|
+
- Any other file keeps it in a sidecar, `<file>.meta`.
|
|
270
|
+
|
|
271
|
+
`update` hands `change` the mod's current keys, and writes what `change` leaves. It changes only the metadata block, keeps other mods' keys, writes nothing when nothing changed, and removes an empty block. It writes through a symbolic link to the file it points at. Writing a file outside the project and the mod's data folder needs the `files` permission, as `mod.fs.write` does ([permissions.md](permissions.md)). A key is letters, digits, `_`, `.`, and `-`, and `update` rejects any other key and a value with a line break.
|
|
272
|
+
|
|
273
|
+
```ts
|
|
274
|
+
await mod.metadata.update('.claude/skills/plan/SKILL.md', (metadata) => {
|
|
275
|
+
metadata['modes'] = 'plan build'
|
|
276
|
+
})
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
`cmod check` reads the metadata of every file the mod ships, and fails one cmod cannot read.
|
|
208
280
|
|
|
209
281
|
### mod.http
|
|
210
282
|
|
|
@@ -218,35 +290,29 @@ mod.http.fetch(url: string, init?: HttpInit): Promise<HttpResponse>
|
|
|
218
290
|
### mod.settings
|
|
219
291
|
|
|
220
292
|
```ts
|
|
221
|
-
mod.settings.
|
|
293
|
+
mod.settings.page(page: Pane<State>): void
|
|
294
|
+
mod.settings.open(pageId?: string): Promise<void>
|
|
222
295
|
```
|
|
223
296
|
|
|
224
|
-
|
|
297
|
+
`/mods` shows each mod's options, permissions, and keys. `page` adds a page of the mod's own to its entry there, drawn as any pane is ([ui.md](ui.md)). `open` opens that page, or with no id opens the mod's entry in `/mods`. `open` rejects for an id `page` did not add.
|
|
225
298
|
|
|
226
|
-
```
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
export const pluginCount = defineMod({
|
|
230
|
-
name: 'plugin-count',
|
|
231
|
-
setup(mod) {
|
|
232
|
-
mod.on('SessionStart', async () => {
|
|
233
|
-
const enabled = (await mod.settings.read({ source: 'user' }))['enabledPlugins'] as Record<string, unknown> | undefined
|
|
234
|
-
mod.ui.toast(`${Object.keys(enabled ?? {}).length} plugins are enabled`)
|
|
235
|
-
})
|
|
236
|
-
},
|
|
237
|
-
})
|
|
299
|
+
```tsx
|
|
300
|
+
mod.settings.page(definePane({ id: 'runs', title: 'Runs', render: (current) => <Text>{current.state.session.lastRun}</Text> }))
|
|
301
|
+
mod.use(slashCommand({ name: 'ci-runs', description: 'Show the CI runs', reply: () => void mod.settings.open('runs') }))
|
|
238
302
|
```
|
|
239
303
|
|
|
304
|
+
Claude Code's own `settings.json` files are read with `mod.claude.settings.read`, such as `(await mod.claude.settings.read({ source: 'user' }))['enabledPlugins']` ([jobs.md](jobs.md#the-claude-members)).
|
|
305
|
+
|
|
240
306
|
### mod.session
|
|
241
307
|
|
|
242
308
|
```ts
|
|
243
309
|
mod.session.messages(): Promise<SessionMessage[]>
|
|
244
|
-
mod.session.messages(args: { agentId
|
|
310
|
+
mod.session.messages(args: { agentId: string }): Promise<SessionMessage[] | { deny: string }>
|
|
245
311
|
mod.session.append(text: string): Promise<void>
|
|
246
312
|
mod.session.submit(text: string): Promise<void>
|
|
247
313
|
```
|
|
248
314
|
|
|
249
|
-
`messages` reads the main conversation, one `{ role, text, toolUses, toolResults }` entry per message. A message has no id of its own, and each tool use carries its `tool_use_id`. `{ agentId }` reads that subagent's conversation instead, the id a `SubagentStart` or `SubagentStop` hook gets as `agent_id`, and answers `{ deny }` when the session cannot read it, so `Array.isArray` tells the two apart.
|
|
315
|
+
`messages` reads the main conversation, one `{ role, text, toolUses, toolResults }` entry per message. A message has no id of its own, and each tool use carries its `tool_use_id`. `{ agentId }` reads that subagent's conversation instead, the id a `SubagentStart` or `SubagentStop` hook gets as `agent_id`, and answers `{ deny }` when the session cannot read it, so `Array.isArray` tells the two apart. The Messages API form, `{ role, content }` with the content blocks whole, is `mod.claude.session.messages({ as: 'api' })`.
|
|
250
316
|
|
|
251
317
|
```ts
|
|
252
318
|
mod.on('SubagentStop', async ({ agent_id }) => {
|