@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.
Files changed (45) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +78 -0
  3. package/dist/client.d.ts +11 -0
  4. package/dist/client.js +28 -0
  5. package/dist/constants.d.ts +13 -0
  6. package/dist/constants.js +15 -0
  7. package/dist/cost.d.ts +12 -0
  8. package/dist/cost.js +36 -0
  9. package/dist/errors.d.ts +29 -0
  10. package/dist/errors.js +68 -0
  11. package/dist/index.d.ts +27 -0
  12. package/dist/index.js +16 -0
  13. package/dist/parser/content.d.ts +4 -0
  14. package/dist/parser/content.js +46 -0
  15. package/dist/parser/ndjson.d.ts +2 -0
  16. package/dist/parser/ndjson.js +12 -0
  17. package/dist/parser/translator.d.ts +7 -0
  18. package/dist/parser/translator.js +127 -0
  19. package/dist/pipeline.d.ts +8 -0
  20. package/dist/pipeline.js +47 -0
  21. package/dist/process.d.ts +15 -0
  22. package/dist/process.js +179 -0
  23. package/dist/reader.d.ts +13 -0
  24. package/dist/reader.js +94 -0
  25. package/dist/runtime.d.ts +18 -0
  26. package/dist/runtime.js +136 -0
  27. package/dist/session.d.ts +8 -0
  28. package/dist/session.js +239 -0
  29. package/dist/stream.d.ts +9 -0
  30. package/dist/stream.js +130 -0
  31. package/dist/tools/handler.d.ts +9 -0
  32. package/dist/tools/handler.js +18 -0
  33. package/dist/tools/registry.d.ts +2 -0
  34. package/dist/tools/registry.js +42 -0
  35. package/dist/types/events.d.ts +41 -0
  36. package/dist/types/events.js +1 -0
  37. package/dist/types/options.d.ts +41 -0
  38. package/dist/types/options.js +1 -0
  39. package/dist/types/protocol.d.ts +42 -0
  40. package/dist/types/protocol.js +1 -0
  41. package/dist/types/results.d.ts +17 -0
  42. package/dist/types/results.js +1 -0
  43. package/dist/writer.d.ts +7 -0
  44. package/dist/writer.js +26 -0
  45. package/package.json +61 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Pavel Ivanov
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,78 @@
1
+ # claude-wire
2
+
3
+ Run [Claude Code](https://claude.ai/download) programmatically from TypeScript.
4
+
5
+ [![npm](https://img.shields.io/npm/v/@pivanov/claude-wire)](https://www.npmjs.com/package/@pivanov/claude-wire)
6
+ [![npm bundle size](https://img.shields.io/bundlephobia/minzip/@pivanov/claude-wire)](https://bundlephobia.com/package/@pivanov/claude-wire)
7
+ [![license](https://img.shields.io/npm/l/claude-wire)](./LICENSE)
8
+
9
+ ```ts
10
+ import { claude } from "@pivanov/claude-wire";
11
+
12
+ const result = await claude.ask("Fix the bug in main.ts", {
13
+ model: "haiku",
14
+ maxCostUsd: 0.50,
15
+ });
16
+
17
+ console.log(result.text); // "Fixed the undefined variable..."
18
+ console.log(result.costUsd); // 0.0084
19
+ ```
20
+
21
+ ## Features
22
+
23
+ - **Simple API** - `claude.ask()` returns a typed result, `claude.stream()` yields events
24
+ - **Tool control** - allow, block, or intercept any tool at runtime
25
+ - **Multi-turn sessions** - persistent process across multiple prompts
26
+ - **Cost tracking** - per-request budgets with auto-abort
27
+ - **Fully typed** - discriminated union events, full IntelliSense
28
+ - **Resilient** - auto-respawn, transient error detection, AbortSignal
29
+ - **Zero dependencies** - 13 kB gzipped
30
+
31
+ ## Install
32
+
33
+ ```bash
34
+ bun add @pivanov/claude-wire
35
+ # or
36
+ npm install @pivanov/claude-wire
37
+ ```
38
+
39
+ Requires [Claude Code CLI](https://claude.ai/download) installed and authenticated. Runs on [Bun](https://bun.sh) >= 1.0 or Node.js >= 22.
40
+
41
+ > This SDK wraps Claude Code's `--output-format stream-json` protocol, which is not officially documented by Anthropic and may change between releases.
42
+
43
+ ## Documentation
44
+
45
+ Full docs, API reference, and protocol guide at **[pivanov.github.io/claude-wire](https://pivanov.github.io/claude-wire/)**
46
+
47
+ ## Try the Examples
48
+
49
+ ```bash
50
+ git clone https://github.com/pivanov/claude-wire
51
+ cd claude-wire && bun install
52
+ bun run examples
53
+ ```
54
+
55
+ Interactive menu with 8 runnable demos covering ask, streaming, sessions, tool control, cost budgets, abort, system prompts, and session resume.
56
+
57
+ ## Project Structure
58
+
59
+ ```
60
+ packages/claude-wire/ the npm package
61
+ apps/docs/ VitePress documentation site
62
+ apps/examples/ interactive example runner
63
+ ```
64
+
65
+ ## Development
66
+
67
+ ```bash
68
+ bun install
69
+ bun run test # 147 tests
70
+ bun run typecheck
71
+ bun run lint
72
+ bun run docs:dev # local docs server
73
+ bun run examples # try the examples
74
+ ```
75
+
76
+ ## License
77
+
78
+ MIT
@@ -0,0 +1,11 @@
1
+ import type { IClaudeSession } from "./session.js";
2
+ import type { IClaudeStream } from "./stream.js";
3
+ import type { IClaudeOptions, ISessionOptions } from "./types/options.js";
4
+ import type { TAskResult } from "./types/results.js";
5
+ export interface IClaudeClient {
6
+ ask: (prompt: string, options?: IClaudeOptions) => Promise<TAskResult>;
7
+ stream: (prompt: string, options?: IClaudeOptions) => IClaudeStream;
8
+ session: (options?: ISessionOptions) => IClaudeSession;
9
+ create: (defaults: IClaudeOptions) => IClaudeClient;
10
+ }
11
+ export declare const createClient: (defaults?: IClaudeOptions) => IClaudeClient;
package/dist/client.js ADDED
@@ -0,0 +1,28 @@
1
+ import { createSession } from "./session.js";
2
+ import { createStream } from "./stream.js";
3
+ const mergeOptions = (defaults, overrides) => ({
4
+ ...defaults,
5
+ ...overrides,
6
+ tools: overrides && "tools" in overrides ? (overrides.tools ? { ...defaults.tools, ...overrides.tools } : overrides.tools) : defaults.tools,
7
+ env: overrides && "env" in overrides ? (overrides.env ? { ...defaults.env, ...overrides.env } : overrides.env) : defaults.env,
8
+ });
9
+ export const createClient = (defaults = {}) => {
10
+ const ask = async (prompt, options) => {
11
+ const merged = mergeOptions(defaults, options);
12
+ const stream = createStream(prompt, merged);
13
+ return stream.result();
14
+ };
15
+ const stream = (prompt, options) => {
16
+ const merged = mergeOptions(defaults, options);
17
+ return createStream(prompt, merged);
18
+ };
19
+ const session = (options) => {
20
+ const merged = mergeOptions(defaults, options);
21
+ return createSession(merged);
22
+ };
23
+ const create = (newDefaults) => {
24
+ const merged = mergeOptions(defaults, newDefaults);
25
+ return createClient(merged);
26
+ };
27
+ return { ask, stream, session, create };
28
+ };
@@ -0,0 +1,13 @@
1
+ export declare const TIMEOUTS: {
2
+ readonly defaultAbortMs: 300000;
3
+ readonly gracefulExitMs: 5000;
4
+ };
5
+ export declare const LIMITS: {
6
+ readonly maxRespawnAttempts: 3;
7
+ readonly sessionMaxTurnsBeforeRecycle: 100;
8
+ readonly ndjsonMaxLineChars: number;
9
+ };
10
+ export declare const BINARY: {
11
+ readonly name: "claude";
12
+ readonly commonPaths: readonly [`${string}/.local/bin/claude`, `${string}/.claude/bin/claude`, "/usr/local/bin/claude", "/opt/homebrew/bin/claude"];
13
+ };
@@ -0,0 +1,15 @@
1
+ import { homedir } from "node:os";
2
+ export const TIMEOUTS = {
3
+ defaultAbortMs: 300_000,
4
+ gracefulExitMs: 5_000,
5
+ };
6
+ export const LIMITS = {
7
+ maxRespawnAttempts: 3,
8
+ sessionMaxTurnsBeforeRecycle: 100,
9
+ ndjsonMaxLineChars: 10 * 1024 * 1024,
10
+ };
11
+ const home = homedir();
12
+ export const BINARY = {
13
+ name: "claude",
14
+ commonPaths: [`${home}/.local/bin/claude`, `${home}/.claude/bin/claude`, "/usr/local/bin/claude", "/opt/homebrew/bin/claude"],
15
+ };
package/dist/cost.d.ts ADDED
@@ -0,0 +1,12 @@
1
+ import type { TCostSnapshot } from "./types/results.js";
2
+ export interface ICostTracker {
3
+ update: (totalCostUsd: number, totalInputTokens: number, totalOutputTokens: number) => void;
4
+ snapshot: () => TCostSnapshot;
5
+ checkBudget: () => void;
6
+ reset: () => void;
7
+ }
8
+ export interface ICostTrackerOptions {
9
+ maxCostUsd?: number;
10
+ onCostUpdate?: (cost: TCostSnapshot) => void;
11
+ }
12
+ export declare const createCostTracker: (options?: ICostTrackerOptions) => ICostTracker;
package/dist/cost.js ADDED
@@ -0,0 +1,36 @@
1
+ import { assertPositiveNumber, BudgetExceededError } from "./errors.js";
2
+ export const createCostTracker = (options = {}) => {
3
+ assertPositiveNumber(options.maxCostUsd, "maxCostUsd");
4
+ let totalUsd = 0;
5
+ let inputTokens = 0;
6
+ let outputTokens = 0;
7
+ const snapshot = () => ({
8
+ totalUsd,
9
+ inputTokens,
10
+ outputTokens,
11
+ });
12
+ const update = (totalCostUsd, totalInputToks, totalOutputToks) => {
13
+ totalUsd = totalCostUsd;
14
+ inputTokens = totalInputToks;
15
+ outputTokens = totalOutputToks;
16
+ if (options.onCostUpdate) {
17
+ try {
18
+ options.onCostUpdate(snapshot());
19
+ }
20
+ catch {
21
+ // user callback error - don't crash the stream/session
22
+ }
23
+ }
24
+ };
25
+ const checkBudget = () => {
26
+ if (options.maxCostUsd !== undefined && totalUsd > options.maxCostUsd) {
27
+ throw new BudgetExceededError(totalUsd, options.maxCostUsd);
28
+ }
29
+ };
30
+ const reset = () => {
31
+ totalUsd = 0;
32
+ inputTokens = 0;
33
+ outputTokens = 0;
34
+ };
35
+ return { update, snapshot, checkBudget, reset };
36
+ };
@@ -0,0 +1,29 @@
1
+ export declare class ClaudeError extends Error {
2
+ constructor(message: string);
3
+ }
4
+ export declare class BudgetExceededError extends ClaudeError {
5
+ readonly spent: number;
6
+ readonly budget: number;
7
+ constructor(spent: number, budget: number);
8
+ }
9
+ export declare class AbortError extends ClaudeError {
10
+ constructor(message?: string);
11
+ }
12
+ export declare class TimeoutError extends ClaudeError {
13
+ constructor(message?: string);
14
+ }
15
+ export declare class ProcessError extends ClaudeError {
16
+ readonly exitCode?: number | undefined;
17
+ constructor(message: string, exitCode?: number | undefined);
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];
21
+ export declare class KnownError extends ClaudeError {
22
+ readonly code: TKnownErrorCode;
23
+ constructor(code: TKnownErrorCode, message?: string);
24
+ }
25
+ export declare const isKnownError: (error: unknown) => error is KnownError;
26
+ export declare const isTransientError: (error: unknown) => boolean;
27
+ export declare const errorMessage: (error: unknown) => string;
28
+ export declare const assertPositiveNumber: (value: number | undefined, name: string) => void;
29
+ export {};
package/dist/errors.js ADDED
@@ -0,0 +1,68 @@
1
+ export class ClaudeError extends Error {
2
+ constructor(message) {
3
+ super(message);
4
+ this.name = "ClaudeError";
5
+ }
6
+ }
7
+ export class BudgetExceededError extends ClaudeError {
8
+ spent;
9
+ budget;
10
+ constructor(spent, budget) {
11
+ super(`Budget exceeded: $${spent.toFixed(4)} spent, $${budget.toFixed(4)} limit`);
12
+ this.spent = spent;
13
+ this.budget = budget;
14
+ this.name = "BudgetExceededError";
15
+ }
16
+ }
17
+ export class AbortError extends ClaudeError {
18
+ constructor(message = "Operation aborted") {
19
+ super(message);
20
+ this.name = "AbortError";
21
+ }
22
+ }
23
+ export class TimeoutError extends ClaudeError {
24
+ constructor(message = "Operation timed out") {
25
+ super(message);
26
+ this.name = "TimeoutError";
27
+ }
28
+ }
29
+ export class ProcessError extends ClaudeError {
30
+ exitCode;
31
+ constructor(message, exitCode) {
32
+ super(message);
33
+ this.exitCode = exitCode;
34
+ this.name = "ProcessError";
35
+ }
36
+ }
37
+ const KNOWN_ERROR_CODES = ["not-authenticated", "binary-not-found", "session-expired", "permission-denied", "invalid-model"];
38
+ export class KnownError extends ClaudeError {
39
+ code;
40
+ constructor(code, message) {
41
+ super(message ?? code);
42
+ this.code = code;
43
+ this.name = "KnownError";
44
+ }
45
+ }
46
+ export const isKnownError = (error) => {
47
+ return error instanceof KnownError;
48
+ };
49
+ const TRANSIENT_PATTERN = /fetch failed|ECONNREFUSED|ETIMEDOUT|ECONNRESET|EAI_AGAIN|network error|network timeout|EPIPE|SIGPIPE|broken pipe/i;
50
+ export const isTransientError = (error) => {
51
+ if (error instanceof AbortError || error instanceof BudgetExceededError) {
52
+ return false;
53
+ }
54
+ if (error instanceof ProcessError) {
55
+ const transientCodes = [137, 143];
56
+ return error.exitCode !== undefined && transientCodes.includes(error.exitCode);
57
+ }
58
+ const message = errorMessage(error);
59
+ return TRANSIENT_PATTERN.test(message);
60
+ };
61
+ export const errorMessage = (error) => {
62
+ return error instanceof Error ? error.message : String(error);
63
+ };
64
+ export const assertPositiveNumber = (value, name) => {
65
+ if (value !== undefined && (!Number.isFinite(value) || value <= 0)) {
66
+ throw new ClaudeError(`${name} must be a finite positive number`);
67
+ }
68
+ };
@@ -0,0 +1,27 @@
1
+ export type { IClaudeClient } from "./client.js";
2
+ export { createClient } from "./client.js";
3
+ export { BINARY, LIMITS, TIMEOUTS } from "./constants.js";
4
+ export type { ICostTracker, ICostTrackerOptions } from "./cost.js";
5
+ export { createCostTracker } from "./cost.js";
6
+ export { AbortError, assertPositiveNumber, BudgetExceededError, ClaudeError, errorMessage, isKnownError, isTransientError, KnownError, ProcessError, TimeoutError, } from "./errors.js";
7
+ export { blockFingerprint, extractContent, parseDoubleEncoded } from "./parser/content.js";
8
+ export { parseLine } from "./parser/ndjson.js";
9
+ export type { ITranslator } from "./parser/translator.js";
10
+ export { createTranslator } from "./parser/translator.js";
11
+ export type { IClaudeProcess, ISpawnOptions } from "./process.js";
12
+ export { buildArgs, resetBinaryCache, spawnClaude } from "./process.js";
13
+ export type { IReaderOptions } from "./reader.js";
14
+ export { readNdjsonEvents } from "./reader.js";
15
+ export type { IClaudeSession } from "./session.js";
16
+ export { createSession } from "./session.js";
17
+ export type { IClaudeStream } from "./stream.js";
18
+ export { createStream } from "./stream.js";
19
+ export type { IToolHandlerInstance, TToolDecision } from "./tools/handler.js";
20
+ export { createToolHandler } from "./tools/handler.js";
21
+ export { BUILT_IN_TOOLS, isBuiltInTool } from "./tools/registry.js";
22
+ export type { TErrorEvent, TRelayEvent, TSessionMetaEvent, TTextEvent, TThinkingEvent, TToolResultEvent, TToolUseEvent, TTurnCompleteEvent, } from "./types/events.js";
23
+ export type { IClaudeOptions, ISessionOptions, IToolHandler } from "./types/options.js";
24
+ export type { TClaudeContent, TClaudeContentType, TClaudeEvent, TClaudeEventType, TClaudeMessage, TModelUsageEntry } from "./types/protocol.js";
25
+ export type { TAskResult, TCostSnapshot } from "./types/results.js";
26
+ export { writer } from "./writer.js";
27
+ export declare const claude: import("./client.js").IClaudeClient;
package/dist/index.js ADDED
@@ -0,0 +1,16 @@
1
+ export { createClient } from "./client.js";
2
+ export { BINARY, LIMITS, TIMEOUTS } from "./constants.js";
3
+ export { createCostTracker } from "./cost.js";
4
+ export { AbortError, assertPositiveNumber, BudgetExceededError, ClaudeError, errorMessage, isKnownError, isTransientError, KnownError, ProcessError, TimeoutError, } from "./errors.js";
5
+ export { blockFingerprint, extractContent, parseDoubleEncoded } from "./parser/content.js";
6
+ export { parseLine } from "./parser/ndjson.js";
7
+ export { createTranslator } from "./parser/translator.js";
8
+ export { buildArgs, resetBinaryCache, spawnClaude } from "./process.js";
9
+ export { readNdjsonEvents } from "./reader.js";
10
+ export { createSession } from "./session.js";
11
+ export { createStream } from "./stream.js";
12
+ export { createToolHandler } from "./tools/handler.js";
13
+ export { BUILT_IN_TOOLS, isBuiltInTool } from "./tools/registry.js";
14
+ export { writer } from "./writer.js";
15
+ import { createClient } from "./client.js";
16
+ export const claude = createClient();
@@ -0,0 +1,4 @@
1
+ import type { TClaudeContent } from "../types/protocol.js";
2
+ export declare const blockFingerprint: (block: TClaudeContent) => string;
3
+ export declare const extractContent: (content: unknown) => string;
4
+ export declare const parseDoubleEncoded: (value: unknown) => string;
@@ -0,0 +1,46 @@
1
+ export const blockFingerprint = (block) => {
2
+ if (block.type === "tool_use" && block.id) {
3
+ return `tool_use:${block.id}`;
4
+ }
5
+ const text = block.type === "thinking" ? (block.thinking ?? block.text ?? "") : (block.text ?? "");
6
+ if (text) {
7
+ return `${block.type}:${text.slice(0, 64)}`;
8
+ }
9
+ return `${block.type}:${block.tool_use_id ?? "unknown"}`;
10
+ };
11
+ export const extractContent = (content) => {
12
+ if (content === null || content === undefined) {
13
+ return "";
14
+ }
15
+ if (typeof content === "string") {
16
+ return content;
17
+ }
18
+ if (Array.isArray(content)) {
19
+ return content
20
+ .filter((block) => typeof block === "object" && block !== null && "text" in block)
21
+ .map((block) => block.text)
22
+ .join("\n");
23
+ }
24
+ return "";
25
+ };
26
+ export const parseDoubleEncoded = (value) => {
27
+ if (typeof value !== "string") {
28
+ if (typeof value === "object" && value !== null) {
29
+ return JSON.stringify(value);
30
+ }
31
+ return String(value ?? "");
32
+ }
33
+ if (!value.startsWith('"')) {
34
+ return value;
35
+ }
36
+ try {
37
+ const parsed = JSON.parse(value);
38
+ if (typeof parsed === "string") {
39
+ return parsed;
40
+ }
41
+ return value;
42
+ }
43
+ catch {
44
+ return value;
45
+ }
46
+ };
@@ -0,0 +1,2 @@
1
+ import type { TClaudeEvent } from "../types/protocol.js";
2
+ export declare const parseLine: (line: string) => TClaudeEvent | undefined;
@@ -0,0 +1,12 @@
1
+ export const parseLine = (line) => {
2
+ const trimmed = line.trim();
3
+ if (trimmed === "") {
4
+ return undefined;
5
+ }
6
+ try {
7
+ return JSON.parse(trimmed);
8
+ }
9
+ catch {
10
+ return undefined;
11
+ }
12
+ };
@@ -0,0 +1,7 @@
1
+ import type { TRelayEvent } from "../types/events.js";
2
+ import type { TClaudeEvent } from "../types/protocol.js";
3
+ export interface ITranslator {
4
+ translate: (raw: TClaudeEvent) => TRelayEvent[];
5
+ reset: () => void;
6
+ }
7
+ export declare const createTranslator: () => ITranslator;
@@ -0,0 +1,127 @@
1
+ import { blockFingerprint, extractContent, parseDoubleEncoded } from "./content.js";
2
+ const extractTokens = (modelUsage) => {
3
+ let inputTokens;
4
+ let outputTokens;
5
+ let contextWindow;
6
+ if (modelUsage) {
7
+ for (const entry of Object.values(modelUsage)) {
8
+ inputTokens = (inputTokens ?? 0) + entry.inputTokens + (entry.cacheReadInputTokens ?? 0) + (entry.cacheCreationInputTokens ?? 0);
9
+ outputTokens = (outputTokens ?? 0) + entry.outputTokens;
10
+ contextWindow = entry.contextWindow;
11
+ }
12
+ }
13
+ return { inputTokens, outputTokens, contextWindow };
14
+ };
15
+ const buildTurnComplete = (raw) => {
16
+ const { inputTokens, outputTokens, contextWindow } = extractTokens(raw.modelUsage);
17
+ return {
18
+ type: "turn_complete",
19
+ sessionId: raw.session_id,
20
+ costUsd: raw.total_cost_usd,
21
+ inputTokens,
22
+ outputTokens,
23
+ contextWindow,
24
+ durationMs: raw.duration_ms,
25
+ };
26
+ };
27
+ const translateContentBlock = (block) => {
28
+ switch (block.type) {
29
+ case "thinking": {
30
+ const content = block.thinking ?? block.text ?? "";
31
+ if (content) {
32
+ return { type: "thinking", content };
33
+ }
34
+ return undefined;
35
+ }
36
+ case "text": {
37
+ const content = block.text ?? "";
38
+ if (content) {
39
+ return { type: "text", content };
40
+ }
41
+ return undefined;
42
+ }
43
+ case "tool_use": {
44
+ if (!block.id) {
45
+ return undefined;
46
+ }
47
+ return {
48
+ type: "tool_use",
49
+ toolUseId: block.id,
50
+ toolName: block.name ?? "",
51
+ input: typeof block.input === "string" ? block.input : JSON.stringify(block.input ?? {}),
52
+ };
53
+ }
54
+ case "tool_result": {
55
+ return {
56
+ type: "tool_result",
57
+ toolUseId: block.tool_use_id ?? "",
58
+ output: extractContent(block.content),
59
+ isError: block.is_error ?? false,
60
+ };
61
+ }
62
+ default:
63
+ return undefined;
64
+ }
65
+ };
66
+ export const createTranslator = () => {
67
+ let lastContentIndex = 0;
68
+ let lastFirstBlockKey;
69
+ const reset = () => {
70
+ lastContentIndex = 0;
71
+ lastFirstBlockKey = undefined;
72
+ };
73
+ const translate = (raw) => {
74
+ const events = [];
75
+ if (raw.type === "system" && raw.subtype === "init") {
76
+ events.push({
77
+ type: "session_meta",
78
+ sessionId: raw.session_id ?? "",
79
+ model: raw.model ?? "",
80
+ tools: raw.tools ?? [],
81
+ });
82
+ return events;
83
+ }
84
+ if (raw.type === "result" || (raw.type === "system" && raw.subtype === "result")) {
85
+ if (raw.is_error) {
86
+ const text = parseDoubleEncoded(raw.result);
87
+ events.push({ type: "error", message: text, sessionId: raw.session_id });
88
+ }
89
+ events.push(buildTurnComplete(raw));
90
+ reset();
91
+ return events;
92
+ }
93
+ if (raw.type === "assistant" && raw.message?.content) {
94
+ const content = raw.message.content;
95
+ if (content.length > 0) {
96
+ const firstBlock = content[0];
97
+ if (firstBlock) {
98
+ const key = blockFingerprint(firstBlock);
99
+ if (lastFirstBlockKey !== undefined && key !== lastFirstBlockKey) {
100
+ lastContentIndex = 0;
101
+ }
102
+ lastFirstBlockKey = key;
103
+ }
104
+ }
105
+ const newBlocks = content.slice(lastContentIndex);
106
+ lastContentIndex = content.length;
107
+ for (const block of newBlocks) {
108
+ const event = translateContentBlock(block);
109
+ if (event) {
110
+ events.push(event);
111
+ }
112
+ }
113
+ return events;
114
+ }
115
+ if (raw.type === "user" && raw.message?.content) {
116
+ for (const block of raw.message.content) {
117
+ const event = translateContentBlock(block);
118
+ if (event) {
119
+ events.push(event);
120
+ }
121
+ }
122
+ return events;
123
+ }
124
+ return events;
125
+ };
126
+ return { translate, reset };
127
+ };
@@ -0,0 +1,8 @@
1
+ import type { ICostTracker } from "./cost.js";
2
+ import type { IClaudeProcess } from "./process.js";
3
+ 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
+ export declare const dispatchToolDecision: (proc: IClaudeProcess, toolHandler: IToolHandlerInstance, event: TToolUseEvent) => Promise<void>;
7
+ export declare const extractText: (events: TRelayEvent[]) => string;
8
+ export declare const buildResult: (events: TRelayEvent[], costTracker: ICostTracker, sessionId: string | undefined) => TAskResult;
@@ -0,0 +1,47 @@
1
+ import { writer } from "./writer.js";
2
+ export const dispatchToolDecision = async (proc, toolHandler, event) => {
3
+ let decision;
4
+ try {
5
+ decision = await toolHandler.decide(event);
6
+ }
7
+ catch (error) {
8
+ console.warn(`[claude-wire] Tool handler threw, defaulting to deny: ${error instanceof Error ? error.message : String(error)}`);
9
+ decision = "deny";
10
+ }
11
+ try {
12
+ if (decision === "approve") {
13
+ proc.write(writer.approve(event.toolUseId));
14
+ }
15
+ else if (decision === "deny") {
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
+ }
26
+ catch {
27
+ // stdin closed - process died, error will surface through read path
28
+ }
29
+ };
30
+ export const extractText = (events) => {
31
+ return events
32
+ .filter((e) => e.type === "text")
33
+ .map((e) => e.content)
34
+ .join("");
35
+ };
36
+ export const buildResult = (events, costTracker, sessionId) => {
37
+ const tc = events.findLast((e) => e.type === "turn_complete");
38
+ const snap = costTracker.snapshot();
39
+ return {
40
+ text: extractText(events),
41
+ costUsd: snap.totalUsd,
42
+ tokens: { input: snap.inputTokens, output: snap.outputTokens },
43
+ duration: tc?.durationMs ?? 0,
44
+ sessionId,
45
+ events,
46
+ };
47
+ };
@@ -0,0 +1,15 @@
1
+ import type { IClaudeOptions } from "./types/options.js";
2
+ export interface IClaudeProcess {
3
+ write: (message: string) => void;
4
+ kill: () => void;
5
+ exited: Promise<number>;
6
+ stdout: ReadableStream<Uint8Array>;
7
+ stderr: ReadableStream<Uint8Array>;
8
+ pid: number;
9
+ }
10
+ export interface ISpawnOptions extends IClaudeOptions {
11
+ prompt?: string;
12
+ }
13
+ export declare const resetBinaryCache: () => void;
14
+ export declare const buildArgs: (options: ISpawnOptions, binaryPath: string) => string[];
15
+ export declare const spawnClaude: (options: ISpawnOptions) => IClaudeProcess;