inject-steer 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +11 -0
- package/LICENSE +21 -0
- package/README.md +253 -2
- package/bin/inject.js +4 -0
- package/mod/.claude-plugin/plugin.json +6 -0
- package/mod/hooks/hooks.json +4 -0
- package/mod/hooks/register.js +69 -0
- package/mod/tsconfig.json +3 -0
- package/package.json +34 -4
- package/src/adapters/claude-code/hook.js +78 -0
- package/src/adapters/claude-code/index.js +14 -0
- package/src/adapters/claude-code/install.js +211 -0
- package/src/adapters/claude-code/output.js +39 -0
- package/src/adapters/claude-code/plain.js +47 -0
- package/src/adapters/claude-code/prompt-hook.js +20 -0
- package/src/adapters/claude-code/prompt.js +14 -0
- package/src/adapters/claude-code/steering.js +98 -0
- package/src/adapters/index.js +8 -0
- package/src/adapters/types.js +28 -0
- package/src/cli.js +40 -0
- package/src/core/format.js +32 -0
- package/src/core/ipc.js +61 -0
- package/src/core/log.js +34 -0
- package/src/core/queue.js +56 -0
- package/src/core/session.js +50 -0
- package/src/wrapper/intercept.js +349 -0
- package/src/wrapper/overlay.js +158 -0
- package/src/wrapper/pty.js +43 -0
- package/src/wrapper/run.js +130 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 inject contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,254 @@
|
|
|
1
|
-
#
|
|
1
|
+
# inject
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
<img width="800" height="500" alt="ezgif-825eae4d6b687d00" src="https://github.com/user-attachments/assets/ca05823c-99ae-4c70-b605-27cf69f3743c" />
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
Steer Claude Code while it works, in the same terminal window.
|
|
7
|
+
|
|
8
|
+
Run `inject` instead of `claude`. While Claude is mid-task, type:
|
|
9
|
+
|
|
10
|
+
```
|
|
11
|
+
/inject use pnpm, not npm
|
|
12
|
+
/inject! stop, don't touch the v1 routes
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
The line never reaches Claude Code's input box. It goes into a local queue,
|
|
16
|
+
and a Claude Code hook delivers it at the next tool boundary, without stopping
|
|
17
|
+
the agent. Every message is logged to `.steer/fixes.jsonl`.
|
|
18
|
+
|
|
19
|
+
`/inject <msg>` also works if you start plain `claude` instead of `inject`
|
|
20
|
+
(`inject --install` adds a small Claude Code plugin for that), without urgent
|
|
21
|
+
mode. See [Plain `claude` sessions](#plain-claude-sessions).
|
|
22
|
+
|
|
23
|
+
| You type | Urgency | Delivered | What Claude sees | `inject` | plain `claude` |
|
|
24
|
+
| --- | --- | --- | --- | :-: | :-: |
|
|
25
|
+
| `/inject <msg>` | normal | after the current tool call finishes | extra context next to the tool result | yes | yes |
|
|
26
|
+
| `/inject! <msg>` | urgent | before the next tool call runs | that call is **blocked** and the message is the reason | yes | no |
|
|
27
|
+
|
|
28
|
+
## Quickstart
|
|
29
|
+
|
|
30
|
+
1. **Install** (Node 18+, macOS, Linux, or Windows via WSL2):
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
npm install -g .
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Run this from a clone of this repository. The package isn't on npm yet.
|
|
37
|
+
Once it is published it will be `npm install -g inject-steer`.
|
|
38
|
+
|
|
39
|
+
2. **Register the hooks and the `/inject` command** (one time, and again after
|
|
40
|
+
upgrading inject). It edits `~/.claude/settings.json` (keeping a backup) and
|
|
41
|
+
installs a small Claude Code plugin with `claude plugin install`, so the
|
|
42
|
+
`claude` command must be on your PATH:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
inject --install
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
3. **Use `inject` wherever you'd use `claude`.** All arguments pass through
|
|
49
|
+
(`inject --resume`, `inject --model opus`, ...). While Claude works, type
|
|
50
|
+
`/inject <message>` or `/inject! <message>` and press Enter.
|
|
51
|
+
|
|
52
|
+
Prefer to keep typing `claude`? After step 2, start a new `claude` session and
|
|
53
|
+
`/inject <message>` works there too, just without urgent mode.
|
|
54
|
+
|
|
55
|
+
## What you'll see
|
|
56
|
+
|
|
57
|
+
- While typing a message, a bar at the bottom of the terminal shows what
|
|
58
|
+
you're writing. **Enter** queues it and **Esc** cancels. Claude never sees
|
|
59
|
+
the keystrokes.
|
|
60
|
+
- After Enter: `queued · delivers at next step` (normal) or
|
|
61
|
+
`queued · will block the next tool call` (urgent). A small
|
|
62
|
+
`N queued` marker stays until the message is picked up.
|
|
63
|
+
- On delivery: `✓ inject: delivered (just ran Bash: npm test)`.
|
|
64
|
+
- If Claude Code exits with messages still queued, inject prints them and
|
|
65
|
+
logs them with `"delivered_at": null`.
|
|
66
|
+
|
|
67
|
+
## How it works
|
|
68
|
+
|
|
69
|
+
```
|
|
70
|
+
keyboard ──► inject (node-pty) ──────────────► claude
|
|
71
|
+
│ "/inject ..." lines ▲ hooks inherit INJECT_DIR
|
|
72
|
+
▼ are held back │
|
|
73
|
+
queue + unix socket ◄──── claim ──── hook (PreToolUse / PostToolUse)
|
|
74
|
+
$INJECT_DIR/sock │
|
|
75
|
+
│ └─► JSON back to Claude Code
|
|
76
|
+
└─► .steer/fixes.jsonl
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
- **Wrapper** (`src/wrapper/`): starts the agent in a pseudo-terminal and
|
|
80
|
+
passes every byte through unchanged. It keeps a shadow copy of what you've
|
|
81
|
+
typed since the last Enter. When that copy is empty and you start typing
|
|
82
|
+
`/inject`, the keys are held back. If what you type turns out to be
|
|
83
|
+
something else, like `/init`, the held keys are forwarded exactly as typed.
|
|
84
|
+
- **Queue** (`src/core/`): lives in the wrapper process and listens on a unix
|
|
85
|
+
socket in a private per-session directory (mode 0700). The path reaches the
|
|
86
|
+
agent's hooks through the `INJECT_DIR` environment variable, so several
|
|
87
|
+
`inject` sessions in different terminals never cross. Claims are atomic, so
|
|
88
|
+
parallel tool calls can't deliver a message twice.
|
|
89
|
+
- **Hooks** (`src/adapters/claude-code/`): the tool hooks are shell one-liners
|
|
90
|
+
that exit right away unless a message of the right kind is queued, so tool
|
|
91
|
+
calls with nothing queued never start Node. The `UserPromptSubmit` hook is
|
|
92
|
+
the exception: it runs Node once per prompt (see
|
|
93
|
+
[Plain `claude` sessions](#plain-claude-sessions)).
|
|
94
|
+
|
|
95
|
+
### Claude Code hook events used
|
|
96
|
+
|
|
97
|
+
Based on the [hooks reference](https://code.claude.com/docs/en/hooks):
|
|
98
|
+
|
|
99
|
+
| Event | Used for | Output |
|
|
100
|
+
| --- | --- | --- |
|
|
101
|
+
| `PostToolUse`, `PostToolUseFailure` | normal notes | `hookSpecificOutput.additionalContext`. Claude Code wraps it in a system reminder that Claude reads on its next request |
|
|
102
|
+
| `PreToolUse` | urgent messages | `hookSpecificOutput.permissionDecision: "deny"` with `permissionDecisionReason`, which is shown to Claude. Any notes still queued ride along in the same reason |
|
|
103
|
+
| `UserPromptSubmit` | logging messages typed into Claude Code's own queue | none; it only appends to `.steer/fixes.jsonl` |
|
|
104
|
+
|
|
105
|
+
Hook changes take effect in Claude Code sessions started after
|
|
106
|
+
`inject --install`.
|
|
107
|
+
|
|
108
|
+
## `.steer/fixes.jsonl`
|
|
109
|
+
|
|
110
|
+
One line per message, written in the project directory (`$CLAUDE_PROJECT_DIR`):
|
|
111
|
+
|
|
112
|
+
```json
|
|
113
|
+
{"ts":"2026-10-16T15:38:04.772Z","text":"stop, don't touch the v1 routes","urgency":"urgent","agent":"claude-code","delivered_at":"2026-10-16T15:38:08.889Z","delivered_via":"PreToolUse","doing":"about to run Edit: src/v1/routes.ts","session_id":"f6afb1c7-..."}
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
`ts` is when you queued the message. `doing` is the tool call Claude was about
|
|
117
|
+
to make, or had just made, when the message was delivered.
|
|
118
|
+
|
|
119
|
+
`delivered_via` says how the message reached Claude:
|
|
120
|
+
|
|
121
|
+
| `delivered_via` | Meaning |
|
|
122
|
+
| --- | --- |
|
|
123
|
+
| `PostToolUse`, `PostToolUseFailure` | an `/inject` note, added as context after a tool call (wrapper or plain `claude`, via the mod) |
|
|
124
|
+
| `PreToolUse` | an `/inject!` message that blocked a tool call |
|
|
125
|
+
| `claude-code-queue` | a message typed into Claude Code's own queue mid-turn. Claude Code delivers it itself once the running tool calls finish, so `delivered_at` is `null` |
|
|
126
|
+
| `null` | an `/inject` message still queued when Claude Code exited |
|
|
127
|
+
|
|
128
|
+
## Plain `claude` sessions
|
|
129
|
+
|
|
130
|
+
After `inject --install`, `/inject <message>` also works in a session started with
|
|
131
|
+
plain `claude`, even while Claude is working. There is no wrapper and no urgent mode:
|
|
132
|
+
|
|
133
|
+
| | `inject` | plain `claude` |
|
|
134
|
+
| --- | --- | --- |
|
|
135
|
+
| `/inject <msg>` | yes | yes |
|
|
136
|
+
| `/inject! <msg>` | yes, blocks the next tool call | no (Claude Code says "Unknown command") |
|
|
137
|
+
| Live "queued" bar at the bottom of the terminal | yes | a short toast: "queued, delivers at the next step" |
|
|
138
|
+
| Message reaches Claude | next tool boundary | next tool boundary |
|
|
139
|
+
| Logged to `.steer/fixes.jsonl` | yes | yes |
|
|
140
|
+
|
|
141
|
+
**Why a plugin and not just a hook.** Claude Code answers `Unknown command: /inject`
|
|
142
|
+
by itself, before any hook runs, and it holds a command typed while Claude is working
|
|
143
|
+
until the turn ends. A hook therefore can't catch `/inject`. A Claude Code
|
|
144
|
+
[mod](https://code.claude.com/docs/en/plugins/mods/overview) can register a real
|
|
145
|
+
command with `immediate: true`, which runs at once even mid-turn. That is all the
|
|
146
|
+
`mod/` folder is: one command, about 60 lines.
|
|
147
|
+
|
|
148
|
+
How it works:
|
|
149
|
+
|
|
150
|
+
1. `/inject use pnpm` runs the mod's command. It appends the message to a small file,
|
|
151
|
+
`~/.claude/inject-pending/<session id>.jsonl` (or under `$CLAUDE_CONFIG_DIR`), and
|
|
152
|
+
shows a toast. Nothing is sent to Claude yet, and the command prints nothing into
|
|
153
|
+
the conversation.
|
|
154
|
+
2. After Claude's next tool call, the same `PostToolUse` hook that the wrapper uses
|
|
155
|
+
claims that file and returns the message as `additionalContext`. It is delivered
|
|
156
|
+
once, and only to the session that queued it. The hook also logs it to
|
|
157
|
+
`.steer/fixes.jsonl`.
|
|
158
|
+
|
|
159
|
+
`inject --install` runs `claude plugin marketplace add <this repo>` and
|
|
160
|
+
`claude plugin install inject@inject`. Because it is a directory marketplace, the
|
|
161
|
+
plugin loads in place: edits under `mod/` apply at the next session start.
|
|
162
|
+
`inject --uninstall` removes it again. If `claude` isn't on your PATH, `inject --install`
|
|
163
|
+
prints the two commands to run yourself.
|
|
164
|
+
|
|
165
|
+
Other messages you type mid-turn (no `/inject` prefix) go into Claude Code's own queue
|
|
166
|
+
as usual. The `UserPromptSubmit` hook logs those to `.steer/fixes.jsonl` too, with
|
|
167
|
+
`delivered_via: "claude-code-queue"`. It keeps the last `prompt_id` per session: a
|
|
168
|
+
queued message carries the running turn's `prompt_id`, and a new turn gets a new one.
|
|
169
|
+
That hook prints nothing and always exits 0, so it never changes or blocks a prompt. It
|
|
170
|
+
runs once per prompt in every Claude Code session that reads `~/.claude/settings.json`,
|
|
171
|
+
including the desktop app's.
|
|
172
|
+
|
|
173
|
+
Limits:
|
|
174
|
+
|
|
175
|
+
- **No urgent mode.** Only a wrapper that holds the queue can make the next tool
|
|
176
|
+
call fail.
|
|
177
|
+
- **A tool call is needed.** A message queued while Claude writes its final
|
|
178
|
+
answer waits for Claude's next tool call, which may be in your next turn.
|
|
179
|
+
- **New sessions only.** Already-running `claude` sessions don't have the command until
|
|
180
|
+
you restart them (or run `/reload-plugins`).
|
|
181
|
+
- **Folder trust.** Claude Code loads plugins only in folders you've trusted.
|
|
182
|
+
- **Desktop app: type plain text, not `/inject`.** The desktop app holds every slash
|
|
183
|
+
command until the turn ends, so `/inject` only runs after Claude has finished. A
|
|
184
|
+
plain message typed mid-turn is delivered after the current tool call and logged to
|
|
185
|
+
`.steer/fixes.jsonl` as `claude-code-queue`.
|
|
186
|
+
- **Inside the wrapper** `/inject` lines are held back before Claude Code sees them,
|
|
187
|
+
so the mod isn't involved.
|
|
188
|
+
- A message you take back out of Claude Code's own queue with `Up` has already
|
|
189
|
+
been logged.
|
|
190
|
+
|
|
191
|
+
## Things to know
|
|
192
|
+
|
|
193
|
+
- **Claude Code's own queue.** Recent Claude Code versions already queue a
|
|
194
|
+
normal message you type while Claude works, and pass it to Claude once the
|
|
195
|
+
current tool calls finish. inject adds `/inject`, the urgent mode, the log,
|
|
196
|
+
and an agent-neutral core that other agents can plug into. Messages sent that
|
|
197
|
+
way are logged too.
|
|
198
|
+
- **Delivery needs a tool boundary.** A message queued while Claude writes its
|
|
199
|
+
final answer stays queued until Claude's next tool call, which may be in
|
|
200
|
+
your next turn. The `N queued` marker shows that it is waiting.
|
|
201
|
+
- **Parallel tool calls.** An urgent message blocks the first tool call that
|
|
202
|
+
reaches the hook. Other calls in the same parallel batch still run.
|
|
203
|
+
- **Detection is conservative.** After cursor keys, Tab completion, or `Esc`
|
|
204
|
+
while typing a prompt, inject can't be sure Claude's input box is empty. It
|
|
205
|
+
stops looking for `/inject` until the next Enter or Ctrl+C. Pressing Enter on
|
|
206
|
+
an empty prompt resets it.
|
|
207
|
+
- **`/` is held for one keystroke.** Claude's slash-command menu appears when
|
|
208
|
+
you type the second character instead of the first.
|
|
209
|
+
- **Windows:** use WSL2. Native Windows isn't supported, because the hooks are
|
|
210
|
+
POSIX shell one-liners and use unix sockets.
|
|
211
|
+
- **Linux / WSL2:** node-pty has no prebuilt binary there, so it compiles
|
|
212
|
+
during install. That needs `python3`, `make` and a C++ compiler. If npm
|
|
213
|
+
skipped node-pty's install script, `inject` tells you which
|
|
214
|
+
`npm rebuild node-pty` to run.
|
|
215
|
+
|
|
216
|
+
## Uninstall
|
|
217
|
+
|
|
218
|
+
```bash
|
|
219
|
+
inject --uninstall
|
|
220
|
+
npm uninstall -g inject-steer
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
`--uninstall` removes only inject's hooks (they're tagged `# inject-hook`) and the
|
|
224
|
+
`inject@inject` plugin, and leaves the rest of your settings alone.
|
|
225
|
+
|
|
226
|
+
## Adding another agent
|
|
227
|
+
|
|
228
|
+
Everything except `src/adapters/claude-code/` is agent-neutral. An adapter
|
|
229
|
+
provides:
|
|
230
|
+
|
|
231
|
+
- the command to start (`claude`),
|
|
232
|
+
- `install` / `uninstall` for its hook configuration,
|
|
233
|
+
- a hook entry point. It reads the agent's hook input, sends
|
|
234
|
+
`{op:"claim", boundary:"before-tool"|"after-tool", ...}` to `$INJECT_DIR/sock`,
|
|
235
|
+
and turns the reply into that agent's output: block the call for
|
|
236
|
+
`before-tool`, add context for `after-tool`.
|
|
237
|
+
|
|
238
|
+
See `src/adapters/types.js` for the contract and `src/adapters/claude-code/`
|
|
239
|
+
for a reference implementation.
|
|
240
|
+
|
|
241
|
+
## Development
|
|
242
|
+
|
|
243
|
+
```bash
|
|
244
|
+
npm install
|
|
245
|
+
npm test
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
The tests drive the real wrapper in a pseudo-terminal with a fake agent. They
|
|
249
|
+
run the exact hook commands that `inject --install` writes, the same way
|
|
250
|
+
Claude Code would.
|
|
251
|
+
|
|
252
|
+
## License
|
|
253
|
+
|
|
254
|
+
MIT
|
package/bin/inject.js
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// inject, inside Claude Code: adds /inject <message> to plain `claude` sessions.
|
|
2
|
+
//
|
|
3
|
+
// A hook can't do this: Claude Code answers "Unknown command: /inject" itself,
|
|
4
|
+
// before UserPromptSubmit fires, and holds a command typed mid-turn until the
|
|
5
|
+
// turn ends. A mod can register a real command with immediate: true, which runs
|
|
6
|
+
// at once even while Claude is working.
|
|
7
|
+
//
|
|
8
|
+
// The command only queues the message: it appends a line to
|
|
9
|
+
// <config dir>/inject-pending/<session id>.jsonl. inject's PostToolUse hook
|
|
10
|
+
// (src/adapters/claude-code/hook.js) claims that file after Claude's next tool
|
|
11
|
+
// call, adds the message to Claude's context and logs it to .steer/fixes.jsonl.
|
|
12
|
+
// Urgent mode (/inject! blocking the next tool call) needs the `inject` wrapper.
|
|
13
|
+
|
|
14
|
+
const MAX_LENGTH = 2000
|
|
15
|
+
|
|
16
|
+
// Keep in sync with pendingDir() in src/adapters/claude-code/plain.js and the
|
|
17
|
+
// shell guard in install.js.
|
|
18
|
+
async function pendingDir($) {
|
|
19
|
+
const config = await $.env.get('CLAUDE_CONFIG_DIR')
|
|
20
|
+
if (config) return config + '/inject-pending'
|
|
21
|
+
const home = await $.env.get('HOME')
|
|
22
|
+
return (home || '') + '/.claude/inject-pending'
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function queue($, text) {
|
|
26
|
+
const id = await $.session.id()
|
|
27
|
+
const cwd = await $.session.cwd()
|
|
28
|
+
if (!id || !/^[\w-]+$/.test(id)) throw new Error('no session id')
|
|
29
|
+
const file = (await pendingDir($)) + '/' + id + '.jsonl'
|
|
30
|
+
let before = ''
|
|
31
|
+
try {
|
|
32
|
+
before = await $.fs.read(file)
|
|
33
|
+
} catch {
|
|
34
|
+
// First message of this session.
|
|
35
|
+
}
|
|
36
|
+
const record = { text, queuedAt: new Date().toISOString(), projectDir: cwd || null }
|
|
37
|
+
await $.fs.write(file, before + JSON.stringify(record) + '\n')
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function register(on) {
|
|
41
|
+
on('session.start', async ($, e, next) => {
|
|
42
|
+
try {
|
|
43
|
+
await $.command.register({
|
|
44
|
+
name: 'inject',
|
|
45
|
+
description: 'Send Claude a note; it reads it at its next tool call, without being interrupted',
|
|
46
|
+
argumentHint: '<message>',
|
|
47
|
+
immediate: true,
|
|
48
|
+
})
|
|
49
|
+
} catch {
|
|
50
|
+
// Name taken: leave the session alone.
|
|
51
|
+
}
|
|
52
|
+
return next(e)
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
on('command.run', { command: 'inject' }, async ($, e) => {
|
|
56
|
+
const text = (e.args || '').replace(/\s+/g, ' ').trim().slice(0, MAX_LENGTH)
|
|
57
|
+
if (!text) {
|
|
58
|
+
$.ui.toast('inject: usage is /inject <message>')
|
|
59
|
+
return {}
|
|
60
|
+
}
|
|
61
|
+
try {
|
|
62
|
+
await queue($, text)
|
|
63
|
+
$.ui.toast('inject: queued, delivers at the next step')
|
|
64
|
+
} catch (err) {
|
|
65
|
+
$.ui.toast('inject: could not queue the message (' + (err && err.message ? err.message : err) + ')')
|
|
66
|
+
}
|
|
67
|
+
return {}
|
|
68
|
+
})
|
|
69
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,36 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "inject-steer",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Type /inject <message> while Claude Code is working and it reaches the running agent at the next tool boundary, without stopping it.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"inject": "bin/inject.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin",
|
|
11
|
+
"src",
|
|
12
|
+
"mod",
|
|
13
|
+
".claude-plugin"
|
|
14
|
+
],
|
|
15
|
+
"scripts": {
|
|
16
|
+
"test": "node --test test/"
|
|
17
|
+
},
|
|
18
|
+
"engines": {
|
|
19
|
+
"node": ">=18"
|
|
20
|
+
},
|
|
21
|
+
"os": [
|
|
22
|
+
"darwin",
|
|
23
|
+
"linux"
|
|
24
|
+
],
|
|
25
|
+
"keywords": [
|
|
26
|
+
"claude-code",
|
|
27
|
+
"hooks",
|
|
28
|
+
"pty",
|
|
29
|
+
"agent",
|
|
30
|
+
"steering"
|
|
31
|
+
],
|
|
32
|
+
"license": "MIT",
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"node-pty": "^1.1.0"
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// Claude Code hook entry point: node hook.js <HookEventName>
|
|
2
|
+
//
|
|
3
|
+
// Registered for PreToolUse (urgent: deny the call, reason goes to Claude)
|
|
4
|
+
// and PostToolUse / PostToolUseFailure (normal: additionalContext).
|
|
5
|
+
// With the wrapper, messages come from its socket ($INJECT_DIR); without it,
|
|
6
|
+
// from the pending file written by the UserPromptSubmit hook (plain.js).
|
|
7
|
+
// See https://code.claude.com/docs/en/hooks
|
|
8
|
+
//
|
|
9
|
+
// Any failure exits 0 with no output, so a missing or crashed wrapper never
|
|
10
|
+
// gets in Claude's way.
|
|
11
|
+
|
|
12
|
+
import { request } from '../../core/ipc.js';
|
|
13
|
+
import { appendFix } from '../../core/log.js';
|
|
14
|
+
import { ENV_DIR, socketPath } from '../../core/session.js';
|
|
15
|
+
import { BOUNDARY, buildOutput, describeTool } from './output.js';
|
|
16
|
+
import { claim } from './plain.js';
|
|
17
|
+
|
|
18
|
+
async function readStdin() {
|
|
19
|
+
let data = '';
|
|
20
|
+
process.stdin.setEncoding('utf8');
|
|
21
|
+
for await (const chunk of process.stdin) data += chunk;
|
|
22
|
+
return data;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function main() {
|
|
26
|
+
const event = process.argv[2];
|
|
27
|
+
const dir = process.env[ENV_DIR];
|
|
28
|
+
if (!BOUNDARY[event]) return;
|
|
29
|
+
if (!dir && BOUNDARY[event] === 'before-tool') return; // urgent needs the wrapper
|
|
30
|
+
|
|
31
|
+
let input = {};
|
|
32
|
+
try {
|
|
33
|
+
input = JSON.parse(await readStdin());
|
|
34
|
+
} catch {
|
|
35
|
+
// Claim anyway; we just lose the "doing" detail.
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const boundary = BOUNDARY[event];
|
|
39
|
+
const tool = describeTool(input.tool_name, input.tool_input);
|
|
40
|
+
const doing = tool ? `${boundary === 'before-tool' ? 'about to run' : 'just ran'} ${tool}` : null;
|
|
41
|
+
|
|
42
|
+
if (!dir) return deliverPlain(event, input, doing);
|
|
43
|
+
|
|
44
|
+
const reply = await request(socketPath(dir), {
|
|
45
|
+
op: 'claim',
|
|
46
|
+
boundary,
|
|
47
|
+
agent: 'claude-code',
|
|
48
|
+
event,
|
|
49
|
+
doing,
|
|
50
|
+
projectDir: process.env.CLAUDE_PROJECT_DIR || input.cwd || null,
|
|
51
|
+
sessionId: input.session_id || null,
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
const out = buildOutput(event, reply.messages || []);
|
|
55
|
+
if (out) process.stdout.write(JSON.stringify(out));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Plain `claude` (no wrapper): messages queued by the UserPromptSubmit hook.
|
|
59
|
+
function deliverPlain(event, input, doing) {
|
|
60
|
+
const messages = claim(input.session_id);
|
|
61
|
+
if (!messages.length) return;
|
|
62
|
+
const deliveredAt = new Date().toISOString();
|
|
63
|
+
for (const m of messages) {
|
|
64
|
+
appendFix(m.projectDir || process.env.CLAUDE_PROJECT_DIR || input.cwd, m, {
|
|
65
|
+
agent: 'claude-code',
|
|
66
|
+
deliveredAt,
|
|
67
|
+
event,
|
|
68
|
+
doing,
|
|
69
|
+
sessionId: input.session_id,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
process.stdout.write(JSON.stringify(buildOutput(event, messages)));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
main().then(
|
|
76
|
+
() => process.exit(0),
|
|
77
|
+
() => process.exit(0),
|
|
78
|
+
);
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { install, installMod, isInstalled, modInstalled, uninstall, uninstallMod } from './install.js';
|
|
2
|
+
|
|
3
|
+
/** @type {import('../types.js').Adapter} */
|
|
4
|
+
export default {
|
|
5
|
+
id: 'claude-code',
|
|
6
|
+
name: 'Claude Code',
|
|
7
|
+
command: 'claude',
|
|
8
|
+
install,
|
|
9
|
+
uninstall,
|
|
10
|
+
isInstalled,
|
|
11
|
+
installMod,
|
|
12
|
+
uninstallMod,
|
|
13
|
+
modInstalled,
|
|
14
|
+
};
|