@pivanov/claude-wire 0.0.2 → 0.0.3
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 +2 -3
- package/dist/client.js +10 -6
- package/dist/constants.d.ts +2 -0
- package/dist/constants.js +3 -0
- package/dist/cost.js +2 -2
- package/dist/errors.d.ts +2 -3
- package/dist/errors.js +9 -6
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -2
- package/dist/parser/content.js +3 -2
- package/dist/parser/translator.js +4 -2
- package/dist/process.d.ts +2 -0
- package/dist/process.js +37 -56
- package/dist/reader.d.ts +7 -1
- package/dist/reader.js +54 -8
- package/dist/runtime.js +13 -41
- package/dist/session.js +63 -51
- package/dist/stream.js +30 -39
- package/dist/tools/handler.js +12 -4
- package/dist/types/options.d.ts +11 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -3,8 +3,7 @@
|
|
|
3
3
|
Run [Claude Code](https://claude.ai/download) programmatically from TypeScript.
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/@pivanov/claude-wire)
|
|
6
|
-
[](./LICENSE)
|
|
6
|
+
[](./LICENSE)
|
|
8
7
|
|
|
9
8
|
```ts
|
|
10
9
|
import { claude } from "@pivanov/claude-wire";
|
|
@@ -26,7 +25,7 @@ console.log(result.costUsd); // 0.0084
|
|
|
26
25
|
- **Cost tracking** - per-request budgets with auto-abort
|
|
27
26
|
- **Fully typed** - discriminated union events, full IntelliSense
|
|
28
27
|
- **Resilient** - auto-respawn, transient error detection, AbortSignal
|
|
29
|
-
- **Zero dependencies** -
|
|
28
|
+
- **Zero dependencies** - 16.2 kB gzipped
|
|
30
29
|
|
|
31
30
|
## Install
|
|
32
31
|
|
package/dist/client.js
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
import { createSession } from "./session.js";
|
|
2
2
|
import { createStream } from "./stream.js";
|
|
3
|
-
const mergeOptions = (defaults, overrides) =>
|
|
4
|
-
...defaults,
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
3
|
+
const mergeOptions = (defaults, overrides) => {
|
|
4
|
+
const merged = { ...defaults, ...overrides };
|
|
5
|
+
if (overrides && "tools" in overrides) {
|
|
6
|
+
merged.tools = overrides.tools ? { ...defaults.tools, ...overrides.tools } : overrides.tools;
|
|
7
|
+
}
|
|
8
|
+
if (overrides && "env" in overrides) {
|
|
9
|
+
merged.env = overrides.env ? { ...defaults.env, ...overrides.env } : overrides.env;
|
|
10
|
+
}
|
|
11
|
+
return merged;
|
|
12
|
+
};
|
|
9
13
|
export const createClient = (defaults = {}) => {
|
|
10
14
|
const ask = async (prompt, options) => {
|
|
11
15
|
const merged = mergeOptions(defaults, options);
|
package/dist/constants.d.ts
CHANGED
|
@@ -6,7 +6,9 @@ export declare const LIMITS: {
|
|
|
6
6
|
readonly maxRespawnAttempts: 3;
|
|
7
7
|
readonly sessionMaxTurnsBeforeRecycle: 100;
|
|
8
8
|
readonly ndjsonMaxLineChars: number;
|
|
9
|
+
readonly fingerprintTextLen: 64;
|
|
9
10
|
};
|
|
11
|
+
export declare const RESPAWN_BACKOFF_MS: readonly [500, 1000, 2000];
|
|
10
12
|
export declare const BINARY: {
|
|
11
13
|
readonly name: "claude";
|
|
12
14
|
readonly commonPaths: readonly [`${string}/.local/bin/claude`, `${string}/.claude/bin/claude`, "/usr/local/bin/claude", "/opt/homebrew/bin/claude"];
|
package/dist/constants.js
CHANGED
|
@@ -7,7 +7,10 @@ export const LIMITS = {
|
|
|
7
7
|
maxRespawnAttempts: 3,
|
|
8
8
|
sessionMaxTurnsBeforeRecycle: 100,
|
|
9
9
|
ndjsonMaxLineChars: 10 * 1024 * 1024,
|
|
10
|
+
fingerprintTextLen: 64,
|
|
10
11
|
};
|
|
12
|
+
// Respawn backoff in ms, indexed by consecutiveCrashes (1st=500ms, 2nd=1s, 3rd=2s).
|
|
13
|
+
export const RESPAWN_BACKOFF_MS = [500, 1000, 2000];
|
|
11
14
|
const home = homedir();
|
|
12
15
|
export const BINARY = {
|
|
13
16
|
name: "claude",
|
package/dist/cost.js
CHANGED
|
@@ -17,8 +17,8 @@ export const createCostTracker = (options = {}) => {
|
|
|
17
17
|
try {
|
|
18
18
|
options.onCostUpdate(snapshot());
|
|
19
19
|
}
|
|
20
|
-
catch {
|
|
21
|
-
|
|
20
|
+
catch (error) {
|
|
21
|
+
console.warn("[claude-wire] onCostUpdate callback threw:", error instanceof Error ? error.message : error);
|
|
22
22
|
}
|
|
23
23
|
}
|
|
24
24
|
};
|
package/dist/errors.d.ts
CHANGED
|
@@ -16,8 +16,8 @@ export declare class ProcessError extends ClaudeError {
|
|
|
16
16
|
readonly exitCode?: number | undefined;
|
|
17
17
|
constructor(message: string, exitCode?: number | undefined);
|
|
18
18
|
}
|
|
19
|
-
declare const KNOWN_ERROR_CODES: readonly ["not-authenticated", "binary-not-found", "session-expired", "permission-denied", "invalid-model"];
|
|
20
|
-
type TKnownErrorCode = (typeof KNOWN_ERROR_CODES)[number];
|
|
19
|
+
export declare const KNOWN_ERROR_CODES: readonly ["not-authenticated", "binary-not-found", "session-expired", "permission-denied", "invalid-model"];
|
|
20
|
+
export type TKnownErrorCode = (typeof KNOWN_ERROR_CODES)[number];
|
|
21
21
|
export declare class KnownError extends ClaudeError {
|
|
22
22
|
readonly code: TKnownErrorCode;
|
|
23
23
|
constructor(code: TKnownErrorCode, message?: string);
|
|
@@ -26,4 +26,3 @@ export declare const isKnownError: (error: unknown) => error is KnownError;
|
|
|
26
26
|
export declare const isTransientError: (error: unknown) => boolean;
|
|
27
27
|
export declare const errorMessage: (error: unknown) => string;
|
|
28
28
|
export declare const assertPositiveNumber: (value: number | undefined, name: string) => void;
|
|
29
|
-
export {};
|
package/dist/errors.js
CHANGED
|
@@ -34,7 +34,7 @@ export class ProcessError extends ClaudeError {
|
|
|
34
34
|
this.name = "ProcessError";
|
|
35
35
|
}
|
|
36
36
|
}
|
|
37
|
-
const KNOWN_ERROR_CODES = ["not-authenticated", "binary-not-found", "session-expired", "permission-denied", "invalid-model"];
|
|
37
|
+
export const KNOWN_ERROR_CODES = ["not-authenticated", "binary-not-found", "session-expired", "permission-denied", "invalid-model"];
|
|
38
38
|
export class KnownError extends ClaudeError {
|
|
39
39
|
code;
|
|
40
40
|
constructor(code, message) {
|
|
@@ -46,14 +46,16 @@ export class KnownError extends ClaudeError {
|
|
|
46
46
|
export const isKnownError = (error) => {
|
|
47
47
|
return error instanceof KnownError;
|
|
48
48
|
};
|
|
49
|
-
const TRANSIENT_PATTERN = /fetch failed|ECONNREFUSED|ETIMEDOUT|ECONNRESET|EAI_AGAIN|network error|network timeout|EPIPE|SIGPIPE|broken pipe/i;
|
|
49
|
+
const TRANSIENT_PATTERN = /fetch failed|ECONNREFUSED|ETIMEDOUT|ECONNRESET|ECONNABORTED|ENETUNREACH|EAI_AGAIN|network error|network timeout|EPIPE|SIGPIPE|broken pipe/i;
|
|
50
|
+
// Exit codes we treat as transient: 137 = SIGKILL (OOM), 141 = SIGPIPE,
|
|
51
|
+
// 143 = SIGTERM. Non-zero normal exits (e.g. 1) stay non-transient.
|
|
52
|
+
const TRANSIENT_EXIT_CODES = new Set([137, 141, 143]);
|
|
50
53
|
export const isTransientError = (error) => {
|
|
51
54
|
if (error instanceof AbortError || error instanceof BudgetExceededError) {
|
|
52
55
|
return false;
|
|
53
56
|
}
|
|
54
57
|
if (error instanceof ProcessError) {
|
|
55
|
-
|
|
56
|
-
return error.exitCode !== undefined && transientCodes.includes(error.exitCode);
|
|
58
|
+
return error.exitCode !== undefined && TRANSIENT_EXIT_CODES.has(error.exitCode);
|
|
57
59
|
}
|
|
58
60
|
const message = errorMessage(error);
|
|
59
61
|
return TRANSIENT_PATTERN.test(message);
|
|
@@ -62,7 +64,8 @@ export const errorMessage = (error) => {
|
|
|
62
64
|
return error instanceof Error ? error.message : String(error);
|
|
63
65
|
};
|
|
64
66
|
export const assertPositiveNumber = (value, name) => {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
+
// Allow 0 so callers can express "no spend permitted" (useful in tests).
|
|
68
|
+
if (value !== undefined && (!Number.isFinite(value) || value < 0)) {
|
|
69
|
+
throw new ClaudeError(`${name} must be a finite non-negative number`);
|
|
67
70
|
}
|
|
68
71
|
};
|
package/dist/index.d.ts
CHANGED
|
@@ -3,7 +3,8 @@ export { createClient } from "./client.js";
|
|
|
3
3
|
export { BINARY, LIMITS, TIMEOUTS } from "./constants.js";
|
|
4
4
|
export type { ICostTracker, ICostTrackerOptions } from "./cost.js";
|
|
5
5
|
export { createCostTracker } from "./cost.js";
|
|
6
|
-
export {
|
|
6
|
+
export type { TKnownErrorCode } from "./errors.js";
|
|
7
|
+
export { AbortError, assertPositiveNumber, BudgetExceededError, ClaudeError, errorMessage, isKnownError, isTransientError, KNOWN_ERROR_CODES, KnownError, ProcessError, TimeoutError, } from "./errors.js";
|
|
7
8
|
export { blockFingerprint, extractContent, parseDoubleEncoded } from "./parser/content.js";
|
|
8
9
|
export { parseLine } from "./parser/ndjson.js";
|
|
9
10
|
export type { ITranslator } from "./parser/translator.js";
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
import { createClient } from "./client.js";
|
|
1
2
|
export { createClient } from "./client.js";
|
|
2
3
|
export { BINARY, LIMITS, TIMEOUTS } from "./constants.js";
|
|
3
4
|
export { createCostTracker } from "./cost.js";
|
|
4
|
-
export { AbortError, assertPositiveNumber, BudgetExceededError, ClaudeError, errorMessage, isKnownError, isTransientError, KnownError, ProcessError, TimeoutError, } from "./errors.js";
|
|
5
|
+
export { AbortError, assertPositiveNumber, BudgetExceededError, ClaudeError, errorMessage, isKnownError, isTransientError, KNOWN_ERROR_CODES, KnownError, ProcessError, TimeoutError, } from "./errors.js";
|
|
5
6
|
export { blockFingerprint, extractContent, parseDoubleEncoded } from "./parser/content.js";
|
|
6
7
|
export { parseLine } from "./parser/ndjson.js";
|
|
7
8
|
export { createTranslator } from "./parser/translator.js";
|
|
@@ -12,5 +13,4 @@ export { createStream } from "./stream.js";
|
|
|
12
13
|
export { createToolHandler } from "./tools/handler.js";
|
|
13
14
|
export { BUILT_IN_TOOLS, isBuiltInTool } from "./tools/registry.js";
|
|
14
15
|
export { writer } from "./writer.js";
|
|
15
|
-
import { createClient } from "./client.js";
|
|
16
16
|
export const claude = createClient();
|
package/dist/parser/content.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
+
import { LIMITS } from "../constants.js";
|
|
1
2
|
export const blockFingerprint = (block) => {
|
|
2
3
|
if (block.type === "tool_use" && block.id) {
|
|
3
4
|
return `tool_use:${block.id}`;
|
|
4
5
|
}
|
|
5
6
|
const text = block.type === "thinking" ? (block.thinking ?? block.text ?? "") : (block.text ?? "");
|
|
6
7
|
if (text) {
|
|
7
|
-
return `${block.type}:${text.slice(0,
|
|
8
|
+
return `${block.type}:${text.slice(0, LIMITS.fingerprintTextLen)}`;
|
|
8
9
|
}
|
|
9
10
|
return `${block.type}:${block.tool_use_id ?? "unknown"}`;
|
|
10
11
|
};
|
|
@@ -17,7 +18,7 @@ export const extractContent = (content) => {
|
|
|
17
18
|
}
|
|
18
19
|
if (Array.isArray(content)) {
|
|
19
20
|
return content
|
|
20
|
-
.filter((block) => typeof block === "object" && block !== null && "text" in block)
|
|
21
|
+
.filter((block) => typeof block === "object" && block !== null && "text" in block && typeof block.text === "string")
|
|
21
22
|
.map((block) => block.text)
|
|
22
23
|
.join("\n");
|
|
23
24
|
}
|
|
@@ -41,13 +41,15 @@ const translateContentBlock = (block) => {
|
|
|
41
41
|
return undefined;
|
|
42
42
|
}
|
|
43
43
|
case "tool_use": {
|
|
44
|
-
|
|
44
|
+
// Drop malformed tool_use events entirely. An empty toolName would
|
|
45
|
+
// otherwise bypass allow/block lists by matching nothing.
|
|
46
|
+
if (!block.id || !block.name) {
|
|
45
47
|
return undefined;
|
|
46
48
|
}
|
|
47
49
|
return {
|
|
48
50
|
type: "tool_use",
|
|
49
51
|
toolUseId: block.id,
|
|
50
|
-
toolName: block.name
|
|
52
|
+
toolName: block.name,
|
|
51
53
|
input: typeof block.input === "string" ? block.input : JSON.stringify(block.input ?? {}),
|
|
52
54
|
};
|
|
53
55
|
}
|
package/dist/process.d.ts
CHANGED
|
@@ -10,6 +10,8 @@ export interface IClaudeProcess {
|
|
|
10
10
|
export interface ISpawnOptions extends IClaudeOptions {
|
|
11
11
|
prompt?: string;
|
|
12
12
|
}
|
|
13
|
+
export declare const ALIAS_PATTERN: RegExp;
|
|
14
|
+
export declare const resolveConfigDirFromAlias: () => string | undefined;
|
|
13
15
|
export declare const resetBinaryCache: () => void;
|
|
14
16
|
export declare const buildArgs: (options: ISpawnOptions, binaryPath: string) => string[];
|
|
15
17
|
export declare const spawnClaude: (options: ISpawnOptions) => IClaudeProcess;
|
package/dist/process.js
CHANGED
|
@@ -17,8 +17,10 @@ const resolveBinaryPath = () => {
|
|
|
17
17
|
}
|
|
18
18
|
return BINARY.name;
|
|
19
19
|
};
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
// Rejects lines whose first non-whitespace char is `#` so commented-out
|
|
21
|
+
// aliases/exports don't silently apply. /m anchors to each line in rc files.
|
|
22
|
+
export const ALIAS_PATTERN = /^(?!\s*#).*?(?:alias\s+claude\s*=|export\s+).*CLAUDE_CONFIG_DIR=["']?\$?(?:HOME|\{HOME\}|~)\/?([^\s"']+?)["']?(?:\s|$)/m;
|
|
23
|
+
export const resolveConfigDirFromAlias = () => {
|
|
22
24
|
const home = homedir();
|
|
23
25
|
const rcFiles = [".zshrc", ".bashrc", ".zprofile", ".bash_profile", ".aliases"];
|
|
24
26
|
for (const rcFile of rcFiles) {
|
|
@@ -50,18 +52,20 @@ const resolve = () => {
|
|
|
50
52
|
};
|
|
51
53
|
export const buildArgs = (options, binaryPath) => {
|
|
52
54
|
const args = [binaryPath, "-p", "--output-format", "stream-json", "--input-format", "stream-json"];
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
55
|
+
const flag = (cond, name) => {
|
|
56
|
+
if (cond) {
|
|
57
|
+
args.push(name);
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
const kv = (value, name) => {
|
|
61
|
+
if (value) {
|
|
62
|
+
args.push(name, value);
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
flag(options.verbose !== false, "--verbose");
|
|
66
|
+
kv(options.model, "--model");
|
|
67
|
+
kv(options.systemPrompt, "--system-prompt");
|
|
68
|
+
kv(options.appendSystemPrompt, "--append-system-prompt");
|
|
65
69
|
if (options.allowedTools) {
|
|
66
70
|
if (options.allowedTools.length === 0) {
|
|
67
71
|
args.push("--tools", "");
|
|
@@ -76,53 +80,27 @@ export const buildArgs = (options, binaryPath) => {
|
|
|
76
80
|
if (options.maxBudgetUsd !== undefined) {
|
|
77
81
|
args.push("--max-budget-usd", String(options.maxBudgetUsd));
|
|
78
82
|
}
|
|
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
|
-
}
|
|
83
|
+
kv(options.resume, "--resume");
|
|
84
|
+
kv(options.mcpConfig, "--mcp-config");
|
|
85
|
+
flag(options.continueSession, "--continue");
|
|
86
|
+
kv(options.permissionMode, "--permission-mode");
|
|
91
87
|
if (options.addDirs && options.addDirs.length > 0) {
|
|
92
88
|
for (const dir of options.addDirs) {
|
|
93
89
|
args.push("--add-dir", dir);
|
|
94
90
|
}
|
|
95
91
|
}
|
|
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
|
-
}
|
|
92
|
+
kv(options.effort, "--effort");
|
|
93
|
+
flag(options.includeHookEvents, "--include-hook-events");
|
|
94
|
+
flag(options.includePartialMessages, "--include-partial-messages");
|
|
95
|
+
flag(options.bare, "--bare");
|
|
96
|
+
kv(options.jsonSchema, "--json-schema");
|
|
97
|
+
flag(options.forkSession, "--fork-session");
|
|
98
|
+
flag(options.noSessionPersistence, "--no-session-persistence");
|
|
99
|
+
kv(options.sessionId, "--session-id");
|
|
120
100
|
if (options.settingSources !== undefined) {
|
|
121
101
|
args.push("--setting-sources", options.settingSources);
|
|
122
102
|
}
|
|
123
|
-
|
|
124
|
-
args.push("--disable-slash-commands");
|
|
125
|
-
}
|
|
103
|
+
flag(options.disableSlashCommands, "--disable-slash-commands");
|
|
126
104
|
return args;
|
|
127
105
|
};
|
|
128
106
|
export const spawnClaude = (options) => {
|
|
@@ -133,13 +111,16 @@ export const spawnClaude = (options) => {
|
|
|
133
111
|
const needsEnv = resolved.aliasConfigDir || options.configDir || options.env;
|
|
134
112
|
let spawnEnv;
|
|
135
113
|
if (needsEnv) {
|
|
114
|
+
// Priority (lowest → highest): process.env < alias-detected config <
|
|
115
|
+
// user's explicit `options.env` < explicit `options.configDir`. User
|
|
116
|
+
// input always outranks the alias heuristic.
|
|
136
117
|
spawnEnv = { ...process.env };
|
|
137
|
-
if (options.env) {
|
|
138
|
-
Object.assign(spawnEnv, options.env);
|
|
139
|
-
}
|
|
140
118
|
if (resolved.aliasConfigDir) {
|
|
141
119
|
spawnEnv.CLAUDE_CONFIG_DIR = resolved.aliasConfigDir;
|
|
142
120
|
}
|
|
121
|
+
if (options.env) {
|
|
122
|
+
Object.assign(spawnEnv, options.env);
|
|
123
|
+
}
|
|
143
124
|
if (options.configDir) {
|
|
144
125
|
spawnEnv.CLAUDE_CONFIG_DIR = options.configDir;
|
|
145
126
|
}
|
package/dist/reader.d.ts
CHANGED
|
@@ -9,5 +9,11 @@ export interface IReaderOptions {
|
|
|
9
9
|
proc?: IClaudeProcess;
|
|
10
10
|
signal?: AbortSignal;
|
|
11
11
|
}
|
|
12
|
+
export interface IStderrDrain {
|
|
13
|
+
chunks: string[];
|
|
14
|
+
done: Promise<void>;
|
|
15
|
+
}
|
|
16
|
+
export declare const drainStderr: (proc: {
|
|
17
|
+
stderr: ReadableStream<Uint8Array>;
|
|
18
|
+
}) => IStderrDrain;
|
|
12
19
|
export declare function readNdjsonEvents(opts: IReaderOptions): AsyncGenerator<TRelayEvent>;
|
|
13
|
-
export type { TRelayEvent };
|
package/dist/reader.js
CHANGED
|
@@ -2,17 +2,68 @@ 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 { writer } from "./writer.js";
|
|
6
|
+
export const drainStderr = (proc) => {
|
|
7
|
+
const chunks = [];
|
|
8
|
+
const stderrReader = proc.stderr.getReader();
|
|
9
|
+
const decoder = new TextDecoder();
|
|
10
|
+
const done = (async () => {
|
|
11
|
+
try {
|
|
12
|
+
while (true) {
|
|
13
|
+
const { done: isDone, value } = await stderrReader.read();
|
|
14
|
+
if (isDone) {
|
|
15
|
+
break;
|
|
16
|
+
}
|
|
17
|
+
chunks.push(decoder.decode(value, { stream: true }));
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
// process exited
|
|
22
|
+
}
|
|
23
|
+
finally {
|
|
24
|
+
// Flush any trailing partial multibyte sequence.
|
|
25
|
+
const tail = decoder.decode();
|
|
26
|
+
if (tail) {
|
|
27
|
+
chunks.push(tail);
|
|
28
|
+
}
|
|
29
|
+
stderrReader.releaseLock();
|
|
30
|
+
}
|
|
31
|
+
})().catch(() => { });
|
|
32
|
+
return { chunks, done };
|
|
33
|
+
};
|
|
5
34
|
export async function* readNdjsonEvents(opts) {
|
|
6
35
|
const { reader, translator, signal } = opts;
|
|
7
36
|
const decoder = new TextDecoder();
|
|
8
37
|
let buffer = "";
|
|
9
38
|
let timeoutId;
|
|
10
39
|
let turnComplete = false;
|
|
40
|
+
let abortReject;
|
|
41
|
+
const abortPromise = new Promise((_, reject) => {
|
|
42
|
+
abortReject = reject;
|
|
43
|
+
});
|
|
44
|
+
// Swallow unhandled rejection if nothing ever races against this promise.
|
|
45
|
+
abortPromise.catch(() => { });
|
|
46
|
+
// Single resettable timeout shared across all iterations — avoids leaking
|
|
47
|
+
// a new Promise + setTimeout per read loop.
|
|
48
|
+
let timeoutReject;
|
|
49
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
50
|
+
timeoutReject = reject;
|
|
51
|
+
});
|
|
52
|
+
timeoutPromise.catch(() => { });
|
|
53
|
+
const resetReadTimeout = () => {
|
|
54
|
+
if (timeoutId) {
|
|
55
|
+
clearTimeout(timeoutId);
|
|
56
|
+
}
|
|
57
|
+
timeoutId = setTimeout(() => {
|
|
58
|
+
timeoutReject?.(new TimeoutError(`No data received within ${TIMEOUTS.defaultAbortMs}ms`));
|
|
59
|
+
}, TIMEOUTS.defaultAbortMs);
|
|
60
|
+
};
|
|
11
61
|
const abortHandler = signal
|
|
12
62
|
? () => {
|
|
63
|
+
abortReject?.(new AbortError());
|
|
13
64
|
if (opts.proc) {
|
|
14
65
|
try {
|
|
15
|
-
opts.proc.write(
|
|
66
|
+
opts.proc.write(writer.abort());
|
|
16
67
|
}
|
|
17
68
|
catch {
|
|
18
69
|
// stdin closed
|
|
@@ -32,13 +83,8 @@ export async function* readNdjsonEvents(opts) {
|
|
|
32
83
|
if (signal?.aborted) {
|
|
33
84
|
throw new AbortError();
|
|
34
85
|
}
|
|
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);
|
|
86
|
+
resetReadTimeout();
|
|
87
|
+
const readResult = await Promise.race([reader.read(), timeoutPromise, abortPromise]);
|
|
42
88
|
const { done, value } = readResult;
|
|
43
89
|
if (done) {
|
|
44
90
|
break;
|
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) {
|
|
@@ -30,15 +31,6 @@ export const whichSync = (name) => {
|
|
|
30
31
|
}
|
|
31
32
|
};
|
|
32
33
|
export const fileExists = (path) => {
|
|
33
|
-
if (isBun) {
|
|
34
|
-
try {
|
|
35
|
-
accessSync(path, constants.X_OK);
|
|
36
|
-
return statSync(path).size > 0;
|
|
37
|
-
}
|
|
38
|
-
catch {
|
|
39
|
-
return false;
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
34
|
try {
|
|
43
35
|
accessSync(path, constants.X_OK);
|
|
44
36
|
return statSync(path).size > 0;
|
|
@@ -73,43 +65,20 @@ const spawnBun = (args, opts) => {
|
|
|
73
65
|
pid: proc.pid,
|
|
74
66
|
};
|
|
75
67
|
};
|
|
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
|
-
};
|
|
68
|
+
const toWeb = (readable) => Readable.toWeb(readable);
|
|
103
69
|
const spawnNode = (args, opts) => {
|
|
104
70
|
const [cmd, ...rest] = args;
|
|
105
71
|
if (!cmd) {
|
|
106
|
-
throw new
|
|
72
|
+
throw new ProcessError("No command specified");
|
|
107
73
|
}
|
|
108
74
|
const child = nodeSpawn(cmd, rest, {
|
|
109
75
|
cwd: opts.cwd,
|
|
110
76
|
stdio: ["pipe", "pipe", "pipe"],
|
|
111
77
|
env: opts.env,
|
|
112
78
|
});
|
|
79
|
+
if (child.pid === undefined) {
|
|
80
|
+
throw new ProcessError(`Failed to spawn ${cmd}: no PID assigned`);
|
|
81
|
+
}
|
|
113
82
|
const exited = new Promise((resolve, reject) => {
|
|
114
83
|
child.on("exit", (code) => {
|
|
115
84
|
resolve(code ?? 1);
|
|
@@ -119,18 +88,21 @@ const spawnNode = (args, opts) => {
|
|
|
119
88
|
return {
|
|
120
89
|
stdin: {
|
|
121
90
|
write: (data) => {
|
|
122
|
-
child.stdin
|
|
91
|
+
if (!child.stdin || child.stdin.destroyed) {
|
|
92
|
+
throw new ProcessError("Cannot write: stdin is not writable");
|
|
93
|
+
}
|
|
94
|
+
child.stdin.write(data);
|
|
123
95
|
},
|
|
124
96
|
end: () => {
|
|
125
97
|
child.stdin?.end();
|
|
126
98
|
},
|
|
127
99
|
},
|
|
128
|
-
stdout: child.stdout ?
|
|
129
|
-
stderr: child.stderr ?
|
|
100
|
+
stdout: child.stdout ? toWeb(child.stdout) : new ReadableStream(),
|
|
101
|
+
stderr: child.stderr ? toWeb(child.stderr) : new ReadableStream(),
|
|
130
102
|
kill: () => {
|
|
131
103
|
child.kill();
|
|
132
104
|
},
|
|
133
105
|
exited,
|
|
134
|
-
pid: child.pid
|
|
106
|
+
pid: child.pid,
|
|
135
107
|
};
|
|
136
108
|
};
|
package/dist/session.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { LIMITS, TIMEOUTS } from "./constants.js";
|
|
1
|
+
import { LIMITS, RESPAWN_BACKOFF_MS, TIMEOUTS } from "./constants.js";
|
|
2
2
|
import { createCostTracker } from "./cost.js";
|
|
3
3
|
import { AbortError, BudgetExceededError, ClaudeError, isTransientError, KnownError, ProcessError, TimeoutError } from "./errors.js";
|
|
4
4
|
import { createTranslator } from "./parser/translator.js";
|
|
5
5
|
import { buildResult } from "./pipeline.js";
|
|
6
6
|
import { spawnClaude } from "./process.js";
|
|
7
|
-
import { readNdjsonEvents } from "./reader.js";
|
|
7
|
+
import { drainStderr, readNdjsonEvents } from "./reader.js";
|
|
8
8
|
import { createToolHandler } from "./tools/handler.js";
|
|
9
9
|
import { writer } from "./writer.js";
|
|
10
10
|
const gracefulKill = async (p) => {
|
|
@@ -62,29 +62,7 @@ export const createSession = (options = {}) => {
|
|
|
62
62
|
reader = undefined;
|
|
63
63
|
};
|
|
64
64
|
let lastStderrChunks = [];
|
|
65
|
-
|
|
66
|
-
const chunks = [];
|
|
67
|
-
lastStderrChunks = chunks;
|
|
68
|
-
const stderrReader = p.stderr.getReader();
|
|
69
|
-
const decoder = new TextDecoder();
|
|
70
|
-
(async () => {
|
|
71
|
-
try {
|
|
72
|
-
while (true) {
|
|
73
|
-
const { done, value } = await stderrReader.read();
|
|
74
|
-
if (done) {
|
|
75
|
-
break;
|
|
76
|
-
}
|
|
77
|
-
chunks.push(decoder.decode(value, { stream: true }));
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
catch {
|
|
81
|
-
// process exited
|
|
82
|
-
}
|
|
83
|
-
finally {
|
|
84
|
-
stderrReader.releaseLock();
|
|
85
|
-
}
|
|
86
|
-
})().catch(() => { });
|
|
87
|
-
};
|
|
65
|
+
let lastDrainDone;
|
|
88
66
|
const getStderrText = () => lastStderrChunks.join("").trim();
|
|
89
67
|
const killProc = () => {
|
|
90
68
|
if (proc) {
|
|
@@ -105,7 +83,16 @@ export const createSession = (options = {}) => {
|
|
|
105
83
|
const spawnOpts = resumeId ? { prompt, ...options, resume: resumeId } : { prompt, ...options };
|
|
106
84
|
proc = spawnClaude(spawnOpts);
|
|
107
85
|
reader = proc.stdout.getReader();
|
|
108
|
-
drainStderr(proc);
|
|
86
|
+
const drain = drainStderr(proc);
|
|
87
|
+
lastStderrChunks = drain.chunks;
|
|
88
|
+
lastDrainDone = drain.done;
|
|
89
|
+
};
|
|
90
|
+
const respawnBackoff = async () => {
|
|
91
|
+
const idx = Math.min(consecutiveCrashes, RESPAWN_BACKOFF_MS.length) - 1;
|
|
92
|
+
const delay = idx >= 0 ? RESPAWN_BACKOFF_MS[idx] : 0;
|
|
93
|
+
if (delay) {
|
|
94
|
+
await new Promise((r) => setTimeout(r, delay));
|
|
95
|
+
}
|
|
109
96
|
};
|
|
110
97
|
const readUntilTurnComplete = async (signal) => {
|
|
111
98
|
if (!proc || !reader) {
|
|
@@ -135,8 +122,25 @@ export const createSession = (options = {}) => {
|
|
|
135
122
|
if (signal?.aborted) {
|
|
136
123
|
throw new AbortError();
|
|
137
124
|
}
|
|
125
|
+
// stdout closed → process is dying. Race `exited` against a short
|
|
126
|
+
// timeout so a zombie/unreaped child doesn't hang us. If exited doesn't
|
|
127
|
+
// resolve in time we leave exitCode undefined (→ non-transient).
|
|
128
|
+
let exitCode;
|
|
129
|
+
if (proc) {
|
|
130
|
+
const live = proc;
|
|
131
|
+
exitCode = await Promise.race([
|
|
132
|
+
live.exited,
|
|
133
|
+
new Promise((r) => setTimeout(() => r(undefined), TIMEOUTS.gracefulExitMs)),
|
|
134
|
+
]);
|
|
135
|
+
if (exitCode === undefined) {
|
|
136
|
+
live.kill();
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
if (lastDrainDone) {
|
|
140
|
+
await Promise.race([lastDrainDone, new Promise((r) => setTimeout(r, 500))]);
|
|
141
|
+
}
|
|
138
142
|
const stderrMsg = getStderrText();
|
|
139
|
-
throw new ProcessError(stderrMsg || "Process exited without completing the turn");
|
|
143
|
+
throw new ProcessError(stderrMsg || "Process exited without completing the turn", exitCode);
|
|
140
144
|
}
|
|
141
145
|
return events;
|
|
142
146
|
};
|
|
@@ -155,30 +159,25 @@ export const createSession = (options = {}) => {
|
|
|
155
159
|
}
|
|
156
160
|
}
|
|
157
161
|
let events;
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
if (error instanceof AbortError || error instanceof TimeoutError) {
|
|
163
|
-
killProc();
|
|
164
|
-
throw error;
|
|
162
|
+
while (true) {
|
|
163
|
+
try {
|
|
164
|
+
events = await readUntilTurnComplete(options.signal);
|
|
165
|
+
break;
|
|
165
166
|
}
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
events = await readUntilTurnComplete(options.signal);
|
|
167
|
+
catch (error) {
|
|
168
|
+
if (error instanceof AbortError || error instanceof TimeoutError) {
|
|
169
|
+
killProc();
|
|
170
|
+
throw error;
|
|
171
171
|
}
|
|
172
|
-
|
|
172
|
+
if (!isTransientError(error) || consecutiveCrashes >= LIMITS.maxRespawnAttempts) {
|
|
173
173
|
killProc();
|
|
174
174
|
translator.reset();
|
|
175
|
-
throw
|
|
175
|
+
throw error;
|
|
176
176
|
}
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
throw error;
|
|
177
|
+
consecutiveCrashes++;
|
|
178
|
+
await respawnBackoff();
|
|
179
|
+
spawnFresh(prompt, currentSessionId);
|
|
180
|
+
// Loop to retry; stops when budget exhausted or turn completes.
|
|
182
181
|
}
|
|
183
182
|
}
|
|
184
183
|
consecutiveCrashes = 0;
|
|
@@ -201,19 +200,32 @@ export const createSession = (options = {}) => {
|
|
|
201
200
|
}
|
|
202
201
|
const prev = inFlight ?? Promise.resolve();
|
|
203
202
|
const run = prev
|
|
204
|
-
.catch((
|
|
205
|
-
|
|
206
|
-
|
|
203
|
+
.catch(() => {
|
|
204
|
+
// Prior ask failure shouldn't prevent this one from running. Fatal
|
|
205
|
+
// errors (KnownError/BudgetExceededError) set `closed` in the .catch
|
|
206
|
+
// below, which the sync check above picks up on the NEXT ask.
|
|
207
|
+
})
|
|
208
|
+
.then(() => {
|
|
209
|
+
if (closed) {
|
|
210
|
+
throw new ClaudeError("Session is closed");
|
|
207
211
|
}
|
|
212
|
+
return doAsk(prompt);
|
|
208
213
|
})
|
|
209
|
-
.
|
|
214
|
+
.catch((error) => {
|
|
215
|
+
if (error instanceof KnownError || error instanceof BudgetExceededError) {
|
|
216
|
+
closed = true;
|
|
217
|
+
}
|
|
218
|
+
throw error;
|
|
219
|
+
});
|
|
210
220
|
inFlight = run;
|
|
211
221
|
return run;
|
|
212
222
|
};
|
|
213
223
|
const close = async () => {
|
|
214
224
|
closed = true;
|
|
215
225
|
if (inFlight) {
|
|
216
|
-
|
|
226
|
+
// Cap the wait: a stuck reader.read() inside the queued ask would
|
|
227
|
+
// otherwise hang close() forever before gracefulKill gets a chance.
|
|
228
|
+
await Promise.race([inFlight.catch(() => { }), new Promise((r) => setTimeout(r, TIMEOUTS.gracefulExitMs))]);
|
|
217
229
|
inFlight = undefined;
|
|
218
230
|
}
|
|
219
231
|
if (proc) {
|
package/dist/stream.js
CHANGED
|
@@ -3,53 +3,41 @@ import { AbortError, ClaudeError, ProcessError } from "./errors.js";
|
|
|
3
3
|
import { createTranslator } from "./parser/translator.js";
|
|
4
4
|
import { buildResult, extractText } from "./pipeline.js";
|
|
5
5
|
import { spawnClaude } from "./process.js";
|
|
6
|
-
import { readNdjsonEvents } from "./reader.js";
|
|
6
|
+
import { drainStderr, readNdjsonEvents } from "./reader.js";
|
|
7
7
|
import { createToolHandler } from "./tools/handler.js";
|
|
8
|
-
const drainStderr = (proc) => {
|
|
9
|
-
const chunks = [];
|
|
10
|
-
const stderrReader = proc.stderr.getReader();
|
|
11
|
-
const decoder = new TextDecoder();
|
|
12
|
-
const done = (async () => {
|
|
13
|
-
try {
|
|
14
|
-
while (true) {
|
|
15
|
-
const { done: isDone, value } = await stderrReader.read();
|
|
16
|
-
if (isDone) {
|
|
17
|
-
break;
|
|
18
|
-
}
|
|
19
|
-
chunks.push(decoder.decode(value, { stream: true }));
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
catch {
|
|
23
|
-
// process exited
|
|
24
|
-
}
|
|
25
|
-
finally {
|
|
26
|
-
stderrReader.releaseLock();
|
|
27
|
-
}
|
|
28
|
-
})().catch(() => { });
|
|
29
|
-
return { chunks, done };
|
|
30
|
-
};
|
|
31
8
|
export const createStream = (prompt, options = {}) => {
|
|
32
9
|
if (options.signal?.aborted) {
|
|
33
10
|
throw new AbortError();
|
|
34
11
|
}
|
|
35
|
-
const proc = spawnClaude({ prompt, ...options });
|
|
36
|
-
const stderr = drainStderr(proc);
|
|
37
12
|
const translator = createTranslator();
|
|
38
13
|
const toolHandler = options.tools ? createToolHandler(options.tools) : undefined;
|
|
39
14
|
const costTracker = createCostTracker({
|
|
40
15
|
maxCostUsd: options.maxCostUsd,
|
|
41
16
|
onCostUpdate: options.onCostUpdate,
|
|
42
17
|
});
|
|
18
|
+
let proc;
|
|
19
|
+
let stderr;
|
|
43
20
|
let cachedGenerator;
|
|
21
|
+
const ensureSpawned = () => {
|
|
22
|
+
if (!proc) {
|
|
23
|
+
if (options.signal?.aborted) {
|
|
24
|
+
throw new AbortError();
|
|
25
|
+
}
|
|
26
|
+
proc = spawnClaude({ prompt, ...options });
|
|
27
|
+
stderr = drainStderr(proc);
|
|
28
|
+
}
|
|
29
|
+
return proc;
|
|
30
|
+
};
|
|
44
31
|
const generate = async function* () {
|
|
45
|
-
const
|
|
32
|
+
const p = ensureSpawned();
|
|
33
|
+
const stdoutReader = p.stdout.getReader();
|
|
46
34
|
let turnComplete = false;
|
|
47
35
|
try {
|
|
48
36
|
for await (const event of readNdjsonEvents({
|
|
49
37
|
reader: stdoutReader,
|
|
50
38
|
translator,
|
|
51
39
|
toolHandler,
|
|
52
|
-
proc,
|
|
40
|
+
proc: p,
|
|
53
41
|
signal: options.signal,
|
|
54
42
|
})) {
|
|
55
43
|
if (event.type === "turn_complete") {
|
|
@@ -60,22 +48,21 @@ export const createStream = (prompt, options = {}) => {
|
|
|
60
48
|
yield event;
|
|
61
49
|
}
|
|
62
50
|
if (!turnComplete) {
|
|
63
|
-
const exitCode = await
|
|
51
|
+
const exitCode = await p.exited;
|
|
64
52
|
if (exitCode !== 0) {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
const stderrText = stderr.chunks.join("").trim();
|
|
68
|
-
if (stderrText) {
|
|
69
|
-
errorMessage = stderrText;
|
|
53
|
+
if (stderr) {
|
|
54
|
+
await stderr.done;
|
|
70
55
|
}
|
|
71
|
-
|
|
56
|
+
const stderrText = stderr ? stderr.chunks.join("").trim() : "";
|
|
57
|
+
const exitMsg = stderrText || `Claude process exited with code ${exitCode}`;
|
|
58
|
+
throw new ProcessError(exitMsg, exitCode);
|
|
72
59
|
}
|
|
73
60
|
throw new ProcessError("Process exited without completing the turn");
|
|
74
61
|
}
|
|
75
62
|
}
|
|
76
63
|
finally {
|
|
77
64
|
stdoutReader.releaseLock();
|
|
78
|
-
|
|
65
|
+
p.kill();
|
|
79
66
|
}
|
|
80
67
|
};
|
|
81
68
|
const bufferedEvents = [];
|
|
@@ -85,9 +72,10 @@ export const createStream = (prompt, options = {}) => {
|
|
|
85
72
|
if (cachedGenerator) {
|
|
86
73
|
throw new ClaudeError("Cannot call text()/cost()/result() after iterating with for-await. Use one or the other.");
|
|
87
74
|
}
|
|
75
|
+
const gen = generate();
|
|
76
|
+
cachedGenerator = gen;
|
|
88
77
|
consumePromise = (async () => {
|
|
89
|
-
|
|
90
|
-
for await (const event of { [Symbol.asyncIterator]: () => cachedGenerator }) {
|
|
78
|
+
for await (const event of gen) {
|
|
91
79
|
bufferedEvents.push(event);
|
|
92
80
|
}
|
|
93
81
|
})();
|
|
@@ -108,7 +96,10 @@ export const createStream = (prompt, options = {}) => {
|
|
|
108
96
|
return buildResult(bufferedEvents, costTracker, sessionId);
|
|
109
97
|
};
|
|
110
98
|
const cleanup = () => {
|
|
111
|
-
if
|
|
99
|
+
// Always kill if a proc was ever spawned — the generator's finally may not
|
|
100
|
+
// have run yet (e.g., iterator created but never ticked). Redundant kill
|
|
101
|
+
// on an already-exited process is a harmless ESRCH.
|
|
102
|
+
if (proc) {
|
|
112
103
|
proc.kill();
|
|
113
104
|
}
|
|
114
105
|
};
|
package/dist/tools/handler.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export const createToolHandler = (options = {}) => {
|
|
2
|
-
const { allowed, blocked, onToolUse } = options;
|
|
2
|
+
const { allowed, blocked, onToolUse, onError } = options;
|
|
3
3
|
const allowedSet = allowed ? new Set(allowed) : undefined;
|
|
4
4
|
const blockedSet = blocked ? new Set(blocked) : undefined;
|
|
5
5
|
const decide = async (tool) => {
|
|
@@ -9,10 +9,18 @@ export const createToolHandler = (options = {}) => {
|
|
|
9
9
|
if (allowedSet && !allowedSet.has(tool.toolName)) {
|
|
10
10
|
return "deny";
|
|
11
11
|
}
|
|
12
|
-
if (onToolUse) {
|
|
13
|
-
return
|
|
12
|
+
if (!onToolUse) {
|
|
13
|
+
return "approve";
|
|
14
|
+
}
|
|
15
|
+
try {
|
|
16
|
+
return await onToolUse(tool);
|
|
17
|
+
}
|
|
18
|
+
catch (error) {
|
|
19
|
+
if (!onError) {
|
|
20
|
+
throw error;
|
|
21
|
+
}
|
|
22
|
+
return onError(error, tool);
|
|
14
23
|
}
|
|
15
|
-
return "approve";
|
|
16
24
|
};
|
|
17
25
|
return { decide };
|
|
18
26
|
};
|
package/dist/types/options.d.ts
CHANGED
|
@@ -5,6 +5,7 @@ export interface IToolHandler {
|
|
|
5
5
|
allowed?: string[];
|
|
6
6
|
blocked?: string[];
|
|
7
7
|
onToolUse?: (tool: TToolUseEvent) => Promise<TToolDecision>;
|
|
8
|
+
onError?: (error: unknown, tool: TToolUseEvent) => TToolDecision | Promise<TToolDecision>;
|
|
8
9
|
}
|
|
9
10
|
export interface IClaudeOptions {
|
|
10
11
|
cwd?: string;
|
|
@@ -14,7 +15,17 @@ export interface IClaudeOptions {
|
|
|
14
15
|
allowedTools?: string[];
|
|
15
16
|
disallowedTools?: string[];
|
|
16
17
|
tools?: IToolHandler;
|
|
18
|
+
/**
|
|
19
|
+
* SDK-side budget limit, evaluated after each turn. Throws `BudgetExceededError`
|
|
20
|
+
* and kills the process when `total_cost_usd` exceeds this value. `0` means
|
|
21
|
+
* "disallow any spend" (useful for tests).
|
|
22
|
+
*/
|
|
17
23
|
maxCostUsd?: number;
|
|
24
|
+
/**
|
|
25
|
+
* CLI-level budget forwarded as `--max-budget-usd`. Enforced by the Claude
|
|
26
|
+
* binary itself, independent of {@link IClaudeOptions.maxCostUsd}. Either
|
|
27
|
+
* can fire first; set both for belt-and-suspenders.
|
|
28
|
+
*/
|
|
18
29
|
maxBudgetUsd?: number;
|
|
19
30
|
onCostUpdate?: (cost: TCostSnapshot) => void;
|
|
20
31
|
signal?: AbortSignal;
|