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
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
// Runs the agent under a pseudo-terminal, forwarding everything unchanged
|
|
2
|
+
// except "/inject ..." lines, which go to the queue instead.
|
|
3
|
+
|
|
4
|
+
import { spawnSync } from 'node:child_process';
|
|
5
|
+
import { Queue } from '../core/queue.js';
|
|
6
|
+
import { listen } from '../core/ipc.js';
|
|
7
|
+
import { appendFix } from '../core/log.js';
|
|
8
|
+
import { ENV_DIR, ENV_NODE, createSessionDir, removeSessionDir, socketPath } from '../core/session.js';
|
|
9
|
+
import { Interceptor } from './intercept.js';
|
|
10
|
+
import { Overlay, deliveredToast, queuedToast, usageToast } from './overlay.js';
|
|
11
|
+
import { loadPty } from './pty.js';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @param {import('../adapters/types.js').Adapter} adapter
|
|
15
|
+
* @param {string[]} args passed to the agent unchanged
|
|
16
|
+
*/
|
|
17
|
+
export async function run(adapter, args) {
|
|
18
|
+
const command = process.env.INJECT_COMMAND || adapter.command;
|
|
19
|
+
|
|
20
|
+
// Piped or scripted use (e.g. `inject -p "..."`): nothing to intercept.
|
|
21
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
22
|
+
const r = spawnSync(command, args, { stdio: 'inherit' });
|
|
23
|
+
if (r.error) fail(`could not start ${command}: ${r.error.message}`);
|
|
24
|
+
process.exit(r.status ?? 1);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
let pty;
|
|
28
|
+
try {
|
|
29
|
+
pty = loadPty();
|
|
30
|
+
} catch (err) {
|
|
31
|
+
fail(err.message);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const dir = createSessionDir();
|
|
35
|
+
const queue = new Queue(dir);
|
|
36
|
+
const overlay = new Overlay(process.stdout);
|
|
37
|
+
const cwd = process.cwd();
|
|
38
|
+
|
|
39
|
+
const server = await listen(socketPath(dir), (req) => {
|
|
40
|
+
if (req.op !== 'claim') return { messages: [] };
|
|
41
|
+
const messages = queue.claim(req.boundary);
|
|
42
|
+
if (messages.length) {
|
|
43
|
+
const deliveredAt = new Date().toISOString();
|
|
44
|
+
for (const m of messages) {
|
|
45
|
+
appendFix(req.projectDir || cwd, m, {
|
|
46
|
+
agent: req.agent,
|
|
47
|
+
deliveredAt,
|
|
48
|
+
event: req.event,
|
|
49
|
+
doing: req.doing,
|
|
50
|
+
sessionId: req.sessionId,
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
const urgent = messages.some((m) => m.urgency === 'urgent');
|
|
54
|
+
overlay.setPending(queue.size);
|
|
55
|
+
overlay.toast(deliveredToast(messages.length, urgent, req.doing));
|
|
56
|
+
}
|
|
57
|
+
return { messages };
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
let child;
|
|
61
|
+
try {
|
|
62
|
+
child = pty.spawn(command, args, {
|
|
63
|
+
name: process.env.TERM || 'xterm-256color',
|
|
64
|
+
cols: process.stdout.columns || 80,
|
|
65
|
+
rows: process.stdout.rows || 24,
|
|
66
|
+
cwd,
|
|
67
|
+
env: { ...process.env, [ENV_DIR]: dir, [ENV_NODE]: process.execPath },
|
|
68
|
+
});
|
|
69
|
+
} catch (err) {
|
|
70
|
+
server.close();
|
|
71
|
+
removeSessionDir(dir);
|
|
72
|
+
fail(`could not start ${command}: ${err.message}. Is it installed and on your PATH?`);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const interceptor = new Interceptor();
|
|
76
|
+
process.stdin.setRawMode(true);
|
|
77
|
+
process.stdin.setEncoding('utf8');
|
|
78
|
+
process.stdin.resume();
|
|
79
|
+
|
|
80
|
+
process.stdin.on('data', (data) => {
|
|
81
|
+
const { forward, events } = interceptor.feed(data);
|
|
82
|
+
if (forward) child.write(forward);
|
|
83
|
+
for (const ev of events) {
|
|
84
|
+
if (ev.type === 'capture') overlay.editing(ev.urgency, ev.text);
|
|
85
|
+
else if (ev.type === 'cancel') overlay.stopEditing();
|
|
86
|
+
else if (ev.type === 'usage') overlay.toast(usageToast());
|
|
87
|
+
else if (ev.type === 'submit') {
|
|
88
|
+
queue.add(ev.text, ev.urgency);
|
|
89
|
+
overlay.stopEditing();
|
|
90
|
+
overlay.pending = queue.size;
|
|
91
|
+
overlay.toast(queuedToast(ev.urgency));
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
child.onData((data) => {
|
|
97
|
+
process.stdout.write(data);
|
|
98
|
+
overlay.afterOutput(data);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
process.stdout.on('resize', () => {
|
|
102
|
+
child.resize(process.stdout.columns, process.stdout.rows);
|
|
103
|
+
overlay.draw();
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
for (const sig of ['SIGTERM', 'SIGHUP']) {
|
|
107
|
+
process.on(sig, () => child.kill(sig));
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
child.onExit(({ exitCode, signal }) => {
|
|
111
|
+
process.stdin.setRawMode(false);
|
|
112
|
+
process.stdin.pause();
|
|
113
|
+
server.close();
|
|
114
|
+
const leftover = queue.claim('before-tool').concat(queue.claim('after-tool'));
|
|
115
|
+
for (const m of leftover) appendFix(cwd, m, { agent: adapter.id });
|
|
116
|
+
removeSessionDir(dir);
|
|
117
|
+
if (leftover.length) {
|
|
118
|
+
process.stderr.write(
|
|
119
|
+
`\ninject: ${leftover.length} message(s) were not delivered (logged to .steer/fixes.jsonl):\n` +
|
|
120
|
+
leftover.map((m) => ` - ${m.urgency === 'urgent' ? '[urgent] ' : ''}${m.text}\n`).join(''),
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
process.exit(signal ? 128 + signal : exitCode);
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function fail(msg) {
|
|
128
|
+
process.stderr.write(`inject: ${msg}\n`);
|
|
129
|
+
process.exit(1);
|
|
130
|
+
}
|