@cmodjs/core 0.1.1 → 0.1.3
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 +6 -2
- package/docs/commands.md +175 -0
- package/docs/dependencies.md +102 -0
- package/docs/hooks.md +156 -0
- package/docs/index.md +101 -0
- package/docs/install-steps.md +151 -0
- package/docs/jobs.md +452 -0
- package/docs/mod.md +217 -0
- package/docs/state.md +140 -0
- package/docs/testing.md +263 -0
- package/docs/ui.md +295 -0
- package/package.json +3 -2
- package/ui/slots.d.ts +2 -0
- package/ui/slots.d.ts.map +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @cmodjs/core
|
|
2
2
|
|
|
3
|
-
The library Claude
|
|
3
|
+
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.
|
|
4
4
|
|
|
5
5
|
```sh
|
|
6
6
|
npm i -g @cmodjs/cli
|
|
@@ -9,4 +9,8 @@ cmod new my-mod
|
|
|
9
9
|
|
|
10
10
|
`cmod new` writes a mod that depends on this package. A mod imports it by path, such as `../node_modules/@cmodjs/core/mod.js`, because Claude Code loads a plugin's own files only.
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
## Docs
|
|
13
|
+
|
|
14
|
+
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).
|
|
15
|
+
|
|
16
|
+
The CMod plugin and the `cmod` source live at [github.com/heyJordanParker/cmod](https://github.com/heyJordanParker/cmod).
|
package/docs/commands.md
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
# cmod commands
|
|
2
|
+
|
|
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
|
+
|
|
5
|
+
Each command below restates what `cmod <command> --help` prints, for a mod author. The help text is the full word.
|
|
6
|
+
|
|
7
|
+
## Build a mod
|
|
8
|
+
|
|
9
|
+
### cmod new
|
|
10
|
+
|
|
11
|
+
```text
|
|
12
|
+
cmod new <name> [--project]
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Creates a mod in `./<name>`: a `defineMod` with one hook, one pane, and one render of the band above the prompt, a Skill, and a test. Then installs its packages.
|
|
16
|
+
|
|
17
|
+
- `<name>` is lowercase letters, digits, and `-`, starting with a letter.
|
|
18
|
+
- `--project` creates a project-scope plugin in `./.claude/skills/<name>/` of the repository in the current folder, which loads for everyone who trusts the repository. Run it in the repository's root.
|
|
19
|
+
- `CMOD_CORE` sets the `@cmodjs/core` dependency it writes. By default it writes `^<the version of this cmod>`. To build against an unreleased `@cmodjs/core`, point it at a packed tarball, such as `file:/path/to/cmodjs-core-<version>.tgz`. Only a `.tgz` works: a folder installs as links into that folder, and `claude plugin validate --strict` refuses the mod.
|
|
20
|
+
- When the packages fail to install, it deletes the new folder.
|
|
21
|
+
|
|
22
|
+
### cmod link
|
|
23
|
+
|
|
24
|
+
```text
|
|
25
|
+
cmod link [path] [--yes]
|
|
26
|
+
```
|
|
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 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
|
+
|
|
30
|
+
- `--yes` approves the mod's install and uninstall commands without asking.
|
|
31
|
+
|
|
32
|
+
### cmod unlink
|
|
33
|
+
|
|
34
|
+
```text
|
|
35
|
+
cmod unlink [path]
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Stops loading the checkout at `path` (default: the current folder) in new Claude Code sessions. Removes the folder from `CLAUDE_CODE_PLUGIN_DIRS` in Claude Code's `settings.json`, runs the checkout's uninstall step, and removes the program `cmod link` built into `~/.local/bin`.
|
|
39
|
+
|
|
40
|
+
### cmod check
|
|
41
|
+
|
|
42
|
+
```text
|
|
43
|
+
cmod check [path]
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Runs every check this machine can run on the mod at `path` (default: the current folder), each failure with its fix:
|
|
47
|
+
|
|
48
|
+
1. Installs its packages.
|
|
49
|
+
2. Checks its layout.
|
|
50
|
+
3. Checks that each step runs a script.
|
|
51
|
+
4. Checks its imports.
|
|
52
|
+
5. Looks for prebuilt binaries.
|
|
53
|
+
6. Validates it with Claude Code, `claude plugin validate --strict`.
|
|
54
|
+
7. Type-checks it with `tsc`. It skips this until `.claude-plugin/types/` exists, which Claude Code writes the first time a session loads the mod: run `cmod link`, start `claude` once, then run `cmod check` again.
|
|
55
|
+
8. Lints it with `oxlint`.
|
|
56
|
+
9. Runs, with `bun test`, every test file that does not import `claude-code/testing`.
|
|
57
|
+
10. Runs, with `claude plugin test`, the test files that import `claude-code/testing`. It skips this when none does.
|
|
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
|
+
|
|
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
|
+
|
|
62
|
+
### cmod try
|
|
63
|
+
|
|
64
|
+
```text
|
|
65
|
+
cmod try <owner/repo | path> [--yes] [-- claude arguments]
|
|
66
|
+
```
|
|
67
|
+
|
|
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
|
+
|
|
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
|
+
|
|
72
|
+
- When the session ends, even when its terminal closes, it runs the mod's uninstall step and deletes its record, data folder, approval, and program, so the mod stays uninstalled. A checkout already set up from the same folder keeps its setup.
|
|
73
|
+
- A GitHub mod is cloned into a temporary folder, which is deleted too.
|
|
74
|
+
- A mod another folder has set up is refused, so `cmod try` never replaces an installed copy.
|
|
75
|
+
- Arguments after `--` go to `claude`, such as `-- -p "hello"`.
|
|
76
|
+
|
|
77
|
+
### cmod publish
|
|
78
|
+
|
|
79
|
+
```text
|
|
80
|
+
cmod publish [path] [--dry-run]
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Releases the mod at `path` (default: the current folder) at the version in its `plugin.json`:
|
|
84
|
+
|
|
85
|
+
1. Builds the release archive from the committed files with `git archive`, leaving out `cli/`.
|
|
86
|
+
2. Builds the program `cli/` declares.
|
|
87
|
+
3. Writes `SHA256SUMS` for every file of the release.
|
|
88
|
+
4. Writes `.claude-plugin/marketplace.json`, listing the archive and the CMod plugin.
|
|
89
|
+
5. Commits that file, tags `v<version>`, pushes, and creates the GitHub release.
|
|
90
|
+
|
|
91
|
+
It needs, and refuses to start without:
|
|
92
|
+
|
|
93
|
+
- the mod's folder as the root of a git repository with at least one commit, because `git archive` builds from `HEAD`
|
|
94
|
+
- an `origin` remote on GitHub
|
|
95
|
+
- a `package.json` with no dependency on a `file:` or `link:` path, which no user has
|
|
96
|
+
- a `"program"` in the `cmod` key of `package.json` that names the program `cli/` declares, when `cli/` declares one
|
|
97
|
+
- the network, to hash the CMod plugin release it lists, unless the mod is CMod itself
|
|
98
|
+
- 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
|
|
99
|
+
|
|
100
|
+
`--dry-run` builds and writes everything, and pushes nothing. It allows uncommitted changes and an existing tag, and builds from the last commit.
|
|
101
|
+
|
|
102
|
+
## Install mods
|
|
103
|
+
|
|
104
|
+
### cmod install
|
|
105
|
+
|
|
106
|
+
```text
|
|
107
|
+
cmod install <owner/repo | path> [name] [--yes]
|
|
108
|
+
cmod install <name | name@marketplace> [--yes]
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
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.
|
|
112
|
+
|
|
113
|
+
- When the marketplace lists several plugins, name the one to install after it.
|
|
114
|
+
- Given `name@marketplace` of a marketplace Claude Code has added, it installs that plugin.
|
|
115
|
+
- Given the name of a plugin Claude Code already holds, it runs a mod's install step again.
|
|
116
|
+
- A path holds a `/`, such as `./my-mod`.
|
|
117
|
+
- `--yes` approves the mod's install and uninstall commands without asking.
|
|
118
|
+
|
|
119
|
+
### cmod update
|
|
120
|
+
|
|
121
|
+
```text
|
|
122
|
+
cmod update [name] [--yes]
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Updates the named plugin, or every plugin Claude Code has installed, through Claude Code, then runs the install step of each mod whose version or scripts changed. A linked plugin is skipped, because saving its files updates it. When Claude Code fails to update a plugin, `cmod update` names it, updates the rest, and exits 1. A running session loads the new version after `/reload-plugins`.
|
|
126
|
+
|
|
127
|
+
- `--yes` approves changed install and uninstall commands without asking.
|
|
128
|
+
|
|
129
|
+
### cmod remove
|
|
130
|
+
|
|
131
|
+
```text
|
|
132
|
+
cmod remove <name>
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
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.
|
|
136
|
+
|
|
137
|
+
### cmod list
|
|
138
|
+
|
|
139
|
+
```text
|
|
140
|
+
cmod list
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
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.
|
|
144
|
+
|
|
145
|
+
- 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.
|
|
146
|
+
|
|
147
|
+
## Run in an install script
|
|
148
|
+
|
|
149
|
+
### cmod download
|
|
150
|
+
|
|
151
|
+
```text
|
|
152
|
+
cmod download <program> <machine> <url> <sha256> [<machine> <url> <sha256> …]
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
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.
|
|
156
|
+
|
|
157
|
+
## Run by CMod
|
|
158
|
+
|
|
159
|
+
CMod runs these two itself. A person runs them to see a step's whole log or to finish a step.
|
|
160
|
+
|
|
161
|
+
### cmod setup
|
|
162
|
+
|
|
163
|
+
```text
|
|
164
|
+
cmod setup <plugin-root> [--events] [--consent <sha256>] [--yes]
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
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.
|
|
168
|
+
|
|
169
|
+
### cmod teardown
|
|
170
|
+
|
|
171
|
+
```text
|
|
172
|
+
cmod teardown <plugin-name> [--events]
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
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.
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# Call another mod
|
|
2
|
+
|
|
3
|
+
A mod offers methods to other mods in `api`. Another mod lists it as a dependency and calls those methods through `mod.dependencies`, typed from the provider's own types file.
|
|
4
|
+
|
|
5
|
+
## Offer methods: api
|
|
6
|
+
|
|
7
|
+
Each method of `api` gets the caller's input and the provider's own `mod`, and returns its result or a promise of it. Inputs and results cross from one plugin to another, so keep them plain JSON data.
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
import { defineMod } from '../node_modules/@cmodjs/core/mod.js'
|
|
11
|
+
|
|
12
|
+
export const tracer = defineMod({
|
|
13
|
+
name: 'tracer',
|
|
14
|
+
api: {
|
|
15
|
+
signatures: async ({ path }: { path: string }, mod) =>
|
|
16
|
+
(await mod.fs.read(path)).split('\n').flatMap((text, index) => {
|
|
17
|
+
const name = /^export function (\w+)/.exec(text)?.[1]
|
|
18
|
+
return name === undefined ? [] : [{ name, line: index + 1 }]
|
|
19
|
+
}),
|
|
20
|
+
},
|
|
21
|
+
setup() {},
|
|
22
|
+
})
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Type the methods: CmodDependencies
|
|
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 (CMod) plugin declares on the `claude-code` module:
|
|
28
|
+
|
|
29
|
+
```ts
|
|
30
|
+
export type TracerSignature = { name: string; line: number }
|
|
31
|
+
|
|
32
|
+
export type Tracer = {
|
|
33
|
+
signatures(input: { path: string }): Promise<TracerSignature[]>
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
declare module 'claude-code' {
|
|
37
|
+
interface CmodDependencies {
|
|
38
|
+
tracer: Tracer
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
- Each method takes one input and returns a promise.
|
|
44
|
+
- Once the name is in `CmodDependencies`, `tsc` checks the provider's own `api` against it, so a method that returns another shape fails `cmod check`.
|
|
45
|
+
- A types file augments only `'claude-code'`. Claude Code refuses a types file that augments any other module.
|
|
46
|
+
|
|
47
|
+
## Call the methods: mod.dependencies
|
|
48
|
+
|
|
49
|
+
A mod that calls tracer lists it in its own `.claude-plugin/plugin.json`, and Claude Code installs it with the mod:
|
|
50
|
+
|
|
51
|
+
```json
|
|
52
|
+
"dependencies": ["cmod", "tracer"]
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Claude Code then lays tracer's types file beside the mod's own types, so the call is typed:
|
|
56
|
+
|
|
57
|
+
```ts
|
|
58
|
+
import { defineMod } from '../node_modules/@cmodjs/core/mod.js'
|
|
59
|
+
import { slashCommand } from '../node_modules/@cmodjs/core/jobs/slash-command.js'
|
|
60
|
+
|
|
61
|
+
export const outline = defineMod({
|
|
62
|
+
name: 'outline',
|
|
63
|
+
setup(mod) {
|
|
64
|
+
mod.use(
|
|
65
|
+
slashCommand({
|
|
66
|
+
name: 'outline',
|
|
67
|
+
description: 'List the functions a file exports',
|
|
68
|
+
reply: async ({ args }, mod) => {
|
|
69
|
+
const signatures = await mod.dependencies.tracer.signatures({ path: args })
|
|
70
|
+
return signatures.map(({ name, line }) => `${name}:${line}`).join('\n')
|
|
71
|
+
},
|
|
72
|
+
}),
|
|
73
|
+
)
|
|
74
|
+
},
|
|
75
|
+
})
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## When a call fails
|
|
79
|
+
|
|
80
|
+
A call that cannot answer rejects with one of these messages:
|
|
81
|
+
|
|
82
|
+
```text
|
|
83
|
+
tracer is not installed. Run cmod install tracer.
|
|
84
|
+
tracer is installing. Try again when it's ready.
|
|
85
|
+
tracer has no method signatures.
|
|
86
|
+
tracer: <the message of the error the method threw>
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
- `tracer is not installed` covers a provider whose code did not load, one the person declined to install, and one whose install failed.
|
|
90
|
+
- `tracer is installing` covers a provider that is still running its install step.
|
|
91
|
+
- When tracer is disabled, Claude Code unloads the mod that lists it, with its own error: `Dependency "tracer" is disabled — enable it or remove the dependency`.
|
|
92
|
+
- A call runs inside the deadline of the job that makes it ([jobs.md](jobs.md)), and 30 seconds anywhere else. Past the deadline of a job, the call fails with `mod.dependencies.tracer.signatures passed the 30 s deadline of slashCommand`, naming the job. Anywhere else it fails with `mod.dependencies.tracer.signatures passed its 30 s deadline`.
|
|
93
|
+
|
|
94
|
+
```ts
|
|
95
|
+
notInstalled(name: string): string
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
`notInstalled`, exported from `mod.js`, returns the first message for a mod name: `<name> is not installed. Run cmod install <name>.` A test uses it to expect that message.
|
|
99
|
+
|
|
100
|
+
## Test the calls
|
|
101
|
+
|
|
102
|
+
`testMod(outline, { dependencies: { tracer: { signatures: async () => [] } } })` answers tracer's methods with the functions given. A call to a mod the test does not fake fails with `tracer is not installed. Run cmod install tracer.` [testing.md](testing.md) covers it.
|
package/docs/hooks.md
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
# Hooks
|
|
2
|
+
|
|
3
|
+
`mod.on` runs a function when Claude Code raises an event, such as a prompt or a tool call. The function reads the event's input and may answer to change what Claude Code does next.
|
|
4
|
+
|
|
5
|
+
## mod.on
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
mod.on<E extends ModEvent>(event: E, hook: ModHook<E>): void
|
|
9
|
+
|
|
10
|
+
type ModHook<E> = (input: HookInputs[E]) => HookAnswer | void | Promise<HookAnswer | void>
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Add hooks in `setup`. Each `mod.on` adds one more hook. Several hooks on one event all run.
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
import { defineMod } from '../node_modules/@cmodjs/core/mod.js'
|
|
17
|
+
|
|
18
|
+
export const safeDelete = defineMod({
|
|
19
|
+
name: 'safe-delete',
|
|
20
|
+
setup(mod) {
|
|
21
|
+
mod.on('PreToolUse', (input) => {
|
|
22
|
+
const command = input.tool_input['command']
|
|
23
|
+
if (input.tool_name !== 'Bash' || typeof command !== 'string' || !command.startsWith('rm ')) return
|
|
24
|
+
return { hookSpecificOutput: { hookEventName: 'PreToolUse', updatedInput: { ...input.tool_input, command: command.replace(/^rm /, 'trash ') } } }
|
|
25
|
+
})
|
|
26
|
+
},
|
|
27
|
+
})
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Every ModEvent
|
|
31
|
+
|
|
32
|
+
`ModEvent` names the 17 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.
|
|
33
|
+
|
|
34
|
+
| Event | Input changes |
|
|
35
|
+
| --- | --- |
|
|
36
|
+
| `SessionStart` | none |
|
|
37
|
+
| `SessionEnd` | none |
|
|
38
|
+
| `UserPromptSubmit` | none |
|
|
39
|
+
| `InstructionsLoaded` | none |
|
|
40
|
+
| `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`. |
|
|
41
|
+
| `PermissionRequest` | none |
|
|
42
|
+
| `PermissionDenied` | none |
|
|
43
|
+
| `PostToolUse` | adds `files` |
|
|
44
|
+
| `PostToolUseFailure` | adds `files` |
|
|
45
|
+
| `PostToolBatch` | none |
|
|
46
|
+
| `SubagentStart` | none |
|
|
47
|
+
| `SubagentStop` | none |
|
|
48
|
+
| `Notification` | none |
|
|
49
|
+
| `PreCompact` | none |
|
|
50
|
+
| `Stop` | none |
|
|
51
|
+
| `StopFailure` | none |
|
|
52
|
+
| `CwdChanged` | no `transcript_path` |
|
|
53
|
+
|
|
54
|
+
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.
|
|
55
|
+
|
|
56
|
+
## files: what a tool call reads and changes
|
|
57
|
+
|
|
58
|
+
The input of `PreToolUse`, `PostToolUse`, and `PostToolUseFailure` has `files: { read: string[]; changed: string[] }`, absolute paths that CMod works out from the call:
|
|
59
|
+
|
|
60
|
+
- `Read` reads its `file_path`. `Edit`, `Write`, and `NotebookEdit` change their path.
|
|
61
|
+
- `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.
|
|
62
|
+
- 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.
|
|
63
|
+
- `read` lists only paths that are files now. `changed` lists every path the call writes, whether it exists or not.
|
|
64
|
+
- When CMod cannot work the files out, both lists are empty, and the reason goes to Claude Code's debug log.
|
|
65
|
+
|
|
66
|
+
## HookAnswer: what a hook can answer
|
|
67
|
+
|
|
68
|
+
Return nothing to leave the event as it is. Return a `HookAnswer` to change it. The fields are the ones a `settings.json` hook writes as JSON:
|
|
69
|
+
|
|
70
|
+
```ts
|
|
71
|
+
type HookAnswer = {
|
|
72
|
+
continue?: boolean
|
|
73
|
+
stopReason?: string
|
|
74
|
+
suppressOutput?: boolean
|
|
75
|
+
systemMessage?: string
|
|
76
|
+
decision?: 'block'
|
|
77
|
+
reason?: string
|
|
78
|
+
hookSpecificOutput?: {
|
|
79
|
+
hookEventName?: ModEvent
|
|
80
|
+
additionalContext?: string
|
|
81
|
+
permissionDecision?: 'allow' | 'deny' | 'ask'
|
|
82
|
+
permissionDecisionReason?: string
|
|
83
|
+
updatedInput?: Record<string, unknown>
|
|
84
|
+
sessionTitle?: string
|
|
85
|
+
suppressOriginalPrompt?: boolean
|
|
86
|
+
initialUserMessage?: string
|
|
87
|
+
watchPaths?: string[]
|
|
88
|
+
reloadSkills?: boolean
|
|
89
|
+
decision?: PermissionRequestDecision
|
|
90
|
+
updatedToolOutput?: unknown
|
|
91
|
+
updatedMCPToolOutput?: unknown
|
|
92
|
+
retry?: boolean
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
- `continue: false` stops the session after the event, and `stopReason` is the text Claude Code shows. `PreToolUse` takes neither.
|
|
98
|
+
- `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.
|
|
99
|
+
- `systemMessage` shows the text to the person as a log line.
|
|
100
|
+
- CMod ignores `suppressOutput`.
|
|
101
|
+
- `hookSpecificOutput.hookEventName` is optional. When set, it must be the event's own name.
|
|
102
|
+
|
|
103
|
+
Each event reads only some `hookSpecificOutput` fields:
|
|
104
|
+
|
|
105
|
+
| Event | `hookSpecificOutput` fields it reads |
|
|
106
|
+
| --- | --- |
|
|
107
|
+
| `SessionStart` | `additionalContext`, `initialUserMessage`, `sessionTitle`, `watchPaths`, `reloadSkills` |
|
|
108
|
+
| `UserPromptSubmit` | `additionalContext`, `sessionTitle`, `suppressOriginalPrompt` |
|
|
109
|
+
| `PreToolUse` | `additionalContext`, `permissionDecision`, `permissionDecisionReason`, `updatedInput` |
|
|
110
|
+
| `PermissionRequest` | `decision` |
|
|
111
|
+
| `PermissionDenied` | `retry` |
|
|
112
|
+
| `PostToolUse` | `additionalContext`, `updatedToolOutput`, `updatedMCPToolOutput` |
|
|
113
|
+
| `PostToolUseFailure`, `PostToolBatch`, `SubagentStart`, `SubagentStop`, `Stop` | `additionalContext` |
|
|
114
|
+
| `SessionEnd`, `InstructionsLoaded`, `Notification`, `PreCompact`, `StopFailure`, `CwdChanged` | none |
|
|
115
|
+
|
|
116
|
+
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.
|
|
117
|
+
|
|
118
|
+
## How answers combine
|
|
119
|
+
|
|
120
|
+
Every mod's hooks and Claude Code's own `settings.json` hooks answer the same event.
|
|
121
|
+
|
|
122
|
+
- `additionalContext` from every hook reaches Claude, each in turn.
|
|
123
|
+
- On `PreToolUse` the strictest decision wins: `deny` over `ask` over `allow`. A mod's `allow` never overrides a `deny` from another hook.
|
|
124
|
+
- For every other field, the mod's answer replaces the answer of the hooks beneath it.
|
|
125
|
+
|
|
126
|
+
```ts
|
|
127
|
+
import { defineMod } from '../node_modules/@cmodjs/core/mod.js'
|
|
128
|
+
|
|
129
|
+
export const keepGoing = defineMod({
|
|
130
|
+
name: 'keep-going',
|
|
131
|
+
state: { session: { hasCheckedTests: false } },
|
|
132
|
+
setup(mod) {
|
|
133
|
+
mod.on('Stop', () => {
|
|
134
|
+
if (mod.state.session.hasCheckedTests) return
|
|
135
|
+
mod.state.session.hasCheckedTests = true
|
|
136
|
+
return { decision: 'block', reason: 'Run the tests before you stop.' }
|
|
137
|
+
})
|
|
138
|
+
},
|
|
139
|
+
})
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
## When a hook fails
|
|
143
|
+
|
|
144
|
+
A hook that throws, rejects, or answers a field its event does not read fails:
|
|
145
|
+
|
|
146
|
+
- On `PreToolUse` the call is denied with `<mod>: the PreToolUse hook failed: <error>`.
|
|
147
|
+
- On `PermissionRequest` the request is denied with `<mod>: the PermissionRequest hook failed: <error>`.
|
|
148
|
+
- On every other event CMod logs `<mod>: the <event> hook failed: <error>`, and the event goes on as if the hook answered nothing.
|
|
149
|
+
|
|
150
|
+
## The first SessionStart
|
|
151
|
+
|
|
152
|
+
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.
|
|
153
|
+
|
|
154
|
+
## Test a hook
|
|
155
|
+
|
|
156
|
+
`tested.fire(event, input)` runs the mod's hooks on an event and returns the combined answer. It fills `session_id`, `transcript_path`, `cwd`, `hook_event_name`, and `tool_use_id`, so a test gives only the event's own fields. [testing.md](testing.md) covers it.
|
package/docs/index.md
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# Claude Mod Manager docs
|
|
2
|
+
|
|
3
|
+
These are the docs of the `@cmodjs/core` version installed beside them. Read them before you write mod code. They match this version, and memory of another version does not.
|
|
4
|
+
|
|
5
|
+
## What a mod is
|
|
6
|
+
|
|
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
|
+
|
|
9
|
+
`cmod new my-mod` writes this layout:
|
|
10
|
+
|
|
11
|
+
```text
|
|
12
|
+
my-mod/
|
|
13
|
+
├── .claude-plugin/plugin.json name, version, and "dependencies": ["cmod"]
|
|
14
|
+
├── .claude/CLAUDE.md facts for coding agents, starting with these docs
|
|
15
|
+
├── .gitignore node_modules/ and .claude-plugin/types/
|
|
16
|
+
├── .oxlintrc.json leaves .claude-plugin/types/ out of the lint
|
|
17
|
+
├── hooks/hooks.json names hooks/register.ts as the hooks module
|
|
18
|
+
├── hooks/register.ts connect(on, myMod), and nothing else
|
|
19
|
+
├── src/mod.tsx defineMod: the state, the setup, the hooks, the panes
|
|
20
|
+
├── src/state.ts the starting state
|
|
21
|
+
├── src/panes/prompts.tsx one definePane per file
|
|
22
|
+
├── src/components/prompt-count.tsx what the panes and slot renders draw with
|
|
23
|
+
├── skills/my-mod/SKILL.md a Skill the mod ships
|
|
24
|
+
├── tests/mod.test.ts testMod tests, run with bun test
|
|
25
|
+
├── package.json depends on @cmodjs/core
|
|
26
|
+
└── tsconfig.json extends .claude-plugin/types/tsconfig.json, with the JSX options
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
`cmod new --project` writes the same files, less `.claude/CLAUDE.md`, into `.claude/skills/<name>/` of the repository.
|
|
30
|
+
|
|
31
|
+
The template is a working example to replace. It counts the prompts of the session in `state.session.prompts`, shows the count in a pane titled with the mod's name and above the prompt, and tests both. Replace the counter, the pane, the component, the Skill, and the tests with the mod's own.
|
|
32
|
+
|
|
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 `Register` come from the `claude-code` module.
|
|
34
|
+
|
|
35
|
+
## Type-check after the first session
|
|
36
|
+
|
|
37
|
+
Claude Code writes `.claude-plugin/types/` the first time a session loads the mod. That folder holds the `tsconfig.json` the mod's own extends, the `claude-code` module, and the `h` JSX global. Until it exists, `tsc` fails, and `cmod check` skips its type check. Run `cmod link`, start `claude` once, then type-check. A project plugin needs no link: start `claude` in its repository and trust it.
|
|
38
|
+
|
|
39
|
+
## Where a mod runs
|
|
40
|
+
|
|
41
|
+
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)).
|
|
42
|
+
|
|
43
|
+
`cmod check` fails a mod that breaks this:
|
|
44
|
+
|
|
45
|
+
- a file in `src/` that imports a Node module, `bun`, or a `bun:` module. Tests in `tests/` run under `bun test` and may import `bun:test`.
|
|
46
|
+
- a file that imports from `cli/`, which the release leaves out
|
|
47
|
+
- `devDependencies` in `package.json`. Claude Code installs them for every person, so move each into `dependencies` or drop it.
|
|
48
|
+
- a `bin/` or `target/` folder at the mod's root
|
|
49
|
+
- a prebuilt binary in the mod, outside `cli/`, `node_modules/`, `.git/`, `target/`, `.target/`, and `.claude-plugin/types/`
|
|
50
|
+
|
|
51
|
+
A program the mod needs lives as source in `cli/`. `cmod publish` builds it and attaches the builds to the release ([install-steps.md](install-steps.md)).
|
|
52
|
+
|
|
53
|
+
## Which file answers which question
|
|
54
|
+
|
|
55
|
+
| Question | File |
|
|
56
|
+
| --- | --- |
|
|
57
|
+
| How do I define a mod, and what can `mod` call? | [mod.md](mod.md) |
|
|
58
|
+
| How do I keep values across prompts, sessions, and projects? How does a person change them? | [state.md](state.md) |
|
|
59
|
+
| How do I react to a Claude Code event, and what can a hook answer? | [hooks.md](hooks.md) |
|
|
60
|
+
| How do I draw a pane, change a row Claude Code draws, or ask the person something? | [ui.md](ui.md) |
|
|
61
|
+
| How do I add a slash command, a tool, a permission rule, a check, a prompt, a status line, or a background program? | [jobs.md](jobs.md) |
|
|
62
|
+
| How does one mod call another? | [dependencies.md](dependencies.md) |
|
|
63
|
+
| How does a mod change the machine, ship a program, or clean up after itself? | [install-steps.md](install-steps.md) |
|
|
64
|
+
| How do I test a mod without Claude Code? | [testing.md](testing.md) |
|
|
65
|
+
| Which `cmod` command links, checks, tries, or publishes a mod? | [commands.md](commands.md) |
|
|
66
|
+
|
|
67
|
+
## The Public API
|
|
68
|
+
|
|
69
|
+
A mod imports from the files below, and every other file in this package is internal. A few of their exports serve the library itself, and their docs say so, such as `drawWith` in [ui.md](ui.md).
|
|
70
|
+
|
|
71
|
+
| Import | Exports | Docs |
|
|
72
|
+
| --- | --- | --- |
|
|
73
|
+
| `mod.js` | `defineMod`, `ModDefinition`, `Mod`, `PartContext`, `Part`, `ProgressStep`, `PaneHandle`, `longestMs`, `messageOf` | [mod.md](mod.md), [jobs.md](jobs.md), [ui.md](ui.md) |
|
|
74
|
+
| `mod.js` | `ModEvent`, `ModHook`, `HookAnswer` | [hooks.md](hooks.md) |
|
|
75
|
+
| `mod.js` | `notInstalled` | [dependencies.md](dependencies.md) |
|
|
76
|
+
| `connect.js` | `connect` | [mod.md](mod.md) |
|
|
77
|
+
| `testing.js` | `testMod`, `TestOptions`, `TestInput`, `TestedMod`, `Fakes`, `Shown`, `TestCall` | [testing.md](testing.md) |
|
|
78
|
+
| `ui/define-pane.js` | `definePane`, `Pane` | [ui.md](ui.md) |
|
|
79
|
+
| `ui/elements.js` | `Box`, `Text`, `Button`, `Link`, `Code`, `Markdown`, `Input`, `Select`, `Image`, `drawWith` | [ui.md](ui.md) |
|
|
80
|
+
| `ui/slots.js` | `slots`, `Slot`, `SlotProps` | [ui.md](ui.md) |
|
|
81
|
+
| `ui/markdown.js` | `markdownSlots`, `markdownBlocks`, `MarkdownKind`, `MarkdownReader`, `MarkdownPiece` | [ui.md](ui.md) |
|
|
82
|
+
| `jobs/slash-command.js` | `slashCommand`, `Reply` | [jobs.md](jobs.md) |
|
|
83
|
+
| `jobs/tool.js` | `tool` | [jobs.md](jobs.md) |
|
|
84
|
+
| `jobs/permissions.js` | `permissions`, `Rule`, `PermissionRules`, `Target`, `CommandCall`, `FileCall`, `FetchCall`, `ToolCall` | [jobs.md](jobs.md) |
|
|
85
|
+
| `jobs/check.js` | `check` | [jobs.md](jobs.md) |
|
|
86
|
+
| `jobs/prompt.js` | `prompt` | [jobs.md](jobs.md) |
|
|
87
|
+
| `jobs/status-line.js` | `statusLine`, `Usage` | [jobs.md](jobs.md) |
|
|
88
|
+
| `jobs/program.js` | `program`, `Program` | [jobs.md](jobs.md) |
|
|
89
|
+
| `jobs/part-context.js` | `afterCall`, `modWithin`, `modOf`, `workspaceReader`, `targetWords` | [jobs.md](jobs.md) |
|
|
90
|
+
| `jobs/permissions/match-target.js` | `targetOf`, `matchTarget`, `folderInScope`, `Target`, `TargetKey`, `TargetOf`, `TargetCalls`, `MatchedCall` | [jobs.md](jobs.md) |
|
|
91
|
+
| `jobs/permissions/decide-permission.js` | `decidePermission`, `stricterVerdict`, `Rule`, `PermissionRules`, `Decision`, `Verdict` | [jobs.md](jobs.md) |
|
|
92
|
+
| `jobs/permissions/find-project-scope.js` | `findProjectScope`, `ProjectScope`, `Workspace` | [jobs.md](jobs.md) |
|
|
93
|
+
|
|
94
|
+
`jobs/permissions.js` and `jobs/permissions/decide-permission.js` each export a `Rule` and a `PermissionRules`, and the two pairs differ:
|
|
95
|
+
|
|
96
|
+
- The `jobs/permissions.js` pair takes the mod's state type. Its `when` gets a `Mod<State>`, so `Rule<State>` is `decide-permission.js`'s `Rule<Mod<State>>`. A mod writes its rules with this pair.
|
|
97
|
+
- The `decide-permission.js` pair takes the type of `when`'s second argument itself, for code that decides permissions without a mod.
|
|
98
|
+
|
|
99
|
+
## Check your work
|
|
100
|
+
|
|
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 once `.claude-plugin/types/` exists, lints it, and runs its tests. Each failure names its fix. [commands.md](commands.md) lists every step.
|