@pivanov/claude-wire 0.0.2 → 0.0.4
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 +5 -4
- package/dist/async.d.ts +10 -0
- package/dist/async.js +27 -0
- package/dist/client.js +10 -6
- package/dist/constants.d.ts +4 -0
- package/dist/constants.js +10 -0
- package/dist/cost.d.ts +2 -0
- package/dist/cost.js +6 -3
- package/dist/errors.d.ts +3 -4
- package/dist/errors.js +18 -9
- package/dist/index.d.ts +6 -4
- package/dist/index.js +4 -4
- package/dist/parser/content.js +3 -2
- package/dist/parser/translator.js +10 -3
- package/dist/pipeline.d.ts +14 -4
- package/dist/pipeline.js +38 -18
- package/dist/process.d.ts +16 -2
- package/dist/process.js +115 -73
- package/dist/reader.d.ts +10 -1
- package/dist/reader.js +83 -35
- package/dist/runtime.d.ts +5 -4
- package/dist/runtime.js +21 -46
- package/dist/session.d.ts +30 -2
- package/dist/session.js +171 -99
- package/dist/stream.js +78 -52
- package/dist/tools/handler.d.ts +1 -0
- package/dist/tools/handler.js +12 -4
- package/dist/tools/registry.d.ts +4 -2
- package/dist/tools/registry.js +7 -4
- package/dist/types/options.d.ts +53 -5
- package/dist/types/protocol.d.ts +1 -5
- package/dist/validation.d.ts +10 -0
- package/dist/validation.js +23 -0
- package/dist/warnings.d.ts +2 -0
- package/dist/warnings.js +24 -0
- package/dist/writer.d.ts +10 -1
- package/dist/writer.js +14 -8
- package/package.json +1 -1
package/dist/process.js
CHANGED
|
@@ -2,25 +2,55 @@ import { readFileSync } from "node:fs";
|
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { BINARY } from "./constants.js";
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
5
|
+
import { errorMessage, KnownError, ProcessError } from "./errors.js";
|
|
6
|
+
import { isExecutableNonEmpty, spawnProcess, whichSync } from "./runtime.js";
|
|
7
|
+
import { assertPositiveNumber } from "./validation.js";
|
|
7
8
|
import { writer } from "./writer.js";
|
|
9
|
+
// Swallow ESRCH/EPIPE-style throws from kill()/write() when the child is
|
|
10
|
+
// already gone. Every call site had the same try/catch -- keeping it in one
|
|
11
|
+
// place stops future adders from forgetting the guard.
|
|
12
|
+
export const safeKill = (proc, signal) => {
|
|
13
|
+
try {
|
|
14
|
+
proc.kill(signal);
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
// already dead
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
export const safeWrite = (proc, line) => {
|
|
21
|
+
try {
|
|
22
|
+
proc.write(line);
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
// stdin closed / process died -- caller surfaces the error via the read path
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
// Resolves the `claude` CLI binary path. POSIX-only today: uses `which` and
|
|
31
|
+
// `$HOME`-rooted common install paths. Windows users running under WSL get
|
|
32
|
+
// the Linux layout, which works; native Windows is not supported yet.
|
|
8
33
|
const resolveBinaryPath = () => {
|
|
9
34
|
const found = whichSync("claude");
|
|
10
35
|
if (found) {
|
|
11
36
|
return found;
|
|
12
37
|
}
|
|
13
38
|
for (const p of BINARY.commonPaths) {
|
|
14
|
-
if (
|
|
39
|
+
if (isExecutableNonEmpty(p)) {
|
|
15
40
|
return p;
|
|
16
41
|
}
|
|
17
42
|
}
|
|
18
43
|
return BINARY.name;
|
|
19
44
|
};
|
|
20
|
-
|
|
45
|
+
// Rejects lines whose first non-whitespace char is `#` so commented-out
|
|
46
|
+
// aliases/exports don't silently apply. /m anchors to each line in rc files.
|
|
47
|
+
export const ALIAS_PATTERN = /^(?!\s*#).*?(?:alias\s+claude\s*=|export\s+).*CLAUDE_CONFIG_DIR=["']?\$?(?:HOME|\{HOME\}|~)\/?([^\s"']+?)["']?(?:\s|$)/m;
|
|
21
48
|
const resolveConfigDirFromAlias = () => {
|
|
22
49
|
const home = homedir();
|
|
23
|
-
|
|
50
|
+
// .zshenv is the one file zsh sources for NON-interactive shells, so
|
|
51
|
+
// users who export CLAUDE_CONFIG_DIR for cron/CI-like contexts often
|
|
52
|
+
// put it there. Include it alongside the interactive-shell rc files.
|
|
53
|
+
const rcFiles = [".zshenv", ".zshrc", ".bashrc", ".zprofile", ".bash_profile", ".aliases"];
|
|
24
54
|
for (const rcFile of rcFiles) {
|
|
25
55
|
try {
|
|
26
56
|
const content = readFileSync(join(home, rcFile), "utf-8");
|
|
@@ -36,7 +66,17 @@ const resolveConfigDirFromAlias = () => {
|
|
|
36
66
|
return undefined;
|
|
37
67
|
};
|
|
38
68
|
let cached;
|
|
39
|
-
|
|
69
|
+
/**
|
|
70
|
+
* Clears the cached resolved environment (binary path + alias-detected
|
|
71
|
+
* `CLAUDE_CONFIG_DIR`). Call this when either has changed mid-process -- for
|
|
72
|
+
* example after installing the Claude CLI during a test run, or when a long-
|
|
73
|
+
* running daemon updates the user's shell rc file. The next `spawnClaude()`
|
|
74
|
+
* will re-resolve from scratch.
|
|
75
|
+
*
|
|
76
|
+
* Normal applications should never need this; the cache is populated once at
|
|
77
|
+
* first use and kept for the process lifetime.
|
|
78
|
+
*/
|
|
79
|
+
export const resetResolvedEnvCache = () => {
|
|
40
80
|
cached = undefined;
|
|
41
81
|
};
|
|
42
82
|
const resolve = () => {
|
|
@@ -50,18 +90,23 @@ const resolve = () => {
|
|
|
50
90
|
};
|
|
51
91
|
export const buildArgs = (options, binaryPath) => {
|
|
52
92
|
const args = [binaryPath, "-p", "--output-format", "stream-json", "--input-format", "stream-json"];
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
93
|
+
const flag = (cond, name) => {
|
|
94
|
+
if (cond) {
|
|
95
|
+
args.push(name);
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
const kv = (value, name) => {
|
|
99
|
+
if (value) {
|
|
100
|
+
args.push(name, value);
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
// Default ON: the translator's block-dedup relies on --verbose emitting
|
|
104
|
+
// cumulative assistant content. Consumers must explicitly pass `false`
|
|
105
|
+
// to opt out (`undefined` still yields --verbose).
|
|
106
|
+
flag(options.verbose !== false, "--verbose");
|
|
107
|
+
kv(options.model, "--model");
|
|
108
|
+
kv(options.systemPrompt, "--system-prompt");
|
|
109
|
+
kv(options.appendSystemPrompt, "--append-system-prompt");
|
|
65
110
|
if (options.allowedTools) {
|
|
66
111
|
if (options.allowedTools.length === 0) {
|
|
67
112
|
args.push("--tools", "");
|
|
@@ -76,82 +121,79 @@ export const buildArgs = (options, binaryPath) => {
|
|
|
76
121
|
if (options.maxBudgetUsd !== undefined) {
|
|
77
122
|
args.push("--max-budget-usd", String(options.maxBudgetUsd));
|
|
78
123
|
}
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
args.push("--mcp-config", options.mcpConfig);
|
|
84
|
-
}
|
|
85
|
-
if (options.continueSession) {
|
|
86
|
-
args.push("--continue");
|
|
87
|
-
}
|
|
88
|
-
if (options.permissionMode) {
|
|
89
|
-
args.push("--permission-mode", options.permissionMode);
|
|
90
|
-
}
|
|
124
|
+
kv(options.resume, "--resume");
|
|
125
|
+
kv(options.mcpConfig, "--mcp-config");
|
|
126
|
+
flag(options.continueSession, "--continue");
|
|
127
|
+
kv(options.permissionMode, "--permission-mode");
|
|
91
128
|
if (options.addDirs && options.addDirs.length > 0) {
|
|
92
129
|
for (const dir of options.addDirs) {
|
|
93
130
|
args.push("--add-dir", dir);
|
|
94
131
|
}
|
|
95
132
|
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
}
|
|
105
|
-
if (options.bare) {
|
|
106
|
-
args.push("--bare");
|
|
107
|
-
}
|
|
108
|
-
if (options.jsonSchema) {
|
|
109
|
-
args.push("--json-schema", options.jsonSchema);
|
|
110
|
-
}
|
|
111
|
-
if (options.forkSession) {
|
|
112
|
-
args.push("--fork-session");
|
|
113
|
-
}
|
|
114
|
-
if (options.noSessionPersistence) {
|
|
115
|
-
args.push("--no-session-persistence");
|
|
116
|
-
}
|
|
117
|
-
if (options.sessionId) {
|
|
118
|
-
args.push("--session-id", options.sessionId);
|
|
119
|
-
}
|
|
133
|
+
kv(options.effort, "--effort");
|
|
134
|
+
flag(options.includeHookEvents, "--include-hook-events");
|
|
135
|
+
flag(options.includePartialMessages, "--include-partial-messages");
|
|
136
|
+
flag(options.bare, "--bare");
|
|
137
|
+
kv(options.jsonSchema, "--json-schema");
|
|
138
|
+
flag(options.forkSession, "--fork-session");
|
|
139
|
+
flag(options.noSessionPersistence, "--no-session-persistence");
|
|
140
|
+
kv(options.sessionId, "--session-id");
|
|
120
141
|
if (options.settingSources !== undefined) {
|
|
121
142
|
args.push("--setting-sources", options.settingSources);
|
|
122
143
|
}
|
|
123
|
-
|
|
124
|
-
args.push("--disable-slash-commands");
|
|
125
|
-
}
|
|
144
|
+
flag(options.disableSlashCommands, "--disable-slash-commands");
|
|
126
145
|
return args;
|
|
127
146
|
};
|
|
147
|
+
// Priority (lowest → highest): baseEnv < alias-detected config <
|
|
148
|
+
// user's explicit `options.env` < explicit `options.configDir`. User
|
|
149
|
+
// input always outranks the alias heuristic. Returns undefined when no
|
|
150
|
+
// override is needed, so spawnProcess can pass the parent env through.
|
|
151
|
+
export const buildSpawnEnv = (baseEnv, aliasConfigDir, options) => {
|
|
152
|
+
const needsEnv = aliasConfigDir || options.configDir || options.env;
|
|
153
|
+
if (!needsEnv) {
|
|
154
|
+
return undefined;
|
|
155
|
+
}
|
|
156
|
+
const spawnEnv = { ...baseEnv };
|
|
157
|
+
if (aliasConfigDir) {
|
|
158
|
+
spawnEnv.CLAUDE_CONFIG_DIR = aliasConfigDir;
|
|
159
|
+
}
|
|
160
|
+
if (options.env) {
|
|
161
|
+
Object.assign(spawnEnv, options.env);
|
|
162
|
+
}
|
|
163
|
+
if (options.configDir) {
|
|
164
|
+
spawnEnv.CLAUDE_CONFIG_DIR = options.configDir;
|
|
165
|
+
}
|
|
166
|
+
return spawnEnv;
|
|
167
|
+
};
|
|
128
168
|
export const spawnClaude = (options) => {
|
|
129
169
|
assertPositiveNumber(options.maxBudgetUsd, "maxBudgetUsd");
|
|
130
170
|
const resolved = resolve();
|
|
131
171
|
const args = buildArgs(options, resolved.binaryPath);
|
|
132
172
|
try {
|
|
133
|
-
const
|
|
134
|
-
let spawnEnv;
|
|
135
|
-
if (needsEnv) {
|
|
136
|
-
spawnEnv = { ...process.env };
|
|
137
|
-
if (options.env) {
|
|
138
|
-
Object.assign(spawnEnv, options.env);
|
|
139
|
-
}
|
|
140
|
-
if (resolved.aliasConfigDir) {
|
|
141
|
-
spawnEnv.CLAUDE_CONFIG_DIR = resolved.aliasConfigDir;
|
|
142
|
-
}
|
|
143
|
-
if (options.configDir) {
|
|
144
|
-
spawnEnv.CLAUDE_CONFIG_DIR = options.configDir;
|
|
145
|
-
}
|
|
146
|
-
}
|
|
173
|
+
const spawnEnv = buildSpawnEnv(process.env, resolved.aliasConfigDir, options);
|
|
147
174
|
const rawProc = spawnProcess(args, { cwd: options.cwd, env: spawnEnv });
|
|
148
175
|
rawProc.exited.catch(() => { });
|
|
176
|
+
// Tear the child down when the caller's signal aborts. Without this,
|
|
177
|
+
// a signal that fires BEFORE stdout emits anything leaves the reader
|
|
178
|
+
// loop to eventually notice -- the child keeps running in the meantime.
|
|
179
|
+
// Register FIRST, then re-check `aborted`: closes the gap where abort
|
|
180
|
+
// could fire between the check and listener attach. `once: true` lets
|
|
181
|
+
// the listener be GC'd after firing.
|
|
182
|
+
if (options.signal) {
|
|
183
|
+
const onAbort = () => {
|
|
184
|
+
safeKill(rawProc);
|
|
185
|
+
};
|
|
186
|
+
options.signal.addEventListener("abort", onAbort, { once: true });
|
|
187
|
+
if (options.signal.aborted) {
|
|
188
|
+
safeKill(rawProc);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
149
191
|
const claudeProc = {
|
|
150
192
|
write: (msg) => {
|
|
151
193
|
rawProc.stdin.write(msg);
|
|
152
194
|
},
|
|
153
|
-
kill: () => {
|
|
154
|
-
rawProc.kill();
|
|
195
|
+
kill: (signal) => {
|
|
196
|
+
rawProc.kill(signal);
|
|
155
197
|
},
|
|
156
198
|
exited: rawProc.exited,
|
|
157
199
|
stdout: rawProc.stdout,
|
package/dist/reader.d.ts
CHANGED
|
@@ -2,12 +2,21 @@ import type { ITranslator } from "./parser/translator.js";
|
|
|
2
2
|
import type { IClaudeProcess } from "./process.js";
|
|
3
3
|
import type { IToolHandlerInstance } from "./tools/handler.js";
|
|
4
4
|
import type { TRelayEvent } from "./types/events.js";
|
|
5
|
+
import type { TWarn } from "./warnings.js";
|
|
5
6
|
export interface IReaderOptions {
|
|
6
7
|
reader: ReadableStreamDefaultReader<Uint8Array>;
|
|
7
8
|
translator: ITranslator;
|
|
8
9
|
toolHandler?: IToolHandlerInstance;
|
|
9
10
|
proc?: IClaudeProcess;
|
|
10
11
|
signal?: AbortSignal;
|
|
12
|
+
onWarning?: TWarn;
|
|
11
13
|
}
|
|
14
|
+
export interface IStderrDrain {
|
|
15
|
+
chunks: string[];
|
|
16
|
+
done: Promise<void>;
|
|
17
|
+
text: () => string;
|
|
18
|
+
}
|
|
19
|
+
export declare const drainStderr: (proc: {
|
|
20
|
+
stderr: ReadableStream<Uint8Array>;
|
|
21
|
+
}) => IStderrDrain;
|
|
12
22
|
export declare function readNdjsonEvents(opts: IReaderOptions): AsyncGenerator<TRelayEvent>;
|
|
13
|
-
export type { TRelayEvent };
|
package/dist/reader.js
CHANGED
|
@@ -2,22 +2,89 @@ import { LIMITS, TIMEOUTS } from "./constants.js";
|
|
|
2
2
|
import { AbortError, ClaudeError, TimeoutError } from "./errors.js";
|
|
3
3
|
import { parseLine } from "./parser/ndjson.js";
|
|
4
4
|
import { dispatchToolDecision } from "./pipeline.js";
|
|
5
|
+
import { safeKill, safeWrite } from "./process.js";
|
|
6
|
+
import { writer } from "./writer.js";
|
|
7
|
+
export const drainStderr = (proc) => {
|
|
8
|
+
const chunks = [];
|
|
9
|
+
const stderrReader = proc.stderr.getReader();
|
|
10
|
+
const decoder = new TextDecoder();
|
|
11
|
+
const done = (async () => {
|
|
12
|
+
try {
|
|
13
|
+
while (true) {
|
|
14
|
+
const { done: isDone, value } = await stderrReader.read();
|
|
15
|
+
if (isDone) {
|
|
16
|
+
break;
|
|
17
|
+
}
|
|
18
|
+
chunks.push(decoder.decode(value, { stream: true }));
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
// process exited
|
|
23
|
+
}
|
|
24
|
+
finally {
|
|
25
|
+
// Flush any trailing partial multibyte sequence.
|
|
26
|
+
const tail = decoder.decode();
|
|
27
|
+
if (tail) {
|
|
28
|
+
chunks.push(tail);
|
|
29
|
+
}
|
|
30
|
+
stderrReader.releaseLock();
|
|
31
|
+
}
|
|
32
|
+
})().catch(() => { });
|
|
33
|
+
return {
|
|
34
|
+
chunks,
|
|
35
|
+
done,
|
|
36
|
+
text: () => chunks.join("").trim(),
|
|
37
|
+
};
|
|
38
|
+
};
|
|
5
39
|
export async function* readNdjsonEvents(opts) {
|
|
6
40
|
const { reader, translator, signal } = opts;
|
|
7
41
|
const decoder = new TextDecoder();
|
|
8
42
|
let buffer = "";
|
|
9
43
|
let timeoutId;
|
|
10
44
|
let turnComplete = false;
|
|
45
|
+
let abortReject;
|
|
46
|
+
const abortPromise = new Promise((_, reject) => {
|
|
47
|
+
abortReject = reject;
|
|
48
|
+
});
|
|
49
|
+
// Swallow unhandled rejection if nothing ever races against this promise.
|
|
50
|
+
abortPromise.catch(() => { });
|
|
51
|
+
// Single resettable timeout shared across all iterations -- avoids leaking
|
|
52
|
+
// a new Promise + setTimeout per read loop.
|
|
53
|
+
let timeoutReject;
|
|
54
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
55
|
+
timeoutReject = reject;
|
|
56
|
+
});
|
|
57
|
+
timeoutPromise.catch(() => { });
|
|
58
|
+
// Shared per-raw-event dispatch. Used by both the main read loop and the
|
|
59
|
+
// trailing-buffer flush so the translate → tool-dispatch → yield sequence
|
|
60
|
+
// lives in one place. `!turnComplete` guards dispatch so we don't approve
|
|
61
|
+
// or deny a tool call the CLI emits after it already said it's done.
|
|
62
|
+
const processRaw = async function* (raw) {
|
|
63
|
+
const translated = translator.translate(raw);
|
|
64
|
+
for (const event of translated) {
|
|
65
|
+
if (event.type === "tool_use" && !turnComplete && opts.toolHandler && opts.proc) {
|
|
66
|
+
await dispatchToolDecision(opts.proc, opts.toolHandler, event, opts.onWarning);
|
|
67
|
+
}
|
|
68
|
+
yield event;
|
|
69
|
+
if (event.type === "turn_complete") {
|
|
70
|
+
turnComplete = true;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
const resetReadTimeout = () => {
|
|
75
|
+
if (timeoutId) {
|
|
76
|
+
clearTimeout(timeoutId);
|
|
77
|
+
}
|
|
78
|
+
timeoutId = setTimeout(() => {
|
|
79
|
+
timeoutReject?.(new TimeoutError(`No data received within ${TIMEOUTS.defaultAbortMs}ms`));
|
|
80
|
+
}, TIMEOUTS.defaultAbortMs);
|
|
81
|
+
};
|
|
11
82
|
const abortHandler = signal
|
|
12
83
|
? () => {
|
|
84
|
+
abortReject?.(new AbortError());
|
|
13
85
|
if (opts.proc) {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
}
|
|
17
|
-
catch {
|
|
18
|
-
// stdin closed
|
|
19
|
-
}
|
|
20
|
-
opts.proc.kill();
|
|
86
|
+
safeWrite(opts.proc, writer.abort());
|
|
87
|
+
safeKill(opts.proc);
|
|
21
88
|
}
|
|
22
89
|
}
|
|
23
90
|
: undefined;
|
|
@@ -32,20 +99,19 @@ export async function* readNdjsonEvents(opts) {
|
|
|
32
99
|
if (signal?.aborted) {
|
|
33
100
|
throw new AbortError();
|
|
34
101
|
}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
reject(new TimeoutError(`No data received within ${TIMEOUTS.defaultAbortMs}ms`));
|
|
38
|
-
}, TIMEOUTS.defaultAbortMs);
|
|
39
|
-
});
|
|
40
|
-
const readResult = await Promise.race([reader.read(), timeoutPromise]);
|
|
41
|
-
clearTimeout(timeoutId);
|
|
102
|
+
resetReadTimeout();
|
|
103
|
+
const readResult = await Promise.race([reader.read(), timeoutPromise, abortPromise]);
|
|
42
104
|
const { done, value } = readResult;
|
|
43
105
|
if (done) {
|
|
44
106
|
break;
|
|
45
107
|
}
|
|
46
108
|
buffer += decoder.decode(value, { stream: true });
|
|
109
|
+
// The limit applies to the accumulated buffer (which contains at most
|
|
110
|
+
// one in-progress line plus any already-split lines being held), so
|
|
111
|
+
// a single oversize line trips the same guard. Name is legacy -- the
|
|
112
|
+
// check is effectively "no NDJSON message may grow past this size".
|
|
47
113
|
if (buffer.length > LIMITS.ndjsonMaxLineChars) {
|
|
48
|
-
throw new ClaudeError(`NDJSON buffer exceeded ${LIMITS.ndjsonMaxLineChars} chars`);
|
|
114
|
+
throw new ClaudeError(`NDJSON buffer exceeded ${LIMITS.ndjsonMaxLineChars} chars (single line or accumulated pending lines)`);
|
|
49
115
|
}
|
|
50
116
|
const lines = buffer.split("\n");
|
|
51
117
|
buffer = lines.pop() ?? "";
|
|
@@ -54,16 +120,7 @@ export async function* readNdjsonEvents(opts) {
|
|
|
54
120
|
if (!raw) {
|
|
55
121
|
continue;
|
|
56
122
|
}
|
|
57
|
-
|
|
58
|
-
for (const event of events) {
|
|
59
|
-
if (event.type === "tool_use" && opts.toolHandler && opts.proc) {
|
|
60
|
-
await dispatchToolDecision(opts.proc, opts.toolHandler, event);
|
|
61
|
-
}
|
|
62
|
-
yield event;
|
|
63
|
-
if (event.type === "turn_complete") {
|
|
64
|
-
turnComplete = true;
|
|
65
|
-
}
|
|
66
|
-
}
|
|
123
|
+
yield* processRaw(raw);
|
|
67
124
|
}
|
|
68
125
|
if (turnComplete) {
|
|
69
126
|
break;
|
|
@@ -72,16 +129,7 @@ export async function* readNdjsonEvents(opts) {
|
|
|
72
129
|
if (buffer.trim()) {
|
|
73
130
|
const raw = parseLine(buffer);
|
|
74
131
|
if (raw) {
|
|
75
|
-
|
|
76
|
-
for (const event of events) {
|
|
77
|
-
if (event.type === "tool_use" && opts.toolHandler && opts.proc && !turnComplete) {
|
|
78
|
-
await dispatchToolDecision(opts.proc, opts.toolHandler, event);
|
|
79
|
-
}
|
|
80
|
-
yield event;
|
|
81
|
-
if (event.type === "turn_complete") {
|
|
82
|
-
turnComplete = true;
|
|
83
|
-
}
|
|
84
|
-
}
|
|
132
|
+
yield* processRaw(raw);
|
|
85
133
|
}
|
|
86
134
|
}
|
|
87
135
|
}
|
package/dist/runtime.d.ts
CHANGED
|
@@ -1,18 +1,19 @@
|
|
|
1
|
-
|
|
1
|
+
interface IRawProcess {
|
|
2
2
|
stdin: {
|
|
3
3
|
write: (data: string) => void;
|
|
4
4
|
end: () => void;
|
|
5
5
|
};
|
|
6
6
|
stdout: ReadableStream<Uint8Array>;
|
|
7
7
|
stderr: ReadableStream<Uint8Array>;
|
|
8
|
-
kill: () => void;
|
|
8
|
+
kill: (signal?: NodeJS.Signals | number) => void;
|
|
9
9
|
exited: Promise<number>;
|
|
10
10
|
pid: number;
|
|
11
11
|
}
|
|
12
|
-
|
|
12
|
+
interface ISpawnOpts {
|
|
13
13
|
cwd?: string;
|
|
14
14
|
env?: Record<string, string | undefined>;
|
|
15
15
|
}
|
|
16
16
|
export declare const spawnProcess: (args: string[], opts: ISpawnOpts) => IRawProcess;
|
|
17
17
|
export declare const whichSync: (name: string) => string | undefined;
|
|
18
|
-
export declare const
|
|
18
|
+
export declare const isExecutableNonEmpty: (path: string) => boolean;
|
|
19
|
+
export {};
|
package/dist/runtime.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { execFileSync, spawn as nodeSpawn } from "node:child_process";
|
|
2
2
|
import { accessSync, constants, statSync } from "node:fs";
|
|
3
3
|
import { Readable } from "node:stream";
|
|
4
|
+
import { ProcessError } from "./errors.js";
|
|
4
5
|
const isBun = typeof globalThis.Bun !== "undefined";
|
|
5
6
|
export const spawnProcess = (args, opts) => {
|
|
6
7
|
if (isBun) {
|
|
@@ -29,16 +30,10 @@ export const whichSync = (name) => {
|
|
|
29
30
|
return undefined;
|
|
30
31
|
}
|
|
31
32
|
};
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
return statSync(path).size > 0;
|
|
37
|
-
}
|
|
38
|
-
catch {
|
|
39
|
-
return false;
|
|
40
|
-
}
|
|
41
|
-
}
|
|
33
|
+
// Used to vet candidate `claude` binary paths -- a zero-byte stub or a
|
|
34
|
+
// non-executable regular file both count as "not a usable binary" here.
|
|
35
|
+
// Name reflects behavior: this is NOT a generic fs.exists check.
|
|
36
|
+
export const isExecutableNonEmpty = (path) => {
|
|
42
37
|
try {
|
|
43
38
|
accessSync(path, constants.X_OK);
|
|
44
39
|
return statSync(path).size > 0;
|
|
@@ -66,50 +61,27 @@ const spawnBun = (args, opts) => {
|
|
|
66
61
|
},
|
|
67
62
|
stdout: proc.stdout,
|
|
68
63
|
stderr: proc.stderr,
|
|
69
|
-
kill: () => {
|
|
70
|
-
proc.kill();
|
|
64
|
+
kill: (signal) => {
|
|
65
|
+
proc.kill(signal);
|
|
71
66
|
},
|
|
72
67
|
exited: proc.exited,
|
|
73
68
|
pid: proc.pid,
|
|
74
69
|
};
|
|
75
70
|
};
|
|
76
|
-
const
|
|
77
|
-
return new ReadableStream({
|
|
78
|
-
start(controller) {
|
|
79
|
-
let closed = false;
|
|
80
|
-
readable.on("data", (chunk) => {
|
|
81
|
-
if (!closed) {
|
|
82
|
-
controller.enqueue(new Uint8Array(chunk));
|
|
83
|
-
}
|
|
84
|
-
});
|
|
85
|
-
readable.on("end", () => {
|
|
86
|
-
if (!closed) {
|
|
87
|
-
closed = true;
|
|
88
|
-
controller.close();
|
|
89
|
-
}
|
|
90
|
-
});
|
|
91
|
-
readable.on("error", (err) => {
|
|
92
|
-
if (!closed) {
|
|
93
|
-
closed = true;
|
|
94
|
-
controller.error(err);
|
|
95
|
-
}
|
|
96
|
-
});
|
|
97
|
-
},
|
|
98
|
-
cancel() {
|
|
99
|
-
readable.destroy();
|
|
100
|
-
},
|
|
101
|
-
});
|
|
102
|
-
};
|
|
71
|
+
const toWeb = (readable) => Readable.toWeb(readable);
|
|
103
72
|
const spawnNode = (args, opts) => {
|
|
104
73
|
const [cmd, ...rest] = args;
|
|
105
74
|
if (!cmd) {
|
|
106
|
-
throw new
|
|
75
|
+
throw new ProcessError("No command specified");
|
|
107
76
|
}
|
|
108
77
|
const child = nodeSpawn(cmd, rest, {
|
|
109
78
|
cwd: opts.cwd,
|
|
110
79
|
stdio: ["pipe", "pipe", "pipe"],
|
|
111
80
|
env: opts.env,
|
|
112
81
|
});
|
|
82
|
+
if (child.pid === undefined) {
|
|
83
|
+
throw new ProcessError(`Failed to spawn ${cmd}: no PID assigned`);
|
|
84
|
+
}
|
|
113
85
|
const exited = new Promise((resolve, reject) => {
|
|
114
86
|
child.on("exit", (code) => {
|
|
115
87
|
resolve(code ?? 1);
|
|
@@ -119,18 +91,21 @@ const spawnNode = (args, opts) => {
|
|
|
119
91
|
return {
|
|
120
92
|
stdin: {
|
|
121
93
|
write: (data) => {
|
|
122
|
-
child.stdin
|
|
94
|
+
if (!child.stdin || child.stdin.destroyed) {
|
|
95
|
+
throw new ProcessError("Cannot write: stdin is not writable");
|
|
96
|
+
}
|
|
97
|
+
child.stdin.write(data);
|
|
123
98
|
},
|
|
124
99
|
end: () => {
|
|
125
100
|
child.stdin?.end();
|
|
126
101
|
},
|
|
127
102
|
},
|
|
128
|
-
stdout: child.stdout ?
|
|
129
|
-
stderr: child.stderr ?
|
|
130
|
-
kill: () => {
|
|
131
|
-
child.kill();
|
|
103
|
+
stdout: child.stdout ? toWeb(child.stdout) : new ReadableStream(),
|
|
104
|
+
stderr: child.stderr ? toWeb(child.stderr) : new ReadableStream(),
|
|
105
|
+
kill: (signal) => {
|
|
106
|
+
child.kill(signal);
|
|
132
107
|
},
|
|
133
108
|
exited,
|
|
134
|
-
pid: child.pid
|
|
109
|
+
pid: child.pid,
|
|
135
110
|
};
|
|
136
111
|
};
|
package/dist/session.d.ts
CHANGED
|
@@ -1,8 +1,36 @@
|
|
|
1
|
-
import type { ISessionOptions } from "./types/options.js";
|
|
1
|
+
import type { IAskOptions, ISessionOptions } from "./types/options.js";
|
|
2
2
|
import type { TAskResult } from "./types/results.js";
|
|
3
3
|
export interface IClaudeSession extends AsyncDisposable {
|
|
4
|
-
ask: (prompt: string) => Promise<TAskResult>;
|
|
4
|
+
ask: (prompt: string, options?: IAskOptions) => Promise<TAskResult>;
|
|
5
5
|
close: () => Promise<void>;
|
|
6
6
|
sessionId: string | undefined;
|
|
7
7
|
}
|
|
8
|
+
/**
|
|
9
|
+
* Creates a multi-turn Claude session backed by a single long-lived CLI
|
|
10
|
+
* process. Each `ask()` sends a user prompt and resolves with `TAskResult`
|
|
11
|
+
* for that turn. Calls are serialized -- a second `ask()` waits for the
|
|
12
|
+
* first to complete. Use `close()` (or `await using`) to free the process.
|
|
13
|
+
*
|
|
14
|
+
* ### Retry behavior
|
|
15
|
+
* Each `ask()` automatically retries transient failures -- process crashes
|
|
16
|
+
* matching SIGKILL/SIGTERM/SIGPIPE exit codes, `ECONNRESET`, `ECONNREFUSED`,
|
|
17
|
+
* `ETIMEDOUT`, `EHOSTUNREACH`, `ENETUNREACH`, `EAI_AGAIN`, Anthropic
|
|
18
|
+
* `overloaded_error` / 529s, broken-pipe / "socket hang up" messages, etc.
|
|
19
|
+
* (see `isTransientError`). Backoff is `500ms → 1s → 2s`; the budget is
|
|
20
|
+
* `LIMITS.maxRespawnAttempts` (currently 3) and is shared across a single
|
|
21
|
+
* `ask()`. When the budget is exhausted the session throws
|
|
22
|
+
* `KnownError("retry-exhausted")` and marks itself closed.
|
|
23
|
+
*
|
|
24
|
+
* Fatal errors -- `KnownError` and `BudgetExceededError` -- also close the
|
|
25
|
+
* session. Any subsequent `ask()` on a closed session rejects with
|
|
26
|
+
* `ClaudeError("Session is closed")`. All other errors (abort, timeout,
|
|
27
|
+
* non-transient `ProcessError`) propagate without closing, and the caller
|
|
28
|
+
* may decide whether to retry at a higher level.
|
|
29
|
+
*
|
|
30
|
+
* ### Observability
|
|
31
|
+
* - `onCostUpdate(snapshot)` -- fires after every `turn_complete`.
|
|
32
|
+
* - `onRetry(attempt, error)` -- fires each time a transient failure triggers
|
|
33
|
+
* a respawn inside one `ask()`. Attempt is 1-indexed.
|
|
34
|
+
* - `onWarning(message, cause)` -- routes all library-emitted warnings.
|
|
35
|
+
*/
|
|
8
36
|
export declare const createSession: (options?: ISessionOptions) => IClaudeSession;
|