@runfusion/fusion 0.13.0 → 0.14.1
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 +13 -0
- package/dist/bin.js +1332 -528
- package/dist/client/assets/AgentDetailView-B3KAsP2O.js +18 -0
- package/dist/client/assets/{AgentsView-Dvf_xUkx.js → AgentsView-DoXb_amw.js} +4 -4
- package/dist/client/assets/ChatView-BJ2c7wvd.js +1 -0
- package/dist/client/assets/{DevServerView-C2qTJch7.js → DevServerView-DbgM4tlT.js} +1 -1
- package/dist/client/assets/{DirectoryPicker-DRfhg9zz.js → DirectoryPicker-DfmtfMiu.js} +1 -1
- package/dist/client/assets/{DocumentsView-j8ic1xUw.js → DocumentsView-_-Efkx_W.js} +1 -1
- package/dist/client/assets/{InsightsView-CpAz3o0i.js → InsightsView-DUjcfW53.js} +1 -1
- package/dist/client/assets/{MemoryView-BcQsi_JK.js → MemoryView-DxMPBb0q.js} +1 -1
- package/dist/client/assets/{NodesView-Bo_Yhr4N.js → NodesView-BEBTI15s.js} +1 -1
- package/dist/client/assets/PiExtensionsManager-BpMYhHH_.js +11 -0
- package/dist/client/assets/PluginManager-CPv7yQd3.js +1 -0
- package/dist/client/assets/PluginManager-DA_T0GHn.css +1 -0
- package/dist/client/assets/{ResearchView-CLyyqAWE.js → ResearchView-BrFvdyXT.js} +1 -1
- package/dist/client/assets/{RoadmapsView-tG7IdOoc.js → RoadmapsView-BDjLrtcj.js} +1 -1
- package/dist/client/assets/SettingsModal-Cd-QGB0C.js +31 -0
- package/dist/client/assets/{SettingsModal-CXUGeZ0_.js → SettingsModal-CxDxiTRy.js} +1 -1
- package/dist/client/assets/SettingsModal-D_AFkDJa.css +1 -0
- package/dist/client/assets/{SetupWizardModal-BMJL6eNR.js → SetupWizardModal-DFUA4X3z.js} +1 -1
- package/dist/client/assets/{SkillMultiselect-ILMft-Kz.js → SkillMultiselect-BUWe5ujb.js} +1 -1
- package/dist/client/assets/{SkillsView-x4_YwBz6.js → SkillsView-RAkqGX3y.js} +1 -1
- package/dist/client/assets/TodoView-Ceb0wrg1.js +6 -0
- package/dist/client/assets/TodoView-SeO9o7km.css +1 -0
- package/dist/client/assets/{folder-open-DDdJt8aE.js → folder-open-DcM-Vd6r.js} +1 -1
- package/dist/client/assets/index-C1prPuSl.css +1 -0
- package/dist/client/assets/index-DH3aprf6.js +661 -0
- package/dist/client/assets/{list-checks-DFxQ9biT.js → list-checks-ByGHVQpZ.js} +1 -1
- package/dist/client/assets/{star-BKs1bgJN.js → star-DlEYI8GL.js} +1 -1
- package/dist/client/assets/{upload-Bb5Pidne.js → upload-DKshabz-.js} +1 -1
- package/dist/client/assets/{users-BImNn91Q.js → users-X6tYPPBV.js} +1 -1
- package/dist/client/index.html +2 -2
- package/dist/client/sw.js +6 -0
- package/dist/client/version.json +1 -1
- package/dist/droid-cli/index.ts +127 -0
- package/dist/droid-cli/package.json +37 -0
- package/dist/droid-cli/src/__tests__/control-handler.test.ts +164 -0
- package/dist/droid-cli/src/__tests__/event-bridge.test.ts +1318 -0
- package/dist/droid-cli/src/__tests__/mcp-config.test.ts +310 -0
- package/dist/droid-cli/src/__tests__/process-manager.test.ts +818 -0
- package/dist/droid-cli/src/__tests__/prompt-builder.test.ts +1206 -0
- package/dist/droid-cli/src/__tests__/provider.test.ts +1894 -0
- package/dist/droid-cli/src/__tests__/setup-test-isolation.test.ts +32 -0
- package/dist/droid-cli/src/__tests__/setup-test-isolation.ts +14 -0
- package/dist/droid-cli/src/__tests__/stream-parser.test.ts +188 -0
- package/dist/droid-cli/src/__tests__/thinking-config.test.ts +141 -0
- package/dist/droid-cli/src/__tests__/tool-mapping.test.ts +253 -0
- package/dist/droid-cli/src/control-handler.ts +82 -0
- package/dist/droid-cli/src/event-bridge.ts +397 -0
- package/dist/droid-cli/src/mcp-config.ts +144 -0
- package/dist/droid-cli/src/mcp-schema-server.cjs +49 -0
- package/dist/droid-cli/src/process-manager.ts +358 -0
- package/dist/droid-cli/src/prompt-builder.ts +629 -0
- package/dist/droid-cli/src/provider.ts +447 -0
- package/dist/droid-cli/src/stream-parser.ts +37 -0
- package/dist/droid-cli/src/thinking-config.ts +83 -0
- package/dist/droid-cli/src/tool-mapping.ts +147 -0
- package/dist/droid-cli/src/types.ts +87 -0
- package/dist/extension.js +555 -125
- package/dist/pi-claude-cli/package.json +1 -1
- package/package.json +2 -1
- package/dist/client/assets/AgentDetailView-B7j297GT.js +0 -18
- package/dist/client/assets/ChatView-BgUt38ty.js +0 -1
- package/dist/client/assets/PiExtensionsManager-DHt2zFg8.js +0 -11
- package/dist/client/assets/PluginManager-BQhBHWrB.js +0 -1
- package/dist/client/assets/PluginManager-jyNkJZSz.css +0 -1
- package/dist/client/assets/SettingsModal-9HS8MnmW.css +0 -1
- package/dist/client/assets/SettingsModal-UziTDnLh.js +0 -31
- package/dist/client/assets/TodoView-BBYcMbXE.js +0 -6
- package/dist/client/assets/TodoView-C1g65hJo.css +0 -1
- package/dist/client/assets/index-B15xwijw.css +0 -1
- package/dist/client/assets/index-DmSs2FGE.js +0 -661
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Process manager for spawning and managing Droid CLI subprocesses.
|
|
3
|
+
*
|
|
4
|
+
* Handles subprocess lifecycle: spawn with correct CLI flags, write NDJSON
|
|
5
|
+
* messages to stdin, force-kill after result (CLI hangs bug), and stderr capture.
|
|
6
|
+
* Also provides startup validation for CLI presence and authentication.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { execSync, spawn, type ChildProcess } from "node:child_process";
|
|
10
|
+
import { writeFileSync, unlinkSync } from "node:fs";
|
|
11
|
+
import { join } from "node:path";
|
|
12
|
+
import { tmpdir } from "node:os";
|
|
13
|
+
|
|
14
|
+
function debugLog(message: string): void {
|
|
15
|
+
if (process.env.PI_DROID_CLI_DEBUG !== "1") return;
|
|
16
|
+
console.error(`[droid-cli] ${message}`);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Spawn a Droid CLI subprocess with all required flags for stream-json communication.
|
|
21
|
+
*
|
|
22
|
+
* @param modelId - The model ID to pass via --model flag
|
|
23
|
+
* @param systemPrompt - Optional system prompt appended via --append-system-prompt
|
|
24
|
+
* @param options - Optional cwd, AbortSignal, and effort level
|
|
25
|
+
* @returns The spawned ChildProcess with piped stdin/stdout/stderr
|
|
26
|
+
*/
|
|
27
|
+
export function buildDroidSpawnArgs(
|
|
28
|
+
modelId: string,
|
|
29
|
+
systemPrompt?: string,
|
|
30
|
+
options?: {
|
|
31
|
+
effort?: string;
|
|
32
|
+
mcpConfigPath?: string;
|
|
33
|
+
resumeSessionId?: string;
|
|
34
|
+
newSessionId?: string;
|
|
35
|
+
},
|
|
36
|
+
): string[] {
|
|
37
|
+
const args = [
|
|
38
|
+
"-p",
|
|
39
|
+
"--input-format",
|
|
40
|
+
"stream-json",
|
|
41
|
+
"--output-format",
|
|
42
|
+
"stream-json",
|
|
43
|
+
"--verbose",
|
|
44
|
+
"--include-partial-messages",
|
|
45
|
+
"--model",
|
|
46
|
+
modelId,
|
|
47
|
+
];
|
|
48
|
+
|
|
49
|
+
if (options?.resumeSessionId) {
|
|
50
|
+
// Resume an existing session — CLI loads prior conversation from disk
|
|
51
|
+
args.push("--resume", options.resumeSessionId);
|
|
52
|
+
} else if (options?.newSessionId) {
|
|
53
|
+
// First turn: create session with this ID so subsequent turns can --resume it
|
|
54
|
+
args.push("--session-id", options.newSessionId);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (systemPrompt) {
|
|
58
|
+
// Write system prompt to a temp file to avoid ENAMETOOLONG on Windows.
|
|
59
|
+
// Droid CLI's --append-system-prompt accepts a file path or literal text.
|
|
60
|
+
const tmpFile = join(
|
|
61
|
+
tmpdir(),
|
|
62
|
+
`droid-cli-sysprompt-${process.pid}.txt`,
|
|
63
|
+
);
|
|
64
|
+
writeFileSync(tmpFile, systemPrompt, "utf-8");
|
|
65
|
+
args.push("--append-system-prompt", tmpFile);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (options?.effort) {
|
|
69
|
+
args.push("--effort", options.effort);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (options?.mcpConfigPath) {
|
|
73
|
+
args.push("--mcp-config", options.mcpConfigPath);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return args;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function spawnDroid(
|
|
80
|
+
modelId: string,
|
|
81
|
+
systemPrompt?: string,
|
|
82
|
+
options?: {
|
|
83
|
+
cwd?: string;
|
|
84
|
+
signal?: AbortSignal;
|
|
85
|
+
effort?: string;
|
|
86
|
+
mcpConfigPath?: string;
|
|
87
|
+
resumeSessionId?: string;
|
|
88
|
+
newSessionId?: string;
|
|
89
|
+
},
|
|
90
|
+
): ChildProcess {
|
|
91
|
+
const args = buildDroidSpawnArgs(modelId, systemPrompt, {
|
|
92
|
+
effort: options?.effort,
|
|
93
|
+
mcpConfigPath: options?.mcpConfigPath,
|
|
94
|
+
resumeSessionId: options?.resumeSessionId,
|
|
95
|
+
newSessionId: options?.newSessionId,
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
const proc = spawn("droid", args, {
|
|
99
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
100
|
+
cwd: options?.cwd ?? process.cwd(),
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
debugLog(`spawnDroid: pid=${proc.pid} model=${modelId}`);
|
|
104
|
+
|
|
105
|
+
return proc as ChildProcess;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Clean up the temp system prompt file created by spawnDroid.
|
|
110
|
+
* Safe to call multiple times or when no file exists.
|
|
111
|
+
*/
|
|
112
|
+
export function cleanupSystemPromptFile(): void {
|
|
113
|
+
try {
|
|
114
|
+
unlinkSync(join(tmpdir(), `droid-cli-sysprompt-${process.pid}.txt`));
|
|
115
|
+
} catch {
|
|
116
|
+
// File doesn't exist or already deleted — ignore
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Write a user message to the subprocess stdin as NDJSON.
|
|
122
|
+
* Calls stdin.end() after writing the user message to signal EOF, allowing
|
|
123
|
+
* Droid CLI to process the input and start generating.
|
|
124
|
+
*
|
|
125
|
+
* Accepts both string (text-only prompt) and array (ContentBlock[] with images)
|
|
126
|
+
* content. JSON.stringify handles both natively. The stream-json protocol
|
|
127
|
+
* supports either format in the content field.
|
|
128
|
+
*
|
|
129
|
+
* @param proc - The Claude subprocess
|
|
130
|
+
* @param prompt - The prompt text or ContentBlock[] to send
|
|
131
|
+
*/
|
|
132
|
+
export function writeUserMessage(
|
|
133
|
+
proc: ChildProcess,
|
|
134
|
+
prompt: string | unknown[],
|
|
135
|
+
): void {
|
|
136
|
+
const message = {
|
|
137
|
+
type: "user",
|
|
138
|
+
message: {
|
|
139
|
+
role: "user",
|
|
140
|
+
content: prompt,
|
|
141
|
+
},
|
|
142
|
+
};
|
|
143
|
+
proc.stdin!.write(JSON.stringify(message) + "\n");
|
|
144
|
+
proc.stdin!.end();
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Force-kill a subprocess immediately via SIGKILL.
|
|
149
|
+
* No-ops if the process is already dead (killed or exited).
|
|
150
|
+
* Cross-platform safe: Node.js treats SIGKILL as forceful termination on Windows.
|
|
151
|
+
*
|
|
152
|
+
* @param proc - The subprocess to force-kill
|
|
153
|
+
*/
|
|
154
|
+
export function forceKillProcess(proc: ChildProcess): void {
|
|
155
|
+
if (proc.killed || proc.exitCode !== null) return;
|
|
156
|
+
proc.kill("SIGKILL");
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** Registry of active subprocesses for cleanup on teardown. */
|
|
160
|
+
const activeProcesses = new Set<ChildProcess>();
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Register a subprocess in the global process registry.
|
|
164
|
+
* The process is automatically removed from the registry when it exits.
|
|
165
|
+
*
|
|
166
|
+
* @param proc - The subprocess to track
|
|
167
|
+
*/
|
|
168
|
+
export function registerProcess(proc: ChildProcess): void {
|
|
169
|
+
activeProcesses.add(proc);
|
|
170
|
+
proc.on("exit", () => activeProcesses.delete(proc));
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Force-kill all registered subprocesses and clear the registry.
|
|
175
|
+
* Safe to call multiple times -- no-ops on already-dead processes.
|
|
176
|
+
*/
|
|
177
|
+
export function killAllProcesses(): void {
|
|
178
|
+
for (const proc of activeProcesses) {
|
|
179
|
+
forceKillProcess(proc);
|
|
180
|
+
}
|
|
181
|
+
activeProcesses.clear();
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Force-kill the subprocess after a 500ms grace period.
|
|
186
|
+
* The Droid CLI hangs after emitting the result message (known bug).
|
|
187
|
+
* Brief grace period allows final stdout flushing before force-kill.
|
|
188
|
+
*
|
|
189
|
+
* @param proc - The Claude subprocess to clean up
|
|
190
|
+
*/
|
|
191
|
+
export function cleanupProcess(proc: ChildProcess): void {
|
|
192
|
+
setTimeout(() => {
|
|
193
|
+
forceKillProcess(proc);
|
|
194
|
+
}, 500);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Attach a data listener to stderr and accumulate output into a buffer.
|
|
199
|
+
*
|
|
200
|
+
* @param proc - The Claude subprocess
|
|
201
|
+
* @returns A function that returns the accumulated stderr string
|
|
202
|
+
*/
|
|
203
|
+
export function captureStderr(proc: ChildProcess): () => string {
|
|
204
|
+
let buffer = "";
|
|
205
|
+
proc.stderr!.on("data", (data: Buffer) => {
|
|
206
|
+
buffer += data.toString();
|
|
207
|
+
});
|
|
208
|
+
return () => buffer;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Validate that the Droid CLI is installed and on PATH.
|
|
213
|
+
* Throws with install instructions if not found.
|
|
214
|
+
*/
|
|
215
|
+
export function validateCliPresence(): void {
|
|
216
|
+
try {
|
|
217
|
+
execSync("droid --version", { stdio: "pipe", timeout: 45000 });
|
|
218
|
+
} catch {
|
|
219
|
+
throw new Error(
|
|
220
|
+
"Droid CLI not found on PATH. Install Droid CLI and then run: droid auth login",
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Validate that the Droid CLI is authenticated.
|
|
227
|
+
* Returns false and warns if not authenticated.
|
|
228
|
+
*
|
|
229
|
+
* @returns true if authenticated, false otherwise
|
|
230
|
+
*/
|
|
231
|
+
export function validateCliAuth(): boolean {
|
|
232
|
+
try {
|
|
233
|
+
execSync("droid auth status", { stdio: "pipe", timeout: 45000 });
|
|
234
|
+
return true;
|
|
235
|
+
} catch {
|
|
236
|
+
console.warn(
|
|
237
|
+
"[droid-cli] Droid CLI is not authenticated. " +
|
|
238
|
+
"Run 'droid auth login' to authenticate.",
|
|
239
|
+
);
|
|
240
|
+
return false;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Run a one-shot `droid <args>` and resolve to the exit code.
|
|
246
|
+
*
|
|
247
|
+
* Why: the sync execSync variants block the Node event loop for the duration
|
|
248
|
+
* of a Droid CLI cold start (1–3s, occasionally longer). When droid-cli's
|
|
249
|
+
* factory is invoked from a per-request createFnAgent path (Fusion dashboard
|
|
250
|
+
* does this on every chat send), those sync probes freeze every other request.
|
|
251
|
+
* This async variant uses spawn so the loop keeps turning while the subprocess
|
|
252
|
+
* starts up.
|
|
253
|
+
*/
|
|
254
|
+
function runDroidProbe(args: string[], timeoutMs = 45000): Promise<number> {
|
|
255
|
+
return new Promise((resolve) => {
|
|
256
|
+
const proc = spawn("droid", args, { stdio: "ignore" });
|
|
257
|
+
const timer = setTimeout(() => {
|
|
258
|
+
try {
|
|
259
|
+
proc.kill("SIGKILL");
|
|
260
|
+
} catch {
|
|
261
|
+
// already dead
|
|
262
|
+
}
|
|
263
|
+
resolve(124);
|
|
264
|
+
}, timeoutMs);
|
|
265
|
+
proc.once("error", () => {
|
|
266
|
+
clearTimeout(timer);
|
|
267
|
+
resolve(127);
|
|
268
|
+
});
|
|
269
|
+
proc.once("exit", (code) => {
|
|
270
|
+
clearTimeout(timer);
|
|
271
|
+
resolve(code ?? 1);
|
|
272
|
+
});
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Async, non-blocking variant of validateCliPresence.
|
|
278
|
+
* Resolves with `{ok: true}` on success, `{ok: false, error}` on failure —
|
|
279
|
+
* never rejects, so callers can fire-and-forget without unhandled rejections.
|
|
280
|
+
*/
|
|
281
|
+
export async function validateCliPresenceAsync(): Promise<
|
|
282
|
+
{ ok: true } | { ok: false; error: Error }
|
|
283
|
+
> {
|
|
284
|
+
const code = await runDroidProbe(["--version"]);
|
|
285
|
+
if (code === 0) return { ok: true };
|
|
286
|
+
return {
|
|
287
|
+
ok: false,
|
|
288
|
+
error: new Error(
|
|
289
|
+
"Droid CLI not found on PATH. Install Droid CLI and then run: droid auth login",
|
|
290
|
+
),
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Async, non-blocking variant of validateCliAuth.
|
|
296
|
+
* Returns true if authenticated. Logs a warning (does not throw) otherwise.
|
|
297
|
+
*/
|
|
298
|
+
export async function validateCliAuthAsync(): Promise<boolean> {
|
|
299
|
+
const code = await runDroidProbe(["auth", "status"]);
|
|
300
|
+
if (code === 0) return true;
|
|
301
|
+
console.warn(
|
|
302
|
+
"[droid-cli] Droid CLI is not authenticated. " +
|
|
303
|
+
"Run 'droid auth login' to authenticate.",
|
|
304
|
+
);
|
|
305
|
+
return false;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
export async function discoverDroidModels(): Promise<string[]> {
|
|
309
|
+
const attempts: string[][] = [["models", "--json"], ["model", "list", "--json"], ["models"]];
|
|
310
|
+
|
|
311
|
+
for (const args of attempts) {
|
|
312
|
+
const models = await new Promise<string[] | null>((resolve) => {
|
|
313
|
+
const proc = spawn("droid", args, { stdio: ["ignore", "pipe", "ignore"] });
|
|
314
|
+
let out = "";
|
|
315
|
+
proc.stdout?.on("data", (chunk: Buffer) => {
|
|
316
|
+
out += chunk.toString();
|
|
317
|
+
});
|
|
318
|
+
proc.once("error", () => resolve(null));
|
|
319
|
+
proc.once("exit", (code) => {
|
|
320
|
+
if (code !== 0) return resolve(null);
|
|
321
|
+
const trimmed = out.trim();
|
|
322
|
+
if (!trimmed) return resolve([]);
|
|
323
|
+
try {
|
|
324
|
+
const parsed = JSON.parse(trimmed);
|
|
325
|
+
if (Array.isArray(parsed)) {
|
|
326
|
+
return resolve(
|
|
327
|
+
parsed
|
|
328
|
+
.map((entry) =>
|
|
329
|
+
typeof entry === "string"
|
|
330
|
+
? entry
|
|
331
|
+
: typeof entry?.id === "string"
|
|
332
|
+
? entry.id
|
|
333
|
+
: typeof entry?.name === "string"
|
|
334
|
+
? entry.name
|
|
335
|
+
: undefined,
|
|
336
|
+
)
|
|
337
|
+
.filter((id): id is string => Boolean(id)),
|
|
338
|
+
);
|
|
339
|
+
}
|
|
340
|
+
} catch {
|
|
341
|
+
// not json, fall through to line parsing
|
|
342
|
+
}
|
|
343
|
+
resolve(
|
|
344
|
+
trimmed
|
|
345
|
+
.split(/\r?\n/)
|
|
346
|
+
.map((line) => line.trim())
|
|
347
|
+
.filter(Boolean),
|
|
348
|
+
);
|
|
349
|
+
});
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
if (models && models.length > 0) {
|
|
353
|
+
return Array.from(new Set(models));
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
return [];
|
|
358
|
+
}
|