agent-base-kit 0.1.0 → 0.1.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.
- package/README.md +49 -162
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -4,228 +4,124 @@
|
|
|
4
4
|
|
|
5
5
|
# agent-base-kit
|
|
6
6
|
|
|
7
|
-
**
|
|
7
|
+
**The base kit for Claude Code and Codex hooks.**
|
|
8
8
|
|
|
9
9
|
[](https://www.npmjs.com/package/agent-base-kit)
|
|
10
10
|
[](https://www.npmjs.com/package/agent-base-kit)
|
|
11
|
-
[](https://github.com/devmaxxx/agent-kit/actions/workflows/ci.yml)
|
|
11
|
+
[](https://github.com/devmaxxx/agent-base-kit/actions/workflows/ci.yml)
|
|
12
12
|
[](https://www.npmjs.com/package/agent-base-kit)
|
|
13
13
|
[](./package.json)
|
|
14
14
|
[](./package.json)
|
|
15
15
|
[](./LICENSE)
|
|
16
16
|
|
|
17
|
-
[
|
|
17
|
+
[Why](#why) · [What you get](#what-you-get) · [Usage](#usage) · [What install writes](#what-install-writes) · [Fail-closed](#the-fail-closed-contract) · [Kinds](#kinds-and-matchers) · [API](#api)
|
|
18
18
|
|
|
19
19
|
</div>
|
|
20
20
|
|
|
21
|
-
<details>
|
|
22
|
-
<summary><b>Table of contents</b></summary>
|
|
23
|
-
|
|
24
|
-
- [Why](#why)
|
|
25
|
-
- [Features](#features)
|
|
26
|
-
- [Install](#install)
|
|
27
|
-
- [Quick start](#quick-start)
|
|
28
|
-
- [Why a devDependency](#why-a-devdependency)
|
|
29
|
-
- [Kinds and matchers](#kinds-and-matchers)
|
|
30
|
-
- [The fail-closed contract](#the-fail-closed-contract)
|
|
31
|
-
- [What install writes](#what-install-writes)
|
|
32
|
-
- [Shipping as a Claude Code plugin](#shipping-as-a-claude-code-plugin)
|
|
33
|
-
- [API](#api)
|
|
34
|
-
- [Design notes](#design-notes)
|
|
35
|
-
- [Contributing](#contributing)
|
|
36
|
-
- [License](#license)
|
|
37
|
-
|
|
38
|
-
</details>
|
|
39
|
-
|
|
40
21
|
## Why
|
|
41
22
|
|
|
42
|
-
A [PreToolUse hook](https://code.claude.com/docs/en/hooks) is the last point where you can stop an agent's tool call
|
|
23
|
+
A [PreToolUse hook](https://code.claude.com/docs/en/hooks) is the last point where you can stop an agent's tool call. Every hook needs the same plumbing before its own check: read the agent's payload, write the agent's config without breaking anyone else's entries, keep running across Node.js upgrades, and block when the check cannot run, since the agents treat every exit except 2 as "go on".
|
|
43
24
|
|
|
44
|
-
agent-base-kit is that plumbing,
|
|
25
|
+
agent-base-kit is that plumbing, shared by every hook you build. You write the check; reuse the rest for a secret scanner, a policy guard, or any other hook.
|
|
45
26
|
|
|
46
|
-
##
|
|
27
|
+
## What you get
|
|
47
28
|
|
|
48
|
-
- **One function.** `check(call)` gets a typed call (`shell`, `write`, `edit` or `patch`) and returns `{ block, reason }
|
|
49
|
-
- **
|
|
50
|
-
- **
|
|
51
|
-
- **
|
|
52
|
-
- **
|
|
29
|
+
- **One function to write.** `check(call)` gets a typed call (`shell`, `write`, `edit` or `patch`) and returns `{ block, reason }`, sync or async.
|
|
30
|
+
- **Both agents.** Claude Code and Codex, with matchers derived from the kinds you list.
|
|
31
|
+
- **Fail-closed by default.** Unreadable payloads, throws, bad verdicts, hangs and a hook that cannot start all block.
|
|
32
|
+
- **One command to install.** Your bin gets `install` and `uninstall` for `claude` or `codex`, at user or project scope.
|
|
33
|
+
- **Other hooks left alone.** Install and uninstall touch only their own entry, and refuse a config they cannot extend safely.
|
|
34
|
+
- **Survives upgrades.** A user install runs its own copy of your build on the Node.js that installed it, so an nvm or Homebrew upgrade or a rebuild does not switch it off.
|
|
53
35
|
- **Claude Code plugin files.** One call writes the manifest, `hooks.json` and a marketplace.
|
|
54
36
|
- **Zero dependencies**, ESM only, typed.
|
|
55
37
|
|
|
56
|
-
##
|
|
38
|
+
## Usage
|
|
57
39
|
|
|
58
40
|
```sh
|
|
59
41
|
pnpm add -D agent-base-kit
|
|
60
|
-
# or: npm i -D agent-base-kit · yarn add -D agent-base-kit · bun add -d agent-base-kit
|
|
61
42
|
```
|
|
62
43
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
## Quick start
|
|
66
|
-
|
|
67
|
-
A toy check that stops force pushes, as a plugin in three files:
|
|
44
|
+
Write the check and hand it to `runCli` in your bin:
|
|
68
45
|
|
|
69
46
|
```ts
|
|
70
|
-
|
|
71
|
-
import { defineHook } from 'agent-base-kit'
|
|
47
|
+
#!/usr/bin/env node
|
|
48
|
+
import { defineHook, runCli } from 'agent-base-kit'
|
|
72
49
|
|
|
73
|
-
|
|
50
|
+
const plugin = defineHook({
|
|
74
51
|
name: 'no-force-push',
|
|
75
52
|
kinds: ['shell'],
|
|
76
|
-
statusMessage: 'Checking for force pushes',
|
|
77
53
|
check: call => /\bgit\s+push\b.*\s(?:-f|--force)(?:\s|$)/.test(call.command)
|
|
78
54
|
? { block: true, reason: 'Blocked by no-force-push: push without --force, or use --force-with-lease.' }
|
|
79
55
|
: { block: false },
|
|
80
56
|
})
|
|
81
|
-
```
|
|
82
|
-
|
|
83
|
-
```ts
|
|
84
|
-
#!/usr/bin/env node
|
|
85
|
-
// src/cli.ts
|
|
86
|
-
import { runCli } from 'agent-base-kit'
|
|
87
|
-
import { plugin } from './plugin'
|
|
88
57
|
|
|
89
58
|
void runCli(plugin, import.meta.url)
|
|
90
59
|
```
|
|
91
60
|
|
|
92
|
-
|
|
93
|
-
// package.json (the parts that matter)
|
|
94
|
-
{
|
|
95
|
-
"name": "no-force-push",
|
|
96
|
-
"type": "module",
|
|
97
|
-
"bin": { "no-force-push": "./dist/cli.mjs" },
|
|
98
|
-
"files": ["dist"],
|
|
99
|
-
"engines": { "node": ">=22" },
|
|
100
|
-
"devDependencies": {
|
|
101
|
-
"agent-base-kit": "^0.1.0",
|
|
102
|
-
"tsdown": "^0.23.1"
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
```
|
|
106
|
-
|
|
107
|
-
Build `src/cli.ts` with tsdown. That gives you this CLI:
|
|
61
|
+
Bundle the bin into `.mjs` files with agent-base-kit inlined, for example with tsdown, since the stable copy has no `node_modules`. Your users then run:
|
|
108
62
|
|
|
109
63
|
```sh
|
|
110
64
|
npx no-force-push install claude # ~/.claude/settings.json
|
|
111
65
|
npx no-force-push install codex --project # ./.codex/hooks.json
|
|
112
66
|
npx no-force-push uninstall claude
|
|
113
|
-
npx no-force-push hook # what the agent runs, with the tool call on stdin
|
|
114
67
|
```
|
|
115
68
|
|
|
116
|
-
|
|
69
|
+
Requires Node.js 22 or later and a POSIX shell.
|
|
117
70
|
|
|
118
|
-
|
|
71
|
+
## What install writes
|
|
119
72
|
|
|
120
|
-
|
|
73
|
+
A user install (the default scope) does four things:
|
|
121
74
|
|
|
122
|
-
|
|
75
|
+
1. **Copies the build.** The `.mjs` files beside your bin go to `~/.<name>/<build-id>/`, where the id is a hash of their content. The same build reuses its directory, a new build gets a new one, and the copy is staged and renamed in, so a hook never loads a half-written one.
|
|
76
|
+
2. **Runs the copy once.** Before any config points at it, the copy runs on a tool no kind covers. If it cannot start, `install` refuses and changes nothing.
|
|
77
|
+
3. **Swaps `~/.<name>/current`.** One rename moves the link to the new build, so the hook command stays the same across versions and Codex keeps trusting it.
|
|
78
|
+
4. **Writes the hook:**
|
|
123
79
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
| `write` | `{ kind: 'write', path, content }` | `Write`, `NotebookEdit` | `apply_patch` |
|
|
128
|
-
| `edit` | `{ kind: 'edit', path, before, after }` (a MultiEdit's edits joined by newlines) | `Edit`, `MultiEdit` | `apply_patch` |
|
|
129
|
-
| `patch` | `{ kind: 'patch', patch }` (the whole patch text) | none | `apply_patch` |
|
|
80
|
+
```sh
|
|
81
|
+
n='/path/to/the/installing/node'; [ -x "$n" ] || n=node; "$n" '/home/you/.<name>/current/cli.mjs' hook || exit 2
|
|
82
|
+
```
|
|
130
83
|
|
|
131
|
-
`
|
|
84
|
+
`--project` writes `npx --no-install <packageName> hook || exit 2` instead, which is safe to commit. Uninstall removes the entry and leaves the copy, since another agent may still run it.
|
|
132
85
|
|
|
133
|
-
|
|
86
|
+
Install replaces any entry it wrote before and keeps every other one. It refuses, and changes nothing, when the settings file is not JSON or not an object with a `hooks` object and a `PreToolUse` list, or when `~/.<name>/current` exists and is not a link.
|
|
134
87
|
|
|
135
88
|
## The fail-closed contract
|
|
136
89
|
|
|
137
|
-
The
|
|
90
|
+
The hook exits 0 only when your check passes the call:
|
|
138
91
|
|
|
139
92
|
| What happens | Exit |
|
|
140
93
|
| --- | --- |
|
|
141
94
|
| `check` returns `{ block: false }` | 0 |
|
|
142
95
|
| `check` returns `{ block: true, reason }` (the reason goes to stderr as is) | 2 |
|
|
143
|
-
| `check` throws or
|
|
144
|
-
|
|
|
145
|
-
| `check` returns a promise that never settles | 2 |
|
|
146
|
-
| The payload is not JSON, not an object, names no tool, or has a field of the wrong shape (a `command` sent as an array, `edits` as an object) | 2 |
|
|
96
|
+
| `check` throws, rejects, never settles, or returns anything else | 2 |
|
|
97
|
+
| The payload is not JSON, names no tool, or has a field of the wrong shape | 2 |
|
|
147
98
|
| The tool is not one of your kinds | 0, and `check` is not called |
|
|
148
99
|
| The hook cannot start: Node.js is missing, the build was deleted, a dependency was not bundled | 2, from `\|\| exit 2` |
|
|
149
100
|
| The hook runs on a Node.js older than 22 | 2, with a message naming the fix |
|
|
150
|
-
| The hook times out | Claude Code blocks it (`onFailure: "block"
|
|
151
|
-
|
|
152
|
-
`check` may be async. Keep it well inside `timeout`, which defaults to 10 seconds. Past the timeout Codex runs the call unchecked.
|
|
153
|
-
|
|
154
|
-
## What install writes
|
|
155
|
-
|
|
156
|
-
The user scope is the default. Install copies the build to `~/.<name>/<build-id>/` and swaps the `~/.<name>/current` link onto it with a rename. Then it writes:
|
|
157
|
-
|
|
158
|
-
```sh
|
|
159
|
-
n='/path/to/the/installing/node'; [ -x "$n" ] || n=node; "$n" '/home/you/.<name>/current/cli.mjs' hook || exit 2
|
|
160
|
-
```
|
|
161
|
-
|
|
162
|
-
- The hook keeps running on the Node.js that installed it, so a project that pins an older Node does not change it.
|
|
163
|
-
- If that Node.js is removed, the hook falls back to the `node` on PATH.
|
|
164
|
-
- The copy outlives the package. An nvm upgrade or an `npx` cache clean does not remove it.
|
|
165
|
-
- The command stays the same across versions installed by the same Node.js, so Codex keeps trusting it.
|
|
166
|
-
|
|
167
|
-
`--project` writes `npx --no-install <packageName> hook || exit 2`, which is safe to commit.
|
|
168
|
-
|
|
169
|
-
Install replaces any entry it wrote before, including the older forms listed below, and keeps every other entry as it is. It refuses, and changes nothing, when:
|
|
170
|
-
|
|
171
|
-
- the settings file is not JSON, or not an object with a `hooks` object and a `PreToolUse` list (a `null` there counts as absent);
|
|
172
|
-
- `~/.<name>/current` exists and is not a link;
|
|
173
|
-
- the bin is not a built `.mjs` file;
|
|
174
|
-
- the copied build does not start.
|
|
175
|
-
|
|
176
|
-
Uninstall leaves the copy in place, since another agent may still run it.
|
|
177
|
-
|
|
178
|
-
In Claude Code the entry also sets `"onFailure": "block"`. That needs Claude Code 2.1.295 or later. Codex has no such field.
|
|
101
|
+
| The hook times out | Claude Code blocks it (`onFailure: "block"`, Claude Code 2.1.295 or later). Codex lets the call run. |
|
|
179
102
|
|
|
180
|
-
|
|
103
|
+
Keep `check` well inside `timeout`, which defaults to 10 seconds.
|
|
181
104
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
```ts
|
|
185
|
-
// tsdown.config.ts
|
|
186
|
-
import { writeClaudePlugin } from 'agent-base-kit'
|
|
187
|
-
import { defineConfig } from 'tsdown'
|
|
188
|
-
import pkg from './package.json' with { type: 'json' }
|
|
189
|
-
import { plugin } from './src/plugin'
|
|
190
|
-
|
|
191
|
-
export default defineConfig({
|
|
192
|
-
entry: ['src/cli.ts'],
|
|
193
|
-
onSuccess() {
|
|
194
|
-
writeClaudePlugin(import.meta.dirname, {
|
|
195
|
-
plugin,
|
|
196
|
-
entry: 'dist/cli.mjs',
|
|
197
|
-
version: pkg.version,
|
|
198
|
-
description: 'Stops force pushes',
|
|
199
|
-
author: { name: 'you' },
|
|
200
|
-
license: 'MIT',
|
|
201
|
-
marketplace: { name: 'no-force-push', owner: { name: 'you' } },
|
|
202
|
-
})
|
|
203
|
-
},
|
|
204
|
-
})
|
|
205
|
-
```
|
|
206
|
-
|
|
207
|
-
This writes three files:
|
|
208
|
-
|
|
209
|
-
- `.claude-plugin/plugin.json` holds the manifest.
|
|
210
|
-
- `hooks/hooks.json` holds the hook: `node "${CLAUDE_PLUGIN_ROOT}/dist/cli.mjs" hook || exit 2`, with the matcher from `kinds`, your `timeout` and `statusMessage`, and `"onFailure": "block"`.
|
|
211
|
-
- `.claude-plugin/marketplace.json` is written only with `marketplace`. It is a one-plugin marketplace whose source is your npm package.
|
|
105
|
+
## Kinds and matchers
|
|
212
106
|
|
|
213
|
-
|
|
107
|
+
`kinds` decides which calls reach your check, and each agent's matcher is derived from it.
|
|
214
108
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
109
|
+
| Kind | The call your check gets | Claude Code tools | Codex tools |
|
|
110
|
+
| --- | --- | --- | --- |
|
|
111
|
+
| `shell` | `{ kind: 'shell', command }` | `Bash` | `Bash` |
|
|
112
|
+
| `write` | `{ kind: 'write', path, content }` | `Write`, `NotebookEdit` | `apply_patch` |
|
|
113
|
+
| `edit` | `{ kind: 'edit', path, before, after }` (a MultiEdit's edits joined by newlines) | `Edit`, `MultiEdit` | `apply_patch` |
|
|
114
|
+
| `patch` | `{ kind: 'patch', patch }` (the whole patch text) | none | `apply_patch` |
|
|
219
115
|
|
|
220
|
-
|
|
116
|
+
Codex sends every file change as one `apply_patch`, so `install codex` refuses a plugin with `write` or `edit` but no `patch`. A plugin with no tool in an agent, such as `['patch']` for Claude Code, is refused too.
|
|
221
117
|
|
|
222
118
|
## API
|
|
223
119
|
|
|
224
120
|
```ts
|
|
225
121
|
function defineHook<const K extends ToolKind>(definition: HookDefinition<K>): HookPlugin
|
|
226
|
-
function runHook(plugin: HookPlugin, stdin: string, options?: { cwd?: string, stderr?: (text: string) => void }): Promise<number>
|
|
227
|
-
function createCli(plugin: HookPlugin, options?: { bin?: string }): (argv: readonly string[], io: CliIO) => Promise<number>
|
|
228
122
|
function runCli(plugin: HookPlugin, entryUrl: string, options?: { bin?: string }): Promise<void>
|
|
123
|
+
function createCli(plugin: HookPlugin, options?: { bin?: string }): (argv: readonly string[], io: CliIO) => Promise<number>
|
|
124
|
+
function runHook(plugin: HookPlugin, stdin: string, options?: { cwd?: string, stderr?: (text: string) => void }): Promise<number>
|
|
229
125
|
function readStdin(stream?: AsyncIterable<unknown>): Promise<string>
|
|
230
126
|
function install(options: InstallOptions): { file: string, command?: string }
|
|
231
127
|
function uninstall(options: UninstallOptions): { file: string }
|
|
@@ -235,22 +131,13 @@ function claudePluginFiles(options: ClaudePluginOptions): Record<string, string>
|
|
|
235
131
|
function writeClaudePlugin(dir: string, options: ClaudePluginOptions): string[]
|
|
236
132
|
```
|
|
237
133
|
|
|
238
|
-
`
|
|
239
|
-
|
|
240
|
-
## Design notes
|
|
241
|
-
|
|
242
|
-
- **Async checks.** A sync-only API would have to break later to allow I/O. A promise costs nothing now.
|
|
243
|
-
- **The reason is printed as is.** Your plugin owns its message, so start it with the plugin's name. Errors get a `Blocked by <name>, it could not check the call: …` prefix.
|
|
244
|
-
- **Which entries count as its own.** The `npx` form must match exactly. The pinned form matches any file under `~/.<name>/current/`, so a bin renamed between releases still replaces its old entry. The bare `node '<path>' hook` form from before the copy existed needs a directory named exactly after the plugin. So a hook that only mentions the plugin, or another plugin whose path merely contains this one's name, is never removed.
|
|
245
|
-
- **POSIX shells only.** Hook commands rely on `|| exit 2` and single-quoted paths, so Windows is not supported yet.
|
|
134
|
+
`runCli` is the only function that reads the home directory, the working directory and the Node.js path from the process; the others take them as arguments, which keeps tests away from your real `~`. `writeClaudePlugin` writes the files to ship the same hook as a Claude Code plugin instead; use either the plugin or `install claude`, not both.
|
|
246
135
|
|
|
247
136
|
## Contributing
|
|
248
137
|
|
|
249
|
-
Issues and pull requests are welcome.
|
|
250
|
-
|
|
251
138
|
```sh
|
|
252
|
-
git clone https://github.com/devmaxxx/agent-kit.git
|
|
253
|
-
cd agent-kit
|
|
139
|
+
git clone https://github.com/devmaxxx/agent-base-kit.git
|
|
140
|
+
cd agent-base-kit
|
|
254
141
|
pnpm install
|
|
255
142
|
pnpm lint && pnpm typecheck && pnpm test
|
|
256
143
|
```
|
package/package.json
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-base-kit",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.1",
|
|
5
5
|
"packageManager": "pnpm@10.23.0",
|
|
6
|
-
"description": "
|
|
6
|
+
"description": "The base kit for Claude Code and Codex hooks: typed tool calls, a fail-closed runner, install and uninstall with a stable copy of the build, and Claude Code plugin files. Zero dependencies.",
|
|
7
7
|
"author": "devmaxxx",
|
|
8
8
|
"license": "MIT",
|
|
9
|
-
"homepage": "https://github.com/devmaxxx/agent-kit#readme",
|
|
9
|
+
"homepage": "https://github.com/devmaxxx/agent-base-kit#readme",
|
|
10
10
|
"repository": {
|
|
11
11
|
"type": "git",
|
|
12
|
-
"url": "git+https://github.com/devmaxxx/agent-kit.git"
|
|
12
|
+
"url": "git+https://github.com/devmaxxx/agent-base-kit.git"
|
|
13
13
|
},
|
|
14
14
|
"bugs": {
|
|
15
|
-
"url": "https://github.com/devmaxxx/agent-kit/issues"
|
|
15
|
+
"url": "https://github.com/devmaxxx/agent-base-kit/issues"
|
|
16
16
|
},
|
|
17
17
|
"keywords": [
|
|
18
18
|
"ai-agent",
|