@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,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,191 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from "vitest";
|
|
2
|
+
import { PassThrough } from "node:stream";
|
|
3
|
+
import type { ClaudeControlRequest } from "../types";
|
|
4
|
+
import {
|
|
5
|
+
handleControlRequest,
|
|
6
|
+
TOOL_EXECUTION_DENIED_MESSAGE,
|
|
7
|
+
MCP_PREFIX,
|
|
8
|
+
} from "../control-handler";
|
|
9
|
+
|
|
10
|
+
function createMockStdin() {
|
|
11
|
+
const stream = new PassThrough();
|
|
12
|
+
const chunks: string[] = [];
|
|
13
|
+
stream.on("data", (data: Buffer) => chunks.push(data.toString()));
|
|
14
|
+
return { stream, chunks };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function makeControlRequest(
|
|
18
|
+
toolName: string,
|
|
19
|
+
requestId = "req-test-001",
|
|
20
|
+
input: Record<string, unknown> = {},
|
|
21
|
+
): ClaudeControlRequest {
|
|
22
|
+
return {
|
|
23
|
+
type: "control_request",
|
|
24
|
+
request_id: requestId,
|
|
25
|
+
request: {
|
|
26
|
+
subtype: "can_use_tool",
|
|
27
|
+
tool_name: toolName,
|
|
28
|
+
input,
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
describe("control-handler", () => {
|
|
34
|
+
describe("exported constants", () => {
|
|
35
|
+
it("exports TOOL_EXECUTION_DENIED_MESSAGE", () => {
|
|
36
|
+
expect(TOOL_EXECUTION_DENIED_MESSAGE).toBe(
|
|
37
|
+
"Tool execution is unavailable in this environment.",
|
|
38
|
+
);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("exports MCP_PREFIX", () => {
|
|
42
|
+
expect(MCP_PREFIX).toBe("mcp__");
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
describe("denies custom MCP tools (mcp__custom-tools__*)", () => {
|
|
47
|
+
it("denies mcp__custom-tools__weather and returns false", () => {
|
|
48
|
+
const { stream, chunks } = createMockStdin();
|
|
49
|
+
const msg = makeControlRequest("mcp__custom-tools__weather");
|
|
50
|
+
|
|
51
|
+
const result = handleControlRequest(msg, stream);
|
|
52
|
+
|
|
53
|
+
expect(result).toBe(false);
|
|
54
|
+
const response = JSON.parse(chunks[0].trim());
|
|
55
|
+
expect(response.response.response.behavior).toBe("deny");
|
|
56
|
+
expect(response.response.response.message).toBe(
|
|
57
|
+
TOOL_EXECUTION_DENIED_MESSAGE,
|
|
58
|
+
);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("denies mcp__custom-tools__deploy", () => {
|
|
62
|
+
const { stream, chunks } = createMockStdin();
|
|
63
|
+
const msg = makeControlRequest("mcp__custom-tools__deploy");
|
|
64
|
+
|
|
65
|
+
const result = handleControlRequest(msg, stream);
|
|
66
|
+
|
|
67
|
+
expect(result).toBe(false);
|
|
68
|
+
const response = JSON.parse(chunks[0].trim());
|
|
69
|
+
expect(response.response.response.behavior).toBe("deny");
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
describe("allows user MCP tools and other tools", () => {
|
|
74
|
+
it("allows user MCP tool mcp__database__query and returns true", () => {
|
|
75
|
+
const { stream, chunks } = createMockStdin();
|
|
76
|
+
const msg = makeControlRequest("mcp__database__query");
|
|
77
|
+
|
|
78
|
+
const result = handleControlRequest(msg, stream);
|
|
79
|
+
|
|
80
|
+
expect(result).toBe(true);
|
|
81
|
+
const response = JSON.parse(chunks[0].trim());
|
|
82
|
+
expect(response.response.response.behavior).toBe("allow");
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("allows built-in tool Read", () => {
|
|
86
|
+
const { stream, chunks } = createMockStdin();
|
|
87
|
+
const msg = makeControlRequest("Read");
|
|
88
|
+
|
|
89
|
+
const result = handleControlRequest(msg, stream);
|
|
90
|
+
|
|
91
|
+
expect(result).toBe(true);
|
|
92
|
+
const response = JSON.parse(chunks[0].trim());
|
|
93
|
+
expect(response.response.response.behavior).toBe("allow");
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("allows internal tools like ToolSearch", () => {
|
|
97
|
+
const { stream, chunks } = createMockStdin();
|
|
98
|
+
const msg = makeControlRequest("ToolSearch");
|
|
99
|
+
|
|
100
|
+
const result = handleControlRequest(msg, stream);
|
|
101
|
+
|
|
102
|
+
expect(result).toBe(true);
|
|
103
|
+
const response = JSON.parse(chunks[0].trim());
|
|
104
|
+
expect(response.response.response.behavior).toBe("allow");
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("allows unknown tools", () => {
|
|
108
|
+
const { stream, chunks } = createMockStdin();
|
|
109
|
+
const msg = makeControlRequest("SomeUnknownTool");
|
|
110
|
+
|
|
111
|
+
const result = handleControlRequest(msg, stream);
|
|
112
|
+
|
|
113
|
+
expect(result).toBe(true);
|
|
114
|
+
const response = JSON.parse(chunks[0].trim());
|
|
115
|
+
expect(response.response.response.behavior).toBe("allow");
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
describe("response format", () => {
|
|
120
|
+
it("includes matching request_id", () => {
|
|
121
|
+
const { stream, chunks } = createMockStdin();
|
|
122
|
+
const msg = makeControlRequest("Read", "custom-req-id-42");
|
|
123
|
+
|
|
124
|
+
handleControlRequest(msg, stream);
|
|
125
|
+
|
|
126
|
+
const response = JSON.parse(chunks[0].trim());
|
|
127
|
+
expect(response.request_id).toBe("custom-req-id-42");
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it("writes response as NDJSON (JSON + newline)", () => {
|
|
131
|
+
const { stream, chunks } = createMockStdin();
|
|
132
|
+
const msg = makeControlRequest("Read");
|
|
133
|
+
|
|
134
|
+
handleControlRequest(msg, stream);
|
|
135
|
+
|
|
136
|
+
expect(chunks[0].endsWith("\n")).toBe(true);
|
|
137
|
+
expect(() => JSON.parse(chunks[0].trim())).not.toThrow();
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it("deny response includes message field", () => {
|
|
141
|
+
const { stream, chunks } = createMockStdin();
|
|
142
|
+
const msg = makeControlRequest("mcp__custom-tools__foo");
|
|
143
|
+
|
|
144
|
+
handleControlRequest(msg, stream);
|
|
145
|
+
|
|
146
|
+
const response = JSON.parse(chunks[0].trim());
|
|
147
|
+
expect(response.response.response.message).toBe(
|
|
148
|
+
TOOL_EXECUTION_DENIED_MESSAGE,
|
|
149
|
+
);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it("allow response does not include a message field", () => {
|
|
153
|
+
const { stream, chunks } = createMockStdin();
|
|
154
|
+
const msg = makeControlRequest("mcp__database__query");
|
|
155
|
+
|
|
156
|
+
handleControlRequest(msg, stream);
|
|
157
|
+
|
|
158
|
+
const response = JSON.parse(chunks[0].trim());
|
|
159
|
+
expect(response.response.response.message).toBeUndefined();
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
describe("malformed input", () => {
|
|
164
|
+
it("returns false for missing request_id", () => {
|
|
165
|
+
const { stream } = createMockStdin();
|
|
166
|
+
const spy = vi.spyOn(console, "error").mockImplementation(() => {});
|
|
167
|
+
|
|
168
|
+
const msg = {
|
|
169
|
+
type: "control_request",
|
|
170
|
+
} as unknown as ClaudeControlRequest;
|
|
171
|
+
const result = handleControlRequest(msg, stream);
|
|
172
|
+
|
|
173
|
+
expect(result).toBe(false);
|
|
174
|
+
spy.mockRestore();
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it("returns false for missing request object", () => {
|
|
178
|
+
const { stream } = createMockStdin();
|
|
179
|
+
const spy = vi.spyOn(console, "error").mockImplementation(() => {});
|
|
180
|
+
|
|
181
|
+
const msg = {
|
|
182
|
+
type: "control_request",
|
|
183
|
+
request_id: "req-001",
|
|
184
|
+
} as unknown as ClaudeControlRequest;
|
|
185
|
+
const result = handleControlRequest(msg, stream);
|
|
186
|
+
|
|
187
|
+
expect(result).toBe(false);
|
|
188
|
+
spy.mockRestore();
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
});
|