fix(extensions): always prefer vendored @fusion/pi-claude-cli over external installs

When users have an external pi-claude-cli (e.g. a global `npm install -g
pi-claude-cli`, or `npm:pi-claude-cli` in ~/.pi/agent/settings.json packages),
pi's extension discovery loaded the upstream copy and shadowed our fork. The
upstream has a once-and-lock MCP-config bug that throws "Extension runtime not
initialized" during early streamSimple calls and never recovers.

Adds reconcileClaudeCliPaths in @fusion/core, used by both the daemon's
extension assembly and the engine's per-session registerExtensionProviders, to
drop any path with a `pi-claude-cli` segment that isn't our vendored fork and
prepend the vendored path. Engine resolves the fork via require.resolve and
gracefully no-ops when it isn't reachable (e.g. embedded standalone usage).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsxdsm
2026-04-25 15:25:08 -07:00
parent d4c4137d83
commit 825fecbb35
7 changed files with 159 additions and 8 deletions

View File

@@ -36,6 +36,7 @@
},
"dependencies": {
"@fusion/core": "workspace:*",
"@fusion/pi-claude-cli": "workspace:*",
"@mariozechner/pi-ai": "^0.70.0",
"@mariozechner/pi-coding-agent": "^0.70.0",
"typebox": "^1.0.0",

View File

@@ -9,6 +9,7 @@
import { existsSync, readFileSync } from "node:fs";
import { exec } from "node:child_process";
import { promisify } from "node:util";
import { createRequire } from "node:module";
import { basename, dirname, join, relative, isAbsolute, resolve } from "node:path";
const execAsync = promisify(exec);
@@ -26,7 +27,7 @@ import {
type AgentSession,
type ToolDefinition,
} from "@mariozechner/pi-coding-agent";
import { getEnabledPiExtensionPaths, getFusionAgentDir, getLegacyPiAgentDir, resolvePiExtensionProjectRoot } from "@fusion/core";
import { getEnabledPiExtensionPaths, getFusionAgentDir, getLegacyPiAgentDir, reconcileClaudeCliPaths, resolvePiExtensionProjectRoot } from "@fusion/core";
import {
resolveSessionSkills,
createSkillsOverrideFromSelection,
@@ -674,6 +675,33 @@ function getPackageManagerAgentDir(): string {
return existsSync(fusionAgentDir) ? fusionAgentDir : legacyAgentDir;
}
/**
* Resolve the absolute path to Fusion's vendored `@fusion/pi-claude-cli`
* extension entry. Used by `registerExtensionProviders` to ensure the fork
* always wins over any externally-installed `pi-claude-cli`.
*
* Returns null when the vendored package isn't available (e.g. someone
* embedded `@fusion/engine` standalone without bundling the fork) — callers
* should treat that as "no override needed, leave external paths alone".
*/
function resolveVendoredClaudeCliEntry(): string | null {
try {
const require_ = createRequire(import.meta.url);
const pkgJsonPath = require_.resolve("@fusion/pi-claude-cli/package.json");
const pkgJson = JSON.parse(readFileSync(pkgJsonPath, "utf-8")) as {
pi?: { extensions?: unknown };
};
const extensions = pkgJson.pi?.extensions;
if (!Array.isArray(extensions) || extensions.length === 0) return null;
const entry = extensions[0];
if (typeof entry !== "string" || entry.length === 0) return null;
const path = resolve(dirname(pkgJsonPath), entry);
return existsSync(path) ? path : null;
} catch {
return null;
}
}
async function registerExtensionProviders(cwd: string, modelRegistry: ModelRegistry): Promise<void> {
try {
const agentDir = getPackageManagerAgentDir();
@@ -687,8 +715,19 @@ async function registerExtensionProviders(cwd: string, modelRegistry: ModelRegis
.filter((resource) => resource.enabled)
.map((resource) => resource.path);
const extensionsResult = await discoverAndLoadExtensions(
// Always prefer Fusion's vendored `@fusion/pi-claude-cli` over any external
// `pi-claude-cli` install (e.g. a global `npm install -g pi-claude-cli`,
// or `npm:pi-claude-cli` in agent settings). Upstream has known timing
// and once-and-lock MCP-config bugs that we fix in the fork; loading both
// also produces unpredictable provider-registration winners.
const vendoredClaudeCli = resolveVendoredClaudeCliEntry();
const reconciledPaths = reconcileClaudeCliPaths(
[...getEnabledPiExtensionPaths(cwd), ...packageExtensionPaths],
vendoredClaudeCli,
);
const extensionsResult = await discoverAndLoadExtensions(
reconciledPaths,
cwd,
join(resolvePiExtensionProjectRoot(cwd), ".fusion", "disabled-auto-extension-discovery"),
);