feat(acp): U11 — drive pi-claude-cli provider via the ACP bridge (kill-switch OFF)

Adds streamViaAcp: a drop-in alternative to streamViaCli that drives Claude
through the claude-code-cli-acp bridge over ACP instead of `claude -p`. Returns
the same AssistantMessageEventStream, so streamSimple dispatches to either
transport behind a kill-switch (FUSION_CLAUDE_ACP=1 + an injected bridge path),
OFF by default — the live `-p` path is byte-for-byte untouched until soak.

- Full-history prompt every turn (buildPrompt) — the ACP path has no --resume (R13).
- Forwards schema-only MCP servers so Claude emits correct tool calls; breaks
  early on the first tool_call (cancel turn, surface to pi) so the bridge never
  executes Fusion's tools — mirrors the `-p` break-early pattern.
- Translation reuses the tested createEventBridge by synthesizing Claude stream
  events from ACP session/updates, sharing pi sequencing + tool-name mapping.
- Bridge env forwards only HOME/PATH so `claude` authenticates from the login
  session (R17); never inherited process.env or API keys.

Verified: 3/3 translation unit tests; real-bridge session/update shapes confirmed
(agent_message_chunk text + tool_call); 326/326 existing pi-claude-cli tests green;
typecheck clean.

Remaining for Route A: engine injection of the bridge path (KTD10), U12 picker/
auth/status, U13 workflow verification.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
gsxdsm
2026-06-15 06:08:27 -07:00
parent 03d12b6289
commit 85c180508c
6 changed files with 455 additions and 76 deletions

View File

@@ -8,6 +8,7 @@
import { getModels } from "@earendil-works/pi-ai";
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
import { streamViaCli } from "./src/provider.js";
import { streamViaAcp } from "./src/acp-driver.js";
import {
validateCliPresenceAsync,
validateCliAuthAsync,
@@ -18,9 +19,32 @@ import {
getCustomToolDefs,
toolsFromContext,
writeMcpConfig,
buildAcpMcpServers,
type McpToolDef,
} from "./src/mcp-config.js";
/**
* Route A kill-switch (U11). When `FUSION_CLAUDE_ACP=1` AND a bridge binary path
* is available (`FUSION_CLAUDE_ACP_BRIDGE`, injected by the engine seam per
* KTD10), the provider drives Claude through the ACP bridge instead of
* `claude -p`. OFF by default: the live `-p` path is untouched until soak.
*/
function resolveAcpBridgePath(): string | undefined {
if (process.env.FUSION_CLAUDE_ACP !== "1") return undefined;
const p = process.env.FUSION_CLAUDE_ACP_BRIDGE;
return typeof p === "string" && p.length > 0 ? p : undefined;
}
/** Resolve custom tool defs the same way ensureMcpConfig does (context → registry). */
function resolveToolDefs(
pi: ExtensionAPI,
contextTools?: ReadonlyArray<{ name: string; description: string; parameters: Record<string, unknown> }>,
): McpToolDef[] {
let toolDefs = toolsFromContext(contextTools);
if (toolDefs.length === 0 && Array.isArray(pi.getAllTools())) toolDefs = getCustomToolDefs(pi);
return toolDefs;
}
// Kill all active Claude subprocesses on process exit to prevent orphans
process.on("exit", killAllProcesses);
@@ -220,14 +244,29 @@ export default function (pi: ExtensionAPI) {
api: "pi-claude-cli",
models,
streamSimple: (model, context, options) => {
const configPath = ensureMcpConfig(
pi,
(context as { tools?: ReadonlyArray<{
name: string;
description: string;
parameters: Record<string, unknown>;
}> }).tools,
);
const contextTools = (context as { tools?: ReadonlyArray<{
name: string;
description: string;
parameters: Record<string, unknown>;
}> }).tools;
// Route A (U11): drive Claude through the ACP bridge when the kill-switch
// is on AND a bridge path is injected. OFF by default → `-p` path below.
const bridgePath = resolveAcpBridgePath();
if (bridgePath) {
const toolDefs = resolveToolDefs(pi, contextTools);
const hash = createHash("sha1").update(JSON.stringify(toolDefs)).digest("hex").slice(0, 12);
return streamViaAcp(model, context, {
...options,
bridgePath,
mcpServers: buildAcpMcpServers(toolDefs, hash),
// Forward only HOME/PATH so the bridged `claude` authenticates from the
// login/keychain session (R17); never inherited process.env or API keys.
bridgeEnv: { HOME: process.env.HOME, PATH: process.env.PATH },
});
}
const configPath = ensureMcpConfig(pi, contextTools);
return streamViaCli(model, context, {
...options,
mcpConfigPath: configPath,

View File

@@ -19,6 +19,9 @@
"url": "https://github.com/Runfusion/Fusion",
"directory": "packages/pi-claude-cli"
},
"dependencies": {
"@agentclientprotocol/sdk": "0.24.0"
},
"peerDependencies": {
"@earendil-works/pi-ai": "*",
"@earendil-works/pi-coding-agent": "*"

View File

@@ -0,0 +1,105 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import { EventEmitter } from "node:events";
import { PassThrough } from "node:stream";
// Synthetic ACP session/update sequence the mocked prompt() will replay.
let scriptedUpdates: Array<Record<string, unknown>> = [];
vi.mock("node:child_process", () => ({
spawn: vi.fn(() => {
const proc = new EventEmitter() as EventEmitter & Record<string, unknown>;
proc.stdin = new PassThrough();
proc.stdout = new PassThrough();
proc.stderr = new PassThrough();
proc.kill = vi.fn();
proc.pid = 4242;
return proc;
}),
}));
// Mock the ACP SDK: ClientSideConnection.prompt() replays scriptedUpdates onto
// the client handler, then resolves — so we exercise the real translation logic.
vi.mock("@agentclientprotocol/sdk", () => ({
PROTOCOL_VERSION: 1,
ndJsonStream: vi.fn(() => ({})),
ClientSideConnection: vi.fn(function (this: Record<string, unknown>, factory: () => { sessionUpdate: (p: unknown) => Promise<void> }) {
const handler = factory();
this.initialize = vi.fn(async () => ({ protocolVersion: 1 }));
this.newSession = vi.fn(async () => ({ sessionId: "s1" }));
this.prompt = vi.fn(async () => {
for (const u of scriptedUpdates) await handler.sessionUpdate({ update: u });
return { stopReason: "end_turn" };
});
}),
}));
const { MockStream } = vi.hoisted(() => {
const MockStream: unknown = vi.fn(function (this: Record<string, unknown>) {
const events: Array<Record<string, unknown>> = [];
this.push = vi.fn((e: Record<string, unknown>) => events.push(e));
this.end = vi.fn();
this._events = events;
});
return { MockStream };
});
vi.mock("@earendil-works/pi-ai", () => ({
AssistantMessageEventStream: MockStream,
calculateCost: vi.fn(),
}));
import { streamViaAcp } from "../acp-driver.js";
const MODEL = { id: "claude-sonnet-4-5", name: "Claude Sonnet 4.5" } as never;
const CTX = { messages: [{ role: "user", content: "hi" }] } as never;
const OPTS = { bridgePath: "/fake/claude-code-cli-acp", cwd: "/tmp", mcpServers: [], bridgeEnv: { HOME: "/h", PATH: "/b" } };
function eventsOf(stream: { _events: Array<Record<string, unknown>> }) {
return stream._events;
}
const flush = () => new Promise((r) => setTimeout(r, 30));
describe("streamViaAcp — ACP→pi translation (U11)", () => {
beforeEach(() => { scriptedUpdates = []; });
it("translates agent_message_chunk text into pi text events + done(stop)", async () => {
scriptedUpdates = [
{ sessionUpdate: "agent_message_chunk", content: { type: "text", text: "Hello " } },
{ sessionUpdate: "agent_message_chunk", content: { type: "text", text: "world" } },
];
const stream = streamViaAcp(MODEL, CTX, OPTS) as unknown as { _events: Array<Record<string, unknown>> };
await flush();
const types = eventsOf(stream).map((e) => e.type);
expect(types).toContain("start");
expect(types).toContain("text_start");
expect(types.filter((t) => t === "text_delta").length).toBe(2);
const done = eventsOf(stream).find((e) => e.type === "done");
expect(done).toBeDefined();
expect(done!.reason).toBe("stop");
});
it("breaks early on a tool_call: emits toolcall_start + done(toolUse), no execution", async () => {
scriptedUpdates = [
{ sessionUpdate: "agent_message_chunk", content: { type: "text", text: "let me check" } },
{ sessionUpdate: "tool_call", toolCallId: "t1", _meta: { claudeCode: { toolName: "mcp__custom-tools__fn_task_list" } }, rawInput: {} },
// anything after the tool call must be ignored (break-early)
{ sessionUpdate: "agent_message_chunk", content: { type: "text", text: "SHOULD NOT APPEAR" } },
];
const stream = streamViaAcp(MODEL, CTX, OPTS) as unknown as { _events: Array<Record<string, unknown>> };
await flush();
const types = eventsOf(stream).map((e) => e.type);
expect(types).toContain("toolcall_start");
const done = eventsOf(stream).find((e) => e.type === "done");
expect(done!.reason).toBe("toolUse");
// break-early: the post-tool text delta must not have been translated
const deltas = eventsOf(stream).filter((e) => e.type === "text_delta").map((e) => e.delta);
expect(deltas.join("")).not.toContain("SHOULD NOT APPEAR");
});
it("ends with done even when the turn produces no content", async () => {
scriptedUpdates = [];
const stream = streamViaAcp(MODEL, CTX, OPTS) as unknown as { _events: Array<Record<string, unknown>> };
await flush();
expect(eventsOf(stream).some((e) => e.type === "done")).toBe(true);
});
});

View File

@@ -0,0 +1,200 @@
/**
* ACP transport for the pi-claude-cli provider (U11 — Route A).
*
* `streamViaAcp` is the drop-in alternative to `streamViaCli` that drives Claude
* through the `claude-code-cli-acp` bridge over the Agent Client Protocol instead
* of `claude -p`. It returns the SAME `AssistantMessageEventStream` shape, so the
* provider's `streamSimple` can dispatch to either transport behind a kill-switch.
*
* Design (see plan U11):
* - Full-history prompt EVERY turn (`buildPrompt`) — the ACP path has no Claude
* `--resume`, so we never send the latest-turn-only `buildResumePrompt` (R13).
* - MCP tool SCHEMAS are forwarded on `session/new` so Claude knows the Fusion
* tools and emits correct `tool_use` calls; we DO NOT let the bridge execute
* them. On the first tool call we break early (cancel the turn) and surface the
* call to pi, which runs the tool itself — mirroring the `-p` break-early
* pattern (the schema-only MCP server never reaches `tools/call`).
* - Translation reuses the tested `createEventBridge` by synthesizing Claude
* stream events from ACP `session/update`s, so pi event sequencing, tool-name
* mapping and arg translation are shared with the `-p` path.
*
* Auth: the bridge spawns the real `claude`, which authenticates from the host
* login/keychain session (R17). The bridge binary path is injected by the caller
* (engine seam, KTD10) — this module never reaches into the ACP plugin.
*/
import { spawn, type ChildProcess } from "node:child_process";
import { Readable, Writable } from "node:stream";
import {
ClientSideConnection,
ndJsonStream,
PROTOCOL_VERSION,
} from "@agentclientprotocol/sdk";
import { AssistantMessageEventStream } from "@earendil-works/pi-ai";
import type { Api, Model, SimpleStreamOptions } from "@earendil-works/pi-ai";
import { buildPrompt, buildSystemPrompt, type PiContext } from "./prompt-builder.js";
import { createEventBridge } from "./event-bridge.js";
import type { ClaudeApiEvent } from "./types.js";
/** A stdio MCP server forwarded on `session/new` (schema-only — never executed here). */
export interface AcpMcpServerSpec {
name: string;
command: string;
args: string[];
env: { name: string; value: string }[];
}
/** Options for the ACP transport: pi's stream options plus ACP wiring. */
export type StreamViaAcpOptions = SimpleStreamOptions & {
cwd?: string;
/** Absolute path to the `claude-code-cli-acp` bridge binary (injected by the engine seam). */
bridgePath: string;
/** MCP servers (tool schemas) forwarded so Claude emits correct tool calls. */
mcpServers?: AcpMcpServerSpec[];
/** Env allow-list forwarded to the bridge (HOME/PATH …); never inherited process.env. */
bridgeEnv?: NodeJS.ProcessEnv;
};
const INITIALIZE_TIMEOUT_MS = 30_000;
function flattenPromptText(prompt: string | { type: string; text?: string }[]): string {
if (typeof prompt === "string") return prompt;
return prompt
.map((b) => (b.type === "text" && typeof b.text === "string" ? b.text : ""))
.join("");
}
/**
* Stream a Claude response via the ACP bridge as an `AssistantMessageEventStream`.
* Mirrors `streamViaCli`'s contract (start → deltas → done; break-early on tools).
*/
export function streamViaAcp(
model: Model<Api>,
context: PiContext,
options: StreamViaAcpOptions,
): AssistantMessageEventStream {
// @ts-expect-error — pi-ai exports AssistantMessageEventStream as a type; the
// constructor exists at runtime (same workaround as streamViaCli).
const stream = new AssistantMessageEventStream();
const bridge = createEventBridge(stream, model);
(async () => {
let child: ChildProcess | undefined;
let ended = false;
// Claude content-block index synthesis: one open text/thinking block at a time.
let blockIndex = -1;
let openKind: "text" | "thinking" | null = null;
let sawToolCall = false;
const finish = (reason: "stop" | "tool_use") => {
if (ended) return;
ended = true;
if (openKind !== null) bridge.handleEvent({ type: "content_block_stop", index: blockIndex } as ClaudeApiEvent);
bridge.handleEvent({ type: "message_delta", delta: { stop_reason: reason === "tool_use" ? "tool_use" : "end_turn" } } as ClaudeApiEvent);
stream.push({ type: "done", reason: reason === "tool_use" ? "toolUse" : "stop", message: bridge.getOutput() });
stream.end();
try { child?.kill("SIGKILL"); } catch { /* registry SIGKILL is authoritative */ }
};
const failWith = (msg: string) => {
if (ended) return;
ended = true;
const output = bridge.getOutput();
stream.push({
type: "done",
reason: "stop",
message: {
...output,
content: output.content?.length ? output.content : [{ type: "text" as const, text: `Error: ${msg}` }],
stopReason: "stop" as const,
},
});
stream.end();
try { child?.kill("SIGKILL"); } catch { /* noop */ }
};
// Ensure a text/thinking block is open, closing any block of the other kind first.
const openBlock = (kind: "text" | "thinking") => {
if (openKind === kind) return;
if (openKind !== null) bridge.handleEvent({ type: "content_block_stop", index: blockIndex } as ClaudeApiEvent);
blockIndex += 1;
openKind = kind;
bridge.handleEvent({
type: "content_block_start",
index: blockIndex,
content_block: { type: kind },
} as ClaudeApiEvent);
};
const clientHandler = {
async sessionUpdate(params: { update?: Record<string, unknown> } & Record<string, unknown>) {
if (ended) return;
const u = (params.update ?? params) as Record<string, unknown>;
const kind = u.sessionUpdate as string;
const content = u.content as { type?: string; text?: string } | undefined;
if (kind === "agent_message_chunk" && content?.type === "text" && content.text) {
openBlock("text");
bridge.handleEvent({ type: "content_block_delta", index: blockIndex, delta: { type: "text_delta", text: content.text } } as ClaudeApiEvent);
} else if (kind === "agent_thought_chunk" && content?.text) {
openBlock("thinking");
bridge.handleEvent({ type: "content_block_delta", index: blockIndex, delta: { type: "thinking_delta", thinking: content.text } } as ClaudeApiEvent);
} else if (kind === "tool_call") {
// Break-early: surface the tool call to pi, do NOT let the bridge execute it.
const meta = (u._meta as { claudeCode?: { toolName?: string } } | undefined)?.claudeCode;
const claudeName = (meta?.toolName as string) ?? (u.title as string) ?? "";
const id = (u.toolCallId as string) ?? `acp_${blockIndex + 1}`;
if (openKind !== null) { bridge.handleEvent({ type: "content_block_stop", index: blockIndex } as ClaudeApiEvent); openKind = null; }
blockIndex += 1;
bridge.handleEvent({ type: "content_block_start", index: blockIndex, content_block: { type: "tool_use", name: claudeName, id } } as ClaudeApiEvent);
const rawInput = u.rawInput ?? u.input ?? {};
bridge.handleEvent({ type: "content_block_delta", index: blockIndex, delta: { type: "input_json_delta", partial_json: JSON.stringify(rawInput) } } as ClaudeApiEvent);
bridge.handleEvent({ type: "content_block_stop", index: blockIndex } as ClaudeApiEvent);
sawToolCall = true;
finish("tool_use");
}
},
async requestPermission() {
// We break early before execution, so this should not fire. Reject to be safe.
return { outcome: { outcome: "cancelled" as const } };
},
};
try {
const env = options.bridgeEnv ?? { HOME: process.env.HOME, PATH: process.env.PATH };
child = spawn(options.bridgePath, [], { stdio: ["pipe", "pipe", "pipe"], cwd: options.cwd ?? process.cwd(), env });
child.on("error", (e) => failWith(`ACP bridge spawn failed: ${e.message}`));
if (options.signal) options.signal.addEventListener("abort", () => { try { child?.kill("SIGKILL"); } catch { /* noop */ } failWith("aborted"); }, { once: true });
const acpStream = ndJsonStream(
Writable.toWeb(child.stdin!) as unknown as WritableStream<Uint8Array>,
Readable.toWeb(child.stdout!) as unknown as ReadableStream<Uint8Array>,
);
const conn = new ClientSideConnection(() => clientHandler, acpStream);
const init = await Promise.race([
conn.initialize({ protocolVersion: PROTOCOL_VERSION, clientCapabilities: { fs: { readTextFile: false, writeTextFile: false } } }),
new Promise<never>((_, rej) => setTimeout(() => rej(new Error("ACP initialize timeout")), INITIALIZE_TIMEOUT_MS)),
]);
if (init.protocolVersion !== PROTOCOL_VERSION) { failWith(`incompatible ACP protocol ${init.protocolVersion}`); return; }
const opened = await conn.newSession({ cwd: options.cwd ?? process.cwd(), mcpServers: options.mcpServers ?? [] });
const cwd = options.cwd ?? process.cwd();
const promptText = flattenPromptText(buildPrompt(context));
const systemPrompt = buildSystemPrompt(context, cwd);
const blocks = [
...(systemPrompt ? [{ type: "text" as const, text: `${systemPrompt}\n\n` }] : []),
{ type: "text" as const, text: promptText },
];
await conn.prompt({ sessionId: opened.sessionId, prompt: blocks });
// Resolved without a tool call → normal end of turn.
if (!sawToolCall) finish("stop");
} catch (err) {
failWith(err instanceof Error ? err.message : String(err));
}
})();
return stream;
}

View File

@@ -142,3 +142,32 @@ export function writeMcpConfig(
return configFilePath;
}
/** A stdio MCP server spec for ACP `session/new.mcpServers` (U11 — Route A). */
export interface AcpMcpServerSpec {
name: string;
command: string;
args: string[];
env: { name: string; value: string }[];
}
/**
* Build the ACP `mcpServers` spec for the same schema-only `custom-tools` server
* `writeMcpConfig` produces for `--mcp-config` — but as the inline ACP shape
* (`session/new.mcpServers`) instead of a config-file path (U11). Writes the
* tool-schema file and points the server at the shared `mcp-schema-server.cjs`.
* Returns `[]` when there are no custom tools (Route B read-only posture).
*/
export function buildAcpMcpServers(
toolDefs: McpToolDef[],
cacheKey?: string,
): AcpMcpServerSpec[] {
if (toolDefs.length === 0) return [];
const suffix = cacheKey ? `${process.pid}-${cacheKey}` : `${process.pid}`;
const schemaFilePath = join(tmpdir(), `pi-claude-mcp-schemas-${suffix}.json`);
writeFileSync(schemaFilePath, JSON.stringify(toolDefs));
const serverPath = join(dirname(fileURLToPath(import.meta.url)), "mcp-schema-server.cjs");
return [
{ name: "custom-tools", command: "node", args: [serverPath, schemaFilePath], env: [] },
];
}

139
pnpm-lock.yaml generated
View File

@@ -47,10 +47,10 @@ importers:
dependencies:
'@earendil-works/pi-ai':
specifier: ^0.79.1
version: 0.79.1(@modelcontextprotocol/sdk@1.28.0(zod@4.3.6))(ws@8.20.0)(zod@4.3.6)
version: 0.79.1(@modelcontextprotocol/sdk@1.28.0(zod@3.25.76))(ws@8.20.0)(zod@3.25.76)
'@earendil-works/pi-coding-agent':
specifier: ^0.79.1
version: 0.79.1(@modelcontextprotocol/sdk@1.28.0(zod@4.3.6))(ws@8.20.0)(zod@4.3.6)
version: 0.79.1(@modelcontextprotocol/sdk@1.28.0(zod@3.25.76))(ws@8.20.0)(zod@3.25.76)
dockerode:
specifier: ^4.0.12
version: 4.0.12
@@ -585,12 +585,15 @@ importers:
packages/pi-claude-cli:
dependencies:
'@agentclientprotocol/sdk':
specifier: 0.24.0
version: 0.24.0(zod@4.3.6)
'@earendil-works/pi-ai':
specifier: '*'
version: 0.77.0(@modelcontextprotocol/sdk@1.28.0(zod@3.25.76))(ws@8.20.0)(zod@3.25.76)
version: 0.77.0(@modelcontextprotocol/sdk@1.28.0(zod@4.3.6))(ws@8.20.0)(zod@4.3.6)
'@earendil-works/pi-coding-agent':
specifier: '*'
version: 0.77.0(@modelcontextprotocol/sdk@1.28.0(zod@3.25.76))(ws@8.20.0)(zod@3.25.76)
version: 0.77.0(@modelcontextprotocol/sdk@1.28.0(zod@4.3.6))(ws@8.20.0)(zod@4.3.6)
devDependencies:
'@types/node':
specifier: ^25.5.2
@@ -7911,20 +7914,6 @@ snapshots:
- ws
- zod
'@earendil-works/pi-agent-core@0.77.0(@modelcontextprotocol/sdk@1.28.0(zod@3.25.76))(ws@8.20.0)(zod@3.25.76)':
dependencies:
'@earendil-works/pi-ai': 0.77.0(@modelcontextprotocol/sdk@1.28.0(zod@3.25.76))(ws@8.20.0)(zod@3.25.76)
ignore: 7.0.5
typebox: 1.1.38
yaml: 2.9.0
transitivePeerDependencies:
- '@modelcontextprotocol/sdk'
- bufferutil
- supports-color
- utf-8-validate
- ws
- zod
'@earendil-works/pi-agent-core@0.77.0(@modelcontextprotocol/sdk@1.28.0(zod@4.3.6))(ws@8.20.0)(zod@4.3.6)':
dependencies:
'@earendil-works/pi-ai': 0.77.0(@modelcontextprotocol/sdk@1.28.0(zod@4.3.6))(ws@8.20.0)(zod@4.3.6)
@@ -7953,6 +7942,20 @@ snapshots:
- ws
- zod
'@earendil-works/pi-agent-core@0.79.1(@modelcontextprotocol/sdk@1.28.0(zod@3.25.76))(ws@8.20.0)(zod@3.25.76)':
dependencies:
'@earendil-works/pi-ai': 0.79.1(@modelcontextprotocol/sdk@1.28.0(zod@3.25.76))(ws@8.20.0)(zod@3.25.76)
ignore: 7.0.5
typebox: 1.1.38
yaml: 2.9.0
transitivePeerDependencies:
- '@modelcontextprotocol/sdk'
- bufferutil
- supports-color
- utf-8-validate
- ws
- zod
'@earendil-works/pi-agent-core@0.79.1(@modelcontextprotocol/sdk@1.28.0(zod@4.3.6))(ws@8.20.0)(zod@4.3.6)':
dependencies:
'@earendil-works/pi-ai': 0.79.1(@modelcontextprotocol/sdk@1.28.0(zod@4.3.6))(ws@8.20.0)(zod@4.3.6)
@@ -8001,26 +8004,6 @@ snapshots:
- ws
- zod
'@earendil-works/pi-ai@0.77.0(@modelcontextprotocol/sdk@1.28.0(zod@3.25.76))(ws@8.20.0)(zod@3.25.76)':
dependencies:
'@anthropic-ai/sdk': 0.91.1(zod@3.25.76)
'@aws-sdk/client-bedrock-runtime': 3.1048.0
'@google/genai': 1.52.0(@modelcontextprotocol/sdk@1.28.0(zod@3.25.76))
'@mistralai/mistralai': 2.2.1
'@smithy/node-http-handler': 4.7.3
http-proxy-agent: 7.0.2
https-proxy-agent: 7.0.6
openai: 6.26.0(ws@8.20.0)(zod@3.25.76)
partial-json: 0.1.7
typebox: 1.1.38
transitivePeerDependencies:
- '@modelcontextprotocol/sdk'
- bufferutil
- supports-color
- utf-8-validate
- ws
- zod
'@earendil-works/pi-ai@0.77.0(@modelcontextprotocol/sdk@1.28.0(zod@4.3.6))(ws@8.20.0)(zod@4.3.6)':
dependencies:
'@anthropic-ai/sdk': 0.91.1(zod@4.3.6)
@@ -8061,6 +8044,26 @@ snapshots:
- ws
- zod
'@earendil-works/pi-ai@0.79.1(@modelcontextprotocol/sdk@1.28.0(zod@3.25.76))(ws@8.20.0)(zod@3.25.76)':
dependencies:
'@anthropic-ai/sdk': 0.91.1(zod@3.25.76)
'@aws-sdk/client-bedrock-runtime': 3.1048.0
'@google/genai': 1.52.0(@modelcontextprotocol/sdk@1.28.0(zod@3.25.76))
'@mistralai/mistralai': 2.2.1
'@smithy/node-http-handler': 4.7.3
http-proxy-agent: 7.0.2
https-proxy-agent: 7.0.6
openai: 6.26.0(ws@8.20.0)(zod@3.25.76)
partial-json: 0.1.7
typebox: 1.1.38
transitivePeerDependencies:
- '@modelcontextprotocol/sdk'
- bufferutil
- supports-color
- utf-8-validate
- ws
- zod
'@earendil-works/pi-ai@0.79.1(@modelcontextprotocol/sdk@1.28.0(zod@4.3.6))(ws@8.20.0)(zod@4.3.6)':
dependencies:
'@anthropic-ai/sdk': 0.91.1(zod@4.3.6)
@@ -8130,35 +8133,6 @@ snapshots:
- ws
- zod
'@earendil-works/pi-coding-agent@0.77.0(@modelcontextprotocol/sdk@1.28.0(zod@3.25.76))(ws@8.20.0)(zod@3.25.76)':
dependencies:
'@earendil-works/pi-agent-core': 0.77.0(@modelcontextprotocol/sdk@1.28.0(zod@3.25.76))(ws@8.20.0)(zod@3.25.76)
'@earendil-works/pi-ai': 0.77.0(@modelcontextprotocol/sdk@1.28.0(zod@3.25.76))(ws@8.20.0)(zod@3.25.76)
'@earendil-works/pi-tui': 0.77.0
'@silvia-odwyer/photon-node': 0.3.4
chalk: 5.6.2
cross-spawn: 7.0.6
diff: 8.0.4
glob: 13.0.6
highlight.js: 10.7.3
hosted-git-info: 9.0.3
ignore: 7.0.5
jiti: 2.7.0
minimatch: 10.2.5
proper-lockfile: 4.1.2
typebox: 1.1.38
undici: 8.3.0
yaml: 2.9.0
optionalDependencies:
'@mariozechner/clipboard': 0.3.9
transitivePeerDependencies:
- '@modelcontextprotocol/sdk'
- bufferutil
- supports-color
- utf-8-validate
- ws
- zod
'@earendil-works/pi-coding-agent@0.77.0(@modelcontextprotocol/sdk@1.28.0(zod@4.3.6))(ws@8.20.0)(zod@4.3.6)':
dependencies:
'@earendil-works/pi-agent-core': 0.77.0(@modelcontextprotocol/sdk@1.28.0(zod@4.3.6))(ws@8.20.0)(zod@4.3.6)
@@ -8217,6 +8191,35 @@ snapshots:
- ws
- zod
'@earendil-works/pi-coding-agent@0.79.1(@modelcontextprotocol/sdk@1.28.0(zod@3.25.76))(ws@8.20.0)(zod@3.25.76)':
dependencies:
'@earendil-works/pi-agent-core': 0.79.1(@modelcontextprotocol/sdk@1.28.0(zod@3.25.76))(ws@8.20.0)(zod@3.25.76)
'@earendil-works/pi-ai': 0.79.1(@modelcontextprotocol/sdk@1.28.0(zod@3.25.76))(ws@8.20.0)(zod@3.25.76)
'@earendil-works/pi-tui': 0.79.1
'@silvia-odwyer/photon-node': 0.3.4
chalk: 5.6.2
cross-spawn: 7.0.6
diff: 8.0.4
glob: 13.0.6
highlight.js: 10.7.3
hosted-git-info: 9.0.3
ignore: 7.0.5
jiti: 2.7.0
minimatch: 10.2.5
proper-lockfile: 4.1.2
typebox: 1.1.38
undici: 8.3.0
yaml: 2.9.0
optionalDependencies:
'@mariozechner/clipboard': 0.3.9
transitivePeerDependencies:
- '@modelcontextprotocol/sdk'
- bufferutil
- supports-color
- utf-8-validate
- ws
- zod
'@earendil-works/pi-coding-agent@0.79.1(@modelcontextprotocol/sdk@1.28.0(zod@4.3.6))(ws@8.20.0)(zod@4.3.6)':
dependencies:
'@earendil-works/pi-agent-core': 0.79.1(@modelcontextprotocol/sdk@1.28.0(zod@4.3.6))(ws@8.20.0)(zod@4.3.6)
@@ -9807,7 +9810,7 @@ snapshots:
obug: 2.1.2
std-env: 4.1.0
tinyrainbow: 3.1.0
vitest: 4.1.8(@types/node@25.5.2)(@vitest/coverage-v8@4.1.8)(happy-dom@20.10.1)(jsdom@29.0.1)(vite@6.4.1(@types/node@25.5.2)(jiti@2.7.0)(tsx@4.21.0)(yaml@2.9.0))
vitest: 4.1.8(@types/node@25.5.2)(@vitest/coverage-v8@4.1.8)(happy-dom@20.10.1)(jsdom@29.0.1)(vite@6.4.1(@types/node@25.5.2)(jiti@2.7.0)(tsx@4.21.0)(yaml@2.8.3))
'@vitest/expect@4.1.8':
dependencies: