@cmodjs/core 0.1.14 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. package/docs/commands.md +32 -21
  2. package/docs/dependencies.md +8 -0
  3. package/docs/index.md +12 -4
  4. package/docs/install-steps.md +36 -4
  5. package/docs/jobs.md +25 -9
  6. package/docs/mod.md +103 -37
  7. package/docs/options.md +93 -0
  8. package/docs/permissions.md +88 -0
  9. package/docs/state.md +4 -31
  10. package/docs/testing.md +13 -2
  11. package/installer.d.ts +10 -0
  12. package/installer.d.ts.map +1 -0
  13. package/installer.js +8 -0
  14. package/jobs/context.d.ts +2 -2
  15. package/jobs/context.d.ts.map +1 -1
  16. package/jobs/context.js +5 -3
  17. package/jobs/permissions.js +1 -1
  18. package/jobs/program.d.ts.map +1 -1
  19. package/jobs/program.js +5 -1
  20. package/jobs/prompt.js +5 -5
  21. package/jobs/schedule.d.ts.map +1 -1
  22. package/jobs/schedule.js +2 -1
  23. package/jobs/slash-command.d.ts.map +1 -1
  24. package/jobs/slash-command.js +3 -2
  25. package/jobs/status-line.d.ts.map +1 -1
  26. package/jobs/status-line.js +3 -2
  27. package/jobs/tool.d.ts.map +1 -1
  28. package/jobs/tool.js +3 -2
  29. package/mod.d.ts +48 -10
  30. package/mod.d.ts.map +1 -1
  31. package/mod.js +3 -0
  32. package/options.d.ts +58 -0
  33. package/options.d.ts.map +1 -0
  34. package/options.js +66 -0
  35. package/package.json +1 -1
  36. package/records.d.ts +16 -1
  37. package/records.d.ts.map +1 -1
  38. package/records.js +119 -7
  39. package/register.d.ts +4 -2
  40. package/register.d.ts.map +1 -1
  41. package/register.js +8 -2
  42. package/runtime/claude.d.ts +5 -1
  43. package/runtime/claude.d.ts.map +1 -1
  44. package/runtime/grants.d.ts +28 -0
  45. package/runtime/grants.d.ts.map +1 -0
  46. package/runtime/grants.js +286 -0
  47. package/runtime/installer.d.ts +24 -0
  48. package/runtime/installer.d.ts.map +1 -0
  49. package/runtime/installer.js +118 -0
  50. package/runtime/lifecycle.d.ts +6 -3
  51. package/runtime/lifecycle.d.ts.map +1 -1
  52. package/runtime/lifecycle.js +226 -36
  53. package/runtime/metadata.d.ts +20 -0
  54. package/runtime/metadata.d.ts.map +1 -0
  55. package/runtime/metadata.js +143 -0
  56. package/runtime/options.d.ts +21 -0
  57. package/runtime/options.d.ts.map +1 -0
  58. package/runtime/options.js +100 -0
  59. package/runtime/state.d.ts +1 -0
  60. package/runtime/state.d.ts.map +1 -1
  61. package/runtime/state.js +5 -75
  62. package/src/installer.ts +17 -0
  63. package/src/jobs/context.ts +5 -3
  64. package/src/jobs/permissions.ts +1 -1
  65. package/src/jobs/program.ts +4 -1
  66. package/src/jobs/prompt.ts +5 -5
  67. package/src/jobs/schedule.ts +2 -1
  68. package/src/jobs/slash-command.ts +3 -2
  69. package/src/jobs/status-line.ts +3 -2
  70. package/src/jobs/tool.ts +3 -2
  71. package/src/mod.ts +46 -11
  72. package/src/options.ts +91 -0
  73. package/src/records.ts +131 -8
  74. package/src/register.ts +12 -3
  75. package/src/runtime/claude.ts +2 -0
  76. package/src/runtime/grants.ts +306 -0
  77. package/src/runtime/installer.ts +169 -0
  78. package/src/runtime/lifecycle.ts +227 -45
  79. package/src/runtime/metadata.ts +153 -0
  80. package/src/runtime/options.ts +102 -0
  81. package/src/runtime/state.ts +7 -70
  82. package/src/testing/fake-claude.ts +6 -1
  83. package/src/testing/fake-files.ts +9 -3
  84. package/src/testing.ts +40 -15
  85. package/src/utils/metadata.ts +242 -0
  86. package/testing/fake-claude.d.ts +4 -0
  87. package/testing/fake-claude.d.ts.map +1 -1
  88. package/testing/fake-claude.js +5 -1
  89. package/testing/fake-files.d.ts.map +1 -1
  90. package/testing/fake-files.js +9 -3
  91. package/testing.d.ts +6 -2
  92. package/testing.d.ts.map +1 -1
  93. package/testing.js +29 -13
  94. package/utils/metadata.d.ts +21 -0
  95. package/utils/metadata.d.ts.map +1 -0
  96. 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. Checks its imports.
52
- 5. Looks for prebuilt binaries outside `cli/`.
53
- 6. Bundles the hooks module as `cmod publish` does, and names each reason a bundle fails.
54
- 7. Validates it with Claude Code, `claude plugin validate --strict`.
55
- 8. 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.
56
- 9. Lints it with `oxlint`.
57
- 10. Runs, with `bun test`, every test file that does not import `claude-code/testing`.
58
- 11. Runs, with `claude plugin test`, the test files that import `claude-code/testing`. It skips this when none does.
59
- 12. 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.
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,15 +90,15 @@ 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.
94
97
  3. Checks the release with `claude plugin validate --strict`, and stops with the validator's message when it fails.
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
- 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.
100
+ 6. Writes `.claude-plugin/marketplace.json`, listing the archive and the cmod plugin. The cmod entry points at the `release` branch of `heyJordanParker/cmod`, so the mod's `"cmod"` dependency resolves in this marketplace and follows each cmod release with no new publish of the mod.
101
+ 7. Commits that file, tags `v<version>`, 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.
@@ -110,7 +113,6 @@ It needs, and refuses to start without:
110
113
  - a `package.json` with no dependency on a `file:` or `link:` path, which no user has
111
114
  - a `"files"` list, when `package.json` has one, whose every path matches a committed file
112
115
  - a `"program"` in the `cmod` key of `package.json` that names the program `cli/` declares, when `cli/` declares one
113
- - the network, to hash the cmod plugin release it lists, unless the mod is cmod itself
114
116
  - 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
115
117
 
116
118
  `--dry-run` builds everything into a temporary folder, and pushes nothing. It writes `marketplace.json` there too, so the working tree stays as it was. It allows uncommitted changes and an existing tag, and builds from the last commit. It prints the folder that holds the release, so you can read what the directory will read.
@@ -120,8 +122,8 @@ It needs, and refuses to start without:
120
122
  ### cmod install
121
123
 
122
124
  ```text
123
- cmod install <owner/repo | path> [name] [--yes]
124
- cmod install <name | name@marketplace> [--yes]
125
+ cmod install <owner/repo | path> [name] [--yes] [--option key=value]...
126
+ cmod install <name | name@marketplace> [--yes] [--option key=value]...
125
127
  ```
126
128
 
127
129
  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 +133,7 @@ Adds the plugin's marketplace to Claude Code and installs the plugin through Cla
131
133
  - Given the name of a plugin Claude Code already holds, it runs a mod's install step again.
132
134
  - A path holds a `/`, such as `./my-mod`.
133
135
  - `--yes` approves the mod's install and uninstall commands without asking.
136
+ - `--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
137
 
135
138
  ### cmod update
136
139
 
@@ -172,7 +175,7 @@ Downloads a program for an install script: fetches the download for this machine
172
175
 
173
176
  ## Run by cmod
174
177
 
175
- cmod runs these two itself. A person runs them to see a step's whole log or to finish a step.
178
+ 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
179
 
177
180
  ### cmod setup
178
181
 
@@ -180,7 +183,7 @@ cmod runs these two itself. A person runs them to see a step's whole log or to f
180
183
  cmod setup <plugin-root> [--events] [--consent <sha256>] [--yes]
181
184
  ```
182
185
 
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.
186
+ 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. A mod installed from a marketplace whose automatic updates the person has not turned on or off gets them turned on, through `"autoUpdate": true` on the marketplace's `extraKnownMarketplaces` entry in Claude Code's `settings.json`, and the consent question says so. 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
187
 
185
188
  ### cmod teardown
186
189
 
@@ -188,4 +191,12 @@ Downloads the program the mod's `package.json` `cmod.program` names into `~/.loc
188
191
  cmod teardown <plugin-name> [--events]
189
192
  ```
190
193
 
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.
194
+ 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.
195
+
196
+ ### cmod permission
197
+
198
+ ```text
199
+ cmod permission <mod> <name> [value] on|off
200
+ ```
201
+
202
+ 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)).
@@ -52,6 +52,14 @@ A mod that calls tracer lists it in its own `.claude-plugin/plugin.json`, and Cl
52
52
  "dependencies": ["cmod", "tracer"]
53
53
  ```
54
54
 
55
+ A provider that changes or removes a method breaks every mod that calls it, and with automatic updates the provider can update first. Name the versions you tested with a range, and Claude Code turns your mod off with a clear error instead of letting its calls fail one by one:
56
+
57
+ ```json
58
+ "dependencies": ["cmod", { "name": "tracer", "version": "^1.0" }]
59
+ ```
60
+
61
+ Claude Code checks the range against the `version` in the provider's `plugin.json` when it loads your mod ([Claude Code's plugin dependencies](https://code.claude.com/docs/en/plugins/dependencies)).
62
+
55
63
  Claude Code then lays tracer's types file beside the mod's own types, so the call is typed:
56
64
 
57
65
  ```ts
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
@@ -59,7 +59,12 @@ A program the mod needs lives as source in `cli/`. `cmod publish` builds it and
59
59
  | How do I define a mod, and what can `mod` call? | [mod.md](mod.md) |
60
60
  | How does a mod watch something outside Claude Code, tell Claude about it, or ask a model? | [mod.md](mod.md#mod-every) |
61
61
  | How does a mod give Claude a prompt on a schedule, like `/loop`? | [jobs.md](jobs.md#schedule) |
62
- | How do I keep values across prompts, sessions, and projects? How does a person change them? | [state.md](state.md) |
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) |
63
68
  | How do I react to a Claude Code event, and what can a hook answer? | [hooks.md](hooks.md) |
64
69
  | How do I draw a pane, change a row Claude Code draws, or ask the person something? | [ui.md](ui.md) |
65
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) |
@@ -75,10 +80,13 @@ A mod imports from the files below, and every other file in this package is inte
75
80
  | Import | Exports | Docs |
76
81
  | --- | --- | --- |
77
82
  | `mod.js` | `defineMod`, `ModDefinition`, `Mod`, `JobContext`, `Job`, `ProgressStep`, `PaneHandle`, `longestMs`, `messageOf` | [mod.md](mod.md), [jobs.md](jobs.md), [ui.md](ui.md) |
78
- | `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) |
79
86
  | `mod.js` | `ModEvent`, `ModHook`, `HookOptions`, `HookInput`, `HookAnswer` | [hooks.md](hooks.md) |
80
87
  | `mod.js` | `notInstalled` | [dependencies.md](dependencies.md) |
81
- | `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) |
82
90
  | `shell.js` | `parseShell`, `ParsedShell`, `ShellCommand` | [hooks.md](hooks.md) |
83
91
  | `path.js` | `resolve`, `join`, `dirname`, `basename`, `extname`, `relative`, `isAbsolute`, `normalize` | [hooks.md](hooks.md) |
84
92
  | `testing.js` | `testMod`, `TestOptions`, `TestInput`, `TestedMod`, `Fakes`, `Shown`, `TestCall` | [testing.md](testing.md) |
@@ -34,16 +34,48 @@ 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 keys, and asks to run them. cmod hashes the commands, the keys, and every file in each script folder, and remembers the hash the person approved. A change to any of those asks again.
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 asks `<mod> runs <install> to install, runs <uninstall> when you remove it and binds shift+tab to /mode. Install it now?` with `Install` and `Not now`, naming only what the mod has. A progress line above the prompt shows the install.
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 keys, and ask `Set up <mod>? [y/N]`. `--yes` approves without asking.
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
+ ## Automatic updates
47
+
48
+ Claude Code updates a plugin by itself only when its marketplace has automatic updates on, and they are off for every marketplace that is not Anthropic's. So the setup of a mod installed from a marketplace turns them on, through `"autoUpdate": true` on that marketplace's `extraKnownMarketplaces` entry in `settings.json`. The consent lists it as `Update <mod> automatically from the <marketplace> marketplace`, and a mod with nothing to consent to logs it. The setup never changes a choice the person made, in `settings.json` or with the toggle under `/plugin` Marketplaces, and leaves a linked mod alone.
49
+
50
+ Claude Code downloads an update in the background and loads it at the next launch or `/reload-plugins`. An update whose scripts, keys, or permissions changed waits for consent again, so an update never runs a new install step unasked.
51
+
52
+ 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.
53
+
54
+ ## Steps in Claude Code
55
+
56
+ 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:
57
+
58
+ ```ts
59
+ import { defineStep } from '../node_modules/@cmodjs/core/installer.js'
60
+
61
+ const signIn = defineStep({
62
+ id: 'sign-in',
63
+ title: 'Sign in to GitHub',
64
+ render: () => <Text>Run gh auth login in a terminal, then press Next.</Text>,
65
+ isDone: async (mod) => (await mod.process.run(['gh', 'auth', 'status'])).exitCode === 0,
66
+ })
67
+
68
+ export const ciWatch = defineMod({ name: 'ci-watch', installer: [signIn], setup(mod) { … } })
69
+ ```
70
+
71
+ ```ts
72
+ defineStep<State>(step: { id: string; title: string; render(mod, props): RenderElement; isDone(mod): boolean | Promise<boolean> }): Step<State>
73
+ ```
74
+
75
+ - `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.
76
+ - `render` draws the step's body, the way a pane's `render` does ([ui.md](ui.md)).
77
+ - `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`.
78
+ - `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
79
 
48
80
  ## Key bindings
49
81
 
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
- - `claude` reaches Claude Code itself. [The claude members](#the-claude-members) lists each one.
406
- - `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.
407
- - `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. A job with a handler on `tool.check` or `classic.PermissionRequest` needs `registerPermissionCheck` in `hooks/register.ts` ([mod.md](mod.md)).
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> }> = ({ claude, announce }) => {
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?)` | What `mod.settings.read` calls. |
472
+ | `settings.read(args?)` | Reads Claude Code's own `settings.json` files. Without `{ source }` it answers every source merged. `user`, `project`, `local`, `flag`, and `policy` each answer one. |
473
+ | `config.list()`, `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() }`. |