@runfusion/fusion 0.18.1 → 0.20.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.
Files changed (60) hide show
  1. package/dist/bin.js +6533 -2558
  2. package/dist/client/assets/AgentDetailView-C6BG7O7i.js +18 -0
  3. package/dist/client/assets/AgentDetailView-CUtWvXBn.css +1 -0
  4. package/dist/client/assets/ChatView-DeXUYwSY.js +1 -0
  5. package/dist/client/assets/{DevServerView-r6V3FqkY.js → DevServerView-Dariyxt_.js} +1 -1
  6. package/dist/client/assets/{DirectoryPicker-CTZE95Fk.js → DirectoryPicker-SchiK-Aq.js} +1 -1
  7. package/dist/client/assets/{DocumentsView-DSEf1Lmg.js → DocumentsView-C6v-tBhG.js} +1 -1
  8. package/dist/client/assets/InsightsView-AWo5o_81.css +1 -0
  9. package/dist/client/assets/InsightsView-Cqim12az.js +11 -0
  10. package/dist/client/assets/{MemoryView-DicXjec9.js → MemoryView-CakLoJtY.js} +2 -2
  11. package/dist/client/assets/NodesView-BxGm3poT.js +14 -0
  12. package/dist/client/assets/{NodesView-sJgPLTzz.css → NodesView-fXqDk9ur.css} +1 -1
  13. package/dist/client/assets/PiExtensionsManager-lJbmskyZ.js +6 -0
  14. package/dist/client/assets/PluginManager-BZjNNf9m.js +1 -0
  15. package/dist/client/assets/ResearchView-Bzsr9V0y.js +1 -0
  16. package/dist/client/assets/{RoadmapsView-DfEF3mql.js → RoadmapsView-CeKks_OI.js} +2 -2
  17. package/dist/client/assets/SettingsModal-BWe0KrGY.css +1 -0
  18. package/dist/client/assets/SettingsModal-D-9CLguN.js +31 -0
  19. package/dist/client/assets/{SettingsModal-YcScdFiG.js → SettingsModal-YdeVPhRJ.js} +1 -1
  20. package/dist/client/assets/{SetupWizardModal-DRF5fOoR.css → SetupWizardModal-CGYGKurR.css} +1 -1
  21. package/dist/client/assets/SetupWizardModal-DAC04LlA.js +1 -0
  22. package/dist/client/assets/{SkillsView-Dkq2CQla.js → SkillsView-CClC_5RN.js} +1 -1
  23. package/dist/client/assets/index-CrHLf3pB.js +1222 -0
  24. package/dist/client/assets/index-Df1bHDY4.css +1 -0
  25. package/dist/client/assets/star-DxVRh9VT.js +6 -0
  26. package/dist/client/assets/{users-Cp5TSxVm.js → users-3SD3oNMQ.js} +1 -1
  27. package/dist/client/index.html +2 -2
  28. package/dist/client/version.json +1 -1
  29. package/dist/droid-cli/index.ts +3 -5
  30. package/dist/droid-cli/package.json +1 -1
  31. package/dist/droid-cli/src/__tests__/event-bridge.test.ts +6 -1315
  32. package/dist/droid-cli/src/__tests__/provider.test.ts +6 -1927
  33. package/dist/droid-cli/src/control-handler.ts +1 -82
  34. package/dist/droid-cli/src/event-bridge.ts +1 -397
  35. package/dist/droid-cli/src/mcp-config.ts +1 -144
  36. package/dist/droid-cli/src/process-manager.ts +1 -358
  37. package/dist/droid-cli/src/prompt-builder.ts +1 -629
  38. package/dist/droid-cli/src/provider.ts +1 -447
  39. package/dist/droid-cli/src/stream-parser.ts +1 -37
  40. package/dist/droid-cli/src/thinking-config.ts +1 -83
  41. package/dist/droid-cli/src/tool-mapping.ts +1 -147
  42. package/dist/droid-cli/src/types.ts +1 -87
  43. package/dist/extension.js +4674 -1748
  44. package/dist/pi-claude-cli/package.json +1 -1
  45. package/dist/plugins/fusion-plugin-dependency-graph/package.json +1 -1
  46. package/package.json +5 -4
  47. package/skill/fusion/references/engine-tools.md +5 -1
  48. package/skill/fusion/references/extension-tools.md +3 -1
  49. package/dist/client/assets/ChatView-3Sqm6teN.js +0 -1
  50. package/dist/client/assets/InsightsView-4KiUKzbz.css +0 -1
  51. package/dist/client/assets/InsightsView-F5PZsX5u.js +0 -11
  52. package/dist/client/assets/NodesView-DddCS7zB.js +0 -14
  53. package/dist/client/assets/PiExtensionsManager-Ch7si-v8.js +0 -11
  54. package/dist/client/assets/PluginManager-LcTh_fHP.js +0 -1
  55. package/dist/client/assets/ResearchView-D0TY1VcX.js +0 -1
  56. package/dist/client/assets/SettingsModal-SOADcCNJ.js +0 -31
  57. package/dist/client/assets/SettingsModal-oOnIed5O.css +0 -1
  58. package/dist/client/assets/SetupWizardModal-EDYuf9Yc.js +0 -1
  59. package/dist/client/assets/index-4hC8zoTD.css +0 -1
  60. package/dist/client/assets/index-DNzA4aZ7.js +0 -1229
@@ -1,82 +1 @@
1
- /**
2
- * Control protocol handler for Droid CLI stream-json communication.
3
- *
4
- * Processes control_request messages from Droid CLI stdout and returns a
5
- * control_response decision object.
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 Droid 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
- * Pure function: no side effects and no stdin writes.
39
- *
40
- * @returns Decision payload with allow/deny result and serialized response object
41
- */
42
- export function handleControlRequest(
43
- msg: ClaudeControlRequest,
44
- ): { allowed: boolean; response: ControlResponse } {
45
- if (!msg.request_id || !msg.request) {
46
- console.error(
47
- "[droid-cli] Malformed control_request: missing request_id or request object",
48
- msg,
49
- );
50
-
51
- return {
52
- allowed: false,
53
- response: {
54
- type: "control_response",
55
- request_id: msg.request_id ?? "",
56
- response: {
57
- subtype: "success",
58
- response: {
59
- behavior: "deny",
60
- message: TOOL_EXECUTION_DENIED_MESSAGE,
61
- },
62
- },
63
- },
64
- };
65
- }
66
-
67
- const toolName = msg.request?.tool_name ?? "";
68
- const isCustomTool = toolName.startsWith(CUSTOM_TOOLS_MCP_PREFIX);
69
-
70
- const response: ControlResponse = {
71
- type: "control_response",
72
- request_id: msg.request_id,
73
- response: {
74
- subtype: "success",
75
- response: isCustomTool
76
- ? { behavior: "deny", message: TOOL_EXECUTION_DENIED_MESSAGE }
77
- : { behavior: "allow" },
78
- },
79
- };
80
-
81
- return { allowed: !isCustomTool, response };
82
- }
1
+ export * from "../../../plugins/fusion-plugin-droid-runtime/src/control-handler.js";
@@ -1,397 +1 @@
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
- mapDroidToolNameToPi,
14
- translateDroidArgsToPi,
15
- isPiKnownDroidTool,
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: "droid-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 (!isPiKnownDroidTool(claudeName)) {
188
- return;
189
- }
190
-
191
- const piName = mapDroidToolNameToPi(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
- // Special case: parameterless MCP tools (e.g. fn_review_spec, schema
328
- // `{type:"object", properties:{}}`) emit ZERO input_json_delta events,
329
- // so `partialJson` stays "". Without this guard we'd JSON.parse("")
330
- // → throw → fall through to `finalArgs = ""` (raw string), and pi's
331
- // TypeBox validator then rejects with "root: must be object" because
332
- // an empty string is not an object. Default to `{}` so the call lands.
333
- let finalArgs: Record<string, unknown> | string;
334
- const trimmedJson = block.partialJson.trim();
335
- if (trimmedJson === "") {
336
- finalArgs = {};
337
- } else {
338
- try {
339
- const parsed = JSON.parse(trimmedJson);
340
- finalArgs = translateDroidArgsToPi(block.claudeName, parsed);
341
- } catch {
342
- finalArgs = block.partialJson;
343
- }
344
- }
345
-
346
- // Update output.content with final arguments
347
- const contentBlock = output.content[idx] as ToolCall;
348
- // ToolCall.arguments is typed as Record<string, any> in pi-ai, but we
349
- // intentionally emit a raw string when JSON parse fails completely.
350
- // Pi handles string arguments gracefully at runtime.
351
- // 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
352
- (contentBlock as any).arguments = finalArgs;
353
- const toolCall = {
354
- type: "toolCall" as const,
355
- id: block.id,
356
- name: block.name,
357
- arguments: finalArgs,
358
- } as ToolCall;
359
-
360
- stream.push({
361
- type: "toolcall_end",
362
- contentIndex: idx,
363
- toolCall,
364
- partial: output,
365
- });
366
- }
367
- }
368
-
369
- function handleMessageDelta(event: ClaudeApiEvent): void {
370
- if (event.delta?.stop_reason) {
371
- output.stopReason = mapStopReason(event.delta.stop_reason);
372
- }
373
-
374
- const usage = event.usage;
375
- if (usage) {
376
- if (usage.input_tokens != null) output.usage.input = usage.input_tokens;
377
- if (usage.output_tokens != null)
378
- output.usage.output = usage.output_tokens;
379
- output.usage.totalTokens =
380
- output.usage.input +
381
- output.usage.output +
382
- output.usage.cacheRead +
383
- output.usage.cacheWrite;
384
- calculateCost(model, output.usage);
385
- }
386
- }
387
-
388
- function handleMessageStop(): void {
389
- // No-op: done event is pushed by the provider after readline closes.
390
- // Pushing done here (synchronously) prevents pi from executing tools.
391
- }
392
-
393
- return {
394
- handleEvent,
395
- getOutput: () => output,
396
- };
397
- }
1
+ export * from "../../../plugins/fusion-plugin-droid-runtime/src/event-bridge.js";
@@ -1,144 +1 @@
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
- /** Minimal pi-ai Tool shape (the subset we need from `Context.tools`). */
71
- interface PiAiToolLike {
72
- name: string;
73
- description: string;
74
- parameters: Record<string, unknown>;
75
- }
76
-
77
- /**
78
- * Convert the pi-ai `Context.tools` array (the authoritative per-session tool
79
- * list pi-coding-agent passes to streamSimple) into MCP tool defs, filtering
80
- * out the 6 built-ins that pi handles natively.
81
- */
82
- export function toolsFromContext(
83
- contextTools: ReadonlyArray<PiAiToolLike> | undefined,
84
- ): McpToolDef[] {
85
- if (!Array.isArray(contextTools)) return [];
86
- return contextTools
87
- .filter((tool) => !BUILT_IN_TOOL_NAMES.has(tool.name))
88
- .map((tool) => ({
89
- name: tool.name,
90
- description: tool.description,
91
- inputSchema: tool.parameters,
92
- }));
93
- }
94
-
95
- /**
96
- * Write MCP config and tool schemas to temp files.
97
- *
98
- * Creates two temp files:
99
- * 1. Schema file: JSON array of tool definitions
100
- * 2. Config file: MCP config pointing to the schema-only server
101
- *
102
- * @param toolDefs - Array of custom tool definitions
103
- * @param cacheKey - Optional suffix appended to filenames so that distinct
104
- * tool sets (e.g. session-scoped tool registrations) get distinct files
105
- * and don't race on a single shared path.
106
- * @returns Path to the MCP config file
107
- */
108
- export function writeMcpConfig(
109
- toolDefs: McpToolDef[],
110
- cacheKey?: string,
111
- ): string {
112
- const suffix = cacheKey ? `${process.pid}-${cacheKey}` : `${process.pid}`;
113
-
114
- // Write tool schemas to temp file
115
- const schemaFilePath = join(
116
- tmpdir(),
117
- `droid-cli-mcp-schemas-${suffix}.json`,
118
- );
119
- writeFileSync(schemaFilePath, JSON.stringify(toolDefs));
120
-
121
- // Resolve path to the schema server .cjs file (sibling of this module)
122
- const __filename = fileURLToPath(import.meta.url);
123
- const __dirname = dirname(__filename);
124
- const serverPath = join(__dirname, "mcp-schema-server.cjs");
125
-
126
- // Build MCP config
127
- const config = {
128
- mcpServers: {
129
- "custom-tools": {
130
- command: "node",
131
- args: [serverPath, schemaFilePath],
132
- },
133
- },
134
- };
135
-
136
- // Write config to temp file
137
- const configFilePath = join(
138
- tmpdir(),
139
- `droid-cli-mcp-config-${suffix}.json`,
140
- );
141
- writeFileSync(configFilePath, JSON.stringify(config));
142
-
143
- return configFilePath;
144
- }
1
+ export * from "../../../plugins/fusion-plugin-droid-runtime/src/mcp-config.js";