@procedures/claude-cli 0.0.0-stage → 0.4.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/CHANGELOG.md +30 -0
- package/LICENSE +21 -0
- package/README.md +92 -2
- package/dist/.tsbuildinfo +1 -0
- package/dist/events.d.ts +93 -0
- package/dist/events.d.ts.map +1 -0
- package/dist/events.js +153 -0
- package/dist/events.js.map +1 -0
- package/dist/executable.d.ts +28 -0
- package/dist/executable.d.ts.map +1 -0
- package/dist/executable.js +37 -0
- package/dist/executable.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -0
- package/dist/module.d.ts +19 -0
- package/dist/module.d.ts.map +1 -0
- package/dist/module.js +81 -0
- package/dist/module.js.map +1 -0
- package/dist/procedures/claude/cancel.d.ts +13 -0
- package/dist/procedures/claude/cancel.d.ts.map +1 -0
- package/dist/procedures/claude/cancel.js +15 -0
- package/dist/procedures/claude/cancel.js.map +1 -0
- package/dist/procedures/claude/follow.d.ts +15 -0
- package/dist/procedures/claude/follow.d.ts.map +1 -0
- package/dist/procedures/claude/follow.js +21 -0
- package/dist/procedures/claude/follow.js.map +1 -0
- package/dist/procedures/claude/index.d.ts +5 -0
- package/dist/procedures/claude/index.d.ts.map +1 -0
- package/dist/procedures/claude/index.js +5 -0
- package/dist/procedures/claude/index.js.map +1 -0
- package/dist/procedures/claude/read.d.ts +12 -0
- package/dist/procedures/claude/read.d.ts.map +1 -0
- package/dist/procedures/claude/read.js +14 -0
- package/dist/procedures/claude/read.js.map +1 -0
- package/dist/procedures/claude/start.d.ts +14 -0
- package/dist/procedures/claude/start.d.ts.map +1 -0
- package/dist/procedures/claude/start.js +26 -0
- package/dist/procedures/claude/start.js.map +1 -0
- package/dist/register.d.ts +6 -0
- package/dist/register.d.ts.map +1 -0
- package/dist/register.js +8 -0
- package/dist/register.js.map +1 -0
- package/dist/turn.d.ts +142 -0
- package/dist/turn.d.ts.map +1 -0
- package/dist/turn.js +208 -0
- package/dist/turn.js.map +1 -0
- package/dist/types.d.ts +61 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +61 -0
- package/dist/types.js.map +1 -0
- package/package.json +78 -3
- package/src/events.ts +250 -0
- package/src/executable.ts +50 -0
- package/src/index.ts +7 -0
- package/src/module.ts +116 -0
- package/src/procedures/claude/cancel.ts +17 -0
- package/src/procedures/claude/follow.ts +26 -0
- package/src/procedures/claude/index.ts +4 -0
- package/src/procedures/claude/read.ts +16 -0
- package/src/procedures/claude/start.ts +28 -0
- package/src/register.ts +10 -0
- package/src/turn.ts +317 -0
- package/src/types.ts +142 -0
package/src/turn.ts
ADDED
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One turn of the Claude Code CLI as a process that can outlive its host.
|
|
3
|
+
*
|
|
4
|
+
* - `startTurn` starts `claude -p --output-format stream-json --verbose` as a detached process.
|
|
5
|
+
* The prompt comes from a file, and the output goes to a file. The host holds no pipe to the
|
|
6
|
+
* process, so a restart of the host does not stop the turn.
|
|
7
|
+
* - `readTurn` and `followTurn` read the output file from a byte offset, and give normalised
|
|
8
|
+
* events. A new host continues from the last offset.
|
|
9
|
+
* - `cancelTurn` stops the process tree of a turn by its process id.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { spawn, type SpawnOptions } from "node:child_process";
|
|
13
|
+
import { closeSync, mkdirSync, mkdtempSync, openSync, readSync, statSync, writeFileSync } from "node:fs";
|
|
14
|
+
import { tmpdir } from "node:os";
|
|
15
|
+
import { dirname, join, resolve } from "node:path";
|
|
16
|
+
import { setTimeout as sleep } from "node:timers/promises";
|
|
17
|
+
import { runCommand, type CommandOptions, type CommandResult } from "@procedures/shell/command";
|
|
18
|
+
import { normalizeLine, type ClaudeEvent } from "./events.js";
|
|
19
|
+
import { CLAUDE_ENV, findClaude } from "./executable.js";
|
|
20
|
+
|
|
21
|
+
/** The flags that make the CLI run one turn and write stream-json. */
|
|
22
|
+
export const CLAUDE_FLAGS: readonly string[] = ["-p", "--output-format", "stream-json", "--verbose"];
|
|
23
|
+
|
|
24
|
+
/** The part of a child process that `startTurn` uses. */
|
|
25
|
+
export interface SpawnedProcess {
|
|
26
|
+
readonly pid?: number | undefined;
|
|
27
|
+
once(event: "spawn" | "error", listener: (error?: Error) => void): unknown;
|
|
28
|
+
unref(): void;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** The function that starts a process. The default is `spawn` of `node:child_process`. */
|
|
32
|
+
export type Spawner = (command: string, args: readonly string[], options: SpawnOptions) => SpawnedProcess;
|
|
33
|
+
|
|
34
|
+
const defaultSpawner: Spawner = (command, args, options) => spawn(command, [...args], options);
|
|
35
|
+
|
|
36
|
+
/** The options of `startTurn`. */
|
|
37
|
+
export interface StartTurnOptions {
|
|
38
|
+
/** The prompt. The process reads it from a file on its stdin. */
|
|
39
|
+
readonly prompt: string;
|
|
40
|
+
/** The working folder of the turn (default: the current folder). */
|
|
41
|
+
readonly cwd?: string | undefined;
|
|
42
|
+
/** More arguments after the default flags, for example `--resume <session>` or `--model <name>`. */
|
|
43
|
+
readonly args?: readonly string[] | undefined;
|
|
44
|
+
/** The file that gets the output. The process appends to it. The default is a file in a new temporary folder. */
|
|
45
|
+
readonly outputFile?: string | undefined;
|
|
46
|
+
/** The file that holds the prompt (default: `<outputFile>.prompt.txt`). */
|
|
47
|
+
readonly promptFile?: string | undefined;
|
|
48
|
+
/** The `claude` program (default: `CLAUDE_BIN` of the host, then a search). */
|
|
49
|
+
readonly executable?: string | undefined;
|
|
50
|
+
/** The arguments before the default flags, for example the path of `cli.js` when `executable` is `node`. */
|
|
51
|
+
readonly executableArgs?: readonly string[] | undefined;
|
|
52
|
+
/** The full environment of the process (default: `process.env`). The caller removes the secrets. */
|
|
53
|
+
readonly env?: Readonly<Record<string, string | undefined>> | undefined;
|
|
54
|
+
/** The function that starts the process (default: `spawn`). The tests give their own. */
|
|
55
|
+
readonly spawner?: Spawner | undefined;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** A turn that runs. Keep `pid`, `outputFile` and `offset` to read or stop the turn later. */
|
|
59
|
+
export interface StartedTurn {
|
|
60
|
+
readonly pid: number;
|
|
61
|
+
/** The absolute path of the output file. */
|
|
62
|
+
readonly outputFile: string;
|
|
63
|
+
/** The absolute path of the prompt file. */
|
|
64
|
+
readonly promptFile: string;
|
|
65
|
+
/** The byte offset in the output file where the output of this turn starts. */
|
|
66
|
+
readonly offset: number;
|
|
67
|
+
/** The program and its arguments. */
|
|
68
|
+
readonly command: string;
|
|
69
|
+
readonly args: readonly string[];
|
|
70
|
+
/** The start time, as an ISO 8601 text. */
|
|
71
|
+
readonly startedAt: string;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** True when the program is a batch file of Windows, which does not start without a shell. */
|
|
75
|
+
function isBatchFile(program: string): boolean {
|
|
76
|
+
return /\.(cmd|bat)$/i.test(program);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function sizeOf(path: string): number {
|
|
80
|
+
try {
|
|
81
|
+
return statSync(path).size;
|
|
82
|
+
} catch {
|
|
83
|
+
return 0;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* This function starts one turn as a detached process: stdin is the prompt file, stdout and
|
|
89
|
+
* stderr go to the output file. The process starts with argv, not through a shell.
|
|
90
|
+
*/
|
|
91
|
+
export async function startTurn(options: StartTurnOptions): Promise<StartedTurn> {
|
|
92
|
+
// The search reads the environment of the host, not the environment of the turn
|
|
93
|
+
const command = findClaude({ executable: options.executable });
|
|
94
|
+
if (command === undefined) throw new Error(`The claude program was not found. Install Claude Code, or set ${CLAUDE_ENV} to the program.`);
|
|
95
|
+
if (process.platform === "win32" && isBatchFile(command)) {
|
|
96
|
+
throw new Error(`${command} is a batch file, which does not start without a shell. Give the native claude.exe, or node and the path of cli.js.`);
|
|
97
|
+
}
|
|
98
|
+
const outputFile = resolve(options.outputFile ?? join(mkdtempSync(join(tmpdir(), "claude-turn-")), "output.ndjson"));
|
|
99
|
+
const promptFile = resolve(options.promptFile ?? `${outputFile}.prompt.txt`);
|
|
100
|
+
mkdirSync(dirname(outputFile), { recursive: true });
|
|
101
|
+
mkdirSync(dirname(promptFile), { recursive: true });
|
|
102
|
+
writeFileSync(promptFile, options.prompt, "utf8");
|
|
103
|
+
const offset = sizeOf(outputFile);
|
|
104
|
+
const args = [...(options.executableArgs ?? []), ...CLAUDE_FLAGS, ...(options.args ?? [])];
|
|
105
|
+
|
|
106
|
+
const input = openSync(promptFile, "r");
|
|
107
|
+
const output = openSync(outputFile, "a");
|
|
108
|
+
try {
|
|
109
|
+
const child = (options.spawner ?? defaultSpawner)(command, args, {
|
|
110
|
+
cwd: options.cwd,
|
|
111
|
+
env: { ...(options.env ?? process.env) },
|
|
112
|
+
// Its own process group on POSIX (the cancel signals the group), and no console window
|
|
113
|
+
detached: true,
|
|
114
|
+
windowsHide: true,
|
|
115
|
+
shell: false,
|
|
116
|
+
stdio: [input, output, output],
|
|
117
|
+
});
|
|
118
|
+
await new Promise<void>((resolveStart, rejectStart) => {
|
|
119
|
+
child.once("spawn", () => resolveStart());
|
|
120
|
+
child.once("error", (error) => rejectStart(new Error(`claude did not start: ${error?.message ?? "unknown error"}`)));
|
|
121
|
+
});
|
|
122
|
+
if (child.pid === undefined) throw new Error("claude did not start: no process id");
|
|
123
|
+
child.unref();
|
|
124
|
+
return { pid: child.pid, outputFile, promptFile, offset, command, args, startedAt: new Date().toISOString() };
|
|
125
|
+
} finally {
|
|
126
|
+
// The process has its own copies of the two files
|
|
127
|
+
closeSync(input);
|
|
128
|
+
closeSync(output);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** This function tells if a process with this id runs. */
|
|
133
|
+
export function isProcessAlive(pid: number): boolean {
|
|
134
|
+
try {
|
|
135
|
+
process.kill(pid, 0);
|
|
136
|
+
return true;
|
|
137
|
+
} catch (error) {
|
|
138
|
+
// EPERM: the process runs, but it belongs to another user
|
|
139
|
+
return (error as NodeJS.ErrnoException).code === "EPERM";
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** One event of a turn, with the byte offset after its line. Give that offset to continue. */
|
|
144
|
+
export interface TurnEvent {
|
|
145
|
+
readonly offset: number;
|
|
146
|
+
readonly event: ClaudeEvent;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** The options of `readTurn`. */
|
|
150
|
+
export interface ReadTurnOptions {
|
|
151
|
+
/** The output file of the turn. */
|
|
152
|
+
readonly outputFile: string;
|
|
153
|
+
/** The byte offset to start from (default: 0). */
|
|
154
|
+
readonly offset?: number | undefined;
|
|
155
|
+
/** The process id of the turn. Without it, the last line without a line end stays unread. */
|
|
156
|
+
readonly pid?: number | undefined;
|
|
157
|
+
/** The function that tells if a process runs (default: `isProcessAlive`). */
|
|
158
|
+
readonly isAlive?: ((pid: number) => boolean) | undefined;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** The result of `readTurn`. */
|
|
162
|
+
export interface ReadTurnResult {
|
|
163
|
+
readonly events: TurnEvent[];
|
|
164
|
+
/** The byte offset after the last line that was read. Give it to the next read. */
|
|
165
|
+
readonly offset: number;
|
|
166
|
+
/** True when the events include the `result` of the turn. */
|
|
167
|
+
readonly finished: boolean;
|
|
168
|
+
/** True when the process runs. It is absent without `pid`. */
|
|
169
|
+
readonly running?: boolean;
|
|
170
|
+
/** The number of new bytes in the file, read or not. */
|
|
171
|
+
readonly newBytes: number;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function readBytes(path: string, start: number, end: number): Buffer {
|
|
175
|
+
const fd = openSync(path, "r");
|
|
176
|
+
try {
|
|
177
|
+
const buffer = Buffer.alloc(end - start);
|
|
178
|
+
let done = 0;
|
|
179
|
+
while (done < buffer.length) {
|
|
180
|
+
const read = readSync(fd, buffer, done, buffer.length - done, start + done);
|
|
181
|
+
if (read === 0) break;
|
|
182
|
+
done += read;
|
|
183
|
+
}
|
|
184
|
+
return buffer.subarray(0, done);
|
|
185
|
+
} finally {
|
|
186
|
+
closeSync(fd);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* This function reads the complete lines of the output file after `offset`, and normalises them.
|
|
192
|
+
* When the process has ended, it also reads a last line without a line end.
|
|
193
|
+
*/
|
|
194
|
+
export function readTurn(options: ReadTurnOptions): ReadTurnResult {
|
|
195
|
+
const start = options.offset ?? 0;
|
|
196
|
+
// The check comes before the read: when the process has ended, the file is complete
|
|
197
|
+
const running = options.pid === undefined ? undefined : (options.isAlive ?? isProcessAlive)(options.pid);
|
|
198
|
+
const status = running === undefined ? {} : { running };
|
|
199
|
+
const size = sizeOf(options.outputFile);
|
|
200
|
+
if (size <= start) return { events: [], offset: start, finished: false, newBytes: 0, ...status };
|
|
201
|
+
|
|
202
|
+
const bytes = readBytes(options.outputFile, start, size);
|
|
203
|
+
const events: TurnEvent[] = [];
|
|
204
|
+
let position = 0;
|
|
205
|
+
let finished = false;
|
|
206
|
+
while (position < bytes.length) {
|
|
207
|
+
let end = bytes.indexOf(0x0a, position);
|
|
208
|
+
if (end === -1) {
|
|
209
|
+
if (running !== false) break;
|
|
210
|
+
end = bytes.length;
|
|
211
|
+
}
|
|
212
|
+
const line = bytes.subarray(position, end).toString("utf8");
|
|
213
|
+
position = Math.min(end + 1, bytes.length);
|
|
214
|
+
for (const event of normalizeLine(line)) {
|
|
215
|
+
events.push({ offset: start + position, event });
|
|
216
|
+
if (event.type === "result") finished = true;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
return { events, offset: start + position, finished, newBytes: bytes.length, ...status };
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/** The options of `followTurn`. */
|
|
223
|
+
export interface FollowTurnOptions extends ReadTurnOptions {
|
|
224
|
+
/** The time between two reads, in milliseconds (default: 250). */
|
|
225
|
+
readonly pollMs?: number | undefined;
|
|
226
|
+
/** The reads without new bytes after the end of the process, before the follow stops (default: 2). */
|
|
227
|
+
readonly quietReads?: number | undefined;
|
|
228
|
+
/** When the signal aborts, the follow stops. The process continues. */
|
|
229
|
+
readonly signal?: AbortSignal | undefined;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* This function follows the output file and gives each event while the turn runs. It stops after
|
|
234
|
+
* the `result` event. When the process ends without a result, it gives an `error` event and stops.
|
|
235
|
+
* Without `pid`, only the result or the signal stops it.
|
|
236
|
+
*/
|
|
237
|
+
export async function* followTurn(options: FollowTurnOptions): AsyncGenerator<TurnEvent, void, undefined> {
|
|
238
|
+
const pollMs = options.pollMs ?? 250;
|
|
239
|
+
const quietLimit = options.quietReads ?? 2;
|
|
240
|
+
let offset = options.offset ?? 0;
|
|
241
|
+
let quiet = 0;
|
|
242
|
+
while (options.signal?.aborted !== true) {
|
|
243
|
+
const read = readTurn({ ...options, offset });
|
|
244
|
+
for (const item of read.events) {
|
|
245
|
+
yield item;
|
|
246
|
+
if (item.event.type === "result") return;
|
|
247
|
+
}
|
|
248
|
+
offset = read.offset;
|
|
249
|
+
if (read.running === false) {
|
|
250
|
+
quiet = read.newBytes > 0 ? 0 : quiet + 1;
|
|
251
|
+
if (quiet >= quietLimit) {
|
|
252
|
+
yield { offset, event: { type: "error", message: "The process ended without a result" } };
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
try {
|
|
257
|
+
await sleep(pollMs, undefined, options.signal ? { signal: options.signal } : undefined);
|
|
258
|
+
} catch {
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/** The function that runs a program. The default is `runCommand` of `@procedures/shell`. */
|
|
265
|
+
export type CommandRunner = (command: string, options: CommandOptions) => Promise<CommandResult>;
|
|
266
|
+
|
|
267
|
+
/** The options of `cancelTurn`. Each one has a default from the host. */
|
|
268
|
+
export interface CancelTurnOptions {
|
|
269
|
+
/** The operating system (default: `process.platform`). */
|
|
270
|
+
readonly platform?: NodeJS.Platform | undefined;
|
|
271
|
+
/** The function that runs `taskkill` on Windows (default: `runCommand`). */
|
|
272
|
+
readonly runner?: CommandRunner | undefined;
|
|
273
|
+
/** The function that sends a signal on POSIX (default: `process.kill`). */
|
|
274
|
+
readonly kill?: ((pid: number, signal: NodeJS.Signals) => void) | undefined;
|
|
275
|
+
/** The function that tells if a process runs (default: `isProcessAlive`). */
|
|
276
|
+
readonly isAlive?: ((pid: number) => boolean) | undefined;
|
|
277
|
+
/** The signal on POSIX (default: `SIGTERM`). */
|
|
278
|
+
readonly signal?: NodeJS.Signals | undefined;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/** The result of `cancelTurn`. */
|
|
282
|
+
export interface CancelTurnResult {
|
|
283
|
+
/** True when the stop reached the process. */
|
|
284
|
+
readonly stopped: boolean;
|
|
285
|
+
/** Why the stop did not occur, when `stopped` is false. */
|
|
286
|
+
readonly reason?: string;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* This function stops the process tree of a turn. On Windows it starts `taskkill /T /F /PID`.
|
|
291
|
+
* On POSIX it signals the process group, which `startTurn` made. It refuses the id of the host.
|
|
292
|
+
*/
|
|
293
|
+
export async function cancelTurn(pid: number, options: CancelTurnOptions = {}): Promise<CancelTurnResult> {
|
|
294
|
+
if (!Number.isSafeInteger(pid) || pid <= 1) throw new Error(`cancelTurn refuses the process id ${pid}`);
|
|
295
|
+
if (pid === process.pid || pid === process.ppid) throw new Error("cancelTurn refuses the process of the host");
|
|
296
|
+
if (!(options.isAlive ?? isProcessAlive)(pid)) return { stopped: false, reason: "The process has ended" };
|
|
297
|
+
|
|
298
|
+
if ((options.platform ?? process.platform) === "win32") {
|
|
299
|
+
const result = await (options.runner ?? runCommand)("taskkill", { args: ["/T", "/F", "/PID", String(pid)], timeout: 30_000 });
|
|
300
|
+
if (result.exitCode === 0) return { stopped: true };
|
|
301
|
+
return { stopped: false, reason: (result.error ?? result.stderr.trim()) || `taskkill gave the exit code ${result.exitCode}` };
|
|
302
|
+
}
|
|
303
|
+
const kill = options.kill ?? ((target: number, signal: NodeJS.Signals): void => void process.kill(target, signal));
|
|
304
|
+
const signal = options.signal ?? "SIGTERM";
|
|
305
|
+
try {
|
|
306
|
+
// The negative id signals the process group: the turn and the programs that it started
|
|
307
|
+
kill(-pid, signal);
|
|
308
|
+
return { stopped: true };
|
|
309
|
+
} catch {
|
|
310
|
+
try {
|
|
311
|
+
kill(pid, signal);
|
|
312
|
+
return { stopped: true };
|
|
313
|
+
} catch (error) {
|
|
314
|
+
return { stopped: false, reason: error instanceof Error ? error.message : String(error) };
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The input and the output of the procedures of this package.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import type { TurnEvent } from "./turn.js";
|
|
7
|
+
|
|
8
|
+
/** The schema of one event. It keeps each field of the event. */
|
|
9
|
+
const ClaudeEventSchema = z.looseObject({
|
|
10
|
+
type: z.string().describe("meta, text, text-delta, thinking, tool-use, tool-result, result, log or error"),
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
/** The schema of one event of a turn, with the byte offset after its line. */
|
|
14
|
+
export const TurnEventSchema: z.ZodType<TurnEvent> = z.object({
|
|
15
|
+
offset: z.number().describe("The byte offset after the line of the event"),
|
|
16
|
+
event: ClaudeEventSchema,
|
|
17
|
+
}) as unknown as z.ZodType<TurnEvent>;
|
|
18
|
+
|
|
19
|
+
const pidSchema = z.number().int().min(2).describe("The process id of the turn");
|
|
20
|
+
const offsetSchema = z.number().int().min(0).describe("The byte offset to start from (default: 0)");
|
|
21
|
+
|
|
22
|
+
// =============================================================================
|
|
23
|
+
// claude.start: start one turn as a detached process that writes to a file
|
|
24
|
+
// =============================================================================
|
|
25
|
+
|
|
26
|
+
export interface ClaudeStartInput {
|
|
27
|
+
prompt: string;
|
|
28
|
+
cwd?: string | undefined;
|
|
29
|
+
args?: string[] | undefined;
|
|
30
|
+
outputFile?: string | undefined;
|
|
31
|
+
promptFile?: string | undefined;
|
|
32
|
+
executable?: string | undefined;
|
|
33
|
+
executableArgs?: string[] | undefined;
|
|
34
|
+
env?: Record<string, string> | undefined;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export const ClaudeStartInputSchema: z.ZodType<ClaudeStartInput> = z.object({
|
|
38
|
+
prompt: z.string().min(1).describe("The prompt of the turn"),
|
|
39
|
+
cwd: z.string().min(1).optional().describe("The working folder of the turn"),
|
|
40
|
+
args: z.array(z.string()).optional().describe('More arguments, for example ["--resume", "<session>"] or ["--model", "<name>"]'),
|
|
41
|
+
outputFile: z.string().min(1).optional().describe("The file that gets the output (default: a file in a new temporary folder)"),
|
|
42
|
+
promptFile: z.string().min(1).optional().describe("The file that holds the prompt (default: <outputFile>.prompt.txt)"),
|
|
43
|
+
executable: z.string().min(1).optional().describe("The claude program (default: CLAUDE_BIN, then a search)"),
|
|
44
|
+
executableArgs: z.array(z.string()).optional().describe("The arguments before the flags, for example the path of cli.js"),
|
|
45
|
+
env: z.record(z.string(), z.string()).optional().describe("Environment variables to add to the environment of the host"),
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
export interface ClaudeStartOutput {
|
|
49
|
+
pid: number;
|
|
50
|
+
outputFile: string;
|
|
51
|
+
promptFile: string;
|
|
52
|
+
offset: number;
|
|
53
|
+
command: string;
|
|
54
|
+
args: readonly string[];
|
|
55
|
+
startedAt: string;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export const ClaudeStartOutputSchema: z.ZodType<ClaudeStartOutput> = z.object({
|
|
59
|
+
pid: z.number().describe("The process id of the turn"),
|
|
60
|
+
outputFile: z.string().describe("The absolute path of the output file"),
|
|
61
|
+
promptFile: z.string().describe("The absolute path of the prompt file"),
|
|
62
|
+
offset: z.number().describe("The byte offset where the output of this turn starts"),
|
|
63
|
+
command: z.string().describe("The program"),
|
|
64
|
+
args: z.array(z.string()).describe("The arguments of the program"),
|
|
65
|
+
startedAt: z.string().describe("The start time (ISO 8601)"),
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// =============================================================================
|
|
69
|
+
// claude.read: read the new events of a turn from its output file
|
|
70
|
+
// =============================================================================
|
|
71
|
+
|
|
72
|
+
export interface ClaudeReadInput {
|
|
73
|
+
outputFile: string;
|
|
74
|
+
offset?: number | undefined;
|
|
75
|
+
pid?: number | undefined;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export const ClaudeReadInputSchema: z.ZodType<ClaudeReadInput> = z.object({
|
|
79
|
+
outputFile: z.string().min(1).describe("The output file of the turn"),
|
|
80
|
+
offset: offsetSchema.optional(),
|
|
81
|
+
pid: pidSchema.optional(),
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
export interface ClaudeReadOutput {
|
|
85
|
+
events: TurnEvent[];
|
|
86
|
+
offset: number;
|
|
87
|
+
finished: boolean;
|
|
88
|
+
running?: boolean | undefined;
|
|
89
|
+
newBytes: number;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export const ClaudeReadOutputSchema: z.ZodType<ClaudeReadOutput> = z.object({
|
|
93
|
+
events: z.array(TurnEventSchema).describe("The new events"),
|
|
94
|
+
offset: z.number().describe("The byte offset for the next read"),
|
|
95
|
+
finished: z.boolean().describe("True when the events include the result of the turn"),
|
|
96
|
+
running: z.boolean().optional().describe("True when the process runs (only with pid)"),
|
|
97
|
+
newBytes: z.number().describe("The number of new bytes in the file"),
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
// =============================================================================
|
|
101
|
+
// claude.follow: stream the events of a turn until the turn ends
|
|
102
|
+
// =============================================================================
|
|
103
|
+
|
|
104
|
+
export interface ClaudeFollowInput {
|
|
105
|
+
outputFile: string;
|
|
106
|
+
offset?: number | undefined;
|
|
107
|
+
pid?: number | undefined;
|
|
108
|
+
pollMs?: number | undefined;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export const ClaudeFollowInputSchema: z.ZodType<ClaudeFollowInput> = z.object({
|
|
112
|
+
outputFile: z.string().min(1).describe("The output file of the turn"),
|
|
113
|
+
offset: offsetSchema.optional(),
|
|
114
|
+
pid: pidSchema.optional().describe("The process id of the turn. Without it, only the result stops the follow."),
|
|
115
|
+
pollMs: z.number().int().min(10).max(60_000).optional().describe("The time between two reads, in milliseconds (default: 250)"),
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
export type ClaudeFollowOutput = TurnEvent;
|
|
119
|
+
|
|
120
|
+
export const ClaudeFollowOutputSchema: z.ZodType<ClaudeFollowOutput> = TurnEventSchema;
|
|
121
|
+
|
|
122
|
+
// =============================================================================
|
|
123
|
+
// claude.cancel: stop a turn and the processes that it started
|
|
124
|
+
// =============================================================================
|
|
125
|
+
|
|
126
|
+
export interface ClaudeCancelInput {
|
|
127
|
+
pid: number;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export const ClaudeCancelInputSchema: z.ZodType<ClaudeCancelInput> = z.object({
|
|
131
|
+
pid: pidSchema,
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
export interface ClaudeCancelOutput {
|
|
135
|
+
stopped: boolean;
|
|
136
|
+
reason?: string | undefined;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export const ClaudeCancelOutputSchema: z.ZodType<ClaudeCancelOutput> = z.object({
|
|
140
|
+
stopped: z.boolean().describe("True when the stop reached the process"),
|
|
141
|
+
reason: z.string().optional().describe("Why the stop did not occur"),
|
|
142
|
+
});
|