@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,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
|
+
});
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Process manager for spawning and managing Claude 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 spawn from "cross-spawn";
|
|
10
|
+
import { execSync } from "node:child_process";
|
|
11
|
+
import { writeFileSync, unlinkSync } from "node:fs";
|
|
12
|
+
import { join } from "node:path";
|
|
13
|
+
import { tmpdir } from "node:os";
|
|
14
|
+
import type { ChildProcess } from "node:child_process";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Spawn a Claude CLI subprocess with all required flags for stream-json communication.
|
|
18
|
+
*
|
|
19
|
+
* @param modelId - The model ID to pass via --model flag
|
|
20
|
+
* @param systemPrompt - Optional system prompt appended via --append-system-prompt
|
|
21
|
+
* @param options - Optional cwd, AbortSignal, and effort level
|
|
22
|
+
* @returns The spawned ChildProcess with piped stdin/stdout/stderr
|
|
23
|
+
*/
|
|
24
|
+
export function spawnClaude(
|
|
25
|
+
modelId: string,
|
|
26
|
+
systemPrompt?: string,
|
|
27
|
+
options?: {
|
|
28
|
+
cwd?: string;
|
|
29
|
+
signal?: AbortSignal;
|
|
30
|
+
effort?: string;
|
|
31
|
+
mcpConfigPath?: string;
|
|
32
|
+
resumeSessionId?: string;
|
|
33
|
+
newSessionId?: string;
|
|
34
|
+
},
|
|
35
|
+
): ChildProcess {
|
|
36
|
+
const args = [
|
|
37
|
+
"-p",
|
|
38
|
+
"--input-format",
|
|
39
|
+
"stream-json",
|
|
40
|
+
"--output-format",
|
|
41
|
+
"stream-json",
|
|
42
|
+
"--verbose",
|
|
43
|
+
"--include-partial-messages",
|
|
44
|
+
"--model",
|
|
45
|
+
modelId,
|
|
46
|
+
"--permission-prompt-tool",
|
|
47
|
+
"stdio",
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
if (options?.resumeSessionId) {
|
|
51
|
+
// Resume an existing session — CLI loads prior conversation from disk
|
|
52
|
+
args.push("--resume", options.resumeSessionId);
|
|
53
|
+
} else if (options?.newSessionId) {
|
|
54
|
+
// First turn: create session with this ID so subsequent turns can --resume it
|
|
55
|
+
args.push("--session-id", options.newSessionId);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (systemPrompt) {
|
|
59
|
+
// Write system prompt to a temp file to avoid ENAMETOOLONG on Windows.
|
|
60
|
+
// Claude CLI's --append-system-prompt accepts a file path or literal text.
|
|
61
|
+
const tmpFile = join(
|
|
62
|
+
tmpdir(),
|
|
63
|
+
`pi-claude-cli-sysprompt-${process.pid}.txt`,
|
|
64
|
+
);
|
|
65
|
+
writeFileSync(tmpFile, systemPrompt, "utf-8");
|
|
66
|
+
args.push("--append-system-prompt", tmpFile);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (options?.effort) {
|
|
70
|
+
args.push("--effort", options.effort);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (options?.mcpConfigPath) {
|
|
74
|
+
args.push("--mcp-config", options.mcpConfigPath);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const proc = spawn("claude", args, {
|
|
78
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
79
|
+
cwd: options?.cwd ?? process.cwd(),
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
return proc as ChildProcess;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Clean up the temp system prompt file created by spawnClaude.
|
|
87
|
+
* Safe to call multiple times or when no file exists.
|
|
88
|
+
*/
|
|
89
|
+
export function cleanupSystemPromptFile(): void {
|
|
90
|
+
try {
|
|
91
|
+
unlinkSync(join(tmpdir(), `pi-claude-cli-sysprompt-${process.pid}.txt`));
|
|
92
|
+
} catch {
|
|
93
|
+
// File doesn't exist or already deleted — ignore
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Write a user message to the subprocess stdin as NDJSON.
|
|
99
|
+
* Does NOT call stdin.end() -- stdin stays open for control_response in Phase 2.
|
|
100
|
+
*
|
|
101
|
+
* Accepts both string (text-only prompt) and array (ContentBlock[] with images)
|
|
102
|
+
* content. JSON.stringify handles both natively. The stream-json protocol
|
|
103
|
+
* supports either format in the content field.
|
|
104
|
+
*
|
|
105
|
+
* @param proc - The Claude subprocess
|
|
106
|
+
* @param prompt - The prompt text or ContentBlock[] to send
|
|
107
|
+
*/
|
|
108
|
+
export function writeUserMessage(
|
|
109
|
+
proc: ChildProcess,
|
|
110
|
+
prompt: string | unknown[],
|
|
111
|
+
): void {
|
|
112
|
+
const message = {
|
|
113
|
+
type: "user",
|
|
114
|
+
message: {
|
|
115
|
+
role: "user",
|
|
116
|
+
content: prompt,
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
proc.stdin!.write(JSON.stringify(message) + "\n");
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Force-kill a subprocess immediately via SIGKILL.
|
|
124
|
+
* No-ops if the process is already dead (killed or exited).
|
|
125
|
+
* Cross-platform safe: Node.js treats SIGKILL as forceful termination on Windows.
|
|
126
|
+
*
|
|
127
|
+
* @param proc - The subprocess to force-kill
|
|
128
|
+
*/
|
|
129
|
+
export function forceKillProcess(proc: ChildProcess): void {
|
|
130
|
+
if (proc.killed || proc.exitCode !== null) return;
|
|
131
|
+
proc.kill("SIGKILL");
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Registry of active subprocesses for cleanup on teardown. */
|
|
135
|
+
const activeProcesses = new Set<ChildProcess>();
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Register a subprocess in the global process registry.
|
|
139
|
+
* The process is automatically removed from the registry when it exits.
|
|
140
|
+
*
|
|
141
|
+
* @param proc - The subprocess to track
|
|
142
|
+
*/
|
|
143
|
+
export function registerProcess(proc: ChildProcess): void {
|
|
144
|
+
activeProcesses.add(proc);
|
|
145
|
+
proc.on("exit", () => activeProcesses.delete(proc));
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Force-kill all registered subprocesses and clear the registry.
|
|
150
|
+
* Safe to call multiple times -- no-ops on already-dead processes.
|
|
151
|
+
*/
|
|
152
|
+
export function killAllProcesses(): void {
|
|
153
|
+
for (const proc of activeProcesses) {
|
|
154
|
+
forceKillProcess(proc);
|
|
155
|
+
}
|
|
156
|
+
activeProcesses.clear();
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Force-kill the subprocess after a 500ms grace period.
|
|
161
|
+
* The Claude CLI hangs after emitting the result message (known bug).
|
|
162
|
+
* Brief grace period allows final stdout flushing before force-kill.
|
|
163
|
+
*
|
|
164
|
+
* @param proc - The Claude subprocess to clean up
|
|
165
|
+
*/
|
|
166
|
+
export function cleanupProcess(proc: ChildProcess): void {
|
|
167
|
+
setTimeout(() => {
|
|
168
|
+
forceKillProcess(proc);
|
|
169
|
+
}, 500);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Attach a data listener to stderr and accumulate output into a buffer.
|
|
174
|
+
*
|
|
175
|
+
* @param proc - The Claude subprocess
|
|
176
|
+
* @returns A function that returns the accumulated stderr string
|
|
177
|
+
*/
|
|
178
|
+
export function captureStderr(proc: ChildProcess): () => string {
|
|
179
|
+
let buffer = "";
|
|
180
|
+
proc.stderr!.on("data", (data: Buffer) => {
|
|
181
|
+
buffer += data.toString();
|
|
182
|
+
});
|
|
183
|
+
return () => buffer;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Validate that the Claude CLI is installed and on PATH.
|
|
188
|
+
* Throws with install instructions if not found.
|
|
189
|
+
*/
|
|
190
|
+
export function validateCliPresence(): void {
|
|
191
|
+
try {
|
|
192
|
+
execSync("claude --version", { stdio: "pipe", timeout: 5000 });
|
|
193
|
+
} catch {
|
|
194
|
+
throw new Error(
|
|
195
|
+
"Claude Code CLI not found. Install it: npm install -g @anthropic-ai/claude-code\n" +
|
|
196
|
+
"Then authenticate: claude auth login",
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Validate that the Claude CLI is authenticated.
|
|
203
|
+
* Returns false and warns if not authenticated.
|
|
204
|
+
*
|
|
205
|
+
* @returns true if authenticated, false otherwise
|
|
206
|
+
*/
|
|
207
|
+
export function validateCliAuth(): boolean {
|
|
208
|
+
try {
|
|
209
|
+
execSync("claude auth status", { stdio: "pipe", timeout: 5000 });
|
|
210
|
+
return true;
|
|
211
|
+
} catch {
|
|
212
|
+
console.warn(
|
|
213
|
+
"[pi-claude-cli] Claude CLI is not authenticated. " +
|
|
214
|
+
"Run 'claude auth login' to authenticate.",
|
|
215
|
+
);
|
|
216
|
+
return false;
|
|
217
|
+
}
|
|
218
|
+
}
|