@cmodjs/core 0.1.7 → 0.1.9
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 +14 -14
- package/docs/dependencies.md +1 -1
- package/docs/hooks.md +69 -13
- package/docs/index.md +7 -5
- package/docs/install-steps.md +19 -19
- package/docs/jobs.md +22 -21
- package/docs/mod.md +44 -12
- package/docs/state.md +8 -8
- package/docs/testing.md +25 -9
- package/docs/ui.md +33 -9
- package/jobs/check.js +2 -2
- package/jobs/context.d.ts.map +1 -1
- package/jobs/context.js +2 -0
- package/jobs/permissions/decide-permission.js +2 -2
- package/jobs/permissions/find-project-scope.d.ts +1 -1
- package/jobs/permissions/find-project-scope.d.ts.map +1 -1
- package/jobs/permissions/find-project-scope.js +9 -9
- package/jobs/permissions/match-target.d.ts.map +1 -1
- package/jobs/permissions/match-target.js +4 -3
- package/mod.d.ts +15 -5
- package/mod.d.ts.map +1 -1
- package/package.json +2 -1
- package/path.d.ts +2 -0
- package/path.d.ts.map +1 -0
- package/path.js +1 -0
- package/records.js +3 -3
- package/register.d.ts.map +1 -1
- package/register.js +8 -2
- package/runtime/claude.d.ts +4 -1
- package/runtime/claude.d.ts.map +1 -1
- package/runtime/hooks.d.ts +10 -2
- package/runtime/hooks.d.ts.map +1 -1
- package/runtime/hooks.js +56 -18
- package/runtime/lifecycle.d.ts.map +1 -1
- package/runtime/lifecycle.js +17 -7
- package/runtime/ui.d.ts.map +1 -1
- package/runtime/ui.js +38 -8
- package/shell.d.ts +2 -0
- package/shell.d.ts.map +1 -0
- package/shell.js +1 -0
- package/src/jobs/check.ts +2 -2
- package/src/jobs/context.ts +2 -0
- package/src/jobs/permissions/decide-permission.ts +2 -2
- package/src/jobs/permissions/find-project-scope.ts +11 -10
- package/src/jobs/permissions/match-target.ts +4 -3
- package/src/mod.ts +22 -3
- package/src/path.ts +1 -0
- package/src/records.ts +3 -3
- package/src/register.ts +8 -2
- package/src/runtime/claude.ts +11 -1
- package/src/runtime/hooks.ts +62 -23
- package/src/runtime/lifecycle.ts +16 -9
- package/src/runtime/ui.ts +35 -8
- package/src/shell.ts +1 -0
- package/src/testing/fake-claude.ts +14 -5
- package/src/testing/fake-elements.ts +5 -7
- package/src/testing/fake-files.ts +5 -5
- package/src/testing.ts +83 -11
- package/src/ui/define-pane.ts +5 -1
- package/src/utils/call-effects.ts +12 -11
- package/src/utils/parse-shell.ts +28 -36
- package/src/utils/paths.ts +4 -19
- package/src/vendor.ts +1 -0
- package/testing/fake-claude.d.ts +5 -0
- package/testing/fake-claude.d.ts.map +1 -1
- package/testing/fake-claude.js +13 -3
- package/testing/fake-elements.d.ts +5 -8
- package/testing/fake-elements.d.ts.map +1 -1
- package/testing/fake-elements.js +6 -6
- package/testing/fake-files.js +5 -5
- package/testing.d.ts +4 -2
- package/testing.d.ts.map +1 -1
- package/testing.js +65 -9
- package/ui/define-pane.d.ts +5 -1
- package/ui/define-pane.d.ts.map +1 -1
- package/utils/call-effects.d.ts.map +1 -1
- package/utils/call-effects.js +12 -11
- package/utils/parse-shell.d.ts +5 -9
- package/utils/parse-shell.d.ts.map +1 -1
- package/utils/parse-shell.js +5 -10
- package/utils/paths.d.ts +0 -3
- package/utils/paths.d.ts.map +1 -1
- package/utils/paths.js +3 -18
- package/vendor.d.ts +1 -0
- package/vendor.d.ts.map +1 -1
- package/vendor.js +209 -12
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
|
|
|
@@ -49,15 +49,15 @@ Runs every check this machine can run on the mod at `path` (default: the current
|
|
|
49
49
|
2. Checks its layout.
|
|
50
50
|
3. Checks that each step runs a script.
|
|
51
51
|
4. Checks its imports.
|
|
52
|
-
5. Looks for prebuilt binaries
|
|
52
|
+
5. Looks for prebuilt binaries outside `cli/`.
|
|
53
53
|
6. Validates it with Claude Code, `claude plugin validate --strict`.
|
|
54
|
-
7. Type-checks it with `tsc`.
|
|
54
|
+
7. Type-checks it with `tsc`. 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.
|
|
55
55
|
8. Lints it with `oxlint`.
|
|
56
56
|
9. Runs, with `bun test`, every test file that does not import `claude-code/testing`.
|
|
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
|
@@ -7,12 +7,29 @@
|
|
|
7
7
|
```ts
|
|
8
8
|
mod.on<E extends ModEvent>(event: E, hook: ModHook<E>): void
|
|
9
9
|
|
|
10
|
-
type ModHook<E> = (input:
|
|
10
|
+
type ModHook<E> = (input: HookInput<E>) => HookAnswer | void | Promise<HookAnswer | void>
|
|
11
11
|
```
|
|
12
12
|
|
|
13
13
|
Add hooks in `setup`. Each `mod.on` adds one more hook. Several hooks on one event all run.
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
`HookInput<E>`, exported from `mod.js`, is the input a hook on `E` gets, for a hook that lives in a function of its own:
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
import type { HookAnswer, HookInput, Mod } from '../node_modules/@cmodjs/core/mod.js'
|
|
19
|
+
|
|
20
|
+
export async function addBranch(mod: Mod, input: HookInput<'SessionStart'>): Promise<HookAnswer | undefined> {
|
|
21
|
+
const { stdout } = await mod.process.run(['git', 'branch', '--show-current'], { cwd: input.cwd })
|
|
22
|
+
return { hookSpecificOutput: { hookEventName: 'SessionStart', additionalContext: `The git branch is ${stdout.trim()}.` } }
|
|
23
|
+
}
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
A `PreToolUse` hook runs on Claude Code's `tool.call`, before Claude Code checks the call's permission. It rewrites the call's input, adds context to it, or denies it, and Claude Code's permission check sees the rewritten input. A `permissionDecision` of `'allow'` or `'ask'` decides the call's permission, so it takes effect in Claude Code's permission check, and only when the mod's `hooks/register.ts` calls `registerPermissionCheck(addHook)` after `registerMod` ([mod.md](mod.md)):
|
|
27
|
+
|
|
28
|
+
- `'allow'` runs the call without asking the person, and `'ask'` asks them, with `permissionDecisionReason` as the question's reason.
|
|
29
|
+
- A deny from a settings rule or another hook still denies the call, and the mod's own `permissions` job can still ask or deny.
|
|
30
|
+
- Without `registerPermissionCheck`, an `'allow'` or `'ask'` denies the call with `<mod>: the PreToolUse hook answered permissionDecision "allow", so hooks/register.ts must call registerPermissionCheck(addHook) after registerMod.`
|
|
31
|
+
|
|
32
|
+
A hook on `PermissionRequest` takes part in that check too, so a mod with one does not start without `registerPermissionCheck`.
|
|
16
33
|
|
|
17
34
|
```ts
|
|
18
35
|
import { defineMod } from '../node_modules/@cmodjs/core/mod.js'
|
|
@@ -31,7 +48,7 @@ export const safeDelete = defineMod({
|
|
|
31
48
|
|
|
32
49
|
## Every ModEvent
|
|
33
50
|
|
|
34
|
-
`ModEvent` names the
|
|
51
|
+
`ModEvent` names the 18 events a mod hooks. The input of each is the hook input a `settings.json` hook reads for that event, typed as `ClassicHookInputs['<event>']` in the `claude-code` module, with the changes this table names.
|
|
35
52
|
|
|
36
53
|
| Event | Input changes |
|
|
37
54
|
| --- | --- |
|
|
@@ -39,7 +56,7 @@ export const safeDelete = defineMod({
|
|
|
39
56
|
| `SessionEnd` | none |
|
|
40
57
|
| `UserPromptSubmit` | none |
|
|
41
58
|
| `InstructionsLoaded` | none |
|
|
42
|
-
| `PreToolUse` | Claude Mod Manager (
|
|
59
|
+
| `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
60
|
| `PermissionRequest` | none |
|
|
44
61
|
| `PermissionDenied` | none |
|
|
45
62
|
| `PostToolUse` | adds `files` |
|
|
@@ -52,18 +69,57 @@ export const safeDelete = defineMod({
|
|
|
52
69
|
| `Stop` | none |
|
|
53
70
|
| `StopFailure` | none |
|
|
54
71
|
| `CwdChanged` | no `transcript_path` |
|
|
72
|
+
| `FileChanged` | none |
|
|
73
|
+
|
|
74
|
+
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.
|
|
75
|
+
|
|
76
|
+
Claude Code raises `FileChanged` when a file it watches changes, with the file's `file_path` and `event`: `'change'`, `'add'`, or `'unlink'`. It watches the paths a `SessionStart` hook answers in `watchPaths`:
|
|
55
77
|
|
|
56
|
-
|
|
78
|
+
```ts
|
|
79
|
+
mod.on('SessionStart', () => ({ hookSpecificOutput: { hookEventName: 'SessionStart', watchPaths: [`${mod.dataFolder}/history`] } }))
|
|
80
|
+
mod.on('FileChanged', ({ file_path, event }) => mod.ui.toast(`${file_path}: ${event}`))
|
|
81
|
+
```
|
|
57
82
|
|
|
58
83
|
## files: what a tool call reads and changes
|
|
59
84
|
|
|
60
|
-
The input of `PreToolUse`, `PostToolUse`, and `PostToolUseFailure` has `files: { read: string[]; changed: string[] }`, absolute paths that
|
|
85
|
+
The input of `PreToolUse`, `PostToolUse`, and `PostToolUseFailure` has `files: { read: string[]; changed: string[] }`, absolute paths that cmod works out from the call:
|
|
61
86
|
|
|
62
87
|
- `Read` reads its `file_path`. `Edit`, `Write`, and `NotebookEdit` change their path.
|
|
63
88
|
- `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
89
|
- 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
90
|
- `read` lists only paths that are files now. `changed` lists every path the call writes, whether it exists or not.
|
|
66
|
-
- When
|
|
91
|
+
- When cmod cannot work the files out, both lists are empty, and the reason goes to Claude Code's debug log.
|
|
92
|
+
|
|
93
|
+
## shell.js and path.js: read a command line
|
|
94
|
+
|
|
95
|
+
`shell.js` reads a Bash line the way cmod does for `files`, and `path.js` is `node:path`'s POSIX functions, which a mod cannot import from Node:
|
|
96
|
+
|
|
97
|
+
```ts
|
|
98
|
+
parseShell(line: string): ParsedShell
|
|
99
|
+
|
|
100
|
+
type ParsedShell = { commands: ShellCommand[]; reads: string[]; writes: string[]; fetches: string[]; isFullyParsed: boolean }
|
|
101
|
+
type ShellCommand = { argv: [string, ...string[]]; folder: string }
|
|
102
|
+
|
|
103
|
+
resolve(...paths): string join(...paths): string dirname(path): string basename(path, suffix?): string
|
|
104
|
+
extname(path): string relative(from, to): string isAbsolute(path): boolean normalize(path): string
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
- `commands` lists every command the line runs, those inside `sudo`, `bash -c`, `$(…)`, and heredocs included, with flags such as `-rf` split into `-r` and `-f`.
|
|
108
|
+
- `folder` is where a command runs, as the line wrote it: `''` for the folder the line starts in, `src` after `cd src`, `/tmp` after `cd /tmp`, and `~/p` after `cd ~/p`. `resolve(input.cwd, folder)` makes it absolute.
|
|
109
|
+
- `reads`, `writes`, and `fetches` are the paths and URLs the commands name, as written. A path that holds a shell expansion makes `isFullyParsed` false, and so does a line the parser cannot read.
|
|
110
|
+
- `resolve` has no working folder to start from in Claude Code's hooks, so pass an absolute folder first, such as `resolve(mod.cwd, path)`.
|
|
111
|
+
|
|
112
|
+
```ts
|
|
113
|
+
import { basename, resolve } from '../node_modules/@cmodjs/core/path.js'
|
|
114
|
+
import { parseShell } from '../node_modules/@cmodjs/core/shell.js'
|
|
115
|
+
|
|
116
|
+
mod.on('PreToolUse', (input) => {
|
|
117
|
+
if (input.tool_name !== 'Bash') return
|
|
118
|
+
for (const { argv, folder } of parseShell(String(input.tool_input['command'])).commands) {
|
|
119
|
+
if (basename(argv[0]) === 'trace') mod.ui.toast(`trace runs in ${resolve(input.cwd, folder)}`)
|
|
120
|
+
}
|
|
121
|
+
})
|
|
122
|
+
```
|
|
67
123
|
|
|
68
124
|
## HookAnswer: what a hook can answer
|
|
69
125
|
|
|
@@ -99,7 +155,7 @@ type HookAnswer = {
|
|
|
99
155
|
- `continue: false` stops the session after the event, and `stopReason` is the text Claude Code shows. `PreToolUse` takes neither.
|
|
100
156
|
- `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
157
|
- `systemMessage` shows the text to the person as a log line.
|
|
102
|
-
-
|
|
158
|
+
- cmod ignores `suppressOutput`.
|
|
103
159
|
- `hookSpecificOutput.hookEventName` is optional. When set, it must be the event's own name.
|
|
104
160
|
|
|
105
161
|
Each event reads only some `hookSpecificOutput` fields:
|
|
@@ -108,12 +164,12 @@ Each event reads only some `hookSpecificOutput` fields:
|
|
|
108
164
|
| --- | --- |
|
|
109
165
|
| `SessionStart` | `additionalContext`, `initialUserMessage`, `sessionTitle`, `watchPaths`, `reloadSkills` |
|
|
110
166
|
| `UserPromptSubmit` | `additionalContext`, `sessionTitle`, `suppressOriginalPrompt` |
|
|
111
|
-
| `PreToolUse` | `additionalContext`, `permissionDecision
|
|
167
|
+
| `PreToolUse` | `additionalContext`, `permissionDecision` (`'allow'` and `'ask'` need `registerPermissionCheck`), `permissionDecisionReason`, `updatedInput` |
|
|
112
168
|
| `PermissionRequest` | `decision` |
|
|
113
169
|
| `PermissionDenied` | `retry` |
|
|
114
170
|
| `PostToolUse` | `additionalContext`, `updatedToolOutput`, `updatedMCPToolOutput` |
|
|
115
171
|
| `PostToolUseFailure`, `PostToolBatch`, `SubagentStart`, `SubagentStop`, `Stop` | `additionalContext` |
|
|
116
|
-
| `SessionEnd`, `InstructionsLoaded`, `Notification`, `PreCompact`, `StopFailure`, `CwdChanged` | none |
|
|
172
|
+
| `SessionEnd`, `InstructionsLoaded`, `Notification`, `PreCompact`, `StopFailure`, `CwdChanged`, `FileChanged` | none |
|
|
117
173
|
|
|
118
174
|
A field the event does not read is an error: `it answered hookSpecificOutput.<field>, which <event> does not read. Remove it from the answer.` A flag set to `false`, such as `retry: false`, counts as unset.
|
|
119
175
|
|
|
@@ -122,7 +178,7 @@ A field the event does not read is an error: `it answered hookSpecificOutput.<fi
|
|
|
122
178
|
Every mod's hooks and Claude Code's own `settings.json` hooks answer the same event.
|
|
123
179
|
|
|
124
180
|
- `additionalContext` from every hook reaches Claude, each in turn.
|
|
125
|
-
-
|
|
181
|
+
- `PreToolUse` hooks run one inside the next, in the order mods and `mod.on` added them. A deny stops the call there, so no hook after it runs. `updatedInput` replaces the tool's input for every hook after it and for Claude Code. `additionalContext` reaches Claude with the call's result, and not at all when Claude Code refuses the call.
|
|
126
182
|
- For every other field, the mod's answer replaces the answer of the hooks beneath it.
|
|
127
183
|
|
|
128
184
|
```ts
|
|
@@ -147,11 +203,11 @@ A hook that throws, rejects, or answers a field its event does not read fails:
|
|
|
147
203
|
|
|
148
204
|
- On `PreToolUse` the call is denied with `<mod>: the PreToolUse hook failed: <error>`.
|
|
149
205
|
- On `PermissionRequest` the request is denied with `<mod>: the PermissionRequest hook failed: <error>`.
|
|
150
|
-
- On every other event
|
|
206
|
+
- On every other event cmod logs `<mod>: the <event> hook failed: <error>`, and the event goes on as if the hook answered nothing.
|
|
151
207
|
|
|
152
208
|
## The first SessionStart
|
|
153
209
|
|
|
154
|
-
A mod that is still installing when the session starts misses Claude Code's `SessionStart`.
|
|
210
|
+
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
211
|
|
|
156
212
|
## Test a hook
|
|
157
213
|
|
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
|
|
|
@@ -32,9 +32,9 @@ The template is a working example to replace. It counts the prompts of the sessi
|
|
|
32
32
|
|
|
33
33
|
A mod imports `@cmodjs/core` by path, such as `../node_modules/@cmodjs/core/mod.js`, because Claude Code loads a plugin's own files only. Types such as `RenderElement` and `On` come from the `claude-code` module.
|
|
34
34
|
|
|
35
|
-
##
|
|
35
|
+
## Claude Code's types
|
|
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
39
|
## Where a mod runs
|
|
40
40
|
|
|
@@ -72,9 +72,11 @@ A mod imports from the files below, and every other file in this package is inte
|
|
|
72
72
|
| --- | --- | --- |
|
|
73
73
|
| `mod.js` | `defineMod`, `ModDefinition`, `Mod`, `JobContext`, `Job`, `ProgressStep`, `PaneHandle`, `longestMs`, `messageOf` | [mod.md](mod.md), [jobs.md](jobs.md), [ui.md](ui.md) |
|
|
74
74
|
| `mod.js` | `Claude`, `RoutedEvent`, `RoutedHook`, `ToolCalls` | [jobs.md](jobs.md) |
|
|
75
|
-
| `mod.js` | `ModEvent`, `ModHook`, `HookAnswer` | [hooks.md](hooks.md) |
|
|
75
|
+
| `mod.js` | `ModEvent`, `ModHook`, `HookInput`, `HookAnswer` | [hooks.md](hooks.md) |
|
|
76
76
|
| `mod.js` | `notInstalled` | [dependencies.md](dependencies.md) |
|
|
77
77
|
| `register.js` | `registerMod`, `registerPermissionCheck` | [mod.md](mod.md) |
|
|
78
|
+
| `shell.js` | `parseShell`, `ParsedShell`, `ShellCommand` | [hooks.md](hooks.md) |
|
|
79
|
+
| `path.js` | `resolve`, `join`, `dirname`, `basename`, `extname`, `relative`, `isAbsolute`, `normalize` | [hooks.md](hooks.md) |
|
|
78
80
|
| `testing.js` | `testMod`, `TestOptions`, `TestInput`, `TestedMod`, `Fakes`, `Shown`, `TestCall` | [testing.md](testing.md) |
|
|
79
81
|
| `ui/define-pane.js` | `definePane`, `Pane` | [ui.md](ui.md) |
|
|
80
82
|
| `ui/elements.js` | `Box`, `Text`, `Button`, `Link`, `Code`, `Markdown`, `Input`, `Select`, `Image`, `drawWith` | [ui.md](ui.md) |
|
|
@@ -98,4 +100,4 @@ A mod imports from the files below, and every other file in this package is inte
|
|
|
98
100
|
|
|
99
101
|
## Check your work
|
|
100
102
|
|
|
101
|
-
Run `cmod check` in the mod's folder. It installs the packages, checks the layout and the imports, validates the mod with Claude Code, type-checks it
|
|
103
|
+
Run `cmod check` in the mod's folder. It installs the packages, checks the layout and the imports, validates the mod with Claude Code, type-checks it, lints it, and runs its tests. Each failure names its fix. [commands.md](commands.md) lists every step.
|
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
|
|
|
@@ -134,9 +134,9 @@ A mod that builds its own command-line program keeps its source in `cli/`, and C
|
|
|
134
134
|
}
|
|
135
135
|
```
|
|
136
136
|
|
|
137
|
-
The program's name is the one `bin` command, or else `name`. A Rust program declares the same keys under `[package.metadata.cmod]` in `cli/Cargo.toml`.
|
|
137
|
+
The program's name is the one `bin` command, or else `name`. A Rust program declares the same keys under `[package.metadata.cmod]` in `cli/Cargo.toml`, and its name is the one `[[bin]]` `name`, or else the `[package]` `name`.
|
|
138
138
|
|
|
139
|
-
2. The build writes one file per machine into `output`, each named `<program>-<os>-<arch>`, such as `hello-darwin-arm64`.
|
|
139
|
+
2. The build writes one file per machine into `output`, each named `<program>-<os>-<arch>`, such as `hello-darwin-arm64`. cmod runs it with `CMOD_MACHINES` set to the machines it needs, separated by spaces: `cmod link` names this machine alone, and `cmod publish` names all four. A build that ignores `CMOD_MACHINES` and builds every machine works too, only slower to link. cmod takes only the files the build wrote in that run, so a build left from an earlier run is never released.
|
|
140
140
|
|
|
141
141
|
3. The mod's own `package.json` names the program: `"cmod": { "program": "hello" }`. `.claude-plugin/plugin.json` names the GitHub `repository`.
|
|
142
142
|
|
|
@@ -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` finds in a folder ahead of `~/.local/bin`.
|
|
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
|
|
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
|
|
365
|
-
- `announce` adds a phrase to the list
|
|
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
|
+
- `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)).
|
|
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'
|
|
@@ -405,15 +405,14 @@ Each member calls the member of the same name in Claude Code's hooks API, `$`, w
|
|
|
405
405
|
| `ui.open(pane)` | Opens a pane, `{ id, title?, focus?, closeOnEscape?, holdToasts?, rows?, columns? }`, and resolves `{ isPlaced }`. |
|
|
406
406
|
| `ui.close(pane)` | Closes the open pane with that `{ id }`. |
|
|
407
407
|
| `ui.panes()` | The plugin's open panes: each one's id, title, and whether it is shown, holds the keyboard, and is placed. |
|
|
408
|
+
| `ui.scroll(args)` | What `mod.ui.scroll` calls. |
|
|
408
409
|
| `ui.resolve(e)` | The element table of the surface a `ui.render` event draws on. |
|
|
409
410
|
| `process.run(argv, init?)` | Runs a program to its end. `mod.process.run` is built on it. |
|
|
410
411
|
| `process.spawn(request)` | Starts a program and streams its output. `mod.process.spawn` is built on it. |
|
|
411
|
-
| `fs.read`, `fs.write`, `fs.list` | What `mod.fs` calls. A relative path is under the session's working folder. |
|
|
412
|
-
| `fs.exists(path)` | Resolves whether the path exists. |
|
|
413
|
-
| `fs.stat(path, options?)` | Resolves `{ kind, size, mtimeMs, isLink }`, and `realPath` with `{ resolve: true }`. It rejects for a missing path. |
|
|
412
|
+
| `fs.read`, `fs.write`, `fs.list`, `fs.exists`, `fs.stat` | What `mod.fs` calls. A relative path is under the session's working folder. |
|
|
414
413
|
| `http.fetch(url, init?)` | What `mod.http.fetch` calls. |
|
|
415
414
|
| `settings.read(args?)` | What `mod.settings.read` calls. |
|
|
416
|
-
| `store.get`, `store.set`, `store.delete`, `store.keys` | The plugin's key-value store, where
|
|
415
|
+
| `store.get`, `store.set`, `store.delete`, `store.keys` | The plugin's key-value store, where cmod also keeps `mod.state`. |
|
|
417
416
|
| `clock.now()` | Resolves milliseconds since the epoch. |
|
|
418
417
|
| `clock.after(ms, fn)` | Calls `fn` once after `ms` milliseconds, and returns `{ cancel() }`. |
|
|
419
418
|
| `clock.every(ms, fn)` | Calls `fn` every `ms` milliseconds, at least 1, and returns `{ cancel() }`. |
|
|
@@ -423,9 +422,11 @@ Each member calls the member of the same name in Claude Code's hooks API, `$`, w
|
|
|
423
422
|
| `session.model()` | The main conversation's model, as `/model` shows it. |
|
|
424
423
|
| `session.usage()` | When the session began, the context window's fill, the rate-limit windows, and the cost, as the status line has them. |
|
|
425
424
|
| `session.surfaces()` | Every surface the session draws on, `terminal` first. Empty in a plain `-p` run. |
|
|
425
|
+
| `session.messages(args?)` | What `mod.session.messages` calls. |
|
|
426
426
|
| `command.register(spec)` | Adds a slash command. `slashCommand` is built on it. |
|
|
427
427
|
| `tool.register(spec)` | Adds a tool. `tool` is built on it. |
|
|
428
428
|
| `agent.list()` | The session's subagents and teammates so far. |
|
|
429
|
+
| `agent.spawn(args)` | What `mod.agent.spawn` calls. |
|
|
429
430
|
| `env.home()` | The `HOME` variable, or `undefined` when it is unset. |
|
|
430
431
|
| `env.dataHome()` | The `XDG_DATA_HOME` variable, or `undefined`. |
|
|
431
432
|
| `env.configHome()` | The `CLAUDE_CONFIG_DIR` variable, or `undefined`. |
|