@runfusion/fusion 0.1.2 → 0.2.0
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 +4055 -1755
- package/dist/client/assets/AgentDetailView-CDZED6Dy.css +1 -0
- package/dist/client/assets/AgentDetailView-zycSdnO8.js +28 -0
- package/dist/client/assets/AgentsView-DoQkkDLf.css +1 -0
- package/dist/client/assets/AgentsView-pO7WiBS5.js +522 -0
- package/dist/client/assets/ChatView-BOd-sxbT.js +1 -0
- package/dist/client/assets/DevServerView-09GQf34f.js +11 -0
- package/dist/client/assets/DevServerView-ZeBGQkLI.css +1 -0
- package/dist/client/assets/DirectoryPicker-CcdN1Zs7.js +1 -0
- package/dist/client/assets/DocumentsView-CS8aiwtz.js +1 -0
- package/dist/client/assets/DocumentsView-Co9to4Zp.css +1 -0
- package/dist/client/assets/InsightsView-Bu9Cv8Ol.js +11 -0
- package/dist/client/assets/InsightsView-Egu71gmh.css +1 -0
- package/dist/client/assets/MemoryView-CtqgDtV9.js +2 -0
- package/dist/client/assets/MemoryView-DhinauGs.css +1 -0
- package/dist/client/assets/NodesView-BInPcedy.js +14 -0
- package/dist/client/assets/NodesView-DlQZHGXA.css +1 -0
- package/dist/client/assets/PiExtensionsManager-COxkYM2m.js +11 -0
- package/dist/client/assets/PiExtensionsManager-CPgmJgDk.css +1 -0
- package/dist/client/assets/PluginManager-CXUWZBOc.js +1 -0
- package/dist/client/assets/PluginManager-D64RIzmL.css +1 -0
- package/dist/client/assets/RoadmapsView-BOYnyMCh.css +1 -0
- package/dist/client/assets/RoadmapsView-BbCexaoi.js +6 -0
- package/dist/client/assets/SetupWizardModal-Cakxqkad.js +1 -0
- package/dist/client/assets/SkillsView-Cytf009Z.css +1 -0
- package/dist/client/assets/SkillsView-D3iqYCVf.js +1 -0
- package/dist/client/assets/folder-open-kO5Hsk66.js +6 -0
- package/dist/client/assets/index-BiSuUXCa.css +1 -0
- package/dist/client/assets/index-y194HxzU.js +644 -0
- package/dist/client/assets/upload-DHBQat92.js +6 -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 +969 -408
- package/dist/pi-claude-cli/index.ts +131 -0
- package/dist/pi-claude-cli/package.json +39 -0
- package/dist/pi-claude-cli/src/__tests__/control-handler.test.ts +191 -0
- package/dist/pi-claude-cli/src/__tests__/event-bridge.test.ts +1244 -0
- package/dist/pi-claude-cli/src/__tests__/mcp-config.test.ts +272 -0
- package/dist/pi-claude-cli/src/__tests__/process-manager.test.ts +619 -0
- package/dist/pi-claude-cli/src/__tests__/prompt-builder.test.ts +1067 -0
- package/dist/pi-claude-cli/src/__tests__/provider.test.ts +1902 -0
- package/dist/pi-claude-cli/src/__tests__/stream-parser.test.ts +188 -0
- package/dist/pi-claude-cli/src/__tests__/thinking-config.test.ts +141 -0
- package/dist/pi-claude-cli/src/__tests__/tool-mapping.test.ts +252 -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 +11 -4
- 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,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
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Custom tool discovery and MCP config file generation.
|
|
3
|
+
*
|
|
4
|
+
* Discovers non-built-in tools from pi, writes their schemas to a temp file,
|
|
5
|
+
* and generates an MCP config that points to the schema-only MCP server.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { writeFileSync } from "node:fs";
|
|
9
|
+
import { join, dirname } from "node:path";
|
|
10
|
+
import { tmpdir } from "node:os";
|
|
11
|
+
import { fileURLToPath } from "node:url";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* A single tool descriptor returned by pi.getAllTools().
|
|
15
|
+
*/
|
|
16
|
+
interface PiToolInfo {
|
|
17
|
+
name: string;
|
|
18
|
+
description: string;
|
|
19
|
+
parameters: Record<string, unknown>;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Minimal duck-type interface for the pi ExtensionAPI instance.
|
|
24
|
+
* We only call getAllTools(), so we only declare that method.
|
|
25
|
+
* The return type is unknown to accommodate defensive runtime checks.
|
|
26
|
+
*/
|
|
27
|
+
interface PiInstance {
|
|
28
|
+
getAllTools(): unknown;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** The 6 built-in tools that pi handles natively (match pi tool names). */
|
|
32
|
+
const BUILT_IN_TOOL_NAMES = new Set([
|
|
33
|
+
"read",
|
|
34
|
+
"write",
|
|
35
|
+
"edit",
|
|
36
|
+
"bash",
|
|
37
|
+
"grep",
|
|
38
|
+
"find",
|
|
39
|
+
]);
|
|
40
|
+
|
|
41
|
+
/** A custom tool definition with MCP-compatible schema. */
|
|
42
|
+
export interface McpToolDef {
|
|
43
|
+
name: string;
|
|
44
|
+
description: string;
|
|
45
|
+
inputSchema: Record<string, unknown>;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Get custom tool definitions from pi, filtering out built-in tools.
|
|
50
|
+
*
|
|
51
|
+
* @param pi - The pi ExtensionAPI instance
|
|
52
|
+
* @returns Array of custom tool definitions (empty if all tools are built-in)
|
|
53
|
+
*/
|
|
54
|
+
export function getCustomToolDefs(pi: PiInstance): McpToolDef[] {
|
|
55
|
+
const allTools = pi.getAllTools();
|
|
56
|
+
|
|
57
|
+
if (!Array.isArray(allTools)) {
|
|
58
|
+
return [];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return (allTools as PiToolInfo[])
|
|
62
|
+
.filter((tool) => !BUILT_IN_TOOL_NAMES.has(tool.name))
|
|
63
|
+
.map((tool) => ({
|
|
64
|
+
name: tool.name,
|
|
65
|
+
description: tool.description,
|
|
66
|
+
inputSchema: tool.parameters,
|
|
67
|
+
}));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Write MCP config and tool schemas to temp files.
|
|
72
|
+
*
|
|
73
|
+
* Creates two temp files:
|
|
74
|
+
* 1. Schema file: JSON array of tool definitions
|
|
75
|
+
* 2. Config file: MCP config pointing to the schema-only server
|
|
76
|
+
*
|
|
77
|
+
* @param toolDefs - Array of custom tool definitions
|
|
78
|
+
* @returns Path to the MCP config file
|
|
79
|
+
*/
|
|
80
|
+
export function writeMcpConfig(toolDefs: McpToolDef[]): string {
|
|
81
|
+
// Write tool schemas to temp file
|
|
82
|
+
const schemaFilePath = join(
|
|
83
|
+
tmpdir(),
|
|
84
|
+
`pi-claude-mcp-schemas-${process.pid}.json`,
|
|
85
|
+
);
|
|
86
|
+
writeFileSync(schemaFilePath, JSON.stringify(toolDefs));
|
|
87
|
+
|
|
88
|
+
// Resolve path to the schema server .cjs file (sibling of this module)
|
|
89
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
90
|
+
const __dirname = dirname(__filename);
|
|
91
|
+
const serverPath = join(__dirname, "mcp-schema-server.cjs");
|
|
92
|
+
|
|
93
|
+
// Build MCP config
|
|
94
|
+
const config = {
|
|
95
|
+
mcpServers: {
|
|
96
|
+
"custom-tools": {
|
|
97
|
+
command: "node",
|
|
98
|
+
args: [serverPath, schemaFilePath],
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
// Write config to temp file
|
|
104
|
+
const configFilePath = join(
|
|
105
|
+
tmpdir(),
|
|
106
|
+
`pi-claude-mcp-config-${process.pid}.json`,
|
|
107
|
+
);
|
|
108
|
+
writeFileSync(configFilePath, JSON.stringify(config));
|
|
109
|
+
|
|
110
|
+
return configFilePath;
|
|
111
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Schema-only MCP server. Reads tool schemas from a JSON file.
|
|
3
|
+
// Only implements initialize + tools/list. tools/call is never reached
|
|
4
|
+
// because the parent process kills the Claude subprocess at message_stop
|
|
5
|
+
// before tool execution (break-early pattern).
|
|
6
|
+
"use strict";
|
|
7
|
+
|
|
8
|
+
const fs = require("fs");
|
|
9
|
+
const readline = require("readline");
|
|
10
|
+
|
|
11
|
+
const schemaPath = process.argv[2];
|
|
12
|
+
if (!schemaPath) {
|
|
13
|
+
process.exit(1);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
let tools = [];
|
|
17
|
+
try {
|
|
18
|
+
tools = JSON.parse(fs.readFileSync(schemaPath, "utf-8"));
|
|
19
|
+
} catch {
|
|
20
|
+
process.exit(1);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const rl = readline.createInterface({ input: process.stdin });
|
|
24
|
+
rl.on("line", (line) => {
|
|
25
|
+
let msg;
|
|
26
|
+
try {
|
|
27
|
+
msg = JSON.parse(line);
|
|
28
|
+
} catch {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (msg.method === "initialize") {
|
|
33
|
+
const resp = {
|
|
34
|
+
jsonrpc: "2.0",
|
|
35
|
+
id: msg.id,
|
|
36
|
+
result: {
|
|
37
|
+
protocolVersion: "2024-11-05",
|
|
38
|
+
capabilities: { tools: {} },
|
|
39
|
+
serverInfo: { name: "custom-tools", version: "1.0.0" },
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
process.stdout.write(JSON.stringify(resp) + "\n");
|
|
43
|
+
} else if (msg.method === "tools/list") {
|
|
44
|
+
const resp = { jsonrpc: "2.0", id: msg.id, result: { tools } };
|
|
45
|
+
process.stdout.write(JSON.stringify(resp) + "\n");
|
|
46
|
+
}
|
|
47
|
+
// notifications/initialized: no response needed (notification)
|
|
48
|
+
// tools/call: never reached (break-early kills subprocess first)
|
|
49
|
+
});
|