From 02028c1886c86f1068c6d214f879a2f5cba44bc4 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 17 Jul 2026 14:51:52 +0000 Subject: [PATCH] refactor(pi): tighten native RPC adapter and downstream Pi features MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Upgrade to @earendil-works/pi-coding-agent 0.80.10, add first-class codingAgentDir config, surface plugin slash commands, and adopt agent_settled / thinking max while dropping unused ACP-adjacent bits. Co-authored-by: Raphael Lüthy --- apps/server/package.json | 2 +- apps/server/scripts/pi-mock-rpc.ts | 3 +- apps/server/src/provider/Drivers/PiDriver.ts | 10 ++- .../Layers/PiAdapter.integration.test.ts | 27 ++++++ apps/server/src/provider/Layers/PiAdapter.ts | 45 ++++------ .../src/provider/Layers/PiEnvironment.test.ts | 24 +++++ .../src/provider/Layers/PiEnvironment.ts | 24 +++++ .../src/provider/Layers/PiProvider.test.ts | 34 +++++-- apps/server/src/provider/Layers/PiProvider.ts | 58 +++++++++--- .../src/provider/Layers/PiRpcClient.test.ts | 53 +++++++++++ .../server/src/provider/Layers/PiRpcClient.ts | 60 +++++++++++-- .../src/provider/assets/pi/t3-approvals.ts | 2 +- .../src/textGeneration/PiTextGeneration.ts | 22 +++-- .../settings/ProviderSettingsForm.test.ts | 5 +- docs/providers/pi.md | 38 +++++--- packages/contracts/src/settings.ts | 15 +++- pnpm-lock.yaml | 88 +++++++++---------- pnpm-workspace.yaml | 4 + 18 files changed, 388 insertions(+), 126 deletions(-) create mode 100644 apps/server/src/provider/Layers/PiEnvironment.test.ts create mode 100644 apps/server/src/provider/Layers/PiEnvironment.ts diff --git a/apps/server/package.json b/apps/server/package.json index 3ef1213169b..79fdf42ea43 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -34,7 +34,7 @@ "node-pty": "^1.1.0" }, "devDependencies": { - "@earendil-works/pi-coding-agent": "^0.80.2", + "@earendil-works/pi-coding-agent": "^0.80.10", "@effect/vitest": "catalog:", "@t3tools/contracts": "workspace:*", "@t3tools/shared": "workspace:*", diff --git a/apps/server/scripts/pi-mock-rpc.ts b/apps/server/scripts/pi-mock-rpc.ts index 28e5d00e487..27cf569e237 100644 --- a/apps/server/scripts/pi-mock-rpc.ts +++ b/apps/server/scripts/pi-mock-rpc.ts @@ -41,7 +41,8 @@ rl.on("line", (line: string) => { lastAssistantText = replyText; write({ type: "message_end" }); write({ type: "turn_end" }); - write({ type: "agent_end" }); + write({ type: "agent_end", willRetry: false }); + write({ type: "agent_settled" }); return; } case "get_last_assistant_text": { diff --git a/apps/server/src/provider/Drivers/PiDriver.ts b/apps/server/src/provider/Drivers/PiDriver.ts index 677eb620e73..c24bc5cac16 100644 --- a/apps/server/src/provider/Drivers/PiDriver.ts +++ b/apps/server/src/provider/Drivers/PiDriver.ts @@ -13,8 +13,8 @@ import { ServerSettingsService } from "../../serverSettings.ts"; import { makePiTextGeneration } from "../../textGeneration/PiTextGeneration.ts"; import { ProviderDriverError } from "../Errors.ts"; import { makePiAdapter } from "../Layers/PiAdapter.ts"; +import { resolvePiProcessEnv } from "../Layers/PiEnvironment.ts"; import { buildInitialPiProviderSnapshot, checkPiProviderStatus } from "../Layers/PiProvider.ts"; -import { ProviderEventLoggers } from "../Layers/ProviderEventLoggers.ts"; import { makeManagedServerProvider } from "../makeManagedServerProvider.ts"; import { defaultProviderContinuationIdentity, @@ -52,7 +52,6 @@ export type PiDriverEnv = | FileSystem.FileSystem | HttpClient.HttpClient | Path.Path - | ProviderEventLoggers | ServerConfig | ServerSettingsService; @@ -86,7 +85,11 @@ export const PiDriver: ProviderDriver = { const httpClient = yield* HttpClient.HttpClient; const serverSettings = yield* ServerSettingsService; const serverConfig = yield* ServerConfig; - const processEnv = mergeProviderInstanceEnvironment(environment); + const effectiveConfig = { ...config, enabled } satisfies PiSettings; + const processEnv = resolvePiProcessEnv( + effectiveConfig, + mergeProviderInstanceEnvironment(environment), + ); const continuationIdentity = defaultProviderContinuationIdentity({ driverKind: DRIVER_KIND, instanceId, @@ -97,7 +100,6 @@ export const PiDriver: ProviderDriver = { accentColor, continuationGroupKey: continuationIdentity.continuationKey, }); - const effectiveConfig = { ...config, enabled } satisfies PiSettings; const maintenanceCapabilities = yield* resolveProviderMaintenanceCapabilitiesEffect(UPDATE, { binaryPath: effectiveConfig.binaryPath, env: processEnv, diff --git a/apps/server/src/provider/Layers/PiAdapter.integration.test.ts b/apps/server/src/provider/Layers/PiAdapter.integration.test.ts index c334d05b1ae..b3af0ff6155 100644 --- a/apps/server/src/provider/Layers/PiAdapter.integration.test.ts +++ b/apps/server/src/provider/Layers/PiAdapter.integration.test.ts @@ -519,4 +519,31 @@ it.layer(HarnessLayer)("PiAdapter integration", (it) => { expect(fake.commands.some((command) => command.type === "steer")).toBe(true); }), ); + + it.effect("completes a turn on agent_settled without double-completing after agent_end", () => + Effect.gen(function* () { + const { adapter, fake } = yield* makePiAdapterForTest(enabledSettings()); + const threadId = ThreadId.make("pi-int-settled"); + const { fiber, store } = yield* collectEvents( + adapter, + threadId, + (event) => event.type === "turn.completed", + ); + yield* adapter.startSession({ + threadId, + provider: PI, + cwd: process.cwd(), + runtimeMode: "full-access", + }); + yield* adapter.sendTurn({ threadId, input: "hello", attachments: [] }); + yield* fake.pushEvent({ type: "turn_start" } as AgentSessionEvent); + yield* fake.pushEvent({ type: "agent_end", willRetry: false } as AgentSessionEvent); + yield* fake.pushEvent({ type: "agent_settled" } as AgentSessionEvent); + yield* Fiber.join(fiber); + const completions = (yield* Ref.get(store)).filter( + (event) => event.type === "turn.completed", + ); + expect(completions.length).toBe(1); + }), + ); }); diff --git a/apps/server/src/provider/Layers/PiAdapter.ts b/apps/server/src/provider/Layers/PiAdapter.ts index aa244070186..34203df322b 100644 --- a/apps/server/src/provider/Layers/PiAdapter.ts +++ b/apps/server/src/provider/Layers/PiAdapter.ts @@ -54,6 +54,7 @@ import { extractStateModelSlug, makePiRpcTransport, type MakePiRpcTransportOptions, + PI_APPROVAL_SENTINEL_COMMAND, piForkSucceeded, piImageContentFromBytes, type PiImageContent, @@ -68,6 +69,7 @@ import { type RpcExtensionUIRequest, type RpcExtensionUIResponse, } from "./PiRpcClient.ts"; +import { resolvePiProcessEnv } from "./PiEnvironment.ts"; const PROVIDER = ProviderDriverKind.make("pi"); @@ -78,9 +80,6 @@ const PI_MESSAGES_TIMEOUT_MS = 5_000; const PI_FORK_TIMEOUT_MS = 15_000; const PI_MODEL_OPTIONS_TIMEOUT_MS = 5_000; -// keep in sync with SENTINEL_COMMAND in t3-approvals.ts -const PI_APPROVAL_SENTINEL_COMMAND = "t3-approval-gate"; - // like Claude/Cursor: full-access runs ungated; approval-required and // auto-accept-edits gate via the bundled extension (Pi has no native per-tool approval) function approvalGateForRuntimeMode( @@ -307,7 +306,8 @@ export const makePiAdapter = Effect.fn("makePiAdapter")(function* ( const crypto = yield* Crypto.Crypto; const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; const fileSystem = yield* FileSystem.FileSystem; - const baseEnvironment = options?.environment ?? process.env; + // Caller may already resolve PI_CODING_AGENT_DIR; re-applying is idempotent. + const baseEnvironment = resolvePiProcessEnv(piSettings, options?.environment ?? process.env); let approvalExtensionPath: string | undefined; for (const candidate of APPROVAL_EXTENSION_CANDIDATES) { @@ -529,8 +529,9 @@ export const makePiAdapter = Effect.fn("makePiAdapter")(function* ( } case "agent_end": { - // willRetry means pi will auto-retry (another agent_start/end cycle) — - // finalize only on the terminal end, since a retry isn't a user interrupt + // willRetry means pi will auto-retry (another agent_start/end cycle). + // Prefer agent_settled (Pi >= 0.80.5) for true idle; keep this as a + // fallback for older binaries. completeTurn is idempotent either way. if (event.willRetry) return; if (context.turnState) { yield* completeTurn(context, "completed"); @@ -538,6 +539,14 @@ export const makePiAdapter = Effect.fn("makePiAdapter")(function* ( return; } + case "agent_settled": { + // Session-level idle: no retry, compaction retry, or queued follow-up remains. + if (context.turnState) { + yield* completeTurn(context, "completed"); + } + return; + } + case "compaction_start": { yield* offerRuntimeEvent({ ...base, @@ -1110,29 +1119,7 @@ export const makePiAdapter = Effect.fn("makePiAdapter")(function* ( yield* applyThinkingLevel(context, input.modelSelection); } - if (!context.turnState) { - const turnId = TurnId.make(yield* nextUuid); - const startedAt = yield* nowIso; - context.turnState = { turnId, startedAt, items: [] }; - context.session = { - ...context.session, - status: "running", - activeTurnId: turnId, - updatedAt: startedAt, - }; - const stamp = yield* makeEventStamp(); - yield* offerRuntimeEvent({ - ...stamp, - type: "turn.started", - provider: PROVIDER, - providerInstanceId: boundInstanceId, - threadId: context.session.threadId, - turnId, - payload: context.currentModel ? { model: context.currentModel } : {}, - }); - } - - const turnId = context.turnState.turnId; + const turnId = context.turnState?.turnId ?? (yield* openTurn(context)); yield* context.transport .writeCommand(buildPiTurnCommand({ isMidTurn, message: promptText, images })) diff --git a/apps/server/src/provider/Layers/PiEnvironment.test.ts b/apps/server/src/provider/Layers/PiEnvironment.test.ts new file mode 100644 index 00000000000..10b071e5ffb --- /dev/null +++ b/apps/server/src/provider/Layers/PiEnvironment.test.ts @@ -0,0 +1,24 @@ +import { describe, expect, it } from "@effect/vitest"; + +import { expandHomePath } from "../../pathExpansion.ts"; +import { resolvePiProcessEnv } from "./PiEnvironment.ts"; + +describe("resolvePiProcessEnv", () => { + it("returns the base env unchanged when codingAgentDir is empty", () => { + const base = { PATH: "/usr/bin", PI_CODING_AGENT_DIR: "/existing" }; + expect(resolvePiProcessEnv({ codingAgentDir: "" }, base)).toBe(base); + expect(resolvePiProcessEnv({ codingAgentDir: " " }, base)).toBe(base); + }); + + it("sets PI_CODING_AGENT_DIR from codingAgentDir, expanding ~", () => { + const resolved = resolvePiProcessEnv( + { codingAgentDir: "~/.pi/work" }, + { PATH: "/usr/bin", KEEP: "1" }, + ); + expect(resolved).toEqual({ + PATH: "/usr/bin", + KEEP: "1", + PI_CODING_AGENT_DIR: expandHomePath("~/.pi/work"), + }); + }); +}); diff --git a/apps/server/src/provider/Layers/PiEnvironment.ts b/apps/server/src/provider/Layers/PiEnvironment.ts new file mode 100644 index 00000000000..25089ffcb4b --- /dev/null +++ b/apps/server/src/provider/Layers/PiEnvironment.ts @@ -0,0 +1,24 @@ +import type { PiSettings } from "@t3tools/contracts"; + +import { expandHomePath } from "../../pathExpansion.ts"; + +/** + * Resolve the process environment for a Pi CLI spawn. + * + * When `codingAgentDir` is set, it becomes `PI_CODING_AGENT_DIR` so Pi loads + * auth/models/settings/extensions/packages from that directory — the same + * config surface the `pi` CLI uses. + */ +export function resolvePiProcessEnv( + settings: Pick, + baseEnv: NodeJS.ProcessEnv = process.env, +): NodeJS.ProcessEnv { + const codingAgentDir = settings.codingAgentDir.trim(); + if (codingAgentDir.length === 0) { + return baseEnv; + } + return { + ...baseEnv, + PI_CODING_AGENT_DIR: expandHomePath(codingAgentDir), + }; +} diff --git a/apps/server/src/provider/Layers/PiProvider.test.ts b/apps/server/src/provider/Layers/PiProvider.test.ts index 9c3a786f1ac..a9e5217f5c0 100644 --- a/apps/server/src/provider/Layers/PiProvider.test.ts +++ b/apps/server/src/provider/Layers/PiProvider.test.ts @@ -10,26 +10,39 @@ import { buildInitialPiProviderSnapshot, checkPiProviderStatus } from "./PiProvi const decodePiSettings = Schema.decodeSync(PiSettings); -// fake `pi`: `--version` exits 0; for RPC, wait for a stdin request then reply with models. -// Responding only after a request avoids racing the transport's pending-request registration. -const healthyPiScript = (modelsJson: string) => +// fake `pi`: `--version` exits 0; for RPC, answer get_available_models + get_commands +// then exit. Responding only after a request avoids racing pending-request registration. +const healthyPiScript = (modelsJson: string, commandsJson = "[]") => [ "#!/bin/sh", 'if [ "$1" = "--version" ]; then', - ' printf "pi 0.80.2\\n"', + ' printf "pi 0.80.10\\n"', " exit 0", "fi", + "answered_models=0", + "answered_commands=0", "while IFS= read -r line; do", ' id=$(printf "%s" "$line" | sed -n \'s/.*"id":"\\([^"]*\\)".*/\\1/p\')', - ' [ -z "$id" ] && id="pi-model-discovery"', - ` printf '{"type":"response","command":"get_available_models","id":"%s","success":true,"data":{"models":${modelsJson}}}\\n' "$id"`, - " exit 0", + ' [ -z "$id" ] && id="pi-discovery"', + ' if printf "%s" "$line" | grep -q \'"type":"get_available_models"\'; then', + ` printf '{"type":"response","command":"get_available_models","id":"%s","success":true,"data":{"models":${modelsJson}}}\\n' "$id"`, + " answered_models=1", + ' elif printf "%s" "$line" | grep -q \'"type":"get_commands"\'; then', + ` printf '{"type":"response","command":"get_commands","id":"%s","success":true,"data":{"commands":${commandsJson}}}\\n' "$id"`, + " answered_commands=1", + " fi", + ' if [ "$answered_models" = "1" ] && [ "$answered_commands" = "1" ]; then', + " exit 0", + " fi", "done", "", ].join("\n"); const HEALTHY_PI_SCRIPT_NO_MODELS = healthyPiScript("[]"); -const HEALTHY_PI_SCRIPT_WITH_MODELS = healthyPiScript('[{"provider":"openai","id":"gpt-4o"}]'); +const HEALTHY_PI_SCRIPT_WITH_MODELS = healthyPiScript( + '[{"provider":"openai","id":"gpt-4o"}]', + '[{"name":"session-name","description":"Rename session","source":"extension"},{"name":"t3-approval-gate","source":"extension"},{"name":"skill:demo","description":"Demo skill","source":"skill"}]', +); describe("buildInitialPiProviderSnapshot", () => { it.effect("returns a disabled snapshot when settings.enabled is false", () => @@ -139,6 +152,11 @@ it.layer(NodeServices.layer)("checkPiProviderStatus", (it) => { expect(snapshot.status).toBe("ready"); expect(snapshot.auth.status).toBe("authenticated"); expect(snapshot.models.map((model) => model.slug)).toContain("openai/gpt-4o"); + expect(snapshot.slashCommands.map((command) => command.name)).toEqual([ + "session-name", + "skill:demo", + ]); + expect(snapshot.showInteractionModeToggle).toBe(false); }), ); diff --git a/apps/server/src/provider/Layers/PiProvider.ts b/apps/server/src/provider/Layers/PiProvider.ts index 4d1283f7436..c3f12aa3fc6 100644 --- a/apps/server/src/provider/Layers/PiProvider.ts +++ b/apps/server/src/provider/Layers/PiProvider.ts @@ -2,6 +2,7 @@ import { type ModelCapabilities, type PiSettings, type ServerProviderModel, + type ServerProviderSlashCommand, ProviderDriverKind, } from "@t3tools/contracts"; import { createModelCapabilities } from "@t3tools/shared/model"; @@ -21,8 +22,10 @@ import { providerModelsFromSettings, spawnAndCollect, } from "../providerSnapshot.ts"; +import { resolvePiProcessEnv } from "./PiEnvironment.ts"; import { extractAvailableModels, + extractSlashCommands, makePiRpcTransport, piModelInfoToServerModel, } from "./PiRpcClient.ts"; @@ -32,7 +35,8 @@ const PROVIDER = ProviderDriverKind.make("pi"); const PI_PRESENTATION = { displayName: "Pi", badgeLabel: "Early Access", - showInteractionModeToggle: true, + // Pi has no T3 interaction-mode mapping (plan/default); hide the unused toggle. + showInteractionModeToggle: false, } as const; const EMPTY_CAPABILITIES: ModelCapabilities = createModelCapabilities({ optionDescriptors: [] }); @@ -58,29 +62,54 @@ const runPiVersion = (piSettings: PiSettings, environment: NodeJS.ProcessEnv) => }); }); -/** Discover models via a short-lived `pi --mode rpc` session; `[]` on any failure. */ -export const discoverPiModelsViaRpc = Effect.fn("discoverPiModelsViaRpc")( +export interface PiCatalogDiscovery { + readonly models: ReadonlyArray; + readonly slashCommands: ReadonlyArray; +} + +/** Discover models + plugin/extension slash commands via a short-lived `pi --mode rpc` session. */ +export const discoverPiCatalogViaRpc = Effect.fn("discoverPiCatalogViaRpc")( function* (piSettings: PiSettings, cwd: string, environment: NodeJS.ProcessEnv) { const transport = yield* makePiRpcTransport({ binaryPath: piSettings.binaryPath || "pi", + // Keep extensions enabled so installed plugins/packages contribute commands. args: ["--mode", "rpc", "--no-session"], cwd, - env: environment, + env: resolvePiProcessEnv(piSettings, environment), onExit: Effect.void, }); - const response = yield* transport.request( + const modelsResponse = yield* transport.request( { type: "get_available_models" }, "pi-model-discovery", PI_MODEL_DISCOVERY_TIMEOUT_MS, ); - return extractAvailableModels(response).map(piModelInfoToServerModel); + const commandsResponse = yield* transport.request( + { type: "get_commands" }, + "pi-command-discovery", + PI_MODEL_DISCOVERY_TIMEOUT_MS, + ); + return { + models: extractAvailableModels(modelsResponse).map(piModelInfoToServerModel), + slashCommands: extractSlashCommands(commandsResponse), + } satisfies PiCatalogDiscovery; }, Effect.scoped, Effect.timeoutOption(PI_MODEL_DISCOVERY_TIMEOUT_MS), - Effect.map(Option.getOrElse(() => [] as ReadonlyArray)), + Effect.map( + Option.getOrElse( + () => + ({ + models: [], + slashCommands: [], + }) satisfies PiCatalogDiscovery, + ), + ), Effect.catchCause((cause) => - Effect.logWarning("Pi model discovery failed", { cause }).pipe( - Effect.as([] as ReadonlyArray), + Effect.logWarning("Pi catalog discovery failed", { cause }).pipe( + Effect.as({ + models: [], + slashCommands: [], + } satisfies PiCatalogDiscovery), ), ), ); @@ -135,6 +164,7 @@ export const checkPiProviderStatus = Effect.fn("checkPiProviderStatus")(function ) { const checkedAt = yield* nowIso; const fallbackModels = modelsFromSettings(piSettings, []); + const processEnv = resolvePiProcessEnv(piSettings, environment); if (!piSettings.enabled) { return buildServerProvider({ @@ -152,7 +182,7 @@ export const checkPiProviderStatus = Effect.fn("checkPiProviderStatus")(function }); } - const versionProbe = yield* runPiVersion(piSettings, environment).pipe( + const versionProbe = yield* runPiVersion(piSettings, processEnv).pipe( Effect.timeoutOption(DEFAULT_TIMEOUT_MS), Effect.result, ); @@ -212,17 +242,19 @@ export const checkPiProviderStatus = Effect.fn("checkPiProviderStatus")(function }); } - const discovered = yield* discoverPiModelsViaRpc(piSettings, cwd, environment); - const models = modelsFromSettings(piSettings, discovered); + // Pass the raw caller env; discoverPiCatalogViaRpc applies codingAgentDir itself. + const catalog = yield* discoverPiCatalogViaRpc(piSettings, cwd, environment); + const models = modelsFromSettings(piSettings, catalog.models); // no auth query in pi; get_available_models only lists once a key is configured in ~/.pi/agent - const authenticated = discovered.length > 0; + const authenticated = catalog.models.length > 0; return buildServerProvider({ presentation: PI_PRESENTATION, enabled: piSettings.enabled, checkedAt, models, + slashCommands: catalog.slashCommands, probe: { installed: true, version: parsedVersion, diff --git a/apps/server/src/provider/Layers/PiRpcClient.test.ts b/apps/server/src/provider/Layers/PiRpcClient.test.ts index fb0166420de..9810aa7edce 100644 --- a/apps/server/src/provider/Layers/PiRpcClient.test.ts +++ b/apps/server/src/provider/Layers/PiRpcClient.test.ts @@ -11,8 +11,10 @@ import { extractLastAssistantText, extractReasoningTextDelta, extractSessionFile, + extractSlashCommands, extractStateModelSlug, parsePiStdoutLine, + PI_APPROVAL_SENTINEL_COMMAND, PI_THINKING_LEVEL_VALUES, piForkSucceeded, piImageContentFromBytes, @@ -168,6 +170,57 @@ describe("piModelCapabilities", () => { it("exposes no option descriptors for non-reasoning models", () => { expect(piModelCapabilities(false).optionDescriptors ?? []).toEqual([]); }); + + it("hides xhigh/max unless the model is known to support them", () => { + const plain = piModelCapabilities( + asModelInfo({ provider: "anthropic", id: "claude-sonnet-4-6", reasoning: true }), + ); + const plainIds = (plain.optionDescriptors ?? []).flatMap((descriptor) => + descriptor.type === "select" ? descriptor.options.map((option) => option.id) : [], + ); + expect(plainIds).not.toContain("xhigh"); + expect(plainIds).not.toContain("max"); + + const extended = piModelCapabilities( + asModelInfo({ provider: "openai", id: "gpt-5.6", reasoning: true }), + ); + const extendedIds = (extended.optionDescriptors ?? []).flatMap((descriptor) => + descriptor.type === "select" ? descriptor.options.map((option) => option.id) : [], + ); + expect(extendedIds).toContain("xhigh"); + expect(extendedIds).toContain("max"); + }); +}); + +describe("extractSlashCommands", () => { + it("maps plugin/extension/skill commands and strips the approval sentinel", () => { + expect( + extractSlashCommands( + asResponse({ + type: "response", + success: true, + data: { + commands: [ + { name: "session-name", description: "Rename", source: "extension" }, + { name: PI_APPROVAL_SENTINEL_COMMAND, source: "extension" }, + { name: "/fix-tests", description: "Fix tests", source: "prompt" }, + { name: "skill:demo", description: "Demo", source: "skill" }, + { name: "session-name", description: "duplicate", source: "extension" }, + ], + }, + }), + ), + ).toEqual([ + { name: "session-name", description: "Rename" }, + { name: "fix-tests", description: "Fix tests" }, + { name: "skill:demo", description: "Demo" }, + ]); + }); + + it("returns an empty array for missing or malformed responses", () => { + expect(extractSlashCommands(undefined)).toEqual([]); + expect(extractSlashCommands(asResponse({ type: "response", success: false }))).toEqual([]); + }); }); describe("piModelInfoToServerModel", () => { diff --git a/apps/server/src/provider/Layers/PiRpcClient.ts b/apps/server/src/provider/Layers/PiRpcClient.ts index f66a35223f1..94247916348 100644 --- a/apps/server/src/provider/Layers/PiRpcClient.ts +++ b/apps/server/src/provider/Layers/PiRpcClient.ts @@ -7,8 +7,12 @@ import type { RpcExtensionUIResponse, RpcResponse, } from "@earendil-works/pi-coding-agent"; -import type { ModelSelection, ServerProviderModel } from "@t3tools/contracts"; -import type { ModelCapabilities } from "@t3tools/contracts"; +import type { + ModelCapabilities, + ModelSelection, + ServerProviderModel, + ServerProviderSlashCommand, +} from "@t3tools/contracts"; import * as Deferred from "effect/Deferred"; import * as Effect from "effect/Effect"; import * as Option from "effect/Option"; @@ -20,6 +24,9 @@ import { buildSelectOptionDescriptor } from "../providerSnapshot.ts"; import { createModelCapabilities, getModelSelectionStringOptionValue } from "@t3tools/shared/model"; import { resolveSpawnCommand } from "@t3tools/shared/shell"; +/** Internal T3 approval-gate sentinel — never surface as a user slash command. */ +export const PI_APPROVAL_SENTINEL_COMMAND = "t3-approval-gate"; + export type PiStdoutMessage = | { readonly _tag: "response"; readonly id: string | undefined; readonly response: RpcResponse } | { readonly _tag: "extension-ui"; readonly request: RpcExtensionUIRequest } @@ -131,8 +138,11 @@ const PI_THINKING_LEVELS = [ { value: "medium", label: "Medium", isDefault: true }, { value: "high", label: "High" }, { value: "xhigh", label: "Extra High" }, + { value: "max", label: "Max" }, ] as const; +const PI_EXTENDED_THINKING_LEVELS = new Set(["xhigh", "max"]); + export type PiThinkingLevel = Extract["level"]; export const PI_THINKING_OPTION_ID = "thinking"; @@ -177,12 +187,21 @@ export function planPiModelSwitch( return { kind: "switch", provider: parts.provider, modelId: parts.id, slug: requestedModel }; } +function supportsExtendedPiThinkingLevels( + model: boolean | Pick, +): boolean { + if (typeof model === "boolean") return model; + if (model.provider !== "openai") return false; + const id = model.id.toLowerCase(); + // Pi exposes xhigh/max only for models that support them (e.g. codex-max, GPT-5.6). + return id === "codex-max" || id.includes("gpt-5.6") || id.includes("gpt-5.5"); +} + export function piModelCapabilities( model: boolean | Pick, ): ModelCapabilities { const reasoning = typeof model === "boolean" ? model : Boolean(model.reasoning); - const supportsExtraHigh = - typeof model === "boolean" || (model.provider === "openai" && model.id === "codex-max"); + const supportsExtended = supportsExtendedPiThinkingLevels(model); return createModelCapabilities({ optionDescriptors: reasoning ? [ @@ -190,7 +209,7 @@ export function piModelCapabilities( id: "thinking", label: "Thinking", options: PI_THINKING_LEVELS.filter( - (level) => level.value !== "xhigh" || supportsExtraHigh, + (level) => !PI_EXTENDED_THINKING_LEVELS.has(level.value) || supportsExtended, ).map((level) => ({ ...level })), }), ] @@ -263,6 +282,37 @@ export function piResponseHasCommand( ); } +/** + * Map Pi `get_commands` into T3 slash-command catalog entries. + * Includes extension commands, prompt templates, and skills from the user's + * Pi config/plugins (invoked via `/name` in a prompt). + */ +export function extractSlashCommands( + response: RpcResponse | undefined, +): ReadonlyArray { + const commands = piResponseData(response)?.["commands"]; + if (!Array.isArray(commands)) return []; + + const byName = new Map(); + for (const entry of commands) { + if (!entry || typeof entry !== "object") continue; + const record = entry as Record; + const rawName = typeof record["name"] === "string" ? record["name"].trim() : ""; + if (rawName.length === 0 || rawName === PI_APPROVAL_SENTINEL_COMMAND) continue; + const name = rawName.startsWith("/") ? rawName.slice(1) : rawName; + if (name.length === 0 || byName.has(name.toLowerCase())) continue; + const description = + typeof record["description"] === "string" && record["description"].trim().length > 0 + ? record["description"].trim() + : undefined; + byName.set(name.toLowerCase(), { + name, + ...(description ? { description } : {}), + }); + } + return [...byName.values()]; +} + export function extractLastAssistantText(response: RpcResponse | undefined): string | null { const text = piResponseData(response)?.["text"]; return typeof text === "string" ? text : null; diff --git a/apps/server/src/provider/assets/pi/t3-approvals.ts b/apps/server/src/provider/assets/pi/t3-approvals.ts index cb27f1a7511..dd11eb6034e 100644 --- a/apps/server/src/provider/assets/pi/t3-approvals.ts +++ b/apps/server/src/provider/assets/pi/t3-approvals.ts @@ -21,7 +21,7 @@ function gateDecision(opts: { return undefined; } -// keep in sync with PI_APPROVAL_SENTINEL_COMMAND in PiAdapter.ts +// keep in sync with PI_APPROVAL_SENTINEL_COMMAND in PiRpcClient.ts const SENTINEL_COMMAND = "t3-approval-gate"; const DENIED_REASON = "Denied in T3 Code"; diff --git a/apps/server/src/textGeneration/PiTextGeneration.ts b/apps/server/src/textGeneration/PiTextGeneration.ts index 4014a1ced7f..5e405fe53a2 100644 --- a/apps/server/src/textGeneration/PiTextGeneration.ts +++ b/apps/server/src/textGeneration/PiTextGeneration.ts @@ -8,6 +8,7 @@ import { type ModelSelection, type PiSettings, TextGenerationError } from "@t3to import { sanitizeBranchFragment, sanitizeFeatureBranchName } from "@t3tools/shared/git"; import { extractJsonObject } from "@t3tools/shared/schemaJson"; +import { resolvePiProcessEnv } from "../provider/Layers/PiEnvironment.ts"; import { extractLastAssistantText, makePiRpcTransport, @@ -39,11 +40,22 @@ type TextGenOperation = const encodeJsonString = Schema.encodeEffect(Schema.UnknownFromJsonString); const isTextGenerationError = Schema.is(TextGenerationError); +function isPiTextGenerationSettled(message: { + readonly _tag: string; + readonly event?: { readonly type: string; readonly willRetry?: boolean }; +}): boolean { + if (message._tag !== "event" || message.event === undefined) return false; + // Prefer agent_settled (0.80.5+); fall back to terminal agent_end for older Pi. + if (message.event.type === "agent_settled") return true; + return message.event.type === "agent_end" && message.event.willRetry !== true; +} + export const makePiTextGeneration = Effect.fn("makePiTextGeneration")(function* ( piSettings: PiSettings, environment: NodeJS.ProcessEnv = process.env, ) { const commandSpawner = yield* ChildProcessSpawner.ChildProcessSpawner; + const processEnv = resolvePiProcessEnv(piSettings, environment); const runPiPrompt = (input: { readonly message: string; @@ -59,6 +71,7 @@ export const makePiTextGeneration = Effect.fn("makePiTextGeneration")(function* "rpc", "--no-session", "--no-tools", + // Isolated prompts should not load user plugins / approval gates. "--no-extensions", ...(resolvePiThinkingLevel(input.modelSelection) ? ["--thinking", resolvePiThinkingLevel(input.modelSelection)!] @@ -66,17 +79,12 @@ export const makePiTextGeneration = Effect.fn("makePiTextGeneration")(function* ...(input.modelSelection.model ? ["--model", input.modelSelection.model] : []), ], cwd: input.cwd, - env: environment, + env: processEnv, onExit: Effect.void, }); yield* transport.writeCommand({ type: "prompt", message: input.message }); yield* Stream.fromQueue(transport.messages).pipe( - Stream.takeUntil( - (message) => - message._tag === "event" && - message.event.type === "agent_end" && - message.event.willRetry !== true, - ), + Stream.takeUntil(isPiTextGenerationSettled), Stream.runDrain, ); const response = yield* transport.request( diff --git a/apps/web/src/components/settings/ProviderSettingsForm.test.ts b/apps/web/src/components/settings/ProviderSettingsForm.test.ts index 659c6bd2afa..ce936d5060f 100644 --- a/apps/web/src/components/settings/ProviderSettingsForm.test.ts +++ b/apps/web/src/components/settings/ProviderSettingsForm.test.ts @@ -26,7 +26,10 @@ describe("ProviderSettingsForm helpers", () => { expect(pi).toBeDefined(); expect(pi!.label).toBe("Pi"); - expect(deriveProviderSettingsFields(pi!).map((field) => field.key)).toEqual(["binaryPath"]); + expect(deriveProviderSettingsFields(pi!).map((field) => field.key)).toEqual([ + "binaryPath", + "codingAgentDir", + ]); }); it("sources labels and descriptions from schema annotations", () => { diff --git a/docs/providers/pi.md b/docs/providers/pi.md index b877784ccf3..4df113a1a81 100644 --- a/docs/providers/pi.md +++ b/docs/providers/pi.md @@ -4,6 +4,8 @@ This guide is for people who want to use the Pi coding agent in T3 Code. Pi support is **Early Access** and is disabled by default. You opt in from Settings, and Pi runs through its own `~/.pi/agent` configuration — the same setup the `pi` CLI uses. +T3 talks to Pi over its native `pi --mode rpc` protocol (not ACP), so installed Pi +extensions, skills, and packages remain available in sessions. ## Before You Start @@ -33,6 +35,7 @@ In Settings, your Pi provider looks like this: ```text Display name: Pi Binary path: pi +Pi config directory: (empty → ~/.pi/agent) Require tool approval: on ``` @@ -41,27 +44,38 @@ absolute path if you run a specific build. ## Where Pi Keeps Its Config -Pi reads auth, models, and settings from a single directory: +Pi reads auth, models, settings, extensions, and packages from a single directory: ```text ~/.pi/agent/auth.json upstream provider API keys ~/.pi/agent/models.json enabled models ~/.pi/agent/settings.json default provider/model, packages, theme +~/.pi/agent/extensions/ TypeScript extensions / plugins +~/.pi/agent/skills/ skills invocable as /skill:name ``` -T3 Code uses this directory as-is, so the models and providers you see in T3 Code match -what the `pi` CLI shows. +T3 Code uses this directory as-is, so the models, providers, and plugin commands you see +in T3 Code match what the `pi` CLI shows. -To point Pi at a different config directory, set `PI_CODING_AGENT_DIR` in the Pi provider's -Environment variables section in Settings. This is the Pi equivalent of a separate home, -and is useful if you want work and personal Pi setups. +To point Pi at a different config directory, set **Pi config directory** in the provider +settings (this sets `PI_CODING_AGENT_DIR`). You can also set that env var under Environment +variables. This is useful if you want work and personal Pi setups. + +## Plugins And Slash Commands + +Normal chat sessions load your Pi extensions and packages from the config directory. +T3 discovers available commands with Pi's `get_commands` RPC and surfaces them in the +composer as provider slash commands (extension commands, prompt templates, and skills). + +Isolated text-generation helpers (titles, commit messages, etc.) intentionally run with +`--no-extensions` so plugins cannot affect structured output. ## Which Models Are Available? T3 Code discovers Pi models live. When it checks Pi's status, it briefly starts -`pi --mode rpc` and asks Pi for its available models, then appends any custom models you -configured. The result is exactly the model catalog your `~/.pi/agent` configuration -exposes. +`pi --mode rpc` and asks Pi for its available models and commands, then appends any +custom models you configured. The result is exactly the catalog your `~/.pi/agent` +configuration exposes — including plugin-provided providers when those are installed. If discovery fails or times out, T3 Code falls back to your custom models only. Enable more models with the Pi CLI (`pi config`) or by editing `~/.pi/agent/models.json`, then refresh @@ -71,7 +85,8 @@ provider status in Settings. Pi has no built-in per-tool approval prompt, so T3 Code adds one with a small bundled Pi extension. When **Require tool approval** is on (the default), T3 Code gates every tool -call that is not read-only. +call that is not read-only. Your other installed Pi extensions still load alongside this +gate. Read-only tools run without asking: @@ -104,4 +119,5 @@ refuses to start the session rather than run Pi with unguarded tools. An invalid key surfaces as an error when you send a message. Use `pi --list-models` to confirm your keys work. - **Config is shared with the Pi CLI.** Changes you make in `~/.pi/agent` affect both T3 - Code and the `pi` CLI. Use `PI_CODING_AGENT_DIR` to isolate a setup. + Code and the `pi` CLI. Use **Pi config directory** / `PI_CODING_AGENT_DIR` to isolate a + setup. diff --git a/packages/contracts/src/settings.ts b/packages/contracts/src/settings.ts index 6b37458c70f..77d2061a61f 100644 --- a/packages/contracts/src/settings.ts +++ b/packages/contracts/src/settings.ts @@ -368,13 +368,25 @@ export const PiSettings = makeProviderSettingsSchema( providerSettingsForm: { placeholder: "pi", clearWhenEmpty: "omit" }, }), ), + codingAgentDir: TrimmedString.pipe( + Schema.withDecodingDefault(Effect.succeed("")), + Schema.annotateKey({ + title: "Pi config directory", + description: + "Custom PI_CODING_AGENT_DIR. Auth, models, settings, extensions, and packages live here (default ~/.pi/agent).", + providerSettingsForm: { + placeholder: "~/.pi/agent", + clearWhenEmpty: "omit", + }, + }), + ), customModels: Schema.Array(Schema.String).pipe( Schema.withDecodingDefault(Effect.succeed([])), Schema.annotateKey({ providerSettingsForm: { hidden: true } }), ), }, { - order: ["binaryPath"], + order: ["binaryPath", "codingAgentDir"], }, ); export type PiSettings = typeof PiSettings.Type; @@ -529,6 +541,7 @@ const OpenCodeSettingsPatch = Schema.Struct({ const PiSettingsPatch = Schema.Struct({ enabled: Schema.optionalKey(Schema.Boolean), binaryPath: Schema.optionalKey(TrimmedString), + codingAgentDir: Schema.optionalKey(TrimmedString), customModels: Schema.optionalKey(Schema.Array(Schema.String)), }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 015e3931980..124dca791f6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -465,8 +465,8 @@ importers: version: 1.1.0 devDependencies: '@earendil-works/pi-coding-agent': - specifier: ^0.80.2 - version: 0.80.3(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(bufferutil@4.1.0)(utf-8-validate@6.0.6)(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@6.0.6))(zod@4.4.3) + specifier: ^0.80.10 + version: 0.80.10(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(bufferutil@4.1.0)(utf-8-validate@6.0.6)(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@6.0.6))(zod@4.4.3) '@effect/vitest': specifier: 4.0.0-beta.78 version: 4.0.0-beta.78(patch_hash=42b87cc47e70d74e62496e7a8261b3fd298ecad4464d209348ab04b96f853a5f)(effect@4.0.0-beta.78(patch_hash=c502bc684210b707dfceb87d8fe6ad6843395af6e19cfc02cd65854898bde2c5)) @@ -1964,22 +1964,22 @@ packages: '@drizzle-team/brocli@0.11.0': resolution: {integrity: sha512-hD3pekGiPg0WPCCGAZmusBBJsDqGUR66Y452YgQsZOnkdQ7ViEPKuyP4huUGEZQefp8g34RRodXYmJ2TbCH+tg==} - '@earendil-works/pi-agent-core@0.80.3': - resolution: {integrity: sha512-3qw0/GeRQBU/nlGjDe5Yb7ePKTmoxefx2YxyKMFAviFUMXpFexBG/hS7mBtwFahFvzrrTPPoRT6sFIDjwoDWPQ==} + '@earendil-works/pi-agent-core@0.80.10': + resolution: {integrity: sha512-nwnOR3SuLYGRFfyQm8ri4Nj5VGVAvAM9GuqQd3u7BUQj0d6hmD2F8w7OHAAjThE3CuySIdM+v8E22QJG6/RfCg==} engines: {node: '>=22.19.0'} - '@earendil-works/pi-ai@0.80.3': - resolution: {integrity: sha512-jPZLMeGL5kkMSEAwAklfXTMHqZvfhsJtCCpKGIr5Duk7mc0n4skjB1dugk7y0z3z8ZHIUCmPAWHdyDqgUz5vdA==} + '@earendil-works/pi-ai@0.80.10': + resolution: {integrity: sha512-Moe/H8c87yacDGK9dPbWphZNjVsrb3nTrIHycOQJAkFEnY9PYxOOd74+ny44kATfPU9Dm7aTHefar3pZF+UKUA==} engines: {node: '>=22.19.0'} hasBin: true - '@earendil-works/pi-coding-agent@0.80.3': - resolution: {integrity: sha512-TIggw9gCXpA+Ph7OjdTA7ka2NPwTVuPmy39KDSyUzaKq8VvHfMGR7vtRz4JB7Um/RMRblmzhu4p9tUCk6MTgGA==} + '@earendil-works/pi-coding-agent@0.80.10': + resolution: {integrity: sha512-aL4apbupCHiVLSXASXvRzH4Q2vmtfrDa+0s909CJuVu/GgGylbDzr7oyF1mPmip5E+VxYYxKWmph4hV04wUcQg==} engines: {node: '>=22.19.0'} hasBin: true - '@earendil-works/pi-tui@0.80.3': - resolution: {integrity: sha512-2BJI6qwRQfnM0Q7seL1+SbacU/jRRjBnN7Hu3n9BjAn7/s5FaBNnvdD1qBQYRsFTHfjqMaDsjYqanPyqwXj99w==} + '@earendil-works/pi-tui@0.80.10': + resolution: {integrity: sha512-c2JO29PbhKPEQ6fgHQKAl0WhwuFqzWfzspMmP+8B5tpDuP+0mvarRbKKg8gq4b+pQx/QX+6aVS4ko7deoyjQjg==} engines: {node: '>=22.19.0'} '@effect/atom-react@4.0.0-beta.78': @@ -10937,7 +10937,7 @@ snapshots: '@aws-crypto/sha256-js': 5.2.0 '@aws-crypto/supports-web-crypto': 5.2.0 '@aws-crypto/util': 5.2.0 - '@aws-sdk/types': 3.973.10 + '@aws-sdk/types': 3.974.0 '@aws-sdk/util-locate-window': 3.965.5 '@smithy/util-utf8': 2.3.0 tslib: 2.8.1 @@ -10945,7 +10945,7 @@ snapshots: '@aws-crypto/sha256-js@5.2.0': dependencies: '@aws-crypto/util': 5.2.0 - '@aws-sdk/types': 3.973.10 + '@aws-sdk/types': 3.974.0 tslib: 2.8.1 '@aws-crypto/supports-web-crypto@5.2.0': @@ -10962,17 +10962,17 @@ snapshots: dependencies: '@aws-crypto/sha256-browser': 5.2.0 '@aws-crypto/sha256-js': 5.2.0 - '@aws-sdk/core': 3.974.17 + '@aws-sdk/core': 3.975.0 '@aws-sdk/credential-provider-node': 3.972.51 '@aws-sdk/eventstream-handler-node': 3.972.26 '@aws-sdk/middleware-eventstream': 3.972.22 '@aws-sdk/middleware-websocket': 3.972.38 '@aws-sdk/token-providers': 3.1048.0 - '@aws-sdk/types': 3.973.10 - '@smithy/core': 3.24.6 - '@smithy/fetch-http-handler': 5.4.6 + '@aws-sdk/types': 3.974.0 + '@smithy/core': 3.29.2 + '@smithy/fetch-http-handler': 5.6.4 '@smithy/node-http-handler': 4.7.7 - '@smithy/types': 4.14.3 + '@smithy/types': 4.16.0 tslib: 2.8.1 '@aws-sdk/client-cognito-identity@3.1062.0': @@ -11161,27 +11161,27 @@ snapshots: '@aws-sdk/signature-v4-multi-region@3.996.31': dependencies: - '@aws-sdk/types': 3.973.10 + '@aws-sdk/types': 3.974.0 '@smithy/signature-v4': 5.4.6 - '@smithy/types': 4.14.3 + '@smithy/types': 4.16.0 tslib: 2.8.1 '@aws-sdk/token-providers@3.1048.0': dependencies: - '@aws-sdk/core': 3.974.17 + '@aws-sdk/core': 3.975.0 '@aws-sdk/nested-clients': 3.997.16 - '@aws-sdk/types': 3.973.10 - '@smithy/core': 3.24.6 - '@smithy/types': 4.14.3 + '@aws-sdk/types': 3.974.0 + '@smithy/core': 3.29.2 + '@smithy/types': 4.16.0 tslib: 2.8.1 '@aws-sdk/token-providers@3.1062.0': dependencies: - '@aws-sdk/core': 3.974.17 + '@aws-sdk/core': 3.975.0 '@aws-sdk/nested-clients': 3.997.16 - '@aws-sdk/types': 3.973.10 - '@smithy/core': 3.24.6 - '@smithy/types': 4.14.3 + '@aws-sdk/types': 3.974.0 + '@smithy/core': 3.29.2 + '@smithy/types': 4.16.0 tslib: 2.8.1 '@aws-sdk/types@3.973.10': @@ -11200,7 +11200,7 @@ snapshots: '@aws-sdk/xml-builder@3.972.27': dependencies: - '@smithy/types': 4.14.3 + '@smithy/types': 4.16.0 fast-xml-parser: 5.7.3 tslib: 2.8.1 @@ -12056,9 +12056,9 @@ snapshots: '@drizzle-team/brocli@0.11.0': {} - '@earendil-works/pi-agent-core@0.80.3(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(bufferutil@4.1.0)(utf-8-validate@6.0.6)(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@6.0.6))(zod@4.4.3)': + '@earendil-works/pi-agent-core@0.80.10(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(bufferutil@4.1.0)(utf-8-validate@6.0.6)(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@6.0.6))(zod@4.4.3)': dependencies: - '@earendil-works/pi-ai': 0.80.3(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(bufferutil@4.1.0)(utf-8-validate@6.0.6)(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@6.0.6))(zod@4.4.3) + '@earendil-works/pi-ai': 0.80.10(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(bufferutil@4.1.0)(utf-8-validate@6.0.6)(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@6.0.6))(zod@4.4.3) ignore: 7.0.5 typebox: 1.1.38 yaml: 2.9.0 @@ -12070,7 +12070,7 @@ snapshots: - ws - zod - '@earendil-works/pi-ai@0.80.3(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(bufferutil@4.1.0)(utf-8-validate@6.0.6)(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@6.0.6))(zod@4.4.3)': + '@earendil-works/pi-ai@0.80.10(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(bufferutil@4.1.0)(utf-8-validate@6.0.6)(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@6.0.6))(zod@4.4.3)': dependencies: '@anthropic-ai/sdk': 0.91.1(zod@4.4.3) '@aws-sdk/client-bedrock-runtime': 3.1048.0 @@ -12091,11 +12091,11 @@ snapshots: - ws - zod - '@earendil-works/pi-coding-agent@0.80.3(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(bufferutil@4.1.0)(utf-8-validate@6.0.6)(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@6.0.6))(zod@4.4.3)': + '@earendil-works/pi-coding-agent@0.80.10(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(bufferutil@4.1.0)(utf-8-validate@6.0.6)(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@6.0.6))(zod@4.4.3)': dependencies: - '@earendil-works/pi-agent-core': 0.80.3(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(bufferutil@4.1.0)(utf-8-validate@6.0.6)(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@6.0.6))(zod@4.4.3) - '@earendil-works/pi-ai': 0.80.3(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(bufferutil@4.1.0)(utf-8-validate@6.0.6)(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@6.0.6))(zod@4.4.3) - '@earendil-works/pi-tui': 0.80.3 + '@earendil-works/pi-agent-core': 0.80.10(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(bufferutil@4.1.0)(utf-8-validate@6.0.6)(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@6.0.6))(zod@4.4.3) + '@earendil-works/pi-ai': 0.80.10(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(bufferutil@4.1.0)(utf-8-validate@6.0.6)(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@6.0.6))(zod@4.4.3) + '@earendil-works/pi-tui': 0.80.10 '@silvia-odwyer/photon-node': 0.3.4 chalk: 5.6.2 cross-spawn: 7.0.6 @@ -12121,7 +12121,7 @@ snapshots: - ws - zod - '@earendil-works/pi-tui@0.80.3': + '@earendil-works/pi-tui@0.80.10': dependencies: get-east-asian-width: 1.6.0 marked: 18.0.5 @@ -15072,8 +15072,8 @@ snapshots: '@smithy/fetch-http-handler@5.4.6': dependencies: - '@smithy/core': 3.24.6 - '@smithy/types': 4.14.3 + '@smithy/core': 3.29.2 + '@smithy/types': 4.16.0 tslib: 2.8.1 '@smithy/fetch-http-handler@5.6.4': @@ -15093,14 +15093,14 @@ snapshots: '@smithy/node-http-handler@4.7.3': dependencies: - '@smithy/core': 3.24.6 - '@smithy/types': 4.14.3 + '@smithy/core': 3.29.2 + '@smithy/types': 4.16.0 tslib: 2.8.1 '@smithy/node-http-handler@4.7.7': dependencies: - '@smithy/core': 3.24.6 - '@smithy/types': 4.14.3 + '@smithy/core': 3.29.2 + '@smithy/types': 4.16.0 tslib: 2.8.1 '@smithy/shared-ini-file-loader@4.5.6': @@ -15110,8 +15110,8 @@ snapshots: '@smithy/signature-v4@5.4.6': dependencies: - '@smithy/core': 3.24.6 - '@smithy/types': 4.14.3 + '@smithy/core': 3.29.2 + '@smithy/types': 4.16.0 tslib: 2.8.1 '@smithy/signature-v4@5.6.3': diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 7750ca2b0f2..799e383a976 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -60,6 +60,10 @@ minimumReleaseAgeExclude: - "@clerk/expo@3.7.2" - "@clerk/react@6.12.1" - "@clerk/shared@4.25.1" + - "@earendil-works/pi-agent-core@0.80.10" + - "@earendil-works/pi-ai@0.80.10" + - "@earendil-works/pi-coding-agent@0.80.10" + - "@earendil-works/pi-tui@0.80.10" overrides: "@clerk/backend": "catalog:"