@runfusion/fusion 0.1.2 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/dist/bin.js +2069 -865
- package/dist/client/assets/index-BuenKJX0.css +1 -0
- package/dist/client/assets/index-CjGu8HRV.js +1250 -0
- package/dist/client/index.html +2 -2
- package/dist/client/sw.js +45 -1
- package/dist/client/theme-data.css +109 -0
- package/dist/extension.js +797 -345
- package/dist/pi-claude-cli/index.ts +131 -0
- package/dist/pi-claude-cli/package.json +39 -0
- package/dist/pi-claude-cli/src/control-handler.ts +68 -0
- package/dist/pi-claude-cli/src/event-bridge.ts +386 -0
- package/dist/pi-claude-cli/src/mcp-config.ts +111 -0
- package/dist/pi-claude-cli/src/mcp-schema-server.cjs +49 -0
- package/dist/pi-claude-cli/src/process-manager.ts +218 -0
- package/dist/pi-claude-cli/src/prompt-builder.ts +536 -0
- package/dist/pi-claude-cli/src/provider.ts +354 -0
- package/dist/pi-claude-cli/src/stream-parser.ts +37 -0
- package/dist/pi-claude-cli/src/thinking-config.ts +83 -0
- package/dist/pi-claude-cli/src/tool-mapping.ts +147 -0
- package/dist/pi-claude-cli/src/types.ts +87 -0
- package/package.json +6 -5
- package/skill/fusion/SKILL.md +5 -3
- package/skill/fusion/references/cli-commands.md +22 -22
- package/skill/fusion/references/extension-tools.md +3 -1
- package/skill/fusion/references/fusion-capabilities.md +28 -35
- package/skill/fusion/references/task-structure.md +4 -4
- package/skill/fusion/workflows/dashboard-cli.md +6 -6
- package/skill/fusion/workflows/specifications.md +5 -3
- package/skill/fusion/workflows/task-lifecycle.md +1 -1
- package/skill/fusion/workflows/task-management.md +3 -1
- package/dist/client/assets/index-Djv5vKo0.css +0 -1
- package/dist/client/assets/index-zfXYuUXG.js +0 -1241
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pi extension entry point for pi-claude-cli.
|
|
3
|
+
*
|
|
4
|
+
* Registers a custom provider that routes LLM calls through the Claude Code CLI
|
|
5
|
+
* subprocess using stream-json NDJSON protocol.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { getModels } from "@mariozechner/pi-ai";
|
|
9
|
+
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
10
|
+
import { streamViaCli } from "./src/provider.js";
|
|
11
|
+
import {
|
|
12
|
+
validateCliPresence,
|
|
13
|
+
validateCliAuth,
|
|
14
|
+
killAllProcesses,
|
|
15
|
+
} from "./src/process-manager.js";
|
|
16
|
+
import { getCustomToolDefs, writeMcpConfig } from "./src/mcp-config.js";
|
|
17
|
+
|
|
18
|
+
// Kill all active Claude subprocesses on process exit to prevent orphans
|
|
19
|
+
process.on("exit", killAllProcesses);
|
|
20
|
+
|
|
21
|
+
const PROVIDER_ID = "pi-claude-cli";
|
|
22
|
+
|
|
23
|
+
let mcpConfigPath: string | undefined;
|
|
24
|
+
let mcpConfigResolved = false;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Lazily generate MCP config on first request (not at load time).
|
|
28
|
+
* pi.getAllTools() fails during extension loading; this defers it
|
|
29
|
+
* until the pi runtime is fully initialized.
|
|
30
|
+
*
|
|
31
|
+
* Only locks (sets mcpConfigResolved) when getAllTools() returns a
|
|
32
|
+
* real array — if it returns undefined/null (registry not ready),
|
|
33
|
+
* we retry on the next request. Once the registry is ready we
|
|
34
|
+
* commit to the result even if there are zero custom tools.
|
|
35
|
+
*
|
|
36
|
+
* Uses warn-don't-block: failure logs a warning but does not
|
|
37
|
+
* prevent the provider from functioning (built-ins still work).
|
|
38
|
+
*/
|
|
39
|
+
function ensureMcpConfig(pi: ExtensionAPI): string | undefined {
|
|
40
|
+
if (mcpConfigResolved) return mcpConfigPath;
|
|
41
|
+
try {
|
|
42
|
+
const allTools = pi.getAllTools();
|
|
43
|
+
|
|
44
|
+
// Registry not ready yet — don't lock, retry on next call
|
|
45
|
+
if (!Array.isArray(allTools)) {
|
|
46
|
+
return mcpConfigPath;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Registry is ready — lock regardless of whether custom tools exist
|
|
50
|
+
mcpConfigResolved = true;
|
|
51
|
+
|
|
52
|
+
const toolDefs = getCustomToolDefs(pi);
|
|
53
|
+
if (toolDefs.length > 0) {
|
|
54
|
+
mcpConfigPath = writeMcpConfig(toolDefs);
|
|
55
|
+
console.error(
|
|
56
|
+
`[pi-claude-cli] MCP config generated with ${toolDefs.length} custom tool(s)`,
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
} catch (err) {
|
|
60
|
+
console.warn(
|
|
61
|
+
"[pi-claude-cli] MCP config generation failed, custom tools unavailable:",
|
|
62
|
+
err,
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
return mcpConfigPath;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export default function (pi: ExtensionAPI) {
|
|
69
|
+
try {
|
|
70
|
+
// Startup validation
|
|
71
|
+
validateCliPresence(); // throws if CLI not on PATH
|
|
72
|
+
validateCliAuth(); // warns if not authenticated
|
|
73
|
+
|
|
74
|
+
const catalogModels = getModels("anthropic").map((model) => ({
|
|
75
|
+
id: model.id,
|
|
76
|
+
name: model.name,
|
|
77
|
+
reasoning: model.reasoning,
|
|
78
|
+
input: model.input,
|
|
79
|
+
cost: model.cost,
|
|
80
|
+
contextWindow: model.contextWindow,
|
|
81
|
+
maxTokens: model.maxTokens,
|
|
82
|
+
}));
|
|
83
|
+
|
|
84
|
+
// Newer models released after the pinned @mariozechner/pi-ai catalog
|
|
85
|
+
// was generated. Dedupe by id so this list is harmless once the upstream
|
|
86
|
+
// catalog catches up.
|
|
87
|
+
// https://platform.claude.com/docs/en/about-claude/models/overview
|
|
88
|
+
const extraModels: typeof catalogModels = [
|
|
89
|
+
{
|
|
90
|
+
id: "claude-opus-4-7",
|
|
91
|
+
name: "Claude Opus 4.7",
|
|
92
|
+
reasoning: true,
|
|
93
|
+
input: ["text", "image"],
|
|
94
|
+
cost: { input: 5, output: 25, cacheRead: 0.5, cacheWrite: 6.25 },
|
|
95
|
+
contextWindow: 1_000_000,
|
|
96
|
+
maxTokens: 128_000,
|
|
97
|
+
},
|
|
98
|
+
];
|
|
99
|
+
|
|
100
|
+
const seen = new Set(catalogModels.map((m) => m.id));
|
|
101
|
+
const models = [
|
|
102
|
+
...catalogModels,
|
|
103
|
+
...extraModels.filter((m) => !seen.has(m.id)),
|
|
104
|
+
];
|
|
105
|
+
|
|
106
|
+
// Ensure all registered tools are active so pi can execute them.
|
|
107
|
+
// Some tools (find, grep, ls) are registered but not activated by default.
|
|
108
|
+
pi.on("session_start", async () => {
|
|
109
|
+
const allTools = pi.getAllTools();
|
|
110
|
+
if (Array.isArray(allTools)) {
|
|
111
|
+
pi.setActiveTools(allTools.map((t: { name: string }) => t.name));
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
pi.registerProvider(PROVIDER_ID, {
|
|
116
|
+
baseUrl: "pi-claude-cli",
|
|
117
|
+
apiKey: "unused",
|
|
118
|
+
api: "pi-claude-cli",
|
|
119
|
+
models,
|
|
120
|
+
streamSimple: (model, context, options) => {
|
|
121
|
+
const configPath = ensureMcpConfig(pi);
|
|
122
|
+
return streamViaCli(model, context, {
|
|
123
|
+
...options,
|
|
124
|
+
mcpConfigPath: configPath,
|
|
125
|
+
});
|
|
126
|
+
},
|
|
127
|
+
});
|
|
128
|
+
} catch (err) {
|
|
129
|
+
console.error(`[pi-claude-cli] Failed to register provider:`, err);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@fusion/pi-claude-cli",
|
|
3
|
+
"version": "0.3.1",
|
|
4
|
+
"description": "Fusion vendored fork: pi coding-agent extension that routes LLM calls through the Claude Code CLI. Forked from rchern/pi-claude-cli (MIT). See UPSTREAM.md.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"private": true,
|
|
7
|
+
"type": "module",
|
|
8
|
+
"main": "index.ts",
|
|
9
|
+
"keywords": [
|
|
10
|
+
"pi-package"
|
|
11
|
+
],
|
|
12
|
+
"pi": {
|
|
13
|
+
"extensions": [
|
|
14
|
+
"index.ts"
|
|
15
|
+
]
|
|
16
|
+
},
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "https://github.com/Runfusion/Fusion",
|
|
20
|
+
"directory": "packages/pi-claude-cli"
|
|
21
|
+
},
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"cross-spawn": "^7.0.6"
|
|
24
|
+
},
|
|
25
|
+
"peerDependencies": {
|
|
26
|
+
"@mariozechner/pi-ai": "*",
|
|
27
|
+
"@mariozechner/pi-coding-agent": "*"
|
|
28
|
+
},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"@types/cross-spawn": "^6.0.6",
|
|
31
|
+
"@types/node": "^22.0.0",
|
|
32
|
+
"typescript": "^5.7.0",
|
|
33
|
+
"vitest": "^3.0.0"
|
|
34
|
+
},
|
|
35
|
+
"scripts": {
|
|
36
|
+
"test": "vitest run --reporter=dot",
|
|
37
|
+
"typecheck": "tsc --noEmit"
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Control protocol handler for Claude CLI stream-json communication.
|
|
3
|
+
*
|
|
4
|
+
* Processes control_request messages from Claude CLI stdout and writes
|
|
5
|
+
* control_response messages to stdin.
|
|
6
|
+
*
|
|
7
|
+
* - Custom MCP tools (mcp__custom-tools__*): DENIED — pi executes these
|
|
8
|
+
* - Everything else (user MCP tools, internal tools): ALLOWED — Claude handles
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { ClaudeControlRequest } from "./types";
|
|
12
|
+
import { CUSTOM_TOOLS_MCP_PREFIX } from "./tool-mapping.js";
|
|
13
|
+
|
|
14
|
+
export const TOOL_EXECUTION_DENIED_MESSAGE =
|
|
15
|
+
"Tool execution is unavailable in this environment.";
|
|
16
|
+
|
|
17
|
+
/** Prefix for MCP (Model Context Protocol) tool names. */
|
|
18
|
+
export const MCP_PREFIX = "mcp__";
|
|
19
|
+
|
|
20
|
+
interface ControlResponse {
|
|
21
|
+
type: "control_response";
|
|
22
|
+
request_id: string;
|
|
23
|
+
response: {
|
|
24
|
+
subtype: "success";
|
|
25
|
+
response: {
|
|
26
|
+
behavior: "allow" | "deny";
|
|
27
|
+
message?: string;
|
|
28
|
+
};
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Handle a control_request from the Claude CLI.
|
|
34
|
+
*
|
|
35
|
+
* Denies custom MCP tools (mcp__custom-tools__*) so pi can execute them.
|
|
36
|
+
* Allows everything else (user MCP tools, internal Claude tools).
|
|
37
|
+
*
|
|
38
|
+
* @returns true if the tool was allowed, false if denied
|
|
39
|
+
*/
|
|
40
|
+
export function handleControlRequest(
|
|
41
|
+
msg: ClaudeControlRequest,
|
|
42
|
+
stdin: NodeJS.WritableStream,
|
|
43
|
+
): boolean {
|
|
44
|
+
if (!msg.request_id || !msg.request) {
|
|
45
|
+
console.error(
|
|
46
|
+
"[pi-claude-cli] Malformed control_request: missing request_id or request object",
|
|
47
|
+
msg,
|
|
48
|
+
);
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const toolName = msg.request?.tool_name ?? "";
|
|
53
|
+
const isCustomTool = toolName.startsWith(CUSTOM_TOOLS_MCP_PREFIX);
|
|
54
|
+
|
|
55
|
+
const response: ControlResponse = {
|
|
56
|
+
type: "control_response",
|
|
57
|
+
request_id: msg.request_id,
|
|
58
|
+
response: {
|
|
59
|
+
subtype: "success",
|
|
60
|
+
response: isCustomTool
|
|
61
|
+
? { behavior: "deny", message: TOOL_EXECUTION_DENIED_MESSAGE }
|
|
62
|
+
: { behavior: "allow" },
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
stdin.write(JSON.stringify(response) + "\n");
|
|
67
|
+
return !isCustomTool;
|
|
68
|
+
}
|
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
import type { ClaudeApiEvent, TrackedContentBlock } from "./types";
|
|
2
|
+
import { calculateCost } from "@mariozechner/pi-ai";
|
|
3
|
+
import type {
|
|
4
|
+
Api,
|
|
5
|
+
AssistantMessage,
|
|
6
|
+
AssistantMessageEventStream,
|
|
7
|
+
Model,
|
|
8
|
+
TextContent,
|
|
9
|
+
ThinkingContent,
|
|
10
|
+
ToolCall,
|
|
11
|
+
} from "@mariozechner/pi-ai";
|
|
12
|
+
import {
|
|
13
|
+
mapClaudeToolNameToPi,
|
|
14
|
+
translateClaudeArgsToPi,
|
|
15
|
+
isPiKnownClaudeTool,
|
|
16
|
+
} from "./tool-mapping.js";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Extended tracking for tool_use content blocks during streaming.
|
|
20
|
+
* Stores the Claude tool name for argument translation at block_stop.
|
|
21
|
+
*/
|
|
22
|
+
interface TrackedToolBlock {
|
|
23
|
+
type: "tool_use";
|
|
24
|
+
index: number;
|
|
25
|
+
id: string;
|
|
26
|
+
name: string; // Already mapped to pi name
|
|
27
|
+
claudeName: string; // Original Claude name for arg translation
|
|
28
|
+
arguments: Record<string, unknown>;
|
|
29
|
+
partialJson: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Union of tracked block types for the blocks array. */
|
|
33
|
+
type TrackedBlock = TrackedContentBlock | TrackedToolBlock;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* The event bridge interface returned by createEventBridge.
|
|
37
|
+
* handleEvent processes each Claude API streaming event and pushes
|
|
38
|
+
* the appropriate pi events to the stream.
|
|
39
|
+
* getOutput returns the accumulated AssistantMessage.
|
|
40
|
+
*/
|
|
41
|
+
export interface EventBridge {
|
|
42
|
+
handleEvent(event: ClaudeApiEvent): void;
|
|
43
|
+
getOutput(): AssistantMessage;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Map Claude API stop reasons to pi's stop reason format.
|
|
48
|
+
*/
|
|
49
|
+
function mapStopReason(
|
|
50
|
+
reason: string | undefined,
|
|
51
|
+
): "stop" | "length" | "toolUse" {
|
|
52
|
+
switch (reason) {
|
|
53
|
+
case "tool_use":
|
|
54
|
+
return "toolUse";
|
|
55
|
+
case "max_tokens":
|
|
56
|
+
return "length";
|
|
57
|
+
case "end_turn":
|
|
58
|
+
default:
|
|
59
|
+
return "stop";
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Create an event bridge that translates Claude API streaming events
|
|
65
|
+
* into pi's AssistantMessageEventStream events.
|
|
66
|
+
*
|
|
67
|
+
* The bridge maintains internal state to track content blocks and
|
|
68
|
+
* accumulate the final AssistantMessage. It handles:
|
|
69
|
+
* - text content blocks (start/delta/stop -> text_start/text_delta/text_end)
|
|
70
|
+
* - message lifecycle (message_start for usage, message_delta for stop reason, message_stop for done)
|
|
71
|
+
* - unsupported block types (tool_use, thinking) with warnings
|
|
72
|
+
*/
|
|
73
|
+
export function createEventBridge(
|
|
74
|
+
stream: AssistantMessageEventStream,
|
|
75
|
+
model: Model<Api>,
|
|
76
|
+
): EventBridge {
|
|
77
|
+
// Tracked content blocks indexed by Claude's content_block index
|
|
78
|
+
const blocks: TrackedBlock[] = [];
|
|
79
|
+
|
|
80
|
+
// The accumulated output message
|
|
81
|
+
const output: AssistantMessage = {
|
|
82
|
+
role: "assistant" as const,
|
|
83
|
+
content: [] as (TextContent | ThinkingContent | ToolCall)[],
|
|
84
|
+
api: "pi-claude-cli",
|
|
85
|
+
provider: model.provider,
|
|
86
|
+
model: model.id,
|
|
87
|
+
usage: {
|
|
88
|
+
input: 0,
|
|
89
|
+
output: 0,
|
|
90
|
+
cacheRead: 0,
|
|
91
|
+
cacheWrite: 0,
|
|
92
|
+
totalTokens: 0,
|
|
93
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
94
|
+
},
|
|
95
|
+
stopReason: "stop" as const,
|
|
96
|
+
timestamp: Date.now(),
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
let started = false;
|
|
100
|
+
|
|
101
|
+
function handleEvent(event: ClaudeApiEvent): void {
|
|
102
|
+
// Emit start event on first message — tells pi to begin incremental rendering
|
|
103
|
+
if (!started) {
|
|
104
|
+
stream.push({ type: "start", partial: output });
|
|
105
|
+
started = true;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
switch (event.type) {
|
|
109
|
+
case "message_start":
|
|
110
|
+
handleMessageStart(event);
|
|
111
|
+
break;
|
|
112
|
+
case "content_block_start":
|
|
113
|
+
handleContentBlockStart(event);
|
|
114
|
+
break;
|
|
115
|
+
case "content_block_delta":
|
|
116
|
+
handleContentBlockDelta(event);
|
|
117
|
+
break;
|
|
118
|
+
case "content_block_stop":
|
|
119
|
+
handleContentBlockStop(event);
|
|
120
|
+
break;
|
|
121
|
+
case "message_delta":
|
|
122
|
+
handleMessageDelta(event);
|
|
123
|
+
break;
|
|
124
|
+
case "message_stop":
|
|
125
|
+
handleMessageStop();
|
|
126
|
+
break;
|
|
127
|
+
// Unknown event types are silently ignored
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function handleMessageStart(event: ClaudeApiEvent): void {
|
|
132
|
+
const usage = event.message?.usage;
|
|
133
|
+
if (usage) {
|
|
134
|
+
output.usage.input = usage.input_tokens ?? 0;
|
|
135
|
+
output.usage.output = usage.output_tokens ?? 0;
|
|
136
|
+
output.usage.cacheRead = usage.cache_read_input_tokens ?? 0;
|
|
137
|
+
output.usage.cacheWrite = usage.cache_creation_input_tokens ?? 0;
|
|
138
|
+
output.usage.totalTokens =
|
|
139
|
+
output.usage.input +
|
|
140
|
+
output.usage.output +
|
|
141
|
+
output.usage.cacheRead +
|
|
142
|
+
output.usage.cacheWrite;
|
|
143
|
+
calculateCost(model, output.usage);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function handleContentBlockStart(event: ClaudeApiEvent): void {
|
|
148
|
+
const blockType = event.content_block?.type;
|
|
149
|
+
|
|
150
|
+
if (blockType === "text") {
|
|
151
|
+
const block: TrackedContentBlock = {
|
|
152
|
+
type: "text",
|
|
153
|
+
text: "",
|
|
154
|
+
index: event.index ?? 0,
|
|
155
|
+
};
|
|
156
|
+
blocks.push(block);
|
|
157
|
+
output.content.push({ type: "text" as const, text: "" });
|
|
158
|
+
|
|
159
|
+
stream.push({
|
|
160
|
+
type: "text_start",
|
|
161
|
+
contentIndex: output.content.length - 1,
|
|
162
|
+
partial: output,
|
|
163
|
+
});
|
|
164
|
+
} else if (blockType === "thinking") {
|
|
165
|
+
const block: TrackedContentBlock = {
|
|
166
|
+
type: "thinking",
|
|
167
|
+
text: "",
|
|
168
|
+
index: event.index ?? 0,
|
|
169
|
+
};
|
|
170
|
+
blocks.push(block);
|
|
171
|
+
output.content.push({
|
|
172
|
+
type: "thinking" as const,
|
|
173
|
+
thinking: "",
|
|
174
|
+
thinkingSignature: "",
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
stream.push({
|
|
178
|
+
type: "thinking_start",
|
|
179
|
+
contentIndex: output.content.length - 1,
|
|
180
|
+
partial: output,
|
|
181
|
+
});
|
|
182
|
+
} else if (blockType === "tool_use") {
|
|
183
|
+
const claudeName = event.content_block!.name!;
|
|
184
|
+
|
|
185
|
+
// Skip internal Claude Code tools (ToolSearch, Task, Agent, etc.)
|
|
186
|
+
// that pi cannot execute — only emit pi-known tools
|
|
187
|
+
if (!isPiKnownClaudeTool(claudeName)) {
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const piName = mapClaudeToolNameToPi(claudeName);
|
|
192
|
+
const id = event.content_block!.id!;
|
|
193
|
+
|
|
194
|
+
const block: TrackedToolBlock = {
|
|
195
|
+
type: "tool_use",
|
|
196
|
+
index: event.index ?? 0,
|
|
197
|
+
id,
|
|
198
|
+
name: piName,
|
|
199
|
+
claudeName,
|
|
200
|
+
arguments: {},
|
|
201
|
+
partialJson: "",
|
|
202
|
+
};
|
|
203
|
+
blocks.push(block);
|
|
204
|
+
output.content.push({
|
|
205
|
+
type: "toolCall" as const,
|
|
206
|
+
id,
|
|
207
|
+
name: piName,
|
|
208
|
+
arguments: {},
|
|
209
|
+
} as ToolCall);
|
|
210
|
+
|
|
211
|
+
stream.push({
|
|
212
|
+
type: "toolcall_start",
|
|
213
|
+
contentIndex: output.content.length - 1,
|
|
214
|
+
partial: output,
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
// Unknown block types silently ignored
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function handleContentBlockDelta(event: ClaudeApiEvent): void {
|
|
221
|
+
const deltaType = event.delta?.type;
|
|
222
|
+
|
|
223
|
+
if (deltaType === "text_delta" && event.delta!.text != null) {
|
|
224
|
+
const idx = blocks.findIndex((b) => b.index === event.index);
|
|
225
|
+
if (idx === -1) return;
|
|
226
|
+
|
|
227
|
+
const block = blocks[idx];
|
|
228
|
+
if (block.type === "text") {
|
|
229
|
+
block.text += event.delta!.text;
|
|
230
|
+
const contentBlock = output.content[idx] as TextContent;
|
|
231
|
+
contentBlock.text = block.text;
|
|
232
|
+
|
|
233
|
+
stream.push({
|
|
234
|
+
type: "text_delta",
|
|
235
|
+
contentIndex: idx,
|
|
236
|
+
delta: event.delta!.text,
|
|
237
|
+
partial: output,
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
} else if (
|
|
241
|
+
deltaType === "thinking_delta" &&
|
|
242
|
+
event.delta!.thinking != null
|
|
243
|
+
) {
|
|
244
|
+
const idx = blocks.findIndex((b) => b.index === event.index);
|
|
245
|
+
if (idx === -1) return;
|
|
246
|
+
|
|
247
|
+
const block = blocks[idx];
|
|
248
|
+
if (block.type === "thinking") {
|
|
249
|
+
block.text += event.delta!.thinking;
|
|
250
|
+
const contentBlock = output.content[idx] as ThinkingContent;
|
|
251
|
+
contentBlock.thinking = block.text;
|
|
252
|
+
|
|
253
|
+
stream.push({
|
|
254
|
+
type: "thinking_delta",
|
|
255
|
+
contentIndex: idx,
|
|
256
|
+
delta: event.delta!.thinking,
|
|
257
|
+
partial: output,
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
} else if (
|
|
261
|
+
deltaType === "input_json_delta" &&
|
|
262
|
+
event.delta!.partial_json != null
|
|
263
|
+
) {
|
|
264
|
+
const idx = blocks.findIndex((b) => b.index === event.index);
|
|
265
|
+
if (idx === -1) return;
|
|
266
|
+
|
|
267
|
+
const block = blocks[idx];
|
|
268
|
+
if (block.type === "tool_use") {
|
|
269
|
+
block.partialJson += event.delta!.partial_json;
|
|
270
|
+
|
|
271
|
+
// Try to parse accumulated JSON -- on success update args, on failure keep previous
|
|
272
|
+
try {
|
|
273
|
+
block.arguments = JSON.parse(block.partialJson);
|
|
274
|
+
(output.content[idx] as ToolCall).arguments = block.arguments as Record<string, unknown>;
|
|
275
|
+
} catch {
|
|
276
|
+
// Partial JSON not yet parseable -- keep previous arguments
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
stream.push({
|
|
280
|
+
type: "toolcall_delta",
|
|
281
|
+
contentIndex: idx,
|
|
282
|
+
delta: event.delta!.partial_json,
|
|
283
|
+
partial: output,
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
} else if (
|
|
287
|
+
deltaType === "signature_delta" &&
|
|
288
|
+
event.delta!.signature != null
|
|
289
|
+
) {
|
|
290
|
+
// Accumulate signature on the thinking block
|
|
291
|
+
const idx = blocks.findIndex((b) => b.index === event.index);
|
|
292
|
+
if (idx === -1) return;
|
|
293
|
+
|
|
294
|
+
const block = blocks[idx];
|
|
295
|
+
if (block.type === "thinking") {
|
|
296
|
+
const contentBlock = output.content[idx] as ThinkingContent;
|
|
297
|
+
contentBlock.thinkingSignature =
|
|
298
|
+
(contentBlock.thinkingSignature || "") + event.delta!.signature;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function handleContentBlockStop(event: ClaudeApiEvent): void {
|
|
304
|
+
const idx = blocks.findIndex((b) => b.index === event.index);
|
|
305
|
+
if (idx === -1) return;
|
|
306
|
+
|
|
307
|
+
const block = blocks[idx];
|
|
308
|
+
// Clean up the tracking index from the block (no longer needed)
|
|
309
|
+
delete (block as unknown as Record<string, unknown>).index;
|
|
310
|
+
|
|
311
|
+
if (block.type === "text") {
|
|
312
|
+
stream.push({
|
|
313
|
+
type: "text_end",
|
|
314
|
+
contentIndex: idx,
|
|
315
|
+
content: block.text,
|
|
316
|
+
partial: output,
|
|
317
|
+
});
|
|
318
|
+
} else if (block.type === "thinking") {
|
|
319
|
+
stream.push({
|
|
320
|
+
type: "thinking_end",
|
|
321
|
+
contentIndex: idx,
|
|
322
|
+
content: block.text,
|
|
323
|
+
partial: output,
|
|
324
|
+
});
|
|
325
|
+
} else if (block.type === "tool_use") {
|
|
326
|
+
// Final JSON parse with fallback to raw string
|
|
327
|
+
let finalArgs: Record<string, unknown> | string;
|
|
328
|
+
try {
|
|
329
|
+
const parsed = JSON.parse(block.partialJson);
|
|
330
|
+
finalArgs = translateClaudeArgsToPi(block.claudeName, parsed);
|
|
331
|
+
} catch {
|
|
332
|
+
finalArgs = block.partialJson;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Update output.content with final arguments
|
|
336
|
+
const contentBlock = output.content[idx] as ToolCall;
|
|
337
|
+
// ToolCall.arguments is typed as Record<string, any> in pi-ai, but we
|
|
338
|
+
// intentionally emit a raw string when JSON parse fails completely.
|
|
339
|
+
// Pi handles string arguments gracefully at runtime.
|
|
340
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- finalArgs may be a raw string when JSON parse fails; pi-ai handles it at runtime
|
|
341
|
+
(contentBlock as any).arguments = finalArgs;
|
|
342
|
+
const toolCall = {
|
|
343
|
+
type: "toolCall" as const,
|
|
344
|
+
id: block.id,
|
|
345
|
+
name: block.name,
|
|
346
|
+
arguments: finalArgs,
|
|
347
|
+
} as ToolCall;
|
|
348
|
+
|
|
349
|
+
stream.push({
|
|
350
|
+
type: "toolcall_end",
|
|
351
|
+
contentIndex: idx,
|
|
352
|
+
toolCall,
|
|
353
|
+
partial: output,
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function handleMessageDelta(event: ClaudeApiEvent): void {
|
|
359
|
+
if (event.delta?.stop_reason) {
|
|
360
|
+
output.stopReason = mapStopReason(event.delta.stop_reason);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
const usage = event.usage;
|
|
364
|
+
if (usage) {
|
|
365
|
+
if (usage.input_tokens != null) output.usage.input = usage.input_tokens;
|
|
366
|
+
if (usage.output_tokens != null)
|
|
367
|
+
output.usage.output = usage.output_tokens;
|
|
368
|
+
output.usage.totalTokens =
|
|
369
|
+
output.usage.input +
|
|
370
|
+
output.usage.output +
|
|
371
|
+
output.usage.cacheRead +
|
|
372
|
+
output.usage.cacheWrite;
|
|
373
|
+
calculateCost(model, output.usage);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function handleMessageStop(): void {
|
|
378
|
+
// No-op: done event is pushed by the provider after readline closes.
|
|
379
|
+
// Pushing done here (synchronously) prevents pi from executing tools.
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
return {
|
|
383
|
+
handleEvent,
|
|
384
|
+
getOutput: () => output,
|
|
385
|
+
};
|
|
386
|
+
}
|