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:
@@ -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",
|
||||
|
||||
@@ -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"),
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user