@pivanov/claude-wire 0.0.3 → 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 +4 -2
- package/dist/async.d.ts +10 -0
- package/dist/async.js +27 -0
- package/dist/constants.d.ts +2 -0
- package/dist/constants.js +7 -0
- package/dist/cost.d.ts +2 -0
- package/dist/cost.js +5 -2
- package/dist/errors.d.ts +2 -2
- package/dist/errors.js +14 -8
- package/dist/index.d.ts +5 -4
- package/dist/index.js +3 -3
- package/dist/parser/translator.js +6 -1
- package/dist/pipeline.d.ts +14 -4
- package/dist/pipeline.js +38 -18
- package/dist/process.d.ts +15 -3
- package/dist/process.js +86 -25
- package/dist/reader.d.ts +3 -0
- package/dist/reader.js +32 -30
- package/dist/runtime.d.ts +5 -4
- package/dist/runtime.js +8 -5
- package/dist/session.d.ts +30 -2
- package/dist/session.js +129 -69
- package/dist/stream.js +60 -25
- package/dist/tools/handler.d.ts +1 -0
- package/dist/tools/registry.d.ts +4 -2
- package/dist/tools/registry.js +7 -4
- package/dist/types/options.d.ts +42 -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/README.md
CHANGED
|
@@ -25,7 +25,7 @@ console.log(result.costUsd); // 0.0084
|
|
|
25
25
|
- **Cost tracking** - per-request budgets with auto-abort
|
|
26
26
|
- **Fully typed** - discriminated union events, full IntelliSense
|
|
27
27
|
- **Resilient** - auto-respawn, transient error detection, AbortSignal
|
|
28
|
-
- **Zero dependencies** -
|
|
28
|
+
- **Zero dependencies** - 25.6 kB gzipped
|
|
29
29
|
|
|
30
30
|
## Install
|
|
31
31
|
|
|
@@ -37,6 +37,8 @@ npm install @pivanov/claude-wire
|
|
|
37
37
|
|
|
38
38
|
Requires [Claude Code CLI](https://claude.ai/download) installed and authenticated. Runs on [Bun](https://bun.sh) >= 1.0 or Node.js >= 22.
|
|
39
39
|
|
|
40
|
+
> **Platform:** POSIX only (macOS, Linux, WSL). Native Windows isn't supported yet -- binary resolution relies on `which` and POSIX path conventions.
|
|
41
|
+
|
|
40
42
|
> This SDK wraps Claude Code's `--output-format stream-json` protocol, which is not officially documented by Anthropic and may change between releases.
|
|
41
43
|
|
|
42
44
|
## Documentation
|
|
@@ -65,7 +67,7 @@ apps/examples/ interactive example runner
|
|
|
65
67
|
|
|
66
68
|
```bash
|
|
67
69
|
bun install
|
|
68
|
-
bun run test #
|
|
70
|
+
bun run test # 192 tests
|
|
69
71
|
bun run typecheck
|
|
70
72
|
bun run lint
|
|
71
73
|
bun run docs:dev # local docs server
|
package/dist/async.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Races `promise` against a timeout. When the timer fires first, resolves
|
|
3
|
+
* with `onTimeout()` (or `undefined` if omitted). The caller decides
|
|
4
|
+
* whether that's an acceptable fallback or a signal to kill/retry.
|
|
5
|
+
*
|
|
6
|
+
* Intentionally does NOT reject on timeout -- callers usually have a
|
|
7
|
+
* specific action to take (kill the process, bail early with undefined)
|
|
8
|
+
* that a thrown error would force into a catch branch for no reason.
|
|
9
|
+
*/
|
|
10
|
+
export declare const withTimeout: <T, F = undefined>(promise: Promise<T>, ms: number, onTimeout?: () => F) => Promise<T | F>;
|
package/dist/async.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// Small async helpers shared between session.ts and stream.ts. Kept in
|
|
2
|
+
// their own file because both consumers care about the *shape* of the
|
|
3
|
+
// timeout pattern, not its underlying mechanics -- drop a single
|
|
4
|
+
// withTimeout() next to whatever else needs it rather than carrying the
|
|
5
|
+
// Promise.race idiom inline at every site.
|
|
6
|
+
/**
|
|
7
|
+
* Races `promise` against a timeout. When the timer fires first, resolves
|
|
8
|
+
* with `onTimeout()` (or `undefined` if omitted). The caller decides
|
|
9
|
+
* whether that's an acceptable fallback or a signal to kill/retry.
|
|
10
|
+
*
|
|
11
|
+
* Intentionally does NOT reject on timeout -- callers usually have a
|
|
12
|
+
* specific action to take (kill the process, bail early with undefined)
|
|
13
|
+
* that a thrown error would force into a catch branch for no reason.
|
|
14
|
+
*/
|
|
15
|
+
export const withTimeout = (promise, ms, onTimeout) => {
|
|
16
|
+
let timer;
|
|
17
|
+
const timeout = new Promise((resolve) => {
|
|
18
|
+
timer = setTimeout(() => {
|
|
19
|
+
resolve(onTimeout ? onTimeout() : undefined);
|
|
20
|
+
}, ms);
|
|
21
|
+
});
|
|
22
|
+
return Promise.race([promise, timeout]).finally(() => {
|
|
23
|
+
if (timer) {
|
|
24
|
+
clearTimeout(timer);
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
};
|
package/dist/constants.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export declare const TIMEOUTS: {
|
|
2
2
|
readonly defaultAbortMs: 300000;
|
|
3
3
|
readonly gracefulExitMs: 5000;
|
|
4
|
+
readonly stderrDrainGraceMs: 500;
|
|
4
5
|
};
|
|
5
6
|
export declare const LIMITS: {
|
|
6
7
|
readonly maxRespawnAttempts: 3;
|
|
@@ -9,6 +10,7 @@ export declare const LIMITS: {
|
|
|
9
10
|
readonly fingerprintTextLen: 64;
|
|
10
11
|
};
|
|
11
12
|
export declare const RESPAWN_BACKOFF_MS: readonly [500, 1000, 2000];
|
|
13
|
+
export declare const MAX_BACKOFF_INDEX: 3;
|
|
12
14
|
export declare const BINARY: {
|
|
13
15
|
readonly name: "claude";
|
|
14
16
|
readonly commonPaths: readonly [`${string}/.local/bin/claude`, `${string}/.claude/bin/claude`, "/usr/local/bin/claude", "/opt/homebrew/bin/claude"];
|
package/dist/constants.js
CHANGED
|
@@ -2,6 +2,9 @@ import { homedir } from "node:os";
|
|
|
2
2
|
export const TIMEOUTS = {
|
|
3
3
|
defaultAbortMs: 300_000,
|
|
4
4
|
gracefulExitMs: 5_000,
|
|
5
|
+
// Grace period for stderr drain to catch up before an error is thrown so
|
|
6
|
+
// the error message carries the CLI's actual complaint instead of "".
|
|
7
|
+
stderrDrainGraceMs: 500,
|
|
5
8
|
};
|
|
6
9
|
export const LIMITS = {
|
|
7
10
|
maxRespawnAttempts: 3,
|
|
@@ -11,6 +14,10 @@ export const LIMITS = {
|
|
|
11
14
|
};
|
|
12
15
|
// Respawn backoff in ms, indexed by consecutiveCrashes (1st=500ms, 2nd=1s, 3rd=2s).
|
|
13
16
|
export const RESPAWN_BACKOFF_MS = [500, 1000, 2000];
|
|
17
|
+
// Highest index into RESPAWN_BACKOFF_MS[]. Used by respawnBackoff() to
|
|
18
|
+
// clamp the delay lookup to the last defined backoff when crashes exceed
|
|
19
|
+
// the table length -- keeps the table and its bound co-located.
|
|
20
|
+
export const MAX_BACKOFF_INDEX = RESPAWN_BACKOFF_MS.length;
|
|
14
21
|
const home = homedir();
|
|
15
22
|
export const BINARY = {
|
|
16
23
|
name: "claude",
|
package/dist/cost.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { TCostSnapshot } from "./types/results.js";
|
|
2
|
+
import type { TWarn } from "./warnings.js";
|
|
2
3
|
export interface ICostTracker {
|
|
3
4
|
update: (totalCostUsd: number, totalInputTokens: number, totalOutputTokens: number) => void;
|
|
4
5
|
snapshot: () => TCostSnapshot;
|
|
@@ -8,5 +9,6 @@ export interface ICostTracker {
|
|
|
8
9
|
export interface ICostTrackerOptions {
|
|
9
10
|
maxCostUsd?: number;
|
|
10
11
|
onCostUpdate?: (cost: TCostSnapshot) => void;
|
|
12
|
+
onWarning?: TWarn;
|
|
11
13
|
}
|
|
12
14
|
export declare const createCostTracker: (options?: ICostTrackerOptions) => ICostTracker;
|
package/dist/cost.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { BudgetExceededError } from "./errors.js";
|
|
2
|
+
import { assertPositiveNumber } from "./validation.js";
|
|
3
|
+
import { createWarn } from "./warnings.js";
|
|
2
4
|
export const createCostTracker = (options = {}) => {
|
|
3
5
|
assertPositiveNumber(options.maxCostUsd, "maxCostUsd");
|
|
6
|
+
const warn = createWarn(options.onWarning);
|
|
4
7
|
let totalUsd = 0;
|
|
5
8
|
let inputTokens = 0;
|
|
6
9
|
let outputTokens = 0;
|
|
@@ -18,7 +21,7 @@ export const createCostTracker = (options = {}) => {
|
|
|
18
21
|
options.onCostUpdate(snapshot());
|
|
19
22
|
}
|
|
20
23
|
catch (error) {
|
|
21
|
-
|
|
24
|
+
warn("onCostUpdate callback threw", error);
|
|
22
25
|
}
|
|
23
26
|
}
|
|
24
27
|
};
|
package/dist/errors.d.ts
CHANGED
|
@@ -16,7 +16,7 @@ export declare class ProcessError extends ClaudeError {
|
|
|
16
16
|
readonly exitCode?: number | undefined;
|
|
17
17
|
constructor(message: string, exitCode?: number | undefined);
|
|
18
18
|
}
|
|
19
|
-
export declare const KNOWN_ERROR_CODES: readonly ["not-authenticated", "binary-not-found", "
|
|
19
|
+
export declare const KNOWN_ERROR_CODES: readonly ["not-authenticated", "binary-not-found", "permission-denied", "retry-exhausted"];
|
|
20
20
|
export type TKnownErrorCode = (typeof KNOWN_ERROR_CODES)[number];
|
|
21
21
|
export declare class KnownError extends ClaudeError {
|
|
22
22
|
readonly code: TKnownErrorCode;
|
|
@@ -25,4 +25,4 @@ export declare class KnownError extends ClaudeError {
|
|
|
25
25
|
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
|
-
export declare const
|
|
28
|
+
export declare const processExitedEarly: (stderr: string, exitCode?: number) => ProcessError;
|
package/dist/errors.js
CHANGED
|
@@ -34,7 +34,10 @@ export class ProcessError extends ClaudeError {
|
|
|
34
34
|
this.name = "ProcessError";
|
|
35
35
|
}
|
|
36
36
|
}
|
|
37
|
-
|
|
37
|
+
// Only codes the SDK actually constructs are listed. Add a new code here
|
|
38
|
+
// alongside the throw site that needs it -- aspirational entries give
|
|
39
|
+
// consumers false confidence that they can pattern-match on them.
|
|
40
|
+
export const KNOWN_ERROR_CODES = ["not-authenticated", "binary-not-found", "permission-denied", "retry-exhausted"];
|
|
38
41
|
export class KnownError extends ClaudeError {
|
|
39
42
|
code;
|
|
40
43
|
constructor(code, message) {
|
|
@@ -46,7 +49,11 @@ export class KnownError extends ClaudeError {
|
|
|
46
49
|
export const isKnownError = (error) => {
|
|
47
50
|
return error instanceof KnownError;
|
|
48
51
|
};
|
|
49
|
-
|
|
52
|
+
// Network-level transients (ECONNRESET/REFUSED/ABORTED, ENETUNREACH, EHOSTUNREACH),
|
|
53
|
+
// DNS transients (EAI_AGAIN), pipe resets (EPIPE/SIGPIPE, broken pipe), fetch
|
|
54
|
+
// errors, ad-hoc "socket hang up" messages from node, and Anthropic
|
|
55
|
+
// overloaded_error which the CLI bubbles up verbatim for 529 responses.
|
|
56
|
+
const TRANSIENT_PATTERN = /fetch failed|ECONNREFUSED|ETIMEDOUT|ECONNRESET|ECONNABORTED|ENETUNREACH|EHOSTUNREACH|EAI_AGAIN|network error|network timeout|EPIPE|SIGPIPE|broken pipe|socket hang up|overloaded_error/i;
|
|
50
57
|
// Exit codes we treat as transient: 137 = SIGKILL (OOM), 141 = SIGPIPE,
|
|
51
58
|
// 143 = SIGTERM. Non-zero normal exits (e.g. 1) stay non-transient.
|
|
52
59
|
const TRANSIENT_EXIT_CODES = new Set([137, 141, 143]);
|
|
@@ -63,9 +70,8 @@ export const isTransientError = (error) => {
|
|
|
63
70
|
export const errorMessage = (error) => {
|
|
64
71
|
return error instanceof Error ? error.message : String(error);
|
|
65
72
|
};
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
};
|
|
73
|
+
// Shared error factory for the "process died before emitting turn_complete"
|
|
74
|
+
// case. session.ts + stream.ts both need this; the string used to be
|
|
75
|
+
// duplicated verbatim, which drifted at least once. Prefix stderr when
|
|
76
|
+
// available because CLI error output is the most actionable signal.
|
|
77
|
+
export const processExitedEarly = (stderr, exitCode) => new ProcessError(stderr || "Process exited without completing the turn", exitCode);
|
package/dist/index.d.ts
CHANGED
|
@@ -4,13 +4,13 @@ export { BINARY, LIMITS, TIMEOUTS } from "./constants.js";
|
|
|
4
4
|
export type { ICostTracker, ICostTrackerOptions } from "./cost.js";
|
|
5
5
|
export { createCostTracker } from "./cost.js";
|
|
6
6
|
export type { TKnownErrorCode } from "./errors.js";
|
|
7
|
-
export { AbortError,
|
|
7
|
+
export { AbortError, BudgetExceededError, ClaudeError, errorMessage, isKnownError, isTransientError, KNOWN_ERROR_CODES, KnownError, ProcessError, TimeoutError, } from "./errors.js";
|
|
8
8
|
export { blockFingerprint, extractContent, parseDoubleEncoded } from "./parser/content.js";
|
|
9
9
|
export { parseLine } from "./parser/ndjson.js";
|
|
10
10
|
export type { ITranslator } from "./parser/translator.js";
|
|
11
11
|
export { createTranslator } from "./parser/translator.js";
|
|
12
12
|
export type { IClaudeProcess, ISpawnOptions } from "./process.js";
|
|
13
|
-
export { buildArgs,
|
|
13
|
+
export { buildArgs, resetResolvedEnvCache, spawnClaude } from "./process.js";
|
|
14
14
|
export type { IReaderOptions } from "./reader.js";
|
|
15
15
|
export { readNdjsonEvents } from "./reader.js";
|
|
16
16
|
export type { IClaudeSession } from "./session.js";
|
|
@@ -19,9 +19,10 @@ export type { IClaudeStream } from "./stream.js";
|
|
|
19
19
|
export { createStream } from "./stream.js";
|
|
20
20
|
export type { IToolHandlerInstance, TToolDecision } from "./tools/handler.js";
|
|
21
21
|
export { createToolHandler } from "./tools/handler.js";
|
|
22
|
-
export {
|
|
22
|
+
export type { TBuiltInToolName } from "./tools/registry.js";
|
|
23
|
+
export { BUILT_IN_TOOL_NAMES, BUILT_IN_TOOLS, isBuiltInTool } from "./tools/registry.js";
|
|
23
24
|
export type { TErrorEvent, TRelayEvent, TSessionMetaEvent, TTextEvent, TThinkingEvent, TToolResultEvent, TToolUseEvent, TTurnCompleteEvent, } from "./types/events.js";
|
|
24
|
-
export type { IClaudeOptions, ISessionOptions, IToolHandler } from "./types/options.js";
|
|
25
|
+
export type { IAskOptions, IClaudeOptions, ISessionOptions, IToolHandler } from "./types/options.js";
|
|
25
26
|
export type { TClaudeContent, TClaudeContentType, TClaudeEvent, TClaudeEventType, TClaudeMessage, TModelUsageEntry } from "./types/protocol.js";
|
|
26
27
|
export type { TAskResult, TCostSnapshot } from "./types/results.js";
|
|
27
28
|
export { writer } from "./writer.js";
|
package/dist/index.js
CHANGED
|
@@ -2,15 +2,15 @@ import { createClient } from "./client.js";
|
|
|
2
2
|
export { createClient } from "./client.js";
|
|
3
3
|
export { BINARY, LIMITS, TIMEOUTS } from "./constants.js";
|
|
4
4
|
export { createCostTracker } from "./cost.js";
|
|
5
|
-
export { AbortError,
|
|
5
|
+
export { AbortError, BudgetExceededError, ClaudeError, errorMessage, isKnownError, isTransientError, KNOWN_ERROR_CODES, KnownError, ProcessError, TimeoutError, } from "./errors.js";
|
|
6
6
|
export { blockFingerprint, extractContent, parseDoubleEncoded } from "./parser/content.js";
|
|
7
7
|
export { parseLine } from "./parser/ndjson.js";
|
|
8
8
|
export { createTranslator } from "./parser/translator.js";
|
|
9
|
-
export { buildArgs,
|
|
9
|
+
export { buildArgs, resetResolvedEnvCache, spawnClaude } from "./process.js";
|
|
10
10
|
export { readNdjsonEvents } from "./reader.js";
|
|
11
11
|
export { createSession } from "./session.js";
|
|
12
12
|
export { createStream } from "./stream.js";
|
|
13
13
|
export { createToolHandler } from "./tools/handler.js";
|
|
14
|
-
export { BUILT_IN_TOOLS, isBuiltInTool } from "./tools/registry.js";
|
|
14
|
+
export { BUILT_IN_TOOL_NAMES, BUILT_IN_TOOLS, isBuiltInTool } from "./tools/registry.js";
|
|
15
15
|
export { writer } from "./writer.js";
|
|
16
16
|
export const claude = createClient();
|
|
@@ -7,7 +7,12 @@ const extractTokens = (modelUsage) => {
|
|
|
7
7
|
for (const entry of Object.values(modelUsage)) {
|
|
8
8
|
inputTokens = (inputTokens ?? 0) + entry.inputTokens + (entry.cacheReadInputTokens ?? 0) + (entry.cacheCreationInputTokens ?? 0);
|
|
9
9
|
outputTokens = (outputTokens ?? 0) + entry.outputTokens;
|
|
10
|
-
|
|
10
|
+
// Multi-model turns (e.g. sub-agent fan-out) report distinct windows
|
|
11
|
+
// per model. Take max so consumers see the widest context available,
|
|
12
|
+
// not whichever model happened to iterate last.
|
|
13
|
+
if (entry.contextWindow !== undefined && (contextWindow === undefined || entry.contextWindow > contextWindow)) {
|
|
14
|
+
contextWindow = entry.contextWindow;
|
|
15
|
+
}
|
|
11
16
|
}
|
|
12
17
|
}
|
|
13
18
|
return { inputTokens, outputTokens, contextWindow };
|
package/dist/pipeline.d.ts
CHANGED
|
@@ -1,8 +1,18 @@
|
|
|
1
1
|
import type { ICostTracker } from "./cost.js";
|
|
2
|
-
import type { IClaudeProcess } from "./process.js";
|
|
2
|
+
import type { IClaudeProcess, ISpawnOptions } from "./process.js";
|
|
3
|
+
import { type IStderrDrain } from "./reader.js";
|
|
3
4
|
import type { IToolHandlerInstance } from "./tools/handler.js";
|
|
4
|
-
import type { TRelayEvent, TToolUseEvent } from "./types/events.js";
|
|
5
|
-
import type { TAskResult } from "./types/results.js";
|
|
6
|
-
|
|
5
|
+
import type { TRelayEvent, TToolUseEvent, TTurnCompleteEvent } from "./types/events.js";
|
|
6
|
+
import type { TAskResult, TCostSnapshot } from "./types/results.js";
|
|
7
|
+
import type { TWarn } from "./warnings.js";
|
|
8
|
+
interface IPipeline {
|
|
9
|
+
proc: IClaudeProcess;
|
|
10
|
+
reader: ReadableStreamDefaultReader<Uint8Array>;
|
|
11
|
+
stderr: IStderrDrain;
|
|
12
|
+
}
|
|
13
|
+
export declare const startPipeline: (options: ISpawnOptions) => IPipeline;
|
|
14
|
+
export declare const dispatchToolDecision: (proc: IClaudeProcess, toolHandler: IToolHandlerInstance, event: TToolUseEvent, onWarning?: TWarn) => Promise<void>;
|
|
15
|
+
export declare const applyTurnComplete: (event: TTurnCompleteEvent, costTracker: ICostTracker, offsets?: TCostSnapshot) => void;
|
|
7
16
|
export declare const extractText: (events: TRelayEvent[]) => string;
|
|
8
17
|
export declare const buildResult: (events: TRelayEvent[], costTracker: ICostTracker, sessionId: string | undefined) => TAskResult;
|
|
18
|
+
export {};
|
package/dist/pipeline.js
CHANGED
|
@@ -1,31 +1,51 @@
|
|
|
1
|
+
import { safeWrite, spawnClaude } from "./process.js";
|
|
2
|
+
import { drainStderr } from "./reader.js";
|
|
3
|
+
import { createWarn } from "./warnings.js";
|
|
1
4
|
import { writer } from "./writer.js";
|
|
2
|
-
|
|
5
|
+
// Shared process-boot: spawn the CLI, lock the stdout reader, drain
|
|
6
|
+
// stderr. session.ts and stream.ts both need this exact trio; keeping
|
|
7
|
+
// the order in one place prevents the "one forgot to drain stderr and
|
|
8
|
+
// the other swallows exits silently" class of bug.
|
|
9
|
+
export const startPipeline = (options) => {
|
|
10
|
+
const proc = spawnClaude(options);
|
|
11
|
+
const reader = proc.stdout.getReader();
|
|
12
|
+
const stderr = drainStderr(proc);
|
|
13
|
+
return { proc, reader, stderr };
|
|
14
|
+
};
|
|
15
|
+
export const dispatchToolDecision = async (proc, toolHandler, event, onWarning) => {
|
|
16
|
+
const warn = createWarn(onWarning);
|
|
3
17
|
let decision;
|
|
4
18
|
try {
|
|
5
19
|
decision = await toolHandler.decide(event);
|
|
6
20
|
}
|
|
7
21
|
catch (error) {
|
|
8
|
-
|
|
22
|
+
warn("Tool handler threw, defaulting to deny", error);
|
|
9
23
|
decision = "deny";
|
|
10
24
|
}
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
proc.write(writer.deny(event.toolUseId));
|
|
17
|
-
}
|
|
18
|
-
else if (typeof decision === "object" && decision !== null && typeof decision.result === "string") {
|
|
19
|
-
proc.write(writer.toolResult(event.toolUseId, decision.result));
|
|
20
|
-
}
|
|
21
|
-
else {
|
|
22
|
-
console.warn("[claude-wire] Invalid tool decision, defaulting to deny");
|
|
23
|
-
proc.write(writer.deny(event.toolUseId));
|
|
24
|
-
}
|
|
25
|
+
if (decision === "approve") {
|
|
26
|
+
safeWrite(proc, writer.approve(event.toolUseId));
|
|
27
|
+
}
|
|
28
|
+
else if (decision === "deny") {
|
|
29
|
+
safeWrite(proc, writer.deny(event.toolUseId));
|
|
25
30
|
}
|
|
26
|
-
|
|
27
|
-
|
|
31
|
+
else if (typeof decision === "object" && decision !== null && typeof decision.result === "string") {
|
|
32
|
+
const isError = "isError" in decision ? decision.isError : undefined;
|
|
33
|
+
safeWrite(proc, writer.toolResult(event.toolUseId, decision.result, isError ? { isError: true } : undefined));
|
|
28
34
|
}
|
|
35
|
+
else {
|
|
36
|
+
warn("Invalid tool decision, defaulting to deny");
|
|
37
|
+
safeWrite(proc, writer.deny(event.toolUseId));
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
// Applies a turn_complete event's cumulative totals to the cost tracker
|
|
41
|
+
// and enforces the budget. `offsets` covers session's respawn case where
|
|
42
|
+
// the new process starts its cumulative count from zero but the session
|
|
43
|
+
// wants to carry forward what previous processes already spent -- stream
|
|
44
|
+
// has no such concept and passes it undefined.
|
|
45
|
+
export const applyTurnComplete = (event, costTracker, offsets) => {
|
|
46
|
+
const base = offsets ?? { totalUsd: 0, inputTokens: 0, outputTokens: 0 };
|
|
47
|
+
costTracker.update(base.totalUsd + (event.costUsd ?? 0), base.inputTokens + (event.inputTokens ?? 0), base.outputTokens + (event.outputTokens ?? 0));
|
|
48
|
+
costTracker.checkBudget();
|
|
29
49
|
};
|
|
30
50
|
export const extractText = (events) => {
|
|
31
51
|
return events
|
package/dist/process.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { IClaudeOptions } from "./types/options.js";
|
|
2
2
|
export interface IClaudeProcess {
|
|
3
3
|
write: (message: string) => void;
|
|
4
|
-
kill: () => void;
|
|
4
|
+
kill: (signal?: NodeJS.Signals | number) => void;
|
|
5
5
|
exited: Promise<number>;
|
|
6
6
|
stdout: ReadableStream<Uint8Array>;
|
|
7
7
|
stderr: ReadableStream<Uint8Array>;
|
|
@@ -10,8 +10,20 @@ export interface IClaudeProcess {
|
|
|
10
10
|
export interface ISpawnOptions extends IClaudeOptions {
|
|
11
11
|
prompt?: string;
|
|
12
12
|
}
|
|
13
|
+
export declare const safeKill: (proc: Pick<IClaudeProcess, "kill">, signal?: NodeJS.Signals | number) => void;
|
|
14
|
+
export declare const safeWrite: (proc: Pick<IClaudeProcess, "write">, line: string) => boolean;
|
|
13
15
|
export declare const ALIAS_PATTERN: RegExp;
|
|
14
|
-
|
|
15
|
-
|
|
16
|
+
/**
|
|
17
|
+
* Clears the cached resolved environment (binary path + alias-detected
|
|
18
|
+
* `CLAUDE_CONFIG_DIR`). Call this when either has changed mid-process -- for
|
|
19
|
+
* example after installing the Claude CLI during a test run, or when a long-
|
|
20
|
+
* running daemon updates the user's shell rc file. The next `spawnClaude()`
|
|
21
|
+
* will re-resolve from scratch.
|
|
22
|
+
*
|
|
23
|
+
* Normal applications should never need this; the cache is populated once at
|
|
24
|
+
* first use and kept for the process lifetime.
|
|
25
|
+
*/
|
|
26
|
+
export declare const resetResolvedEnvCache: () => void;
|
|
16
27
|
export declare const buildArgs: (options: ISpawnOptions, binaryPath: string) => string[];
|
|
28
|
+
export declare const buildSpawnEnv: (baseEnv: Record<string, string | undefined>, aliasConfigDir: string | undefined, options: Pick<ISpawnOptions, "configDir" | "env">) => Record<string, string | undefined> | undefined;
|
|
17
29
|
export declare const spawnClaude: (options: ISpawnOptions) => IClaudeProcess;
|
package/dist/process.js
CHANGED
|
@@ -2,16 +2,41 @@ 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
|
}
|
|
@@ -20,9 +45,12 @@ const resolveBinaryPath = () => {
|
|
|
20
45
|
// Rejects lines whose first non-whitespace char is `#` so commented-out
|
|
21
46
|
// aliases/exports don't silently apply. /m anchors to each line in rc files.
|
|
22
47
|
export const ALIAS_PATTERN = /^(?!\s*#).*?(?:alias\s+claude\s*=|export\s+).*CLAUDE_CONFIG_DIR=["']?\$?(?:HOME|\{HOME\}|~)\/?([^\s"']+?)["']?(?:\s|$)/m;
|
|
23
|
-
|
|
48
|
+
const resolveConfigDirFromAlias = () => {
|
|
24
49
|
const home = homedir();
|
|
25
|
-
|
|
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"];
|
|
26
54
|
for (const rcFile of rcFiles) {
|
|
27
55
|
try {
|
|
28
56
|
const content = readFileSync(join(home, rcFile), "utf-8");
|
|
@@ -38,7 +66,17 @@ export const resolveConfigDirFromAlias = () => {
|
|
|
38
66
|
return undefined;
|
|
39
67
|
};
|
|
40
68
|
let cached;
|
|
41
|
-
|
|
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 = () => {
|
|
42
80
|
cached = undefined;
|
|
43
81
|
};
|
|
44
82
|
const resolve = () => {
|
|
@@ -62,6 +100,9 @@ export const buildArgs = (options, binaryPath) => {
|
|
|
62
100
|
args.push(name, value);
|
|
63
101
|
}
|
|
64
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).
|
|
65
106
|
flag(options.verbose !== false, "--verbose");
|
|
66
107
|
kv(options.model, "--model");
|
|
67
108
|
kv(options.systemPrompt, "--system-prompt");
|
|
@@ -103,36 +144,56 @@ export const buildArgs = (options, binaryPath) => {
|
|
|
103
144
|
flag(options.disableSlashCommands, "--disable-slash-commands");
|
|
104
145
|
return args;
|
|
105
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
|
+
};
|
|
106
168
|
export const spawnClaude = (options) => {
|
|
107
169
|
assertPositiveNumber(options.maxBudgetUsd, "maxBudgetUsd");
|
|
108
170
|
const resolved = resolve();
|
|
109
171
|
const args = buildArgs(options, resolved.binaryPath);
|
|
110
172
|
try {
|
|
111
|
-
const
|
|
112
|
-
let spawnEnv;
|
|
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.
|
|
117
|
-
spawnEnv = { ...process.env };
|
|
118
|
-
if (resolved.aliasConfigDir) {
|
|
119
|
-
spawnEnv.CLAUDE_CONFIG_DIR = resolved.aliasConfigDir;
|
|
120
|
-
}
|
|
121
|
-
if (options.env) {
|
|
122
|
-
Object.assign(spawnEnv, options.env);
|
|
123
|
-
}
|
|
124
|
-
if (options.configDir) {
|
|
125
|
-
spawnEnv.CLAUDE_CONFIG_DIR = options.configDir;
|
|
126
|
-
}
|
|
127
|
-
}
|
|
173
|
+
const spawnEnv = buildSpawnEnv(process.env, resolved.aliasConfigDir, options);
|
|
128
174
|
const rawProc = spawnProcess(args, { cwd: options.cwd, env: spawnEnv });
|
|
129
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
|
+
}
|
|
130
191
|
const claudeProc = {
|
|
131
192
|
write: (msg) => {
|
|
132
193
|
rawProc.stdin.write(msg);
|
|
133
194
|
},
|
|
134
|
-
kill: () => {
|
|
135
|
-
rawProc.kill();
|
|
195
|
+
kill: (signal) => {
|
|
196
|
+
rawProc.kill(signal);
|
|
136
197
|
},
|
|
137
198
|
exited: rawProc.exited,
|
|
138
199
|
stdout: rawProc.stdout,
|
package/dist/reader.d.ts
CHANGED
|
@@ -2,16 +2,19 @@ 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
|
}
|
|
12
14
|
export interface IStderrDrain {
|
|
13
15
|
chunks: string[];
|
|
14
16
|
done: Promise<void>;
|
|
17
|
+
text: () => string;
|
|
15
18
|
}
|
|
16
19
|
export declare const drainStderr: (proc: {
|
|
17
20
|
stderr: ReadableStream<Uint8Array>;
|