@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/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 { assertPositiveNumber, errorMessage, KnownError, ProcessError } from "./errors.js";
6
- import { fileExists, spawnProcess, whichSync } from "./runtime.js";
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 (fileExists(p)) {
39
+ if (isExecutableNonEmpty(p)) {
15
40
  return p;
16
41
  }
17
42
  }
18
43
  return BINARY.name;
19
44
  };
20
- const ALIAS_PATTERN = /(?:alias\s+claude\s*=|export\s+).*CLAUDE_CONFIG_DIR=["']?\$?(?:HOME|\{HOME\}|~)\/?([^\s"']+?)["']?(?:\s|\/|$)/;
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
- const rcFiles = [".zshrc", ".bashrc", ".zprofile", ".bash_profile", ".aliases"];
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
- export const resetBinaryCache = () => {
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
- if (options.verbose !== false) {
54
- args.push("--verbose");
55
- }
56
- if (options.model) {
57
- args.push("--model", options.model);
58
- }
59
- if (options.systemPrompt) {
60
- args.push("--system-prompt", options.systemPrompt);
61
- }
62
- if (options.appendSystemPrompt) {
63
- args.push("--append-system-prompt", options.appendSystemPrompt);
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
- if (options.resume) {
80
- args.push("--resume", options.resume);
81
- }
82
- if (options.mcpConfig) {
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
- if (options.effort) {
97
- args.push("--effort", options.effort);
98
- }
99
- if (options.includeHookEvents) {
100
- args.push("--include-hook-events");
101
- }
102
- if (options.includePartialMessages) {
103
- args.push("--include-partial-messages");
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
- if (options.disableSlashCommands) {
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 needsEnv = resolved.aliasConfigDir || options.configDir || options.env;
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
- try {
15
- opts.proc.write('{"type":"abort"}\n');
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
- const timeoutPromise = new Promise((_, reject) => {
36
- timeoutId = setTimeout(() => {
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
- const events = translator.translate(raw);
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
- const events = translator.translate(raw);
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
- export interface IRawProcess {
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
- export interface ISpawnOpts {
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 fileExists: (path: string) => boolean;
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
- 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
- }
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 nodeReadableToWeb = (readable) => {
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 Error("No command specified");
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?.write(data);
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 ? nodeReadableToWeb(child.stdout) : new ReadableStream(),
129
- stderr: child.stderr ? nodeReadableToWeb(child.stderr) : new ReadableStream(),
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 ?? 0,
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;