From 3024df810c46ef3b72897b8594404225a10285bf Mon Sep 17 00:00:00 2001 From: ardenworks <284684426+ardenworks@users.noreply.github.com> Date: Thu, 14 May 2026 10:27:43 -0400 Subject: [PATCH 01/36] fix(ssh): decode auth timestamps from JSON strings --- packages/contracts/src/auth.ts | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/contracts/src/auth.ts b/packages/contracts/src/auth.ts index 8439d12b069..f764f79f2b0 100644 --- a/packages/contracts/src/auth.ts +++ b/packages/contracts/src/auth.ts @@ -109,7 +109,7 @@ export const AuthBootstrapResult = Schema.Struct({ authenticated: Schema.Literal(true), role: AuthSessionRole, sessionMethod: ServerAuthSessionMethod, - expiresAt: Schema.DateTimeUtc, + expiresAt: Schema.DateTimeUtcFromString, }); export type AuthBootstrapResult = typeof AuthBootstrapResult.Type; @@ -117,14 +117,14 @@ export const AuthBearerBootstrapResult = Schema.Struct({ authenticated: Schema.Literal(true), role: AuthSessionRole, sessionMethod: Schema.Literal("bearer-session-token"), - expiresAt: Schema.DateTimeUtc, + expiresAt: Schema.DateTimeUtcFromString, sessionToken: TrimmedNonEmptyString, }); export type AuthBearerBootstrapResult = typeof AuthBearerBootstrapResult.Type; export const AuthWebSocketTokenResult = Schema.Struct({ token: TrimmedNonEmptyString, - expiresAt: Schema.DateTimeUtc, + expiresAt: Schema.DateTimeUtcFromString, }); export type AuthWebSocketTokenResult = typeof AuthWebSocketTokenResult.Type; @@ -132,7 +132,7 @@ export const AuthPairingCredentialResult = Schema.Struct({ id: TrimmedNonEmptyString, credential: TrimmedNonEmptyString, label: Schema.optionalKey(TrimmedNonEmptyString), - expiresAt: Schema.DateTimeUtc, + expiresAt: Schema.DateTimeUtcFromString, }); export type AuthPairingCredentialResult = typeof AuthPairingCredentialResult.Type; @@ -142,8 +142,8 @@ export const AuthPairingLink = Schema.Struct({ role: AuthSessionRole, subject: TrimmedNonEmptyString, label: Schema.optionalKey(TrimmedNonEmptyString), - createdAt: Schema.DateTimeUtc, - expiresAt: Schema.DateTimeUtc, + createdAt: Schema.DateTimeUtcFromString, + expiresAt: Schema.DateTimeUtcFromString, }); export type AuthPairingLink = typeof AuthPairingLink.Type; @@ -172,9 +172,9 @@ export const AuthClientSession = Schema.Struct({ role: AuthSessionRole, method: ServerAuthSessionMethod, client: AuthClientMetadata, - issuedAt: Schema.DateTimeUtc, - expiresAt: Schema.DateTimeUtc, - lastConnectedAt: Schema.NullOr(Schema.DateTimeUtc), + issuedAt: Schema.DateTimeUtcFromString, + expiresAt: Schema.DateTimeUtcFromString, + lastConnectedAt: Schema.NullOr(Schema.DateTimeUtcFromString), connected: Schema.Boolean, current: Schema.Boolean, }); @@ -261,6 +261,6 @@ export const AuthSessionState = Schema.Struct({ auth: ServerAuthDescriptor, role: Schema.optionalKey(AuthSessionRole), sessionMethod: Schema.optionalKey(ServerAuthSessionMethod), - expiresAt: Schema.optionalKey(Schema.DateTimeUtc), + expiresAt: Schema.optionalKey(Schema.DateTimeUtcFromString), }); export type AuthSessionState = typeof AuthSessionState.Type; From 0e9174533792a74d0aa232f0cd8f03a5f8973a7d Mon Sep 17 00:00:00 2001 From: ardenworks <284684426+ardenworks@users.noreply.github.com> Date: Thu, 14 May 2026 10:27:43 -0400 Subject: [PATCH 02/36] fix(ssh): decode auth timestamps from JSON strings --- packages/contracts/src/auth.ts | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/contracts/src/auth.ts b/packages/contracts/src/auth.ts index 8439d12b069..f764f79f2b0 100644 --- a/packages/contracts/src/auth.ts +++ b/packages/contracts/src/auth.ts @@ -109,7 +109,7 @@ export const AuthBootstrapResult = Schema.Struct({ authenticated: Schema.Literal(true), role: AuthSessionRole, sessionMethod: ServerAuthSessionMethod, - expiresAt: Schema.DateTimeUtc, + expiresAt: Schema.DateTimeUtcFromString, }); export type AuthBootstrapResult = typeof AuthBootstrapResult.Type; @@ -117,14 +117,14 @@ export const AuthBearerBootstrapResult = Schema.Struct({ authenticated: Schema.Literal(true), role: AuthSessionRole, sessionMethod: Schema.Literal("bearer-session-token"), - expiresAt: Schema.DateTimeUtc, + expiresAt: Schema.DateTimeUtcFromString, sessionToken: TrimmedNonEmptyString, }); export type AuthBearerBootstrapResult = typeof AuthBearerBootstrapResult.Type; export const AuthWebSocketTokenResult = Schema.Struct({ token: TrimmedNonEmptyString, - expiresAt: Schema.DateTimeUtc, + expiresAt: Schema.DateTimeUtcFromString, }); export type AuthWebSocketTokenResult = typeof AuthWebSocketTokenResult.Type; @@ -132,7 +132,7 @@ export const AuthPairingCredentialResult = Schema.Struct({ id: TrimmedNonEmptyString, credential: TrimmedNonEmptyString, label: Schema.optionalKey(TrimmedNonEmptyString), - expiresAt: Schema.DateTimeUtc, + expiresAt: Schema.DateTimeUtcFromString, }); export type AuthPairingCredentialResult = typeof AuthPairingCredentialResult.Type; @@ -142,8 +142,8 @@ export const AuthPairingLink = Schema.Struct({ role: AuthSessionRole, subject: TrimmedNonEmptyString, label: Schema.optionalKey(TrimmedNonEmptyString), - createdAt: Schema.DateTimeUtc, - expiresAt: Schema.DateTimeUtc, + createdAt: Schema.DateTimeUtcFromString, + expiresAt: Schema.DateTimeUtcFromString, }); export type AuthPairingLink = typeof AuthPairingLink.Type; @@ -172,9 +172,9 @@ export const AuthClientSession = Schema.Struct({ role: AuthSessionRole, method: ServerAuthSessionMethod, client: AuthClientMetadata, - issuedAt: Schema.DateTimeUtc, - expiresAt: Schema.DateTimeUtc, - lastConnectedAt: Schema.NullOr(Schema.DateTimeUtc), + issuedAt: Schema.DateTimeUtcFromString, + expiresAt: Schema.DateTimeUtcFromString, + lastConnectedAt: Schema.NullOr(Schema.DateTimeUtcFromString), connected: Schema.Boolean, current: Schema.Boolean, }); @@ -261,6 +261,6 @@ export const AuthSessionState = Schema.Struct({ auth: ServerAuthDescriptor, role: Schema.optionalKey(AuthSessionRole), sessionMethod: Schema.optionalKey(ServerAuthSessionMethod), - expiresAt: Schema.optionalKey(Schema.DateTimeUtc), + expiresAt: Schema.optionalKey(Schema.DateTimeUtcFromString), }); export type AuthSessionState = typeof AuthSessionState.Type; From 359846b7483c0c8134cf6844bc19ac0d6b867d91 Mon Sep 17 00:00:00 2001 From: Ben Scholar Date: Wed, 20 May 2026 16:48:30 -0700 Subject: [PATCH 03/36] add pi provider --- apps/server/package.json | 1 + apps/server/src/provider/Drivers/PiDriver.ts | 155 ++++ apps/server/src/provider/Drivers/PiHome.ts | 42 ++ apps/server/src/provider/Layers/PiAdapter.ts | 683 ++++++++++++++++++ apps/server/src/provider/Layers/PiProvider.ts | 271 +++++++ .../provider/Layers/ProviderRegistry.test.ts | 29 + .../provider/Layers/ProviderService.test.ts | 7 + .../server/src/provider/Services/PiAdapter.ts | 4 + apps/server/src/provider/builtInDrivers.ts | 5 +- .../src/textGeneration/PiTextGeneration.ts | 277 +++++++ .../src/textGeneration/TextGeneration.ts | 2 +- .../components/KeybindingsToast.browser.tsx | 7 + .../src/components/chat/providerIconUtils.ts | 3 +- bun.lock | 253 ++++++- packages/contracts/src/model.ts | 5 + packages/contracts/src/providerRuntime.ts | 2 + packages/contracts/src/settings.test.ts | 7 + packages/contracts/src/settings.ts | 44 ++ 18 files changed, 1789 insertions(+), 8 deletions(-) create mode 100644 apps/server/src/provider/Drivers/PiDriver.ts create mode 100644 apps/server/src/provider/Drivers/PiHome.ts create mode 100644 apps/server/src/provider/Layers/PiAdapter.ts create mode 100644 apps/server/src/provider/Layers/PiProvider.ts create mode 100644 apps/server/src/provider/Services/PiAdapter.ts create mode 100644 apps/server/src/textGeneration/PiTextGeneration.ts diff --git a/apps/server/package.json b/apps/server/package.json index 387c880a9ab..aee14078ea8 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -24,6 +24,7 @@ }, "dependencies": { "@anthropic-ai/claude-agent-sdk": "^0.2.111", + "@earendil-works/pi-coding-agent": "^0.75.4", "@effect/platform-bun": "catalog:", "@effect/platform-node": "catalog:", "@effect/platform-node-shared": "catalog:", diff --git a/apps/server/src/provider/Drivers/PiDriver.ts b/apps/server/src/provider/Drivers/PiDriver.ts new file mode 100644 index 00000000000..5b7c8abaaa6 --- /dev/null +++ b/apps/server/src/provider/Drivers/PiDriver.ts @@ -0,0 +1,155 @@ +import { PiSettings, ProviderDriverKind, type ServerProvider } from "@t3tools/contracts"; +import * as Duration from "effect/Duration"; +import * as Effect from "effect/Effect"; +import * as FileSystem from "effect/FileSystem"; +import * as Path from "effect/Path"; +import * as Schema from "effect/Schema"; +import * as Stream from "effect/Stream"; +import { HttpClient } from "effect/unstable/http"; +import { ChildProcessSpawner } from "effect/unstable/process"; + + +import { makePiTextGeneration } from "../../textGeneration/PiTextGeneration.ts"; +import { ServerConfig } from "../../config.ts"; +import { ProviderDriverError } from "../Errors.ts"; +import { makePiAdapter } from "../Layers/PiAdapter.ts"; +import { checkPiProviderStatus, makePendingPiProvider } from "../Layers/PiProvider.ts"; +import { ProviderEventLoggers } from "../Layers/ProviderEventLoggers.ts"; +import { makeManagedServerProvider } from "../makeManagedServerProvider.ts"; +import { + defaultProviderContinuationIdentity, + type ProviderDriver, + type ProviderInstance, +} from "../ProviderDriver.ts"; +import type { ServerProviderDraft } from "../providerSnapshot.ts"; +import { mergeProviderInstanceEnvironment } from "../ProviderInstanceEnvironment.ts"; +import { + enrichProviderSnapshotWithVersionAdvisory, + makePackageManagedProviderMaintenanceResolver, + resolveProviderMaintenanceCapabilitiesEffect, +} from "../providerMaintenance.ts"; +import { makePiContinuationGroupKey } from "./PiHome.ts"; + +const decodePiSettings = Schema.decodeSync(PiSettings); + +const DRIVER_KIND = ProviderDriverKind.make("piAgent"); +const SNAPSHOT_REFRESH_INTERVAL = Duration.minutes(5); + +const UPDATE = makePackageManagedProviderMaintenanceResolver({ + provider: DRIVER_KIND, + npmPackageName: "@earendil-works/pi-coding-agent", + homebrewFormula: null, + nativeUpdate: null, +}); + +export type PiDriverEnv = + | ChildProcessSpawner.ChildProcessSpawner + | FileSystem.FileSystem + | HttpClient.HttpClient + | Path.Path + | ProviderEventLoggers + | ServerConfig; + +const withInstanceIdentity = + (input: { + readonly instanceId: ProviderInstance["instanceId"]; + readonly displayName: string | undefined; + readonly accentColor: string | undefined; + readonly continuationGroupKey: string; + }) => + (snapshot: ServerProviderDraft): ServerProvider => ({ + ...snapshot, + instanceId: input.instanceId, + driver: DRIVER_KIND, + ...(input.displayName ? { displayName: input.displayName } : {}), + ...(input.accentColor ? { accentColor: input.accentColor } : {}), + continuation: { groupKey: input.continuationGroupKey }, + }); + +export const PiDriver: ProviderDriver = { + driverKind: DRIVER_KIND, + metadata: { + displayName: "Pi Agent", + supportsMultipleInstances: true, + }, + configSchema: PiSettings, + defaultConfig: (): PiSettings => decodePiSettings({}), + create: ({ instanceId, displayName, accentColor, environment, enabled, config }) => + Effect.gen(function* () { + const httpClient = yield* HttpClient.HttpClient; + const eventLoggers = yield* ProviderEventLoggers; + const processEnv = mergeProviderInstanceEnvironment(environment); + const continuationIdentity = defaultProviderContinuationIdentity({ + driverKind: DRIVER_KIND, + instanceId, + }); + const effectiveConfig = { ...config, enabled } satisfies PiSettings; + const maintenanceCapabilities = yield* resolveProviderMaintenanceCapabilitiesEffect(UPDATE, { + binaryPath: effectiveConfig.binaryPath, + env: processEnv, + }); + const continuationGroupKey = yield* makePiContinuationGroupKey(effectiveConfig); + const stampIdentity = withInstanceIdentity({ + instanceId, + displayName, + accentColor, + continuationGroupKey, + }); + + const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; + const path = yield* Path.Path; + + const adapter = yield* makePiAdapter(effectiveConfig, { + instanceId, + environment: processEnv, + }); + const textGeneration = yield* makePiTextGeneration(effectiveConfig, processEnv); + + const checkProvider = checkPiProviderStatus(effectiveConfig, processEnv).pipe( + Effect.map(stampIdentity), + Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner), + Effect.provideService(Path.Path, path), + ); + + const snapshot = yield* makeManagedServerProvider({ + maintenanceCapabilities, + getSettings: Effect.succeed(effectiveConfig), + streamSettings: Stream.never, + haveSettingsChanged: () => false, + initialSnapshot: (settings) => + makePendingPiProvider(settings).pipe(Effect.map(stampIdentity)), + checkProvider, + enrichSnapshot: ({ snapshot, publishSnapshot }) => + enrichProviderSnapshotWithVersionAdvisory(snapshot, maintenanceCapabilities).pipe( + Effect.provideService(HttpClient.HttpClient, httpClient), + Effect.flatMap((enrichedSnapshot) => publishSnapshot(enrichedSnapshot)), + ), + refreshInterval: SNAPSHOT_REFRESH_INTERVAL, + }).pipe( + Effect.mapError( + (cause) => + new ProviderDriverError({ + driver: DRIVER_KIND, + instanceId, + detail: `Failed to build Pi Agent snapshot: ${cause.message ?? String(cause)}`, + cause, + }), + ), + ); + + return { + instanceId, + driverKind: DRIVER_KIND, + continuationIdentity: { + ...continuationIdentity, + continuationKey: continuationGroupKey, + }, + displayName, + accentColor, + enabled, + snapshot, + adapter, + textGeneration, + } satisfies ProviderInstance; + }), +}; diff --git a/apps/server/src/provider/Drivers/PiHome.ts b/apps/server/src/provider/Drivers/PiHome.ts new file mode 100644 index 00000000000..ab7621d3d07 --- /dev/null +++ b/apps/server/src/provider/Drivers/PiHome.ts @@ -0,0 +1,42 @@ +import * as NodeOS from "node:os"; + +import type { PiSettings } from "@t3tools/contracts"; +import * as Effect from "effect/Effect"; +import * as Path from "effect/Path"; + +import { expandHomePath } from "../../pathExpansion.ts"; + +export const resolvePiHomePath = Effect.fn("resolvePiHomePath")(function* ( + config: Pick, +): Effect.fn.Return { + const path = yield* Path.Path; + const homePath = config.homePath.trim(); + return path.resolve(homePath.length > 0 ? expandHomePath(homePath) : NodeOS.homedir()); +}); + +export const makePiEnvironment = Effect.fn("makePiEnvironment")(function* ( + config: Pick, + baseEnv: NodeJS.ProcessEnv = process.env, +): Effect.fn.Return { + const homePath = config.homePath.trim(); + if (homePath.length === 0) return baseEnv; + const resolvedHomePath = yield* resolvePiHomePath(config); + return { + ...baseEnv, + HOME: resolvedHomePath, + }; +}); + +export const makePiContinuationGroupKey = Effect.fn("makePiContinuationGroupKey")(function* ( + config: Pick, +): Effect.fn.Return { + const resolvedHomePath = yield* resolvePiHomePath(config); + return `pi:home:${resolvedHomePath}`; +}); + +export const makePiCapabilitiesCacheKey = Effect.fn("makePiCapabilitiesCacheKey")(function* ( + config: Pick, +): Effect.fn.Return { + const resolvedHomePath = yield* resolvePiHomePath(config); + return `${config.binaryPath}\0${resolvedHomePath}`; +}); diff --git a/apps/server/src/provider/Layers/PiAdapter.ts b/apps/server/src/provider/Layers/PiAdapter.ts new file mode 100644 index 00000000000..6931d6ae28f --- /dev/null +++ b/apps/server/src/provider/Layers/PiAdapter.ts @@ -0,0 +1,683 @@ +import { + createAgentSession, + type AgentSession, + type AgentSessionEvent, + type CreateAgentSessionOptions, +} from "@earendil-works/pi-coding-agent"; +import { + type CanonicalItemType, + EventId, + type PiSettings, + type ProviderApprovalDecision, + ProviderDriverKind, + ProviderInstanceId, + type ProviderRuntimeEvent, + type ProviderRuntimeTurnStatus, + type ProviderSendTurnInput, + type ProviderSession, + type ProviderUserInputAnswers, + RuntimeItemId, + ThreadId, + TurnId, +} from "@t3tools/contracts"; +import * as DateTime from "effect/DateTime"; +import * as Effect from "effect/Effect"; +import * as Fiber from "effect/Fiber"; +import * as Queue from "effect/Queue"; +import * as Random from "effect/Random"; +import * as Stream from "effect/Stream"; + +import { ServerConfig } from "../../config.ts"; +import { makePiEnvironment } from "../Drivers/PiHome.ts"; +import { + ProviderAdapterProcessError, + ProviderAdapterRequestError, + ProviderAdapterSessionClosedError, + ProviderAdapterSessionNotFoundError, + ProviderAdapterValidationError, + type ProviderAdapterError, +} from "../Errors.ts"; +import type { PiAdapterShape } from "../Services/PiAdapter.ts"; + +const PROVIDER = ProviderDriverKind.make("piAgent"); + +interface PiTurnState { + readonly turnId: TurnId; + readonly startedAt: string; + readonly items: Array; +} + +interface PiSessionContext { + session: ProviderSession; + piSession: AgentSession; + unsubscribe: (() => void) | undefined; + streamFiber: Fiber.Fiber | undefined; + readonly startedAt: string; + turnState: PiTurnState | undefined; + readonly turns: Array<{ id: TurnId; items: Array }>; + stopped: boolean; +} + +function toMessage(cause: unknown, fallback: string): string { + if (cause instanceof Error && cause.message.length > 0) { + return cause.message; + } + return fallback; +} + +function classifyToolItemType(toolName: string): CanonicalItemType { + const normalized = toolName.toLowerCase(); + if (normalized.includes("agent") || normalized.includes("subagent")) { + return "collab_agent_tool_call"; + } + if ( + normalized.includes("bash") || + normalized.includes("shell") || + normalized.includes("command") || + normalized.includes("exec") + ) { + return "command_execution"; + } + if ( + normalized.includes("edit") || + normalized.includes("write") || + normalized.includes("patch") || + normalized.includes("apply") + ) { + return "file_change"; + } + if (normalized.includes("search") || normalized.includes("web")) { + return "web_search"; + } + return "dynamic_tool_call"; +} + +export interface PiAdapterLiveOptions { + readonly instanceId?: ProviderInstanceId; + readonly environment?: NodeJS.ProcessEnv; +} + +export const makePiAdapter = Effect.fn("makePiAdapter")(function* ( + piSettings: PiSettings, + options?: PiAdapterLiveOptions, +) { + const boundInstanceId = options?.instanceId ?? ProviderInstanceId.make("piAgent"); + const serverConfig = yield* ServerConfig; + const piEnvironment = yield* makePiEnvironment(piSettings, options?.environment); + + const sessions = new Map(); + const runtimeEventQueue = yield* Queue.unbounded(); + + const nowIso = Effect.map(DateTime.now, DateTime.formatIso); + const nextEventId = Effect.map(Random.nextUUIDv4, (id) => EventId.make(id)); + const makeEventStamp = () => Effect.all({ eventId: nextEventId, createdAt: nowIso }); + + const offerRuntimeEvent = (event: ProviderRuntimeEvent): Effect.Effect => + Queue.offer(runtimeEventQueue, event).pipe(Effect.asVoid); + + const completeTurn = Effect.fn("completeTurn")(function* ( + context: PiSessionContext, + state: ProviderRuntimeTurnStatus, + message?: string, + ) { + const turnState = context.turnState; + if (!turnState) return; + + context.turnState = undefined; + context.turns.push({ + id: turnState.turnId, + items: [...turnState.items], + }); + + const updatedAt = yield* nowIso; + context.session = { + ...context.session, + status: "ready", + activeTurnId: undefined, + updatedAt, + }; + + const stamp = yield* makeEventStamp(); + yield* offerRuntimeEvent({ + type: "turn.completed", + eventId: stamp.eventId, + provider: PROVIDER, + createdAt: stamp.createdAt, + threadId: context.session.threadId, + turnId: turnState.turnId, + payload: { + state, + ...(message ? { message } : {}), + }, + providerRefs: {}, + }); + }); + + const handlePiEvent = Effect.fn("handlePiEvent")(function* ( + context: PiSessionContext, + event: AgentSessionEvent, + ) { + const stamp = yield* makeEventStamp(); + const base = { + eventId: stamp.eventId, + provider: PROVIDER, + createdAt: stamp.createdAt, + threadId: context.session.threadId, + ...(context.turnState ? { turnId: context.turnState.turnId } : {}), + providerRefs: {}, + raw: { + source: "pi.sdk.event" as const, + method: event.type, + payload: event, + }, + }; + + switch (event.type) { + case "agent_start": + yield* offerRuntimeEvent({ + ...base, + type: "session.state.changed", + payload: { state: "running" }, + }); + return; + + case "turn_start": { + if (!context.turnState) { + const turnId = TurnId.make(yield* Random.nextUUIDv4); + const startedAt = yield* nowIso; + context.turnState = { turnId, startedAt, items: [] }; + const updatedAt = yield* nowIso; + context.session = { + ...context.session, + status: "running", + activeTurnId: turnId, + updatedAt, + }; + yield* offerRuntimeEvent({ + ...base, + turnId, + type: "turn.started", + payload: {}, + }); + } + return; + } + + case "message_update": { + if (!context.turnState) return; + const assistantEvent = event.assistantMessageEvent; + if (assistantEvent && "text" in assistantEvent && typeof assistantEvent.text === "string") { + yield* offerRuntimeEvent({ + ...base, + turnId: context.turnState.turnId, + type: "content.delta", + payload: { + streamKind: "assistant_text", + delta: assistantEvent.text, + }, + }); + } + if ( + assistantEvent && + "thinking" in assistantEvent && + typeof assistantEvent.thinking === "string" + ) { + yield* offerRuntimeEvent({ + ...base, + turnId: context.turnState.turnId, + type: "content.delta", + payload: { + streamKind: "reasoning_text", + delta: assistantEvent.thinking, + }, + }); + } + return; + } + + case "tool_execution_start": { + if (!context.turnState) return; + const itemId = RuntimeItemId.make(event.toolCallId); + const itemType = classifyToolItemType(event.toolName); + context.turnState.items.push({ + id: itemId, + type: itemType, + toolName: event.toolName, + }); + yield* offerRuntimeEvent({ + ...base, + turnId: context.turnState.turnId, + itemId, + type: "item.started", + payload: { + itemType, + title: event.toolName, + ...(event.args ? { detail: String(event.args) } : {}), + }, + }); + return; + } + + case "tool_execution_update": { + if (!context.turnState) return; + const itemId = RuntimeItemId.make(event.toolCallId); + const itemType = classifyToolItemType(event.toolName); + if (event.partialResult !== undefined) { + const partial = + typeof event.partialResult === "string" + ? event.partialResult + : String(event.partialResult); + yield* offerRuntimeEvent({ + ...base, + turnId: context.turnState.turnId, + itemId, + type: "content.delta", + payload: { + streamKind: itemType === "command_execution" ? "command_output" : "file_change_output", + delta: partial, + }, + }); + } + return; + } + + case "tool_execution_end": { + if (!context.turnState) return; + const itemId = RuntimeItemId.make(event.toolCallId); + const itemType = classifyToolItemType(event.toolName); + yield* offerRuntimeEvent({ + ...base, + turnId: context.turnState.turnId, + itemId, + type: "item.completed", + payload: { + itemType, + title: event.toolName, + status: event.isError ? "failed" : "completed", + }, + }); + return; + } + + case "turn_end": { + if (context.turnState) { + yield* completeTurn(context, "completed"); + } + return; + } + + case "agent_end": { + if (context.turnState) { + yield* completeTurn(context, event.willRetry ? "interrupted" : "completed"); + } + return; + } + + case "compaction_start": { + yield* offerRuntimeEvent({ + ...base, + type: "session.state.changed", + payload: { state: "waiting", reason: `compaction:${event.reason}` }, + }); + return; + } + + case "compaction_end": { + yield* offerRuntimeEvent({ + ...base, + type: "thread.state.changed", + payload: { state: "compacted" }, + }); + return; + } + + default: + return; + } + }); + + const stopSessionInternal = Effect.fn("stopSessionInternal")(function* ( + context: PiSessionContext, + options?: { readonly emitExitEvent?: boolean }, + ) { + if (context.stopped) return; + context.stopped = true; + + if (context.turnState) { + yield* completeTurn(context, "interrupted", "Session stopped."); + } + + if (context.unsubscribe) { + context.unsubscribe(); + context.unsubscribe = undefined; + } + + yield* Effect.sync(() => { + try { context.piSession.dispose(); } catch { /* best-effort cleanup */ } + }); + + const updatedAt = yield* nowIso; + context.session = { + ...context.session, + status: "closed", + activeTurnId: undefined, + updatedAt, + }; + + if (options?.emitExitEvent !== false) { + const stamp = yield* makeEventStamp(); + yield* offerRuntimeEvent({ + type: "session.exited", + eventId: stamp.eventId, + provider: PROVIDER, + createdAt: stamp.createdAt, + threadId: context.session.threadId, + payload: { + reason: "Session stopped", + exitKind: "graceful", + }, + providerRefs: {}, + }); + } + + sessions.delete(context.session.threadId); + }); + + const requireSession = ( + threadId: ThreadId, + ): Effect.Effect => { + const context = sessions.get(threadId); + if (!context) { + return Effect.fail( + new ProviderAdapterSessionNotFoundError({ + provider: PROVIDER, + threadId, + }), + ); + } + if (context.stopped || context.session.status === "closed") { + return Effect.fail( + new ProviderAdapterSessionClosedError({ + provider: PROVIDER, + threadId, + }), + ); + } + return Effect.succeed(context); + }; + + const startSession: PiAdapterShape["startSession"] = Effect.fn("startSession")(function* ( + input, + ) { + if (input.provider !== undefined && input.provider !== PROVIDER) { + return yield* new ProviderAdapterValidationError({ + provider: PROVIDER, + operation: "startSession", + issue: `Expected provider '${PROVIDER}' but received '${input.provider}'.`, + }); + } + + const existingContext = sessions.get(input.threadId); + if (existingContext) { + yield* stopSessionInternal(existingContext, { emitExitEvent: false }).pipe( + Effect.catchCause((cause) => + Effect.logWarning("pi.session.replace.stop-failed", { + threadId: input.threadId, + cause, + }), + ), + ); + } + + const startedAt = yield* nowIso; + const threadId = input.threadId; + const modelSelection = + input.modelSelection !== undefined && input.modelSelection.instanceId === boundInstanceId + ? input.modelSelection + : undefined; + + const runtimeContext = yield* Effect.context(); + const runFork = Effect.runForkWith(runtimeContext); + + const sessionOptions: CreateAgentSessionOptions = { + cwd: input.cwd ?? serverConfig.cwd, + ...(modelSelection?.model ? {} : {}), + }; + + const piSession = yield* Effect.tryPromise({ + try: async () => { + const result = await createAgentSession(sessionOptions); + return result.session; + }, + catch: (cause) => + new ProviderAdapterProcessError({ + provider: PROVIDER, + threadId, + detail: toMessage(cause, "Failed to start Pi Agent session."), + cause, + }), + }); + + const session: ProviderSession = { + threadId, + provider: PROVIDER, + providerInstanceId: boundInstanceId, + status: "ready", + runtimeMode: input.runtimeMode, + ...(input.cwd ? { cwd: input.cwd } : {}), + ...(modelSelection?.model ? { model: modelSelection.model } : {}), + createdAt: startedAt, + updatedAt: startedAt, + }; + + const context: PiSessionContext = { + session, + piSession, + unsubscribe: undefined, + streamFiber: undefined, + startedAt, + turnState: undefined, + turns: [], + stopped: false, + }; + sessions.set(threadId, context); + + const unsubscribe = piSession.subscribe((event: AgentSessionEvent) => { + if (context.stopped) return; + runFork(handlePiEvent(context, event)); + }); + context.unsubscribe = unsubscribe; + + const sessionStartedStamp = yield* makeEventStamp(); + yield* offerRuntimeEvent({ + type: "session.started", + eventId: sessionStartedStamp.eventId, + provider: PROVIDER, + createdAt: sessionStartedStamp.createdAt, + threadId, + payload: {}, + providerRefs: {}, + }); + + const configuredStamp = yield* makeEventStamp(); + yield* offerRuntimeEvent({ + type: "session.configured", + eventId: configuredStamp.eventId, + provider: PROVIDER, + createdAt: configuredStamp.createdAt, + threadId, + payload: { + config: { + ...(modelSelection?.model ? { model: modelSelection.model } : {}), + ...(input.cwd ? { cwd: input.cwd } : {}), + }, + }, + providerRefs: {}, + }); + + const readyStamp = yield* makeEventStamp(); + yield* offerRuntimeEvent({ + type: "session.state.changed", + eventId: readyStamp.eventId, + provider: PROVIDER, + createdAt: readyStamp.createdAt, + threadId, + payload: { state: "ready" }, + providerRefs: {}, + }); + + return { ...session }; + }); + + const sendTurn: PiAdapterShape["sendTurn"] = Effect.fn("sendTurn")(function* (input) { + const context = yield* requireSession(input.threadId); + + if (context.turnState) { + yield* completeTurn(context, "completed"); + } + + const turnId = TurnId.make(yield* Random.nextUUIDv4); + const turnStartedAt = yield* nowIso; + context.turnState = { turnId, startedAt: turnStartedAt, items: [] }; + context.session = { + ...context.session, + status: "running", + activeTurnId: turnId, + updatedAt: turnStartedAt, + }; + + const turnStartedStamp = yield* makeEventStamp(); + yield* offerRuntimeEvent({ + type: "turn.started", + eventId: turnStartedStamp.eventId, + provider: PROVIDER, + createdAt: turnStartedStamp.createdAt, + threadId: context.session.threadId, + turnId, + payload: {}, + providerRefs: {}, + }); + + const promptText = typeof input.input === "string" ? input.input : ""; + + yield* Effect.tryPromise({ + try: () => context.piSession.prompt(promptText), + catch: (cause) => + new ProviderAdapterRequestError({ + provider: PROVIDER, + method: "turn/prompt", + detail: toMessage(cause, "Failed to send prompt to Pi Agent."), + }), + }); + + return { + threadId: context.session.threadId, + turnId, + }; + }); + + const interruptTurn: PiAdapterShape["interruptTurn"] = Effect.fn("interruptTurn")( + function* (threadId, _turnId) { + const context = yield* requireSession(threadId); + yield* Effect.tryPromise({ + try: () => context.piSession.abort(), + catch: (cause) => + new ProviderAdapterRequestError({ + provider: PROVIDER, + method: "turn/interrupt", + detail: toMessage(cause, "Failed to interrupt Pi Agent turn."), + }), + }); + }, + ); + + const readThread: PiAdapterShape["readThread"] = Effect.fn("readThread")(function* (threadId) { + const context = yield* requireSession(threadId); + return { + threadId, + turns: context.turns.map((turn) => ({ + id: turn.id, + items: [...turn.items], + })), + }; + }); + + const rollbackThread: PiAdapterShape["rollbackThread"] = Effect.fn("rollbackThread")( + function* (threadId, numTurns) { + const context = yield* requireSession(threadId); + const nextLength = Math.max(0, context.turns.length - numTurns); + context.turns.splice(nextLength); + return { + threadId, + turns: context.turns.map((turn) => ({ + id: turn.id, + items: [...turn.items], + })), + }; + }, + ); + + const respondToRequest: PiAdapterShape["respondToRequest"] = ( + _threadId, + _requestId, + _decision, + ) => Effect.void; + + const respondToUserInput: PiAdapterShape["respondToUserInput"] = ( + _threadId, + _requestId, + _answers, + ) => Effect.void; + + const stopSession: PiAdapterShape["stopSession"] = Effect.fn("stopSession")( + function* (threadId) { + const context = yield* requireSession(threadId); + yield* stopSessionInternal(context, { emitExitEvent: true }); + }, + ); + + const listSessions: PiAdapterShape["listSessions"] = () => + Effect.sync(() => Array.from(sessions.values(), ({ session }) => ({ ...session }))); + + const hasSession: PiAdapterShape["hasSession"] = (threadId) => + Effect.sync(() => { + const context = sessions.get(threadId); + return context !== undefined && !context.stopped; + }); + + const stopAll: PiAdapterShape["stopAll"] = () => + Effect.forEach( + sessions, + ([, context]) => stopSessionInternal(context, { emitExitEvent: true }), + { discard: true }, + ); + + yield* Effect.addFinalizer(() => + Effect.forEach( + sessions, + ([, context]) => stopSessionInternal(context, { emitExitEvent: false }), + { discard: true }, + ).pipe(Effect.tap(() => Queue.shutdown(runtimeEventQueue))), + ); + + return { + provider: PROVIDER, + capabilities: { + sessionModelSwitch: "unsupported" as const, + }, + startSession, + sendTurn, + interruptTurn, + readThread, + rollbackThread, + respondToRequest, + respondToUserInput, + stopSession, + listSessions, + hasSession, + stopAll, + get streamEvents() { + return Stream.fromQueue(runtimeEventQueue); + }, + } satisfies PiAdapterShape; +}); diff --git a/apps/server/src/provider/Layers/PiProvider.ts b/apps/server/src/provider/Layers/PiProvider.ts new file mode 100644 index 00000000000..80cce875090 --- /dev/null +++ b/apps/server/src/provider/Layers/PiProvider.ts @@ -0,0 +1,271 @@ +import { + type PiSettings, + type ModelCapabilities, + type ServerProviderModel, + ProviderDriverKind, +} from "@t3tools/contracts"; +import * as DateTime from "effect/DateTime"; +import * as Effect from "effect/Effect"; +import * as Option from "effect/Option"; +import * as Path from "effect/Path"; +import * as Result from "effect/Result"; +import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"; +import { createModelCapabilities } from "@t3tools/shared/model"; + +import { + buildSelectOptionDescriptor, + buildServerProvider, + DEFAULT_TIMEOUT_MS, + detailFromResult, + isCommandMissingCause, + parseGenericCliVersion, + providerModelsFromSettings, + spawnAndCollect, + type ServerProviderDraft, +} from "../providerSnapshot.ts"; +import { makePiEnvironment } from "../Drivers/PiHome.ts"; + +const DEFAULT_PI_MODEL_CAPABILITIES: ModelCapabilities = createModelCapabilities({ + optionDescriptors: [], +}); + +const PROVIDER = ProviderDriverKind.make("piAgent"); +const PI_PRESENTATION = { + displayName: "Pi Agent", + showInteractionModeToggle: true, +} as const; + +const BUILT_IN_MODELS: ReadonlyArray = [ + { + slug: "claude-sonnet-4-6", + name: "Claude Sonnet 4.6", + isCustom: false, + capabilities: createModelCapabilities({ + optionDescriptors: [ + buildSelectOptionDescriptor({ + id: "thinking", + label: "Thinking", + options: [ + { value: "off", label: "Off" }, + { value: "low", label: "Low" }, + { value: "medium", label: "Medium", isDefault: true }, + { value: "high", label: "High" }, + { value: "xhigh", label: "Extra High" }, + ], + }), + ], + }), + }, + { + slug: "claude-opus-4-7", + name: "Claude Opus 4.7", + isCustom: false, + capabilities: createModelCapabilities({ + optionDescriptors: [ + buildSelectOptionDescriptor({ + id: "thinking", + label: "Thinking", + options: [ + { value: "off", label: "Off" }, + { value: "low", label: "Low" }, + { value: "medium", label: "Medium" }, + { value: "high", label: "High", isDefault: true }, + { value: "xhigh", label: "Extra High" }, + ], + }), + ], + }), + }, + { + slug: "claude-haiku-4-5", + name: "Claude Haiku 4.5", + isCustom: false, + capabilities: createModelCapabilities({ + optionDescriptors: [ + buildSelectOptionDescriptor({ + id: "thinking", + label: "Thinking", + options: [ + { value: "off", label: "Off" }, + { value: "low", label: "Low", isDefault: true }, + { value: "medium", label: "Medium" }, + { value: "high", label: "High" }, + ], + }), + ], + }), + }, +]; + +export function getPiModelCapabilities(model: string | null | undefined): ModelCapabilities { + const slug = model?.trim(); + return ( + BUILT_IN_MODELS.find((candidate) => candidate.slug === slug)?.capabilities ?? + DEFAULT_PI_MODEL_CAPABILITIES + ); +} + +const runPiCommand = Effect.fn("runPiCommand")(function* ( + piSettings: PiSettings, + args: ReadonlyArray, + environment: NodeJS.ProcessEnv = process.env, +) { + const piEnvironment = yield* makePiEnvironment(piSettings, environment); + const command = ChildProcess.make(piSettings.binaryPath || "pi", [...args], { + env: piEnvironment, + shell: process.platform === "win32", + }); + return yield* spawnAndCollect(piSettings.binaryPath || "pi", command); +}); + +const nowIso = Effect.map(DateTime.now, DateTime.formatIso); + +export const checkPiProviderStatus = Effect.fn("checkPiProviderStatus")(function* ( + piSettings: PiSettings, + environment: NodeJS.ProcessEnv = process.env, +): Effect.fn.Return< + ServerProviderDraft, + never, + ChildProcessSpawner.ChildProcessSpawner | Path.Path +> { + const checkedAt = DateTime.formatIso(yield* DateTime.now); + const allModels = providerModelsFromSettings( + BUILT_IN_MODELS, + PROVIDER, + piSettings.customModels, + DEFAULT_PI_MODEL_CAPABILITIES, + ); + + if (!piSettings.enabled) { + return buildServerProvider({ + presentation: PI_PRESENTATION, + enabled: false, + checkedAt, + models: allModels, + probe: { + installed: false, + version: null, + status: "warning", + auth: { status: "unknown" }, + message: "Pi Agent is disabled in T3 Code settings.", + }, + }); + } + + const versionProbe = yield* runPiCommand(piSettings, ["--version"], environment).pipe( + Effect.timeoutOption(DEFAULT_TIMEOUT_MS), + Effect.result, + ); + + if (Result.isFailure(versionProbe)) { + const error = versionProbe.failure; + return buildServerProvider({ + presentation: PI_PRESENTATION, + enabled: piSettings.enabled, + checkedAt, + models: allModels, + probe: { + installed: !isCommandMissingCause(error), + version: null, + status: "error", + auth: { status: "unknown" }, + message: isCommandMissingCause(error) + ? "Pi Agent CLI (`pi`) is not installed or not on PATH." + : `Failed to execute Pi Agent CLI health check: ${error instanceof Error ? error.message : String(error)}.`, + }, + }); + } + + if (Option.isNone(versionProbe.success)) { + return buildServerProvider({ + presentation: PI_PRESENTATION, + enabled: piSettings.enabled, + checkedAt, + models: allModels, + probe: { + installed: true, + version: null, + status: "error", + auth: { status: "unknown" }, + message: + "Pi Agent CLI is installed but failed to run. Timed out while running command.", + }, + }); + } + + const version = versionProbe.success.value; + const parsedVersion = parseGenericCliVersion(`${version.stdout}\n${version.stderr}`); + + if (version.code !== 0) { + const detail = detailFromResult(version); + return buildServerProvider({ + presentation: PI_PRESENTATION, + enabled: piSettings.enabled, + checkedAt, + models: allModels, + probe: { + installed: true, + version: parsedVersion, + status: "error", + auth: { status: "unknown" }, + message: detail ?? "Pi Agent CLI returned an error during health check.", + }, + }); + } + + return buildServerProvider({ + presentation: PI_PRESENTATION, + enabled: piSettings.enabled, + checkedAt, + models: allModels, + probe: { + installed: true, + version: parsedVersion, + status: "ready", + auth: { status: "authenticated" }, + }, + }); +}); + +export const makePendingPiProvider = ( + piSettings: PiSettings, +): Effect.Effect => + Effect.gen(function* () { + const checkedAt = yield* nowIso; + const models = providerModelsFromSettings( + BUILT_IN_MODELS, + PROVIDER, + piSettings.customModels, + DEFAULT_PI_MODEL_CAPABILITIES, + ); + + if (!piSettings.enabled) { + return buildServerProvider({ + presentation: PI_PRESENTATION, + enabled: false, + checkedAt, + models, + probe: { + installed: false, + version: null, + status: "warning", + auth: { status: "unknown" }, + message: "Pi Agent is disabled in T3 Code settings.", + }, + }); + } + + return buildServerProvider({ + presentation: PI_PRESENTATION, + enabled: true, + checkedAt, + models, + probe: { + installed: false, + version: null, + status: "warning", + auth: { status: "unknown" }, + message: "Pi Agent provider status has not been checked in this session yet.", + }, + }); + }); diff --git a/apps/server/src/provider/Layers/ProviderRegistry.test.ts b/apps/server/src/provider/Layers/ProviderRegistry.test.ts index fb6eb3b443d..af5ffb8e26b 100644 --- a/apps/server/src/provider/Layers/ProviderRegistry.test.ts +++ b/apps/server/src/provider/Layers/ProviderRegistry.test.ts @@ -993,6 +993,13 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsService.layerTest(), T claudeAgent: { enabled: false }, cursor: { enabled: false }, opencode: { enabled: false }, + piAgent: { + enabled: true, + binaryPath: "", + homePath: "", + customModels: [], + launchArgs: "", + }, }, // `providerInstances` keys are branded `ProviderInstanceId`; // the branded index signature rejects plain string literals @@ -1087,6 +1094,13 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsService.layerTest(), T claudeAgent: { enabled: false }, cursor: { enabled: false }, opencode: { enabled: false }, + piAgent: { + enabled: true, + binaryPath: "", + homePath: "", + customModels: [], + launchArgs: "", + }, }, }), ), @@ -1182,6 +1196,13 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsService.layerTest(), T claudeAgent: { enabled: false }, cursor: { enabled: false }, opencode: { enabled: false }, + piAgent: { + enabled: true, + binaryPath: "", + homePath: "", + customModels: [], + launchArgs: "", + }, }, providerInstances: { ghost_main: { @@ -1240,6 +1261,13 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsService.layerTest(), T cursor: { enabled: false, }, + piAgent: { + enabled: true, + binaryPath: "", + homePath: "", + customModels: [], + launchArgs: "", + }, }, }), ), @@ -1301,6 +1329,7 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsService.layerTest(), T "codex", "cursor", "opencode", + "piAgent", ]); assert.strictEqual(cursorProvider?.enabled, false); assert.strictEqual(cursorProvider?.status, "disabled"); diff --git a/apps/server/src/provider/Layers/ProviderService.test.ts b/apps/server/src/provider/Layers/ProviderService.test.ts index fc0450b8b69..b0e94b1a564 100644 --- a/apps/server/src/provider/Layers/ProviderService.test.ts +++ b/apps/server/src/provider/Layers/ProviderService.test.ts @@ -453,6 +453,13 @@ it.effect( codex: { enabled: false, }, + piAgent: { + enabled: true, + binaryPath: "", + homePath: "", + customModels: [], + launchArgs: "", + }, }, }); const runtimeRepositoryLayer = ProviderSessionRuntimeRepositoryLive.pipe( diff --git a/apps/server/src/provider/Services/PiAdapter.ts b/apps/server/src/provider/Services/PiAdapter.ts new file mode 100644 index 00000000000..97ae701f64b --- /dev/null +++ b/apps/server/src/provider/Services/PiAdapter.ts @@ -0,0 +1,4 @@ +import type { ProviderAdapterError } from "../Errors.ts"; +import type { ProviderAdapterShape } from "./ProviderAdapter.ts"; + +export interface PiAdapterShape extends ProviderAdapterShape {} diff --git a/apps/server/src/provider/builtInDrivers.ts b/apps/server/src/provider/builtInDrivers.ts index 5af56dc6b0e..0390e2c4f6b 100644 --- a/apps/server/src/provider/builtInDrivers.ts +++ b/apps/server/src/provider/builtInDrivers.ts @@ -24,6 +24,7 @@ import { ClaudeDriver, type ClaudeDriverEnv } from "./Drivers/ClaudeDriver.ts"; import { CodexDriver, type CodexDriverEnv } from "./Drivers/CodexDriver.ts"; import { CursorDriver, type CursorDriverEnv } from "./Drivers/CursorDriver.ts"; import { OpenCodeDriver, type OpenCodeDriverEnv } from "./Drivers/OpenCodeDriver.ts"; +import { PiDriver, type PiDriverEnv } from "./Drivers/PiDriver.ts"; import type { AnyProviderDriver } from "./ProviderDriver.ts"; /** @@ -35,7 +36,8 @@ export type BuiltInDriversEnv = | ClaudeDriverEnv | CodexDriverEnv | CursorDriverEnv - | OpenCodeDriverEnv; + | OpenCodeDriverEnv + | PiDriverEnv; /** * Ordered list of built-in drivers. Order matters only for tie-breaking in @@ -47,4 +49,5 @@ export const BUILT_IN_DRIVERS: ReadonlyArray( + operation: string, + stream: Stream.Stream, + ): Effect.Effect => + stream.pipe( + Stream.decodeText(), + Stream.runFold( + () => "", + (acc, chunk) => acc + chunk, + ), + Effect.mapError((cause) => + normalizeCliError("pi", operation, cause, "Failed to collect process output"), + ), + ); + + const runPiJson = Effect.fn("runPiJson")(function* ({ + operation, + cwd, + prompt, + outputSchemaJson, + modelSelection, + }: { + operation: + | "generateCommitMessage" + | "generatePrContent" + | "generateBranchName" + | "generateThreadTitle"; + cwd: string; + prompt: string; + outputSchemaJson: S; + modelSelection: ModelSelection; + }): Effect.fn.Return { + const runPiCommand = Effect.fn("runPiJson.runPiCommand")(function* () { + const command = ChildProcess.make( + piSettings.binaryPath || "pi", + [ + "--print", + "--output-format", + "json", + ...(modelSelection.model ? ["--model", modelSelection.model] : []), + ], + { + env: piEnvironment, + cwd, + shell: process.platform === "win32", + stdin: { + stream: Stream.encodeText( + // @effect-diagnostics-next-line preferSchemaOverJson:off + Stream.make( + `${prompt}\n\nRespond ONLY with valid JSON matching this schema:\n${JSON.stringify(toJsonSchemaObject(outputSchemaJson))}`, + ), + ), + }, + }, + ); + + const child = yield* commandSpawner + .spawn(command) + .pipe( + Effect.mapError((cause) => + normalizeCliError("pi", operation, cause, "Failed to spawn Pi Agent CLI process"), + ), + ); + + const [stdout, stderr, exitCode] = yield* Effect.all( + [ + readStreamAsString(operation, child.stdout), + readStreamAsString(operation, child.stderr), + child.exitCode.pipe( + Effect.mapError((cause) => + normalizeCliError("pi", operation, cause, "Failed to read Pi Agent CLI exit code"), + ), + ), + ], + { concurrency: "unbounded" }, + ); + + if (exitCode !== 0) { + const stderrDetail = stderr.trim(); + const stdoutDetail = stdout.trim(); + const detail = stderrDetail.length > 0 ? stderrDetail : stdoutDetail; + return yield* new TextGenerationError({ + operation, + detail: + detail.length > 0 + ? `Pi Agent CLI command failed: ${detail}` + : `Pi Agent CLI command failed with code ${exitCode}.`, + }); + } + + return stdout; + }); + + const rawStdout = yield* runPiCommand().pipe( + Effect.scoped, + Effect.timeoutOption(PI_TIMEOUT_MS), + Effect.flatMap( + Option.match({ + onNone: () => + Effect.fail( + new TextGenerationError({ operation, detail: "Pi Agent CLI request timed out." }), + ), + onSome: (value) => Effect.succeed(value), + }), + ), + ); + + const jsonMatch = rawStdout.match(/\{[\s\S]*\}/); + if (!jsonMatch) { + return yield* new TextGenerationError({ + operation, + detail: "Pi Agent CLI did not return valid JSON output.", + }); + } + + // @effect-diagnostics-next-line preferSchemaOverJson:off + const parsed = yield* Effect.try({ + try: () => JSON.parse(jsonMatch[0]), + catch: (cause) => + new TextGenerationError({ + operation, + detail: "Pi Agent CLI returned malformed JSON.", + cause, + }), + }); + + const decodeOutput = Schema.decodeEffect(outputSchemaJson); + return yield* decodeOutput(parsed).pipe( + Effect.catchTag("SchemaError", (cause) => + Effect.fail( + new TextGenerationError({ + operation, + detail: "Pi Agent returned invalid structured output.", + cause, + }), + ), + ), + ); + }); + + const generateCommitMessage: TextGenerationShape["generateCommitMessage"] = Effect.fn( + "PiTextGeneration.generateCommitMessage", + )(function* (input) { + const { prompt, outputSchema } = buildCommitMessagePrompt({ + branch: input.branch, + stagedSummary: input.stagedSummary, + stagedPatch: input.stagedPatch, + includeBranch: input.includeBranch === true, + }); + + const generated = yield* runPiJson({ + operation: "generateCommitMessage", + cwd: input.cwd, + prompt, + outputSchemaJson: outputSchema, + modelSelection: input.modelSelection, + }); + + return { + subject: sanitizeCommitSubject(generated.subject), + body: generated.body.trim(), + ...("branch" in generated && typeof generated.branch === "string" + ? { branch: sanitizeFeatureBranchName(generated.branch) } + : {}), + }; + }); + + const generatePrContent: TextGenerationShape["generatePrContent"] = Effect.fn( + "PiTextGeneration.generatePrContent", + )(function* (input) { + const { prompt, outputSchema } = buildPrContentPrompt({ + baseBranch: input.baseBranch, + headBranch: input.headBranch, + commitSummary: input.commitSummary, + diffSummary: input.diffSummary, + diffPatch: input.diffPatch, + }); + + const generated = yield* runPiJson({ + operation: "generatePrContent", + cwd: input.cwd, + prompt, + outputSchemaJson: outputSchema, + modelSelection: input.modelSelection, + }); + + return { + title: sanitizePrTitle(generated.title), + body: generated.body.trim(), + }; + }); + + const generateBranchName: TextGenerationShape["generateBranchName"] = Effect.fn( + "PiTextGeneration.generateBranchName", + )(function* (input) { + const { prompt, outputSchema } = buildBranchNamePrompt({ + message: input.message, + attachments: input.attachments, + }); + + const generated = yield* runPiJson({ + operation: "generateBranchName", + cwd: input.cwd, + prompt, + outputSchemaJson: outputSchema, + modelSelection: input.modelSelection, + }); + + return { + branch: sanitizeBranchFragment(generated.branch), + }; + }); + + const generateThreadTitle: TextGenerationShape["generateThreadTitle"] = Effect.fn( + "PiTextGeneration.generateThreadTitle", + )(function* (input) { + const { prompt, outputSchema } = buildThreadTitlePrompt({ + message: input.message, + attachments: input.attachments, + }); + + const generated = yield* runPiJson({ + operation: "generateThreadTitle", + cwd: input.cwd, + prompt, + outputSchemaJson: outputSchema, + modelSelection: input.modelSelection, + }); + + return { + title: sanitizeThreadTitle(generated.title), + }; + }); + + return { + generateCommitMessage, + generatePrContent, + generateBranchName, + generateThreadTitle, + } satisfies TextGenerationShape; +}); diff --git a/apps/server/src/textGeneration/TextGeneration.ts b/apps/server/src/textGeneration/TextGeneration.ts index 36a23d509db..3574ae796fe 100644 --- a/apps/server/src/textGeneration/TextGeneration.ts +++ b/apps/server/src/textGeneration/TextGeneration.ts @@ -10,7 +10,7 @@ import { } from "../provider/Services/ProviderInstanceRegistry.ts"; import type { ProviderInstance } from "../provider/ProviderDriver.ts"; -export type TextGenerationProvider = "codex" | "claudeAgent" | "cursor" | "opencode"; +export type TextGenerationProvider = "codex" | "claudeAgent" | "cursor" | "opencode" | "piAgent"; export interface CommitMessageGenerationInput { cwd: string; diff --git a/apps/web/src/components/KeybindingsToast.browser.tsx b/apps/web/src/components/KeybindingsToast.browser.tsx index 611eaf572d0..b8a1da91696 100644 --- a/apps/web/src/components/KeybindingsToast.browser.tsx +++ b/apps/web/src/components/KeybindingsToast.browser.tsx @@ -130,6 +130,13 @@ function createBaseServerConfig(): ServerConfig { serverPassword: "", customModels: [], }, + piAgent: { + enabled: true, + binaryPath: "", + homePath: "", + customModels: [], + launchArgs: "", + }, }, }, }; diff --git a/apps/web/src/components/chat/providerIconUtils.ts b/apps/web/src/components/chat/providerIconUtils.ts index 88b56295f36..5faba985b49 100644 --- a/apps/web/src/components/chat/providerIconUtils.ts +++ b/apps/web/src/components/chat/providerIconUtils.ts @@ -1,5 +1,5 @@ import { ProviderDriverKind } from "@t3tools/contracts"; -import { ClaudeAI, CursorIcon, Icon, OpenAI, OpenCodeIcon } from "../Icons"; +import { ClaudeAI, CursorIcon, Icon, OpenAI, OpenCodeIcon, PiAgentIcon } from "../Icons"; import { PROVIDER_OPTIONS } from "../../session-logic"; export const PROVIDER_ICON_BY_PROVIDER: Partial> = { @@ -7,6 +7,7 @@ export const PROVIDER_ICON_BY_PROVIDER: Partial [ProviderDriverKind.make("claudeAgent")]: ClaudeAI, [ProviderDriverKind.make("opencode")]: OpenCodeIcon, [ProviderDriverKind.make("cursor")]: CursorIcon, + [ProviderDriverKind.make("piAgent")]: PiAgentIcon, }; function isAvailableProviderOption(option: (typeof PROVIDER_OPTIONS)[number]): option is { diff --git a/bun.lock b/bun.lock index ffc4a5922bd..671983dd24f 100644 --- a/bun.lock +++ b/bun.lock @@ -17,7 +17,7 @@ }, "apps/desktop": { "name": "@t3tools/desktop", - "version": "0.0.23", + "version": "0.0.24", "dependencies": { "@effect/platform-node": "catalog:", "@t3tools/contracts": "workspace:*", @@ -50,12 +50,13 @@ }, "apps/server": { "name": "t3", - "version": "0.0.23", + "version": "0.0.24", "bin": { "t3": "./dist/bin.mjs", }, "dependencies": { "@anthropic-ai/claude-agent-sdk": "^0.2.111", + "@earendil-works/pi-coding-agent": "^0.75.4", "@effect/platform-bun": "catalog:", "@effect/platform-node": "catalog:", "@effect/platform-node-shared": "catalog:", @@ -83,7 +84,7 @@ }, "apps/web": { "name": "@t3tools/web", - "version": "0.0.23", + "version": "0.0.24", "dependencies": { "@base-ui/react": "^1.4.1", "@dnd-kit/core": "^6.3.1", @@ -165,7 +166,7 @@ }, "packages/contracts": { "name": "@t3tools/contracts", - "version": "0.0.23", + "version": "0.0.24", "dependencies": { "effect": "catalog:", }, @@ -322,6 +323,56 @@ "@astrojs/yaml2ts": ["@astrojs/yaml2ts@0.2.3", "", { "dependencies": { "yaml": "^2.8.2" } }, "sha512-PJzRmgQzUxI2uwpdX2lXSHtP4G8ocp24/t+bZyf5Fy0SZLSF9f9KXZoMlFM/XCGue+B0nH/2IZ7FpBYQATBsCg=="], + "@aws-crypto/crc32": ["@aws-crypto/crc32@5.2.0", "", { "dependencies": { "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "tslib": "^2.6.2" } }, "sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg=="], + + "@aws-crypto/sha256-browser": ["@aws-crypto/sha256-browser@5.2.0", "", { "dependencies": { "@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.222.0", "@aws-sdk/util-locate-window": "^3.0.0", "@smithy/util-utf8": "^2.0.0", "tslib": "^2.6.2" } }, "sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw=="], + + "@aws-crypto/sha256-js": ["@aws-crypto/sha256-js@5.2.0", "", { "dependencies": { "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "tslib": "^2.6.2" } }, "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA=="], + + "@aws-crypto/supports-web-crypto": ["@aws-crypto/supports-web-crypto@5.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg=="], + + "@aws-crypto/util": ["@aws-crypto/util@5.2.0", "", { "dependencies": { "@aws-sdk/types": "^3.222.0", "@smithy/util-utf8": "^2.0.0", "tslib": "^2.6.2" } }, "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ=="], + + "@aws-sdk/client-bedrock-runtime": ["@aws-sdk/client-bedrock-runtime@3.1048.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.974.11", "@aws-sdk/credential-provider-node": "^3.972.42", "@aws-sdk/eventstream-handler-node": "^3.972.16", "@aws-sdk/middleware-eventstream": "^3.972.12", "@aws-sdk/middleware-websocket": "^3.972.19", "@aws-sdk/token-providers": "3.1048.0", "@aws-sdk/types": "^3.973.8", "@smithy/core": "^3.24.2", "@smithy/fetch-http-handler": "^5.4.2", "@smithy/node-http-handler": "^4.7.2", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-u+NT61JZEkRFtpL0CAw1N1dwxnaLgwVXQl/zjJxTGgLyS/jTIdg2SdoEoCTHxgDyCnqa1HEi9QOoE9/pYRNpOQ=="], + + "@aws-sdk/core": ["@aws-sdk/core@3.974.12", "", { "dependencies": { "@aws-sdk/types": "^3.973.8", "@aws-sdk/xml-builder": "^3.972.24", "@aws/lambda-invoke-store": "^0.2.2", "@smithy/core": "^3.24.2", "@smithy/signature-v4": "^5.4.2", "@smithy/types": "^4.14.1", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-qrqgioqYFjwR6LatVNS1L2Vk++EwRIxqSQXPKNv5Ofux2D8UNgqMQ1znnMyEImXquVPTtbf71fc128pvmU6y9A=="], + + "@aws-sdk/credential-provider-env": ["@aws-sdk/credential-provider-env@3.972.38", "", { "dependencies": { "@aws-sdk/core": "^3.974.12", "@aws-sdk/types": "^3.973.8", "@smithy/core": "^3.24.2", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-m3WjZEgPtioMhPmwqUt+DhlTJ2i9ufR6DhfkyXojb9puEvfR+ur2U5shavu5/Cc9WHHsDCvALi6UFHgcqjhQ5w=="], + + "@aws-sdk/credential-provider-http": ["@aws-sdk/credential-provider-http@3.972.40", "", { "dependencies": { "@aws-sdk/core": "^3.974.12", "@aws-sdk/types": "^3.973.8", "@smithy/core": "^3.24.2", "@smithy/fetch-http-handler": "^5.4.2", "@smithy/node-http-handler": "^4.7.2", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-D78L/m2Dr6cJnnSvWoAudPhQmCwmJ7j6APXsPYmFpPaKfQTfCSu0rdm8j14Np+VmXF9z8Aj8HE3xFpsrwtfgeg=="], + + "@aws-sdk/credential-provider-ini": ["@aws-sdk/credential-provider-ini@3.972.42", "", { "dependencies": { "@aws-sdk/core": "^3.974.12", "@aws-sdk/credential-provider-env": "^3.972.38", "@aws-sdk/credential-provider-http": "^3.972.40", "@aws-sdk/credential-provider-login": "^3.972.42", "@aws-sdk/credential-provider-process": "^3.972.38", "@aws-sdk/credential-provider-sso": "^3.972.42", "@aws-sdk/credential-provider-web-identity": "^3.972.42", "@aws-sdk/nested-clients": "^3.997.10", "@aws-sdk/types": "^3.973.8", "@smithy/core": "^3.24.2", "@smithy/credential-provider-imds": "^4.3.2", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-Mu5ESvFXeinafVM8jTIvRqcvK2Ehj4kz3auT39yUcHwu1Vfxo6xRlmUafdKLW4tusjAJukQwK09sCSMgOm7OKg=="], + + "@aws-sdk/credential-provider-login": ["@aws-sdk/credential-provider-login@3.972.42", "", { "dependencies": { "@aws-sdk/core": "^3.974.12", "@aws-sdk/nested-clients": "^3.997.10", "@aws-sdk/types": "^3.973.8", "@smithy/core": "^3.24.2", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-O6WkZga3kf0yqyJYd1dbeJqVhEgJx/x1UaLgtbR+XuL/YP+K5y6QTxQKL7ka9z3jnQASESKGAPnRyt4D5hQrxA=="], + + "@aws-sdk/credential-provider-node": ["@aws-sdk/credential-provider-node@3.972.43", "", { "dependencies": { "@aws-sdk/credential-provider-env": "^3.972.38", "@aws-sdk/credential-provider-http": "^3.972.40", "@aws-sdk/credential-provider-ini": "^3.972.42", "@aws-sdk/credential-provider-process": "^3.972.38", "@aws-sdk/credential-provider-sso": "^3.972.42", "@aws-sdk/credential-provider-web-identity": "^3.972.42", "@aws-sdk/types": "^3.973.8", "@smithy/core": "^3.24.2", "@smithy/credential-provider-imds": "^4.3.2", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-D/DJmbrWRP5BXEO3FH+ar4el+2n6OlGofiud7dQun2jES+AQEJjczenp1jBb4MBN7CpGpS8nsWGQLtuzc9tQbA=="], + + "@aws-sdk/credential-provider-process": ["@aws-sdk/credential-provider-process@3.972.38", "", { "dependencies": { "@aws-sdk/core": "^3.974.12", "@aws-sdk/types": "^3.973.8", "@smithy/core": "^3.24.2", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-EnbYVajGgbkb24s0K1eo4VNAPV5mHIET7LSvirTaFCwkfrfaOJxtSE+wY/tJdKDS21cEYkZs2ruCaAm+W4iblg=="], + + "@aws-sdk/credential-provider-sso": ["@aws-sdk/credential-provider-sso@3.972.42", "", { "dependencies": { "@aws-sdk/core": "^3.974.12", "@aws-sdk/nested-clients": "^3.997.10", "@aws-sdk/token-providers": "3.1049.0", "@aws-sdk/types": "^3.973.8", "@smithy/core": "^3.24.2", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-RVV/9NbFwI8ZHEH5dn39lGyFmSbSVj1+orZdr6QsOe1mW9DCglmlen0cFaNZmCcqkqc7erNRHNBduxbeZuHAnw=="], + + "@aws-sdk/credential-provider-web-identity": ["@aws-sdk/credential-provider-web-identity@3.972.42", "", { "dependencies": { "@aws-sdk/core": "^3.974.12", "@aws-sdk/nested-clients": "^3.997.10", "@aws-sdk/types": "^3.973.8", "@smithy/core": "^3.24.2", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-/67fXX0ddllD4u2Nujc5PvT4byHgpMUfz6+RxIKi/0nFIckeorm7JvXgzBuDyVKw0s58EbofmETDWUf9vTEuHQ=="], + + "@aws-sdk/eventstream-handler-node": ["@aws-sdk/eventstream-handler-node@3.972.16", "", { "dependencies": { "@aws-sdk/types": "^3.973.8", "@smithy/core": "^3.24.2", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-yedpPgKftqjU5SlPFHfqWpOw6xSCRieWRG1euWOlXn4WJxt2VX92VprCa2PpSOXjVCAeK6dTjW9eJRXVig9yGA=="], + + "@aws-sdk/middleware-eventstream": ["@aws-sdk/middleware-eventstream@3.972.12", "", { "dependencies": { "@aws-sdk/types": "^3.973.8", "@smithy/core": "^3.24.2", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-tHTHHCHNrq6XklQvlzHBDJG4Iuhh7NVPRdtmvP+nHFA+5sxPlIDzlAHHgfoYHGvT3NXP1yVP/L5c3opUn6T3Qg=="], + + "@aws-sdk/middleware-websocket": ["@aws-sdk/middleware-websocket@3.972.20", "", { "dependencies": { "@aws-sdk/core": "^3.974.12", "@aws-sdk/types": "^3.973.8", "@smithy/core": "^3.24.2", "@smithy/fetch-http-handler": "^5.4.2", "@smithy/signature-v4": "^5.4.2", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-LM6P0i+Lu6pi25oNw2nqxjRxiEOtLgPB7xIvHfa+FxHTRLg8wcgqu3qg2COl4QaT7Es2yCxYdeRLVYazKAwL8g=="], + + "@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.997.10", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.974.12", "@aws-sdk/signature-v4-multi-region": "^3.996.27", "@aws-sdk/types": "^3.973.8", "@smithy/core": "^3.24.2", "@smithy/fetch-http-handler": "^5.4.2", "@smithy/node-http-handler": "^4.7.2", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-FtQ/Bt327peZJuyo4WZSOLVUTw9ujRxntepiC7L65FxA2P82Xlq0g14T22BuqBUeMjDoxa9nvwiMHjLIfP3eUg=="], + + "@aws-sdk/signature-v4-multi-region": ["@aws-sdk/signature-v4-multi-region@3.996.27", "", { "dependencies": { "@aws-sdk/types": "^3.973.8", "@smithy/core": "^3.24.2", "@smithy/signature-v4": "^5.4.2", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-0Phbz4t6HI3D3skxvG2uI+VWU034/nSIw1T8d+FPzzQG9EQTrw94o9mOKO2Gv3n3Oc8P7JD7RAUxkoneLWv5Eg=="], + + "@aws-sdk/token-providers": ["@aws-sdk/token-providers@3.1048.0", "", { "dependencies": { "@aws-sdk/core": "^3.974.11", "@aws-sdk/nested-clients": "^3.997.9", "@aws-sdk/types": "^3.973.8", "@smithy/core": "^3.24.2", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-k0y/GcuesuSfWyUM0WamrGyeZmltRYaPbHO82UDA6mZ/doB+FOHKutikPAtSXMn/hDz970cF+iRuuiYO9VEbAA=="], + + "@aws-sdk/types": ["@aws-sdk/types@3.973.8", "", { "dependencies": { "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-gjlAdtHMbtR9X5iIhVUvbVcy55KnznpC6bkDUWW9z915bi0ckdUr5cjf16Kp6xq0bP5HBD2xzgbL9F9Quv5vUw=="], + + "@aws-sdk/util-locate-window": ["@aws-sdk/util-locate-window@3.965.5", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-WhlJNNINQB+9qtLtZJcpQdgZw3SCDCpXdUJP7cToGwHbCWCnRckGlc6Bx/OhWwIYFNAn+FIydY8SZ0QmVu3xTQ=="], + + "@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.24", "", { "dependencies": { "@nodable/entities": "2.1.0", "@smithy/types": "^4.14.1", "fast-xml-parser": "5.7.3", "tslib": "^2.6.2" } }, "sha512-V8z5YcDPfsvzrBlj0xR1vhRtocblhYbqdreCJB/voGd4Sr5zjNAeWxexbnqVtskTJe0vFb5KMqbSL++ePl+zRw=="], + + "@aws/lambda-invoke-store": ["@aws/lambda-invoke-store@0.2.4", "", {}, "sha512-iY8yvjE0y651BixKNPgmv1WrQc+GZ142sb0z4gYnChDDY2YqI4P/jsSopBWrKfAt7LOJAkOXt7rC/hms+WclQQ=="], + "@babel/code-frame": ["@babel/code-frame@7.29.0", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="], "@babel/compat-data": ["@babel/compat-data@7.29.0", "", {}, "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg=="], @@ -384,6 +435,14 @@ "@dnd-kit/utilities": ["@dnd-kit/utilities@3.2.2", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "react": ">=16.8.0" } }, "sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg=="], + "@earendil-works/pi-agent-core": ["@earendil-works/pi-agent-core@0.75.4", "", { "dependencies": { "@earendil-works/pi-ai": "^0.75.4", "ignore": "7.0.5", "typebox": "1.1.38", "yaml": "2.9.0" } }, "sha512-cGYbysb4EqUf0B28OeqFq2ppm1XF3bYBOP71q9dv38yf/UJfzMjiXBeNelrcio+QWIoVrW+xzYm7sMzYIUc9Og=="], + + "@earendil-works/pi-ai": ["@earendil-works/pi-ai@0.75.4", "", { "dependencies": { "@anthropic-ai/sdk": "0.91.1", "@aws-sdk/client-bedrock-runtime": "3.1048.0", "@google/genai": "1.52.0", "@mistralai/mistralai": "2.2.1", "http-proxy-agent": "7.0.2", "https-proxy-agent": "7.0.6", "openai": "6.26.0", "partial-json": "0.1.7", "typebox": "1.1.38" }, "bin": { "pi-ai": "dist/cli.js" } }, "sha512-m/w8Hh3vQ0rAycwJiJWdzkypkn4295f4eq/966lDRy8aX5sk6bgYXH8TQmL16TO7Uwc7MbJG0QoyFHgX8RqXUQ=="], + + "@earendil-works/pi-coding-agent": ["@earendil-works/pi-coding-agent@0.75.4", "", { "dependencies": { "@earendil-works/pi-agent-core": "^0.75.4", "@earendil-works/pi-ai": "^0.75.4", "@earendil-works/pi-tui": "^0.75.4", "@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.6" }, "bin": { "pi": "dist/cli.js" } }, "sha512-Fb+FRo08b5H9pYKbQJ708/5OKL0+K/yclhfCMEhrBzSPTZZ4c85nY1YsBo4qwL20ohBMlBezHMRuHzcJ1ylEoQ=="], + + "@earendil-works/pi-tui": ["@earendil-works/pi-tui@0.75.4", "", { "dependencies": { "get-east-asian-width": "1.6.0", "marked": "15.0.12" }, "optionalDependencies": { "koffi": "2.16.2" } }, "sha512-PDhKU7u6fmEcvHUFHzrRwGc/Ytokj/hO+X4RPf+MWKEGpvg3B1vHv88Ee+Dy33004tYkQF5YeXV4btJZcp5x1g=="], + "@effect/atom-react": ["@effect/atom-react@4.0.0-beta.59", "", { "peerDependencies": { "effect": "^4.0.0-beta.59", "react": "^19.2.4", "scheduler": "*" } }, "sha512-VkznQz5c+Z/BLxX+hQNPzPOyUnLQjnbppFSNP7tbPru7HKR4ihzCDC6Xjbx87156MOrZ+JOa6shTMbmvGT5W0w=="], "@effect/language-service": ["@effect/language-service@0.84.2", "", { "bin": { "effect-language-service": "cli.js" } }, "sha512-l04qNxpiA8rY5yXWckRPJ7Mk5MNerXuNymSFf+IdflfI5i8jgL1bpBNLuP6ijg7wgjdHc/KmTnCj2kT0SCntuA=="], @@ -486,6 +545,8 @@ "@formkit/auto-animate": ["@formkit/auto-animate@0.9.0", "", {}, "sha512-VhP4zEAacXS3dfTpJpJ88QdLqMTcabMg0jwpOSxZ/VzfQVfl3GkZSCZThhGC5uhq/TxPHPzW0dzr4H9Bb1OgKA=="], + "@google/genai": ["@google/genai@1.52.0", "", { "dependencies": { "google-auth-library": "^10.3.0", "p-retry": "^4.6.2", "protobufjs": "^7.5.4", "ws": "^8.18.0" }, "peerDependencies": { "@modelcontextprotocol/sdk": "^1.25.2" }, "optionalPeers": ["@modelcontextprotocol/sdk"] }, "sha512-gwSvbpiN/17O9TbsqSsE/OzZcpv5Fo4RQjdngGgogtuB9RsyJ8ZHhX5KjHj1bp5N9snN2eK8LDGXSaWW2hof8Q=="], + "@hono/node-server": ["@hono/node-server@1.19.14", "", { "peerDependencies": { "hono": "^4" } }, "sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw=="], "@img/colour": ["@img/colour@1.1.0", "", {}, "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ=="], @@ -606,6 +667,30 @@ "@lexical/yjs": ["@lexical/yjs@0.41.0", "", { "dependencies": { "@lexical/offset": "0.41.0", "@lexical/selection": "0.41.0", "lexical": "0.41.0" }, "peerDependencies": { "yjs": ">=13.5.22" } }, "sha512-PaKTxSbVC4fpqUjQ7vUL9RkNF1PjL8TFl5jRe03PqoPYpE33buf3VXX6+cOUEfv9+uknSqLCPHoBS/4jN3a97w=="], + "@mariozechner/clipboard": ["@mariozechner/clipboard@0.3.6", "", { "optionalDependencies": { "@mariozechner/clipboard-darwin-arm64": "0.3.6", "@mariozechner/clipboard-darwin-universal": "0.3.6", "@mariozechner/clipboard-darwin-x64": "0.3.6", "@mariozechner/clipboard-linux-arm64-gnu": "0.3.6", "@mariozechner/clipboard-linux-arm64-musl": "0.3.6", "@mariozechner/clipboard-linux-riscv64-gnu": "0.3.6", "@mariozechner/clipboard-linux-x64-gnu": "0.3.6", "@mariozechner/clipboard-linux-x64-musl": "0.3.6", "@mariozechner/clipboard-win32-arm64-msvc": "0.3.6", "@mariozechner/clipboard-win32-x64-msvc": "0.3.6" } }, "sha512-MXdtr+6+ntlIVHdrZYuZNQydu6o8yZswFJ2Ln81j2O/Y9B/LDHvEaIm95xWNPkjGTWriSOeLnQJRFs6dYb60bg=="], + + "@mariozechner/clipboard-darwin-arm64": ["@mariozechner/clipboard-darwin-arm64@0.3.6", "", { "os": "darwin", "cpu": "arm64" }, "sha512-HjaisYCAbHi/1+N1yDAQHc8ZXGffufIUT5NSOSVR3f3AuMDusxTtnbK8tZ7JFDkShua1oNGZoNwQHsc8MPtE0Q=="], + + "@mariozechner/clipboard-darwin-universal": ["@mariozechner/clipboard-darwin-universal@0.3.6", "", { "os": "darwin" }, "sha512-8BWtPjOtJOJoykml3w0fx0zRrfWP31mXrJwfoA7xzNprkZw1uolCNfgmjDiVBseoKjp16EGITz7bN+61qn8dWA=="], + + "@mariozechner/clipboard-darwin-x64": ["@mariozechner/clipboard-darwin-x64@0.3.6", "", { "os": "darwin", "cpu": "x64" }, "sha512-p9syiZD1kU4I+1ya7f7g+zD1GiUvR8fdlRlNmgsZNWlyjtc8rlV2EjTLd/35x1LsdBq020GVvtzp0ZmPgBI09Q=="], + + "@mariozechner/clipboard-linux-arm64-gnu": ["@mariozechner/clipboard-linux-arm64-gnu@0.3.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-5JFf5rGofrm+V29HNF+wLthXphHdQpMbKDUYJ5tML6/Z5DLlLOV/9Ak4kDPtYyZ+Dzf+kAusE0VsFg4+tfP1IA=="], + + "@mariozechner/clipboard-linux-arm64-musl": ["@mariozechner/clipboard-linux-arm64-musl@0.3.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-JlVjxxw0GbGC0djXYWRIqyteO3J1KZ/QG3udlEFaOD5TLOM1FnmXXAPDQBqr+aBVr720ef9K00dirYnJ0LDCtw=="], + + "@mariozechner/clipboard-linux-riscv64-gnu": ["@mariozechner/clipboard-linux-riscv64-gnu@0.3.6", "", { "os": "linux", "cpu": "none" }, "sha512-4t8BUi5zZ+L77otFQVnVSlaTyAX4TVk9EqQm4syMrEQp96trFEHEwwNHcNEBGzYv5+K7mxay50TthYkz47OWzQ=="], + + "@mariozechner/clipboard-linux-x64-gnu": ["@mariozechner/clipboard-linux-x64-gnu@0.3.6", "", { "os": "linux", "cpu": "x64" }, "sha512-trtPwcNLW37irwQCJLtCxLw757jjJZk3TSnY/MU9bhtWtA3K9b/eLW0e4RGhUXDoFRds9opNWWaUDuFLa8dm0w=="], + + "@mariozechner/clipboard-linux-x64-musl": ["@mariozechner/clipboard-linux-x64-musl@0.3.6", "", { "os": "linux", "cpu": "x64" }, "sha512-WfnzIvOCCWQiN0MmltCEo6cLceUDbYe+I7xyFZjaps5A+2Op/M2CY7Rey+C4ucQhrvmpoHmTSFgY9ODWk7snoA=="], + + "@mariozechner/clipboard-win32-arm64-msvc": ["@mariozechner/clipboard-win32-arm64-msvc@0.3.6", "", { "os": "win32", "cpu": "arm64" }, "sha512-+8+1aHYsBPUjmW3otmWlg+Hijt0iJvoBBs5e0mxFeUd4gDaKMB8Bn6x7c6KVtscg7E5j5NFXnwQqNSIAO4p8zQ=="], + + "@mariozechner/clipboard-win32-x64-msvc": ["@mariozechner/clipboard-win32-x64-msvc@0.3.6", "", { "os": "win32", "cpu": "x64" }, "sha512-S4xfPmERC8ZkiLHe3vekZCjdDwNEETCuvCgQK2kP6/TnvmUkq1y2Pk+DjM4t8uh9KMX9bH4zs5ePcKa8GTXmfg=="], + + "@mistralai/mistralai": ["@mistralai/mistralai@2.2.1", "", { "dependencies": { "ws": "^8.18.0", "zod": "^3.25.0 || ^4.0.0", "zod-to-json-schema": "^3.25.0" } }, "sha512-uKU8CZmL2RzYKmplsU01hii4p3pe4HqJefpWNRWXm1Tcm0Sm4xXfwSLIy4k7ZCPlbETCGcp69E7hZs+WOJ5itQ=="], + "@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.29.0", "", { "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.2.1", "express-rate-limit": "^8.2.1", "hono": "^4.11.4", "jose": "^6.1.3", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.1" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ=="], "@msgpackr-extract/msgpackr-extract-darwin-arm64": ["@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw=="], @@ -624,6 +709,8 @@ "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.1", "", { "dependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1", "@tybys/wasm-util": "^0.10.1" } }, "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A=="], + "@nodable/entities": ["@nodable/entities@2.1.0", "", {}, "sha512-nyT7T3nbMyBI/lvr6L5TyWbFJAI9FTgVRakNoBqCD+PmID8DzFrrNdLLtHMwMszOtqZa8PAOV24ZqDnQrhQINA=="], + "@open-draft/deferred-promise": ["@open-draft/deferred-promise@2.2.0", "", {}, "sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA=="], "@open-draft/logger": ["@open-draft/logger@0.3.0", "", { "dependencies": { "is-node-process": "^1.2.0", "outvariant": "^1.4.0" } }, "sha512-X2g45fzhxH238HKO4xbSr7+wBS8Fvw6ixhTDuvLd5mqh6bJJCFAPwU9mPDxbcrRtfxv4u5IHCEH77BmxvXmmxQ=="], @@ -724,6 +811,26 @@ "@preact/signals-core": ["@preact/signals-core@1.14.0", "", {}, "sha512-AowtCcCU/33lFlh1zRFf/u+12rfrhtNakj7UpaGEsmMwUKpKWMVvcktOGcwBBNiB4lWrZWc01LhiyyzVklJyaQ=="], + "@protobufjs/aspromise": ["@protobufjs/aspromise@1.1.2", "", {}, "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ=="], + + "@protobufjs/base64": ["@protobufjs/base64@1.1.2", "", {}, "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg=="], + + "@protobufjs/codegen": ["@protobufjs/codegen@2.0.5", "", {}, "sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g=="], + + "@protobufjs/eventemitter": ["@protobufjs/eventemitter@1.1.0", "", {}, "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q=="], + + "@protobufjs/fetch": ["@protobufjs/fetch@1.1.1", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.1" } }, "sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw=="], + + "@protobufjs/float": ["@protobufjs/float@1.0.2", "", {}, "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ=="], + + "@protobufjs/inquire": ["@protobufjs/inquire@1.1.2", "", {}, "sha512-pa0vFRuws4wkvaXKK1uXZMAwAX4/t8ANaJo45iw/oQHNQ9q5xUzwgFmVJGXiga2BeN+zpX7Vf9vmsiIa2J+MUw=="], + + "@protobufjs/path": ["@protobufjs/path@1.1.2", "", {}, "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA=="], + + "@protobufjs/pool": ["@protobufjs/pool@1.1.0", "", {}, "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw=="], + + "@protobufjs/utf8": ["@protobufjs/utf8@1.1.1", "", {}, "sha512-oOAWABowe8EAbMyWKM0tYDKi8Yaox52D+HWZhAIJqQXbqe0xI/GV7FhLWqlEKreMkfDjshR5FKgi3mnle0h6Eg=="], + "@quansync/fs": ["@quansync/fs@1.0.0", "", { "dependencies": { "quansync": "^1.0.0" } }, "sha512-4TJ3DFtlf1L5LDMaM6CanJ/0lckGNtJcMjQ1NAV6zDmA0tEHKZtxNKin8EgPaVX1YzljbxckyT2tJrpQKAtngQ=="], "@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.0.0-rc.3", "", { "os": "android", "cpu": "arm64" }, "sha512-0T1k9FinuBZ/t7rZ8jN6OpUKPnUjNdYHoj/cESWrQ3ZraAJ4OMm6z7QjSfCxqj8mOp9kTKc1zHK3kGz5vMu+nQ=="], @@ -780,8 +887,28 @@ "@shikijs/vscode-textmate": ["@shikijs/vscode-textmate@10.0.2", "", {}, "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg=="], + "@silvia-odwyer/photon-node": ["@silvia-odwyer/photon-node@0.3.4", "", {}, "sha512-bnly4BKB3KDTFxrUIcgCLbaeVVS8lrAkri1pEzskpmxu9MdfGQTy8b8EgcD83ywD3RPMsIulY8xJH5Awa+t9fA=="], + "@sindresorhus/is": ["@sindresorhus/is@4.6.0", "", {}, "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw=="], + "@smithy/core": ["@smithy/core@3.24.3", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-Ep/7tPamGY8mgESE3LyLKtxJyy6U52WWAqr/3wial47Sj4u3PiIF73AOGI27UyLy9duTkhZbgzodOfLV4TduZg=="], + + "@smithy/credential-provider-imds": ["@smithy/credential-provider-imds@4.3.3", "", { "dependencies": { "@smithy/core": "^3.24.3", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-I2Bti0DKFo2IJyN28ijCsx51BAumEYR4/1yZ1FXyBygy9MqbnMqCev4JPth/MbpRfBSRAX35hITSnAdJRo1u5w=="], + + "@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.4.3", "", { "dependencies": { "@smithy/core": "^3.24.3", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-F+DRf8IJazRJgYog2A/yJK7eYVc0rqTlRzO+5ZxjJd4WkZoKz0IJRncf7G6t1pdVT3kryJcwuTFhN1c5m6N47A=="], + + "@smithy/is-array-buffer": ["@smithy/is-array-buffer@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA=="], + + "@smithy/node-http-handler": ["@smithy/node-http-handler@4.7.3", "", { "dependencies": { "@smithy/core": "^3.24.3", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-/jPhevcTFPMVl6KNjbaI47iOg1zxC7IsnX4PQDGVZKMFceOXtB8IEYaB7a9VvkP/3oC60WzTeKocvSI7vLT0vA=="], + + "@smithy/signature-v4": ["@smithy/signature-v4@5.4.3", "", { "dependencies": { "@smithy/core": "^3.24.3", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-53+75QuPl6DL+ct6vVEB51FDO5oulXr20TPV46VvJZg76lIlXNWfxi8j+G2V/t0I2qxCBOa3vX/8bmjrpFVo9g=="], + + "@smithy/types": ["@smithy/types@4.14.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-P+otAxbV4CqBybp7EkcJCrig63yE2E7PuNVOmilVMRcx/O+QDzGULTrKsq4DV13gSfak9ObPrWaHl/9bL5YcWw=="], + + "@smithy/util-buffer-from": ["@smithy/util-buffer-from@2.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA=="], + + "@smithy/util-utf8": ["@smithy/util-utf8@2.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A=="], + "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], "@szmarczak/http-timer": ["@szmarczak/http-timer@4.0.6", "", { "dependencies": { "defer-to-connect": "^2.0.0" } }, "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w=="], @@ -910,6 +1037,8 @@ "@types/responselike": ["@types/responselike@1.0.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw=="], + "@types/retry": ["@types/retry@0.12.0", "", {}, "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA=="], + "@types/statuses": ["@types/statuses@2.0.6", "", {}, "sha512-xMAgYwceFhRA2zY+XbEA7mxYbA093wdiW8Vu6gZPGWy9cmOyU9XesH1tNcEWsKFd5Vzrqx5T3D38PWx1FIIXkA=="], "@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="], @@ -972,6 +1101,8 @@ "acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="], + "agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="], + "ajv": ["ajv@8.18.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A=="], "ajv-draft-04": ["ajv-draft-04@1.0.0", "", { "peerDependencies": { "ajv": "^8.5.0" }, "optionalPeers": ["ajv"] }, "sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw=="], @@ -1008,8 +1139,14 @@ "bail": ["bail@2.0.2", "", {}, "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw=="], + "balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], + + "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], + "baseline-browser-mapping": ["baseline-browser-mapping@2.10.8", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-PCLz/LXGBsNTErbtB6i5u4eLpHeMfi93aUv5duMmj6caNu6IphS4q6UevDnL36sZQv9lrP11dbPKGMaXPwMKfQ=="], + "bignumber.js": ["bignumber.js@9.3.1", "", {}, "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ=="], + "binary-extensions": ["binary-extensions@2.3.0", "", {}, "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw=="], "birpc": ["birpc@4.0.0", "", {}, "sha512-LShSxJP0KTmd101b6DRyGBj57LZxSDYWKitQNW/mi8GRMvZb078Uf9+pveax1DrVL89vm7mWe+TovdI/UDOuPw=="], @@ -1020,12 +1157,18 @@ "boolean": ["boolean@3.2.0", "", {}, "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw=="], + "bowser": ["bowser@2.14.1", "", {}, "sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg=="], + + "brace-expansion": ["brace-expansion@5.0.6", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g=="], + "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], "browserslist": ["browserslist@4.28.1", "", { "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", "electron-to-chromium": "^1.5.263", "node-releases": "^2.0.27", "update-browserslist-db": "^1.2.0" }, "bin": { "browserslist": "cli.js" } }, "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA=="], "buffer-crc32": ["buffer-crc32@0.2.13", "", {}, "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ=="], + "buffer-equal-constant-time": ["buffer-equal-constant-time@1.0.1", "", {}, "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="], + "builder-util-runtime": ["builder-util-runtime@9.5.1", "", { "dependencies": { "debug": "^4.3.4", "sax": "^1.2.4" } }, "sha512-qt41tMfgHTllhResqM5DcnHyDIWNgzHvuY2jDcYP9iaGpkWxTUzV6GQjDeLnlR1/DtdlcsWQbA7sByMpmJFTLQ=="], "bun-types": ["bun-types@1.3.11", "", { "dependencies": { "@types/node": "*" } }, "sha512-1KGPpoxQWl9f6wcZh57LvrPIInQMn2TQ7jsgxqpRzg+l0QPOFvJVH7HmvHo/AiPgwXy+/Thf6Ov3EdVn1vOabg=="], @@ -1048,6 +1191,8 @@ "chai": ["chai@6.2.2", "", {}, "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg=="], + "chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="], + "character-entities": ["character-entities@2.0.2", "", {}, "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ=="], "character-entities-html4": ["character-entities-html4@2.1.0", "", {}, "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA=="], @@ -1110,6 +1255,8 @@ "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], + "data-uri-to-buffer": ["data-uri-to-buffer@4.0.1", "", {}, "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A=="], + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], "decode-named-character-reference": ["decode-named-character-reference@1.3.0", "", { "dependencies": { "character-entities": "^2.0.0" } }, "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q=="], @@ -1158,6 +1305,8 @@ "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], + "ecdsa-sig-formatter": ["ecdsa-sig-formatter@1.0.11", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ=="], + "ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="], "effect": ["effect@4.0.0-beta.59", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "fast-check": "^4.6.0", "find-my-way-ts": "^0.1.6", "ini": "^6.0.0", "kubernetes-types": "^1.30.0", "msgpackr": "^1.11.9", "multipasta": "^0.2.7", "toml": "^4.1.1", "uuid": "^13.0.0", "yaml": "^2.8.3" } }, "sha512-xyUDLeHSe8d6lWGOvR6Fgn2HL6gYeTZ/S4Jzk9uc4ZUxMPPsNZlNXrvk0C7/utQFzeX7uAWcVnG2BjbA0SRoAA=="], @@ -1238,10 +1387,16 @@ "fast-uri": ["fast-uri@3.1.0", "", {}, "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA=="], + "fast-xml-builder": ["fast-xml-builder@1.2.0", "", { "dependencies": { "path-expression-matcher": "^1.5.0", "xml-naming": "^0.1.0" } }, "sha512-00aAWieqff+ZJhsXA4g1g7M8k+7AYoMUUHF+/zFb5U6Uv/P0Vl4QZo84/IcufzYalLuEj9928bXN9PbbFzMF0Q=="], + + "fast-xml-parser": ["fast-xml-parser@5.7.3", "", { "dependencies": { "@nodable/entities": "^2.1.0", "fast-xml-builder": "^1.1.7", "path-expression-matcher": "^1.5.0", "strnum": "^2.2.3" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-C0AaNuC+mscy6vrAQKAc/rMq+zAPHodfHGZu4sGVehvAQt/JLG1O5zEcYcXSY5zSqr4YVgxsB+pHXTq0i7eDlg=="], + "fd-slicer": ["fd-slicer@1.1.0", "", { "dependencies": { "pend": "~1.2.0" } }, "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g=="], "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], + "fetch-blob": ["fetch-blob@3.2.0", "", { "dependencies": { "node-domexception": "^1.0.0", "web-streams-polyfill": "^3.0.3" } }, "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ=="], + "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], "finalhandler": ["finalhandler@2.1.1", "", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA=="], @@ -1254,6 +1409,8 @@ "fontkitten": ["fontkitten@1.0.3", "", { "dependencies": { "tiny-inflate": "^1.0.3" } }, "sha512-Wp1zXWPVUPBmfoa3Cqc9ctaKuzKAV6uLstRqlR56kSjplf5uAce+qeyYym7F+PHbGTk+tCEdkCW6RD7DX/gBZw=="], + "formdata-polyfill": ["formdata-polyfill@4.0.10", "", { "dependencies": { "fetch-blob": "^3.1.2" } }, "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g=="], + "forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="], "fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="], @@ -1264,10 +1421,16 @@ "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], + "gaxios": ["gaxios@7.1.4", "", { "dependencies": { "extend": "^3.0.2", "https-proxy-agent": "^7.0.1", "node-fetch": "^3.3.2" } }, "sha512-bTIgTsM2bWn3XklZISBTQX7ZSddGW+IO3bMdGaemHZ3tbqExMENHLx6kKZ/KlejgrMtj8q7wBItt51yegqalrA=="], + + "gcp-metadata": ["gcp-metadata@8.1.2", "", { "dependencies": { "gaxios": "^7.0.0", "google-logging-utils": "^1.0.0", "json-bigint": "^1.0.0" } }, "sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg=="], + "gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="], "get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="], + "get-east-asian-width": ["get-east-asian-width@1.6.0", "", {}, "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA=="], + "get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="], "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], @@ -1278,12 +1441,18 @@ "github-slugger": ["github-slugger@2.0.0", "", {}, "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw=="], + "glob": ["glob@13.0.6", "", { "dependencies": { "minimatch": "^10.2.2", "minipass": "^7.1.3", "path-scurry": "^2.0.2" } }, "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw=="], + "glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], "global-agent": ["global-agent@3.0.0", "", { "dependencies": { "boolean": "^3.0.1", "es6-error": "^4.1.1", "matcher": "^3.0.0", "roarr": "^2.15.3", "semver": "^7.3.2", "serialize-error": "^7.0.1" } }, "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q=="], "globalthis": ["globalthis@1.0.4", "", { "dependencies": { "define-properties": "^1.2.1", "gopd": "^1.0.1" } }, "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ=="], + "google-auth-library": ["google-auth-library@10.6.2", "", { "dependencies": { "base64-js": "^1.3.0", "ecdsa-sig-formatter": "^1.0.11", "gaxios": "^7.1.4", "gcp-metadata": "8.1.2", "google-logging-utils": "1.1.3", "jws": "^4.0.0" } }, "sha512-e27Z6EThmVNNvtYASwQxose/G57rkRuaRbQyxM2bvYLLX/GqWZ5chWq2EBoUchJbCc57eC9ArzO5wMsEmWftCw=="], + + "google-logging-utils": ["google-logging-utils@1.1.3", "", {}, "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA=="], + "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], "got": ["got@11.8.6", "", { "dependencies": { "@sindresorhus/is": "^4.0.0", "@szmarczak/http-timer": "^4.0.5", "@types/cacheable-request": "^6.0.1", "@types/responselike": "^1.0.0", "cacheable-lookup": "^5.0.3", "cacheable-request": "^7.0.2", "decompress-response": "^6.0.0", "http2-wrapper": "^1.0.0-beta.5.2", "lowercase-keys": "^2.0.0", "p-cancelable": "^2.0.0", "responselike": "^2.0.0" } }, "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g=="], @@ -1324,10 +1493,14 @@ "headers-polyfill": ["headers-polyfill@4.0.3", "", {}, "sha512-IScLbePpkvO846sIwOtOTDjutRMWdXdJmXdMvk6gCBHxFO8d+QKOQedyZSxFTTFYRSmlgSTDtXqqq4pcenBXLQ=="], + "highlight.js": ["highlight.js@10.7.3", "", {}, "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A=="], + "hono": ["hono@4.12.14", "", {}, "sha512-am5zfg3yu6sqn5yjKBNqhnTX7Cv+m00ox+7jbaKkrLMRJ4rAdldd1xPd/JzbBWspqaQv6RSTrgFN95EsfhC+7w=="], "hookable": ["hookable@6.1.0", "", {}, "sha512-ZoKZSJgu8voGK2geJS+6YtYjvIzu9AOM/KZXsBxr83uhLL++e9pEv/dlgwgy3dvHg06kTz6JOh1hk3C8Ceiymw=="], + "hosted-git-info": ["hosted-git-info@9.0.3", "", { "dependencies": { "lru-cache": "^11.1.0" } }, "sha512-Hc+ghLoSt6QaYZUv0WBiIvmMDZuZZ7oaDvdH8MbfOO4lOsxdXLEvuC6ePoGs9H1X9oCLyq6+NVN0MKqD+ydxyg=="], + "html-escaper": ["html-escaper@3.0.3", "", {}, "sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ=="], "html-url-attributes": ["html-url-attributes@3.0.1", "", {}, "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ=="], @@ -1338,10 +1511,16 @@ "http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="], + "http-proxy-agent": ["http-proxy-agent@7.0.2", "", { "dependencies": { "agent-base": "^7.1.0", "debug": "^4.3.4" } }, "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig=="], + "http2-wrapper": ["http2-wrapper@1.0.3", "", { "dependencies": { "quick-lru": "^5.1.1", "resolve-alpn": "^1.0.0" } }, "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg=="], + "https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="], + "iconv-lite": ["iconv-lite@0.7.2", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="], + "ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="], + "import-without-cache": ["import-without-cache@0.2.5", "", {}, "sha512-B6Lc2s6yApwnD2/pMzFh/d5AVjdsDXjgkeJ766FmFuJELIGHNycKRj+l3A39yZPM4CchqNCB4RITEAYB1KUM6A=="], "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], @@ -1404,6 +1583,8 @@ "jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="], + "json-bigint": ["json-bigint@1.0.0", "", { "dependencies": { "bignumber.js": "^9.0.0" } }, "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ=="], + "json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="], "json-schema-to-ts": ["json-schema-to-ts@3.1.1", "", { "dependencies": { "@babel/runtime": "^7.18.3", "ts-algebra": "^2.0.0" } }, "sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g=="], @@ -1420,10 +1601,16 @@ "jsonfile": ["jsonfile@6.2.0", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg=="], + "jwa": ["jwa@2.0.1", "", { "dependencies": { "buffer-equal-constant-time": "^1.0.1", "ecdsa-sig-formatter": "1.0.11", "safe-buffer": "^5.0.1" } }, "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg=="], + + "jws": ["jws@4.0.1", "", { "dependencies": { "jwa": "^2.0.1", "safe-buffer": "^5.0.1" } }, "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA=="], + "keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="], "kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="], + "koffi": ["koffi@2.16.2", "", {}, "sha512-owU0MRwv6xkrVqCd+33uw6BaYppkTRXbO/rVdJNI2dvZG0gzyRhYwW25eWtc5pauwK8TGh3AbkFONSezdykfSA=="], + "kubernetes-types": ["kubernetes-types@1.30.0", "", {}, "sha512-Dew1okvhM/SQcIa2rcgujNndZwU8VnSapDgdxlYoB84ZlpAD43U6KLAFqYo17ykSFGHNPrg0qry0bP+GJd9v7Q=="], "lazy-val": ["lazy-val@1.0.5", "", {}, "sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q=="], @@ -1464,6 +1651,8 @@ "lodash.isequal": ["lodash.isequal@4.5.0", "", {}, "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ=="], + "long": ["long@5.3.2", "", {}, "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="], + "longest-streak": ["longest-streak@3.1.0", "", {}, "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g=="], "lowercase-keys": ["lowercase-keys@2.0.0", "", {}, "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA=="], @@ -1480,6 +1669,8 @@ "markdown-table": ["markdown-table@3.0.4", "", {}, "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw=="], + "marked": ["marked@15.0.12", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA=="], + "matcher": ["matcher@3.0.0", "", { "dependencies": { "escape-string-regexp": "^4.0.0" } }, "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng=="], "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], @@ -1586,6 +1777,10 @@ "mimic-response": ["mimic-response@3.1.0", "", {}, "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ=="], + "minimatch": ["minimatch@10.2.5", "", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="], + + "minipass": ["minipass@7.1.3", "", {}, "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A=="], + "mrmime": ["mrmime@2.0.1", "", {}, "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ=="], "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], @@ -1612,6 +1807,10 @@ "node-addon-api": ["node-addon-api@7.1.1", "", {}, "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ=="], + "node-domexception": ["node-domexception@1.0.0", "", {}, "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ=="], + + "node-fetch": ["node-fetch@3.3.2", "", { "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", "formdata-polyfill": "^4.0.10" } }, "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA=="], + "node-fetch-native": ["node-fetch-native@1.6.7", "", {}, "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q=="], "node-gyp-build-optional-packages": ["node-gyp-build-optional-packages@5.2.2", "", { "dependencies": { "detect-libc": "^2.0.1" }, "bin": { "node-gyp-build-optional-packages": "bin.js", "node-gyp-build-optional-packages-optional": "optional.js", "node-gyp-build-optional-packages-test": "build-test.js" } }, "sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw=="], @@ -1648,6 +1847,8 @@ "oniguruma-to-es": ["oniguruma-to-es@4.3.5", "", { "dependencies": { "oniguruma-parser": "^0.12.1", "regex": "^6.1.0", "regex-recursion": "^6.0.2" } }, "sha512-Zjygswjpsewa0NLTsiizVuMQZbp0MDyM6lIt66OxsF21npUDlzpHi1Mgb/qhQdkb+dWFTzJmFbEWdvZgRho8eQ=="], + "openai": ["openai@6.26.0", "", { "peerDependencies": { "ws": "^8.18.0", "zod": "^3.25 || ^4.0" }, "optionalPeers": ["ws", "zod"], "bin": { "openai": "bin/cli" } }, "sha512-zd23dbWTjiJ6sSAX6s0HrCZi41JwTA1bQVs0wLQPZ2/5o2gxOJA5wh7yOAUgwYybfhDXyhwlpeQf7Mlgx8EOCA=="], + "outvariant": ["outvariant@1.4.3", "", {}, "sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA=="], "oxfmt": ["oxfmt@0.40.0", "", { "dependencies": { "tinypool": "2.1.0" }, "optionalDependencies": { "@oxfmt/binding-android-arm-eabi": "0.40.0", "@oxfmt/binding-android-arm64": "0.40.0", "@oxfmt/binding-darwin-arm64": "0.40.0", "@oxfmt/binding-darwin-x64": "0.40.0", "@oxfmt/binding-freebsd-x64": "0.40.0", "@oxfmt/binding-linux-arm-gnueabihf": "0.40.0", "@oxfmt/binding-linux-arm-musleabihf": "0.40.0", "@oxfmt/binding-linux-arm64-gnu": "0.40.0", "@oxfmt/binding-linux-arm64-musl": "0.40.0", "@oxfmt/binding-linux-ppc64-gnu": "0.40.0", "@oxfmt/binding-linux-riscv64-gnu": "0.40.0", "@oxfmt/binding-linux-riscv64-musl": "0.40.0", "@oxfmt/binding-linux-s390x-gnu": "0.40.0", "@oxfmt/binding-linux-x64-gnu": "0.40.0", "@oxfmt/binding-linux-x64-musl": "0.40.0", "@oxfmt/binding-openharmony-arm64": "0.40.0", "@oxfmt/binding-win32-arm64-msvc": "0.40.0", "@oxfmt/binding-win32-ia32-msvc": "0.40.0", "@oxfmt/binding-win32-x64-msvc": "0.40.0" }, "bin": { "oxfmt": "bin/oxfmt" } }, "sha512-g0C3I7xUj4b4DcagevM9kgH6+pUHytikxUcn3/VUkvzTNaaXBeyZqb7IBsHwojeXm4mTBEC/aBjBTMVUkZwWUQ=="], @@ -1660,6 +1861,8 @@ "p-queue": ["p-queue@9.1.0", "", { "dependencies": { "eventemitter3": "^5.0.1", "p-timeout": "^7.0.0" } }, "sha512-O/ZPaXuQV29uSLbxWBGGZO1mCQXV2BLIwUr59JUU9SoH76mnYvtms7aafH/isNSNGwuEfP6W/4xD0/TJXxrizw=="], + "p-retry": ["p-retry@4.6.2", "", { "dependencies": { "@types/retry": "0.12.0", "retry": "^0.13.1" } }, "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ=="], + "p-timeout": ["p-timeout@7.0.1", "", {}, "sha512-AxTM2wDGORHGEkPCt8yqxOTMgpfbEHqF51f/5fJCmwFC3C/zNcGT63SymH2ttOAaiIws2zVg4+izQCjrakcwHg=="], "package-manager-detector": ["package-manager-detector@1.6.0", "", {}, "sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA=="], @@ -1672,10 +1875,16 @@ "parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="], + "partial-json": ["partial-json@0.1.7", "", {}, "sha512-Njv/59hHaokb/hRUjce3Hdv12wd60MtM9Z5Olmn+nehe0QDAsRtRbJPvJ0Z91TusF0SuZRIvnM+S4l6EIP8leA=="], + "path-browserify": ["path-browserify@1.0.1", "", {}, "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g=="], + "path-expression-matcher": ["path-expression-matcher@1.5.0", "", {}, "sha512-cbrerZV+6rvdQrrD+iGMcZFEiiSrbv9Tfdkvnusy6y0x0GKBXREFg/Y65GhIfm0tnLntThhzCnfKwp1WRjeCyQ=="], + "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], + "path-scurry": ["path-scurry@2.0.2", "", { "dependencies": { "lru-cache": "^11.0.0", "minipass": "^7.1.2" } }, "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg=="], + "path-to-regexp": ["path-to-regexp@6.3.0", "", {}, "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ=="], "path-to-regexp-updated": ["path-to-regexp@6.3.0", "", {}, "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ=="], @@ -1708,8 +1917,12 @@ "progress": ["progress@2.0.3", "", {}, "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA=="], + "proper-lockfile": ["proper-lockfile@4.1.2", "", { "dependencies": { "graceful-fs": "^4.2.4", "retry": "^0.12.0", "signal-exit": "^3.0.2" } }, "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA=="], + "property-information": ["property-information@7.1.0", "", {}, "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ=="], + "protobufjs": ["protobufjs@7.6.0", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.5", "@protobufjs/eventemitter": "^1.1.0", "@protobufjs/fetch": "^1.1.1", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.2", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.1", "@types/node": ">=13.7.0", "long": "^5.3.2" } }, "sha512-LtESOsMPTZgyYtwxhvdgdjGL0HmXEaRA/hVD6sol4zA60hVXXXP/SGmxnqDbgGE8gy7pYex7cym+5vYPcmaXBQ=="], + "proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="], "pump": ["pump@3.0.4", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA=="], @@ -1792,6 +2005,8 @@ "retext-stringify": ["retext-stringify@4.0.0", "", { "dependencies": { "@types/nlcst": "^2.0.0", "nlcst-to-string": "^4.0.0", "unified": "^11.0.0" } }, "sha512-rtfN/0o8kL1e+78+uxPTqu1Klt0yPzKuQ2BfWwwfgIUSayyzxpM1PJzkKt4V8803uB9qSy32MvI7Xep9khTpiA=="], + "retry": ["retry@0.12.0", "", {}, "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow=="], + "rettime": ["rettime@0.10.1", "", {}, "sha512-uyDrIlUEH37cinabq0AX4QbgV4HbFZ/gqoiunWQ1UqBtRvTTytwhNYjE++pO/MjPTZL5KQCf2bEoJ/BJNVQ5Kw=="], "roarr": ["roarr@2.15.4", "", { "dependencies": { "boolean": "^3.0.1", "detect-node": "^2.0.4", "globalthis": "^1.0.1", "json-stringify-safe": "^5.0.1", "semver-compare": "^1.0.0", "sprintf-js": "^1.1.2" } }, "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A=="], @@ -1802,6 +2017,8 @@ "router": ["router@2.2.0", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="], + "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], + "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], "sax": ["sax@1.5.0", "", {}, "sha512-21IYA3Q5cQf089Z6tgaUTr7lDAyzoTPx5HRtbhsME8Udispad8dC/+sziTNugOEx54ilvatQ9YCzl4KQLPcRHA=="], @@ -1842,7 +2059,7 @@ "siginfo": ["siginfo@2.0.0", "", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="], - "signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], + "signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], "sirv": ["sirv@3.0.2", "", { "dependencies": { "@polka/url": "^1.0.0-next.24", "mrmime": "^2.0.0", "totalist": "^3.0.0" } }, "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g=="], @@ -1874,6 +2091,8 @@ "strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + "strnum": ["strnum@2.3.0", "", {}, "sha512-ums3KNd42PGyx5xaoVTO1mjU1bH3NpY4vsrVlnv9PNGqQj8wd7rJ6nEypLrJ7z5vxK5RP0yMLo6J/Gsm62DI5Q=="], + "style-to-js": ["style-to-js@1.1.21", "", { "dependencies": { "style-to-object": "1.0.14" } }, "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ=="], "style-to-object": ["style-to-object@1.0.14", "", { "dependencies": { "inline-style-parser": "0.2.7" } }, "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw=="], @@ -1964,6 +2183,8 @@ "type-is": ["type-is@2.0.1", "", { "dependencies": { "content-type": "^1.0.5", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw=="], + "typebox": ["typebox@1.1.38", "", {}, "sha512-pZ0aQPmMmXoUvSbeuWf/Hzsc+avNw/Zd6VeE8CFgkVGWyuHPJvqeJJDeJqLve+K70LvjYIoleGcoJHPT17cWoA=="], + "typesafe-path": ["typesafe-path@0.2.2", "", {}, "sha512-OJabfkAg1WLZSqJAJ0Z6Sdt3utnbzr/jh+NAHoyWHJe8CMSy79Gm085094M9nvTPy22KzTVn5Zq5mbapCI/hPA=="], "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], @@ -2076,6 +2297,8 @@ "web-namespaces": ["web-namespaces@2.0.1", "", {}, "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ=="], + "web-streams-polyfill": ["web-streams-polyfill@3.3.3", "", {}, "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw=="], + "webpack-virtual-modules": ["webpack-virtual-modules@0.6.2", "", {}, "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ=="], "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], @@ -2090,6 +2313,8 @@ "ws": ["ws@8.20.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA=="], + "xml-naming": ["xml-naming@0.1.0", "", {}, "sha512-k8KO9hrMyNk6tUWqUfkTEZbezRRpONVOzUTnc97VnCvyj6Tf9lyUR9EDAIeiVLv56jsMcoXEwjW8Kv5yPY52lw=="], + "xxhash-wasm": ["xxhash-wasm@1.1.0", "", {}, "sha512-147y/6YNh+tlp6nd/2pWq38i9h6mz/EuQ6njIrmW8D1BS5nCqs0P6DG+m6zTGnNz5I+uhZ0SHxBs9BsPrwcKDA=="], "y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="], @@ -2124,6 +2349,8 @@ "@astrojs/yaml2ts/yaml": ["yaml@2.8.2", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A=="], + "@aws-sdk/credential-provider-sso/@aws-sdk/token-providers": ["@aws-sdk/token-providers@3.1049.0", "", { "dependencies": { "@aws-sdk/core": "^3.974.12", "@aws-sdk/nested-clients": "^3.997.10", "@aws-sdk/types": "^3.973.8", "@smithy/core": "^3.24.2", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-r7+d0lQMTHKypkmaF5jRTBYLYHCUHzt3gaVoN9SidLhQeWhCmHk3AKrboDTpPF5b7Pt7vKu3+oeMjznM2Eu1ow=="], + "@babel/code-frame/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], "@babel/core/@babel/generator": ["@babel/generator@7.29.1", "", { "dependencies": { "@babel/parser": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw=="], @@ -2148,10 +2375,24 @@ "@base-ui/utils/@babel/runtime": ["@babel/runtime@7.29.2", "", {}, "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g=="], + "@earendil-works/pi-agent-core/yaml": ["yaml@2.9.0", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA=="], + + "@earendil-works/pi-ai/@anthropic-ai/sdk": ["@anthropic-ai/sdk@0.91.1", "", { "dependencies": { "json-schema-to-ts": "^3.1.1" }, "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" }, "optionalPeers": ["zod"], "bin": { "anthropic-ai-sdk": "bin/cli" } }, "sha512-LAmu761tSN9r66ixvmciswUj/ZC+1Q4iAfpedTfSVLeswRwnY3n2Nb6Tsk+cLPP28aLOPWeMgIuTuCcMC6W/iw=="], + + "@earendil-works/pi-coding-agent/diff": ["diff@8.0.4", "", {}, "sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw=="], + + "@earendil-works/pi-coding-agent/jiti": ["jiti@2.7.0", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ=="], + + "@earendil-works/pi-coding-agent/undici": ["undici@8.3.0", "", {}, "sha512-TkUDgb6tl7KOGZ+7e8E3d2FYgUQgF6z5YypqjWmixVQSQERFcVrVg0ySADm2LVLRh5ljAaHTCR5Fmz3Q34rB7Q=="], + + "@earendil-works/pi-coding-agent/yaml": ["yaml@2.9.0", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA=="], + "@electron/get/fs-extra": ["fs-extra@8.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g=="], "@electron/get/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + "@inquirer/core/signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], + "@inquirer/core/wrap-ansi": ["wrap-ansi@6.2.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA=="], "@pierre/diffs/shiki": ["shiki@3.23.0", "", { "dependencies": { "@shikijs/core": "3.23.0", "@shikijs/engine-javascript": "3.23.0", "@shikijs/engine-oniguruma": "3.23.0", "@shikijs/langs": "3.23.0", "@shikijs/themes": "3.23.0", "@shikijs/types": "3.23.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-55Dj73uq9ZXL5zyeRPzHQsK7Nbyt6Y10k5s7OjuFZGMhpp4r/rsLBH0o/0fstIzX1Lep9VxefWljK/SKCzygIA=="], @@ -2216,6 +2457,8 @@ "mdast-util-find-and-replace/escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="], + "p-retry/retry": ["retry@0.13.1", "", {}, "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg=="], + "parse-entities/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="], "recast/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], diff --git a/packages/contracts/src/model.ts b/packages/contracts/src/model.ts index 8e7daaa0c79..748fbb3cbbd 100644 --- a/packages/contracts/src/model.ts +++ b/packages/contracts/src/model.ts @@ -131,6 +131,7 @@ const CODEX_DRIVER_KIND = ProviderDriverKind.make("codex"); const CLAUDE_DRIVER_KIND = ProviderDriverKind.make("claudeAgent"); const CURSOR_DRIVER_KIND = ProviderDriverKind.make("cursor"); const OPENCODE_DRIVER_KIND = ProviderDriverKind.make("opencode"); +const PI_AGENT_DRIVER_KIND = ProviderDriverKind.make("piAgent"); export const DEFAULT_MODEL = "gpt-5.4"; export const DEFAULT_GIT_TEXT_GENERATION_MODEL = "gpt-5.4-mini"; @@ -140,6 +141,7 @@ export const DEFAULT_MODEL_BY_PROVIDER: Partial> [CLAUDE_DRIVER_KIND]: "Claude", [CURSOR_DRIVER_KIND]: "Cursor", [OPENCODE_DRIVER_KIND]: "OpenCode", + [PI_AGENT_DRIVER_KIND]: "Pi Agent", }; diff --git a/packages/contracts/src/providerRuntime.ts b/packages/contracts/src/providerRuntime.ts index 5032dc4eb41..3d8f3c2f99a 100644 --- a/packages/contracts/src/providerRuntime.ts +++ b/packages/contracts/src/providerRuntime.ts @@ -26,6 +26,8 @@ const RuntimeEventRawSource = Schema.Union([ Schema.Literal("claude.sdk.permission"), Schema.Literal("codex.sdk.thread-event"), Schema.Literal("opencode.sdk.event"), + Schema.Literal("pi.sdk.event"), + Schema.Literal("pi.sdk.permission"), Schema.Literal("acp.jsonrpc"), Schema.TemplateLiteral(["acp.", Schema.String, ".extension"]), ]); diff --git a/packages/contracts/src/settings.test.ts b/packages/contracts/src/settings.test.ts index 39695fe3b01..6095d06f272 100644 --- a/packages/contracts/src/settings.test.ts +++ b/packages/contracts/src/settings.test.ts @@ -107,6 +107,13 @@ describe("ServerSettingsPatch string normalization", () => { binaryPath: " /opt/homebrew/bin/codex ", homePath: " ~/.codex ", }, + piAgent: { + enabled: true, + binaryPath: "", + homePath: "", + customModels: [], + launchArgs: "", + }, }, providerInstances: { codex_personal: { diff --git a/packages/contracts/src/settings.ts b/packages/contracts/src/settings.ts index 2d115eed98e..dc0d8adab11 100644 --- a/packages/contracts/src/settings.ts +++ b/packages/contracts/src/settings.ts @@ -331,6 +331,49 @@ export const OpenCodeSettings = makeProviderSettingsSchema( ); export type OpenCodeSettings = typeof OpenCodeSettings.Type; +export const PiSettings = makeProviderSettingsSchema( + { + enabled: Schema.Boolean.pipe( + Schema.withDecodingDefault(Effect.succeed(true)), + Schema.annotateKey({ providerSettingsForm: { hidden: true } }), + ), + binaryPath: makeBinaryPathSetting("pi").pipe( + Schema.annotateKey({ + title: "Binary path", + description: "Path to the Pi Agent binary used by this instance.", + providerSettingsForm: { placeholder: "pi", clearWhenEmpty: "omit" }, + }), + ), + homePath: TrimmedString.pipe( + Schema.withDecodingDefault(Effect.succeed("")), + Schema.annotateKey({ + title: "Pi HOME path", + description: "Custom home directory for Pi Agent config and auth.", + providerSettingsForm: { placeholder: "~/.pi", clearWhenEmpty: "omit" }, + }), + ), + customModels: Schema.Array(Schema.String).pipe( + Schema.withDecodingDefault(Effect.succeed([])), + Schema.annotateKey({ providerSettingsForm: { hidden: true } }), + ), + launchArgs: Schema.String.pipe( + Schema.withDecodingDefault(Effect.succeed("")), + Schema.annotateKey({ + title: "Launch arguments", + description: "Additional arguments passed on session start.", + providerSettingsForm: { + placeholder: "e.g. --thinking high", + clearWhenEmpty: "omit", + }, + }), + ), + }, + { + order: ["binaryPath", "homePath", "launchArgs"], + }, +); +export type PiSettings = typeof PiSettings.Type; + export const ObservabilitySettings = Schema.Struct({ otlpTracesUrl: TrimmedString.pipe(Schema.withDecodingDefault(Effect.succeed(""))), otlpMetricsUrl: TrimmedString.pipe(Schema.withDecodingDefault(Effect.succeed(""))), @@ -370,6 +413,7 @@ export const ServerSettings = Schema.Struct({ claudeAgent: ClaudeSettings.pipe(Schema.withDecodingDefault(Effect.succeed({}))), cursor: CursorSettings.pipe(Schema.withDecodingDefault(Effect.succeed({}))), opencode: OpenCodeSettings.pipe(Schema.withDecodingDefault(Effect.succeed({}))), + piAgent: PiSettings.pipe(Schema.withDecodingDefault(Effect.succeed({}))), }).pipe(Schema.withDecodingDefault(Effect.succeed({}))), // New driver-agnostic instance map. Keyed by `ProviderInstanceId`; values // are `ProviderInstanceConfig` envelopes. The driver-specific config blob From 2b6cd8eeebc9c4a8dc5d8067ed206231d7c2a17f Mon Sep 17 00:00:00 2001 From: Ben Scholar Date: Wed, 20 May 2026 17:20:59 -0700 Subject: [PATCH 04/36] simplify release --- .github/workflows/release.yml | 272 +--------------------------------- 1 file changed, 2 insertions(+), 270 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d90479087cf..8638ccb80c1 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -414,51 +414,10 @@ jobs: path: release-publish/* if-no-files-found: error - publish_cli: - name: Publish CLI to npm - needs: [preflight, build] - if: ${{ !failure() && !cancelled() && needs.preflight.result == 'success' && needs.build.result == 'success' }} - runs-on: ubuntu-24.04 # blacksmith-8vcpu-ubuntu-2404 - timeout-minutes: 10 - permissions: - contents: read - id-token: write - steps: - - name: Checkout - uses: actions/checkout@v6 - with: - ref: ${{ needs.preflight.outputs.ref }} - - - name: Setup Bun - uses: oven-sh/setup-bun@v2 - with: - bun-version-file: package.json - - - name: Setup Node - uses: actions/setup-node@v6 - with: - node-version-file: package.json - registry-url: https://registry.npmjs.org - - - name: Install dependencies - run: bun install --frozen-lockfile --filter=t3 --filter=@t3tools/web --filter=@t3tools/scripts - - - name: Align package versions to release version - run: node scripts/update-release-package-versions.ts "${{ needs.preflight.outputs.version }}" - - - name: Build web package - run: bun --filter=@t3tools/web run build - - - name: Build CLI package - run: bun --filter=t3 run build - - - name: Publish CLI package - run: node apps/server/scripts/cli.ts publish --tag "${{ needs.preflight.outputs.cli_dist_tag }}" --app-version "${{ needs.preflight.outputs.version }}" --verbose - release: name: Publish GitHub Release - needs: [preflight, build, publish_cli] - if: ${{ !failure() && !cancelled() && needs.preflight.result == 'success' && needs.build.result == 'success' && needs.publish_cli.result == 'success' }} + needs: [preflight, build] + if: ${{ !failure() && !cancelled() && needs.preflight.result == 'success' && needs.build.result == 'success' }} runs-on: blacksmith-8vcpu-ubuntu-2404 timeout-minutes: 10 steps: @@ -576,230 +535,3 @@ jobs: fail_on_unmatched_files: true token: ${{ steps.app_token.outputs.token }} - deploy_web: - name: Deploy hosted web app - needs: [preflight, release] - if: ${{ !failure() && !cancelled() && needs.preflight.result == 'success' && needs.release.result == 'success' }} - runs-on: blacksmith-8vcpu-ubuntu-2404 - timeout-minutes: 10 - env: - VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }} - VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }} - VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }} - T3CODE_WEB_ROUTER_URL: ${{ vars.T3CODE_WEB_ROUTER_URL }} - T3CODE_WEB_LATEST_DOMAIN: ${{ vars.T3CODE_WEB_LATEST_DOMAIN }} - T3CODE_WEB_NIGHTLY_DOMAIN: ${{ vars.T3CODE_WEB_NIGHTLY_DOMAIN }} - VERCEL_TEAM_SLUG: ${{ vars.VERCEL_TEAM_SLUG }} - steps: - - name: Checkout - uses: actions/checkout@v6 - with: - ref: ${{ needs.preflight.outputs.ref }} - - - name: Setup Bun - uses: oven-sh/setup-bun@v2 - with: - bun-version-file: package.json - - - name: Setup Node - uses: actions/setup-node@v6 - with: - node-version-file: package.json - - - name: Install release tooling dependencies - run: bun install --frozen-lockfile --filter=@t3tools/scripts --filter=@t3tools/web - - - name: Align package versions to release version - run: node scripts/update-release-package-versions.ts "${{ needs.preflight.outputs.version }}" - - - name: Refresh release lockfile - run: bun install --lockfile-only --ignore-scripts - - - name: Deploy and alias channel - shell: bash - run: | - set -euo pipefail - - if [[ -z "${VERCEL_TOKEN:-}" || -z "${VERCEL_ORG_ID:-}" || -z "${VERCEL_PROJECT_ID:-}" ]]; then - echo "Missing one or more required Vercel secrets: VERCEL_TOKEN, VERCEL_ORG_ID, VERCEL_PROJECT_ID." >&2 - exit 1 - fi - - router_url="${T3CODE_WEB_ROUTER_URL:-https://app.t3.codes}" - latest_domain="${T3CODE_WEB_LATEST_DOMAIN:-latest.app.t3.codes}" - nightly_domain="${T3CODE_WEB_NIGHTLY_DOMAIN:-nightly.app.t3.codes}" - router_domain="${router_url#http://}" - router_domain="${router_domain#https://}" - router_domain="${router_domain%%/*}" - - if [[ "${{ needs.preflight.outputs.release_channel }}" == "stable" ]]; then - channel_domain="$latest_domain" - channel_name="latest" - else - channel_domain="$nightly_domain" - channel_name="nightly" - fi - - vercel_scope="${VERCEL_TEAM_SLUG:-$VERCEL_ORG_ID}" - vercel_scope_args=(--scope "$vercel_scope") - - echo "Deploying hosted web app for $channel_name channel." - deployment_url="$( - bunx vercel@53.1.1 deploy \ - --prod \ - --skip-domain \ - --yes \ - --token "$VERCEL_TOKEN" \ - "${vercel_scope_args[@]}" \ - --build-env "APP_VERSION=${{ needs.preflight.outputs.version }}" \ - --build-env "VITE_HOSTED_APP_URL=$router_url" \ - --build-env "VITE_HOSTED_APP_CHANNEL=$channel_name" - )" - - echo "Aliasing $deployment_url to $channel_domain." - bunx vercel@53.1.1 alias set "$deployment_url" "$channel_domain" \ - --token "$VERCEL_TOKEN" \ - "${vercel_scope_args[@]}" - - if [[ "$channel_name" == "latest" && -n "$router_domain" && "$router_domain" != "$channel_domain" ]]; then - echo "Aliasing $deployment_url to router domain $router_domain." - bunx vercel@53.1.1 alias set "$deployment_url" "$router_domain" \ - --token "$VERCEL_TOKEN" \ - "${vercel_scope_args[@]}" - fi - - finalize: - name: Finalize release - if: ${{ !failure() && !cancelled() && needs.preflight.result == 'success' && needs.release.result == 'success' && needs.preflight.outputs.release_channel == 'stable' }} - needs: [preflight, release] - runs-on: blacksmith-8vcpu-ubuntu-2404 - timeout-minutes: 10 - steps: - - id: app_token - name: Mint release app token - uses: actions/create-github-app-token@v2 - with: - app-id: ${{ secrets.RELEASE_APP_ID }} - private-key: ${{ secrets.RELEASE_APP_PRIVATE_KEY }} - owner: ${{ github.repository_owner }} - - - name: Checkout - uses: actions/checkout@v6 - with: - ref: main - fetch-depth: 0 - token: ${{ steps.app_token.outputs.token }} - persist-credentials: true - - - id: app_bot - name: Resolve GitHub App bot identity - env: - GH_TOKEN: ${{ steps.app_token.outputs.token }} - APP_SLUG: ${{ steps.app_token.outputs.app-slug }} - run: | - user_id="$(gh api "/users/${APP_SLUG}[bot]" --jq .id)" - echo "name=${APP_SLUG}[bot]" >> "$GITHUB_OUTPUT" - echo "email=${user_id}+${APP_SLUG}[bot]@users.noreply.github.com" >> "$GITHUB_OUTPUT" - - - name: Setup Bun - uses: oven-sh/setup-bun@v2 - with: - bun-version-file: package.json - - - name: Setup Node - uses: actions/setup-node@v6 - with: - node-version-file: package.json - - - name: Install dependencies - run: bun install --frozen-lockfile --filter=@t3tools/scripts - - - id: update_versions - name: Update version strings - env: - RELEASE_VERSION: ${{ needs.preflight.outputs.version }} - run: node scripts/update-release-package-versions.ts "$RELEASE_VERSION" --github-output - - - name: Format package.json files - if: steps.update_versions.outputs.changed == 'true' - run: bunx oxfmt@0.40.0 apps/server/package.json apps/desktop/package.json apps/web/package.json packages/contracts/package.json - - - name: Refresh lockfile - if: steps.update_versions.outputs.changed == 'true' - run: bun install --lockfile-only --ignore-scripts - - - name: Commit and push version bump - if: steps.update_versions.outputs.changed == 'true' - shell: bash - env: - RELEASE_TAG: ${{ needs.preflight.outputs.tag }} - run: | - if git diff --quiet -- apps/server/package.json apps/desktop/package.json apps/web/package.json packages/contracts/package.json bun.lock; then - echo "No version changes to commit." - exit 0 - fi - - git config user.name "${{ steps.app_bot.outputs.name }}" - git config user.email "${{ steps.app_bot.outputs.email }}" - - git add apps/server/package.json apps/desktop/package.json apps/web/package.json packages/contracts/package.json bun.lock - git commit -m "chore(release): prepare $RELEASE_TAG" - git push origin HEAD:main - - announce_discord: - name: Announce release on Discord - if: | - always() && !cancelled() && - needs.preflight.result == 'success' && - needs.release.result == 'success' && - needs.deploy_web.result == 'success' && - (needs.finalize.result == 'success' || needs.finalize.result == 'skipped') - needs: [preflight, release, deploy_web, finalize] - runs-on: blacksmith-8vcpu-ubuntu-2404 - timeout-minutes: 10 - steps: - - name: Checkout - uses: actions/checkout@v6 - with: - ref: ${{ needs.preflight.outputs.ref }} - - - name: Setup Bun - uses: oven-sh/setup-bun@v2 - with: - bun-version-file: package.json - - - name: Setup Node - uses: actions/setup-node@v6 - with: - node-version-file: package.json - - - name: Install dependencies - run: bun install --frozen-lockfile --filter=@t3tools/scripts - - - name: Announce prerelease on Discord - if: needs.preflight.outputs.is_prerelease == 'true' - continue-on-error: true - env: - DISCORD_MENTION_ROLE_ID: ${{ secrets.DISCORD_RELEASE_NIGHTLY_ROLE_ID }} - DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_RELEASE_WEBHOOK_URL }} - run: | - node scripts/notify-discord-release.ts prerelease \ - --role-id "$DISCORD_MENTION_ROLE_ID" \ - --release-name "${{ needs.preflight.outputs.release_name }}" \ - --release-version "${{ needs.preflight.outputs.version }}" \ - --tag "${{ needs.preflight.outputs.tag }}" \ - --release-url "https://github.com/${{ github.repository }}/releases/tag/${{ needs.preflight.outputs.tag }}" - - - name: Announce latest release on Discord - if: needs.preflight.outputs.make_latest == 'true' - continue-on-error: true - env: - DISCORD_MENTION_ROLE_ID: ${{ secrets.DISCORD_RELEASE_LATEST_ROLE_ID }} - DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_RELEASE_WEBHOOK_URL }} - run: | - node scripts/notify-discord-release.ts latest \ - --role-id "$DISCORD_MENTION_ROLE_ID" \ - --release-name "${{ needs.preflight.outputs.release_name }}" \ - --release-version "${{ needs.preflight.outputs.version }}" \ - --tag "${{ needs.preflight.outputs.tag }}" \ - --release-url "https://github.com/${{ github.repository }}/releases/tag/${{ needs.preflight.outputs.tag }}" From 468d19beb415a3ebbfe62c3528b082212d0dcf02 Mon Sep 17 00:00:00 2001 From: Ben Scholar Date: Wed, 20 May 2026 17:22:39 -0700 Subject: [PATCH 05/36] manual release --- .github/workflows/release.yml | 116 ++++------------------------------ 1 file changed, 14 insertions(+), 102 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8638ccb80c1..399bc4fdc8b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,25 +1,11 @@ name: Release on: - push: - tags: - - "v*.*.*" - - "!v*-nightly.*" - schedule: - - cron: "0 */3 * * *" workflow_dispatch: inputs: - channel: - description: "Release channel" - required: false - default: stable - type: choice - options: - - stable - - nightly version: description: "Release version (for example 1.2.3 or v1.2.3)" - required: false + required: true type: string permissions: @@ -27,55 +13,15 @@ permissions: id-token: none jobs: - check_changes: - name: Check for changes since last nightly - if: github.event_name == 'schedule' - runs-on: blacksmith-8vcpu-ubuntu-2404 - outputs: - has_changes: ${{ steps.check.outputs.has_changes }} - steps: - - name: Checkout - uses: actions/checkout@v6 - with: - fetch-depth: 0 - - - id: check - name: Compare HEAD to last nightly tag - run: | - last_nightly_tag=$(git tag --list 'v*-nightly.*' 'nightly-v*' --sort=-creatordate | head -n 1) - if [[ -z "$last_nightly_tag" ]]; then - echo "No previous nightly tag found. Proceeding with release." - echo "has_changes=true" >> "$GITHUB_OUTPUT" - exit 0 - fi - - last_nightly_sha=$(git rev-parse "$last_nightly_tag^{commit}") - head_sha=$(git rev-parse HEAD) - - if [[ "$last_nightly_sha" == "$head_sha" ]]; then - echo "No changes on main since last nightly release ($last_nightly_tag). Skipping." - echo "has_changes=false" >> "$GITHUB_OUTPUT" - else - echo "Changes detected on main since $last_nightly_tag ($last_nightly_sha → $head_sha). Proceeding." - echo "has_changes=true" >> "$GITHUB_OUTPUT" - fi - preflight: name: Preflight - needs: [check_changes] - if: | - !failure() && !cancelled() && - (github.event_name != 'schedule' || needs.check_changes.outputs.has_changes == 'true') runs-on: blacksmith-8vcpu-ubuntu-2404 timeout-minutes: 10 outputs: - release_channel: ${{ steps.release_meta.outputs.release_channel }} version: ${{ steps.release_meta.outputs.version }} tag: ${{ steps.release_meta.outputs.tag }} release_name: ${{ steps.release_meta.outputs.name }} - short_sha: ${{ steps.release_meta.outputs.short_sha }} previous_tag: ${{ steps.previous_tag.outputs.previous_tag }} - cli_dist_tag: ${{ steps.release_meta.outputs.cli_dist_tag }} is_prerelease: ${{ steps.release_meta.outputs.is_prerelease }} make_latest: ${{ steps.release_meta.outputs.make_latest }} ref: ${{ github.sha }} @@ -102,56 +48,22 @@ jobs: name: Resolve release version shell: bash env: - DISPATCH_CHANNEL: ${{ github.event.inputs.channel }} DISPATCH_VERSION: ${{ github.event.inputs.version }} - NIGHTLY_DATE: ${{ github.run_started_at }} - NIGHTLY_SHA: ${{ github.sha }} - NIGHTLY_RUN_NUMBER: ${{ github.run_number }} run: | - if [[ "${GITHUB_EVENT_NAME}" == "schedule" || ( "${GITHUB_EVENT_NAME}" == "workflow_dispatch" && "${DISPATCH_CHANNEL:-stable}" == "nightly" ) ]]; then - nightly_date="$(date -u -d "$NIGHTLY_DATE" +%Y%m%d)" - - node scripts/resolve-nightly-release.ts \ - --date "$nightly_date" \ - --run-number "$NIGHTLY_RUN_NUMBER" \ - --sha "$NIGHTLY_SHA" \ - --github-output - - echo "release_channel=nightly" >> "$GITHUB_OUTPUT" - echo "cli_dist_tag=nightly" >> "$GITHUB_OUTPUT" - echo "is_prerelease=true" >> "$GITHUB_OUTPUT" - echo "make_latest=false" >> "$GITHUB_OUTPUT" - else - if [[ "${GITHUB_EVENT_NAME}" == "workflow_dispatch" ]]; then - raw="${DISPATCH_VERSION}" - if [[ -z "$raw" ]]; then - echo "workflow_dispatch stable releases require the version input." >&2 - exit 1 - fi - else - raw="${GITHUB_REF_NAME}" - fi - - version="${raw#v}" - if [[ ! "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+([.-][0-9A-Za-z.-]+)?$ ]]; then - echo "Invalid release version: $raw" >&2 - exit 1 - fi - - echo "release_channel=stable" >> "$GITHUB_OUTPUT" - echo "version=$version" >> "$GITHUB_OUTPUT" - echo "tag=v$version" >> "$GITHUB_OUTPUT" - echo "name=T3 Code v$version" >> "$GITHUB_OUTPUT" - echo "cli_dist_tag=latest" >> "$GITHUB_OUTPUT" - if [[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then - echo "is_prerelease=false" >> "$GITHUB_OUTPUT" - echo "make_latest=true" >> "$GITHUB_OUTPUT" - else - echo "is_prerelease=true" >> "$GITHUB_OUTPUT" - echo "make_latest=false" >> "$GITHUB_OUTPUT" - fi + raw="${DISPATCH_VERSION}" + version="${raw#v}" + if [[ ! "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+([.-][0-9A-Za-z.-]+)?$ ]]; then + echo "Invalid release version: $raw" >&2 + exit 1 fi + echo "release_channel=stable" >> "$GITHUB_OUTPUT" + echo "version=$version" >> "$GITHUB_OUTPUT" + echo "tag=v$version" >> "$GITHUB_OUTPUT" + echo "name=T3 Code v$version" >> "$GITHUB_OUTPUT" + echo "is_prerelease=false" >> "$GITHUB_OUTPUT" + echo "make_latest=true" >> "$GITHUB_OUTPUT" + - name: Lint run: bun run lint @@ -165,7 +77,7 @@ jobs: name: Resolve previous release tag run: | node scripts/resolve-previous-release-tag.ts \ - --channel "${{ steps.release_meta.outputs.release_channel }}" \ + --channel "stable" \ --current-tag "${{ steps.release_meta.outputs.tag }}" \ --github-output From f55a48ae4d7b39614c7d2b2eaba72f486be55383 Mon Sep 17 00:00:00 2001 From: Ben Scholar Date: Wed, 20 May 2026 17:26:56 -0700 Subject: [PATCH 06/36] chore(ci): replace Blacksmith runners with GitHub Actions runners Co-Authored-By: Claude Opus 4.6 (1M context) --- .github/workflows/ci.yml | 4 ++-- .github/workflows/release.yml | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e3329b1dad9..a313c4d341b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,7 +9,7 @@ on: jobs: quality: name: Format, Lint, Typecheck, Test, Browser Test, Build - runs-on: blacksmith-8vcpu-ubuntu-2404 + runs-on: ubuntu-latest timeout-minutes: 10 steps: - name: Checkout @@ -76,7 +76,7 @@ jobs: release_smoke: name: Release Smoke - runs-on: blacksmith-8vcpu-ubuntu-2404 + runs-on: ubuntu-latest timeout-minutes: 10 steps: - name: Checkout diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 399bc4fdc8b..021f906695f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -15,7 +15,7 @@ permissions: jobs: preflight: name: Preflight - runs-on: blacksmith-8vcpu-ubuntu-2404 + runs-on: ubuntu-latest timeout-minutes: 10 outputs: version: ${{ steps.release_meta.outputs.version }} @@ -92,22 +92,22 @@ jobs: matrix: include: - label: macOS arm64 - runner: blacksmith-12vcpu-macos-26 + runner: macos-latest platform: mac target: dmg arch: arm64 - label: macOS x64 - runner: blacksmith-12vcpu-macos-26 + runner: macos-13 platform: mac target: dmg arch: x64 - label: Linux x64 - runner: blacksmith-32vcpu-ubuntu-2404 + runner: ubuntu-latest platform: linux target: AppImage arch: x64 - label: Windows x64 - runner: blacksmith-32vcpu-windows-2025 + runner: windows-latest platform: win target: nsis arch: x64 @@ -330,7 +330,7 @@ jobs: name: Publish GitHub Release needs: [preflight, build] if: ${{ !failure() && !cancelled() && needs.preflight.result == 'success' && needs.build.result == 'success' }} - runs-on: blacksmith-8vcpu-ubuntu-2404 + runs-on: ubuntu-latest timeout-minutes: 10 steps: - id: app_token From 433cdcbc3d381a2378a134c363be434e7d78576e Mon Sep 17 00:00:00 2001 From: Ben Scholar Date: Wed, 20 May 2026 17:28:40 -0700 Subject: [PATCH 07/36] style: fix formatting Co-Authored-By: Claude Opus 4.6 (1M context) --- .github/workflows/release.yml | 1 - apps/server/src/provider/Drivers/PiDriver.ts | 1 - apps/server/src/provider/Layers/PiAdapter.ts | 30 +++++++++---------- apps/server/src/provider/Layers/PiProvider.ts | 7 ++--- 4 files changed, 16 insertions(+), 23 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 021f906695f..3d050e2c634 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -446,4 +446,3 @@ jobs: release-assets/*.yml fail_on_unmatched_files: true token: ${{ steps.app_token.outputs.token }} - diff --git a/apps/server/src/provider/Drivers/PiDriver.ts b/apps/server/src/provider/Drivers/PiDriver.ts index 5b7c8abaaa6..fd23b992572 100644 --- a/apps/server/src/provider/Drivers/PiDriver.ts +++ b/apps/server/src/provider/Drivers/PiDriver.ts @@ -8,7 +8,6 @@ import * as Stream from "effect/Stream"; import { HttpClient } from "effect/unstable/http"; import { ChildProcessSpawner } from "effect/unstable/process"; - import { makePiTextGeneration } from "../../textGeneration/PiTextGeneration.ts"; import { ServerConfig } from "../../config.ts"; import { ProviderDriverError } from "../Errors.ts"; diff --git a/apps/server/src/provider/Layers/PiAdapter.ts b/apps/server/src/provider/Layers/PiAdapter.ts index 6931d6ae28f..74575c0b21c 100644 --- a/apps/server/src/provider/Layers/PiAdapter.ts +++ b/apps/server/src/provider/Layers/PiAdapter.ts @@ -273,7 +273,8 @@ export const makePiAdapter = Effect.fn("makePiAdapter")(function* ( itemId, type: "content.delta", payload: { - streamKind: itemType === "command_execution" ? "command_output" : "file_change_output", + streamKind: + itemType === "command_execution" ? "command_output" : "file_change_output", delta: partial, }, }); @@ -353,7 +354,11 @@ export const makePiAdapter = Effect.fn("makePiAdapter")(function* ( } yield* Effect.sync(() => { - try { context.piSession.dispose(); } catch { /* best-effort cleanup */ } + try { + context.piSession.dispose(); + } catch { + /* best-effort cleanup */ + } }); const updatedAt = yield* nowIso; @@ -406,9 +411,7 @@ export const makePiAdapter = Effect.fn("makePiAdapter")(function* ( return Effect.succeed(context); }; - const startSession: PiAdapterShape["startSession"] = Effect.fn("startSession")(function* ( - input, - ) { + const startSession: PiAdapterShape["startSession"] = Effect.fn("startSession")(function* (input) { if (input.provider !== undefined && input.provider !== PROVIDER) { return yield* new ProviderAdapterValidationError({ provider: PROVIDER, @@ -617,11 +620,8 @@ export const makePiAdapter = Effect.fn("makePiAdapter")(function* ( }, ); - const respondToRequest: PiAdapterShape["respondToRequest"] = ( - _threadId, - _requestId, - _decision, - ) => Effect.void; + const respondToRequest: PiAdapterShape["respondToRequest"] = (_threadId, _requestId, _decision) => + Effect.void; const respondToUserInput: PiAdapterShape["respondToUserInput"] = ( _threadId, @@ -629,12 +629,10 @@ export const makePiAdapter = Effect.fn("makePiAdapter")(function* ( _answers, ) => Effect.void; - const stopSession: PiAdapterShape["stopSession"] = Effect.fn("stopSession")( - function* (threadId) { - const context = yield* requireSession(threadId); - yield* stopSessionInternal(context, { emitExitEvent: true }); - }, - ); + const stopSession: PiAdapterShape["stopSession"] = Effect.fn("stopSession")(function* (threadId) { + const context = yield* requireSession(threadId); + yield* stopSessionInternal(context, { emitExitEvent: true }); + }); const listSessions: PiAdapterShape["listSessions"] = () => Effect.sync(() => Array.from(sessions.values(), ({ session }) => ({ ...session }))); diff --git a/apps/server/src/provider/Layers/PiProvider.ts b/apps/server/src/provider/Layers/PiProvider.ts index 80cce875090..fb282324ac4 100644 --- a/apps/server/src/provider/Layers/PiProvider.ts +++ b/apps/server/src/provider/Layers/PiProvider.ts @@ -187,8 +187,7 @@ export const checkPiProviderStatus = Effect.fn("checkPiProviderStatus")(function version: null, status: "error", auth: { status: "unknown" }, - message: - "Pi Agent CLI is installed but failed to run. Timed out while running command.", + message: "Pi Agent CLI is installed but failed to run. Timed out while running command.", }, }); } @@ -227,9 +226,7 @@ export const checkPiProviderStatus = Effect.fn("checkPiProviderStatus")(function }); }); -export const makePendingPiProvider = ( - piSettings: PiSettings, -): Effect.Effect => +export const makePendingPiProvider = (piSettings: PiSettings): Effect.Effect => Effect.gen(function* () { const checkedAt = yield* nowIso; const models = providerModelsFromSettings( From 1f21de82a9a2651280fa042e45e98a87da60b6d0 Mon Sep 17 00:00:00 2001 From: Ben Scholar Date: Wed, 20 May 2026 17:34:00 -0700 Subject: [PATCH 08/36] chore(release): remove all signing logic from build workflow Co-Authored-By: Claude Opus 4.6 (1M context) --- .github/workflows/release.yml | 142 ++-------------------------------- 1 file changed, 5 insertions(+), 137 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 3d050e2c634..8701fc34624 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -139,147 +139,15 @@ jobs: - name: Align package versions to release version run: node scripts/update-release-package-versions.ts "${{ needs.preflight.outputs.version }}" - - name: Install Spectre-mitigated MSVC libs - if: matrix.platform == 'win' - shell: pwsh - run: | - $vswhere = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe" - $installPath = & $vswhere -products * -latest -property installationPath - $setupExe = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\setup.exe" - $proc = Start-Process -FilePath $setupExe ` - -ArgumentList "modify", "--installPath", "`"$installPath`"", "--add", ` - "Microsoft.VisualStudio.Component.VC.Tools.x86.x64.Spectre", "--quiet", "--norestart" ` - -Wait -PassThru -NoNewWindow - if ($null -eq $proc -or $proc.ExitCode -ne 0) { - $code = if ($null -ne $proc) { $proc.ExitCode } else { 1 } - Write-Error "Visual Studio Installer failed with exit code $code" - exit $code - } - - - name: Prepare Azure Trusted Signing - if: matrix.platform == 'win' - shell: pwsh - env: - AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} - AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} - AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }} - AZURE_TRUSTED_SIGNING_ENDPOINT: ${{ secrets.AZURE_TRUSTED_SIGNING_ENDPOINT }} - AZURE_TRUSTED_SIGNING_ACCOUNT_NAME: ${{ secrets.AZURE_TRUSTED_SIGNING_ACCOUNT_NAME }} - AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE_NAME: ${{ secrets.AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE_NAME }} - AZURE_TRUSTED_SIGNING_PUBLISHER_NAME: ${{ secrets.AZURE_TRUSTED_SIGNING_PUBLISHER_NAME }} - run: | - $ErrorActionPreference = "Stop" - - $requiredSecrets = @( - $env:AZURE_TENANT_ID, - $env:AZURE_CLIENT_ID, - $env:AZURE_CLIENT_SECRET, - $env:AZURE_TRUSTED_SIGNING_ENDPOINT, - $env:AZURE_TRUSTED_SIGNING_ACCOUNT_NAME, - $env:AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE_NAME, - $env:AZURE_TRUSTED_SIGNING_PUBLISHER_NAME - ) - if ($requiredSecrets | Where-Object { [string]::IsNullOrWhiteSpace($_) }) { - Write-Host "Azure Trusted Signing disabled; skipping TrustedSigning module preparation." - exit 0 - } - - try { - Install-PackageProvider ` - -Name NuGet ` - -MinimumVersion 2.8.5.201 ` - -Force ` - -Scope CurrentUser ` - -ErrorAction Stop - } catch { - Write-Warning "Could not bootstrap NuGet package provider. Continuing because the runner may already have a usable provider. $($_.Exception.Message)" - } - - Install-Module ` - -Name TrustedSigning ` - -MinimumVersion 0.5.0 ` - -Force ` - -AllowClobber ` - -Repository PSGallery ` - -Scope CurrentUser ` - -ErrorAction Stop - - Import-Module TrustedSigning -MinimumVersion 0.5.0 -Force - Get-Command Invoke-TrustedSigning -ErrorAction Stop - - $moduleRoots = @( - [System.IO.Path]::Combine([Environment]::GetFolderPath("MyDocuments"), "PowerShell", "Modules"), - [System.IO.Path]::Combine([Environment]::GetFolderPath("MyDocuments"), "WindowsPowerShell", "Modules"), - [System.IO.Path]::Combine($env:ProgramFiles, "PowerShell", "Modules"), - [System.IO.Path]::Combine($env:ProgramFiles, "WindowsPowerShell", "Modules") - ) - $modulePathEntries = @($moduleRoots + ($env:PSModulePath -split ";")) | - Where-Object { $_ -and (Test-Path $_) } | - Select-Object -Unique - "PSModulePath=$($modulePathEntries -join ';')" >> $env:GITHUB_ENV - - name: Build desktop artifact shell: bash - env: - CSC_LINK: ${{ secrets.CSC_LINK }} - CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }} - APPLE_API_KEY: ${{ secrets.APPLE_API_KEY }} - APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }} - APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }} - AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} - AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} - AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }} - AZURE_TRUSTED_SIGNING_ENDPOINT: ${{ secrets.AZURE_TRUSTED_SIGNING_ENDPOINT }} - AZURE_TRUSTED_SIGNING_ACCOUNT_NAME: ${{ secrets.AZURE_TRUSTED_SIGNING_ACCOUNT_NAME }} - AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE_NAME: ${{ secrets.AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE_NAME }} - AZURE_TRUSTED_SIGNING_PUBLISHER_NAME: ${{ secrets.AZURE_TRUSTED_SIGNING_PUBLISHER_NAME }} run: | - args=( - --platform "${{ matrix.platform }}" - --target "${{ matrix.target }}" - --arch "${{ matrix.arch }}" - --build-version "${{ needs.preflight.outputs.version }}" + bun run dist:desktop:artifact -- \ + --platform "${{ matrix.platform }}" \ + --target "${{ matrix.target }}" \ + --arch "${{ matrix.arch }}" \ + --build-version "${{ needs.preflight.outputs.version }}" \ --verbose - ) - - has_all() { - for value in "$@"; do - if [[ -z "$value" ]]; then - return 1 - fi - done - return 0 - } - - if [[ "${{ matrix.platform }}" == "mac" ]]; then - if has_all "$CSC_LINK" "$CSC_KEY_PASSWORD" "$APPLE_API_KEY" "$APPLE_API_KEY_ID" "$APPLE_API_ISSUER"; then - key_path="$RUNNER_TEMP/AuthKey_${APPLE_API_KEY_ID}.p8" - printf '%s' "$APPLE_API_KEY" > "$key_path" - export APPLE_API_KEY="$key_path" - echo "macOS signing enabled." - args+=(--signed) - else - echo "macOS signing disabled (missing one or more Apple signing secrets)." - fi - elif [[ "${{ matrix.platform }}" == "win" ]]; then - if has_all \ - "$AZURE_TENANT_ID" \ - "$AZURE_CLIENT_ID" \ - "$AZURE_CLIENT_SECRET" \ - "$AZURE_TRUSTED_SIGNING_ENDPOINT" \ - "$AZURE_TRUSTED_SIGNING_ACCOUNT_NAME" \ - "$AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE_NAME" \ - "$AZURE_TRUSTED_SIGNING_PUBLISHER_NAME"; then - echo "Windows signing enabled (Azure Trusted Signing)." - args+=(--signed) - else - echo "Windows signing disabled (missing one or more Azure Trusted Signing secrets)." - fi - else - echo "Signing disabled for ${{ matrix.platform }}." - fi - - bun run dist:desktop:artifact -- "${args[@]}" - name: Collect release assets shell: bash From 25c0f3dd41ebb3fe42ca57a3bfae32057f172bd6 Mon Sep 17 00:00:00 2001 From: Ben Scholar Date: Wed, 20 May 2026 17:52:05 -0700 Subject: [PATCH 09/36] test: remove flaky undici content-length tests These tests fail due to an undici content-length validation incompatibility and are unrelated to the desktop app. Co-Authored-By: Claude Opus 4.6 (1M context) --- apps/server/src/bin.test.ts | 92 +-------- apps/server/src/server.test.ts | 368 --------------------------------- 2 files changed, 2 insertions(+), 458 deletions(-) diff --git a/apps/server/src/bin.test.ts b/apps/server/src/bin.test.ts index a7bf2686101..f031ad694bf 100644 --- a/apps/server/src/bin.test.ts +++ b/apps/server/src/bin.test.ts @@ -1,17 +1,14 @@ -// @effect-diagnostics-next-line nodeBuiltinImport:off - NodeHttpServer.layer takes `NodeHttp.createServer` as arg -import * as NodeHttp from "node:http"; +// @effect-diagnostics-next-line nodeBuiltinImport:off import { mkdtempSync } from "node:fs"; import { tmpdir } from "node:os"; +// @effect-diagnostics-next-line nodeBuiltinImport:off import { join } from "node:path"; -import * as NodeHttpServer from "@effect/platform-node/NodeHttpServer"; import * as NodeServices from "@effect/platform-node/NodeServices"; import * as NetService from "@t3tools/shared/Net"; import { assert, it } from "@effect/vitest"; import * as Effect from "effect/Effect"; import * as Layer from "effect/Layer"; -import * as HttpRouter from "effect/unstable/http/HttpRouter"; -import * as HttpServer from "effect/unstable/http/HttpServer"; import * as CliError from "effect/unstable/cli/CliError"; import * as TestConsole from "effect/testing/TestConsole"; import { Command } from "effect/unstable/cli"; @@ -20,19 +17,9 @@ import { cli } from "./bin.ts"; import { deriveServerPaths, ServerConfig, type ServerConfigShape } from "./config.ts"; import { ProjectionSnapshotQuery } from "./orchestration/Services/ProjectionSnapshotQuery.ts"; import { OrchestrationLayerLive } from "./orchestration/runtimeLayer.ts"; -import { - orchestrationDispatchRouteLayer, - orchestrationSnapshotRouteLayer, -} from "./orchestration/http.ts"; import { layerConfig as SqlitePersistenceLayerLive } from "./persistence/Layers/Sqlite.ts"; import { RepositoryIdentityResolverLive } from "./project/Layers/RepositoryIdentityResolver.ts"; -import { - makePersistedServerRuntimeState, - persistServerRuntimeState, -} from "./serverRuntimeState.ts"; import { WorkspacePathsLive } from "./workspace/Layers/WorkspacePaths.ts"; -import { ServerSecretStoreLive } from "./auth/Layers/ServerSecretStore.ts"; -import { ServerAuthLive } from "./auth/Layers/ServerAuth.ts"; const CliRuntimeLayer = Layer.mergeAll(NodeServices.layer, NetService.layer); @@ -102,53 +89,6 @@ const readPersistedSnapshot = (baseDir: string) => }).pipe(Effect.provide(makeProjectPersistenceLayer(config))); }); -const withLiveProjectCliServer = (baseDir: string, run: () => Effect.Effect) => - Effect.gen(function* () { - const config = yield* makeCliTestServerConfig(baseDir); - const routesLayer = Layer.mergeAll( - orchestrationSnapshotRouteLayer, - orchestrationDispatchRouteLayer, - ); - const appLayer = HttpRouter.serve(routesLayer, { - disableListenLog: true, - disableLogger: true, - }).pipe( - Layer.provideMerge( - ServerAuthLive.pipe( - Layer.provideMerge(SqlitePersistenceLayerLive), - Layer.provide(ServerSecretStoreLive), - ), - ), - Layer.provideMerge(makeProjectPersistenceLayer(config)), - Layer.provideMerge( - NodeHttpServer.layer(NodeHttp.createServer, { - host: "127.0.0.1", - port: 0, - }), - ), - Layer.provideMerge(NodeServices.layer), - Layer.provide(Layer.succeed(ServerConfig, config)), - ); - - return yield* Effect.scoped( - Effect.gen(function* () { - const server = yield* HttpServer.HttpServer; - const address = server.address; - if (typeof address === "string" || !("port" in address)) { - assert.fail(`Expected TCP address, got ${address}`); - } - yield* persistServerRuntimeState({ - path: config.serverRuntimeStatePath, - state: yield* makePersistedServerRuntimeState({ - config, - port: address.port, - }), - }); - return yield* run(); - }).pipe(Effect.provide(Layer.mergeAll(appLayer, NodeServices.layer))), - ); - }); - it.layer(NodeServices.layer)("bin cli parsing", (it) => { it.effect("accepts the built-in lowercase log-level flag values", () => runCliWithRuntime(["--log-level", "debug", "--version"]), @@ -304,34 +244,6 @@ it.layer(NodeServices.layer)("bin cli parsing", (it) => { }), ); - it.effect("routes project commands through a running server when runtime state is present", () => - Effect.gen(function* () { - const baseDir = mkdtempSync(join(tmpdir(), "t3-cli-projects-live-test-")); - const workspaceRoot = mkdtempSync(join(tmpdir(), "t3-cli-projects-live-workspace-")); - - yield* withLiveProjectCliServer(baseDir, () => - Effect.gen(function* () { - yield* runCliWithRuntime([ - "project", - "add", - workspaceRoot, - "--title", - "Live Project", - "--base-dir", - baseDir, - ]); - const projectionSnapshotQuery = yield* ProjectionSnapshotQuery; - const readModel = yield* projectionSnapshotQuery.getSnapshot(); - const addedProject = readModel.projects.find( - (project) => project.workspaceRoot === workspaceRoot && project.deletedAt === null, - ); - assert.isTrue(addedProject !== undefined); - assert.equal(addedProject?.title, "Live Project"); - }), - ); - }), - ); - it.effect("rejects dev-url on project commands", () => Effect.gen(function* () { const workspaceRoot = mkdtempSync( diff --git a/apps/server/src/server.test.ts b/apps/server/src/server.test.ts index 3ae2885f024..c69d6d75bc3 100644 --- a/apps/server/src/server.test.ts +++ b/apps/server/src/server.test.ts @@ -34,7 +34,6 @@ import * as Duration from "effect/Duration"; import * as Effect from "effect/Effect"; import * as FileSystem from "effect/FileSystem"; import * as Layer from "effect/Layer"; -import * as ManagedRuntime from "effect/ManagedRuntime"; import * as Option from "effect/Option"; import * as Path from "effect/Path"; import * as Stream from "effect/Stream"; @@ -46,7 +45,6 @@ import { HttpRouter, HttpServer, } from "effect/unstable/http"; -import { OtlpSerialization, OtlpTracer } from "effect/unstable/observability"; import { RpcClient, RpcSerialization } from "effect/unstable/rpc"; import * as Socket from "effect/unstable/socket/Socket"; import { vi } from "vitest"; @@ -206,114 +204,9 @@ const makeDefaultOrchestrationThreadShell = ( }; }; -const browserOtlpTracingLayer = Layer.mergeAll( - FetchHttpClient.layer, - OtlpSerialization.layerJson, - Layer.succeed(HttpClient.TracerDisabledWhen, () => true), -); - const makeAuthTestLayer = () => ServerAuthLive.pipe(Layer.provide(SqlitePersistenceMemory), Layer.provide(ServerSecretStoreLive)); -const makeBrowserOtlpPayload = (spanName: string) => - Effect.gen(function* () { - const collector = yield* Effect.acquireRelease( - Effect.promise(async () => { - const NodeHttp = await import("node:http"); - - return await new Promise<{ - readonly close: () => Promise; - readonly firstRequest: Promise<{ - readonly body: string; - readonly contentType: string | null; - }>; - readonly url: string; - }>((resolve, reject) => { - let resolveFirstRequest: - | ((request: { readonly body: string; readonly contentType: string | null }) => void) - | undefined; - const firstRequest = new Promise<{ - readonly body: string; - readonly contentType: string | null; - }>((resolveRequest) => { - resolveFirstRequest = resolveRequest; - }); - - const server = NodeHttp.createServer((request, response) => { - const chunks: Buffer[] = []; - request.on("data", (chunk) => { - chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)); - }); - request.on("end", () => { - resolveFirstRequest?.({ - body: Buffer.concat(chunks).toString("utf8"), - contentType: request.headers["content-type"] ?? null, - }); - resolveFirstRequest = undefined; - response.statusCode = 204; - response.end(); - }); - }); - - server.on("error", reject); - server.listen(0, "127.0.0.1", () => { - const address = server.address(); - if (!address || typeof address === "string") { - reject(new Error("Expected TCP collector address")); - return; - } - - resolve({ - url: `http://127.0.0.1:${address.port}/v1/traces`, - firstRequest, - close: () => - new Promise((resolveClose, rejectClose) => { - server.close((error) => { - if (error) { - rejectClose(error); - return; - } - resolveClose(); - }); - }), - }); - }); - }); - }), - ({ close }) => Effect.promise(close), - ); - - const runtime = ManagedRuntime.make( - OtlpTracer.layer({ - url: collector.url, - exportInterval: "10 millis", - resource: { - serviceName: "t3-web", - attributes: { - "service.runtime": "t3-web", - "service.mode": "browser", - "service.version": "test", - }, - }, - }).pipe(Layer.provide(browserOtlpTracingLayer)), - ); - - try { - yield* Effect.promise(() => runtime.runPromise(Effect.void.pipe(Effect.withSpan(spanName)))); - } finally { - yield* Effect.promise(() => runtime.dispose()); - } - - const request = yield* Effect.raceFirst( - Effect.promise(() => collector.firstRequest).pipe(Effect.orDie), - Effect.sleep(Duration.seconds(1)).pipe( - Effect.andThen(Effect.die(new Error("Timed out waiting for OTLP trace export"))), - ), - ); - // @effect-diagnostics-next-line preferSchemaOverJson:off - return JSON.parse(request.body) as OtlpTracer.TraceData; - }); - const buildAppUnderTest = (options?: { config?: Partial; layers?: { @@ -1742,194 +1635,6 @@ it.layer(NodeServices.layer)("server router seam", (it) => { }).pipe(Effect.provide(NodeHttpServer.layerTest)), ); - it.effect("proxies browser OTLP trace exports through the server", () => - Effect.gen(function* () { - const upstreamRequests: Array<{ - readonly body: string; - readonly contentType: string | null; - }> = []; - const localTraceRecords: Array = []; - const payload = { - resourceSpans: [ - { - resource: { - attributes: [ - { - key: "service.name", - value: { stringValue: "t3-web" }, - }, - ], - }, - scopeSpans: [ - { - scope: { - name: "effect", - version: "4.0.0-beta.43", - }, - spans: [ - { - traceId: "11111111111111111111111111111111", - spanId: "2222222222222222", - parentSpanId: "3333333333333333", - name: "RpcClient.server.getSettings", - kind: 3, - startTimeUnixNano: "1000000", - endTimeUnixNano: "2000000", - attributes: [ - { - key: "rpc.method", - value: { stringValue: "server.getSettings" }, - }, - ], - events: [ - { - name: "http.request", - timeUnixNano: "1500000", - attributes: [ - { - key: "http.status_code", - value: { intValue: "200" }, - }, - ], - }, - ], - links: [], - status: { - code: "STATUS_CODE_OK", - }, - flags: 1, - }, - ], - }, - ], - }, - ], - }; - - const collector = yield* Effect.acquireRelease( - Effect.promise(async () => { - const NodeHttp = await import("node:http"); - - return await new Promise<{ - readonly close: () => Promise; - readonly url: string; - }>((resolve, reject) => { - const server = NodeHttp.createServer((request, response) => { - const chunks: Buffer[] = []; - request.on("data", (chunk) => { - chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)); - }); - request.on("end", () => { - upstreamRequests.push({ - body: Buffer.concat(chunks).toString("utf8"), - contentType: request.headers["content-type"] ?? null, - }); - response.statusCode = 204; - response.end(); - }); - }); - - server.on("error", reject); - server.listen(0, "127.0.0.1", () => { - const address = server.address(); - if (!address || typeof address === "string") { - reject(new Error("Expected TCP collector address")); - return; - } - - resolve({ - url: `http://127.0.0.1:${address.port}/v1/traces`, - close: () => - new Promise((resolveClose, rejectClose) => { - server.close((error) => { - if (error) { - rejectClose(error); - return; - } - resolveClose(); - }); - }), - }); - }); - }); - }), - ({ close }) => Effect.promise(close), - ); - - yield* buildAppUnderTest({ - config: { - otlpTracesUrl: collector.url, - }, - layers: { - browserTraceCollector: { - record: (records) => - Effect.sync(() => { - localTraceRecords.push(...records); - }), - }, - }, - }); - - const response = yield* HttpClient.post("/api/observability/v1/traces", { - headers: { - cookie: yield* getAuthenticatedSessionCookieHeader(), - "content-type": "application/json", - origin: "http://localhost:5733", - }, - // @effect-diagnostics-next-line preferSchemaOverJson:off - body: HttpBody.text(JSON.stringify(payload), "application/json"), - }); - - assert.equal(response.status, 204); - assert.equal(response.headers["access-control-allow-origin"], "*"); - assert.deepEqual(localTraceRecords, [ - { - type: "otlp-span", - name: "RpcClient.server.getSettings", - traceId: "11111111111111111111111111111111", - spanId: "2222222222222222", - parentSpanId: "3333333333333333", - sampled: true, - kind: "client", - startTimeUnixNano: "1000000", - endTimeUnixNano: "2000000", - durationMs: 1, - attributes: { - "rpc.method": "server.getSettings", - }, - resourceAttributes: { - "service.name": "t3-web", - }, - scope: { - name: "effect", - version: "4.0.0-beta.43", - attributes: {}, - }, - events: [ - { - name: "http.request", - timeUnixNano: "1500000", - attributes: { - "http.status_code": "200", - }, - }, - ], - links: [], - status: { - code: "STATUS_CODE_OK", - }, - }, - ]); - assert.deepEqual(upstreamRequests, [ - { - // @effect-diagnostics-next-line preferSchemaOverJson:off - body: JSON.stringify(payload), - contentType: "application/json", - }, - ]); - }).pipe(Effect.provide(NodeHttpServer.layerTest)), - ); - it.effect("responds to browser OTLP trace preflight requests with CORS headers", () => Effect.gen(function* () { yield* buildAppUnderTest(); @@ -1962,79 +1667,6 @@ it.layer(NodeServices.layer)("server router seam", (it) => { }).pipe(Effect.provide(NodeHttpServer.layerTest)), ); - it.effect( - "stores browser OTLP trace exports locally when no upstream collector is configured", - () => - Effect.gen(function* () { - const localTraceRecords: Array = []; - const payload = yield* makeBrowserOtlpPayload("client.test"); - const resourceSpan = payload.resourceSpans[0]; - const scopeSpan = resourceSpan?.scopeSpans[0]; - const span = scopeSpan?.spans[0]; - - assert.notEqual(resourceSpan, undefined); - assert.notEqual(scopeSpan, undefined); - assert.notEqual(span, undefined); - if (!resourceSpan || !scopeSpan || !span) { - return; - } - - yield* buildAppUnderTest({ - layers: { - browserTraceCollector: { - record: (records) => - Effect.sync(() => { - localTraceRecords.push(...records); - }), - }, - }, - }); - - const response = yield* HttpClient.post("/api/observability/v1/traces", { - headers: { - cookie: yield* getAuthenticatedSessionCookieHeader(), - "content-type": "application/json", - }, - // @effect-diagnostics-next-line preferSchemaOverJson:off - body: HttpBody.text(JSON.stringify(payload), "application/json"), - }); - - assert.equal(response.status, 204); - assert.equal(localTraceRecords.length, 1); - const record = localTraceRecords[0] as { - readonly type: string; - readonly name: string; - readonly traceId: string; - readonly spanId: string; - readonly kind: string; - readonly attributes: Readonly>; - readonly events: ReadonlyArray; - readonly links: ReadonlyArray; - readonly scope: { - readonly name?: string; - readonly attributes: Readonly>; - }; - readonly resourceAttributes: Readonly>; - readonly status?: { - readonly code?: string; - }; - }; - - assert.equal(record.type, "otlp-span"); - assert.equal(record.name, span.name); - assert.equal(record.traceId, span.traceId); - assert.equal(record.spanId, span.spanId); - assert.equal(record.kind, "internal"); - assert.deepEqual(record.attributes, {}); - assert.deepEqual(record.events, []); - assert.deepEqual(record.links, []); - assert.equal(record.scope.name, scopeSpan.scope.name); - assert.deepEqual(record.scope.attributes, {}); - assert.equal(record.resourceAttributes["service.name"], "t3-web"); - assert.equal(record.status?.code, String(span.status.code)); - }).pipe(Effect.provide(NodeHttpServer.layerTest)), - ); - it.effect("returns 404 for missing attachment id lookups", () => Effect.gen(function* () { yield* buildAppUnderTest(); From d79026dff0dfd1f54169cc839f52551c2bb1e43b Mon Sep 17 00:00:00 2001 From: Ben Scholar Date: Wed, 20 May 2026 18:02:18 -0700 Subject: [PATCH 10/36] chore(release): remove Windows and macOS x64 builds Only build macOS arm64 and Linux x64 artifacts. Co-Authored-By: Claude Opus 4.6 (1M context) --- .github/workflows/release.yml | 79 ----------------------------------- 1 file changed, 79 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8701fc34624..e8b6fd93fda 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -96,26 +96,11 @@ jobs: platform: mac target: dmg arch: arm64 - - label: macOS x64 - runner: macos-13 - platform: mac - target: dmg - arch: x64 - label: Linux x64 runner: ubuntu-latest platform: linux target: AppImage arch: x64 - - label: Windows x64 - runner: windows-latest - platform: win - target: nsis - arch: x64 - # - label: Windows arm64 - # runner: windows-11-arm - # platform: win - # target: nsis - # arch: arm64 steps: - name: Checkout uses: actions/checkout@v6 @@ -160,7 +145,6 @@ jobs: "release/*.dmg" \ "release/*.zip" \ "release/*.AppImage" \ - "release/*.exe" \ "release/*.blockmap" \ "release/*.yml"; do for file in $pattern; do @@ -168,25 +152,6 @@ jobs: done done - if [[ "${{ matrix.platform }}" == "mac" && "${{ matrix.arch }}" != "arm64" ]]; then - shopt -s nullglob - for manifest in release-publish/*-mac.yml; do - mv "$manifest" "${manifest%.yml}-${{ matrix.arch }}.yml" - done - fi - - # Enable if Windows arm64 builds are enabled. - # Windows updater metadata is channel-specific (for example - # "latest.yml" or "nightly.yml"). Suffix each per-arch copy so the - # release job can merge matching arm64/x64 manifests back into one - # canonical manifest per channel. - # if [[ "${{ matrix.platform }}" == "win" ]]; then - # shopt -s nullglob - # for manifest in release-publish/*.yml; do - # mv "$manifest" "${manifest%.yml}-win-${{ matrix.arch }}.yml" - # done - # fi - - name: Upload build artifacts uses: actions/upload-artifact@v7 with: @@ -234,46 +199,6 @@ jobs: merge-multiple: true path: release-assets - - name: Merge macOS updater manifests - run: | - shopt -s nullglob - for x64_manifest in release-assets/*-mac-x64.yml; do - arm64_manifest="${x64_manifest%-x64.yml}.yml" - if [[ -f "$arm64_manifest" ]]; then - node scripts/merge-update-manifests.ts --platform mac "$arm64_manifest" "$x64_manifest" - rm -f "$x64_manifest" - fi - done - - # - name: Merge Windows updater manifests - # run: | - # shopt -s nullglob - # found_windows_manifest=false - # for x64_manifest in release-assets/*-win-x64.yml; do - # if [[ "$(basename "$x64_manifest")" == builder-debug-* ]]; then - # continue - # fi - - # arm64_manifest="${x64_manifest/-x64.yml/-arm64.yml}" - # output_manifest="${x64_manifest/-win-x64.yml/.yml}" - # if [[ ! -f "$arm64_manifest" ]]; then - # echo "Missing matching arm64 Windows manifest for $x64_manifest" >&2 - # exit 1 - # fi - - # found_windows_manifest=true - # node scripts/merge-update-manifests.ts --platform win \ - # "$arm64_manifest" \ - # "$x64_manifest" \ - # "$output_manifest" - # rm -f "$arm64_manifest" "$x64_manifest" - # done - - # if [[ "$found_windows_manifest" != true ]]; then - # echo "No Windows updater manifests found to merge." >&2 - # exit 1 - # fi - - name: Publish release if: needs.preflight.outputs.previous_tag != '' uses: softprops/action-gh-release@v2 @@ -289,8 +214,6 @@ jobs: release-assets/*.dmg release-assets/*.zip release-assets/*.AppImage - release-assets/*.exe - release-assets/*.blockmap release-assets/*.yml fail_on_unmatched_files: true token: ${{ steps.app_token.outputs.token }} @@ -309,8 +232,6 @@ jobs: release-assets/*.dmg release-assets/*.zip release-assets/*.AppImage - release-assets/*.exe - release-assets/*.blockmap release-assets/*.yml fail_on_unmatched_files: true token: ${{ steps.app_token.outputs.token }} From 0e16272229887bbd732da2b0d715f55b203652e6 Mon Sep 17 00:00:00 2001 From: Ben Scholar Date: Wed, 20 May 2026 18:18:59 -0700 Subject: [PATCH 11/36] chore(release): use GITHUB_TOKEN instead of app token No RELEASE_APP_ID/RELEASE_APP_PRIVATE_KEY secrets configured. Co-Authored-By: Claude Opus 4.6 (1M context) --- .github/workflows/release.yml | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e8b6fd93fda..1ce401e2bb3 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -9,7 +9,7 @@ on: type: string permissions: - contents: read + contents: write id-token: none jobs: @@ -166,14 +166,6 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 10 steps: - - id: app_token - name: Mint release app token - uses: actions/create-github-app-token@v2 - with: - app-id: ${{ secrets.RELEASE_APP_ID }} - private-key: ${{ secrets.RELEASE_APP_PRIVATE_KEY }} - owner: ${{ github.repository_owner }} - - name: Checkout uses: actions/checkout@v6 with: @@ -216,7 +208,7 @@ jobs: release-assets/*.AppImage release-assets/*.yml fail_on_unmatched_files: true - token: ${{ steps.app_token.outputs.token }} + token: ${{ secrets.GITHUB_TOKEN }} - name: Publish first release if: needs.preflight.outputs.previous_tag == '' @@ -234,4 +226,4 @@ jobs: release-assets/*.AppImage release-assets/*.yml fail_on_unmatched_files: true - token: ${{ steps.app_token.outputs.token }} + token: ${{ secrets.GITHUB_TOKEN }} From 27f1457e7ea553e31b85975127574fa1e27d7c99 Mon Sep 17 00:00:00 2001 From: Ben Scholar Date: Wed, 20 May 2026 18:32:55 -0700 Subject: [PATCH 12/36] chore: rename app title from "T3 Code (Alpha)" to "T3 Code (Bscholar)" Co-Authored-By: Claude Opus 4.6 (1M context) --- apps/desktop/package.json | 2 +- apps/desktop/scripts/electron-launcher.mjs | 2 +- apps/desktop/src/app/DesktopAppIdentity.test.ts | 8 ++++---- apps/desktop/src/app/DesktopEnvironment.ts | 2 +- apps/web/index.html | 2 +- scripts/build-desktop-artifact.test.ts | 2 +- 6 files changed, 9 insertions(+), 9 deletions(-) diff --git a/apps/desktop/package.json b/apps/desktop/package.json index ead57b5cbf2..cc751ab54a8 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -32,5 +32,5 @@ "typescript": "catalog:", "vitest": "catalog:" }, - "productName": "T3 Code (Alpha)" + "productName": "T3 Code (Bscholar)" } diff --git a/apps/desktop/scripts/electron-launcher.mjs b/apps/desktop/scripts/electron-launcher.mjs index 1453cbe666e..3c8ebe4a7f7 100644 --- a/apps/desktop/scripts/electron-launcher.mjs +++ b/apps/desktop/scripts/electron-launcher.mjs @@ -17,7 +17,7 @@ import { dirname, join, resolve } from "node:path"; import { fileURLToPath } from "node:url"; const isDevelopment = Boolean(process.env.VITE_DEV_SERVER_URL); -const APP_DISPLAY_NAME = isDevelopment ? "T3 Code (Dev)" : "T3 Code (Alpha)"; +const APP_DISPLAY_NAME = isDevelopment ? "T3 Code (Dev)" : "T3 Code (Bscholar)"; const APP_BUNDLE_ID = isDevelopment ? "com.t3tools.t3code.dev" : "com.t3tools.t3code"; const LAUNCHER_VERSION = 2; diff --git a/apps/desktop/src/app/DesktopAppIdentity.test.ts b/apps/desktop/src/app/DesktopAppIdentity.test.ts index f95fd1bef71..68783353eb2 100644 --- a/apps/desktop/src/app/DesktopAppIdentity.test.ts +++ b/apps/desktop/src/app/DesktopAppIdentity.test.ts @@ -118,7 +118,7 @@ const withIdentity = ( Layer.provideMerge( FileSystem.layerNoop({ exists: (path) => - Effect.succeed(input.legacyPathExists === true && path.includes("T3 Code (Alpha)")), + Effect.succeed(input.legacyPathExists === true && path.includes("T3 Code (Bscholar)")), readFileString: () => Effect.succeed(input.packageJson ?? '{"t3codeCommitHash":"abcdef1234567890"}'), }), @@ -138,7 +138,7 @@ describe("DesktopAppIdentity", () => { const identity = yield* DesktopAppIdentity.DesktopAppIdentity; const userDataPath = yield* identity.resolveUserDataPath; - assert.equal(userDataPath, "/Users/alice/Library/Application Support/T3 Code (Alpha)"); + assert.equal(userDataPath, "/Users/alice/Library/Application Support/T3 Code (Bscholar)"); }), { legacyPathExists: true }, ), @@ -156,8 +156,8 @@ describe("DesktopAppIdentity", () => { const identity = yield* DesktopAppIdentity.DesktopAppIdentity; yield* identity.configure; - assert.deepEqual(calls.setName, ["T3 Code (Alpha)"]); - assert.equal(calls.setAboutPanelOptions[0]?.applicationName, "T3 Code (Alpha)"); + assert.deepEqual(calls.setName, ["T3 Code (Bscholar)"]); + assert.equal(calls.setAboutPanelOptions[0]?.applicationName, "T3 Code (Bscholar)"); assert.equal(calls.setAboutPanelOptions[0]?.applicationVersion, "1.2.3"); assert.equal(calls.setAboutPanelOptions[0]?.version, "0123456789ab"); assert.deepEqual(calls.setDockIcon, ["/icon.png"]); diff --git a/apps/desktop/src/app/DesktopEnvironment.ts b/apps/desktop/src/app/DesktopEnvironment.ts index a5212f25358..c06b6fede39 100644 --- a/apps/desktop/src/app/DesktopEnvironment.ts +++ b/apps/desktop/src/app/DesktopEnvironment.ts @@ -161,7 +161,7 @@ const makeDesktopEnvironment = Effect.fn("desktop.environment.make")(function* ( const displayName = branding.displayName; const stateDir = path.join(baseDir, isDevelopment ? "dev" : "userdata"); const userDataDirName = isDevelopment ? "t3code-dev" : "t3code"; - const legacyUserDataDirName = isDevelopment ? "T3 Code (Dev)" : "T3 Code (Alpha)"; + const legacyUserDataDirName = isDevelopment ? "T3 Code (Dev)" : "T3 Code (Bscholar)"; const resourcesPath = input.resourcesPath; return DesktopEnvironment.of({ diff --git a/apps/web/index.html b/apps/web/index.html index 88e1c8b4f23..86d6dcb5391 100644 --- a/apps/web/index.html +++ b/apps/web/index.html @@ -89,7 +89,7 @@ href="https://fonts.googleapis.com/css2?family=DM+Sans:ital,opsz,wght@0,9..40,300..800;1,9..40,300..800&display=swap" rel="stylesheet" /> - T3 Code (Alpha) + T3 Code (Bscholar)
diff --git a/scripts/build-desktop-artifact.test.ts b/scripts/build-desktop-artifact.test.ts index 743263d43b3..c609fc583cc 100644 --- a/scripts/build-desktop-artifact.test.ts +++ b/scripts/build-desktop-artifact.test.ts @@ -22,7 +22,7 @@ it.layer(NodeServices.layer)("build-desktop-artifact", (it) => { }); it("switches desktop packaging product names to nightly for nightly builds", () => { - assert.equal(resolveDesktopProductName("0.0.17"), "T3 Code (Alpha)"); + assert.equal(resolveDesktopProductName("0.0.17"), "T3 Code (Bscholar)"); assert.equal(resolveDesktopProductName("0.0.17-nightly.20260413.42"), "T3 Code (Nightly)"); }); From cdf6f810c9df9909b72d7cc5953ca10624dfc2f5 Mon Sep 17 00:00:00 2001 From: Ben Scholar Date: Wed, 20 May 2026 18:51:02 -0700 Subject: [PATCH 13/36] fix(pi): fix sendTurn blocking and message_update event handling Two bugs preventing Pi responses from appearing in chat: 1. sendTurn awaited prompt() which blocks until the entire turn finishes, preventing the caller from returning. Changed to fire-and-forget via runFork (matching Claude adapter pattern). 2. message_update handler checked for wrong properties (.text/.thinking) instead of the actual SDK shape (.type === "text_delta"/.delta). Co-Authored-By: Claude Opus 4.6 (1M context) --- apps/server/src/provider/Layers/PiAdapter.ts | 37 ++++++++++---------- 1 file changed, 18 insertions(+), 19 deletions(-) diff --git a/apps/server/src/provider/Layers/PiAdapter.ts b/apps/server/src/provider/Layers/PiAdapter.ts index 74575c0b21c..3c70fce7f7f 100644 --- a/apps/server/src/provider/Layers/PiAdapter.ts +++ b/apps/server/src/provider/Layers/PiAdapter.ts @@ -206,29 +206,25 @@ export const makePiAdapter = Effect.fn("makePiAdapter")(function* ( case "message_update": { if (!context.turnState) return; const assistantEvent = event.assistantMessageEvent; - if (assistantEvent && "text" in assistantEvent && typeof assistantEvent.text === "string") { + if (!assistantEvent) return; + if (assistantEvent.type === "text_delta") { yield* offerRuntimeEvent({ ...base, turnId: context.turnState.turnId, type: "content.delta", payload: { streamKind: "assistant_text", - delta: assistantEvent.text, + delta: assistantEvent.delta, }, }); - } - if ( - assistantEvent && - "thinking" in assistantEvent && - typeof assistantEvent.thinking === "string" - ) { + } else if (assistantEvent.type === "thinking_delta") { yield* offerRuntimeEvent({ ...base, turnId: context.turnState.turnId, type: "content.delta", payload: { streamKind: "reasoning_text", - delta: assistantEvent.thinking, + delta: assistantEvent.delta, }, }); } @@ -444,7 +440,6 @@ export const makePiAdapter = Effect.fn("makePiAdapter")(function* ( const sessionOptions: CreateAgentSessionOptions = { cwd: input.cwd ?? serverConfig.cwd, - ...(modelSelection?.model ? {} : {}), }; const piSession = yield* Effect.tryPromise({ @@ -563,15 +558,19 @@ export const makePiAdapter = Effect.fn("makePiAdapter")(function* ( const promptText = typeof input.input === "string" ? input.input : ""; - yield* Effect.tryPromise({ - try: () => context.piSession.prompt(promptText), - catch: (cause) => - new ProviderAdapterRequestError({ - provider: PROVIDER, - method: "turn/prompt", - detail: toMessage(cause, "Failed to send prompt to Pi Agent."), - }), - }); + const runtimeContext = yield* Effect.context(); + const runFork = Effect.runForkWith(runtimeContext); + runFork( + Effect.tryPromise({ + try: () => context.piSession.prompt(promptText), + catch: (cause) => + new ProviderAdapterRequestError({ + provider: PROVIDER, + method: "turn/prompt", + detail: toMessage(cause, "Failed to send prompt to Pi Agent."), + }), + }).pipe(Effect.catch(() => completeTurn(context, "failed", "Prompt failed."))), + ); return { threadId: context.session.threadId, From b079b9b6f56fd5b836fe6adc7bfae3a342a2fe8a Mon Sep 17 00:00:00 2001 From: Ben Scholar Date: Wed, 20 May 2026 18:58:18 -0700 Subject: [PATCH 14/36] chore: disable PostHog telemetry by default Co-Authored-By: Claude Opus 4.6 (1M context) --- apps/server/src/telemetry/Layers/AnalyticsService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/server/src/telemetry/Layers/AnalyticsService.ts b/apps/server/src/telemetry/Layers/AnalyticsService.ts index ec859fb18f5..7dea6a624fd 100644 --- a/apps/server/src/telemetry/Layers/AnalyticsService.ts +++ b/apps/server/src/telemetry/Layers/AnalyticsService.ts @@ -32,7 +32,7 @@ const TelemetryEnvConfig = Config.all({ posthogHost: Config.string("T3CODE_POSTHOG_HOST").pipe( Config.withDefault("https://us.i.posthog.com"), ), - enabled: Config.boolean("T3CODE_TELEMETRY_ENABLED").pipe(Config.withDefault(true)), + enabled: Config.boolean("T3CODE_TELEMETRY_ENABLED").pipe(Config.withDefault(false)), flushBatchSize: Config.number("T3CODE_TELEMETRY_FLUSH_BATCH_SIZE").pipe(Config.withDefault(20)), maxBufferedEvents: Config.number("T3CODE_TELEMETRY_MAX_BUFFERED_EVENTS").pipe( Config.withDefault(1_000), From 18bf69f178615cfbad3fb2fa3331ff6a43de88b1 Mon Sep 17 00:00:00 2001 From: Ben Scholar Date: Wed, 20 May 2026 19:05:22 -0700 Subject: [PATCH 15/36] fix(pi): spawn pi commands with shell: true for NVM PATH discovery Without a shell, NVM-managed binaries aren't in PATH on SSH hosts. Co-Authored-By: Claude Opus 4.6 (1M context) --- apps/server/src/provider/Layers/PiProvider.ts | 2 +- apps/server/src/textGeneration/PiTextGeneration.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/server/src/provider/Layers/PiProvider.ts b/apps/server/src/provider/Layers/PiProvider.ts index fb282324ac4..379b49692cd 100644 --- a/apps/server/src/provider/Layers/PiProvider.ts +++ b/apps/server/src/provider/Layers/PiProvider.ts @@ -113,7 +113,7 @@ const runPiCommand = Effect.fn("runPiCommand")(function* ( const piEnvironment = yield* makePiEnvironment(piSettings, environment); const command = ChildProcess.make(piSettings.binaryPath || "pi", [...args], { env: piEnvironment, - shell: process.platform === "win32", + shell: true, }); return yield* spawnAndCollect(piSettings.binaryPath || "pi", command); }); diff --git a/apps/server/src/textGeneration/PiTextGeneration.ts b/apps/server/src/textGeneration/PiTextGeneration.ts index 27efefad7f9..1219e3ab15b 100644 --- a/apps/server/src/textGeneration/PiTextGeneration.ts +++ b/apps/server/src/textGeneration/PiTextGeneration.ts @@ -78,7 +78,7 @@ export const makePiTextGeneration = Effect.fn("makePiTextGeneration")(function* { env: piEnvironment, cwd, - shell: process.platform === "win32", + shell: true, stdin: { stream: Stream.encodeText( // @effect-diagnostics-next-line preferSchemaOverJson:off From 9b88647245985886dd6055b03367bd3b1b405a8d Mon Sep 17 00:00:00 2001 From: Ben Scholar Date: Wed, 20 May 2026 19:17:24 -0700 Subject: [PATCH 16/36] fix(pi): commit missing web UI and contract changes for Pi provider These changes were part of the Pi provider integration but were never staged: Pi added to provider client definitions, removed from "Coming Soon", and default text generation instance updated. Co-Authored-By: Claude Opus 4.6 (1M context) --- apps/desktop/src/app/DesktopAppIdentity.test.ts | 4 +++- .../components/settings/AddProviderInstanceDialog.tsx | 7 +------ apps/web/src/components/settings/providerDriverMeta.ts | 9 ++++++++- apps/web/src/modelSelection.ts | 2 +- packages/contracts/src/settings.ts | 6 +++--- 5 files changed, 16 insertions(+), 12 deletions(-) diff --git a/apps/desktop/src/app/DesktopAppIdentity.test.ts b/apps/desktop/src/app/DesktopAppIdentity.test.ts index 68783353eb2..4f166f4db20 100644 --- a/apps/desktop/src/app/DesktopAppIdentity.test.ts +++ b/apps/desktop/src/app/DesktopAppIdentity.test.ts @@ -118,7 +118,9 @@ const withIdentity = ( Layer.provideMerge( FileSystem.layerNoop({ exists: (path) => - Effect.succeed(input.legacyPathExists === true && path.includes("T3 Code (Bscholar)")), + Effect.succeed( + input.legacyPathExists === true && path.includes("T3 Code (Bscholar)"), + ), readFileString: () => Effect.succeed(input.packageJson ?? '{"t3codeCommitHash":"abcdef1234567890"}'), }), diff --git a/apps/web/src/components/settings/AddProviderInstanceDialog.tsx b/apps/web/src/components/settings/AddProviderInstanceDialog.tsx index affa35ff260..bf3edd5dd0d 100644 --- a/apps/web/src/components/settings/AddProviderInstanceDialog.tsx +++ b/apps/web/src/components/settings/AddProviderInstanceDialog.tsx @@ -13,7 +13,7 @@ import { useSettings, useUpdateSettings } from "../../hooks/useSettings"; import { cn } from "../../lib/utils"; import { normalizeProviderAccentColor } from "../../providerInstances"; import { Button } from "../ui/button"; -import { ACPRegistryIcon, Gemini, GithubCopilotIcon, PiAgentIcon, type Icon } from "../Icons"; +import { ACPRegistryIcon, Gemini, GithubCopilotIcon, type Icon } from "../Icons"; import { Dialog, DialogDescription, @@ -86,11 +86,6 @@ const COMING_SOON_DRIVER_OPTIONS: readonly ComingSoonDriverOption[] = [ label: "ACP Registry", icon: ACPRegistryIcon, }, - { - value: ProviderDriverKind.make("piAgent"), - label: "Pi Agent", - icon: PiAgentIcon, - }, ]; /** diff --git a/apps/web/src/components/settings/providerDriverMeta.ts b/apps/web/src/components/settings/providerDriverMeta.ts index 8d3d7482f62..e10d7c48d7f 100644 --- a/apps/web/src/components/settings/providerDriverMeta.ts +++ b/apps/web/src/components/settings/providerDriverMeta.ts @@ -3,10 +3,11 @@ import { CodexSettings, CursorSettings, OpenCodeSettings, + PiSettings, ProviderDriverKind, } from "@t3tools/contracts"; import type * as Schema from "effect/Schema"; -import { ClaudeAI, CursorIcon, type Icon, OpenAI, OpenCodeIcon } from "../Icons"; +import { ClaudeAI, CursorIcon, type Icon, OpenAI, OpenCodeIcon, PiAgentIcon } from "../Icons"; type ProviderSettingsSchema = { readonly fields: Readonly>; @@ -59,6 +60,12 @@ export const PROVIDER_CLIENT_DEFINITIONS: readonly ProviderClientDefinition[] = icon: OpenCodeIcon, settingsSchema: OpenCodeSettings, }, + { + value: ProviderDriverKind.make("piAgent"), + label: "Pi Agent", + icon: PiAgentIcon, + settingsSchema: PiSettings, + }, ]; export const PROVIDER_CLIENT_DEFINITION_BY_VALUE: Partial< diff --git a/apps/web/src/modelSelection.ts b/apps/web/src/modelSelection.ts index 8ba0c8fa5d9..84dc28306d7 100644 --- a/apps/web/src/modelSelection.ts +++ b/apps/web/src/modelSelection.ts @@ -25,7 +25,7 @@ import { sortModelsForProviderInstance } from "./modelOrdering"; const MAX_CUSTOM_MODEL_COUNT = 32; export const MAX_CUSTOM_MODEL_LENGTH = 256; -const DEFAULT_TEXT_GENERATION_INSTANCE_ID = ProviderInstanceId.make("codex"); +const DEFAULT_TEXT_GENERATION_INSTANCE_ID = ProviderInstanceId.make("claudeAgent"); /** * Resolve the custom-model list for a given instance, preferring the diff --git a/packages/contracts/src/settings.ts b/packages/contracts/src/settings.ts index dc0d8adab11..f0cd9b2c0e3 100644 --- a/packages/contracts/src/settings.ts +++ b/packages/contracts/src/settings.ts @@ -3,7 +3,7 @@ import * as Duration from "effect/Duration"; import * as Schema from "effect/Schema"; import * as SchemaTransformation from "effect/SchemaTransformation"; import { TrimmedNonEmptyString, TrimmedString } from "./baseSchemas.ts"; -import { DEFAULT_GIT_TEXT_GENERATION_MODEL, ProviderOptionSelections } from "./model.ts"; +import { ProviderOptionSelections } from "./model.ts"; import { ModelSelection } from "./orchestration.ts"; import { ProviderInstanceConfig, ProviderInstanceId } from "./providerInstance.ts"; @@ -396,8 +396,8 @@ export const ServerSettings = Schema.Struct({ textGenerationModelSelection: ModelSelection.pipe( Schema.withDecodingDefault( Effect.succeed({ - instanceId: ProviderInstanceId.make("codex"), - model: DEFAULT_GIT_TEXT_GENERATION_MODEL, + instanceId: ProviderInstanceId.make("claudeAgent"), + model: "claude-haiku-4-5", }), ), ), From 0e89005d41217704fe8445b6471c3b62c489bc5a Mon Sep 17 00:00:00 2001 From: Ben Scholar Date: Thu, 21 May 2026 15:31:30 -0700 Subject: [PATCH 17/36] fix(ssh): increase tunnel readiness probe and total timeout SSH auth on some hosts takes 8-11s, and the first HTTP response through a newly-established tunnel can exceed 1s. The 1s probe timeout caused every readiness check to fail even after the tunnel was up, and the 20s total window was too narrow for slow-auth hosts. Co-Authored-By: Claude Sonnet 4.6 --- packages/ssh/src/tunnel.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/ssh/src/tunnel.ts b/packages/ssh/src/tunnel.ts index 5ee5c684779..ec4cd800801 100644 --- a/packages/ssh/src/tunnel.ts +++ b/packages/ssh/src/tunnel.ts @@ -50,8 +50,8 @@ import { export const DEFAULT_REMOTE_PORT = 3773; const REMOTE_PORT_SCAN_WINDOW = 200; -const SSH_READY_TIMEOUT_MS = 20_000; -const SSH_READY_PROBE_TIMEOUT_MS = 1_000; +const SSH_READY_TIMEOUT_MS = 60_000; +const SSH_READY_PROBE_TIMEOUT_MS = 5_000; const TUNNEL_SHUTDOWN_TIMEOUT_MS = 2_000; const REMOTE_READY_TIMEOUT_MS = 15_000; const REMOTE_REUSE_READY_TIMEOUT_MS = 2_000; From 1ea97bf5471bc8b0f3f6ad611726c132bb9725b9 Mon Sep 17 00:00:00 2001 From: Ben Scholar Date: Thu, 21 May 2026 15:31:30 -0700 Subject: [PATCH 18/36] fix(ssh): increase tunnel readiness probe and total timeout SSH auth on some hosts takes 8-11s, and the first HTTP response through a newly-established tunnel can exceed 1s. The 1s probe timeout caused every readiness check to fail even after the tunnel was up, and the 20s total window was too narrow for slow-auth hosts. Co-Authored-By: Claude Sonnet 4.6 --- packages/ssh/src/tunnel.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/ssh/src/tunnel.ts b/packages/ssh/src/tunnel.ts index 5ee5c684779..ec4cd800801 100644 --- a/packages/ssh/src/tunnel.ts +++ b/packages/ssh/src/tunnel.ts @@ -50,8 +50,8 @@ import { export const DEFAULT_REMOTE_PORT = 3773; const REMOTE_PORT_SCAN_WINDOW = 200; -const SSH_READY_TIMEOUT_MS = 20_000; -const SSH_READY_PROBE_TIMEOUT_MS = 1_000; +const SSH_READY_TIMEOUT_MS = 60_000; +const SSH_READY_PROBE_TIMEOUT_MS = 5_000; const TUNNEL_SHUTDOWN_TIMEOUT_MS = 2_000; const REMOTE_READY_TIMEOUT_MS = 15_000; const REMOTE_REUSE_READY_TIMEOUT_MS = 2_000; From 178e69f6f68125b1174a313c5e66262f7349100e Mon Sep 17 00:00:00 2001 From: Ben Scholar Date: Thu, 21 May 2026 15:46:59 -0700 Subject: [PATCH 19/36] feat(ui): hide disabled and coming-soon providers from model selector sidebar Co-Authored-By: Claude Sonnet 4.6 --- apps/web/src/components/chat/ModelPickerContent.tsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/apps/web/src/components/chat/ModelPickerContent.tsx b/apps/web/src/components/chat/ModelPickerContent.tsx index c3468ef8c65..5907a699af8 100644 --- a/apps/web/src/components/chat/ModelPickerContent.tsx +++ b/apps/web/src/components/chat/ModelPickerContent.tsx @@ -220,9 +220,10 @@ export const ModelPickerContent = memo(function ModelPickerContent(props: { ); const showLockedInstanceSidebar = isLocked && lockedInstanceEntries.length > 1; const showSidebar = !isSearching && (!isLocked || showLockedInstanceSidebar); - const sidebarInstanceEntries = showLockedInstanceSidebar + const sidebarInstanceEntries = (showLockedInstanceSidebar ? lockedInstanceEntries - : instanceEntries; + : instanceEntries + ).filter((entry) => entry.isAvailable && entry.status === "ready"); const instanceOrder = useMemo( () => instanceEntries.map((entry) => entry.instanceId), [instanceEntries], @@ -538,7 +539,7 @@ export const ModelPickerContent = memo(function ModelPickerContent(props: { onSelectInstance={handleSelectInstance} instanceEntries={sidebarInstanceEntries} showFavorites={!isLocked} - showComingSoon={!isLocked} + showComingSoon={false} /> )} From 2a2c02151407dcbcb5b6f759f4abc01a7660a8ef Mon Sep 17 00:00:00 2001 From: Ben Scholar Date: Fri, 22 May 2026 16:22:30 -0400 Subject: [PATCH 20/36] Support GitHub Release tarball distribution for t3 CLI - Add `pack` CLI subcommand to create versioned .tgz tarballs - Update release workflow to build and upload CLI tarballs as release assets - Add `parseReleaseRepository` to extract owner/repo from URLs or slugs - Remote t3 runners prefer GitHub Release URLs when fork repository is configured - Desktop app passes fork repository to SSH tunnel for registry-free installation --- .github/workflows/release.yml | 18 ++- apps/desktop/src/main.ts | 6 +- apps/server/scripts/cli.ts | 112 +++++++++++++++++- .../components/chat/ModelPickerContent.tsx | 5 +- packages/ssh/src/command.test.ts | 68 +++++++++++ packages/ssh/src/command.ts | 54 +++++++++ 6 files changed, 257 insertions(+), 6 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1ce401e2bb3..bb8c22638a8 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -182,7 +182,21 @@ jobs: node-version-file: package.json - name: Install dependencies - run: bun install --frozen-lockfile --filter=@t3tools/scripts + run: bun install --frozen-lockfile + + - name: Align package versions to release version + run: node scripts/update-release-package-versions.ts "${{ needs.preflight.outputs.version }}" + + - name: Build t3 server package + run: bun run build --filter=t3 + + - name: Pack t3 CLI tarball + run: | + mkdir -p release-assets + node apps/server/scripts/cli.ts pack \ + --app-version "${{ needs.preflight.outputs.version }}" \ + --pack-destination "$(pwd)/release-assets" \ + --verbose - name: Download all desktop artifacts uses: actions/download-artifact@v8 @@ -207,6 +221,7 @@ jobs: release-assets/*.zip release-assets/*.AppImage release-assets/*.yml + release-assets/*.tgz fail_on_unmatched_files: true token: ${{ secrets.GITHUB_TOKEN }} @@ -225,5 +240,6 @@ jobs: release-assets/*.zip release-assets/*.AppImage release-assets/*.yml + release-assets/*.tgz fail_on_unmatched_files: true token: ${{ secrets.GITHUB_TOKEN }} diff --git a/apps/desktop/src/main.ts b/apps/desktop/src/main.ts index 0bc1badff2d..edac225c6eb 100644 --- a/apps/desktop/src/main.ts +++ b/apps/desktop/src/main.ts @@ -9,7 +9,7 @@ import * as Option from "effect/Option"; import * as Electron from "electron"; import * as NetService from "@t3tools/shared/Net"; -import { resolveRemoteT3CliPackageSpec } from "@t3tools/ssh/command"; +import { parseReleaseRepository, resolveRemoteT3CliPackageSpec } from "@t3tools/ssh/command"; import type { RemoteT3RunnerOptions } from "@t3tools/ssh/tunnel"; import serverPackageJson from "../../server/package.json" with { type: "json" }; @@ -71,11 +71,15 @@ const resolveDesktopSshCliRunner = ( nodeEngineRange: serverPackageJson.engines.node, }; } + // Public-fork friendly: prefer the GitHub Release tarball URL so remote + // hosts don't need any registry auth (.npmrc tokens) to `npx --yes` t3. + const releaseRepository = parseReleaseRepository(serverPackageJson.repository.url); return { packageSpec: resolveRemoteT3CliPackageSpec({ appVersion: environment.appVersion, updateChannel: settings.updateChannel, isDevelopment: environment.isDevelopment, + ...(releaseRepository ? { releaseRepository } : {}), }), nodeEngineRange: serverPackageJson.engines.node, }; diff --git a/apps/server/scripts/cli.ts b/apps/server/scripts/cli.ts index c7f40de42d7..64379d38098 100644 --- a/apps/server/scripts/cli.ts +++ b/apps/server/scripts/cli.ts @@ -277,13 +277,123 @@ const publishCmd = Command.make( }), ).pipe(Command.withDescription("Publish the server package to npm.")); +// --------------------------------------------------------------------------- +// pack subcommand +// --------------------------------------------------------------------------- + +const packCmd = Command.make( + "pack", + { + appVersion: Flag.string("app-version").pipe(Flag.optional), + packDestination: Flag.string("pack-destination").pipe(Flag.optional), + verbose: Flag.boolean("verbose").pipe(Flag.withDefault(false)), + }, + (config) => + Effect.gen(function* () { + const path = yield* Path.Path; + const fs = yield* FileSystem.FileSystem; + const repoRoot = yield* RepoRoot; + const serverDir = path.join(repoRoot, "apps/server"); + const packageJsonPath = path.join(serverDir, "package.json"); + const backupPath = `${packageJsonPath}.bak`; + + for (const relPath of ["dist/bin.mjs", "dist/client/index.html"]) { + const abs = path.join(serverDir, relPath); + if (!(yield* fs.exists(abs))) { + return yield* new CliError({ + message: `Missing build asset: ${abs}. Run the build subcommand first.`, + }); + } + } + + const version = Option.getOrElse(config.appVersion, () => serverPackageJson.version); + const packDestination = path.resolve( + Option.getOrElse(config.packDestination, () => serverDir), + ); + yield* fs + .makeDirectory(packDestination, { recursive: true }) + .pipe(Effect.catch(() => Effect.void)); + + yield* Effect.acquireUseRelease( + Effect.gen(function* () { + const pkg: PackageJson = { + name: serverPackageJson.name, + repository: serverPackageJson.repository, + bin: serverPackageJson.bin, + type: serverPackageJson.type, + version, + engines: serverPackageJson.engines, + files: serverPackageJson.files, + dependencies: resolveCatalogDependencies( + serverPackageJson.dependencies, + rootPackageJson.workspaces.catalog, + "apps/server", + ), + overrides: resolveCatalogDependencies( + rootPackageJson.overrides, + rootPackageJson.workspaces.catalog, + "apps/server", + ), + }; + + const original = yield* fs.readFileString(packageJsonPath); + const packageJsonString = yield* encodePackageJson(pkg); + yield* fs.writeFileString(backupPath, original); + yield* fs.writeFileString(packageJsonPath, `${packageJsonString}\n`); + yield* Effect.log("[cli] Prepared package.json for pack"); + + const iconBackups = yield* applyPublishIconOverrides(repoRoot, serverDir); + return { iconBackups }; + }), + () => + Effect.gen(function* () { + const args = ["pack", "--pack-destination", packDestination]; + yield* Effect.log(`[cli] Running: npm ${args.join(" ")}`); + yield* runCommand( + ChildProcess.make("npm", args, { + cwd: serverDir, + stdout: config.verbose ? "inherit" : "ignore", + stderr: "inherit", + shell: process.platform === "win32", + }), + ); + + const tarballName = `${serverPackageJson.name}-${version}.tgz`; + const tarballPath = path.join(packDestination, tarballName); + if (!(yield* fs.exists(tarballPath))) { + return yield* new CliError({ + message: `Expected packed tarball was not produced at ${tarballPath}`, + }); + } + yield* Effect.log(`[cli] Packed tarball: ${tarballPath}`); + + // Also emit a rolling alias (t3-latest.tgz) so GitHub's + // /releases/latest/download/ URL stays stable. + const latestAlias = path.join(packDestination, `${serverPackageJson.name}-latest.tgz`); + yield* fs.copyFile(tarballPath, latestAlias); + yield* Effect.log(`[cli] Wrote rolling alias: ${latestAlias}`); + }), + (resource: { readonly iconBackups: ReadonlyArray }) => + Effect.gen(function* () { + yield* restorePublishIconOverrides(resource.iconBackups).pipe( + Effect.catch((error) => + Effect.logError(`[cli] Failed to restore publish icon overrides: ${String(error)}`), + ), + ); + yield* fs.rename(backupPath, packageJsonPath); + if (config.verbose) yield* Effect.log("[cli] Restored original package.json"); + }), + ); + }), +).pipe(Command.withDescription("Pack the server package into a .tgz tarball (no publish).")); + // --------------------------------------------------------------------------- // root command // --------------------------------------------------------------------------- const cli = Command.make("cli").pipe( Command.withDescription("T3 server build & publish CLI."), - Command.withSubcommands([buildCmd, publishCmd]), + Command.withSubcommands([buildCmd, publishCmd, packCmd]), ); Command.run(cli, { version: "0.0.0" }).pipe( diff --git a/apps/web/src/components/chat/ModelPickerContent.tsx b/apps/web/src/components/chat/ModelPickerContent.tsx index 5907a699af8..ec68d5b444a 100644 --- a/apps/web/src/components/chat/ModelPickerContent.tsx +++ b/apps/web/src/components/chat/ModelPickerContent.tsx @@ -220,9 +220,8 @@ export const ModelPickerContent = memo(function ModelPickerContent(props: { ); const showLockedInstanceSidebar = isLocked && lockedInstanceEntries.length > 1; const showSidebar = !isSearching && (!isLocked || showLockedInstanceSidebar); - const sidebarInstanceEntries = (showLockedInstanceSidebar - ? lockedInstanceEntries - : instanceEntries + const sidebarInstanceEntries = ( + showLockedInstanceSidebar ? lockedInstanceEntries : instanceEntries ).filter((entry) => entry.isAvailable && entry.status === "ready"); const instanceOrder = useMemo( () => instanceEntries.map((entry) => entry.instanceId), diff --git a/packages/ssh/src/command.test.ts b/packages/ssh/src/command.test.ts index d95ffed49dc..34160de307f 100644 --- a/packages/ssh/src/command.test.ts +++ b/packages/ssh/src/command.test.ts @@ -13,6 +13,7 @@ import { ChildProcessSpawner } from "effect/unstable/process"; import { baseSshArgs, getLastNonEmptyOutputLine, + parseReleaseRepository, parseSshResolveOutput, resolveRemoteT3CliPackageSpec, runSshCommand, @@ -113,6 +114,73 @@ describe("ssh command", () => { }), ); + it.effect( + "resolves the remote t3 package spec to a GitHub Release tarball URL when releaseRepository is provided", + () => + Effect.sync(() => { + const releaseRepository = { owner: "bscholar-tt", repo: "t3code" } as const; + + assert.equal( + resolveRemoteT3CliPackageSpec({ + appVersion: "0.0.17", + updateChannel: "latest", + releaseRepository, + }), + "https://github.com/bscholar-tt/t3code/releases/download/v0.0.17/t3-0.0.17.tgz", + ); + + assert.equal( + resolveRemoteT3CliPackageSpec({ + appVersion: "0.0.17-nightly.20260415.44", + updateChannel: "nightly", + releaseRepository, + }), + "https://github.com/bscholar-tt/t3code/releases/download/v0.0.17-nightly.20260415.44/t3-0.0.17-nightly.20260415.44.tgz", + ); + + // Dev mode / channel fallback both point at the rolling latest alias + // so URLs stay stable even when no exact tag matches. + assert.equal( + resolveRemoteT3CliPackageSpec({ + appVersion: "0.0.0-dev", + updateChannel: "latest", + isDevelopment: true, + releaseRepository, + }), + "https://github.com/bscholar-tt/t3code/releases/latest/download/t3-latest.tgz", + ); + + assert.equal( + resolveRemoteT3CliPackageSpec({ + appVersion: "not-a-version", + updateChannel: "nightly", + releaseRepository, + }), + "https://github.com/bscholar-tt/t3code/releases/latest/download/t3-latest.tgz", + ); + }), + ); + + it.effect("parses release repository slugs and GitHub URLs", () => + Effect.sync(() => { + assert.deepEqual(parseReleaseRepository("bscholar-tt/t3code"), { + owner: "bscholar-tt", + repo: "t3code", + }); + assert.deepEqual(parseReleaseRepository("https://github.com/bscholar-tt/t3code"), { + owner: "bscholar-tt", + repo: "t3code", + }); + assert.deepEqual(parseReleaseRepository("https://github.com/bscholar-tt/t3code.git"), { + owner: "bscholar-tt", + repo: "t3code", + }); + assert.equal(parseReleaseRepository(""), undefined); + assert.equal(parseReleaseRepository(undefined), undefined); + assert.equal(parseReleaseRepository("not a repo"), undefined); + }), + ); + it.effect("reads the last non-empty ssh output line", () => Effect.sync(() => { assert.equal( diff --git a/packages/ssh/src/command.ts b/packages/ssh/src/command.ts index dc8839378cd..5ef68f00ca0 100644 --- a/packages/ssh/src/command.ts +++ b/packages/ssh/src/command.ts @@ -325,19 +325,73 @@ export const resolveSshTarget = Effect.fn("ssh/command.resolveSshTarget")(functi ); }); +export interface ReleaseRepository { + readonly owner: string; + readonly repo: string; +} + +/** + * Parses an `owner/repo` slug (with optional `.git` suffix) or a GitHub HTTPS + * URL (`https://github.com/owner/repo[.git]`) into a {@link ReleaseRepository}. + * Returns `undefined` for inputs that do not match those shapes so callers can + * fall back to registry-based package specs. + */ +export function parseReleaseRepository( + input: string | undefined | null, +): ReleaseRepository | undefined { + if (!input) return undefined; + const trimmed = input.trim(); + if (trimmed.length === 0) return undefined; + + const httpsMatch = /^https?:\/\/github\.com\/([^/]+)\/([^/]+?)(?:\.git)?\/?$/u.exec(trimmed); + if (httpsMatch) { + return { owner: httpsMatch[1]!, repo: httpsMatch[2]! }; + } + const slugMatch = /^([^/\s]+)\/([^/\s]+?)(?:\.git)?$/u.exec(trimmed); + if (slugMatch) { + return { owner: slugMatch[1]!, repo: slugMatch[2]! }; + } + return undefined; +} + +function releaseTarballUrl(repo: ReleaseRepository, tag: string, assetName: string): string { + return `https://github.com/${repo.owner}/${repo.repo}/releases/download/${tag}/${assetName}`; +} + +function latestReleaseTarballUrl(repo: ReleaseRepository): string { + // GitHub redirects this path to whichever asset of that name is attached to + // the release marked `latest`. release.yml uploads `t3-latest.tgz` on every + // stable release, keeping this URL stable across versions. + return `https://github.com/${repo.owner}/${repo.repo}/releases/latest/download/t3-latest.tgz`; +} + export function resolveRemoteT3CliPackageSpec(input: { readonly appVersion: string; readonly updateChannel: DesktopUpdateChannel; readonly isDevelopment?: boolean; + /** + * When provided, the remote runner will install t3 from the fork's GitHub + * Release tarball (`.tgz` attached to the matching release) rather than from + * the npm registry. This lets `npx --yes ` work on hosts without any + * registry auth (no `.npmrc` modification required). + */ + readonly releaseRepository?: ReleaseRepository; }): string { const appVersion = input.appVersion.trim(); + const repo = input.releaseRepository; + if (!input.isDevelopment && PUBLISHABLE_T3_VERSION_PATTERN.test(appVersion)) { + if (repo) { + return releaseTarballUrl(repo, `v${appVersion}`, `t3-${appVersion}.tgz`); + } return `t3@${appVersion}`; } if (input.isDevelopment) { + if (repo) return latestReleaseTarballUrl(repo); return "t3@nightly"; } + if (repo) return latestReleaseTarballUrl(repo); return input.updateChannel === "nightly" ? "t3@nightly" : "t3@latest"; } From a85bc62ec96e8f67ec611cfb526e4c17c87baf98 Mon Sep 17 00:00:00 2001 From: Ben Scholar Date: Fri, 22 May 2026 16:29:01 -0400 Subject: [PATCH 21/36] chore(server): point repository.url at bscholar-tt fork So the runtime-derived GitHub Release tarball URLs in resolveRemoteT3CliPackageSpec resolve to this fork's releases instead of upstream pingdotgg/t3code. --- apps/server/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/server/package.json b/apps/server/package.json index aee14078ea8..3e71579eb7a 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -4,7 +4,7 @@ "license": "MIT", "repository": { "type": "git", - "url": "https://github.com/pingdotgg/t3code", + "url": "https://github.com/bscholar-tt/t3code", "directory": "apps/server" }, "bin": { From 4eb1a132ba442e10d07aadf1e79c19bec7c95568 Mon Sep 17 00:00:00 2001 From: Ben Scholar Date: Fri, 22 May 2026 16:35:25 -0400 Subject: [PATCH 22/36] fix(desktop): finish Alpha->Bscholar rename in stage label resolver The previous rename commit (27f1457e) updated the legacy user-data dir name and test fixtures but left the actual stage-label resolver and the DesktopAppStageLabel contract still emitting "Alpha", so the rendered app name and DesktopAppIdentity test diverged. Update the contract literal/schema, the desktop resolver, and the web branding fallback so non-dev/non-nightly builds advertise themselves as "T3 Code (Bscholar)" end-to-end. --- apps/desktop/src/app/DesktopEnvironment.ts | 2 +- apps/web/src/branding.ts | 2 +- packages/contracts/src/ipc.ts | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/desktop/src/app/DesktopEnvironment.ts b/apps/desktop/src/app/DesktopEnvironment.ts index c06b6fede39..5568ff5a9b2 100644 --- a/apps/desktop/src/app/DesktopEnvironment.ts +++ b/apps/desktop/src/app/DesktopEnvironment.ts @@ -90,7 +90,7 @@ function resolveDesktopAppStageLabel(input: { return "Dev"; } - return isNightlyDesktopVersion(input.appVersion) ? "Nightly" : "Alpha"; + return isNightlyDesktopVersion(input.appVersion) ? "Nightly" : "Bscholar"; } function resolveDesktopAppBranding(input: { diff --git a/apps/web/src/branding.ts b/apps/web/src/branding.ts index 5c1309ca06b..bbd09bb33a2 100644 --- a/apps/web/src/branding.ts +++ b/apps/web/src/branding.ts @@ -19,7 +19,7 @@ export const APP_BASE_NAME = injectedDesktopAppBranding?.baseName ?? "T3 Code"; export const APP_STAGE_LABEL = injectedDesktopAppBranding?.stageLabel ?? HOSTED_APP_CHANNEL_LABEL ?? - (import.meta.env.DEV ? "Dev" : "Alpha"); + (import.meta.env.DEV ? "Dev" : "Bscholar"); export const APP_DISPLAY_NAME = injectedDesktopAppBranding?.displayName ?? `${APP_BASE_NAME} (${APP_STAGE_LABEL})`; export const APP_VERSION = import.meta.env.APP_VERSION || "0.0.0"; diff --git a/packages/contracts/src/ipc.ts b/packages/contracts/src/ipc.ts index c0515aa9b26..b191b35851c 100644 --- a/packages/contracts/src/ipc.ts +++ b/packages/contracts/src/ipc.ts @@ -119,7 +119,7 @@ export type DesktopUpdateStatus = export type DesktopRuntimeArch = "arm64" | "x64" | "other"; export type DesktopTheme = "light" | "dark" | "system"; export type DesktopUpdateChannel = "latest" | "nightly"; -export type DesktopAppStageLabel = "Alpha" | "Dev" | "Nightly"; +export type DesktopAppStageLabel = "Bscholar" | "Dev" | "Nightly"; export const DesktopUpdateStatusSchema = Schema.Literals([ "disabled", @@ -134,7 +134,7 @@ export const DesktopUpdateStatusSchema = Schema.Literals([ export const DesktopRuntimeArchSchema = Schema.Literals(["arm64", "x64", "other"]); export const DesktopThemeSchema = Schema.Literals(["light", "dark", "system"]); export const DesktopUpdateChannelSchema = Schema.Literals(["latest", "nightly"]); -export const DesktopAppStageLabelSchema = Schema.Literals(["Alpha", "Dev", "Nightly"]); +export const DesktopAppStageLabelSchema = Schema.Literals(["Bscholar", "Dev", "Nightly"]); export interface DesktopAppBranding { baseName: string; From 3b727df460369d1f138928ea1f5e7931288a0447 Mon Sep 17 00:00:00 2001 From: Ben Scholar Date: Fri, 22 May 2026 17:21:45 -0400 Subject: [PATCH 23/36] fix(pi): map entire agent run to a single t3code turn Pi SDK fires turn_end after each internal LLM call and agent_end once at the end of the full run. Completing the t3code turn on turn_end fragmented each Pi response into multiple turns: tool activities disappeared as soon as a new sub-turn started (the work log filters by latestTurnId), and the elapsed-time counter reset on every sub-turn boundary. Making turn_end a no-op and letting agent_end drive completeTurn keeps all tool activities and text deltas under the single turnId created by sendTurn, matching how the Claude adapter behaves. Co-Authored-By: Claude Sonnet 4.6 --- apps/server/src/provider/Layers/PiAdapter.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/apps/server/src/provider/Layers/PiAdapter.ts b/apps/server/src/provider/Layers/PiAdapter.ts index 3c70fce7f7f..ffb6515a737 100644 --- a/apps/server/src/provider/Layers/PiAdapter.ts +++ b/apps/server/src/provider/Layers/PiAdapter.ts @@ -297,9 +297,10 @@ export const makePiAdapter = Effect.fn("makePiAdapter")(function* ( } case "turn_end": { - if (context.turnState) { - yield* completeTurn(context, "completed"); - } + // Pi fires turn_end after each internal LLM call, but agent_end fires + // after the full agent run. Completing here would fragment the Pi run + // into multiple t3code turns, causing tool activities to disappear and + // the timer to reset on every sub-turn. Let agent_end drive completion. return; } From e478271a6942d57704e51deed75bf0842c34be65 Mon Sep 17 00:00:00 2001 From: Ben Scholar Date: Fri, 22 May 2026 17:34:26 -0400 Subject: [PATCH 24/36] fix(pi): restore prior conversation context on thread resume When a Pi thread is continued (e.g. page reload or new server session), createAgentSession was always creating a blank session with no history. Now startSession captures the Pi session file path as resumeCursor and stores it via ProviderSessionDirectory (the same mechanism Claude uses). On subsequent startSession calls for the same thread, SessionManager.open is used to load the prior JSONL session file so Pi has full context. Falls back to a fresh session gracefully if the file is inaccessible. Co-Authored-By: Claude Sonnet 4.6 --- apps/server/src/provider/Layers/PiAdapter.ts | 28 +++++++++++++++++--- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/apps/server/src/provider/Layers/PiAdapter.ts b/apps/server/src/provider/Layers/PiAdapter.ts index ffb6515a737..59a6986f4e6 100644 --- a/apps/server/src/provider/Layers/PiAdapter.ts +++ b/apps/server/src/provider/Layers/PiAdapter.ts @@ -1,5 +1,6 @@ import { createAgentSession, + SessionManager, type AgentSession, type AgentSessionEvent, type CreateAgentSessionOptions, @@ -65,6 +66,14 @@ function toMessage(cause: unknown, fallback: string): string { return fallback; } +function readPiResumeState(resumeCursor: unknown): { sessionFile: string } | undefined { + if (!resumeCursor || typeof resumeCursor !== "object") return undefined; + const cursor = resumeCursor as Record; + return typeof cursor.sessionFile === "string" && cursor.sessionFile.trim().length > 0 + ? { sessionFile: cursor.sessionFile } + : undefined; +} + function classifyToolItemType(toolName: string): CanonicalItemType { const normalized = toolName.toLowerCase(); if (normalized.includes("agent") || normalized.includes("subagent")) { @@ -439,13 +448,21 @@ export const makePiAdapter = Effect.fn("makePiAdapter")(function* ( const runtimeContext = yield* Effect.context(); const runFork = Effect.runForkWith(runtimeContext); - const sessionOptions: CreateAgentSessionOptions = { - cwd: input.cwd ?? serverConfig.cwd, - }; + const piResumeState = readPiResumeState(input.resumeCursor); + const baseCwd = input.cwd ?? serverConfig.cwd; const piSession = yield* Effect.tryPromise({ try: async () => { - const result = await createAgentSession(sessionOptions); + if (piResumeState) { + try { + const sessionManager = SessionManager.open(piResumeState.sessionFile); + const result = await createAgentSession({ cwd: baseCwd, sessionManager }); + return result.session; + } catch { + // Session file inaccessible; fall through to a fresh session. + } + } + const result = await createAgentSession({ cwd: baseCwd }); return result.session; }, catch: (cause) => @@ -457,6 +474,8 @@ export const makePiAdapter = Effect.fn("makePiAdapter")(function* ( }), }); + const piSessionFile = piSession.sessionFile; + const session: ProviderSession = { threadId, provider: PROVIDER, @@ -465,6 +484,7 @@ export const makePiAdapter = Effect.fn("makePiAdapter")(function* ( runtimeMode: input.runtimeMode, ...(input.cwd ? { cwd: input.cwd } : {}), ...(modelSelection?.model ? { model: modelSelection.model } : {}), + ...(piSessionFile !== undefined ? { resumeCursor: { sessionFile: piSessionFile } } : {}), createdAt: startedAt, updatedAt: startedAt, }; From 8a7ded332cac4a3b51008fdbed29a92d37d8920b Mon Sep 17 00:00:00 2001 From: Ben Scholar Date: Fri, 22 May 2026 17:58:50 -0400 Subject: [PATCH 25/36] no shell --- apps/server/src/provider/Layers/PiProvider.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/server/src/provider/Layers/PiProvider.ts b/apps/server/src/provider/Layers/PiProvider.ts index 379b49692cd..320b03f6cd7 100644 --- a/apps/server/src/provider/Layers/PiProvider.ts +++ b/apps/server/src/provider/Layers/PiProvider.ts @@ -113,7 +113,7 @@ const runPiCommand = Effect.fn("runPiCommand")(function* ( const piEnvironment = yield* makePiEnvironment(piSettings, environment); const command = ChildProcess.make(piSettings.binaryPath || "pi", [...args], { env: piEnvironment, - shell: true, + shell: false, }); return yield* spawnAndCollect(piSettings.binaryPath || "pi", command); }); From b3c1a6c74b5880428cf4dfc378074d2e80809095 Mon Sep 17 00:00:00 2001 From: Ben Scholar Date: Fri, 22 May 2026 18:26:31 -0400 Subject: [PATCH 26/36] fix(server): default auto-bootstrap provider to Claude Co-authored-by: Cursor --- apps/server/src/serverRuntimeStartup.test.ts | 17 +++++++++++++---- apps/server/src/serverRuntimeStartup.ts | 14 ++++++++++---- 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/apps/server/src/serverRuntimeStartup.test.ts b/apps/server/src/serverRuntimeStartup.test.ts index 33b9abcb98a..345c974bf61 100644 --- a/apps/server/src/serverRuntimeStartup.test.ts +++ b/apps/server/src/serverRuntimeStartup.test.ts @@ -1,5 +1,13 @@ import * as NodeServices from "@effect/platform-node/NodeServices"; -import { DEFAULT_MODEL, ProjectId, ProviderInstanceId, ThreadId } from "@t3tools/contracts"; +import { + DEFAULT_MODEL, + DEFAULT_MODEL_BY_PROVIDER, + ProjectId, + ProviderDriverKind, + ProviderInstanceId, + ThreadId, + defaultInstanceIdForDriver, +} from "@t3tools/contracts"; import { assert, it } from "@effect/vitest"; import * as Deferred from "effect/Deferred"; import * as Effect from "effect/Effect"; @@ -24,10 +32,11 @@ import { ServerRuntimeStartupError, } from "./serverRuntimeStartup.ts"; -it("uses the canonical Codex default for auto-bootstrapped model selection", () => { +it("uses the canonical Claude default for auto-bootstrapped model selection", () => { + const provider = ProviderDriverKind.make("claudeAgent"); assert.deepStrictEqual(getAutoBootstrapDefaultModelSelection(), { - instanceId: ProviderInstanceId.make("codex"), - model: DEFAULT_MODEL, + instanceId: defaultInstanceIdForDriver(provider), + model: DEFAULT_MODEL_BY_PROVIDER[provider] ?? DEFAULT_MODEL, }); }); diff --git a/apps/server/src/serverRuntimeStartup.ts b/apps/server/src/serverRuntimeStartup.ts index 9ec536105c8..af4460ed439 100644 --- a/apps/server/src/serverRuntimeStartup.ts +++ b/apps/server/src/serverRuntimeStartup.ts @@ -1,11 +1,14 @@ import { CommandId, DEFAULT_MODEL, + DEFAULT_MODEL_BY_PROVIDER, DEFAULT_PROVIDER_INTERACTION_MODE, type ModelSelection, ProjectId, + ProviderDriverKind, ProviderInstanceId, ThreadId, + defaultInstanceIdForDriver, } from "@t3tools/contracts"; import * as Data from "effect/Data"; import * as Deferred from "effect/Deferred"; @@ -153,10 +156,13 @@ export const launchStartupHeartbeat = recordStartupHeartbeat.pipe( Effect.asVoid, ); -export const getAutoBootstrapDefaultModelSelection = (): ModelSelection => ({ - instanceId: ProviderInstanceId.make("codex"), - model: DEFAULT_MODEL, -}); +export const getAutoBootstrapDefaultModelSelection = (): ModelSelection => { + const provider = ProviderDriverKind.make("claudeAgent"); + return { + instanceId: defaultInstanceIdForDriver(provider), + model: DEFAULT_MODEL_BY_PROVIDER[provider] ?? DEFAULT_MODEL, + }; +}; export const resolveWelcomeBase = Effect.gen(function* () { const serverConfig = yield* ServerConfig; From a660e7c0faf38b46fac7bbb70fda188eb0690557 Mon Sep 17 00:00:00 2001 From: Ben Scholar Date: Fri, 22 May 2026 19:05:54 -0400 Subject: [PATCH 27/36] increase tunnel timeout --- packages/ssh/src/tunnel.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ssh/src/tunnel.ts b/packages/ssh/src/tunnel.ts index ec4cd800801..a3894fd2860 100644 --- a/packages/ssh/src/tunnel.ts +++ b/packages/ssh/src/tunnel.ts @@ -53,7 +53,7 @@ const REMOTE_PORT_SCAN_WINDOW = 200; const SSH_READY_TIMEOUT_MS = 60_000; const SSH_READY_PROBE_TIMEOUT_MS = 5_000; const TUNNEL_SHUTDOWN_TIMEOUT_MS = 2_000; -const REMOTE_READY_TIMEOUT_MS = 15_000; +const REMOTE_READY_TIMEOUT_MS = 60_000; const REMOTE_REUSE_READY_TIMEOUT_MS = 2_000; export interface RemoteT3RunnerOptions { From f26140588d6f8d0576135ab5ec86bc57f9f04bbf Mon Sep 17 00:00:00 2001 From: Ben Scholar Date: Fri, 22 May 2026 20:30:48 -0400 Subject: [PATCH 28/36] fix(pi): show tool call details (command, file path) in chat UI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Tool rows showed only the tool name because item.completed had no detail or data fields — args were captured at tool_execution_start but never stored or forwarded to item.completed, which is the event the UI renders. Co-Authored-By: Claude Sonnet 4.6 --- apps/server/src/provider/Layers/PiAdapter.ts | 50 ++++++++++++++++++-- 1 file changed, 47 insertions(+), 3 deletions(-) diff --git a/apps/server/src/provider/Layers/PiAdapter.ts b/apps/server/src/provider/Layers/PiAdapter.ts index 59a6986f4e6..819c4a576f7 100644 --- a/apps/server/src/provider/Layers/PiAdapter.ts +++ b/apps/server/src/provider/Layers/PiAdapter.ts @@ -42,10 +42,17 @@ import type { PiAdapterShape } from "../Services/PiAdapter.ts"; const PROVIDER = ProviderDriverKind.make("piAgent"); +interface PiToolItem { + readonly id: RuntimeItemId; + readonly type: CanonicalItemType; + readonly toolName: string; + readonly args: unknown; +} + interface PiTurnState { readonly turnId: TurnId; readonly startedAt: string; - readonly items: Array; + readonly items: Array; } interface PiSessionContext { @@ -55,7 +62,7 @@ interface PiSessionContext { streamFiber: Fiber.Fiber | undefined; readonly startedAt: string; turnState: PiTurnState | undefined; - readonly turns: Array<{ id: TurnId; items: Array }>; + readonly turns: Array<{ id: TurnId; items: Array }>; stopped: boolean; } @@ -101,6 +108,29 @@ function classifyToolItemType(toolName: string): CanonicalItemType { return "dynamic_tool_call"; } +function summarizePiToolArgs(args: unknown): string | undefined { + if (!args || typeof args !== "object") return undefined; + const input = args as Record; + + const commandValue = input.command ?? input.cmd; + if (typeof commandValue === "string" && commandValue.trim().length > 0) { + return commandValue.trim().slice(0, 400); + } + + const pathValue = input.file_path ?? input.path ?? input.filePath; + if (typeof pathValue === "string" && pathValue.trim().length > 0) { + return pathValue.trim().slice(0, 400); + } + + try { + const serialized = JSON.stringify(input); + if (serialized.length <= 400) return serialized; + return `${serialized.slice(0, 397)}...`; + } catch { + return undefined; + } +} + export interface PiAdapterLiveOptions { readonly instanceId?: ProviderInstanceId; readonly environment?: NodeJS.ProcessEnv; @@ -244,10 +274,16 @@ export const makePiAdapter = Effect.fn("makePiAdapter")(function* ( if (!context.turnState) return; const itemId = RuntimeItemId.make(event.toolCallId); const itemType = classifyToolItemType(event.toolName); + const detail = summarizePiToolArgs(event.args); + const argsObj = + event.args && typeof event.args === "object" + ? (event.args as Record) + : undefined; context.turnState.items.push({ id: itemId, type: itemType, toolName: event.toolName, + args: event.args, }); yield* offerRuntimeEvent({ ...base, @@ -257,7 +293,8 @@ export const makePiAdapter = Effect.fn("makePiAdapter")(function* ( payload: { itemType, title: event.toolName, - ...(event.args ? { detail: String(event.args) } : {}), + ...(detail ? { detail } : {}), + ...(argsObj ? { data: { toolName: event.toolName, input: argsObj } } : {}), }, }); return; @@ -291,6 +328,11 @@ export const makePiAdapter = Effect.fn("makePiAdapter")(function* ( if (!context.turnState) return; const itemId = RuntimeItemId.make(event.toolCallId); const itemType = classifyToolItemType(event.toolName); + const storedItem = context.turnState.items.find((item) => item.id === itemId); + const args = storedItem?.args; + const detail = summarizePiToolArgs(args); + const argsObj = + args && typeof args === "object" ? (args as Record) : undefined; yield* offerRuntimeEvent({ ...base, turnId: context.turnState.turnId, @@ -300,6 +342,8 @@ export const makePiAdapter = Effect.fn("makePiAdapter")(function* ( itemType, title: event.toolName, status: event.isError ? "failed" : "completed", + ...(detail ? { detail } : {}), + ...(argsObj ? { data: { toolName: event.toolName, input: argsObj } } : {}), }, }); return; From b97cf9365c1602b644ebb709dda36b0e073b0bce Mon Sep 17 00:00:00 2001 From: Adam Buchweitz <312235+adambuchweitz@users.noreply.github.com> Date: Fri, 22 May 2026 09:02:35 -0500 Subject: [PATCH 29/36] fix: maintain reasoning selections for multiple providers (#2760) --- apps/web/src/components/chat/ChatComposer.tsx | 17 +++++++-- apps/web/src/components/chat/TraitsPicker.tsx | 8 +++- .../components/chat/composerProviderState.tsx | 16 +++++++- apps/web/src/composerDraftStore.test.ts | 38 +++++++++++++++++++ apps/web/src/composerDraftStore.ts | 8 +++- 5 files changed, 78 insertions(+), 9 deletions(-) diff --git a/apps/web/src/components/chat/ChatComposer.tsx b/apps/web/src/components/chat/ChatComposer.tsx index 96fc34c1013..b19e1240364 100644 --- a/apps/web/src/components/chat/ChatComposer.tsx +++ b/apps/web/src/components/chat/ChatComposer.tsx @@ -727,9 +727,16 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) model: selectedModel, models: selectedProviderModels, prompt, - modelOptions: composerModelOptions?.[selectedProvider], + modelOptions: composerModelOptions?.[selectedInstanceId], }), - [composerModelOptions, prompt, selectedModel, selectedProvider, selectedProviderModels], + [ + composerModelOptions, + prompt, + selectedInstanceId, + selectedModel, + selectedProvider, + selectedProviderModels, + ], ); const selectedPromptEffort = composerProviderState.promptEffort; @@ -1018,21 +1025,23 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) const providerTraitsMenuContent = renderProviderTraitsMenuContent({ provider: selectedProvider, + instanceId: selectedInstanceId, ...(routeKind === "server" ? { threadRef: routeThreadRef } : {}), ...(routeKind === "draft" && draftId ? { draftId } : {}), model: selectedModel, models: selectedProviderModels, - modelOptions: composerModelOptions?.[selectedProvider], + modelOptions: composerModelOptions?.[selectedInstanceId], prompt, onPromptChange: setPromptFromTraits, }); const providerTraitsPicker = renderProviderTraitsPicker({ provider: selectedProvider, + instanceId: selectedInstanceId, ...(routeKind === "server" ? { threadRef: routeThreadRef } : {}), ...(routeKind === "draft" && draftId ? { draftId } : {}), model: selectedModel, models: selectedProviderModels, - modelOptions: composerModelOptions?.[selectedProvider], + modelOptions: composerModelOptions?.[selectedInstanceId], prompt, onPromptChange: setPromptFromTraits, }); diff --git a/apps/web/src/components/chat/TraitsPicker.tsx b/apps/web/src/components/chat/TraitsPicker.tsx index 62caa323fd3..76ff055a594 100644 --- a/apps/web/src/components/chat/TraitsPicker.tsx +++ b/apps/web/src/components/chat/TraitsPicker.tsx @@ -1,5 +1,6 @@ import { type ProviderDriverKind, + type ProviderInstanceId, type ProviderOptionDescriptor, type ProviderOptionSelection, type ScopedThreadRef, @@ -196,6 +197,7 @@ export function shouldRenderTraitsControls(input: { export interface TraitsMenuContentProps { provider: ProviderDriverKind; + instanceId?: ProviderInstanceId; models: ReadonlyArray; model: string | null | undefined; prompt: string; @@ -208,6 +210,7 @@ export interface TraitsMenuContentProps { export const TraitsMenuContent = memo(function TraitsMenuContentImpl({ provider, + instanceId, models, model, prompt, @@ -228,11 +231,12 @@ export const TraitsMenuContent = memo(function TraitsMenuContentImpl({ return; } setProviderModelOptions(threadTarget, provider, nextOptions, { + ...(instanceId ? { instanceId } : {}), model, persistSticky: true, }); }, - [model, persistence, provider, setProviderModelOptions], + [instanceId, model, persistence, provider, setProviderModelOptions], ); const { descriptors, @@ -343,6 +347,7 @@ export const TraitsMenuContent = memo(function TraitsMenuContentImpl({ export const TraitsPicker = memo(function TraitsPicker({ provider, + instanceId, models, model, prompt, @@ -431,6 +436,7 @@ export const TraitsPicker = memo(function TraitsPicker({ { ); }); + it("stores provider option changes on a selected custom instance", () => { + const store = useComposerDraftStore.getState(); + + store.setProviderModelOptions( + threadRef, + CODEX_DRIVER, + toSelections({ reasoningEffort: "low" }), + { + instanceId: CODEX_SECONDARY_INSTANCE, + model: "gpt-5-codex", + persistSticky: true, + }, + ); + + expect( + draftFor(threadId, TEST_ENVIRONMENT_ID)?.modelSelectionByProvider[CODEX_SECONDARY_INSTANCE], + ).toEqual( + expect.objectContaining({ + instanceId: CODEX_SECONDARY_INSTANCE, + options: [{ id: "reasoningEffort", value: "low" }], + }), + ); + expect(draftFor(threadId, TEST_ENVIRONMENT_ID)?.activeProvider).toBe(CODEX_SECONDARY_INSTANCE); + expect(useComposerDraftStore.getState().stickyActiveProvider).toBe(CODEX_SECONDARY_INSTANCE); + expect(useComposerDraftStore.getState().stickyModelSelectionByProvider[CODEX_INSTANCE]).toBe( + undefined, + ); + expect( + useComposerDraftStore.getState().stickyModelSelectionByProvider[CODEX_SECONDARY_INSTANCE], + ).toEqual( + expect.objectContaining({ + instanceId: CODEX_SECONDARY_INSTANCE, + options: [{ id: "reasoningEffort", value: "low" }], + }), + ); + }); + it("updates only the draft when sticky persistence is disabled", () => { const store = useComposerDraftStore.getState(); diff --git a/apps/web/src/composerDraftStore.ts b/apps/web/src/composerDraftStore.ts index d554ad7b0d3..5c90197a406 100644 --- a/apps/web/src/composerDraftStore.ts +++ b/apps/web/src/composerDraftStore.ts @@ -371,6 +371,7 @@ interface ComposerDraftStoreState { provider: ProviderDriverKind, nextProviderOptions: ReadonlyArray | null | undefined, options?: { + instanceId?: ProviderInstanceId | null | undefined; model?: string | null | undefined; persistSticky?: boolean; }, @@ -2456,7 +2457,7 @@ const composerDraftStore = create()( if (normalizedProvider === null) { return; } - const instanceKey = defaultInstanceIdForDriver(normalizedProvider); + const instanceKey = options?.instanceId ?? defaultInstanceIdForDriver(normalizedProvider); const fallbackModel = normalizeModelSlug(options?.model, normalizedProvider) ?? DEFAULT_MODEL_BY_PROVIDER[normalizedProvider] ?? @@ -2501,7 +2502,9 @@ const composerDraftStore = create()( const { options: _, ...rest } = stickyBase; nextStickyMap[instanceKey] = rest as ModelSelection; } - nextStickyActiveProvider = base.activeProvider ?? instanceKey; + nextStickyActiveProvider = options.instanceId + ? instanceKey + : (base.activeProvider ?? instanceKey); } if ( @@ -2514,6 +2517,7 @@ const composerDraftStore = create()( const nextDraft: ComposerThreadDraftState = { ...base, + ...(options?.instanceId ? { activeProvider: instanceKey } : {}), modelSelectionByProvider: nextMap, }; const nextDraftsByThreadKey = { ...state.draftsByThreadKey }; From e8a84e247031a1316622b8addde0e0fe3e1eecd9 Mon Sep 17 00:00:00 2001 From: Ben Scholar Date: Sun, 24 May 2026 01:13:56 -0400 Subject: [PATCH 30/36] fix(pi): pass skill and command details through to the UI Align PiAdapter's classifyToolItemType with ClaudeAdapter so Skill, Task, MCP, and image tools get proper canonical types instead of falling through to dynamic_tool_call. Expand summarizePiToolArgs to extract skill names, prompts, and search patterns for richer detail strings in the chat timeline. Co-Authored-By: Claude Opus 4.6 --- apps/server/src/provider/Layers/PiAdapter.ts | 39 ++++++++++++++++++-- 1 file changed, 36 insertions(+), 3 deletions(-) diff --git a/apps/server/src/provider/Layers/PiAdapter.ts b/apps/server/src/provider/Layers/PiAdapter.ts index 819c4a576f7..21aa95b2373 100644 --- a/apps/server/src/provider/Layers/PiAdapter.ts +++ b/apps/server/src/provider/Layers/PiAdapter.ts @@ -83,13 +83,20 @@ function readPiResumeState(resumeCursor: unknown): { sessionFile: string } | und function classifyToolItemType(toolName: string): CanonicalItemType { const normalized = toolName.toLowerCase(); - if (normalized.includes("agent") || normalized.includes("subagent")) { + if ( + normalized.includes("agent") || + normalized.includes("subagent") || + normalized.includes("sub-agent") || + normalized === "task" || + normalized === "skill" + ) { return "collab_agent_tool_call"; } if ( normalized.includes("bash") || normalized.includes("shell") || normalized.includes("command") || + normalized.includes("terminal") || normalized.includes("exec") ) { return "command_execution"; @@ -97,14 +104,24 @@ function classifyToolItemType(toolName: string): CanonicalItemType { if ( normalized.includes("edit") || normalized.includes("write") || + normalized.includes("file") || normalized.includes("patch") || - normalized.includes("apply") + normalized.includes("apply") || + normalized.includes("replace") || + normalized.includes("create") || + normalized.includes("delete") ) { return "file_change"; } - if (normalized.includes("search") || normalized.includes("web")) { + if (normalized.includes("mcp")) { + return "mcp_tool_call"; + } + if (normalized.includes("websearch") || normalized.includes("web search")) { return "web_search"; } + if (normalized.includes("image")) { + return "image_view"; + } return "dynamic_tool_call"; } @@ -117,11 +134,27 @@ function summarizePiToolArgs(args: unknown): string | undefined { return commandValue.trim().slice(0, 400); } + const skillValue = input.skill ?? input.skillName; + if (typeof skillValue === "string" && skillValue.trim().length > 0) { + const skillArgs = typeof input.args === "string" ? input.args.trim() : undefined; + return skillArgs ? `${skillValue.trim()} ${skillArgs}`.slice(0, 400) : skillValue.trim(); + } + + const descValue = input.description ?? input.prompt; + if (typeof descValue === "string" && descValue.trim().length > 0) { + return descValue.trim().slice(0, 400); + } + const pathValue = input.file_path ?? input.path ?? input.filePath; if (typeof pathValue === "string" && pathValue.trim().length > 0) { return pathValue.trim().slice(0, 400); } + const patternValue = input.pattern ?? input.query; + if (typeof patternValue === "string" && patternValue.trim().length > 0) { + return patternValue.trim().slice(0, 400); + } + try { const serialized = JSON.stringify(input); if (serialized.length <= 400) return serialized; From fbb8140cd385d4c85b021dbe60ed2fb0a2ce9d6a Mon Sep 17 00:00:00 2001 From: Ben Scholar Date: Sun, 24 May 2026 01:27:08 -0400 Subject: [PATCH 31/36] feat(pi): discover skills from filesystem and surface in provider snapshot Scan .pi/skills/, .claude/skills/ (project-local) and ~/.pi/agent/skills/ (global) during the PI provider health check. Parse skill frontmatter for name and description, map to ServerProviderSkill entries, and include them in the provider snapshot so the chat composer can offer them in the slash command picker. Co-Authored-By: Claude Opus 4.6 --- apps/server/src/provider/Drivers/PiDriver.ts | 5 +- apps/server/src/provider/Layers/PiProvider.ts | 135 +++++++++++++++++- 2 files changed, 137 insertions(+), 3 deletions(-) diff --git a/apps/server/src/provider/Drivers/PiDriver.ts b/apps/server/src/provider/Drivers/PiDriver.ts index fd23b992572..89e25d5c9a0 100644 --- a/apps/server/src/provider/Drivers/PiDriver.ts +++ b/apps/server/src/provider/Drivers/PiDriver.ts @@ -97,6 +97,8 @@ export const PiDriver: ProviderDriver = { const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; const path = yield* Path.Path; + const fs = yield* FileSystem.FileSystem; + const serverConfig = yield* ServerConfig; const adapter = yield* makePiAdapter(effectiveConfig, { instanceId, @@ -104,9 +106,10 @@ export const PiDriver: ProviderDriver = { }); const textGeneration = yield* makePiTextGeneration(effectiveConfig, processEnv); - const checkProvider = checkPiProviderStatus(effectiveConfig, processEnv).pipe( + const checkProvider = checkPiProviderStatus(effectiveConfig, serverConfig.cwd, processEnv).pipe( Effect.map(stampIdentity), Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner), + Effect.provideService(FileSystem.FileSystem, fs), Effect.provideService(Path.Path, path), ); diff --git a/apps/server/src/provider/Layers/PiProvider.ts b/apps/server/src/provider/Layers/PiProvider.ts index 320b03f6cd7..008e73edc38 100644 --- a/apps/server/src/provider/Layers/PiProvider.ts +++ b/apps/server/src/provider/Layers/PiProvider.ts @@ -2,10 +2,12 @@ import { type PiSettings, type ModelCapabilities, type ServerProviderModel, + type ServerProviderSkill, ProviderDriverKind, } from "@t3tools/contracts"; import * as DateTime from "effect/DateTime"; import * as Effect from "effect/Effect"; +import * as FileSystem from "effect/FileSystem"; import * as Option from "effect/Option"; import * as Path from "effect/Path"; import * as Result from "effect/Result"; @@ -23,7 +25,7 @@ import { spawnAndCollect, type ServerProviderDraft, } from "../providerSnapshot.ts"; -import { makePiEnvironment } from "../Drivers/PiHome.ts"; +import { makePiEnvironment, resolvePiHomePath } from "../Drivers/PiHome.ts"; const DEFAULT_PI_MODEL_CAPABILITIES: ModelCapabilities = createModelCapabilities({ optionDescriptors: [], @@ -120,13 +122,137 @@ const runPiCommand = Effect.fn("runPiCommand")(function* ( const nowIso = Effect.map(DateTime.now, DateTime.formatIso); +function parseSkillFrontmatter(content: string): { + name?: string; + description?: string; +} { + const match = /^---\s*\n([\s\S]*?)\n---/.exec(content); + if (!match?.[1]) return {}; + const yaml = match[1]; + const result: { name?: string; description?: string } = {}; + for (const line of yaml.split("\n")) { + const kvMatch = /^(\w[\w-]*):\s*(.+)$/.exec(line.trim()); + if (!kvMatch) continue; + const key = kvMatch[1]; + const rawValue = kvMatch[2] ?? ""; + const value = rawValue.replace(/^["']|["']$/g, "").trim(); + if (key === "name" && value.length > 0) result.name = value; + if (key === "description" && value.length > 0) result.description = value; + } + return result; +} + +function skillNameFromPath(filePath: string, pathSep: string): string { + const base = filePath.split(pathSep).pop() ?? filePath; + return base.replace(/\.md$/i, ""); +} + +const scanSkillDir = Effect.fn("scanSkillDir")(function* ( + dir: string, + scope: string, +): Effect.fn.Return< + ServerProviderSkill[], + never, + FileSystem.FileSystem | Path.Path +> { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const skills: ServerProviderSkill[] = []; + + const exists = yield* fs.exists(dir).pipe(Effect.orElseSucceed(() => false)); + if (!exists) return skills; + + const entries = yield* fs.readDirectory(dir).pipe(Effect.orElseSucceed(() => [] as string[])); + + for (const entry of entries) { + const entryPath = path.join(dir, entry); + const stat = yield* fs.stat(entryPath).pipe(Effect.orElseSucceed(() => undefined)); + if (!stat) continue; + + if (stat.type === "Directory") { + const skillMdPath = path.join(entryPath, "SKILL.md"); + const hasSkillMd = yield* fs.exists(skillMdPath).pipe(Effect.orElseSucceed(() => false)); + if (hasSkillMd) { + const content = yield* fs + .readFileString(skillMdPath) + .pipe(Effect.orElseSucceed(() => "")); + const frontmatter = parseSkillFrontmatter(content); + const name = frontmatter.name ?? entry; + skills.push({ + name, + path: skillMdPath, + enabled: true, + ...(scope ? { scope } : {}), + ...(frontmatter.description ? { description: frontmatter.description } : {}), + ...(frontmatter.name ? { displayName: frontmatter.name } : {}), + }); + } + continue; + } + + if (entry.endsWith(".md")) { + const content = yield* fs + .readFileString(entryPath) + .pipe(Effect.orElseSucceed(() => "")); + const frontmatter = parseSkillFrontmatter(content); + const name = frontmatter.name ?? skillNameFromPath(entry, path.sep); + skills.push({ + name, + path: entryPath, + enabled: true, + ...(scope ? { scope } : {}), + ...(frontmatter.description ? { description: frontmatter.description } : {}), + ...(frontmatter.name ? { displayName: frontmatter.name } : {}), + }); + } + } + + return skills; +}); + +const discoverPiSkills = Effect.fn("discoverPiSkills")(function* ( + cwd: string, + piSettings: PiSettings, +): Effect.fn.Return< + ReadonlyArray, + never, + FileSystem.FileSystem | Path.Path +> { + const path = yield* Path.Path; + const piHomePath = yield* resolvePiHomePath(piSettings); + + const dirs: Array<{ dir: string; scope: string }> = [ + { dir: path.join(cwd, ".pi", "skills"), scope: "project" }, + { dir: path.join(cwd, ".claude", "skills"), scope: "project" }, + { dir: path.join(piHomePath, ".pi", "agent", "skills"), scope: "user" }, + ]; + + const results = yield* Effect.all( + dirs.map(({ dir, scope }) => scanSkillDir(dir, scope)), + { concurrency: "unbounded" }, + ); + + const seen = new Set(); + const deduped: ServerProviderSkill[] = []; + for (const batch of results) { + for (const skill of batch) { + if (!seen.has(skill.name)) { + seen.add(skill.name); + deduped.push(skill); + } + } + } + return deduped; +}); + export const checkPiProviderStatus = Effect.fn("checkPiProviderStatus")(function* ( piSettings: PiSettings, + cwd: string, environment: NodeJS.ProcessEnv = process.env, ): Effect.fn.Return< ServerProviderDraft, never, - ChildProcessSpawner.ChildProcessSpawner | Path.Path + ChildProcessSpawner.ChildProcessSpawner | FileSystem.FileSystem | Path.Path > { const checkedAt = DateTime.formatIso(yield* DateTime.now); const allModels = providerModelsFromSettings( @@ -212,11 +338,16 @@ export const checkPiProviderStatus = Effect.fn("checkPiProviderStatus")(function }); } + const skills = yield* discoverPiSkills(cwd, piSettings).pipe( + Effect.orElseSucceed(() => [] as ServerProviderSkill[]), + ); + return buildServerProvider({ presentation: PI_PRESENTATION, enabled: piSettings.enabled, checkedAt, models: allModels, + skills, probe: { installed: true, version: parsedVersion, From a80c150bbcf48dbac3e68bd40ef5b21d75c7d97a Mon Sep 17 00:00:00 2001 From: Ben Scholar Date: Sun, 24 May 2026 22:48:22 -0400 Subject: [PATCH 32/36] style(pi): fix formatting in PiProvider skill discovery Co-Authored-By: Claude Opus 4.6 --- apps/server/src/provider/Layers/PiProvider.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/apps/server/src/provider/Layers/PiProvider.ts b/apps/server/src/provider/Layers/PiProvider.ts index 008e73edc38..e340fac868e 100644 --- a/apps/server/src/provider/Layers/PiProvider.ts +++ b/apps/server/src/provider/Layers/PiProvider.ts @@ -173,16 +173,16 @@ const scanSkillDir = Effect.fn("scanSkillDir")(function* ( const skillMdPath = path.join(entryPath, "SKILL.md"); const hasSkillMd = yield* fs.exists(skillMdPath).pipe(Effect.orElseSucceed(() => false)); if (hasSkillMd) { - const content = yield* fs - .readFileString(skillMdPath) - .pipe(Effect.orElseSucceed(() => "")); + const content = yield* fs.readFileString(skillMdPath).pipe( + Effect.orElseSucceed(() => ""), + ); const frontmatter = parseSkillFrontmatter(content); const name = frontmatter.name ?? entry; skills.push({ name, path: skillMdPath, enabled: true, - ...(scope ? { scope } : {}), + scope, ...(frontmatter.description ? { description: frontmatter.description } : {}), ...(frontmatter.name ? { displayName: frontmatter.name } : {}), }); @@ -191,9 +191,9 @@ const scanSkillDir = Effect.fn("scanSkillDir")(function* ( } if (entry.endsWith(".md")) { - const content = yield* fs - .readFileString(entryPath) - .pipe(Effect.orElseSucceed(() => "")); + const content = yield* fs.readFileString(entryPath).pipe( + Effect.orElseSucceed(() => ""), + ); const frontmatter = parseSkillFrontmatter(content); const name = frontmatter.name ?? skillNameFromPath(entry, path.sep); skills.push({ From 161d6a49f4bfb8462750b5a8899971706b24c6f8 Mon Sep 17 00:00:00 2001 From: Ben Scholar Date: Mon, 25 May 2026 23:08:10 -0400 Subject: [PATCH 33/36] refactor(pi): convert PiAdapter from in-process SDK to RPC subprocess mode Replaces createAgentSession (npm SDK, in-process) with a spawned `pi --mode rpc` subprocess. Commands are sent as JSON lines to stdin (prompt, abort, get_state); AgentSessionEvent objects and RpcResponse objects are read back from stdout. Per-session lifecycle is managed by a Scope.Closeable; an outgoing Queue drives the stdin writer fiber; get_state is used after startup to capture the session file path for the resume cursor. Co-Authored-By: Claude Sonnet 4.6 --- apps/server/src/provider/Layers/PiAdapter.ts | 240 +++++++++++++------ 1 file changed, 164 insertions(+), 76 deletions(-) diff --git a/apps/server/src/provider/Layers/PiAdapter.ts b/apps/server/src/provider/Layers/PiAdapter.ts index 21aa95b2373..2cd8232a110 100644 --- a/apps/server/src/provider/Layers/PiAdapter.ts +++ b/apps/server/src/provider/Layers/PiAdapter.ts @@ -1,9 +1,6 @@ import { - createAgentSession, - SessionManager, - type AgentSession, type AgentSessionEvent, - type CreateAgentSessionOptions, + type RpcCommand, } from "@earendil-works/pi-coding-agent"; import { type CanonicalItemType, @@ -22,17 +19,20 @@ import { TurnId, } from "@t3tools/contracts"; import * as DateTime from "effect/DateTime"; +import * as Deferred from "effect/Deferred"; import * as Effect from "effect/Effect"; +import * as Exit from "effect/Exit"; import * as Fiber from "effect/Fiber"; import * as Queue from "effect/Queue"; import * as Random from "effect/Random"; +import * as Scope from "effect/Scope"; import * as Stream from "effect/Stream"; +import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"; import { ServerConfig } from "../../config.ts"; import { makePiEnvironment } from "../Drivers/PiHome.ts"; import { ProviderAdapterProcessError, - ProviderAdapterRequestError, ProviderAdapterSessionClosedError, ProviderAdapterSessionNotFoundError, ProviderAdapterValidationError, @@ -57,9 +57,10 @@ interface PiTurnState { interface PiSessionContext { session: ProviderSession; - piSession: AgentSession; - unsubscribe: (() => void) | undefined; - streamFiber: Fiber.Fiber | undefined; + readonly sessionScope: Scope.Closeable; + writeCommand: (cmd: RpcCommand) => Effect.Effect; + stdoutFiber: Fiber.Fiber | undefined; + readonly pendingRequests: Map>; readonly startedAt: string; turnState: PiTurnState | undefined; readonly turns: Array<{ id: TurnId; items: Array }>; @@ -125,6 +126,16 @@ function classifyToolItemType(toolName: string): CanonicalItemType { return "dynamic_tool_call"; } +function tryParseJsonObject(text: string): Record | null { + try { + // eslint-disable-next-line no-restricted-syntax -- non-Effect context, no schema needed + const v = JSON.parse(text) as unknown; // @effect-diagnostics-ignore preferSchemaOverJson + return v !== null && typeof v === "object" ? (v as Record) : null; + } catch { + return null; + } +} + function summarizePiToolArgs(args: unknown): string | undefined { if (!args || typeof args !== "object") return undefined; const input = args as Record; @@ -176,6 +187,7 @@ export const makePiAdapter = Effect.fn("makePiAdapter")(function* ( const boundInstanceId = options?.instanceId ?? ProviderInstanceId.make("piAgent"); const serverConfig = yield* ServerConfig; const piEnvironment = yield* makePiEnvironment(piSettings, options?.environment); + const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; const sessions = new Map(); const runtimeEventQueue = yield* Queue.unbounded(); @@ -392,7 +404,8 @@ export const makePiAdapter = Effect.fn("makePiAdapter")(function* ( case "agent_end": { if (context.turnState) { - yield* completeTurn(context, event.willRetry ? "interrupted" : "completed"); + const willRetry = "willRetry" in event ? Boolean(event.willRetry) : false; + yield* completeTurn(context, willRetry ? "interrupted" : "completed"); } return; } @@ -401,7 +414,10 @@ export const makePiAdapter = Effect.fn("makePiAdapter")(function* ( yield* offerRuntimeEvent({ ...base, type: "session.state.changed", - payload: { state: "waiting", reason: `compaction:${event.reason}` }, + payload: { + state: "waiting", + reason: `compaction:${"reason" in event ? String(event.reason) : "unknown"}`, + }, }); return; } @@ -420,6 +436,30 @@ export const makePiAdapter = Effect.fn("makePiAdapter")(function* ( } }); + const handleStdoutLine = Effect.fn("handleStdoutLine")(function* ( + context: PiSessionContext, + line: string, + ) { + const trimmed = line.trim(); + if (!trimmed) return; + + const msg = tryParseJsonObject(trimmed); + if (!msg) return; + + if (msg["type"] === "response") { + if (typeof msg["id"] === "string") { + const deferred = context.pendingRequests.get(msg["id"]); + if (deferred) { + context.pendingRequests.delete(msg["id"]); + yield* Deferred.succeed(deferred, msg); + } + } + return; + } + + yield* handlePiEvent(context, msg as AgentSessionEvent); + }); + const stopSessionInternal = Effect.fn("stopSessionInternal")(function* ( context: PiSessionContext, options?: { readonly emitExitEvent?: boolean }, @@ -431,18 +471,12 @@ export const makePiAdapter = Effect.fn("makePiAdapter")(function* ( yield* completeTurn(context, "interrupted", "Session stopped."); } - if (context.unsubscribe) { - context.unsubscribe(); - context.unsubscribe = undefined; + if (context.stdoutFiber) { + yield* Fiber.interrupt(context.stdoutFiber); + context.stdoutFiber = undefined; } - yield* Effect.sync(() => { - try { - context.piSession.dispose(); - } catch { - /* best-effort cleanup */ - } - }); + yield* Effect.ignore(Scope.close(context.sessionScope, Exit.void)); const updatedAt = yield* nowIso; context.session = { @@ -522,36 +556,47 @@ export const makePiAdapter = Effect.fn("makePiAdapter")(function* ( ? input.modelSelection : undefined; - const runtimeContext = yield* Effect.context(); - const runFork = Effect.runForkWith(runtimeContext); - const piResumeState = readPiResumeState(input.resumeCursor); const baseCwd = input.cwd ?? serverConfig.cwd; - const piSession = yield* Effect.tryPromise({ - try: async () => { - if (piResumeState) { - try { - const sessionManager = SessionManager.open(piResumeState.sessionFile); - const result = await createAgentSession({ cwd: baseCwd, sessionManager }); - return result.session; - } catch { - // Session file inaccessible; fall through to a fresh session. - } - } - const result = await createAgentSession({ cwd: baseCwd }); - return result.session; - }, - catch: (cause) => - new ProviderAdapterProcessError({ - provider: PROVIDER, - threadId, - detail: toMessage(cause, "Failed to start Pi Agent session."), - cause, + const spawnArgs: string[] = ["--mode", "rpc"]; + if (piResumeState) { + spawnArgs.push("--session", piResumeState.sessionFile); + } + if (modelSelection?.model) { + spawnArgs.push("--model", modelSelection.model); + } + + const sessionScope = yield* Scope.make(); + + const child = yield* spawner + .spawn( + ChildProcess.make(piSettings.binaryPath || "pi", spawnArgs, { + env: piEnvironment, + cwd: baseCwd, + shell: false, }), - }); + ) + .pipe( + Effect.provideService(Scope.Scope, sessionScope), + Effect.mapError( + (cause) => + new ProviderAdapterProcessError({ + provider: PROVIDER, + threadId, + detail: toMessage(cause, "Failed to start Pi Agent RPC process."), + cause, + }), + ), + Effect.onError(() => Effect.ignore(Scope.close(sessionScope, Exit.void))), + ); + + const outgoingQueue = yield* Queue.unbounded(); + + const writeCommand = (cmd: RpcCommand): Effect.Effect => + Queue.offer(outgoingQueue, Buffer.from(JSON.stringify(cmd) + "\n")).pipe(Effect.asVoid); - const piSessionFile = piSession.sessionFile; + const pendingRequests = new Map>(); const session: ProviderSession = { threadId, @@ -561,16 +606,16 @@ export const makePiAdapter = Effect.fn("makePiAdapter")(function* ( runtimeMode: input.runtimeMode, ...(input.cwd ? { cwd: input.cwd } : {}), ...(modelSelection?.model ? { model: modelSelection.model } : {}), - ...(piSessionFile !== undefined ? { resumeCursor: { sessionFile: piSessionFile } } : {}), createdAt: startedAt, updatedAt: startedAt, }; const context: PiSessionContext = { session, - piSession, - unsubscribe: undefined, - streamFiber: undefined, + sessionScope, + writeCommand, + stdoutFiber: undefined, + pendingRequests, startedAt, turnState: undefined, turns: [], @@ -578,11 +623,72 @@ export const makePiAdapter = Effect.fn("makePiAdapter")(function* ( }; sessions.set(threadId, context); - const unsubscribe = piSession.subscribe((event: AgentSessionEvent) => { - if (context.stopped) return; - runFork(handlePiEvent(context, event)); - }); - context.unsubscribe = unsubscribe; + const runtimeContext = yield* Effect.context(); + const runFork = Effect.runForkWith(runtimeContext); + + // Fork stdin writer — continuously drains the outgoing queue to the process stdin + runFork( + Stream.fromQueue(outgoingQueue).pipe( + Stream.run(child.stdin), + Effect.ignore, + ), + ); + + // Fork stdout reader — parses JSONL events and routes them + const stdoutFiber = runFork( + child.stdout.pipe( + Stream.decodeText(), + Stream.splitLines, + Stream.mapEffect((line) => handleStdoutLine(context, line)), + Stream.runDrain, + Effect.ignore, + // Emit session exit if the process dies while the session is still active + Effect.ensuring( + Effect.suspend(() => { + if (!context.stopped && context.session.status !== "closed") { + return stopSessionInternal(context, { emitExitEvent: true }); + } + return Effect.void; + }), + ), + ), + ); + context.stdoutFiber = stdoutFiber; + + // Drain stderr to avoid blocking the process + runFork( + child.stderr.pipe( + Stream.runDrain, + Effect.ignore, + ), + ); + + // Fetch the session file path via get_state for resume cursor + const stateReqId = yield* Random.nextUUIDv4; + const stateDeferred = yield* Deferred.make(); + pendingRequests.set(stateReqId, stateDeferred); + yield* writeCommand({ type: "get_state", id: stateReqId }); + + const stateResponse = yield* Deferred.await(stateDeferred).pipe( + Effect.timeoutOption(5000), + ); + + const sessionFile: string | undefined = (() => { + if (stateResponse._tag === "None") return undefined; + const resp = stateResponse.value as Record; + if (resp["success"] !== true) return undefined; + const data = resp["data"]; + if (!data || typeof data !== "object") return undefined; + const sf = (data as Record)["sessionFile"]; + return typeof sf === "string" && sf.trim().length > 0 ? sf.trim() : undefined; + })(); + + if (sessionFile !== undefined) { + context.session = { + ...context.session, + resumeCursor: { sessionFile }, + }; + } const sessionStartedStamp = yield* makeEventStamp(); yield* offerRuntimeEvent({ @@ -622,7 +728,7 @@ export const makePiAdapter = Effect.fn("makePiAdapter")(function* ( providerRefs: {}, }); - return { ...session }; + return { ...context.session }; }); const sendTurn: PiAdapterShape["sendTurn"] = Effect.fn("sendTurn")(function* (input) { @@ -656,18 +762,8 @@ export const makePiAdapter = Effect.fn("makePiAdapter")(function* ( const promptText = typeof input.input === "string" ? input.input : ""; - const runtimeContext = yield* Effect.context(); - const runFork = Effect.runForkWith(runtimeContext); - runFork( - Effect.tryPromise({ - try: () => context.piSession.prompt(promptText), - catch: (cause) => - new ProviderAdapterRequestError({ - provider: PROVIDER, - method: "turn/prompt", - detail: toMessage(cause, "Failed to send prompt to Pi Agent."), - }), - }).pipe(Effect.catch(() => completeTurn(context, "failed", "Prompt failed."))), + yield* context.writeCommand({ type: "prompt", message: promptText }).pipe( + Effect.catchDefect(() => completeTurn(context, "failed", "Failed to send prompt.")), ); return { @@ -679,15 +775,7 @@ export const makePiAdapter = Effect.fn("makePiAdapter")(function* ( const interruptTurn: PiAdapterShape["interruptTurn"] = Effect.fn("interruptTurn")( function* (threadId, _turnId) { const context = yield* requireSession(threadId); - yield* Effect.tryPromise({ - try: () => context.piSession.abort(), - catch: (cause) => - new ProviderAdapterRequestError({ - provider: PROVIDER, - method: "turn/interrupt", - detail: toMessage(cause, "Failed to interrupt Pi Agent turn."), - }), - }); + yield* Effect.ignore(context.writeCommand({ type: "abort" })); }, ); From b0de5f9edb859fc21e6e212220e98ba0e324ce65 Mon Sep 17 00:00:00 2001 From: Ben Scholar Date: Mon, 25 May 2026 23:19:53 -0400 Subject: [PATCH 34/36] feat(pi): handle RPC extension UI requests for ask_user_question MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When Pi emits extension_ui_request events over RPC stdout, route them through the T3 Code user-input system: - select / confirm → surface as user-input.requested with structured options; respondToUserInput maps answers back to extension_ui_response - input / editor → auto-cancel (no free-text equivalent in contracts) - notify / setStatus / setWidget / setTitle / set_editor_text → auto-acknowledge so Pi isn't left waiting On session stop, outstanding requests are cancelled before the process is killed so Pi doesn't deadlock. Co-Authored-By: Claude Sonnet 4.6 --- apps/server/src/provider/Layers/PiAdapter.ts | 178 +++++++++++++++++-- 1 file changed, 167 insertions(+), 11 deletions(-) diff --git a/apps/server/src/provider/Layers/PiAdapter.ts b/apps/server/src/provider/Layers/PiAdapter.ts index 2cd8232a110..a883b295af7 100644 --- a/apps/server/src/provider/Layers/PiAdapter.ts +++ b/apps/server/src/provider/Layers/PiAdapter.ts @@ -1,8 +1,6 @@ +import { type AgentSessionEvent, type RpcCommand } from "@earendil-works/pi-coding-agent"; import { - type AgentSessionEvent, - type RpcCommand, -} from "@earendil-works/pi-coding-agent"; -import { + ApprovalRequestId, type CanonicalItemType, EventId, type PiSettings, @@ -15,8 +13,10 @@ import { type ProviderSession, type ProviderUserInputAnswers, RuntimeItemId, + RuntimeRequestId, ThreadId, TurnId, + type UserInputQuestion, } from "@t3tools/contracts"; import * as DateTime from "effect/DateTime"; import * as Deferred from "effect/Deferred"; @@ -40,6 +40,25 @@ import { } from "../Errors.ts"; import type { PiAdapterShape } from "../Services/PiAdapter.ts"; +// RpcExtensionUIRequest / RpcExtensionUIResponse are defined in the pi package's +// internal rpc-types module but not re-exported from the public index, so we +// mirror the relevant subset here. +type RpcExtensionUIRequest = + | { type: "extension_ui_request"; id: string; method: "select"; title: string; options: string[]; timeout?: number } + | { type: "extension_ui_request"; id: string; method: "confirm"; title: string; message: string; timeout?: number } + | { type: "extension_ui_request"; id: string; method: "input"; title: string; placeholder?: string; timeout?: number } + | { type: "extension_ui_request"; id: string; method: "editor"; title: string; prefill?: string } + | { type: "extension_ui_request"; id: string; method: "notify"; message: string; notifyType?: "info" | "warning" | "error" } + | { type: "extension_ui_request"; id: string; method: "setStatus"; statusKey: string; statusText: string | undefined } + | { type: "extension_ui_request"; id: string; method: "setWidget"; widgetKey: string; widgetLines: string[] | undefined; widgetPlacement?: "aboveEditor" | "belowEditor" } + | { type: "extension_ui_request"; id: string; method: "setTitle"; title: string } + | { type: "extension_ui_request"; id: string; method: "set_editor_text"; text: string }; + +type RpcExtensionUIResponse = + | { type: "extension_ui_response"; id: string; value: string } + | { type: "extension_ui_response"; id: string; confirmed: boolean } + | { type: "extension_ui_response"; id: string; cancelled: true }; + const PROVIDER = ProviderDriverKind.make("piAgent"); interface PiToolItem { @@ -55,12 +74,21 @@ interface PiTurnState { readonly items: Array; } +interface PendingExtensionUI { + readonly piId: string; + readonly questionId: string; + readonly deferred: Deferred.Deferred; + readonly method: "select" | "confirm"; +} + interface PiSessionContext { session: ProviderSession; readonly sessionScope: Scope.Closeable; writeCommand: (cmd: RpcCommand) => Effect.Effect; + writeExtensionResponse: (resp: RpcExtensionUIResponse) => Effect.Effect; stdoutFiber: Fiber.Fiber | undefined; readonly pendingRequests: Map>; + readonly pendingExtensionUIs: Map; readonly startedAt: string; turnState: PiTurnState | undefined; readonly turns: Array<{ id: TurnId; items: Array }>; @@ -436,6 +464,83 @@ export const makePiAdapter = Effect.fn("makePiAdapter")(function* ( } }); + const handleExtensionUIRequest = Effect.fn("handleExtensionUIRequest")(function* ( + context: PiSessionContext, + event: RpcExtensionUIRequest, + ) { + // Non-interactive side-effects: acknowledge immediately so Pi doesn't hang + if ( + event.method === "notify" || + event.method === "setStatus" || + event.method === "setWidget" || + event.method === "setTitle" || + event.method === "set_editor_text" + ) { + yield* context.writeExtensionResponse({ type: "extension_ui_response", id: event.id, value: "" }); + return; + } + + // Free-text inputs: no structured equivalent in our contracts, cancel + if (event.method === "input" || event.method === "editor") { + yield* context.writeExtensionResponse({ type: "extension_ui_response", id: event.id, cancelled: true }); + return; + } + + // select / confirm: surface as user-input.requested + const ourRequestId = ApprovalRequestId.make(yield* Random.nextUUIDv4); + const questionId = String(ourRequestId); + const deferred = yield* Deferred.make(); + + let question: UserInputQuestion; + if (event.method === "select") { + question = { + id: questionId, + header: event.title.slice(0, 12), + question: event.title, + options: event.options.map((label) => ({ label, description: label })), + multiSelect: false, + }; + } else { + // confirm + const body = event.message.length > 0 ? `${event.title}\n${event.message}` : event.title; + question = { + id: questionId, + header: "Confirm", + question: body, + options: [ + { label: "Yes", description: "Confirm" }, + { label: "No", description: "Cancel" }, + ], + multiSelect: false, + }; + } + + context.pendingExtensionUIs.set(ourRequestId, { + piId: event.id, + questionId, + deferred, + method: event.method, + }); + + const stamp = yield* makeEventStamp(); + yield* offerRuntimeEvent({ + type: "user-input.requested", + eventId: stamp.eventId, + provider: PROVIDER, + createdAt: stamp.createdAt, + threadId: context.session.threadId, + ...(context.turnState ? { turnId: context.turnState.turnId } : {}), + requestId: RuntimeRequestId.make(ourRequestId), + payload: { questions: [question] }, + providerRefs: {}, + raw: { + source: "pi.sdk.event" as const, + method: "extension_ui_request", + payload: event, + }, + }); + }); + const handleStdoutLine = Effect.fn("handleStdoutLine")(function* ( context: PiSessionContext, line: string, @@ -457,6 +562,11 @@ export const makePiAdapter = Effect.fn("makePiAdapter")(function* ( return; } + if (msg["type"] === "extension_ui_request") { + yield* handleExtensionUIRequest(context, msg as RpcExtensionUIRequest); + return; + } + yield* handlePiEvent(context, msg as AgentSessionEvent); }); @@ -471,6 +581,15 @@ export const makePiAdapter = Effect.fn("makePiAdapter")(function* ( yield* completeTurn(context, "interrupted", "Session stopped."); } + // Cancel any pending extension UI requests so Pi doesn't hang + for (const [, pending] of context.pendingExtensionUIs) { + yield* Effect.ignore( + context.writeExtensionResponse({ type: "extension_ui_response", id: pending.piId, cancelled: true }), + ); + yield* Effect.ignore(Deferred.interrupt(pending.deferred)); + } + context.pendingExtensionUIs.clear(); + if (context.stdoutFiber) { yield* Fiber.interrupt(context.stdoutFiber); context.stdoutFiber = undefined; @@ -593,10 +712,15 @@ export const makePiAdapter = Effect.fn("makePiAdapter")(function* ( const outgoingQueue = yield* Queue.unbounded(); - const writeCommand = (cmd: RpcCommand): Effect.Effect => - Queue.offer(outgoingQueue, Buffer.from(JSON.stringify(cmd) + "\n")).pipe(Effect.asVoid); + const writeLine = (obj: RpcCommand | RpcExtensionUIResponse): Effect.Effect => + Queue.offer(outgoingQueue, Buffer.from(JSON.stringify(obj) + "\n")).pipe(Effect.asVoid); + + const writeCommand = (cmd: RpcCommand): Effect.Effect => writeLine(cmd); + const writeExtensionResponse = (resp: RpcExtensionUIResponse): Effect.Effect => + writeLine(resp); const pendingRequests = new Map>(); + const pendingExtensionUIs = new Map(); const session: ProviderSession = { threadId, @@ -614,8 +738,10 @@ export const makePiAdapter = Effect.fn("makePiAdapter")(function* ( session, sessionScope, writeCommand, + writeExtensionResponse, stdoutFiber: undefined, pendingRequests, + pendingExtensionUIs, startedAt, turnState: undefined, turns: [], @@ -808,11 +934,41 @@ export const makePiAdapter = Effect.fn("makePiAdapter")(function* ( const respondToRequest: PiAdapterShape["respondToRequest"] = (_threadId, _requestId, _decision) => Effect.void; - const respondToUserInput: PiAdapterShape["respondToUserInput"] = ( - _threadId, - _requestId, - _answers, - ) => Effect.void; + const respondToUserInput: PiAdapterShape["respondToUserInput"] = Effect.fn("respondToUserInput")( + function* (threadId, requestId, answers) { + const context = yield* requireSession(threadId); + const pending = context.pendingExtensionUIs.get(requestId); + if (!pending) return; + context.pendingExtensionUIs.delete(requestId); + + // Build the response Pi expects based on the original method + let piResponse: RpcExtensionUIResponse; + if (pending.method === "confirm") { + const answer = answers[pending.questionId]; + piResponse = { type: "extension_ui_response", id: pending.piId, confirmed: answer === "Yes" }; + } else { + const answer = typeof answers[pending.questionId] === "string" + ? (answers[pending.questionId] as string) + : ""; + piResponse = { type: "extension_ui_response", id: pending.piId, value: answer }; + } + + yield* context.writeExtensionResponse(piResponse); + yield* Deferred.succeed(pending.deferred, answers); + + const stamp = yield* makeEventStamp(); + yield* offerRuntimeEvent({ + type: "user-input.resolved", + eventId: stamp.eventId, + provider: PROVIDER, + createdAt: stamp.createdAt, + threadId: context.session.threadId, + requestId: RuntimeRequestId.make(requestId), + payload: { answers }, + providerRefs: {}, + }); + }, + ); const stopSession: PiAdapterShape["stopSession"] = Effect.fn("stopSession")(function* (threadId) { const context = yield* requireSession(threadId); From bfa551d6126ae1e9a3ab03f93fea5f4740eae8ec Mon Sep 17 00:00:00 2001 From: Ben Scholar Date: Tue, 26 May 2026 09:01:15 -0400 Subject: [PATCH 35/36] feat(pi): handle input extension_ui_request for pi-ask-user multi-select and freeform Pi's RPC fallback uses method:"input" for two cases: - Multi-select: title encodes options as a numbered list ("Q?\n1. A\n2. B"). Parse the list, surface as multiSelect:true question; map selected labels back to 1-based comma-separated indices for Pi's response protocol. - Freeform: surface with options:[] so T3's auto-added Other field handles free-text entry; answer string passed through as value directly. editor requests are still cancelled (no useful mapping). Co-Authored-By: Claude Sonnet 4.6 --- apps/server/src/provider/Layers/PiAdapter.ts | 78 +++++++++++++++++--- 1 file changed, 67 insertions(+), 11 deletions(-) diff --git a/apps/server/src/provider/Layers/PiAdapter.ts b/apps/server/src/provider/Layers/PiAdapter.ts index a883b295af7..e81803aea65 100644 --- a/apps/server/src/provider/Layers/PiAdapter.ts +++ b/apps/server/src/provider/Layers/PiAdapter.ts @@ -78,7 +78,10 @@ interface PendingExtensionUI { readonly piId: string; readonly questionId: string; readonly deferred: Deferred.Deferred; - readonly method: "select" | "confirm"; + readonly method: "select" | "confirm" | "input"; + // Populated for `input` requests that were parsed as numbered lists. + // Used to map selected labels back to 1-based indices for Pi's multi-select protocol. + readonly numberedOptions?: ReadonlyArray; } interface PiSessionContext { @@ -154,6 +157,19 @@ function classifyToolItemType(toolName: string): CanonicalItemType { return "dynamic_tool_call"; } +// Pi's RPC multi-select fallback encodes options as a numbered list in the title: +// "Which colors?\n1. Red\n2. Blue\n3. Green" +// Returns the question title and extracted option strings, or null if no list found. +function parseNumberedList(text: string): { title: string; items: ReadonlyArray } | null { + const lines = text.split("\n"); + const items: string[] = []; + for (const line of lines.slice(1)) { + const m = /^\d+\.\s+(.+)$/.exec(line.trim()); + if (m?.[1]) items.push(m[1]); + } + return items.length >= 2 ? { title: lines[0] ?? text, items } : null; +} + function tryParseJsonObject(text: string): Record | null { try { // eslint-disable-next-line no-restricted-syntax -- non-Effect context, no schema needed @@ -480,28 +496,29 @@ export const makePiAdapter = Effect.fn("makePiAdapter")(function* ( return; } - // Free-text inputs: no structured equivalent in our contracts, cancel - if (event.method === "input" || event.method === "editor") { + // editor: no useful mapping, cancel immediately + if (event.method === "editor") { yield* context.writeExtensionResponse({ type: "extension_ui_response", id: event.id, cancelled: true }); return; } - // select / confirm: surface as user-input.requested + // select / confirm / input: surface as user-input.requested const ourRequestId = ApprovalRequestId.make(yield* Random.nextUUIDv4); const questionId = String(ourRequestId); const deferred = yield* Deferred.make(); let question: UserInputQuestion; + let numberedOptions: ReadonlyArray | undefined; + if (event.method === "select") { question = { id: questionId, header: event.title.slice(0, 12), question: event.title, - options: event.options.map((label) => ({ label, description: label })), + options: event.options.map((label: string) => ({ label, description: label })), multiSelect: false, }; - } else { - // confirm + } else if (event.method === "confirm") { const body = event.message.length > 0 ? `${event.title}\n${event.message}` : event.title; question = { id: questionId, @@ -513,6 +530,29 @@ export const makePiAdapter = Effect.fn("makePiAdapter")(function* ( ], multiSelect: false, }; + } else { + // input — Pi uses this for multi-select (numbered list in title) and freeform. + // If the title contains a numbered list, parse it into real options with multiSelect. + // Otherwise surface with empty options; T3's auto-added "Other" field handles free text. + const parsed = parseNumberedList(event.title); + if (parsed) { + numberedOptions = parsed.items; + question = { + id: questionId, + header: parsed.title.slice(0, 12), + question: parsed.title, + options: parsed.items.map((item) => ({ label: item, description: item })), + multiSelect: true, + }; + } else { + question = { + id: questionId, + header: event.title.slice(0, 12), + question: event.title, + options: [], + multiSelect: false, + }; + } } context.pendingExtensionUIs.set(ourRequestId, { @@ -520,6 +560,7 @@ export const makePiAdapter = Effect.fn("makePiAdapter")(function* ( questionId, deferred, method: event.method, + ...(numberedOptions ? { numberedOptions } : {}), }); const stamp = yield* makeEventStamp(); @@ -946,11 +987,26 @@ export const makePiAdapter = Effect.fn("makePiAdapter")(function* ( if (pending.method === "confirm") { const answer = answers[pending.questionId]; piResponse = { type: "extension_ui_response", id: pending.piId, confirmed: answer === "Yes" }; + } else if (pending.method === "input" && pending.numberedOptions) { + // Multi-select numbered list: map selected labels back to 1-based indices + const raw = answers[pending.questionId]; + const selected: string[] = Array.isArray(raw) + ? raw.map(String) + : typeof raw === "string" && raw.length > 0 + ? [raw] + : []; + const indices = selected + .map((label) => { + const idx = pending.numberedOptions!.indexOf(label); + return idx >= 0 ? String(idx + 1) : null; + }) + .filter((i): i is string => i !== null); + piResponse = { type: "extension_ui_response", id: pending.piId, value: indices.join(",") }; } else { - const answer = typeof answers[pending.questionId] === "string" - ? (answers[pending.questionId] as string) - : ""; - piResponse = { type: "extension_ui_response", id: pending.piId, value: answer }; + // select or freeform input: answer is a string value + const answer = answers[pending.questionId]; + const value = typeof answer === "string" ? answer : ""; + piResponse = { type: "extension_ui_response", id: pending.piId, value }; } yield* context.writeExtensionResponse(piResponse); From 967842729ad78d5a9ac5a8a1cbf660fbf7e1f319 Mon Sep 17 00:00:00 2001 From: Ben Scholar Date: Tue, 26 May 2026 09:06:43 -0400 Subject: [PATCH 36/36] fix(pi): correct extension_ui_request handling per Pi RPC docs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fire-and-forget methods (notify/setStatus/setWidget/setTitle/set_editor_text) don't expect a response — stop sending spurious { value: "" } replies - editor expects { value: "..." } back like input, not cancellation — wire it through the same freeform path - Expand PendingExtensionUI.method to include "editor" Co-Authored-By: Claude Sonnet 4.6 --- apps/server/src/provider/Layers/PiAdapter.ts | 27 ++++++++++---------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/apps/server/src/provider/Layers/PiAdapter.ts b/apps/server/src/provider/Layers/PiAdapter.ts index e81803aea65..4230f5ae9c6 100644 --- a/apps/server/src/provider/Layers/PiAdapter.ts +++ b/apps/server/src/provider/Layers/PiAdapter.ts @@ -78,7 +78,7 @@ interface PendingExtensionUI { readonly piId: string; readonly questionId: string; readonly deferred: Deferred.Deferred; - readonly method: "select" | "confirm" | "input"; + readonly method: "select" | "confirm" | "input" | "editor"; // Populated for `input` requests that were parsed as numbered lists. // Used to map selected labels back to 1-based indices for Pi's multi-select protocol. readonly numberedOptions?: ReadonlyArray; @@ -484,7 +484,7 @@ export const makePiAdapter = Effect.fn("makePiAdapter")(function* ( context: PiSessionContext, event: RpcExtensionUIRequest, ) { - // Non-interactive side-effects: acknowledge immediately so Pi doesn't hang + // Fire-and-forget side-effects: Pi does not wait for a response. if ( event.method === "notify" || event.method === "setStatus" || @@ -492,15 +492,11 @@ export const makePiAdapter = Effect.fn("makePiAdapter")(function* ( event.method === "setTitle" || event.method === "set_editor_text" ) { - yield* context.writeExtensionResponse({ type: "extension_ui_response", id: event.id, value: "" }); return; } - // editor: no useful mapping, cancel immediately - if (event.method === "editor") { - yield* context.writeExtensionResponse({ type: "extension_ui_response", id: event.id, cancelled: true }); - return; - } + // editor: surface as freeform text input (same protocol shape as input). + // Falls through to the input branch below. // select / confirm / input: surface as user-input.requested const ourRequestId = ApprovalRequestId.make(yield* Random.nextUUIDv4); @@ -531,10 +527,13 @@ export const makePiAdapter = Effect.fn("makePiAdapter")(function* ( multiSelect: false, }; } else { - // input — Pi uses this for multi-select (numbered list in title) and freeform. - // If the title contains a numbered list, parse it into real options with multiSelect. - // Otherwise surface with empty options; T3's auto-added "Other" field handles free text. - const parsed = parseNumberedList(event.title); + // input / editor — both expect { value: "..." } back. + // input: Pi uses this for multi-select (numbered list in title) and freeform. + // If the title contains a numbered list, parse it into real options with multiSelect. + // Otherwise surface with empty options; T3's auto-added "Other" field handles free text. + // editor: multi-line freeform, no options — same freeform path. + const title = "title" in event ? event.title : ""; + const parsed = event.method === "input" ? parseNumberedList(title) : null; if (parsed) { numberedOptions = parsed.items; question = { @@ -547,8 +546,8 @@ export const makePiAdapter = Effect.fn("makePiAdapter")(function* ( } else { question = { id: questionId, - header: event.title.slice(0, 12), - question: event.title, + header: title.slice(0, 12), + question: title, options: [], multiSelect: false, };