@runfusion/fusion 0.2.7 → 0.4.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/dist/bin.js +55392 -51635
- package/dist/client/assets/{AgentDetailView-BMrHuWGs.css → AgentDetailView-C1b9PC5l.css} +1 -1
- package/dist/client/assets/{AgentDetailView-B4lRk--v.js → AgentDetailView-DJwWfkpv.js} +1 -1
- package/dist/client/assets/{AgentsView-yCYBY2km.js → AgentsView-DegK8aw-.js} +5 -5
- package/dist/client/assets/ChatView-CYpEShLS.js +1 -0
- package/dist/client/assets/{DevServerView-jXXtoQUx.js → DevServerView-DfCTA9fx.js} +2 -2
- package/dist/client/assets/{DirectoryPicker-izgMlS27.js → DirectoryPicker-B0qNpfLW.js} +1 -1
- package/dist/client/assets/DirectoryPicker-DzKVmxOf.css +1 -0
- package/dist/client/assets/{DocumentsView-DkkoHRwL.js → DocumentsView-CsQxuyz3.js} +1 -1
- package/dist/client/assets/{InsightsView-DaRtUPHX.js → InsightsView-Bzs7A2jv.js} +2 -2
- package/dist/client/assets/MemoryView-Cl5ASqjW.js +2 -0
- package/dist/client/assets/MemoryView-DiajLXby.css +1 -0
- package/dist/client/assets/{NodesView-BsUk_oiU.js → NodesView-BpiqRlvc.js} +1 -1
- package/dist/client/assets/PiExtensionsManager-Cr6EoC7S.js +11 -0
- package/dist/client/assets/PiExtensionsManager-kgTOHPE9.css +1 -0
- package/dist/client/assets/PluginManager-DRiIqol2.css +1 -0
- package/dist/client/assets/PluginManager-DXtQdfns.js +1 -0
- package/dist/client/assets/{RoadmapsView-SQol126Y.js → RoadmapsView-CYPLTTB0.js} +2 -2
- package/dist/client/assets/SettingsModal-CDWvhvrd.css +1 -0
- package/dist/client/assets/SettingsModal-CNdVTVqD.js +1 -0
- package/dist/client/assets/SettingsModal-CyCC7MzL.js +31 -0
- package/dist/client/assets/SettingsModal-G0ESQXRD.css +1 -0
- package/dist/client/assets/{SetupWizardModal-CQc1uGSq.js → SetupWizardModal-BLiljNn7.js} +1 -1
- package/dist/client/assets/SetupWizardModal-BMa6p24b.css +1 -0
- package/dist/client/assets/SkillsView-Dlpw5LKI.js +1 -0
- package/dist/client/assets/{folder-open-CI4TCD7P.js → folder-open-B_38R5AA.js} +1 -1
- package/dist/client/assets/index-DQKtk17v.js +616 -0
- package/dist/client/assets/index-DjOxzdj3.css +1 -0
- package/dist/client/assets/{upload-CAlKC4qI.js → upload-DNQF7XCK.js} +1 -1
- package/dist/client/assets/users-CG2_rCdk.js +6 -0
- package/dist/client/index.html +2 -2
- package/dist/client/version.json +1 -0
- package/dist/extension.js +3222 -937
- package/dist/pi-claude-cli/index.ts +72 -28
- package/dist/pi-claude-cli/package.json +1 -1
- package/dist/pi-claude-cli/src/__tests__/event-bridge.test.ts +34 -0
- package/dist/pi-claude-cli/src/__tests__/mcp-config.test.ts +22 -0
- package/dist/pi-claude-cli/src/__tests__/prompt-builder.test.ts +72 -10
- package/dist/pi-claude-cli/src/__tests__/provider.test.ts +9 -9
- package/dist/pi-claude-cli/src/event-bridge.ts +17 -6
- package/dist/pi-claude-cli/src/mcp-config.ts +36 -3
- package/dist/pi-claude-cli/src/prompt-builder.ts +111 -7
- package/dist/pi-claude-cli/src/provider.ts +17 -2
- package/package.json +17 -16
- package/skill/fusion/SKILL.md +6 -1
- package/skill/fusion/references/engine-tools.md +54 -0
- package/skill/fusion/references/extension-tools.md +83 -84
- package/skill/fusion/references/fusion-capabilities.md +33 -31
- package/LICENSE +0 -21
- package/dist/client/assets/ChatView-CH9T0dDs.js +0 -1
- package/dist/client/assets/MemoryView-85NKuU3h.js +0 -2
- package/dist/client/assets/MemoryView-DhinauGs.css +0 -1
- package/dist/client/assets/PiExtensionsManager-BF5pxrSE.js +0 -11
- package/dist/client/assets/PiExtensionsManager-K7HQ08L4.css +0 -1
- package/dist/client/assets/PluginManager-ccq3uK50.css +0 -1
- package/dist/client/assets/PluginManager-s6btydh5.js +0 -1
- package/dist/client/assets/SkillsView-BtUhs_QW.js +0 -1
- package/dist/client/assets/index-Ct-OqLpP.css +0 -1
- package/dist/client/assets/index-rNf7s96d.js +0 -649
|
@@ -13,56 +13,93 @@ import {
|
|
|
13
13
|
validateCliAuth,
|
|
14
14
|
killAllProcesses,
|
|
15
15
|
} from "./src/process-manager.js";
|
|
16
|
-
import {
|
|
16
|
+
import { createHash } from "node:crypto";
|
|
17
|
+
import {
|
|
18
|
+
getCustomToolDefs,
|
|
19
|
+
toolsFromContext,
|
|
20
|
+
writeMcpConfig,
|
|
21
|
+
type McpToolDef,
|
|
22
|
+
} from "./src/mcp-config.js";
|
|
17
23
|
|
|
18
24
|
// Kill all active Claude subprocesses on process exit to prevent orphans
|
|
19
25
|
process.on("exit", killAllProcesses);
|
|
20
26
|
|
|
21
27
|
const PROVIDER_ID = "pi-claude-cli";
|
|
22
28
|
|
|
23
|
-
let
|
|
24
|
-
let mcpConfigResolved = false;
|
|
29
|
+
let cachedMcpConfig: { hash: string; configPath: string } | undefined;
|
|
25
30
|
|
|
26
31
|
/**
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
32
|
+
* Resolve the MCP config path for the current request, regenerating it when
|
|
33
|
+
* the set of custom tools changes.
|
|
34
|
+
*
|
|
35
|
+
* Source of truth (in order of preference):
|
|
36
|
+
* 1. `context.tools` — the per-session tool list pi-ai actually hands to
|
|
37
|
+
* `streamSimple`. This is what the session is asking the model to see, so
|
|
38
|
+
* it includes session-scoped registrations (e.g. `fn_review_spec` and
|
|
39
|
+
* `fn_review_step` injected by the engine's triage/executor sessions).
|
|
40
|
+
* 2. `pi.getAllTools()` — fallback for older callers that don't supply
|
|
41
|
+
* `context.tools`.
|
|
30
42
|
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
43
|
+
* Why not a single once-and-lock cache:
|
|
44
|
+
* - The engine spawns triage/executor sessions with session-scoped tools.
|
|
45
|
+
* A locked-on-first-call cache silently drops them and the Claude CLI
|
|
46
|
+
* subprocess refuses with "unknown tool fn_review_spec".
|
|
47
|
+
* - Hashing the tool defs lets us reuse temp files when the tool set is
|
|
48
|
+
* unchanged across calls and produce fresh files (with the hash in the
|
|
49
|
+
* filename to avoid races) when it changes.
|
|
35
50
|
*
|
|
36
|
-
* Uses warn-don't-block: failure logs a warning but does not
|
|
37
|
-
*
|
|
51
|
+
* Uses warn-don't-block: failure logs a warning but does not prevent the
|
|
52
|
+
* provider from functioning (built-ins still work).
|
|
38
53
|
*/
|
|
39
|
-
function ensureMcpConfig(
|
|
40
|
-
|
|
54
|
+
function ensureMcpConfig(
|
|
55
|
+
pi: ExtensionAPI,
|
|
56
|
+
contextTools?: ReadonlyArray<{
|
|
57
|
+
name: string;
|
|
58
|
+
description: string;
|
|
59
|
+
parameters: Record<string, unknown>;
|
|
60
|
+
}>,
|
|
61
|
+
): string | undefined {
|
|
41
62
|
try {
|
|
42
|
-
|
|
63
|
+
let toolDefs: McpToolDef[] = toolsFromContext(contextTools);
|
|
64
|
+
|
|
65
|
+
// Fallback to the pi runtime registry if the context didn't carry tools.
|
|
66
|
+
// (Older agent-loop versions don't populate Context.tools for streamSimple.)
|
|
67
|
+
if (toolDefs.length === 0) {
|
|
68
|
+
const allTools = pi.getAllTools();
|
|
69
|
+
if (!Array.isArray(allTools)) {
|
|
70
|
+
return cachedMcpConfig?.configPath;
|
|
71
|
+
}
|
|
72
|
+
toolDefs = getCustomToolDefs(pi);
|
|
73
|
+
}
|
|
43
74
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
return
|
|
75
|
+
if (toolDefs.length === 0) {
|
|
76
|
+
cachedMcpConfig = undefined;
|
|
77
|
+
return undefined;
|
|
47
78
|
}
|
|
48
79
|
|
|
49
|
-
|
|
50
|
-
|
|
80
|
+
const hash = createHash("sha1")
|
|
81
|
+
.update(JSON.stringify(toolDefs))
|
|
82
|
+
.digest("hex")
|
|
83
|
+
.slice(0, 12);
|
|
51
84
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
mcpConfigPath = writeMcpConfig(toolDefs);
|
|
55
|
-
console.error(
|
|
56
|
-
`[pi-claude-cli] MCP config generated with ${toolDefs.length} custom tool(s)`,
|
|
57
|
-
);
|
|
85
|
+
if (cachedMcpConfig?.hash === hash) {
|
|
86
|
+
return cachedMcpConfig.configPath;
|
|
58
87
|
}
|
|
88
|
+
|
|
89
|
+
const configPath = writeMcpConfig(toolDefs, hash);
|
|
90
|
+
cachedMcpConfig = { hash, configPath };
|
|
91
|
+
const toolNames = toolDefs.map((t) => t.name).join(", ");
|
|
92
|
+
console.error(
|
|
93
|
+
`[pi-claude-cli] MCP config refreshed with ${toolDefs.length} custom tool(s) [${toolNames}] (hash=${hash})`,
|
|
94
|
+
);
|
|
95
|
+
return configPath;
|
|
59
96
|
} catch (err) {
|
|
60
97
|
console.warn(
|
|
61
98
|
"[pi-claude-cli] MCP config generation failed, custom tools unavailable:",
|
|
62
99
|
err,
|
|
63
100
|
);
|
|
101
|
+
return cachedMcpConfig?.configPath;
|
|
64
102
|
}
|
|
65
|
-
return mcpConfigPath;
|
|
66
103
|
}
|
|
67
104
|
|
|
68
105
|
export default function (pi: ExtensionAPI) {
|
|
@@ -118,7 +155,14 @@ export default function (pi: ExtensionAPI) {
|
|
|
118
155
|
api: "pi-claude-cli",
|
|
119
156
|
models,
|
|
120
157
|
streamSimple: (model, context, options) => {
|
|
121
|
-
const configPath = ensureMcpConfig(
|
|
158
|
+
const configPath = ensureMcpConfig(
|
|
159
|
+
pi,
|
|
160
|
+
(context as { tools?: ReadonlyArray<{
|
|
161
|
+
name: string;
|
|
162
|
+
description: string;
|
|
163
|
+
parameters: Record<string, unknown>;
|
|
164
|
+
}> }).tools,
|
|
165
|
+
);
|
|
122
166
|
return streamViaCli(model, context, {
|
|
123
167
|
...options,
|
|
124
168
|
mcpConfigPath: configPath,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fusion/pi-claude-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.0",
|
|
4
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
5
|
"license": "MIT",
|
|
6
6
|
"private": true,
|
|
@@ -444,6 +444,40 @@ describe("createEventBridge", () => {
|
|
|
444
444
|
expect(event.toolCall.arguments).toEqual({ path: "/foo.ts" });
|
|
445
445
|
});
|
|
446
446
|
|
|
447
|
+
it("emits {} for parameterless MCP tool calls (no input_json_delta)", () => {
|
|
448
|
+
// Parameterless MCP tools (e.g. fn_review_spec, schema
|
|
449
|
+
// {type:"object", properties:{}}) emit ZERO input_json_delta events.
|
|
450
|
+
// Without the empty-partialJson guard, finalArgs would fall through to
|
|
451
|
+
// the raw-string fallback ("") and pi's TypeBox validator would reject
|
|
452
|
+
// the call with "Validation failed for tool ...: root: must be object".
|
|
453
|
+
const bridge = createBridgeWithStart();
|
|
454
|
+
bridge.handleEvent({
|
|
455
|
+
type: "content_block_start",
|
|
456
|
+
index: 0,
|
|
457
|
+
content_block: {
|
|
458
|
+
type: "tool_use",
|
|
459
|
+
id: "toolu_01XYZ",
|
|
460
|
+
name: "mcp__custom-tools__fn_review_spec",
|
|
461
|
+
},
|
|
462
|
+
});
|
|
463
|
+
// No content_block_delta with input_json_delta — Claude emits none for
|
|
464
|
+
// parameterless tools.
|
|
465
|
+
stream.push.mockClear();
|
|
466
|
+
stream.events.length = 0;
|
|
467
|
+
|
|
468
|
+
bridge.handleEvent({
|
|
469
|
+
type: "content_block_stop",
|
|
470
|
+
index: 0,
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
expect(stream.push).toHaveBeenCalledTimes(1);
|
|
474
|
+
const event = stream.events[0] as any;
|
|
475
|
+
expect(event.type).toBe("toolcall_end");
|
|
476
|
+
expect(event.toolCall.arguments).toEqual({});
|
|
477
|
+
// The MCP prefix should be stripped: pi sees the bare tool name.
|
|
478
|
+
expect(event.toolCall.name).toBe("fn_review_spec");
|
|
479
|
+
});
|
|
480
|
+
|
|
447
481
|
it("tracks multiple tool_use blocks independently by Claude event.index", () => {
|
|
448
482
|
const bridge = createBridgeWithStart();
|
|
449
483
|
|
|
@@ -269,4 +269,26 @@ describe("writeMcpConfig", () => {
|
|
|
269
269
|
expect(result).toMatch(/pi-claude-mcp-config/);
|
|
270
270
|
expect(result).toMatch(/\.json$/);
|
|
271
271
|
});
|
|
272
|
+
|
|
273
|
+
it("includes cacheKey in filenames when provided so distinct tool sets do not collide", () => {
|
|
274
|
+
const toolDefs: McpToolDef[] = [
|
|
275
|
+
{
|
|
276
|
+
name: "search",
|
|
277
|
+
description: "Search",
|
|
278
|
+
inputSchema: { type: "object" },
|
|
279
|
+
},
|
|
280
|
+
];
|
|
281
|
+
|
|
282
|
+
const pathA = writeMcpConfig(toolDefs, "aaaaaaaaaaaa");
|
|
283
|
+
const pathB = writeMcpConfig(toolDefs, "bbbbbbbbbbbb");
|
|
284
|
+
|
|
285
|
+
expect(pathA).toContain("aaaaaaaaaaaa");
|
|
286
|
+
expect(pathB).toContain("bbbbbbbbbbbb");
|
|
287
|
+
expect(pathA).not.toBe(pathB);
|
|
288
|
+
|
|
289
|
+
const schemaPathA = mocks.writeFileSync.mock.calls[0][0];
|
|
290
|
+
const schemaPathB = mocks.writeFileSync.mock.calls[2][0];
|
|
291
|
+
expect(schemaPathA).toContain("aaaaaaaaaaaa");
|
|
292
|
+
expect(schemaPathB).toContain("bbbbbbbbbbbb");
|
|
293
|
+
});
|
|
272
294
|
});
|
|
@@ -21,7 +21,7 @@ describe("buildPrompt", () => {
|
|
|
21
21
|
expect(buildPrompt(context)).toBe("ASSISTANT:\nHi there");
|
|
22
22
|
});
|
|
23
23
|
|
|
24
|
-
it("produces 'TOOL RESULT (
|
|
24
|
+
it("produces 'TOOL RESULT ({claudeName}):\\n{content}' for a tool result message", () => {
|
|
25
25
|
const context = {
|
|
26
26
|
messages: [
|
|
27
27
|
{
|
|
@@ -33,7 +33,7 @@ describe("buildPrompt", () => {
|
|
|
33
33
|
} as unknown as any;
|
|
34
34
|
// Pi tool name "read" should be mapped to Claude name "Read" in the label
|
|
35
35
|
expect(buildPrompt(context)).toBe(
|
|
36
|
-
"TOOL RESULT (
|
|
36
|
+
"TOOL RESULT (Read):\nfile contents here",
|
|
37
37
|
);
|
|
38
38
|
});
|
|
39
39
|
|
|
@@ -57,7 +57,7 @@ describe("buildPrompt", () => {
|
|
|
57
57
|
"What is in file.ts?",
|
|
58
58
|
"ASSISTANT:",
|
|
59
59
|
"Let me read that file.",
|
|
60
|
-
"TOOL RESULT (
|
|
60
|
+
"TOOL RESULT (Read):",
|
|
61
61
|
"export const x = 1;",
|
|
62
62
|
"USER:",
|
|
63
63
|
"Now explain it.",
|
|
@@ -109,7 +109,7 @@ describe("buildPrompt", () => {
|
|
|
109
109
|
// Tool name should be mapped from pi "read" to Claude "Read"
|
|
110
110
|
// Arg "path" should be mapped from pi format to Claude "file_path"
|
|
111
111
|
expect(result).toContain(
|
|
112
|
-
'
|
|
112
|
+
'[Prior tool call — already executed; result follows in TOOL RESULT (Read):] args={"file_path":"/file.ts"}',
|
|
113
113
|
);
|
|
114
114
|
});
|
|
115
115
|
|
|
@@ -158,7 +158,7 @@ describe("buildPrompt", () => {
|
|
|
158
158
|
const result = buildPrompt(context);
|
|
159
159
|
// Pi "bash" maps to Claude "Bash"
|
|
160
160
|
expect(result).toContain(
|
|
161
|
-
"
|
|
161
|
+
"[Prior tool call — already executed; result follows in TOOL RESULT (Bash):] args={}",
|
|
162
162
|
);
|
|
163
163
|
});
|
|
164
164
|
|
|
@@ -177,7 +177,7 @@ describe("buildPrompt", () => {
|
|
|
177
177
|
} as unknown as any;
|
|
178
178
|
|
|
179
179
|
const result = buildPrompt(context);
|
|
180
|
-
expect(result).toBe("TOOL RESULT (
|
|
180
|
+
expect(result).toBe("TOOL RESULT (Bash):\nline 1\nline 2");
|
|
181
181
|
});
|
|
182
182
|
|
|
183
183
|
describe("tool name and argument reverse mapping", () => {
|
|
@@ -237,7 +237,7 @@ describe("buildPrompt", () => {
|
|
|
237
237
|
} as unknown as any;
|
|
238
238
|
|
|
239
239
|
const result = buildPrompt(context);
|
|
240
|
-
expect(result).toContain("TOOL RESULT (
|
|
240
|
+
expect(result).toContain("TOOL RESULT (Read):");
|
|
241
241
|
});
|
|
242
242
|
|
|
243
243
|
it("prefixes custom (non-built-in) tool names with MCP prefix", () => {
|
|
@@ -281,7 +281,8 @@ describe("buildPrompt", () => {
|
|
|
281
281
|
|
|
282
282
|
const result = buildPrompt(context);
|
|
283
283
|
// String arguments should be serialized as JSON string
|
|
284
|
-
expect(result).toContain('
|
|
284
|
+
expect(result).toContain('TOOL RESULT (Read):');
|
|
285
|
+
expect(result).toContain('args="raw string args"');
|
|
285
286
|
});
|
|
286
287
|
});
|
|
287
288
|
});
|
|
@@ -688,7 +689,7 @@ describe("custom tool history replay", () => {
|
|
|
688
689
|
} as unknown as any;
|
|
689
690
|
|
|
690
691
|
const result = buildPrompt(context);
|
|
691
|
-
expect(result).toContain("TOOL RESULT (
|
|
692
|
+
expect(result).toContain("TOOL RESULT (Read):");
|
|
692
693
|
expect(result).not.toContain("mcp__custom-tools__");
|
|
693
694
|
});
|
|
694
695
|
|
|
@@ -910,6 +911,67 @@ describe("buildSystemPrompt", () => {
|
|
|
910
911
|
expect(result).toContain("IMPORTANT:");
|
|
911
912
|
expect(result).toContain("tool results");
|
|
912
913
|
});
|
|
914
|
+
|
|
915
|
+
it("rewrites bare custom tool references to MCP-prefixed names", async () => {
|
|
916
|
+
vi.resetModules();
|
|
917
|
+
vi.doMock("node:fs", () => ({
|
|
918
|
+
existsSync: () => false,
|
|
919
|
+
readFileSync: () => "",
|
|
920
|
+
}));
|
|
921
|
+
|
|
922
|
+
const { buildSystemPrompt: bsp } = await import("../prompt-builder");
|
|
923
|
+
const context = {
|
|
924
|
+
systemPrompt:
|
|
925
|
+
"Write the PROMPT.md, then call `fn_review_spec()` for review. " +
|
|
926
|
+
"If REVISE, call fn_review_spec again. Do not call mcp__custom-tools__fn_review_spec twice manually.",
|
|
927
|
+
messages: [],
|
|
928
|
+
tools: [
|
|
929
|
+
{ name: "fn_review_spec", description: "review", parameters: {} },
|
|
930
|
+
{ name: "read", description: "builtin", parameters: {} },
|
|
931
|
+
],
|
|
932
|
+
} as unknown as any;
|
|
933
|
+
const result = bsp(context, "/some/project");
|
|
934
|
+
|
|
935
|
+
// Bare name occurrences are rewritten
|
|
936
|
+
expect(result).toContain(
|
|
937
|
+
"call `mcp__custom-tools__fn_review_spec()` for review",
|
|
938
|
+
);
|
|
939
|
+
expect(result).toContain(
|
|
940
|
+
"call mcp__custom-tools__fn_review_spec again",
|
|
941
|
+
);
|
|
942
|
+
// Already-prefixed occurrence is not double-prefixed
|
|
943
|
+
expect(result).not.toContain(
|
|
944
|
+
"mcp__custom-tools__mcp__custom-tools__fn_review_spec",
|
|
945
|
+
);
|
|
946
|
+
// Built-in pi tool names are not rewritten
|
|
947
|
+
expect(result).not.toContain("mcp__custom-tools__read");
|
|
948
|
+
// The addendum still lists the custom tool with its full mapping
|
|
949
|
+
expect(result).toContain("mcp__custom-tools__fn_review_spec");
|
|
950
|
+
});
|
|
951
|
+
|
|
952
|
+
it("does not rewrite identifier substrings that happen to overlap a tool name", async () => {
|
|
953
|
+
vi.resetModules();
|
|
954
|
+
vi.doMock("node:fs", () => ({
|
|
955
|
+
existsSync: () => false,
|
|
956
|
+
readFileSync: () => "",
|
|
957
|
+
}));
|
|
958
|
+
|
|
959
|
+
const { buildSystemPrompt: bsp } = await import("../prompt-builder");
|
|
960
|
+
const context = {
|
|
961
|
+
systemPrompt: "fn_review_specifier and fn_reviews are not the tool.",
|
|
962
|
+
messages: [],
|
|
963
|
+
tools: [
|
|
964
|
+
{ name: "fn_review", description: "x", parameters: {} },
|
|
965
|
+
{ name: "fn_review_spec", description: "y", parameters: {} },
|
|
966
|
+
],
|
|
967
|
+
} as unknown as any;
|
|
968
|
+
const result = bsp(context, "/some/project");
|
|
969
|
+
// Neither substring should be rewritten — they're different identifiers.
|
|
970
|
+
expect(result).toContain("fn_review_specifier");
|
|
971
|
+
expect(result).toContain("fn_reviews");
|
|
972
|
+
expect(result).not.toContain("mcp__custom-tools__fn_review_specifier");
|
|
973
|
+
expect(result).not.toContain("mcp__custom-tools__fn_reviews");
|
|
974
|
+
});
|
|
913
975
|
});
|
|
914
976
|
|
|
915
977
|
describe("buildResumePrompt", () => {
|
|
@@ -958,7 +1020,7 @@ describe("buildResumePrompt", () => {
|
|
|
958
1020
|
],
|
|
959
1021
|
};
|
|
960
1022
|
const result = buildResumePrompt(context) as string;
|
|
961
|
-
expect(result).toContain("TOOL RESULT (
|
|
1023
|
+
expect(result).toContain("TOOL RESULT (Read):");
|
|
962
1024
|
expect(result).toContain("file contents here");
|
|
963
1025
|
expect(result).toContain("Now explain it");
|
|
964
1026
|
});
|
|
@@ -1292,7 +1292,7 @@ describe("streamViaCli", () => {
|
|
|
1292
1292
|
});
|
|
1293
1293
|
|
|
1294
1294
|
describe("inactivity timeout", () => {
|
|
1295
|
-
it("kills subprocess and pushes error after
|
|
1295
|
+
it("kills subprocess and pushes error after 1800s of no output", async () => {
|
|
1296
1296
|
const model = mockModels[0] as any;
|
|
1297
1297
|
const context = {
|
|
1298
1298
|
messages: [{ role: "user", content: "Hello" }],
|
|
@@ -1303,8 +1303,8 @@ describe("streamViaCli", () => {
|
|
|
1303
1303
|
|
|
1304
1304
|
const proc = (spawn as any).mock.results[0].value;
|
|
1305
1305
|
|
|
1306
|
-
// Advance timers by
|
|
1307
|
-
await vi.advanceTimersByTimeAsync(
|
|
1306
|
+
// Advance timers by 1800 seconds without writing to stdout
|
|
1307
|
+
await vi.advanceTimersByTimeAsync(1_800_000);
|
|
1308
1308
|
|
|
1309
1309
|
const mockStream = MockAssistantMessageEventStream.mock.instances[0];
|
|
1310
1310
|
const doneEvent = mockStream._events.find(
|
|
@@ -1330,8 +1330,8 @@ describe("streamViaCli", () => {
|
|
|
1330
1330
|
|
|
1331
1331
|
const proc = (spawn as any).mock.results[0].value;
|
|
1332
1332
|
|
|
1333
|
-
// Advance to
|
|
1334
|
-
await vi.advanceTimersByTimeAsync(
|
|
1333
|
+
// Advance to 290s then write a line (just under the 300s cap)
|
|
1334
|
+
await vi.advanceTimersByTimeAsync(1_790_000);
|
|
1335
1335
|
|
|
1336
1336
|
// Write a stream event line
|
|
1337
1337
|
proc.stdout.write(
|
|
@@ -1345,8 +1345,8 @@ describe("streamViaCli", () => {
|
|
|
1345
1345
|
);
|
|
1346
1346
|
await vi.advanceTimersByTimeAsync(0);
|
|
1347
1347
|
|
|
1348
|
-
// Advance another
|
|
1349
|
-
await vi.advanceTimersByTimeAsync(
|
|
1348
|
+
// Advance another 290s (580s total, 290s since last line) -- should NOT timeout
|
|
1349
|
+
await vi.advanceTimersByTimeAsync(1_790_000);
|
|
1350
1350
|
|
|
1351
1351
|
const mockStream = MockAssistantMessageEventStream.mock.instances[0];
|
|
1352
1352
|
const doneEvent = mockStream._events.find(
|
|
@@ -1354,7 +1354,7 @@ describe("streamViaCli", () => {
|
|
|
1354
1354
|
);
|
|
1355
1355
|
expect(doneEvent).toBeUndefined();
|
|
1356
1356
|
|
|
1357
|
-
// Advance 10 more seconds (
|
|
1357
|
+
// Advance 10 more seconds (1800s since last line) -- NOW should timeout
|
|
1358
1358
|
await vi.advanceTimersByTimeAsync(10_000);
|
|
1359
1359
|
|
|
1360
1360
|
const doneEvent2 = mockStream._events.find(
|
|
@@ -1401,7 +1401,7 @@ describe("streamViaCli", () => {
|
|
|
1401
1401
|
await vi.advanceTimersByTimeAsync(100);
|
|
1402
1402
|
|
|
1403
1403
|
// Advance past 180s -- should NOT timeout since result was received
|
|
1404
|
-
await vi.advanceTimersByTimeAsync(
|
|
1404
|
+
await vi.advanceTimersByTimeAsync(1_800_000);
|
|
1405
1405
|
|
|
1406
1406
|
const mockStream = MockAssistantMessageEventStream.mock.instances[0];
|
|
1407
1407
|
const errorEvents = mockStream._events.filter(
|
|
@@ -323,13 +323,24 @@ export function createEventBridge(
|
|
|
323
323
|
partial: output,
|
|
324
324
|
});
|
|
325
325
|
} else if (block.type === "tool_use") {
|
|
326
|
-
// Final JSON parse with fallback to raw string
|
|
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.
|
|
327
333
|
let finalArgs: Record<string, unknown> | string;
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
finalArgs =
|
|
331
|
-
}
|
|
332
|
-
|
|
334
|
+
const trimmedJson = block.partialJson.trim();
|
|
335
|
+
if (trimmedJson === "") {
|
|
336
|
+
finalArgs = {};
|
|
337
|
+
} else {
|
|
338
|
+
try {
|
|
339
|
+
const parsed = JSON.parse(trimmedJson);
|
|
340
|
+
finalArgs = translateClaudeArgsToPi(block.claudeName, parsed);
|
|
341
|
+
} catch {
|
|
342
|
+
finalArgs = block.partialJson;
|
|
343
|
+
}
|
|
333
344
|
}
|
|
334
345
|
|
|
335
346
|
// Update output.content with final arguments
|
|
@@ -67,6 +67,31 @@ export function getCustomToolDefs(pi: PiInstance): McpToolDef[] {
|
|
|
67
67
|
}));
|
|
68
68
|
}
|
|
69
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
|
+
|
|
70
95
|
/**
|
|
71
96
|
* Write MCP config and tool schemas to temp files.
|
|
72
97
|
*
|
|
@@ -75,13 +100,21 @@ export function getCustomToolDefs(pi: PiInstance): McpToolDef[] {
|
|
|
75
100
|
* 2. Config file: MCP config pointing to the schema-only server
|
|
76
101
|
*
|
|
77
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.
|
|
78
106
|
* @returns Path to the MCP config file
|
|
79
107
|
*/
|
|
80
|
-
export function writeMcpConfig(
|
|
108
|
+
export function writeMcpConfig(
|
|
109
|
+
toolDefs: McpToolDef[],
|
|
110
|
+
cacheKey?: string,
|
|
111
|
+
): string {
|
|
112
|
+
const suffix = cacheKey ? `${process.pid}-${cacheKey}` : `${process.pid}`;
|
|
113
|
+
|
|
81
114
|
// Write tool schemas to temp file
|
|
82
115
|
const schemaFilePath = join(
|
|
83
116
|
tmpdir(),
|
|
84
|
-
`pi-claude-mcp-schemas-${
|
|
117
|
+
`pi-claude-mcp-schemas-${suffix}.json`,
|
|
85
118
|
);
|
|
86
119
|
writeFileSync(schemaFilePath, JSON.stringify(toolDefs));
|
|
87
120
|
|
|
@@ -103,7 +136,7 @@ export function writeMcpConfig(toolDefs: McpToolDef[]): string {
|
|
|
103
136
|
// Write config to temp file
|
|
104
137
|
const configFilePath = join(
|
|
105
138
|
tmpdir(),
|
|
106
|
-
`pi-claude-mcp-config-${
|
|
139
|
+
`pi-claude-mcp-config-${suffix}.json`,
|
|
107
140
|
);
|
|
108
141
|
writeFileSync(configFilePath, JSON.stringify(config));
|
|
109
142
|
|