@pivanov/claude-wire 0.0.2
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/LICENSE +21 -0
- package/README.md +78 -0
- package/dist/client.d.ts +11 -0
- package/dist/client.js +28 -0
- package/dist/constants.d.ts +13 -0
- package/dist/constants.js +15 -0
- package/dist/cost.d.ts +12 -0
- package/dist/cost.js +36 -0
- package/dist/errors.d.ts +29 -0
- package/dist/errors.js +68 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.js +16 -0
- package/dist/parser/content.d.ts +4 -0
- package/dist/parser/content.js +46 -0
- package/dist/parser/ndjson.d.ts +2 -0
- package/dist/parser/ndjson.js +12 -0
- package/dist/parser/translator.d.ts +7 -0
- package/dist/parser/translator.js +127 -0
- package/dist/pipeline.d.ts +8 -0
- package/dist/pipeline.js +47 -0
- package/dist/process.d.ts +15 -0
- package/dist/process.js +179 -0
- package/dist/reader.d.ts +13 -0
- package/dist/reader.js +94 -0
- package/dist/runtime.d.ts +18 -0
- package/dist/runtime.js +136 -0
- package/dist/session.d.ts +8 -0
- package/dist/session.js +239 -0
- package/dist/stream.d.ts +9 -0
- package/dist/stream.js +130 -0
- package/dist/tools/handler.d.ts +9 -0
- package/dist/tools/handler.js +18 -0
- package/dist/tools/registry.d.ts +2 -0
- package/dist/tools/registry.js +42 -0
- package/dist/types/events.d.ts +41 -0
- package/dist/types/events.js +1 -0
- package/dist/types/options.d.ts +41 -0
- package/dist/types/options.js +1 -0
- package/dist/types/protocol.d.ts +42 -0
- package/dist/types/protocol.js +1 -0
- package/dist/types/results.d.ts +17 -0
- package/dist/types/results.js +1 -0
- package/dist/writer.d.ts +7 -0
- package/dist/writer.js +26 -0
- package/package.json +61 -0
package/dist/process.js
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { BINARY } from "./constants.js";
|
|
5
|
+
import { assertPositiveNumber, errorMessage, KnownError, ProcessError } from "./errors.js";
|
|
6
|
+
import { fileExists, spawnProcess, whichSync } from "./runtime.js";
|
|
7
|
+
import { writer } from "./writer.js";
|
|
8
|
+
const resolveBinaryPath = () => {
|
|
9
|
+
const found = whichSync("claude");
|
|
10
|
+
if (found) {
|
|
11
|
+
return found;
|
|
12
|
+
}
|
|
13
|
+
for (const p of BINARY.commonPaths) {
|
|
14
|
+
if (fileExists(p)) {
|
|
15
|
+
return p;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return BINARY.name;
|
|
19
|
+
};
|
|
20
|
+
const ALIAS_PATTERN = /(?:alias\s+claude\s*=|export\s+).*CLAUDE_CONFIG_DIR=["']?\$?(?:HOME|\{HOME\}|~)\/?([^\s"']+?)["']?(?:\s|\/|$)/;
|
|
21
|
+
const resolveConfigDirFromAlias = () => {
|
|
22
|
+
const home = homedir();
|
|
23
|
+
const rcFiles = [".zshrc", ".bashrc", ".zprofile", ".bash_profile", ".aliases"];
|
|
24
|
+
for (const rcFile of rcFiles) {
|
|
25
|
+
try {
|
|
26
|
+
const content = readFileSync(join(home, rcFile), "utf-8");
|
|
27
|
+
const match = content.match(ALIAS_PATTERN);
|
|
28
|
+
if (match?.[1]) {
|
|
29
|
+
return join(home, match[1]);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
// file doesn't exist or can't be read
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return undefined;
|
|
37
|
+
};
|
|
38
|
+
let cached;
|
|
39
|
+
export const resetBinaryCache = () => {
|
|
40
|
+
cached = undefined;
|
|
41
|
+
};
|
|
42
|
+
const resolve = () => {
|
|
43
|
+
if (!cached) {
|
|
44
|
+
cached = {
|
|
45
|
+
binaryPath: resolveBinaryPath(),
|
|
46
|
+
aliasConfigDir: resolveConfigDirFromAlias(),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
return cached;
|
|
50
|
+
};
|
|
51
|
+
export const buildArgs = (options, binaryPath) => {
|
|
52
|
+
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
|
+
}
|
|
65
|
+
if (options.allowedTools) {
|
|
66
|
+
if (options.allowedTools.length === 0) {
|
|
67
|
+
args.push("--tools", "");
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
args.push("--allowedTools", options.allowedTools.join(","));
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
if (options.disallowedTools && options.disallowedTools.length > 0) {
|
|
74
|
+
args.push("--disallowedTools", options.disallowedTools.join(","));
|
|
75
|
+
}
|
|
76
|
+
if (options.maxBudgetUsd !== undefined) {
|
|
77
|
+
args.push("--max-budget-usd", String(options.maxBudgetUsd));
|
|
78
|
+
}
|
|
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
|
+
}
|
|
91
|
+
if (options.addDirs && options.addDirs.length > 0) {
|
|
92
|
+
for (const dir of options.addDirs) {
|
|
93
|
+
args.push("--add-dir", dir);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
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
|
+
}
|
|
120
|
+
if (options.settingSources !== undefined) {
|
|
121
|
+
args.push("--setting-sources", options.settingSources);
|
|
122
|
+
}
|
|
123
|
+
if (options.disableSlashCommands) {
|
|
124
|
+
args.push("--disable-slash-commands");
|
|
125
|
+
}
|
|
126
|
+
return args;
|
|
127
|
+
};
|
|
128
|
+
export const spawnClaude = (options) => {
|
|
129
|
+
assertPositiveNumber(options.maxBudgetUsd, "maxBudgetUsd");
|
|
130
|
+
const resolved = resolve();
|
|
131
|
+
const args = buildArgs(options, resolved.binaryPath);
|
|
132
|
+
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
|
+
}
|
|
147
|
+
const rawProc = spawnProcess(args, { cwd: options.cwd, env: spawnEnv });
|
|
148
|
+
rawProc.exited.catch(() => { });
|
|
149
|
+
const claudeProc = {
|
|
150
|
+
write: (msg) => {
|
|
151
|
+
rawProc.stdin.write(msg);
|
|
152
|
+
},
|
|
153
|
+
kill: () => {
|
|
154
|
+
rawProc.kill();
|
|
155
|
+
},
|
|
156
|
+
exited: rawProc.exited,
|
|
157
|
+
stdout: rawProc.stdout,
|
|
158
|
+
stderr: rawProc.stderr,
|
|
159
|
+
pid: rawProc.pid,
|
|
160
|
+
};
|
|
161
|
+
if (options.prompt) {
|
|
162
|
+
try {
|
|
163
|
+
rawProc.stdin.write(writer.user(options.prompt));
|
|
164
|
+
}
|
|
165
|
+
catch {
|
|
166
|
+
rawProc.kill();
|
|
167
|
+
throw new ProcessError("Failed to write initial prompt to process");
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return claudeProc;
|
|
171
|
+
}
|
|
172
|
+
catch (error) {
|
|
173
|
+
const msg = errorMessage(error);
|
|
174
|
+
if (msg.includes("ENOENT") || msg.includes("not found")) {
|
|
175
|
+
throw new KnownError("binary-not-found", "Claude CLI not found. Install it from https://claude.ai/download");
|
|
176
|
+
}
|
|
177
|
+
throw new ProcessError(`Failed to spawn claude: ${msg}`);
|
|
178
|
+
}
|
|
179
|
+
};
|
package/dist/reader.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { ITranslator } from "./parser/translator.js";
|
|
2
|
+
import type { IClaudeProcess } from "./process.js";
|
|
3
|
+
import type { IToolHandlerInstance } from "./tools/handler.js";
|
|
4
|
+
import type { TRelayEvent } from "./types/events.js";
|
|
5
|
+
export interface IReaderOptions {
|
|
6
|
+
reader: ReadableStreamDefaultReader<Uint8Array>;
|
|
7
|
+
translator: ITranslator;
|
|
8
|
+
toolHandler?: IToolHandlerInstance;
|
|
9
|
+
proc?: IClaudeProcess;
|
|
10
|
+
signal?: AbortSignal;
|
|
11
|
+
}
|
|
12
|
+
export declare function readNdjsonEvents(opts: IReaderOptions): AsyncGenerator<TRelayEvent>;
|
|
13
|
+
export type { TRelayEvent };
|
package/dist/reader.js
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { LIMITS, TIMEOUTS } from "./constants.js";
|
|
2
|
+
import { AbortError, ClaudeError, TimeoutError } from "./errors.js";
|
|
3
|
+
import { parseLine } from "./parser/ndjson.js";
|
|
4
|
+
import { dispatchToolDecision } from "./pipeline.js";
|
|
5
|
+
export async function* readNdjsonEvents(opts) {
|
|
6
|
+
const { reader, translator, signal } = opts;
|
|
7
|
+
const decoder = new TextDecoder();
|
|
8
|
+
let buffer = "";
|
|
9
|
+
let timeoutId;
|
|
10
|
+
let turnComplete = false;
|
|
11
|
+
const abortHandler = signal
|
|
12
|
+
? () => {
|
|
13
|
+
if (opts.proc) {
|
|
14
|
+
try {
|
|
15
|
+
opts.proc.write('{"type":"abort"}\n');
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
// stdin closed
|
|
19
|
+
}
|
|
20
|
+
opts.proc.kill();
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
: undefined;
|
|
24
|
+
if (signal && abortHandler) {
|
|
25
|
+
if (signal.aborted) {
|
|
26
|
+
throw new AbortError();
|
|
27
|
+
}
|
|
28
|
+
signal.addEventListener("abort", abortHandler, { once: true });
|
|
29
|
+
}
|
|
30
|
+
try {
|
|
31
|
+
while (true) {
|
|
32
|
+
if (signal?.aborted) {
|
|
33
|
+
throw new AbortError();
|
|
34
|
+
}
|
|
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);
|
|
42
|
+
const { done, value } = readResult;
|
|
43
|
+
if (done) {
|
|
44
|
+
break;
|
|
45
|
+
}
|
|
46
|
+
buffer += decoder.decode(value, { stream: true });
|
|
47
|
+
if (buffer.length > LIMITS.ndjsonMaxLineChars) {
|
|
48
|
+
throw new ClaudeError(`NDJSON buffer exceeded ${LIMITS.ndjsonMaxLineChars} chars`);
|
|
49
|
+
}
|
|
50
|
+
const lines = buffer.split("\n");
|
|
51
|
+
buffer = lines.pop() ?? "";
|
|
52
|
+
for (const line of lines) {
|
|
53
|
+
const raw = parseLine(line);
|
|
54
|
+
if (!raw) {
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
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
|
+
}
|
|
67
|
+
}
|
|
68
|
+
if (turnComplete) {
|
|
69
|
+
break;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
if (buffer.trim()) {
|
|
73
|
+
const raw = parseLine(buffer);
|
|
74
|
+
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
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
finally {
|
|
89
|
+
clearTimeout(timeoutId);
|
|
90
|
+
if (signal && abortHandler) {
|
|
91
|
+
signal.removeEventListener("abort", abortHandler);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export interface IRawProcess {
|
|
2
|
+
stdin: {
|
|
3
|
+
write: (data: string) => void;
|
|
4
|
+
end: () => void;
|
|
5
|
+
};
|
|
6
|
+
stdout: ReadableStream<Uint8Array>;
|
|
7
|
+
stderr: ReadableStream<Uint8Array>;
|
|
8
|
+
kill: () => void;
|
|
9
|
+
exited: Promise<number>;
|
|
10
|
+
pid: number;
|
|
11
|
+
}
|
|
12
|
+
export interface ISpawnOpts {
|
|
13
|
+
cwd?: string;
|
|
14
|
+
env?: Record<string, string | undefined>;
|
|
15
|
+
}
|
|
16
|
+
export declare const spawnProcess: (args: string[], opts: ISpawnOpts) => IRawProcess;
|
|
17
|
+
export declare const whichSync: (name: string) => string | undefined;
|
|
18
|
+
export declare const fileExists: (path: string) => boolean;
|
package/dist/runtime.js
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { execFileSync, spawn as nodeSpawn } from "node:child_process";
|
|
2
|
+
import { accessSync, constants, statSync } from "node:fs";
|
|
3
|
+
import { Readable } from "node:stream";
|
|
4
|
+
const isBun = typeof globalThis.Bun !== "undefined";
|
|
5
|
+
export const spawnProcess = (args, opts) => {
|
|
6
|
+
if (isBun) {
|
|
7
|
+
return spawnBun(args, opts);
|
|
8
|
+
}
|
|
9
|
+
return spawnNode(args, opts);
|
|
10
|
+
};
|
|
11
|
+
export const whichSync = (name) => {
|
|
12
|
+
if (isBun) {
|
|
13
|
+
try {
|
|
14
|
+
const result = Bun.spawnSync(["which", name], { stdout: "pipe", stderr: "pipe" });
|
|
15
|
+
const path = new TextDecoder().decode(result.stdout).trim();
|
|
16
|
+
if (path) {
|
|
17
|
+
return path;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
// fall through
|
|
22
|
+
}
|
|
23
|
+
return undefined;
|
|
24
|
+
}
|
|
25
|
+
try {
|
|
26
|
+
return execFileSync("which", [name], { encoding: "utf-8" }).trim() || undefined;
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
31
|
+
};
|
|
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
|
+
}
|
|
42
|
+
try {
|
|
43
|
+
accessSync(path, constants.X_OK);
|
|
44
|
+
return statSync(path).size > 0;
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
const spawnBun = (args, opts) => {
|
|
51
|
+
const proc = Bun.spawn(args, {
|
|
52
|
+
cwd: opts.cwd,
|
|
53
|
+
stdin: "pipe",
|
|
54
|
+
stdout: "pipe",
|
|
55
|
+
stderr: "pipe",
|
|
56
|
+
env: opts.env,
|
|
57
|
+
});
|
|
58
|
+
return {
|
|
59
|
+
stdin: {
|
|
60
|
+
write: (data) => {
|
|
61
|
+
proc.stdin.write(data);
|
|
62
|
+
},
|
|
63
|
+
end: () => {
|
|
64
|
+
proc.stdin.end();
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
stdout: proc.stdout,
|
|
68
|
+
stderr: proc.stderr,
|
|
69
|
+
kill: () => {
|
|
70
|
+
proc.kill();
|
|
71
|
+
},
|
|
72
|
+
exited: proc.exited,
|
|
73
|
+
pid: proc.pid,
|
|
74
|
+
};
|
|
75
|
+
};
|
|
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
|
+
};
|
|
103
|
+
const spawnNode = (args, opts) => {
|
|
104
|
+
const [cmd, ...rest] = args;
|
|
105
|
+
if (!cmd) {
|
|
106
|
+
throw new Error("No command specified");
|
|
107
|
+
}
|
|
108
|
+
const child = nodeSpawn(cmd, rest, {
|
|
109
|
+
cwd: opts.cwd,
|
|
110
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
111
|
+
env: opts.env,
|
|
112
|
+
});
|
|
113
|
+
const exited = new Promise((resolve, reject) => {
|
|
114
|
+
child.on("exit", (code) => {
|
|
115
|
+
resolve(code ?? 1);
|
|
116
|
+
});
|
|
117
|
+
child.on("error", reject);
|
|
118
|
+
});
|
|
119
|
+
return {
|
|
120
|
+
stdin: {
|
|
121
|
+
write: (data) => {
|
|
122
|
+
child.stdin?.write(data);
|
|
123
|
+
},
|
|
124
|
+
end: () => {
|
|
125
|
+
child.stdin?.end();
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
stdout: child.stdout ? nodeReadableToWeb(child.stdout) : new ReadableStream(),
|
|
129
|
+
stderr: child.stderr ? nodeReadableToWeb(child.stderr) : new ReadableStream(),
|
|
130
|
+
kill: () => {
|
|
131
|
+
child.kill();
|
|
132
|
+
},
|
|
133
|
+
exited,
|
|
134
|
+
pid: child.pid ?? 0,
|
|
135
|
+
};
|
|
136
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ISessionOptions } from "./types/options.js";
|
|
2
|
+
import type { TAskResult } from "./types/results.js";
|
|
3
|
+
export interface IClaudeSession extends AsyncDisposable {
|
|
4
|
+
ask: (prompt: string) => Promise<TAskResult>;
|
|
5
|
+
close: () => Promise<void>;
|
|
6
|
+
sessionId: string | undefined;
|
|
7
|
+
}
|
|
8
|
+
export declare const createSession: (options?: ISessionOptions) => IClaudeSession;
|
package/dist/session.js
ADDED
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import { LIMITS, TIMEOUTS } from "./constants.js";
|
|
2
|
+
import { createCostTracker } from "./cost.js";
|
|
3
|
+
import { AbortError, BudgetExceededError, ClaudeError, isTransientError, KnownError, ProcessError, TimeoutError } from "./errors.js";
|
|
4
|
+
import { createTranslator } from "./parser/translator.js";
|
|
5
|
+
import { buildResult } from "./pipeline.js";
|
|
6
|
+
import { spawnClaude } from "./process.js";
|
|
7
|
+
import { readNdjsonEvents } from "./reader.js";
|
|
8
|
+
import { createToolHandler } from "./tools/handler.js";
|
|
9
|
+
import { writer } from "./writer.js";
|
|
10
|
+
const gracefulKill = async (p) => {
|
|
11
|
+
p.kill();
|
|
12
|
+
let timer;
|
|
13
|
+
let timedOut = false;
|
|
14
|
+
try {
|
|
15
|
+
await Promise.race([
|
|
16
|
+
p.exited,
|
|
17
|
+
new Promise((r) => {
|
|
18
|
+
timer = setTimeout(() => {
|
|
19
|
+
timedOut = true;
|
|
20
|
+
r();
|
|
21
|
+
}, TIMEOUTS.gracefulExitMs);
|
|
22
|
+
}),
|
|
23
|
+
]);
|
|
24
|
+
}
|
|
25
|
+
finally {
|
|
26
|
+
if (timer) {
|
|
27
|
+
clearTimeout(timer);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
if (timedOut) {
|
|
31
|
+
try {
|
|
32
|
+
p.kill();
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
// already dead
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
export const createSession = (options = {}) => {
|
|
40
|
+
let proc;
|
|
41
|
+
let currentSessionId;
|
|
42
|
+
let consecutiveCrashes = 0;
|
|
43
|
+
let turnCount = 0;
|
|
44
|
+
let costOffsets = { totalUsd: 0, inputTokens: 0, outputTokens: 0 };
|
|
45
|
+
const translator = createTranslator();
|
|
46
|
+
const costTracker = createCostTracker({
|
|
47
|
+
maxCostUsd: options.maxCostUsd,
|
|
48
|
+
onCostUpdate: options.onCostUpdate,
|
|
49
|
+
});
|
|
50
|
+
const toolHandler = options.tools ? createToolHandler(options.tools) : undefined;
|
|
51
|
+
let inFlight;
|
|
52
|
+
let reader;
|
|
53
|
+
const cleanupProcess = () => {
|
|
54
|
+
if (reader) {
|
|
55
|
+
try {
|
|
56
|
+
reader.releaseLock();
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
// already released
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
reader = undefined;
|
|
63
|
+
};
|
|
64
|
+
let lastStderrChunks = [];
|
|
65
|
+
const drainStderr = (p) => {
|
|
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
|
+
};
|
|
88
|
+
const getStderrText = () => lastStderrChunks.join("").trim();
|
|
89
|
+
const killProc = () => {
|
|
90
|
+
if (proc) {
|
|
91
|
+
proc.kill();
|
|
92
|
+
proc.exited.catch(() => { });
|
|
93
|
+
}
|
|
94
|
+
proc = undefined;
|
|
95
|
+
cleanupProcess();
|
|
96
|
+
};
|
|
97
|
+
const spawnFresh = (prompt, resumeId) => {
|
|
98
|
+
if (consecutiveCrashes >= LIMITS.maxRespawnAttempts) {
|
|
99
|
+
killProc();
|
|
100
|
+
throw new ProcessError(`Process crashed ${consecutiveCrashes} times, giving up`);
|
|
101
|
+
}
|
|
102
|
+
costOffsets = costTracker.snapshot();
|
|
103
|
+
killProc();
|
|
104
|
+
translator.reset();
|
|
105
|
+
const spawnOpts = resumeId ? { prompt, ...options, resume: resumeId } : { prompt, ...options };
|
|
106
|
+
proc = spawnClaude(spawnOpts);
|
|
107
|
+
reader = proc.stdout.getReader();
|
|
108
|
+
drainStderr(proc);
|
|
109
|
+
};
|
|
110
|
+
const readUntilTurnComplete = async (signal) => {
|
|
111
|
+
if (!proc || !reader) {
|
|
112
|
+
throw new ClaudeError("Session not started");
|
|
113
|
+
}
|
|
114
|
+
const events = [];
|
|
115
|
+
let gotTurnComplete = false;
|
|
116
|
+
for await (const event of readNdjsonEvents({
|
|
117
|
+
reader,
|
|
118
|
+
translator,
|
|
119
|
+
toolHandler,
|
|
120
|
+
proc,
|
|
121
|
+
signal,
|
|
122
|
+
})) {
|
|
123
|
+
if (event.type === "session_meta") {
|
|
124
|
+
currentSessionId = event.sessionId;
|
|
125
|
+
}
|
|
126
|
+
events.push(event);
|
|
127
|
+
if (event.type === "turn_complete") {
|
|
128
|
+
costTracker.update(costOffsets.totalUsd + (event.costUsd ?? 0), costOffsets.inputTokens + (event.inputTokens ?? 0), costOffsets.outputTokens + (event.outputTokens ?? 0));
|
|
129
|
+
costTracker.checkBudget();
|
|
130
|
+
gotTurnComplete = true;
|
|
131
|
+
break;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
if (!gotTurnComplete) {
|
|
135
|
+
if (signal?.aborted) {
|
|
136
|
+
throw new AbortError();
|
|
137
|
+
}
|
|
138
|
+
const stderrMsg = getStderrText();
|
|
139
|
+
throw new ProcessError(stderrMsg || "Process exited without completing the turn");
|
|
140
|
+
}
|
|
141
|
+
return events;
|
|
142
|
+
};
|
|
143
|
+
const doAsk = async (prompt) => {
|
|
144
|
+
if (!proc) {
|
|
145
|
+
spawnFresh(prompt, currentSessionId);
|
|
146
|
+
}
|
|
147
|
+
else {
|
|
148
|
+
try {
|
|
149
|
+
proc.write(writer.user(prompt));
|
|
150
|
+
}
|
|
151
|
+
catch {
|
|
152
|
+
consecutiveCrashes++;
|
|
153
|
+
translator.reset();
|
|
154
|
+
spawnFresh(prompt, currentSessionId);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
let events;
|
|
158
|
+
try {
|
|
159
|
+
events = await readUntilTurnComplete(options.signal);
|
|
160
|
+
}
|
|
161
|
+
catch (error) {
|
|
162
|
+
if (error instanceof AbortError || error instanceof TimeoutError) {
|
|
163
|
+
killProc();
|
|
164
|
+
throw error;
|
|
165
|
+
}
|
|
166
|
+
if (isTransientError(error) && consecutiveCrashes < LIMITS.maxRespawnAttempts) {
|
|
167
|
+
consecutiveCrashes++;
|
|
168
|
+
spawnFresh(prompt, currentSessionId);
|
|
169
|
+
try {
|
|
170
|
+
events = await readUntilTurnComplete(options.signal);
|
|
171
|
+
}
|
|
172
|
+
catch (retryError) {
|
|
173
|
+
killProc();
|
|
174
|
+
translator.reset();
|
|
175
|
+
throw retryError;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
else {
|
|
179
|
+
killProc();
|
|
180
|
+
translator.reset();
|
|
181
|
+
throw error;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
consecutiveCrashes = 0;
|
|
185
|
+
turnCount++;
|
|
186
|
+
if (turnCount >= LIMITS.sessionMaxTurnsBeforeRecycle) {
|
|
187
|
+
if (proc) {
|
|
188
|
+
await gracefulKill(proc);
|
|
189
|
+
}
|
|
190
|
+
proc = undefined;
|
|
191
|
+
cleanupProcess();
|
|
192
|
+
turnCount = 0;
|
|
193
|
+
consecutiveCrashes = 0;
|
|
194
|
+
}
|
|
195
|
+
return buildResult(events, costTracker, currentSessionId);
|
|
196
|
+
};
|
|
197
|
+
let closed = false;
|
|
198
|
+
const ask = (prompt) => {
|
|
199
|
+
if (closed) {
|
|
200
|
+
return Promise.reject(new ClaudeError("Session is closed"));
|
|
201
|
+
}
|
|
202
|
+
const prev = inFlight ?? Promise.resolve();
|
|
203
|
+
const run = prev
|
|
204
|
+
.catch((prevError) => {
|
|
205
|
+
if (prevError instanceof KnownError || prevError instanceof BudgetExceededError) {
|
|
206
|
+
throw prevError;
|
|
207
|
+
}
|
|
208
|
+
})
|
|
209
|
+
.then(() => doAsk(prompt));
|
|
210
|
+
inFlight = run;
|
|
211
|
+
return run;
|
|
212
|
+
};
|
|
213
|
+
const close = async () => {
|
|
214
|
+
closed = true;
|
|
215
|
+
if (inFlight) {
|
|
216
|
+
await inFlight.catch(() => { });
|
|
217
|
+
inFlight = undefined;
|
|
218
|
+
}
|
|
219
|
+
if (proc) {
|
|
220
|
+
try {
|
|
221
|
+
proc.write(writer.abort());
|
|
222
|
+
}
|
|
223
|
+
catch {
|
|
224
|
+
// stdin may already be closed
|
|
225
|
+
}
|
|
226
|
+
await gracefulKill(proc);
|
|
227
|
+
proc = undefined;
|
|
228
|
+
}
|
|
229
|
+
cleanupProcess();
|
|
230
|
+
};
|
|
231
|
+
return {
|
|
232
|
+
ask,
|
|
233
|
+
close,
|
|
234
|
+
get sessionId() {
|
|
235
|
+
return currentSessionId;
|
|
236
|
+
},
|
|
237
|
+
[Symbol.asyncDispose]: close,
|
|
238
|
+
};
|
|
239
|
+
};
|
package/dist/stream.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { TRelayEvent } from "./types/events.js";
|
|
2
|
+
import type { IClaudeOptions } from "./types/options.js";
|
|
3
|
+
import type { TAskResult, TCostSnapshot } from "./types/results.js";
|
|
4
|
+
export interface IClaudeStream extends AsyncIterable<TRelayEvent>, AsyncDisposable {
|
|
5
|
+
text: () => Promise<string>;
|
|
6
|
+
cost: () => Promise<TCostSnapshot>;
|
|
7
|
+
result: () => Promise<TAskResult>;
|
|
8
|
+
}
|
|
9
|
+
export declare const createStream: (prompt: string, options?: IClaudeOptions) => IClaudeStream;
|