From fcd96df6f2cb12732972b7cb5bb3090161499c56 Mon Sep 17 00:00:00 2001 From: Sam Jones Date: Wed, 6 May 2026 14:45:13 -0400 Subject: [PATCH 001/166] config selector: scale maxVisible to terminal height ResourceList hardcoded maxVisible=15, leaving most of the screen empty on tall terminals. Pass terminalHeight through from ProcessTerminal.rows and compute maxVisible dynamically, matching the pattern tree-selector already uses. Closes #4241 --- packages/coding-agent/src/cli/config-selector.ts | 1 + .../interactive/components/config-selector.ts | 16 +++++++++++++--- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/packages/coding-agent/src/cli/config-selector.ts b/packages/coding-agent/src/cli/config-selector.ts index 93d591cd..22d497ff 100644 --- a/packages/coding-agent/src/cli/config-selector.ts +++ b/packages/coding-agent/src/cli/config-selector.ts @@ -43,6 +43,7 @@ export async function selectConfig(options: ConfigSelectorOptions): Promise ui.requestRender(), + ui.terminal.rows, ); ui.addChild(selector); diff --git a/packages/coding-agent/src/modes/interactive/components/config-selector.ts b/packages/coding-agent/src/modes/interactive/components/config-selector.ts index c321dfe5..0ddbdf53 100644 --- a/packages/coding-agent/src/modes/interactive/components/config-selector.ts +++ b/packages/coding-agent/src/modes/interactive/components/config-selector.ts @@ -178,7 +178,7 @@ class ResourceList implements Component, Focusable { private filteredItems: FlatEntry[] = []; private selectedIndex = 0; private searchInput: Input; - private maxVisible = 15; + private maxVisible: number; private settingsManager: SettingsManager; private cwd: string; private agentDir: string; @@ -196,12 +196,21 @@ class ResourceList implements Component, Focusable { this.searchInput.focused = value; } - constructor(groups: ResourceGroup[], settingsManager: SettingsManager, cwd: string, agentDir: string) { + constructor( + groups: ResourceGroup[], + settingsManager: SettingsManager, + cwd: string, + agentDir: string, + terminalHeight?: number, + ) { this.groups = groups; this.settingsManager = settingsManager; this.cwd = cwd; this.agentDir = agentDir; this.searchInput = new Input(); + // 8 lines of chrome: top spacer + top border + spacer + header (2 lines) + spacer + bottom spacer + bottom border + const chrome = 8; + this.maxVisible = Math.max(5, (terminalHeight ?? 24) - chrome); this.buildFlatList(); this.filteredItems = [...this.flatItems]; } @@ -565,6 +574,7 @@ export class ConfigSelectorComponent extends Container implements Focusable { onClose: () => void, onExit: () => void, requestRender: () => void, + terminalHeight?: number, ) { super(); @@ -578,7 +588,7 @@ export class ConfigSelectorComponent extends Container implements Focusable { this.addChild(new Spacer(1)); // Resource list - this.resourceList = new ResourceList(groups, settingsManager, cwd, agentDir); + this.resourceList = new ResourceList(groups, settingsManager, cwd, agentDir, terminalHeight); this.resourceList.onCancel = onClose; this.resourceList.onExit = onExit; this.resourceList.onToggle = () => requestRender(); From 8c2e3eddecb997ac4d110fe39725a27e236d8644 Mon Sep 17 00:00:00 2001 From: haoqixu Date: Sun, 10 May 2026 02:45:44 +0800 Subject: [PATCH 002/166] fix(ai): respect proxy envs in bun's websocket fixes #4346 --- package-lock.json | 1 + packages/ai/package.json | 3 +- .../src/providers/openai-codex-responses.ts | 32 +++++++++++++++++-- 3 files changed, 33 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 824b8d98..071a4371 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7337,6 +7337,7 @@ "openai": "6.26.0", "partial-json": "^0.1.7", "proxy-agent": "^6.5.0", + "proxy-from-env": "^1.1.0", "typebox": "^1.1.24", "undici": "^7.19.1", "zod-to-json-schema": "^3.24.6" diff --git a/packages/ai/package.json b/packages/ai/package.json index f8d7b260..1744a2fd 100644 --- a/packages/ai/package.json +++ b/packages/ai/package.json @@ -73,11 +73,12 @@ "@aws-sdk/client-bedrock-runtime": "^3.1030.0", "@google/genai": "^1.40.0", "@mistralai/mistralai": "^2.2.0", - "typebox": "^1.1.24", "chalk": "^5.6.2", "openai": "6.26.0", "partial-json": "^0.1.7", "proxy-agent": "^6.5.0", + "proxy-from-env": "^1.1.0", + "typebox": "^1.1.24", "undici": "^7.19.1", "zod-to-json-schema": "^3.24.6" }, diff --git a/packages/ai/src/providers/openai-codex-responses.ts b/packages/ai/src/providers/openai-codex-responses.ts index dc6b6463..64542b2c 100644 --- a/packages/ai/src/providers/openai-codex-responses.ts +++ b/packages/ai/src/providers/openai-codex-responses.ts @@ -711,7 +711,35 @@ type WebSocketConstructor = new ( protocols?: string | string[] | { headers?: Record }, ) => WebSocketLike; -function getWebSocketConstructor(): WebSocketConstructor | null { +let _cachedWebsocket: WebSocketConstructor | null = null; +async function getWebSocketConstructor(): Promise { + if (_cachedWebsocket) return _cachedWebsocket; + + // bun doesn't respect http proxy envs, ref: https://github.com/oven-sh/bun/issues/15489 + // TODO: remove this when bun supports proxy envs in websocket. + if ( + process?.versions?.bun && + (process.env.HTTP_PROXY || process.env.HTTPS_PROXY || process.env.http_proxy || process.env.https_proxy) + ) { + const m = await dynamicImport("proxy-from-env"); + const getProxyForUrl = (m as { getProxyForUrl: (url: string | object | URL) => string }).getProxyForUrl; + + _cachedWebsocket = class extends WebSocket { + constructor(url: string | URL, options?: string | string[] | Record) { + let _opts: Record = {}; + if (Array.isArray(options) || typeof options === "string") { + _opts = { protocols: options }; + } else { + _opts = { ...options }; + } + + const proxy = getProxyForUrl(url.toString().replace(/^wss:/, "https:").replace(/^ws:/, "http:")); + super(url, { ..._opts, ...(proxy ? { proxy } : {}) } as any); + } + }; + return _cachedWebsocket; + } + const ctor = (globalThis as { WebSocket?: unknown }).WebSocket; if (typeof ctor !== "function") return null; return ctor as unknown as WebSocketConstructor; @@ -760,7 +788,7 @@ function scheduleSessionWebSocketExpiry(sessionId: string, entry: CachedWebSocke } async function connectWebSocket(url: string, headers: Headers, signal?: AbortSignal): Promise { - const WebSocketCtor = getWebSocketConstructor(); + const WebSocketCtor = await getWebSocketConstructor(); if (!WebSocketCtor) { throw new Error("WebSocket transport is not available in this runtime"); } From 99dc6fcec8cb32aeba0aaf66e21db91bcdbdeb60 Mon Sep 17 00:00:00 2001 From: yanirz <3331050+yanirz@users.noreply.github.com> Date: Sun, 10 May 2026 00:11:36 +0200 Subject: [PATCH 003/166] fix(ai): add session affinity and compat fixes for Fireworks provider caching Fireworks prompt caching is enabled by default (automatic prefix matching), but on serverless infrastructure, requests hit random replicas. Without session affinity, the per-replica cache misses, negating cache hit rates and the discounted cacheRead pricing. Changes: - Add sendSessionAffinityHeaders and supportsCacheControlOnTools to AnthropicMessagesCompat interface - Send x-session-affinity header for Fireworks (and Cloudflare AI Gateway Anthropic) when sessionId is available and caching is enabled - Omit cache_control on tool definitions for Fireworks (unsupported per https://docs.fireworks.ai/tools-sdks/anthropic-compatibility) - Default supportsEagerToolInputStreaming to false for Fireworks (unsupported field) - Default supportsLongCacheRetention to false for Fireworks (cache_control.ttl not supported) - Add compat settings to Fireworks models in generate-models.ts - Update generated models with Fireworks compat settings - Add integration tests for session affinity and tool compat Refs: https://docs.fireworks.ai/guides/prompt-caching Refs: https://docs.fireworks.ai/tools-sdks/anthropic-compatibility --- packages/ai/scripts/generate-models.ts | 10 ++ packages/ai/src/models.generated.ts | 114 +++++++++++++ packages/ai/src/providers/anthropic.ts | 24 ++- packages/ai/src/types.ts | 16 ++ packages/ai/test/fireworks-models.test.ts | 198 ++++++++++++++++++++++ 5 files changed, 358 insertions(+), 4 deletions(-) diff --git a/packages/ai/scripts/generate-models.ts b/packages/ai/scripts/generate-models.ts index e1d5c3ed..798d059c 100644 --- a/packages/ai/scripts/generate-models.ts +++ b/packages/ai/scripts/generate-models.ts @@ -764,6 +764,16 @@ async function loadModelsDevData(): Promise[]> { }, contextWindow: m.limit?.context || 4096, maxTokens: m.limit?.output || 4096, + // Fireworks prompt caching uses automatic prefix matching + session affinity. + // x-session-affinity routes requests to the same replica for cache hits. + // cache_control on tools and eager_input_streaming are not supported. + // See: https://docs.fireworks.ai/tools-sdks/anthropic-compatibility + compat: { + sendSessionAffinityHeaders: true, + supportsEagerToolInputStreaming: false, + supportsCacheControlOnTools: false, + supportsLongCacheRetention: false, + }, }); } } diff --git a/packages/ai/src/models.generated.ts b/packages/ai/src/models.generated.ts index e700a56c..69559c78 100644 --- a/packages/ai/src/models.generated.ts +++ b/packages/ai/src/models.generated.ts @@ -3614,6 +3614,12 @@ export const MODELS = { }, contextWindow: 163840, maxTokens: 163840, + compat: { + sendSessionAffinityHeaders: true, + supportsEagerToolInputStreaming: false, + supportsCacheControlOnTools: false, + supportsLongCacheRetention: false, + }, } satisfies Model<"anthropic-messages">, "accounts/fireworks/models/deepseek-v3p2": { id: "accounts/fireworks/models/deepseek-v3p2", @@ -3631,6 +3637,12 @@ export const MODELS = { }, contextWindow: 160000, maxTokens: 160000, + compat: { + sendSessionAffinityHeaders: true, + supportsEagerToolInputStreaming: false, + supportsCacheControlOnTools: false, + supportsLongCacheRetention: false, + }, } satisfies Model<"anthropic-messages">, "accounts/fireworks/models/deepseek-v4-pro": { id: "accounts/fireworks/models/deepseek-v4-pro", @@ -3648,6 +3660,12 @@ export const MODELS = { }, contextWindow: 1000000, maxTokens: 384000, + compat: { + sendSessionAffinityHeaders: true, + supportsEagerToolInputStreaming: false, + supportsCacheControlOnTools: false, + supportsLongCacheRetention: false, + }, } satisfies Model<"anthropic-messages">, "accounts/fireworks/models/glm-4p5": { id: "accounts/fireworks/models/glm-4p5", @@ -3665,6 +3683,12 @@ export const MODELS = { }, contextWindow: 131072, maxTokens: 131072, + compat: { + sendSessionAffinityHeaders: true, + supportsEagerToolInputStreaming: false, + supportsCacheControlOnTools: false, + supportsLongCacheRetention: false, + }, } satisfies Model<"anthropic-messages">, "accounts/fireworks/models/glm-4p5-air": { id: "accounts/fireworks/models/glm-4p5-air", @@ -3682,6 +3706,12 @@ export const MODELS = { }, contextWindow: 131072, maxTokens: 131072, + compat: { + sendSessionAffinityHeaders: true, + supportsEagerToolInputStreaming: false, + supportsCacheControlOnTools: false, + supportsLongCacheRetention: false, + }, } satisfies Model<"anthropic-messages">, "accounts/fireworks/models/glm-4p7": { id: "accounts/fireworks/models/glm-4p7", @@ -3699,6 +3729,12 @@ export const MODELS = { }, contextWindow: 198000, maxTokens: 198000, + compat: { + sendSessionAffinityHeaders: true, + supportsEagerToolInputStreaming: false, + supportsCacheControlOnTools: false, + supportsLongCacheRetention: false, + }, } satisfies Model<"anthropic-messages">, "accounts/fireworks/models/glm-5": { id: "accounts/fireworks/models/glm-5", @@ -3716,6 +3752,12 @@ export const MODELS = { }, contextWindow: 202752, maxTokens: 131072, + compat: { + sendSessionAffinityHeaders: true, + supportsEagerToolInputStreaming: false, + supportsCacheControlOnTools: false, + supportsLongCacheRetention: false, + }, } satisfies Model<"anthropic-messages">, "accounts/fireworks/models/glm-5p1": { id: "accounts/fireworks/models/glm-5p1", @@ -3733,6 +3775,12 @@ export const MODELS = { }, contextWindow: 202800, maxTokens: 131072, + compat: { + sendSessionAffinityHeaders: true, + supportsEagerToolInputStreaming: false, + supportsCacheControlOnTools: false, + supportsLongCacheRetention: false, + }, } satisfies Model<"anthropic-messages">, "accounts/fireworks/models/gpt-oss-120b": { id: "accounts/fireworks/models/gpt-oss-120b", @@ -3750,6 +3798,12 @@ export const MODELS = { }, contextWindow: 131072, maxTokens: 32768, + compat: { + sendSessionAffinityHeaders: true, + supportsEagerToolInputStreaming: false, + supportsCacheControlOnTools: false, + supportsLongCacheRetention: false, + }, } satisfies Model<"anthropic-messages">, "accounts/fireworks/models/gpt-oss-20b": { id: "accounts/fireworks/models/gpt-oss-20b", @@ -3767,6 +3821,12 @@ export const MODELS = { }, contextWindow: 131072, maxTokens: 32768, + compat: { + sendSessionAffinityHeaders: true, + supportsEagerToolInputStreaming: false, + supportsCacheControlOnTools: false, + supportsLongCacheRetention: false, + }, } satisfies Model<"anthropic-messages">, "accounts/fireworks/models/kimi-k2-instruct": { id: "accounts/fireworks/models/kimi-k2-instruct", @@ -3784,6 +3844,12 @@ export const MODELS = { }, contextWindow: 128000, maxTokens: 16384, + compat: { + sendSessionAffinityHeaders: true, + supportsEagerToolInputStreaming: false, + supportsCacheControlOnTools: false, + supportsLongCacheRetention: false, + }, } satisfies Model<"anthropic-messages">, "accounts/fireworks/models/kimi-k2-thinking": { id: "accounts/fireworks/models/kimi-k2-thinking", @@ -3801,6 +3867,12 @@ export const MODELS = { }, contextWindow: 256000, maxTokens: 256000, + compat: { + sendSessionAffinityHeaders: true, + supportsEagerToolInputStreaming: false, + supportsCacheControlOnTools: false, + supportsLongCacheRetention: false, + }, } satisfies Model<"anthropic-messages">, "accounts/fireworks/models/kimi-k2p5": { id: "accounts/fireworks/models/kimi-k2p5", @@ -3818,6 +3890,12 @@ export const MODELS = { }, contextWindow: 256000, maxTokens: 256000, + compat: { + sendSessionAffinityHeaders: true, + supportsEagerToolInputStreaming: false, + supportsCacheControlOnTools: false, + supportsLongCacheRetention: false, + }, } satisfies Model<"anthropic-messages">, "accounts/fireworks/models/kimi-k2p6": { id: "accounts/fireworks/models/kimi-k2p6", @@ -3835,6 +3913,12 @@ export const MODELS = { }, contextWindow: 262000, maxTokens: 262000, + compat: { + sendSessionAffinityHeaders: true, + supportsEagerToolInputStreaming: false, + supportsCacheControlOnTools: false, + supportsLongCacheRetention: false, + }, } satisfies Model<"anthropic-messages">, "accounts/fireworks/models/minimax-m2p1": { id: "accounts/fireworks/models/minimax-m2p1", @@ -3852,6 +3936,12 @@ export const MODELS = { }, contextWindow: 200000, maxTokens: 200000, + compat: { + sendSessionAffinityHeaders: true, + supportsEagerToolInputStreaming: false, + supportsCacheControlOnTools: false, + supportsLongCacheRetention: false, + }, } satisfies Model<"anthropic-messages">, "accounts/fireworks/models/minimax-m2p5": { id: "accounts/fireworks/models/minimax-m2p5", @@ -3869,6 +3959,12 @@ export const MODELS = { }, contextWindow: 196608, maxTokens: 196608, + compat: { + sendSessionAffinityHeaders: true, + supportsEagerToolInputStreaming: false, + supportsCacheControlOnTools: false, + supportsLongCacheRetention: false, + }, } satisfies Model<"anthropic-messages">, "accounts/fireworks/models/minimax-m2p7": { id: "accounts/fireworks/models/minimax-m2p7", @@ -3886,6 +3982,12 @@ export const MODELS = { }, contextWindow: 196608, maxTokens: 196608, + compat: { + sendSessionAffinityHeaders: true, + supportsEagerToolInputStreaming: false, + supportsCacheControlOnTools: false, + supportsLongCacheRetention: false, + }, } satisfies Model<"anthropic-messages">, "accounts/fireworks/models/qwen3p6-plus": { id: "accounts/fireworks/models/qwen3p6-plus", @@ -3903,6 +4005,12 @@ export const MODELS = { }, contextWindow: 128000, maxTokens: 8192, + compat: { + sendSessionAffinityHeaders: true, + supportsEagerToolInputStreaming: false, + supportsCacheControlOnTools: false, + supportsLongCacheRetention: false, + }, } satisfies Model<"anthropic-messages">, "accounts/fireworks/routers/kimi-k2p5-turbo": { id: "accounts/fireworks/routers/kimi-k2p5-turbo", @@ -3920,6 +4028,12 @@ export const MODELS = { }, contextWindow: 256000, maxTokens: 256000, + compat: { + sendSessionAffinityHeaders: true, + supportsEagerToolInputStreaming: false, + supportsCacheControlOnTools: false, + supportsLongCacheRetention: false, + }, } satisfies Model<"anthropic-messages">, }, "github-copilot": { diff --git a/packages/ai/src/providers/anthropic.ts b/packages/ai/src/providers/anthropic.ts index 62febfc3..0897e8bc 100644 --- a/packages/ai/src/providers/anthropic.ts +++ b/packages/ai/src/providers/anthropic.ts @@ -165,9 +165,16 @@ const FINE_GRAINED_TOOL_STREAMING_BETA = "fine-grained-tool-streaming-2025-05-14 const INTERLEAVED_THINKING_BETA = "interleaved-thinking-2025-05-14"; function getAnthropicCompat(model: Model<"anthropic-messages">): Required { + // Auto-detect session affinity and cache control support from provider + const isFireworks = model.provider === "fireworks"; + const isCloudflareAiGatewayAnthropic = + model.provider === "cloudflare-ai-gateway" && model.baseUrl.includes("anthropic"); return { - supportsEagerToolInputStreaming: model.compat?.supportsEagerToolInputStreaming ?? true, - supportsLongCacheRetention: model.compat?.supportsLongCacheRetention ?? true, + supportsEagerToolInputStreaming: model.compat?.supportsEagerToolInputStreaming ?? !isFireworks, + supportsLongCacheRetention: model.compat?.supportsLongCacheRetention ?? !isFireworks, + sendSessionAffinityHeaders: + model.compat?.sendSessionAffinityHeaders ?? !!(isFireworks || isCloudflareAiGatewayAnthropic), + supportsCacheControlOnTools: model.compat?.supportsCacheControlOnTools ?? !isFireworks, }; } @@ -463,6 +470,9 @@ export const streamAnthropic: StreamFunction<"anthropic-messages", AnthropicOpti }); } + const cacheRetention = options?.cacheRetention ?? resolveCacheRetention(); + const cacheSessionId = cacheRetention === "none" ? undefined : options?.sessionId; + const created = createClient( model, apiKey, @@ -470,6 +480,7 @@ export const streamAnthropic: StreamFunction<"anthropic-messages", AnthropicOpti shouldUseFineGrainedToolStreamingBeta(model, context), options?.headers, copilotDynamicHeaders, + cacheSessionId, ); client = created.client; isOAuth = created.isOAuthToken; @@ -766,6 +777,7 @@ function createClient( useFineGrainedToolStreamingBeta: boolean, optionsHeaders?: Record, dynamicHeaders?: Record, + sessionId?: string, ): { client: Anthropic; isOAuthToken: boolean } { // Adaptive thinking models (Opus 4.6, Sonnet 4.6) have interleaved thinking built-in. // The beta header is deprecated on Opus 4.6 and redundant on Sonnet 4.6, so skip it. @@ -847,6 +859,8 @@ function createClient( } // API key auth + const sessionAffinityHeaders: Record = + sessionId && getAnthropicCompat(model).sendSessionAffinityHeaders ? { "x-session-affinity": sessionId } : {}; const client = new Anthropic({ apiKey, baseURL: model.baseUrl, @@ -857,6 +871,7 @@ function createClient( "anthropic-dangerous-direct-browser-access": "true", ...(betaFeatures.length > 0 ? { "anthropic-beta": betaFeatures.join(",") } : {}), }, + sessionAffinityHeaders, model.headers, optionsHeaders, ), @@ -912,11 +927,12 @@ function buildParams( } if (context.tools && context.tools.length > 0) { + const compat = getAnthropicCompat(model); params.tools = convertTools( context.tools, isOAuthToken, - getAnthropicCompat(model).supportsEagerToolInputStreaming, - cacheControl, + compat.supportsEagerToolInputStreaming, + compat.supportsCacheControlOnTools ? cacheControl : undefined, ); } diff --git a/packages/ai/src/types.ts b/packages/ai/src/types.ts index ec929ea6..851e4d4b 100644 --- a/packages/ai/src/types.ts +++ b/packages/ai/src/types.ts @@ -419,6 +419,22 @@ export interface AnthropicMessagesCompat { supportsEagerToolInputStreaming?: boolean; /** Whether the provider supports Anthropic long cache retention (`cache_control.ttl: "1h"`). Default: true. */ supportsLongCacheRetention?: boolean; + /** + * Whether to send the `x-session-affinity` header from `options.sessionId` + * when caching is enabled. Required for providers like Fireworks that use + * session affinity for prompt cache routing (requests to the same replica + * maximize cache hits). + * Default: false. + */ + sendSessionAffinityHeaders?: boolean; + /** + * Whether the provider supports Anthropic-style `cache_control` markers on + * tool definitions. When false, `cache_control` is omitted from tool params. + * Some Anthropic-compatible providers (e.g., Fireworks) do not support this + * field on tools and may reject or ignore it. + * Default: true. + */ + supportsCacheControlOnTools?: boolean; } /** diff --git a/packages/ai/test/fireworks-models.test.ts b/packages/ai/test/fireworks-models.test.ts index 7fec9899..35427c10 100644 --- a/packages/ai/test/fireworks-models.test.ts +++ b/packages/ai/test/fireworks-models.test.ts @@ -1,6 +1,11 @@ +import { createServer, type IncomingMessage, type ServerResponse } from "node:http"; +import type { AddressInfo } from "node:net"; +import { Type } from "typebox"; import { afterEach, describe, expect, it } from "vitest"; import { findEnvKeys, getEnvApiKey } from "../src/env-api-keys.js"; import { getModel } from "../src/models.js"; +import { streamAnthropic } from "../src/providers/anthropic.js"; +import type { Context, Model, Tool } from "../src/types.js"; const originalFireworksApiKey = process.env.FIREWORKS_API_KEY; @@ -47,4 +52,197 @@ describe("Fireworks models", () => { expect(findEnvKeys("fireworks")).toEqual(["FIREWORKS_API_KEY"]); expect(getEnvApiKey("fireworks")).toBe("test-fireworks-key"); }); + + it("sets Fireworks-specific compat for session affinity and unsupported tool fields", () => { + const model = getModel("fireworks", "accounts/fireworks/models/kimi-k2p6"); + + expect(model.compat).toBeDefined(); + expect(model.compat?.sendSessionAffinityHeaders).toBe(true); + expect(model.compat?.supportsEagerToolInputStreaming).toBe(false); + expect(model.compat?.supportsCacheControlOnTools).toBe(false); + expect(model.compat?.supportsLongCacheRetention).toBe(false); + }); +}); + +// --- Integration tests for Fireworks Anthropic session affinity and tool compat --- + +interface CapturedRequest { + headers: IncomingMessage["headers"]; + body: Record; +} + +const tool: Tool = { + name: "lookup", + description: "Look up a value", + parameters: Type.Object({ value: Type.String() }), +}; + +function createFireworksModel(compat?: Model<"anthropic-messages">["compat"]): Model<"anthropic-messages"> { + return { + id: "accounts/fireworks/models/kimi-k2p6", + name: "Kimi K2.6", + api: "anthropic-messages", + provider: "fireworks", + baseUrl: "http://127.0.0.1:0", // overridden by captureAnthropicRequest + reasoning: true, + input: ["text", "image"], + cost: { input: 0.95, output: 4, cacheRead: 0.16, cacheWrite: 0 }, + contextWindow: 262000, + maxTokens: 262000, + compat, + }; +} + +function createAnthropicModel(): Model<"anthropic-messages"> { + return { + id: "claude-opus-4-7", + name: "Claude Opus 4.7", + api: "anthropic-messages", + provider: "anthropic", + baseUrl: "http://127.0.0.1:0", // overridden by captureAnthropicRequest + reasoning: true, + input: ["text"], + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, + contextWindow: 200000, + maxTokens: 32000, + }; +} + +function createContext(tools: Tool[] = [tool]): Context { + return { + messages: [{ role: "user", content: "Use the tool", timestamp: Date.now() }], + ...(tools.length > 0 ? { tools } : {}), + }; +} + +async function readRequestBody(request: IncomingMessage): Promise> { + const chunks: Buffer[] = []; + for await (const chunk of request) { + chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)); + } + return JSON.parse(Buffer.concat(chunks).toString("utf8")) as Record; +} + +function writeEmptySseResponse(response: ServerResponse): void { + response.writeHead(200, { "content-type": "text/event-stream" }); + response.end(); +} + +async function captureAnthropicRequest( + model: Model<"anthropic-messages">, + context: Context, + options?: { sessionId?: string; cacheRetention?: string }, +): Promise { + let capturedRequest: CapturedRequest | undefined; + + const server = createServer(async (request, response) => { + capturedRequest = { + headers: request.headers, + body: await readRequestBody(request), + }; + writeEmptySseResponse(response); + }); + + await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve)); + const address = server.address() as AddressInfo; + + try { + // Override the model's baseUrl to point to the local test server + const localModel = { ...model, baseUrl: `http://127.0.0.1:${address.port}` }; + + const stream = streamAnthropic(localModel, context, { + apiKey: "test-key", + cacheRetention: (options?.cacheRetention as "none" | "short" | "long") ?? "short", + sessionId: options?.sessionId, + }); + + for await (const event of stream) { + if (event.type === "done" || event.type === "error") break; + } + } finally { + await new Promise((resolve, reject) => { + server.close((error) => (error ? reject(error) : resolve())); + }); + } + + if (!capturedRequest) { + throw new Error("Anthropic request was not captured"); + } + return capturedRequest; +} + +function getTools(body: Record): Record[] { + const tools = body.tools; + if (!Array.isArray(tools)) { + throw new Error("Expected tools in request body"); + } + return tools as Record[]; +} + +describe("Fireworks Anthropic session affinity and tool compat", () => { + it("sends x-session-affinity header for Fireworks models", async () => { + const model = createFireworksModel(); + // Need a real port, capture will assign one + const request = await captureAnthropicRequest(model, createContext(), { + sessionId: "fireworks-session-1", + }); + + expect(request.headers["x-session-affinity"]).toBe("fireworks-session-1"); + }); + + it("omits x-session-affinity header for native Anthropic models", async () => { + const model = createAnthropicModel(); + const request = await captureAnthropicRequest(model, createContext(), { + sessionId: "anthropic-session-1", + }); + + expect(request.headers["x-session-affinity"]).toBeUndefined(); + }); + + it("omits x-session-affinity header when cacheRetention is none", async () => { + const model = createFireworksModel(); + const request = await captureAnthropicRequest(model, createContext(), { + sessionId: "fireworks-session-2", + cacheRetention: "none", + }); + + expect(request.headers["x-session-affinity"]).toBeUndefined(); + }); + + it("omits cache_control on tools for Fireworks models", async () => { + const model = createFireworksModel(); + const request = await captureAnthropicRequest(model, createContext()); + + const tools = getTools(request.body); + const lastTool = tools[tools.length - 1]; + expect(lastTool.cache_control).toBeUndefined(); + }); + + it("omits eager_input_streaming on tools for Fireworks models", async () => { + const model = createFireworksModel(); + const request = await captureAnthropicRequest(model, createContext()); + + const tools = getTools(request.body); + for (const t of tools) { + expect(t.eager_input_streaming).toBeUndefined(); + } + }); + + it("sends cache_control on tools for native Anthropic models", async () => { + const model = createAnthropicModel(); + const request = await captureAnthropicRequest(model, createContext()); + + const tools = getTools(request.body); + const lastTool = tools[tools.length - 1]; + expect(lastTool.cache_control).toBeDefined(); + expect((lastTool.cache_control as { type: string }).type).toBe("ephemeral"); + }); + + it("sends eager_input_streaming on tools for native Anthropic models", async () => { + const model = createAnthropicModel(); + const request = await captureAnthropicRequest(model, createContext()); + + const tools = getTools(request.body); + expect(tools[0].eager_input_streaming).toBe(true); + }); }); From cf7f2e3dbbb514446aa66d71855e9afec4eb2ab4 Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Sun, 10 May 2026 17:47:45 +0200 Subject: [PATCH 004/166] fix(ai): update copilot claude test model --- packages/ai/CHANGELOG.md | 1 + packages/ai/test/context-overflow.test.ts | 2 +- packages/ai/test/empty.test.ts | 8 ++++---- packages/ai/test/github-copilot-anthropic.test.ts | 9 ++++----- packages/ai/test/image-tool-result.test.ts | 4 ++-- packages/ai/test/responseid.test.ts | 2 +- packages/ai/test/stream.test.ts | 2 +- packages/ai/test/tokens.test.ts | 2 +- packages/ai/test/tool-call-without-result.test.ts | 2 +- packages/ai/test/total-tokens.test.ts | 2 +- ...ransform-messages-copilot-openai-to-anthropic.test.ts | 4 ++-- packages/ai/test/unicode-surrogate.test.ts | 6 +++--- 12 files changed, 22 insertions(+), 22 deletions(-) diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index 67c402e6..2be28e9b 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -8,6 +8,7 @@ ### Fixed +- Fixed GitHub Copilot Claude test coverage to use the current Claude Sonnet 4.6 model ID. - Fixed OpenAI Responses requests for models that support disabling reasoning to send `reasoning.effort: "none"` when thinking is off. ## [0.74.0] - 2026-05-07 diff --git a/packages/ai/test/context-overflow.test.ts b/packages/ai/test/context-overflow.test.ts index 56d6620d..f84c3f47 100644 --- a/packages/ai/test/context-overflow.test.ts +++ b/packages/ai/test/context-overflow.test.ts @@ -143,7 +143,7 @@ describe("Context overflow error handling", () => { it.skipIf(!githubCopilotToken)( "claude-sonnet-4 - should detect overflow via isContextOverflow", async () => { - const model = getModel("github-copilot", "claude-sonnet-4"); + const model = getModel("github-copilot", "claude-sonnet-4.6"); const result = await testContextOverflow(model, githubCopilotToken!); logResult(result); diff --git a/packages/ai/test/empty.test.ts b/packages/ai/test/empty.test.ts index b14e9e55..eaf0e666 100644 --- a/packages/ai/test/empty.test.ts +++ b/packages/ai/test/empty.test.ts @@ -669,7 +669,7 @@ describe("AI Providers Empty Message Tests", () => { "claude-sonnet-4 - should handle empty content array", { retry: 3, timeout: 30000 }, async () => { - const llm = getModel("github-copilot", "claude-sonnet-4"); + const llm = getModel("github-copilot", "claude-sonnet-4.6"); await testEmptyMessage(llm, { apiKey: githubCopilotToken }); }, ); @@ -678,7 +678,7 @@ describe("AI Providers Empty Message Tests", () => { "claude-sonnet-4 - should handle empty string content", { retry: 3, timeout: 30000 }, async () => { - const llm = getModel("github-copilot", "claude-sonnet-4"); + const llm = getModel("github-copilot", "claude-sonnet-4.6"); await testEmptyStringMessage(llm, { apiKey: githubCopilotToken }); }, ); @@ -687,7 +687,7 @@ describe("AI Providers Empty Message Tests", () => { "claude-sonnet-4 - should handle whitespace-only content", { retry: 3, timeout: 30000 }, async () => { - const llm = getModel("github-copilot", "claude-sonnet-4"); + const llm = getModel("github-copilot", "claude-sonnet-4.6"); await testWhitespaceOnlyMessage(llm, { apiKey: githubCopilotToken }); }, ); @@ -696,7 +696,7 @@ describe("AI Providers Empty Message Tests", () => { "claude-sonnet-4 - should handle empty assistant message in conversation", { retry: 3, timeout: 30000 }, async () => { - const llm = getModel("github-copilot", "claude-sonnet-4"); + const llm = getModel("github-copilot", "claude-sonnet-4.6"); await testEmptyAssistantMessage(llm, { apiKey: githubCopilotToken }); }, ); diff --git a/packages/ai/test/github-copilot-anthropic.test.ts b/packages/ai/test/github-copilot-anthropic.test.ts index 2fb44f23..c09de06b 100644 --- a/packages/ai/test/github-copilot-anthropic.test.ts +++ b/packages/ai/test/github-copilot-anthropic.test.ts @@ -1,5 +1,6 @@ import { describe, expect, it, vi } from "vitest"; import { getModel } from "../src/models.js"; +import { streamAnthropic } from "../src/providers/anthropic.js"; import type { Context } from "../src/types.js"; const mockState = vi.hoisted(() => ({ @@ -54,10 +55,9 @@ describe("Copilot Claude via Anthropic Messages", () => { }; it("uses Bearer auth, Copilot headers, and valid Anthropic Messages payload", async () => { - const model = getModel("github-copilot", "claude-sonnet-4"); + const model = getModel("github-copilot", "claude-sonnet-4.6"); expect(model.api).toBe("anthropic-messages"); - const { streamAnthropic } = await import("../src/providers/anthropic.js"); const s = streamAnthropic(model, context, { apiKey: "tid_copilot_session_test_token" }); for await (const event of s) { if (event.type === "error") break; @@ -85,15 +85,14 @@ describe("Copilot Claude via Anthropic Messages", () => { // Payload is valid Anthropic Messages format const params = mockState.createParams!; - expect(params.model).toBe("claude-sonnet-4"); + expect(params.model).toBe("claude-sonnet-4.6"); expect(params.stream).toBe(true); expect(params.max_tokens).toBeGreaterThan(0); expect(Array.isArray(params.messages)).toBe(true); }); it("includes interleaved-thinking beta when reasoning is enabled", async () => { - const model = getModel("github-copilot", "claude-sonnet-4"); - const { streamAnthropic } = await import("../src/providers/anthropic.js"); + const model = getModel("github-copilot", "claude-sonnet-4.6"); const s = streamAnthropic(model, context, { apiKey: "tid_copilot_session_test_token", interleavedThinking: true, diff --git a/packages/ai/test/image-tool-result.test.ts b/packages/ai/test/image-tool-result.test.ts index 0477b162..72bd0414 100644 --- a/packages/ai/test/image-tool-result.test.ts +++ b/packages/ai/test/image-tool-result.test.ts @@ -464,7 +464,7 @@ describe("Tool Results with Images", () => { "claude-sonnet-4 - should handle tool result with only image", { retry: 3, timeout: 30000 }, async () => { - const llm = getModel("github-copilot", "claude-sonnet-4"); + const llm = getModel("github-copilot", "claude-sonnet-4.6"); await handleToolWithImageResult(llm, { apiKey: githubCopilotToken }); }, ); @@ -473,7 +473,7 @@ describe("Tool Results with Images", () => { "claude-sonnet-4 - should handle tool result with text and image", { retry: 3, timeout: 30000 }, async () => { - const llm = getModel("github-copilot", "claude-sonnet-4"); + const llm = getModel("github-copilot", "claude-sonnet-4.6"); await handleToolWithTextAndImageResult(llm, { apiKey: githubCopilotToken }); }, ); diff --git a/packages/ai/test/responseid.test.ts b/packages/ai/test/responseid.test.ts index 6181ffa3..00e6cc5d 100644 --- a/packages/ai/test/responseid.test.ts +++ b/packages/ai/test/responseid.test.ts @@ -106,7 +106,7 @@ describe("responseId E2E Tests", () => { "Anthropic path should expose responseId", { retry: 3, timeout: 30000 }, async () => { - const llm = getModel("github-copilot", "claude-sonnet-4"); + const llm = getModel("github-copilot", "claude-sonnet-4.6"); await expectResponseId(llm, { apiKey: githubCopilotToken }); }, ); diff --git a/packages/ai/test/stream.test.ts b/packages/ai/test/stream.test.ts index 68d2810c..21199d38 100644 --- a/packages/ai/test/stream.test.ts +++ b/packages/ai/test/stream.test.ts @@ -1245,7 +1245,7 @@ describe("Generate E2E Tests", () => { }); describe("GitHub Copilot Provider (claude-sonnet-4 via Anthropic Messages)", () => { - const llm = getModel("github-copilot", "claude-sonnet-4"); + const llm = getModel("github-copilot", "claude-sonnet-4.6"); it.skipIf(!githubCopilotToken)("should complete basic text generation", { retry: 3 }, async () => { await basicTextGeneration(llm, { apiKey: githubCopilotToken }); diff --git a/packages/ai/test/tokens.test.ts b/packages/ai/test/tokens.test.ts index a63d9321..00c2b567 100644 --- a/packages/ai/test/tokens.test.ts +++ b/packages/ai/test/tokens.test.ts @@ -301,7 +301,7 @@ describe("Token Statistics on Abort", () => { "claude-sonnet-4 - should include token stats when aborted mid-stream", { retry: 3, timeout: 30000 }, async () => { - const llm = getModel("github-copilot", "claude-sonnet-4"); + const llm = getModel("github-copilot", "claude-sonnet-4.6"); await testTokensOnAbort(llm, { apiKey: githubCopilotToken }); }, ); diff --git a/packages/ai/test/tool-call-without-result.test.ts b/packages/ai/test/tool-call-without-result.test.ts index 3f375e47..18a21f99 100644 --- a/packages/ai/test/tool-call-without-result.test.ts +++ b/packages/ai/test/tool-call-without-result.test.ts @@ -309,7 +309,7 @@ describe("Tool Call Without Result Tests", () => { "claude-sonnet-4 - should filter out tool calls without corresponding tool results", { retry: 3, timeout: 30000 }, async () => { - const model = getModel("github-copilot", "claude-sonnet-4"); + const model = getModel("github-copilot", "claude-sonnet-4.6"); await testToolCallWithoutResult(model, { apiKey: githubCopilotToken }); }, ); diff --git a/packages/ai/test/total-tokens.test.ts b/packages/ai/test/total-tokens.test.ts index 3e956ff0..f634a5c3 100644 --- a/packages/ai/test/total-tokens.test.ts +++ b/packages/ai/test/total-tokens.test.ts @@ -726,7 +726,7 @@ describe("totalTokens field", () => { "claude-sonnet-4 - should return totalTokens equal to sum of components", { retry: 3, timeout: 60000 }, async () => { - const llm = getModel("github-copilot", "claude-sonnet-4"); + const llm = getModel("github-copilot", "claude-sonnet-4.6"); console.log(`\nGitHub Copilot / ${llm.id}:`); const { first, second } = await testTotalTokensWithCache(llm, { apiKey: githubCopilotToken }); diff --git a/packages/ai/test/transform-messages-copilot-openai-to-anthropic.test.ts b/packages/ai/test/transform-messages-copilot-openai-to-anthropic.test.ts index 111bd8d4..1db3bad6 100644 --- a/packages/ai/test/transform-messages-copilot-openai-to-anthropic.test.ts +++ b/packages/ai/test/transform-messages-copilot-openai-to-anthropic.test.ts @@ -13,8 +13,8 @@ function anthropicNormalizeToolCallId( function makeCopilotClaudeModel(): Model<"anthropic-messages"> { return { - id: "claude-sonnet-4", - name: "Claude Sonnet 4", + id: "claude-sonnet-4.6", + name: "Claude Sonnet 4.6", api: "anthropic-messages", provider: "github-copilot", baseUrl: "https://api.individual.githubcopilot.com", diff --git a/packages/ai/test/unicode-surrogate.test.ts b/packages/ai/test/unicode-surrogate.test.ts index 75fbf19a..5b348ca5 100644 --- a/packages/ai/test/unicode-surrogate.test.ts +++ b/packages/ai/test/unicode-surrogate.test.ts @@ -426,7 +426,7 @@ describe("AI Providers Unicode Surrogate Pair Tests", () => { "claude-sonnet-4 - should handle emoji in tool results", { retry: 3, timeout: 30000 }, async () => { - const llm = getModel("github-copilot", "claude-sonnet-4"); + const llm = getModel("github-copilot", "claude-sonnet-4.6"); await testEmojiInToolResults(llm, { apiKey: githubCopilotToken }); }, ); @@ -435,7 +435,7 @@ describe("AI Providers Unicode Surrogate Pair Tests", () => { "claude-sonnet-4 - should handle real-world LinkedIn comment data with emoji", { retry: 3, timeout: 30000 }, async () => { - const llm = getModel("github-copilot", "claude-sonnet-4"); + const llm = getModel("github-copilot", "claude-sonnet-4.6"); await testRealWorldLinkedInData(llm, { apiKey: githubCopilotToken }); }, ); @@ -444,7 +444,7 @@ describe("AI Providers Unicode Surrogate Pair Tests", () => { "claude-sonnet-4 - should handle unpaired high surrogate (0xD83D) in tool results", { retry: 3, timeout: 30000 }, async () => { - const llm = getModel("github-copilot", "claude-sonnet-4"); + const llm = getModel("github-copilot", "claude-sonnet-4.6"); await testUnpairedHighSurrogate(llm, { apiKey: githubCopilotToken }); }, ); From 533d37305c3c189a8f7f82e0919672cf71efdaa6 Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Sun, 10 May 2026 17:51:08 +0200 Subject: [PATCH 005/166] fix(ai): align copilot claude adaptive test --- packages/ai/test/github-copilot-anthropic.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/ai/test/github-copilot-anthropic.test.ts b/packages/ai/test/github-copilot-anthropic.test.ts index c09de06b..30b5b83e 100644 --- a/packages/ai/test/github-copilot-anthropic.test.ts +++ b/packages/ai/test/github-copilot-anthropic.test.ts @@ -91,7 +91,7 @@ describe("Copilot Claude via Anthropic Messages", () => { expect(Array.isArray(params.messages)).toBe(true); }); - it("includes interleaved-thinking beta when reasoning is enabled", async () => { + it("omits interleaved-thinking beta for adaptive-thinking models", async () => { const model = getModel("github-copilot", "claude-sonnet-4.6"); const s = streamAnthropic(model, context, { apiKey: "tid_copilot_session_test_token", @@ -102,6 +102,6 @@ describe("Copilot Claude via Anthropic Messages", () => { } const headers = mockState.constructorOpts!.defaultHeaders as Record; - expect(headers["anthropic-beta"]).toContain("interleaved-thinking-2025-05-14"); + expect(headers["anthropic-beta"] ?? "").not.toContain("interleaved-thinking-2025-05-14"); }); }); From f2231fa5f7a892c134cdd9936e536fcc198c225c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 10 May 2026 15:55:29 +0000 Subject: [PATCH 006/166] chore: approve contributor maximilianzuern --- .github/APPROVED_CONTRIBUTORS | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/APPROVED_CONTRIBUTORS b/.github/APPROVED_CONTRIBUTORS index 9ee75aff..6f2193ab 100644 --- a/.github/APPROVED_CONTRIBUTORS +++ b/.github/APPROVED_CONTRIBUTORS @@ -203,3 +203,5 @@ pandada8 pr npupko issue chrisvariety pr + +maximilianzuern pr From f8d0fa67dc67669374bf1442c2df9227f848a192 Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Sun, 10 May 2026 18:12:40 +0200 Subject: [PATCH 007/166] fix(coding-agent): share theme across package scopes closes #4333 --- packages/coding-agent/CHANGELOG.md | 1 + packages/coding-agent/src/modes/interactive/theme/theme.ts | 2 ++ 2 files changed, 3 insertions(+) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 3136e113..4a9261c4 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -8,6 +8,7 @@ ### Fixed +- Fixed theme sharing across package scopes so extensions do not crash with `Theme not initialized` ([#4333](https://github.com/earendil-works/pi/issues/4333)). - Fixed keybinding hints to show Option instead of Alt on macOS ([#4289](https://github.com/earendil-works/pi/issues/4289)). - Fixed the interactive update notification to render the changelog as an OSC 8 hyperlink when the terminal supports hyperlinks ([#4280](https://github.com/earendil-works/pi/issues/4280)). diff --git a/packages/coding-agent/src/modes/interactive/theme/theme.ts b/packages/coding-agent/src/modes/interactive/theme/theme.ts index 955fa44a..30e4a233 100644 --- a/packages/coding-agent/src/modes/interactive/theme/theme.ts +++ b/packages/coding-agent/src/modes/interactive/theme/theme.ts @@ -658,6 +658,7 @@ function getDefaultTheme(): string { // Use globalThis to share theme across module loaders (tsx + jiti in dev mode) const THEME_KEY = Symbol.for("@earendil-works/pi-coding-agent:theme"); +const THEME_KEY_OLD = Symbol.for("@mariozechner/pi-coding-agent:theme"); // Export theme as a getter that reads from globalThis // This ensures all module instances (tsx, jiti) see the same theme @@ -671,6 +672,7 @@ export const theme: Theme = new Proxy({} as Theme, { function setGlobalTheme(t: Theme): void { (globalThis as Record)[THEME_KEY] = t; + (globalThis as Record)[THEME_KEY_OLD] = t; } let currentThemeName: string | undefined; From c0f416aabc6c3f991fde19e68bb37dec6c8b9a7e Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Sun, 10 May 2026 18:12:59 +0200 Subject: [PATCH 008/166] feat(agent): add harness stream configuration --- packages/agent/docs/agent-harness.md | 157 +++++++++++++++- packages/agent/src/harness/agent-harness.ts | 190 ++++++++++++++++++-- packages/agent/src/harness/types.ts | 59 ++++-- 3 files changed, 371 insertions(+), 35 deletions(-) diff --git a/packages/agent/docs/agent-harness.md b/packages/agent/docs/agent-harness.md index c88cf7aa..b3a57377 100644 --- a/packages/agent/docs/agent-harness.md +++ b/packages/agent/docs/agent-harness.md @@ -31,6 +31,7 @@ Harness config is the latest runtime configuration set by the application or ext - tools - active tool names - resources +- stream options - system prompt or system prompt provider Getters return harness config. They do not return the snapshot used by an in-flight provider request. @@ -52,11 +53,15 @@ A turn snapshot is the concrete state used for one LLM turn. It is created by `c - thinking level - all tools - active tools +- stream options +- derived session id Static option values are used directly. System-prompt provider callbacks are invoked once per `createTurnState()` call. All logic for that turn uses the same snapshot. Resource arrays are shallow-copied when a snapshot is created. Individual skill and prompt-template objects are not deep-copied. +Stream options are shallow-copied when a snapshot is created. `headers` and `metadata` maps are shallow-copied; their values are not deep-copied. Credentials from `getApiKeyAndHeaders()` are resolved per provider request so expiring tokens can refresh, but the configured stream options and derived session id come from the current turn snapshot. + ### Session The session contains persisted entries only. Session reads return persisted state and do not include queued writes. @@ -125,9 +130,9 @@ At a save point the harness: 1. flushes pending session writes after the agent-emitted messages for that turn 2. creates a fresh turn snapshot if the low-level loop may continue -3. applies the fresh context/model/thinking-level state before the next provider request +3. applies the fresh context/model/thinking-level/stream-options/session-id state before the next provider request -This lets model, thinking level, tool, resource, and system prompt changes made during a turn affect the next turn in the same run, while never mutating an in-flight provider request. The loop callbacks are not recreated at save points. +This lets model, thinking level, tool, resource, stream option, and system prompt changes made during a turn affect the next turn in the same run, while never mutating an in-flight provider request. The loop callbacks are not recreated at save points. The low-level loop converts harness `ThinkingLevel` to provider `reasoning` at the provider boundary: @@ -188,12 +193,154 @@ Branch summary generation is part of the tree navigation operation. Auto-compaction and retry decision points are not implemented in `AgentHarness` yet. -## Final lifecycle hardening todo +## Implementation todo -Before treating `AgentHarness` as migration-ready, add a broad test suite that exercises listeners and hooks closing over the harness and calling public APIs during every relevant event: +This list tracks the remaining work before treating `AgentHarness` as migration-ready. + +### 1. Finish curated provider/stream configuration + +Implemented so far: + +- `AgentHarnessOptions.streamOptions` provides curated request configuration. +- `getStreamOptions()` returns a shallow copy of current harness config. +- `setStreamOptions()` replaces current harness config. +- Stream options are snapshotted in `createTurnState()` and applied with `applyTurnState()`. +- `headers` and `metadata` maps are shallow-copied when stream options are copied. +- `sessionId` is derived from `session.getMetadata().id` in the turn snapshot. +- The harness installs its own internal stream wrapper and calls `streamSimple()`. +- The wrapper ignores raw incoming provider options except lifecycle-owned fields that must come from the low-level loop: `signal` and `reasoning`. +- Credentials and auth headers from `getApiKeyAndHeaders()` are resolved per provider request. + +Implemented provider hook behavior: + +- `before_provider_request` runs before `streamSimple()` and can patch curated stream options for the current request only. +- `before_provider_payload` maps to the underlying `pi-ai` `onPayload` and can inspect/replace provider-specific payloads. +- `after_provider_response` maps to the underlying `pi-ai` `onResponse` and observes response status/headers before body consumption. +- `AgentHarnessStreamOptionsPatch` has explicit deletion semantics: + - top-level fields present with `undefined` clear that option. + - `headers` and `metadata` patches may set individual keys to `undefined` to delete them. + - `headers: undefined` or `metadata: undefined`, when explicitly present, clears the whole map. +- Current-request stream option merge order is: + 1. snapshotted `streamOptions` + 2. auth headers from `getApiKeyAndHeaders()` + 3. `before_provider_request` patches, in hook registration order +- `before_provider_request` does not patch `reasoning`; add that only if a concrete use case appears. + +Still needed: + +- Add tests proving stream options are snapshotted per turn and changes while busy affect only future provider requests/save-point snapshots. +- Add tests proving provider hook patch/deletion/chaining semantics. + +### 2. Design per-`AgentHarness` model registry + +Not started. + +Still needed: + +- Decide how applications supply the model registry. +- Decide whether the harness stores concrete `Model` objects, model references, or both. +- Validate model selection against the registry. +- Define model change semantics during active turns and save points. +- Preserve current `setModel()` behavior until the registry model is designed. + +### 3. Design generic hook/event extension mechanism + +Current cleanup already done: + +- Removed `AgentHarnessContext`. +- Hooks receive only event payloads. +- `emitHook(event)` derives the hook type from `event.type`. + +Still needed: + +- Define extension context shape. +- Likely expose a harness facade plus a session facade rather than raw internals. +- Decide which public harness APIs are allowed from each hook/event. +- Decide whether hooks can mutate turn snapshots directly or only through explicit hook results/public APIs. +- Clarify event payload semantics versus harness getter semantics. +- Revisit `AgentHarnessOwnEvent` versus `AgentHarnessEvent`. +- Define hook result chaining where it has clean transform semantics: + - `before_provider_request`: each hook receives the stream options produced by previous hooks. + - `before_provider_payload`: each hook receives the payload produced by previous hooks. + - possibly `context`: each hook receives the messages produced by previous hooks. + - possibly `tool_result`: each hook receives the result fields produced by previous hooks. +- Do not chain hooks where semantics are policy-based or ambiguous until explicitly designed, such as `tool_call`, `session_before_compact`, `session_before_tree`, and `before_agent_start`. + +### 4. Add explicit tool registry read/update semantics + +Implemented so far: + +- `setTools(tools, activeToolNames?)` +- `setActiveTools(toolNames)` +- invalid active tool names throw +- generic common app tool shape via `AgentHarness` +- `QueueMode` exported from `Agent` +- `AgentHarnessOptions.steeringMode` / `followUpMode` +- live `steeringMode` / `followUpMode` getters/setters +- queue modes are immediate/live, matching coding-agent behavior + +Still needed: + +- Add `getTools()` semantics. +- Add `getActiveTools()` semantics. +- Decide and implement tool update observability events. +- Include active-tool-only updates in the uniform runtime config observability plan. + +### 5. Full `AgentHarness` lifecycle/state pass + +Implemented so far: + +- Removed constructor `void syncFromTree()`. +- Removed `syncFromTree()`. +- Added `createTurnState()`, `applyTurnState()`, and `executeTurn()`. +- Low-level `AgentLoopConfig.prepareNextTurn` save-point update exists. +- `prepareNextTurn` updates low-level context/model/thinking-level and harness-applied stream/session snapshot state. +- The loop converts `ThinkingLevel` to provider `reasoning` internally. +- `phase` replaces boolean idle. +- Pending session writes are based on session-entry shapes without generated fields. +- Pending session writes flush at save points, settlement, and failure cleanup. +- `steer`, `followUp`, and `nextTurn` accept text plus optional images and create `UserMessage` internally. +- `nextTurn` ordering is fixed: queued messages before the new user message. +- Removed `liveOperationId`. +- Removed `shell()`; use `harness.env`. + +Still needed: + +- Finalize phase/idle semantics. +- Audit whether `settled` can fire too early. +- Make session writes inside `settled` callbacks deterministic. +- Audit follow-up behavior around `agent_end`. +- Implement auto-compaction decision point. +- Implement retry handling. +- Ensure structural operations use consistent `try/finally` phase cleanup. +- Verify `before_agent_start` hook semantics against coding-agent: + - current behavior prepends returned messages. + - decide whether replacement, prepend, append, or transform semantics are correct. +- Decide if `before_agent_start` needs more turn info such as tools/tool snippets. +- Document or change timing for model/thinking/stream-option events that may fire before queued session entries persist while busy. +- Audit `abort()` barrier semantics. + +### 6. Later coding-agent migration plan + +Not started. + +Still needed: + +- Map coding-agent resources to sourced loaders. +- Keep app-level resource dedupe/provenance outside the harness. +- Adapt extension loader to the future hook/session facade. +- Preserve UI/session behavior outside core. +- Move coding-agent stream/auth/retry/header behavior onto the harness stream configuration and provider hooks. + +### 7. Final lifecycle hardening suite + +Before treating `AgentHarness` as migration-ready, add a broad test suite that exercises listeners and hooks closing over the harness and calling public APIs during every relevant event. + +Needs broad tests for: - runtime config setters from low-level lifecycle events and harness events -- resource/tool/model/thinking updates during active turns and save points +- uniform runtime config observability events for model, thinking, resources, tools, active tools, and stream options +- resource/tool/model/thinking/stream-option updates during active turns and save points - session writes from listeners and hooks, including writes from `settled` - queue operations from turn events, tool events, and provider hooks - rejected structural operations while busy diff --git a/packages/agent/src/harness/agent-harness.ts b/packages/agent/src/harness/agent-harness.ts index 33b96b37..879a3599 100644 --- a/packages/agent/src/harness/agent-harness.ts +++ b/packages/agent/src/harness/agent-harness.ts @@ -1,4 +1,10 @@ -import type { AssistantMessage, ImageContent, Model, UserMessage } from "@earendil-works/pi-ai"; +import { + type AssistantMessage, + type ImageContent, + type Model, + streamSimple, + type UserMessage, +} from "@earendil-works/pi-ai"; import { Agent, type QueueMode } from "../agent.js"; import type { AgentEvent, AgentMessage, AgentTool, ThinkingLevel } from "../types.js"; import { collectEntriesForBranchSummary, generateBranchSummary } from "./compaction/branch-summarization.js"; @@ -13,7 +19,8 @@ import type { AgentHarnessOwnEvent, AgentHarnessPhase, AgentHarnessResources, - AgentHarnessTurnState, + AgentHarnessStreamOptions, + AgentHarnessStreamOptionsPatch, ExecutionEnv, NavigateTreeResult, PendingSessionWrite, @@ -28,6 +35,87 @@ function createUserMessage(text: string, images?: ImageContent[]): UserMessage { return { role: "user", content, timestamp: Date.now() }; } +function cloneStreamOptions(streamOptions?: AgentHarnessStreamOptions): AgentHarnessStreamOptions { + return { + ...streamOptions, + headers: streamOptions?.headers ? { ...streamOptions.headers } : undefined, + metadata: streamOptions?.metadata ? { ...streamOptions.metadata } : undefined, + }; +} + +function mergeHeaders(...headers: Array | undefined>): Record | undefined { + const merged: Record = {}; + let hasHeaders = false; + for (const entry of headers) { + if (!entry) continue; + Object.assign(merged, entry); + hasHeaders = true; + } + return hasHeaders ? merged : undefined; +} + +function hasOwn(object: object, key: PropertyKey): boolean { + return Object.hasOwn(object, key); +} + +function applyStreamOptionsPatch( + base: AgentHarnessStreamOptions, + patch?: AgentHarnessStreamOptionsPatch, +): AgentHarnessStreamOptions { + const result = cloneStreamOptions(base); + if (!patch) return result; + + if (hasOwn(patch, "transport")) result.transport = patch.transport; + if (hasOwn(patch, "timeoutMs")) result.timeoutMs = patch.timeoutMs; + if (hasOwn(patch, "maxRetries")) result.maxRetries = patch.maxRetries; + if (hasOwn(patch, "maxRetryDelayMs")) result.maxRetryDelayMs = patch.maxRetryDelayMs; + if (hasOwn(patch, "cacheRetention")) result.cacheRetention = patch.cacheRetention; + + if (hasOwn(patch, "headers")) { + if (patch.headers === undefined) { + result.headers = undefined; + } else { + const headers = { ...(result.headers ?? {}) }; + for (const [key, value] of Object.entries(patch.headers)) { + if (value === undefined) delete headers[key]; + else headers[key] = value; + } + result.headers = Object.keys(headers).length > 0 ? headers : undefined; + } + } + + if (hasOwn(patch, "metadata")) { + if (patch.metadata === undefined) { + result.metadata = undefined; + } else { + const metadata = { ...(result.metadata ?? {}) }; + for (const [key, value] of Object.entries(patch.metadata)) { + if (value === undefined) delete metadata[key]; + else metadata[key] = value; + } + result.metadata = Object.keys(metadata).length > 0 ? metadata : undefined; + } + } + + return result; +} + +interface AgentHarnessTurnState< + TSkill extends Skill = Skill, + TPromptTemplate extends PromptTemplate = PromptTemplate, + TTool extends AgentTool = AgentTool, +> { + messages: AgentMessage[]; + resources: AgentHarnessResources; + streamOptions: AgentHarnessStreamOptions; + sessionId: string; + systemPrompt: string; + model: Model; + thinkingLevel: ThinkingLevel; + tools: TTool[]; + activeTools: TTool[]; +} + export class AgentHarness< TSkill extends Skill = Skill, TPromptTemplate extends PromptTemplate = PromptTemplate, @@ -45,6 +133,9 @@ export class AgentHarness< private followUpQueue: UserMessage[] = []; private pendingSessionWrites: PendingSessionWrite[] = []; private resources: AgentHarnessResources; + private streamOptions: AgentHarnessStreamOptions; + private appliedStreamOptions: AgentHarnessStreamOptions = {}; + private appliedSessionId?: string; private systemPrompt: AgentHarnessOptions["systemPrompt"]; private getApiKeyAndHeaders?: AgentHarnessOptions["getApiKeyAndHeaders"]; private tools = new Map(); @@ -60,12 +151,46 @@ export class AgentHarness< thinkingLevel: options.thinkingLevel, tools: options.tools ?? [], }, + streamFn: async (model, context, streamOptions) => { + const auth = await this.getApiKeyAndHeaders?.(model); + const snapshotOptions: AgentHarnessStreamOptions = { + ...this.appliedStreamOptions, + headers: mergeHeaders(this.appliedStreamOptions.headers, auth?.headers), + }; + const requestOptions = await this.emitBeforeProviderRequest( + model, + this.appliedSessionId ?? "", + snapshotOptions, + ); + return streamSimple(model, context, { + cacheRetention: requestOptions.cacheRetention, + headers: requestOptions.headers, + maxRetries: requestOptions.maxRetries, + maxRetryDelayMs: requestOptions.maxRetryDelayMs, + metadata: requestOptions.metadata, + onPayload: async (payload) => await this.emitBeforeProviderPayload(model, payload), + onResponse: async (response) => { + const headers = { ...(response.headers as Record) }; + await this.emitOwn( + { type: "after_provider_response", status: response.status, headers }, + this.agent.signal, + ); + }, + reasoning: streamOptions?.reasoning, + signal: streamOptions?.signal, + sessionId: this.appliedSessionId, + timeoutMs: requestOptions.timeoutMs, + transport: requestOptions.transport, + apiKey: auth?.apiKey, + }); + }, steeringMode: options.steeringMode, followUpMode: options.followUpMode, }); this.env = options.env; this.session = options.session; this.resources = options.resources ?? {}; + this.streamOptions = cloneStreamOptions(options.streamOptions); this.systemPrompt = options.systemPrompt; this.getApiKeyAndHeaders = options.getApiKeyAndHeaders; for (const tool of options.tools ?? []) { @@ -76,11 +201,6 @@ export class AgentHarness< this.activeToolNames = options.activeToolNames ?? (options.tools ?? []).map((tool) => tool.name); this.agent.state.model = this.model; this.agent.state.thinkingLevel = this.thinkingLevel; - this.agent.getApiKey = async (provider) => { - const model = this.model; - if (!this.getApiKeyAndHeaders || model.provider !== provider) return undefined; - return (await this.getApiKeyAndHeaders(model))?.apiKey; - }; this.agent.transformContext = async (messages) => { const result = await this.emitHook({ type: "context", messages: [...messages] }); return result?.messages ?? messages; @@ -108,14 +228,6 @@ export class AgentHarness< ? { content: patch.content, details: patch.details, isError: patch.isError, terminate: patch.terminate } : undefined; }; - this.agent.onPayload = async (payload) => { - const result = await this.emitHook({ type: "before_provider_request", payload }); - return result?.payload ?? payload; - }; - this.agent.onResponse = async (response) => { - const headers = { ...(response.headers as Record) }; - await this.emitOwn({ type: "after_provider_response", status: response.status, headers }, this.agent.signal); - }; this.agent.prepareNextTurn = async () => { await this.flushPendingSessionWrites(); const turnState = await this.createTurnState(); @@ -162,6 +274,41 @@ export class AgentHarness< return lastResult; } + private async emitBeforeProviderRequest( + model: Model, + sessionId: string, + streamOptions: AgentHarnessStreamOptions, + ): Promise { + const handlers = this.hooks.get("before_provider_request"); + let current = cloneStreamOptions(streamOptions); + if (!handlers || handlers.size === 0) return current; + for (const handler of handlers) { + const result = await handler({ + type: "before_provider_request", + model, + sessionId, + streamOptions: cloneStreamOptions(current), + }); + if (result?.streamOptions) { + current = applyStreamOptionsPatch(current, result.streamOptions); + } + } + return current; + } + + private async emitBeforeProviderPayload(model: Model, payload: unknown): Promise { + const handlers = this.hooks.get("before_provider_payload"); + let current = payload; + if (!handlers || handlers.size === 0) return current; + for (const handler of handlers) { + const result = await handler({ type: "before_provider_payload", model, payload: current }); + if (result !== undefined) { + current = result.payload; + } + } + return current; + } + private async emitQueueUpdate(): Promise { await this.emitOwn({ type: "queue_update", @@ -174,6 +321,7 @@ export class AgentHarness< private async createTurnState(): Promise> { const context = await this.session.buildContext(); const resources = this.getResources(); + const sessionMetadata = await this.session.getMetadata(); const tools = [...this.tools.values()]; const activeTools = this.activeToolNames .map((name) => this.tools.get(name)) @@ -194,6 +342,8 @@ export class AgentHarness< return { messages: context.messages, resources, + streamOptions: cloneStreamOptions(this.streamOptions), + sessionId: sessionMetadata.id, systemPrompt, model: this.model, thinkingLevel: this.thinkingLevel, @@ -204,6 +354,8 @@ export class AgentHarness< private applyTurnState(turnState: AgentHarnessTurnState): void { this.agent.state.messages = turnState.messages; + this.appliedStreamOptions = cloneStreamOptions(turnState.streamOptions); + this.appliedSessionId = turnState.sessionId; this.agent.state.systemPrompt = turnState.systemPrompt; this.agent.state.model = turnState.model; this.agent.state.thinkingLevel = turnState.thinkingLevel; @@ -602,6 +754,14 @@ export class AgentHarness< await this.emitOwn({ type: "resources_update", resources: this.getResources(), previousResources }); } + getStreamOptions(): AgentHarnessStreamOptions { + return cloneStreamOptions(this.streamOptions); + } + + setStreamOptions(streamOptions: AgentHarnessStreamOptions): void { + this.streamOptions = cloneStreamOptions(streamOptions); + } + async setTools(tools: TTool[], activeToolNames?: string[]): Promise { this.tools = new Map(tools.map((tool) => [tool.name, tool])); if (activeToolNames) { diff --git a/packages/agent/src/harness/types.ts b/packages/agent/src/harness/types.ts index 24b5feeb..f50337f8 100644 --- a/packages/agent/src/harness/types.ts +++ b/packages/agent/src/harness/types.ts @@ -1,4 +1,4 @@ -import type { ImageContent, Model, TextContent } from "@earendil-works/pi-ai"; +import type { ImageContent, Model, SimpleStreamOptions, TextContent, Transport } from "@earendil-works/pi-ai"; import type { QueueMode } from "../agent.js"; import type { AgentEvent, AgentMessage, AgentTool, ThinkingLevel } from "../index.js"; import type { Session } from "./session/session.js"; @@ -43,6 +43,33 @@ export interface AgentHarnessResources< skills?: TSkill[]; } +/** Curated provider request options owned by the harness and snapshotted per turn. */ +export interface AgentHarnessStreamOptions { + /** Preferred transport forwarded to the stream function. */ + transport?: Transport; + /** Provider request timeout in milliseconds. */ + timeoutMs?: number; + /** Maximum provider retry attempts. */ + maxRetries?: number; + /** Optional cap for provider-requested retry delays. */ + maxRetryDelayMs?: number; + /** Additional request headers merged with auth and lifecycle headers. */ + headers?: Record; + /** Provider metadata forwarded with requests. */ + metadata?: SimpleStreamOptions["metadata"]; + /** Provider cache retention hint. */ + cacheRetention?: SimpleStreamOptions["cacheRetention"]; +} + +/** Per-request stream option patch returned by provider hooks. */ +export interface AgentHarnessStreamOptionsPatch + extends Omit, "headers" | "metadata"> { + /** Header patch. `undefined` values delete keys; explicit `headers: undefined` clears all headers. */ + headers?: Record; + /** Metadata patch. `undefined` values delete keys; explicit `metadata: undefined` clears all metadata. */ + metadata?: Record; +} + /** Kind of filesystem object as addressed by an {@link ExecutionEnv}. Symlinks are not followed automatically. */ export type FileKind = "file" | "directory" | "symlink"; @@ -298,20 +325,6 @@ export type PendingSessionWrite = SessionTreeEntry extends infer TEntry : never : never; -export interface AgentHarnessTurnState< - TSkill extends Skill = Skill, - TPromptTemplate extends PromptTemplate = PromptTemplate, - TTool extends AgentTool = AgentTool, -> { - messages: AgentMessage[]; - resources: AgentHarnessResources; - systemPrompt: string; - model: Model; - thinkingLevel: ThinkingLevel; - tools: TTool[]; - activeTools: TTool[]; -} - export interface QueueUpdateEvent { type: "queue_update"; steer: AgentMessage[]; @@ -353,6 +366,14 @@ export interface ContextEvent { export interface BeforeProviderRequestEvent { type: "before_provider_request"; + model: Model; + sessionId: string; + streamOptions: AgentHarnessStreamOptions; +} + +export interface BeforeProviderPayloadEvent { + type: "before_provider_payload"; + model: Model; payload: unknown; } @@ -440,6 +461,7 @@ export type AgentHarnessOwnEvent< | BeforeAgentStartEvent | ContextEvent | BeforeProviderRequestEvent + | BeforeProviderPayloadEvent | AfterProviderResponseEvent | ToolCallEvent | ToolResultEvent @@ -465,6 +487,10 @@ export interface ContextResult { } export interface BeforeProviderRequestResult { + streamOptions?: AgentHarnessStreamOptionsPatch; +} + +export interface BeforeProviderPayloadResult { payload: unknown; } @@ -497,6 +523,7 @@ export type AgentHarnessEventResultMap = { before_agent_start: BeforeAgentStartResult | undefined; context: ContextResult | undefined; before_provider_request: BeforeProviderRequestResult | undefined; + before_provider_payload: BeforeProviderPayloadResult | undefined; after_provider_response: undefined; tool_call: ToolCallResult | undefined; tool_result: ToolResultPatch | undefined; @@ -613,6 +640,8 @@ export interface AgentHarnessOptions< getApiKeyAndHeaders?: ( model: Model, ) => Promise<{ apiKey: string; headers?: Record } | undefined>; + /** Curated stream/provider request options. Snapshotted at turn start. */ + streamOptions?: AgentHarnessStreamOptions; model: Model; thinkingLevel?: ThinkingLevel; activeToolNames?: string[]; From f348a06294f12792858c083dac2bcc462988a3d3 Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Sun, 10 May 2026 19:10:54 +0200 Subject: [PATCH 009/166] test(agent): cover harness stream configuration --- packages/agent/docs/agent-harness.md | 55 ++++- .../test/harness/agent-harness-stream.test.ts | 206 ++++++++++++++++++ 2 files changed, 251 insertions(+), 10 deletions(-) create mode 100644 packages/agent/test/harness/agent-harness-stream.test.ts diff --git a/packages/agent/docs/agent-harness.md b/packages/agent/docs/agent-harness.md index b3a57377..5baccdff 100644 --- a/packages/agent/docs/agent-harness.md +++ b/packages/agent/docs/agent-harness.md @@ -193,11 +193,46 @@ Branch summary generation is part of the tree navigation operation. Auto-compaction and retry decision points are not implemented in `AgentHarness` yet. +## Test organization + +Harness tests should stay focused by area instead of growing one large catch-all file. + +Current structure: + +- `packages/agent/test/harness/agent-harness.test.ts`: basic construction/API smoke tests. +- `packages/agent/test/harness/agent-harness-stream.test.ts`: stream options and provider hook semantics. + +Preferred future structure: + +- `agent-harness-resources.test.ts`: resource snapshot/loading semantics. +- `agent-harness-tools.test.ts`: tool registry getters, active-tool semantics, and update events. +- `agent-harness-lifecycle.test.ts`: phase/save-point/settled/reentrancy behavior. + +Use the `pi-ai` faux provider (`registerFauxProvider`, `fauxAssistantMessage`) for deterministic harness/provider tests. Faux response factories can inspect `StreamOptions`, invoke `options.onPayload`, and return scripted assistant messages without real provider APIs or network access. + ## Implementation todo This list tracks the remaining work before treating `AgentHarness` as migration-ready. -### 1. Finish curated provider/stream configuration +### 1. Remove `Agent` dependency from `AgentHarness` + +New top priority. + +`AgentHarness` should likely call `agentLoop` / `agentLoopContinue` directly instead of owning an internal `Agent` instance. The harness already owns session persistence, runtime config snapshots, queues, provider stream configuration, hooks/events, phase semantics, and abort semantics. Keeping `Agent` in the middle creates duplicated state and adapter seams. + +Still needed: + +- Replace internal `new Agent(...)` with direct low-level loop calls. +- Move active run/abort-controller lifecycle into `AgentHarness`. +- Move queue draining into `AgentHarness` only, removing duplicated low-level `Agent` queues. +- Reduce low-level `AgentEvent` state directly in the harness where needed. +- Preserve current public behavior for `prompt`, `skill`, `promptFromTemplate`, `steer`, `followUp`, `nextTurn`, `abort`, and `waitForIdle`. +- Preserve provider hook behavior implemented by the harness stream wrapper. +- Preserve save-point snapshot refresh semantics without side-effecting through `Agent.prepareNextTurn`. +- Decide whether `AgentHarness.agent` remains temporarily for compatibility or is removed before migration. +- Add tests covering parity with the current harness behavior before and after the refactor. + +### 2. Finish curated provider/stream configuration Implemented so far: @@ -226,12 +261,12 @@ Implemented provider hook behavior: 3. `before_provider_request` patches, in hook registration order - `before_provider_request` does not patch `reasoning`; add that only if a concrete use case appears. -Still needed: +Implemented validation: -- Add tests proving stream options are snapshotted per turn and changes while busy affect only future provider requests/save-point snapshots. -- Add tests proving provider hook patch/deletion/chaining semantics. +- `packages/agent/test/harness/agent-harness-stream.test.ts` uses the `pi-ai` faux provider. +- Tests cover stream option forwarding, auth header merge, request hook patching, request hook deletion semantics, request hook chaining, payload hook chaining, and busy/save-point snapshot behavior. -### 2. Design per-`AgentHarness` model registry +### 3. Design per-`AgentHarness` model registry Not started. @@ -243,7 +278,7 @@ Still needed: - Define model change semantics during active turns and save points. - Preserve current `setModel()` behavior until the registry model is designed. -### 3. Design generic hook/event extension mechanism +### 4. Design generic hook/event extension mechanism Current cleanup already done: @@ -266,7 +301,7 @@ Still needed: - possibly `tool_result`: each hook receives the result fields produced by previous hooks. - Do not chain hooks where semantics are policy-based or ambiguous until explicitly designed, such as `tool_call`, `session_before_compact`, `session_before_tree`, and `before_agent_start`. -### 4. Add explicit tool registry read/update semantics +### 5. Add explicit tool registry read/update semantics Implemented so far: @@ -286,7 +321,7 @@ Still needed: - Decide and implement tool update observability events. - Include active-tool-only updates in the uniform runtime config observability plan. -### 5. Full `AgentHarness` lifecycle/state pass +### 6. Full `AgentHarness` lifecycle/state pass Implemented so far: @@ -320,7 +355,7 @@ Still needed: - Document or change timing for model/thinking/stream-option events that may fire before queued session entries persist while busy. - Audit `abort()` barrier semantics. -### 6. Later coding-agent migration plan +### 7. Later coding-agent migration plan Not started. @@ -332,7 +367,7 @@ Still needed: - Preserve UI/session behavior outside core. - Move coding-agent stream/auth/retry/header behavior onto the harness stream configuration and provider hooks. -### 7. Final lifecycle hardening suite +### 8. Final lifecycle hardening suite Before treating `AgentHarness` as migration-ready, add a broad test suite that exercises listeners and hooks closing over the harness and calling public APIs during every relevant event. diff --git a/packages/agent/test/harness/agent-harness-stream.test.ts b/packages/agent/test/harness/agent-harness-stream.test.ts new file mode 100644 index 00000000..e014b377 --- /dev/null +++ b/packages/agent/test/harness/agent-harness-stream.test.ts @@ -0,0 +1,206 @@ +import { fauxAssistantMessage, fauxToolCall, registerFauxProvider, type StreamOptions } from "@earendil-works/pi-ai"; +import { afterEach, describe, expect, it } from "vitest"; +import { AgentHarness } from "../../src/harness/agent-harness.js"; +import { NodeExecutionEnv } from "../../src/harness/execution-env.js"; +import { Session } from "../../src/harness/session/session.js"; +import { InMemorySessionStorage } from "../../src/harness/session/storage/memory.js"; +import { calculateTool } from "../utils/calculate.js"; + +const registrations: Array<{ unregister(): void }> = []; + +afterEach(() => { + for (const registration of registrations.splice(0)) { + registration.unregister(); + } +}); + +function createHarness(options: ConstructorParameters[0]): AgentHarness { + return new AgentHarness(options); +} + +function captureOptions(options: StreamOptions | undefined): StreamOptions { + return { + ...options, + headers: options?.headers ? { ...options.headers } : undefined, + metadata: options?.metadata ? { ...options.metadata } : undefined, + }; +} + +describe("AgentHarness stream configuration", () => { + it("snapshots stream options and merges auth headers before provider request hooks", async () => { + let capturedOptions: StreamOptions | undefined; + const registration = registerFauxProvider(); + registrations.push(registration); + registration.setResponses([ + (_context, options) => { + capturedOptions = options; + return fauxAssistantMessage("ok"); + }, + ]); + + const session = new Session(new InMemorySessionStorage({ metadata: { id: "session-1", createdAt: "now" } })); + const harness = createHarness({ + env: new NodeExecutionEnv({ cwd: process.cwd() }), + session, + model: registration.getModel(), + streamOptions: { + timeoutMs: 1000, + maxRetries: 2, + maxRetryDelayMs: 3000, + headers: { "x-base": "base" }, + metadata: { base: true }, + cacheRetention: "none", + }, + getApiKeyAndHeaders: async () => ({ apiKey: "secret", headers: { "x-auth": "auth" } }), + }); + + harness.on("before_provider_request", (event) => { + expect(event.sessionId).toBe("session-1"); + expect(event.streamOptions.headers).toEqual({ "x-base": "base", "x-auth": "auth" }); + return { + streamOptions: { + headers: { "x-hook": "hook" }, + metadata: { hook: true }, + }, + }; + }); + + await harness.prompt("hello"); + + expect(capturedOptions).toMatchObject({ + apiKey: "secret", + timeoutMs: 1000, + maxRetries: 2, + maxRetryDelayMs: 3000, + sessionId: "session-1", + cacheRetention: "none", + }); + expect(capturedOptions?.headers).toEqual({ "x-base": "base", "x-auth": "auth", "x-hook": "hook" }); + expect(capturedOptions?.metadata).toEqual({ base: true, hook: true }); + }); + + it("chains provider request patches and supports deletion semantics", async () => { + let capturedOptions: StreamOptions | undefined; + const registration = registerFauxProvider(); + registrations.push(registration); + registration.setResponses([ + (_context, options) => { + capturedOptions = options; + return fauxAssistantMessage("ok"); + }, + ]); + + const harness = createHarness({ + env: new NodeExecutionEnv({ cwd: process.cwd() }), + session: new Session(new InMemorySessionStorage()), + model: registration.getModel(), + streamOptions: { + timeoutMs: 1000, + maxRetries: 2, + headers: { keep: "base", remove: "base" }, + metadata: { keep: "base", remove: "base" }, + }, + }); + + harness.on("before_provider_request", (event) => { + expect(event.streamOptions.headers).toEqual({ keep: "base", remove: "base" }); + return { + streamOptions: { + headers: { first: "1", remove: undefined }, + metadata: { first: 1, remove: undefined }, + }, + }; + }); + harness.on("before_provider_request", (event) => { + expect(event.streamOptions.headers).toEqual({ keep: "base", first: "1" }); + expect(event.streamOptions.metadata).toEqual({ keep: "base", first: 1 }); + return { + streamOptions: { + timeoutMs: undefined, + headers: { second: "2" }, + metadata: undefined, + }, + }; + }); + + await harness.prompt("hello"); + + expect(capturedOptions?.timeoutMs).toBeUndefined(); + expect(capturedOptions?.maxRetries).toBe(2); + expect(capturedOptions?.headers).toEqual({ keep: "base", first: "1", second: "2" }); + expect(capturedOptions?.metadata).toBeUndefined(); + }); + + it("uses updated stream options for save-point snapshots without mutating the active request", async () => { + const capturedOptions: StreamOptions[] = []; + const registration = registerFauxProvider(); + registrations.push(registration); + registration.setResponses([ + (_context, options) => { + capturedOptions.push(captureOptions(options)); + return fauxAssistantMessage(fauxToolCall("calculate", { expression: "1 + 1" }, { id: "call-1" }), { + stopReason: "toolUse", + }); + }, + (_context, options) => { + capturedOptions.push(captureOptions(options)); + return fauxAssistantMessage("done"); + }, + ]); + + const harness = createHarness({ + env: new NodeExecutionEnv({ cwd: process.cwd() }), + session: new Session(new InMemorySessionStorage()), + model: registration.getModel(), + tools: [calculateTool], + streamOptions: { timeoutMs: 1000, headers: { turn: "first" } }, + }); + + harness.subscribe((event) => { + if (event.type === "tool_execution_start") { + harness.setStreamOptions({ timeoutMs: 2000, headers: { turn: "second" } }); + } + }); + + await harness.prompt("hello"); + + expect(capturedOptions).toHaveLength(2); + expect(capturedOptions[0].timeoutMs).toBe(1000); + expect(capturedOptions[0].headers).toEqual({ turn: "first" }); + expect(capturedOptions[1].timeoutMs).toBe(2000); + expect(capturedOptions[1].headers).toEqual({ turn: "second" }); + }); + + it("chains provider payload hooks", async () => { + const seenPayloads: unknown[] = []; + let finalPayload: unknown; + const registration = registerFauxProvider(); + registrations.push(registration); + registration.setResponses([ + async (_context, options, _state, model) => { + finalPayload = await options?.onPayload?.({ steps: ["provider"] }, model); + return fauxAssistantMessage("ok"); + }, + ]); + + const harness = createHarness({ + env: new NodeExecutionEnv({ cwd: process.cwd() }), + session: new Session(new InMemorySessionStorage()), + model: registration.getModel(), + }); + + harness.on("before_provider_payload", (event) => { + seenPayloads.push(event.payload); + return { payload: { steps: ["provider", "first"] } }; + }); + harness.on("before_provider_payload", (event) => { + seenPayloads.push(event.payload); + return { payload: { steps: ["provider", "first", "second"] } }; + }); + + await harness.prompt("hello"); + + expect(seenPayloads).toEqual([{ steps: ["provider"] }, { steps: ["provider", "first"] }]); + expect(finalPayload).toEqual({ steps: ["provider", "first", "second"] }); + }); +}); From 863341fbf5e80afa0c12428cf3933f30b255ff7d Mon Sep 17 00:00:00 2001 From: Sviatoslav Abakumov Date: Sun, 10 May 2026 22:14:43 +0400 Subject: [PATCH 010/166] fix(tui): render the checkboxes in the to-do list items --- packages/tui/src/components/markdown.ts | 6 ++++-- packages/tui/test/markdown.test.ts | 8 ++++++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/packages/tui/src/components/markdown.ts b/packages/tui/src/components/markdown.ts index 29d4ff13..0f8ffdfe 100644 --- a/packages/tui/src/components/markdown.ts +++ b/packages/tui/src/components/markdown.ts @@ -556,8 +556,10 @@ export class Markdown implements Component { for (let i = 0; i < token.items.length; i++) { const item = token.items[i]; const bullet = token.ordered ? `${startNumber + i}. ` : "- "; - const firstPrefix = indent + this.theme.listBullet(bullet); - const continuationPrefix = indent + " ".repeat(visibleWidth(bullet)); + const taskMarker = item.task ? `[${item.checked ? "x" : " "}] ` : ""; + const marker = bullet + taskMarker; + const firstPrefix = indent + this.theme.listBullet(marker); + const continuationPrefix = indent + " ".repeat(visibleWidth(marker)); const itemWidth = Math.max(1, width - visibleWidth(firstPrefix)); let renderedAnyLine = false; diff --git a/packages/tui/test/markdown.test.ts b/packages/tui/test/markdown.test.ts index 9c90eff9..4ba33dca 100644 --- a/packages/tui/test/markdown.test.ts +++ b/packages/tui/test/markdown.test.ts @@ -124,6 +124,14 @@ describe("Markdown component", () => { assert.ok(plainLines.some((line) => line.includes("2. Second ordered"))); }); + it("should render task list markers", () => { + const markdown = new Markdown("- [ ] beep\n- [x] boop", 0, 0, defaultMarkdownTheme); + + const lines = markdown.render(80).map((line) => stripAnsi(line).trimEnd()); + + assert.deepStrictEqual(lines, ["- [ ] beep", "- [x] boop"]); + }); + it("should maintain numbering when code blocks are not indented (LLM output)", () => { // When code blocks aren't indented, marked parses each item as a separate list. // We use token.start to preserve the original numbering. From c3ce1d33d4c5edf4c70c8a1f0839b1faca25cf6f Mon Sep 17 00:00:00 2001 From: Maximilian <85677120+maximilianzuern@users.noreply.github.com> Date: Sun, 10 May 2026 23:32:15 +0200 Subject: [PATCH 011/166] fix tool config in example in sdk.ts --- packages/coding-agent/src/core/sdk.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/coding-agent/src/core/sdk.ts b/packages/coding-agent/src/core/sdk.ts index 8bc96072..3e0533d0 100644 --- a/packages/coding-agent/src/core/sdk.ts +++ b/packages/coding-agent/src/core/sdk.ts @@ -184,7 +184,7 @@ function getAttributionHeaders( * await loader.reload(); * const { session } = await createAgentSession({ * model: myModel, - * tools: [readTool, bashTool], + * tools: ["read", "bash"], * resourceLoader: loader, * sessionManager: SessionManager.inMemory(), * }); From 4eadc8fd3e01395dfd8b9392de473ebecb81b9c8 Mon Sep 17 00:00:00 2001 From: Maximilian <85677120+maximilianzuern@users.noreply.github.com> Date: Sun, 10 May 2026 23:34:28 +0200 Subject: [PATCH 012/166] fix tool config in example in SDK README.md --- packages/coding-agent/examples/sdk/README.md | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/packages/coding-agent/examples/sdk/README.md b/packages/coding-agent/examples/sdk/README.md index 0fd95cf0..f9d14966 100644 --- a/packages/coding-agent/examples/sdk/README.md +++ b/packages/coding-agent/examples/sdk/README.md @@ -40,9 +40,6 @@ import { ModelRegistry, SessionManager, SettingsManager, - codingTools, - readOnlyTools, - readTool, bashTool, editTool, writeTool, } from "@earendil-works/pi-coding-agent"; // Auth and models setup @@ -64,7 +61,7 @@ await loader.reload(); const { session } = await createAgentSession({ resourceLoader: loader, authStorage, modelRegistry }); // Read-only -const { session } = await createAgentSession({ tools: readOnlyTools, authStorage, modelRegistry }); +const { session } = await createAgentSession({ tools: ["read", "grep", "find", "ls"], authStorage, modelRegistry }); // In-memory const { session } = await createAgentSession({ @@ -92,8 +89,8 @@ const { session } = await createAgentSession({ authStorage: customAuth, modelRegistry: customRegistry, resourceLoader, - tools: [readTool, bashTool], - customTools: [{ tool: myTool }], + tools: ["read", "bash", "my_tool"], + customTools: [myTool], sessionManager: SessionManager.inMemory(), settingsManager: SettingsManager.inMemory(), }); @@ -117,7 +114,7 @@ await session.prompt("Hello"); | `agentDir` | `~/.pi/agent` | Config directory | | `model` | From settings/first available | Model to use | | `thinkingLevel` | From settings/"off" | off, low, medium, high | -| `tools` | `codingTools` | Built-in tools | +| `tools` | `["read", "grep", "find", "ls", "bash", "edit", "write"]` | Built-in tools | | `customTools` | `[]` | Additional tool definitions | | `resourceLoader` | DefaultResourceLoader | Resource loader for extensions, skills, prompts, themes | | `sessionManager` | `SessionManager.create(cwd)` | Persistence | From df72d5eefe92f533b582357d67f7c3ddd28f6c45 Mon Sep 17 00:00:00 2001 From: Maximilian <85677120+maximilianzuern@users.noreply.github.com> Date: Mon, 11 May 2026 00:16:46 +0200 Subject: [PATCH 013/166] fix inconsistency in README --- packages/coding-agent/examples/sdk/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/coding-agent/examples/sdk/README.md b/packages/coding-agent/examples/sdk/README.md index f9d14966..a8011aae 100644 --- a/packages/coding-agent/examples/sdk/README.md +++ b/packages/coding-agent/examples/sdk/README.md @@ -12,7 +12,7 @@ The runtime example shows how to build a recreate function that closes over proc | `02-custom-model.ts` | Select model and thinking level | | `03-custom-prompt.ts` | Replace or modify system prompt | | `04-skills.ts` | Discover, filter, or replace skills | -| `05-tools.ts` | Built-in tools, custom tools | +| `05-tools.ts` | Built-in tool allowlists | | `06-extensions.ts` | Logging, blocking, result modification | | `07-context-files.ts` | AGENTS.md context files | | `08-slash-commands.ts` | File-based slash commands | @@ -114,7 +114,7 @@ await session.prompt("Hello"); | `agentDir` | `~/.pi/agent` | Config directory | | `model` | From settings/first available | Model to use | | `thinkingLevel` | From settings/"off" | off, low, medium, high | -| `tools` | `["read", "grep", "find", "ls", "bash", "edit", "write"]` | Built-in tools | +| `tools` | `["read", "bash", "edit", "write"]` built-ins | Allowlist tool names across built-in, extension, and custom tools | | `customTools` | `[]` | Additional tool definitions | | `resourceLoader` | DefaultResourceLoader | Resource loader for extensions, skills, prompts, themes | | `sessionManager` | `SessionManager.create(cwd)` | Persistence | From 74739567b94d8361b5b280a48646a8356cfab965 Mon Sep 17 00:00:00 2001 From: Maximilian <85677120+maximilianzuern@users.noreply.github.com> Date: Mon, 11 May 2026 00:32:19 +0200 Subject: [PATCH 014/166] fix tool config in SDK docs --- packages/coding-agent/docs/sdk.md | 70 +++++++++++-------------------- 1 file changed, 25 insertions(+), 45 deletions(-) diff --git a/packages/coding-agent/docs/sdk.md b/packages/coding-agent/docs/sdk.md index 6db72279..cd90dbd6 100644 --- a/packages/coding-agent/docs/sdk.md +++ b/packages/coding-agent/docs/sdk.md @@ -54,7 +54,7 @@ The main factory function for a single `AgentSession`. `createAgentSession()` uses a `ResourceLoader` to supply extensions, skills, prompt templates, themes, and context files. If you do not provide one, it uses `DefaultResourceLoader` with standard discovery. ```typescript -import { createAgentSession } from "@earendil-works/pi-coding-agent"; +import { createAgentSession, SessionManager } from "@earendil-works/pi-coding-agent"; // Minimal: defaults with DefaultResourceLoader const { session } = await createAgentSession(); @@ -62,7 +62,7 @@ const { session } = await createAgentSession(); // Custom: override specific options const { session } = await createAgentSession({ model: myModel, - tools: [readTool, bashTool], + tools: ["read", "bash"], sessionManager: SessionManager.inMemory(), }); ``` @@ -466,64 +466,50 @@ const { session } = await createAgentSession({ resourceLoader: loader }); ### Tools -```typescript -import { - codingTools, // read, bash, edit, write (default) - readOnlyTools, // read, grep, find, ls - readTool, bashTool, editTool, writeTool, - grepTool, findTool, lsTool, -} from "@earendil-works/pi-coding-agent"; +Use tool names, not imported tool instances. -// Use built-in tool set +- Built-in tool names: `read`, `bash`, `edit`, `write`, `grep`, `find`, `ls` +- Default built-ins: `read`, `bash`, `edit`, `write` +- `noTools: "all"` disables all tools +- `noTools: "builtin"` disables default built-ins while keeping extension and custom tools enabled + +```typescript +import { createAgentSession } from "@earendil-works/pi-coding-agent"; + +// Read-only mode const { session } = await createAgentSession({ - tools: readOnlyTools, + tools: ["read", "grep", "find", "ls"], }); // Pick specific tools const { session } = await createAgentSession({ - tools: [readTool, bashTool, grepTool], + tools: ["read", "bash", "grep"], }); ``` #### Tools with Custom cwd -**Important:** The pre-built tool instances (`readTool`, `bashTool`, etc.) use `process.cwd()` for path resolution. When you specify a custom `cwd` AND provide explicit `tools`, you must use the tool factory functions to ensure paths resolve correctly: +When you pass a custom `cwd`, `createAgentSession()` builds selected built-in tools for that cwd. ```typescript -import { - createCodingTools, // Creates [read, bash, edit, write] for specific cwd - createReadOnlyTools, // Creates [read, grep, find, ls] for specific cwd - createReadTool, - createBashTool, - createEditTool, - createWriteTool, - createGrepTool, - createFindTool, - createLsTool, -} from "@earendil-works/pi-coding-agent"; +import { createAgentSession, SessionManager } from "@earendil-works/pi-coding-agent"; const cwd = "/path/to/project"; -// Use factory for tool sets +// Use default tools for custom cwd const { session } = await createAgentSession({ cwd, - tools: createCodingTools(cwd), // Tools resolve paths relative to cwd + sessionManager: SessionManager.inMemory(cwd), }); -// Or pick specific tools +// Or pick specific tools for custom cwd const { session } = await createAgentSession({ cwd, - tools: [createReadTool(cwd), createBashTool(cwd), createGrepTool(cwd)], + tools: ["read", "bash", "grep"], + sessionManager: SessionManager.inMemory(cwd), }); ``` -**When you don't need factories:** -- If you omit `tools`, pi automatically creates them with the correct `cwd` -- If you use `process.cwd()` as your `cwd`, the pre-built instances work fine - -**When you must use factories:** -- When you specify both `cwd` (different from `process.cwd()`) AND `tools` - > See [examples/sdk/05-tools.ts](../examples/sdk/05-tools.ts) ### Custom Tools @@ -556,6 +542,8 @@ Use `defineTool()` for standalone definitions and arrays like `customTools: [myT Custom tools passed via `customTools` are combined with extension-registered tools. Extensions loaded by the ResourceLoader can also register tools via `pi.registerTool()`. +If you pass `tools`, include each custom or extension tool name you want enabled, for example `tools: ["read", "bash", "my_tool"]`. + > See [examples/sdk/05-tools.ts](../examples/sdk/05-tools.ts) ### Extensions @@ -885,12 +873,10 @@ import { getModel } from "@earendil-works/pi-ai"; import { Type } from "typebox"; import { AuthStorage, - bashTool, createAgentSession, DefaultResourceLoader, defineTool, ModelRegistry, - readTool, SessionManager, SettingsManager, } from "@earendil-works/pi-coding-agent"; @@ -944,7 +930,7 @@ const { session } = await createAgentSession({ authStorage, modelRegistry, - tools: [readTool, bashTool], + tools: ["read", "bash", "status"], customTools: [statusTool], resourceLoader: loader, @@ -1123,13 +1109,7 @@ defineTool SessionManager SettingsManager -// Built-in tools (use process.cwd()) -codingTools -readOnlyTools -readTool, bashTool, editTool, writeTool -grepTool, findTool, lsTool - -// Tool factories (for custom cwd) +// Tool factories createCodingTools createReadOnlyTools createReadTool, createBashTool, createEditTool, createWriteTool From 68deecfb4f79766c9505a832abb90ae36cd123e0 Mon Sep 17 00:00:00 2001 From: Maximilian <85677120+maximilianzuern@users.noreply.github.com> Date: Mon, 11 May 2026 00:45:30 +0200 Subject: [PATCH 015/166] minor fix in body --- packages/coding-agent/docs/sdk.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/coding-agent/docs/sdk.md b/packages/coding-agent/docs/sdk.md index cd90dbd6..2fa85cd0 100644 --- a/packages/coding-agent/docs/sdk.md +++ b/packages/coding-agent/docs/sdk.md @@ -466,7 +466,7 @@ const { session } = await createAgentSession({ resourceLoader: loader }); ### Tools -Use tool names, not imported tool instances. +Specify which built-in tools to enable: - Built-in tool names: `read`, `bash`, `edit`, `write`, `grep`, `find`, `ls` - Default built-ins: `read`, `bash`, `edit`, `write` From d68011dab17f3a5e66b03ee1a1b82968ae9749f5 Mon Sep 17 00:00:00 2001 From: Michael Yu Date: Sun, 10 May 2026 23:40:12 +0800 Subject: [PATCH 016/166] fix(coding-agent): dispose SDK example sessions --- .../coding-agent/examples/sdk/01-minimal.ts | 24 ++++++----- .../examples/sdk/02-custom-model.ts | 18 +++++---- .../examples/sdk/03-custom-prompt.ts | 40 +++++++++++-------- .../coding-agent/examples/sdk/04-skills.ts | 4 +- .../coding-agent/examples/sdk/05-tools.ts | 12 ++++-- .../examples/sdk/06-extensions.ts | 18 +++++---- .../examples/sdk/07-context-files.ts | 4 +- .../examples/sdk/08-prompt-templates.ts | 4 +- .../examples/sdk/09-api-keys-and-oauth.ts | 12 ++++-- .../coding-agent/examples/sdk/10-settings.ts | 8 ++-- .../coding-agent/examples/sdk/11-sessions.ts | 4 ++ .../examples/sdk/12-full-control.ts | 18 +++++---- 12 files changed, 101 insertions(+), 65 deletions(-) diff --git a/packages/coding-agent/examples/sdk/01-minimal.ts b/packages/coding-agent/examples/sdk/01-minimal.ts index be35bab2..a01f9c1a 100644 --- a/packages/coding-agent/examples/sdk/01-minimal.ts +++ b/packages/coding-agent/examples/sdk/01-minimal.ts @@ -9,14 +9,18 @@ import { createAgentSession } from "@earendil-works/pi-coding-agent"; const { session } = await createAgentSession(); -session.subscribe((event) => { - if (event.type === "message_update" && event.assistantMessageEvent.type === "text_delta") { - process.stdout.write(event.assistantMessageEvent.delta); - } -}); +try { + session.subscribe((event) => { + if (event.type === "message_update" && event.assistantMessageEvent.type === "text_delta") { + process.stdout.write(event.assistantMessageEvent.delta); + } + }); -await session.prompt("What files are in the current directory?"); -session.state.messages.forEach((msg) => { - console.log(msg); -}); -console.log(); + await session.prompt("What files are in the current directory?"); + session.state.messages.forEach((msg) => { + console.log(msg); + }); + console.log(); +} finally { + session.dispose(); +} diff --git a/packages/coding-agent/examples/sdk/02-custom-model.ts b/packages/coding-agent/examples/sdk/02-custom-model.ts index ff1b4287..531a51c8 100644 --- a/packages/coding-agent/examples/sdk/02-custom-model.ts +++ b/packages/coding-agent/examples/sdk/02-custom-model.ts @@ -38,12 +38,16 @@ if (available.length > 0) { modelRegistry, }); - session.subscribe((event) => { - if (event.type === "message_update" && event.assistantMessageEvent.type === "text_delta") { - process.stdout.write(event.assistantMessageEvent.delta); - } - }); + try { + session.subscribe((event) => { + if (event.type === "message_update" && event.assistantMessageEvent.type === "text_delta") { + process.stdout.write(event.assistantMessageEvent.delta); + } + }); - await session.prompt("Say hello in one sentence."); - console.log(); + await session.prompt("Say hello in one sentence."); + console.log(); + } finally { + session.dispose(); + } } diff --git a/packages/coding-agent/examples/sdk/03-custom-prompt.ts b/packages/coding-agent/examples/sdk/03-custom-prompt.ts index 7c3ea5ff..ad1fdaa1 100644 --- a/packages/coding-agent/examples/sdk/03-custom-prompt.ts +++ b/packages/coding-agent/examples/sdk/03-custom-prompt.ts @@ -30,15 +30,19 @@ const { session: session1 } = await createAgentSession({ sessionManager: SessionManager.inMemory(), }); -session1.subscribe((event) => { - if (event.type === "message_update" && event.assistantMessageEvent.type === "text_delta") { - process.stdout.write(event.assistantMessageEvent.delta); - } -}); +try { + session1.subscribe((event) => { + if (event.type === "message_update" && event.assistantMessageEvent.type === "text_delta") { + process.stdout.write(event.assistantMessageEvent.delta); + } + }); -console.log("=== Replace prompt ==="); -await session1.prompt("What is 2 + 2?"); -console.log("\n"); + console.log("=== Replace prompt ==="); + await session1.prompt("What is 2 + 2?"); + console.log("\n"); +} finally { + session1.dispose(); +} // Option 2: Append instructions to the default prompt const loader2 = new DefaultResourceLoader({ @@ -56,12 +60,16 @@ const { session: session2 } = await createAgentSession({ sessionManager: SessionManager.inMemory(), }); -session2.subscribe((event) => { - if (event.type === "message_update" && event.assistantMessageEvent.type === "text_delta") { - process.stdout.write(event.assistantMessageEvent.delta); - } -}); +try { + session2.subscribe((event) => { + if (event.type === "message_update" && event.assistantMessageEvent.type === "text_delta") { + process.stdout.write(event.assistantMessageEvent.delta); + } + }); -console.log("=== Modify prompt ==="); -await session2.prompt("List 3 benefits of TypeScript."); -console.log(); + console.log("=== Modify prompt ==="); + await session2.prompt("List 3 benefits of TypeScript."); + console.log(); +} finally { + session2.dispose(); +} diff --git a/packages/coding-agent/examples/sdk/04-skills.ts b/packages/coding-agent/examples/sdk/04-skills.ts index 0aadc19e..3c0887cb 100644 --- a/packages/coding-agent/examples/sdk/04-skills.ts +++ b/packages/coding-agent/examples/sdk/04-skills.ts @@ -47,9 +47,9 @@ if (diagnostics.length > 0) { console.log("Warnings:", diagnostics); } -await createAgentSession({ +const { session } = await createAgentSession({ resourceLoader: loader, sessionManager: SessionManager.inMemory(), }); - console.log("Session created with filtered skills"); +session.dispose(); diff --git a/packages/coding-agent/examples/sdk/05-tools.ts b/packages/coding-agent/examples/sdk/05-tools.ts index 4a2e49ce..5f6f710d 100644 --- a/packages/coding-agent/examples/sdk/05-tools.ts +++ b/packages/coding-agent/examples/sdk/05-tools.ts @@ -13,32 +13,36 @@ import { createAgentSession, SessionManager } from "@earendil-works/pi-coding-agent"; // Read-only mode (no edit/write) -await createAgentSession({ +const { session: readOnlySession } = await createAgentSession({ tools: ["read", "grep", "find", "ls"], sessionManager: SessionManager.inMemory(), }); console.log("Read-only session created"); +readOnlySession.dispose(); // Custom tool selection -await createAgentSession({ +const { session: customToolsSession } = await createAgentSession({ tools: ["read", "bash", "grep"], sessionManager: SessionManager.inMemory(), }); console.log("Custom tools session created"); +customToolsSession.dispose(); // With custom cwd const customCwd = "/path/to/project"; -await createAgentSession({ +const { session: customCwdSession } = await createAgentSession({ cwd: customCwd, tools: ["read", "bash", "edit", "write"], sessionManager: SessionManager.inMemory(customCwd), }); console.log("Custom cwd session created"); +customCwdSession.dispose(); // Or pick specific tools for custom cwd -await createAgentSession({ +const { session: specificToolsSession } = await createAgentSession({ cwd: customCwd, tools: ["read", "bash", "grep"], sessionManager: SessionManager.inMemory(customCwd), }); console.log("Specific tools with custom cwd session created"); +specificToolsSession.dispose(); diff --git a/packages/coding-agent/examples/sdk/06-extensions.ts b/packages/coding-agent/examples/sdk/06-extensions.ts index 8e55a80a..6a8e6a11 100644 --- a/packages/coding-agent/examples/sdk/06-extensions.ts +++ b/packages/coding-agent/examples/sdk/06-extensions.ts @@ -42,14 +42,18 @@ const { session } = await createAgentSession({ sessionManager: SessionManager.inMemory(), }); -session.subscribe((event) => { - if (event.type === "message_update" && event.assistantMessageEvent.type === "text_delta") { - process.stdout.write(event.assistantMessageEvent.delta); - } -}); +try { + session.subscribe((event) => { + if (event.type === "message_update" && event.assistantMessageEvent.type === "text_delta") { + process.stdout.write(event.assistantMessageEvent.delta); + } + }); -await session.prompt("List files in the current directory."); -console.log(); + await session.prompt("List files in the current directory."); + console.log(); +} finally { + session.dispose(); +} // Example extension file (./my-logging-extension.ts): /* diff --git a/packages/coding-agent/examples/sdk/07-context-files.ts b/packages/coding-agent/examples/sdk/07-context-files.ts index c8ce77a1..c97b4887 100644 --- a/packages/coding-agent/examples/sdk/07-context-files.ts +++ b/packages/coding-agent/examples/sdk/07-context-files.ts @@ -39,9 +39,9 @@ for (const file of discovered) { console.log(` - ${file.path} (${file.content.length} chars)`); } -await createAgentSession({ +const { session } = await createAgentSession({ resourceLoader: loader, sessionManager: SessionManager.inMemory(), }); - console.log(`Session created with ${discovered.length + 1} context files`); +session.dispose(); diff --git a/packages/coding-agent/examples/sdk/08-prompt-templates.ts b/packages/coding-agent/examples/sdk/08-prompt-templates.ts index 814e3230..b52de6ca 100644 --- a/packages/coding-agent/examples/sdk/08-prompt-templates.ts +++ b/packages/coding-agent/examples/sdk/08-prompt-templates.ts @@ -43,9 +43,9 @@ for (const template of discovered) { console.log(` /${template.name}: ${template.description}`); } -await createAgentSession({ +const { session } = await createAgentSession({ resourceLoader: loader, sessionManager: SessionManager.inMemory(), }); - console.log(`Session created with ${discovered.length + 1} prompt templates`); +session.dispose(); diff --git a/packages/coding-agent/examples/sdk/09-api-keys-and-oauth.ts b/packages/coding-agent/examples/sdk/09-api-keys-and-oauth.ts index 127e5933..120662e8 100644 --- a/packages/coding-agent/examples/sdk/09-api-keys-and-oauth.ts +++ b/packages/coding-agent/examples/sdk/09-api-keys-and-oauth.ts @@ -11,38 +11,42 @@ import { AuthStorage, createAgentSession, ModelRegistry, SessionManager } from " const authStorage = AuthStorage.create(); const modelRegistry = ModelRegistry.create(authStorage); -await createAgentSession({ +const { session: defaultAuthSession } = await createAgentSession({ sessionManager: SessionManager.inMemory(), authStorage, modelRegistry, }); console.log("Session with default auth storage and model registry"); +defaultAuthSession.dispose(); // Custom auth storage location const customAuthStorage = AuthStorage.create("/tmp/my-app/auth.json"); const customModelRegistry = ModelRegistry.create(customAuthStorage, "/tmp/my-app/models.json"); -await createAgentSession({ +const { session: customAuthSession } = await createAgentSession({ sessionManager: SessionManager.inMemory(), authStorage: customAuthStorage, modelRegistry: customModelRegistry, }); console.log("Session with custom auth storage location"); +customAuthSession.dispose(); // Runtime API key override (not persisted to disk) authStorage.setRuntimeApiKey("anthropic", "sk-my-temp-key"); -await createAgentSession({ +const { session: runtimeKeySession } = await createAgentSession({ sessionManager: SessionManager.inMemory(), authStorage, modelRegistry, }); console.log("Session with runtime API key override"); +runtimeKeySession.dispose(); // No models.json - only built-in models const simpleRegistry = ModelRegistry.inMemory(authStorage); -await createAgentSession({ +const { session: builtInModelsSession } = await createAgentSession({ sessionManager: SessionManager.inMemory(), authStorage, modelRegistry: simpleRegistry, }); console.log("Session with only built-in models"); +builtInModelsSession.dispose(); diff --git a/packages/coding-agent/examples/sdk/10-settings.ts b/packages/coding-agent/examples/sdk/10-settings.ts index 9b6f93d8..93f7baf9 100644 --- a/packages/coding-agent/examples/sdk/10-settings.ts +++ b/packages/coding-agent/examples/sdk/10-settings.ts @@ -19,12 +19,12 @@ settingsManager.applyOverrides({ retry: { enabled: true, maxRetries: 5, baseDelayMs: 1000 }, }); -await createAgentSession({ +const { session: customSettingsSession } = await createAgentSession({ settingsManager, sessionManager: SessionManager.inMemory(), }); - console.log("Session created with custom settings"); +customSettingsSession.dispose(); // Setters update memory immediately and queue persistence writes. // Call flush() when you need a durability boundary. @@ -45,9 +45,9 @@ const inMemorySettings = SettingsManager.inMemory({ retry: { enabled: false }, }); -await createAgentSession({ +const { session: testSession } = await createAgentSession({ settingsManager: inMemorySettings, sessionManager: SessionManager.inMemory(), }); - console.log("Test session created with in-memory settings"); +testSession.dispose(); diff --git a/packages/coding-agent/examples/sdk/11-sessions.ts b/packages/coding-agent/examples/sdk/11-sessions.ts index 414db8b6..975a6bb1 100644 --- a/packages/coding-agent/examples/sdk/11-sessions.ts +++ b/packages/coding-agent/examples/sdk/11-sessions.ts @@ -11,12 +11,14 @@ const { session: inMemory } = await createAgentSession({ sessionManager: SessionManager.inMemory(), }); console.log("In-memory session:", inMemory.sessionFile ?? "(none)"); +inMemory.dispose(); // New persistent session const { session: newSession } = await createAgentSession({ sessionManager: SessionManager.create(process.cwd()), }); console.log("New session file:", newSession.sessionFile); +newSession.dispose(); // Continue most recent session (or create new if none) const { session: continued, modelFallbackMessage } = await createAgentSession({ @@ -24,6 +26,7 @@ const { session: continued, modelFallbackMessage } = await createAgentSession({ }); if (modelFallbackMessage) console.log("Note:", modelFallbackMessage); console.log("Continued session:", continued.sessionFile); +continued.dispose(); // List and open specific session const sessions = await SessionManager.list(process.cwd()); @@ -37,6 +40,7 @@ if (sessions.length > 0) { sessionManager: SessionManager.open(sessions[0].path), }); console.log(`\nOpened: ${opened.sessionId}`); + opened.dispose(); } // Custom session directory (no cwd encoding) diff --git a/packages/coding-agent/examples/sdk/12-full-control.ts b/packages/coding-agent/examples/sdk/12-full-control.ts index 4bcc0726..cd8ba343 100644 --- a/packages/coding-agent/examples/sdk/12-full-control.ts +++ b/packages/coding-agent/examples/sdk/12-full-control.ts @@ -63,11 +63,15 @@ const { session } = await createAgentSession({ settingsManager, }); -session.subscribe((event) => { - if (event.type === "message_update" && event.assistantMessageEvent.type === "text_delta") { - process.stdout.write(event.assistantMessageEvent.delta); - } -}); +try { + session.subscribe((event) => { + if (event.type === "message_update" && event.assistantMessageEvent.type === "text_delta") { + process.stdout.write(event.assistantMessageEvent.delta); + } + }); -await session.prompt("List files in the current directory."); -console.log(); + await session.prompt("List files in the current directory."); + console.log(); +} finally { + session.dispose(); +} From 3d9e14d7482f4a99d5224926099bec0d17ff86fd Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 11 May 2026 16:36:27 +0200 Subject: [PATCH 017/166] fix(compaction): clamp summary output tokens Fixes #4390. --- .../src/harness/compaction/compaction.ts | 10 ++++- .../agent/test/harness/compaction.test.ts | 37 ++++++++++++++++++- .../src/core/compaction/compaction.ts | 10 ++++- .../test/compaction-summary-reasoning.test.ts | 22 +++++++++-- 4 files changed, 70 insertions(+), 9 deletions(-) diff --git a/packages/agent/src/harness/compaction/compaction.ts b/packages/agent/src/harness/compaction/compaction.ts index 0776222b..298dc86d 100644 --- a/packages/agent/src/harness/compaction/compaction.ts +++ b/packages/agent/src/harness/compaction/compaction.ts @@ -545,7 +545,10 @@ export async function generateSummary( previousSummary?: string, thinkingLevel?: ThinkingLevel, ): Promise { - const maxTokens = Math.floor(0.8 * reserveTokens); + const maxTokens = Math.min( + Math.floor(0.8 * reserveTokens), + model.maxTokens > 0 ? model.maxTokens : Number.POSITIVE_INFINITY, + ); // Use update prompt if we have a previous summary, otherwise initial prompt let basePrompt = previousSummary ? UPDATE_SUMMARIZATION_PROMPT : SUMMARIZATION_PROMPT; @@ -817,7 +820,10 @@ async function generateTurnPrefixSummary( signal?: AbortSignal, thinkingLevel?: ThinkingLevel, ): Promise { - const maxTokens = Math.floor(0.5 * reserveTokens); // Smaller budget for turn prefix + const maxTokens = Math.min( + Math.floor(0.5 * reserveTokens), + model.maxTokens > 0 ? model.maxTokens : Number.POSITIVE_INFINITY, + ); // Smaller budget for turn prefix const llmMessages = convertToLlm(messages); const conversationText = serializeConversation(llmMessages); const promptText = `\n${conversationText}\n\n\n${TURN_PREFIX_SUMMARIZATION_PROMPT}`; diff --git a/packages/agent/test/harness/compaction.test.ts b/packages/agent/test/harness/compaction.test.ts index d73517f0..d39be48b 100644 --- a/packages/agent/test/harness/compaction.test.ts +++ b/packages/agent/test/harness/compaction.test.ts @@ -8,6 +8,7 @@ import { } from "@earendil-works/pi-ai"; import { afterEach, beforeEach, describe, expect, it } from "vitest"; import { + type CompactionPreparation, calculateContextTokens, compact, DEFAULT_COMPACTION_SETTINGS, @@ -113,14 +114,17 @@ function createModelChangeEntry(provider: string, modelId: string, parentId: str }; } -function createFauxModel(reasoning: boolean): { faux: FauxProviderRegistration; model: Model } { +function createFauxModel( + reasoning: boolean, + maxTokens = 8192, +): { faux: FauxProviderRegistration; model: Model } { const faux = registerFauxProvider({ models: [ { id: reasoning ? "reasoning-model" : "non-reasoning-model", reasoning, contextWindow: 200000, - maxTokens: 8192, + maxTokens, }, ], }); @@ -285,6 +289,35 @@ describe("harness compaction", () => { expect(seenOptions[2]).not.toHaveProperty("reasoning"); }); + it("clamps compaction summary maxTokens to the model output cap", async () => { + const messages: AgentMessage[] = [createUserMessage("Summarize this.")]; + const seenOptions: Array | undefined> = []; + const { faux, model } = createFauxModel(false, 128000); + faux.setResponses([ + (_context, options) => { + seenOptions.push(options as Record | undefined); + return fauxAssistantMessage("## Goal\nTest summary"); + }, + (_context, options) => { + seenOptions.push(options as Record | undefined); + return fauxAssistantMessage("## Goal\nTest summary"); + }, + ]); + const preparation: CompactionPreparation = { + firstKeptEntryId: "entry-keep", + messagesToSummarize: messages, + turnPrefixMessages: messages, + isSplitTurn: true, + tokensBefore: 600000, + fileOps: { read: new Set(), written: new Set(), edited: new Set() }, + settings: { enabled: true, reserveTokens: 500000, keepRecentTokens: 20000 }, + }; + + await compact(preparation, model, "test-key"); + + expect(seenOptions.map((options) => options?.maxTokens)).toEqual([128000, 128000]); + }); + it("returns a compaction result with file details", async () => { const u1 = createMessageEntry(createUserMessage("read a file")); const assistantMessage: AssistantMessage = { diff --git a/packages/coding-agent/src/core/compaction/compaction.ts b/packages/coding-agent/src/core/compaction/compaction.ts index c11c0254..5075e124 100644 --- a/packages/coding-agent/src/core/compaction/compaction.ts +++ b/packages/coding-agent/src/core/compaction/compaction.ts @@ -538,7 +538,10 @@ export async function generateSummary( previousSummary?: string, thinkingLevel?: ThinkingLevel, ): Promise { - const maxTokens = Math.floor(0.8 * reserveTokens); + const maxTokens = Math.min( + Math.floor(0.8 * reserveTokens), + model.maxTokens > 0 ? model.maxTokens : Number.POSITIVE_INFINITY, + ); // Use update prompt if we have a previous summary, otherwise initial prompt let basePrompt = previousSummary ? UPDATE_SUMMARIZATION_PROMPT : SUMMARIZATION_PROMPT; @@ -808,7 +811,10 @@ async function generateTurnPrefixSummary( signal?: AbortSignal, thinkingLevel?: ThinkingLevel, ): Promise { - const maxTokens = Math.floor(0.5 * reserveTokens); // Smaller budget for turn prefix + const maxTokens = Math.min( + Math.floor(0.5 * reserveTokens), + model.maxTokens > 0 ? model.maxTokens : Number.POSITIVE_INFINITY, + ); // Smaller budget for turn prefix const llmMessages = convertToLlm(messages); const conversationText = serializeConversation(llmMessages); const promptText = `\n${conversationText}\n\n\n${TURN_PREFIX_SUMMARIZATION_PROMPT}`; diff --git a/packages/coding-agent/test/compaction-summary-reasoning.test.ts b/packages/coding-agent/test/compaction-summary-reasoning.test.ts index c674ef1a..ea52016b 100644 --- a/packages/coding-agent/test/compaction-summary-reasoning.test.ts +++ b/packages/coding-agent/test/compaction-summary-reasoning.test.ts @@ -1,7 +1,7 @@ import type { AgentMessage } from "@earendil-works/pi-agent-core"; import type { AssistantMessage, Model } from "@earendil-works/pi-ai"; import { beforeEach, describe, expect, it, vi } from "vitest"; -import { generateSummary } from "../src/core/compaction/index.js"; +import { type CompactionPreparation, compact, generateSummary } from "../src/core/compaction/index.js"; const { completeSimpleMock } = vi.hoisted(() => ({ completeSimpleMock: vi.fn(), @@ -15,7 +15,7 @@ vi.mock("@earendil-works/pi-ai", async (importOriginal) => { }; }); -function createModel(reasoning: boolean): Model<"anthropic-messages"> { +function createModel(reasoning: boolean, maxTokens = 8192): Model<"anthropic-messages"> { return { id: reasoning ? "reasoning-model" : "non-reasoning-model", name: reasoning ? "Reasoning Model" : "Non-reasoning Model", @@ -26,7 +26,7 @@ function createModel(reasoning: boolean): Model<"anthropic-messages"> { input: ["text"], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: 200000, - maxTokens: 8192, + maxTokens, }; } @@ -115,4 +115,20 @@ describe("generateSummary reasoning options", () => { }); expect(completeSimpleMock.mock.calls[0][2]).not.toHaveProperty("reasoning"); }); + + it("clamps compaction summary maxTokens to the model output cap", async () => { + const preparation: CompactionPreparation = { + firstKeptEntryId: "entry-keep", + messagesToSummarize: messages, + turnPrefixMessages: messages, + isSplitTurn: true, + tokensBefore: 600000, + fileOps: { read: new Set(), written: new Set(), edited: new Set() }, + settings: { enabled: true, reserveTokens: 500000, keepRecentTokens: 20000 }, + }; + + await compact(preparation, createModel(false, 128000), "test-key"); + + expect(completeSimpleMock.mock.calls.map((call) => call[2]?.maxTokens)).toEqual([128000, 128000]); + }); }); From 9d84e28692f9483eea5a950ca017174cb641867a Mon Sep 17 00:00:00 2001 From: Omair Ahmed Date: Mon, 11 May 2026 16:27:27 -0400 Subject: [PATCH 018/166] fix(coding-agent): restore terminal on uncaught exception When an uncaught exception fires in interactive mode, the process dies with stdin still in raw mode and the cursor hidden, leaving the user with a "borked" terminal that needs `stty sane && reset` to recover. The most common trigger is an extension's async ChildProcess `exit` callback that throws (e.g. from accessing a stale ctx after session replacement), but this affects any uncaught throw from anywhere in pi. Add an uncaughtException handler in registerSignalHandlers that calls ui.stop() before exiting, mirroring the existing emergencyTerminalExit pattern. The handler is registered with prependListener and tracked in signalCleanupHandlers, so it is removed on graceful shutdown the same way the other handlers are. Unlike emergencyTerminalExit (used for SIGHUP / dead-terminal EIO), the terminal is still alive on uncaughtException, so we run the normal ui.stop() to restore cooked mode, the cursor, bracketed paste mode, and Kitty / modifyOtherKeys sequences. --- .../src/modes/interactive/interactive-mode.ts | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/packages/coding-agent/src/modes/interactive/interactive-mode.ts b/packages/coding-agent/src/modes/interactive/interactive-mode.ts index 006ee7c5..d03c7f26 100644 --- a/packages/coding-agent/src/modes/interactive/interactive-mode.ts +++ b/packages/coding-agent/src/modes/interactive/interactive-mode.ts @@ -3244,6 +3244,36 @@ export class InteractiveMode { process.exit(129); } + /** + * Last-resort handler for uncaught exceptions. The TUI puts stdin into raw + * mode and hides the cursor; without this handler, an uncaught throw from + * anywhere (e.g. an extension's async `ChildProcess.on("exit")` callback) + * tears down the process while leaving the terminal in raw mode with no + * cursor, requiring `stty sane && reset` to recover. + * + * Unlike emergencyTerminalExit, the terminal is still alive here, so we + * call ui.stop() to restore cooked mode, the cursor, and disable bracketed + * paste / Kitty / modifyOtherKeys sequences. + */ + private uncaughtCrash(error: Error): never { + if (this.isShuttingDown) { + process.exit(1); + } + this.isShuttingDown = true; + try { + this.unregisterSignalHandlers(); + } catch {} + try { + killTrackedDetachedChildren(); + } catch {} + try { + this.ui.stop(); + } catch {} + console.error("pi exiting due to uncaughtException:"); + console.error(error); + process.exit(1); + } + /** * Check if shutdown was requested and perform shutdown if so. */ @@ -3282,6 +3312,13 @@ export class InteractiveMode { process.stderr.on("error", terminalErrorHandler); this.signalCleanupHandlers.push(() => process.stdout.off("error", terminalErrorHandler)); this.signalCleanupHandlers.push(() => process.stderr.off("error", terminalErrorHandler)); + + // Restore the terminal before the process dies on any uncaught throw. + // Without this, an unhandled exception from extension code (or anywhere + // in pi) leaves the terminal in raw mode with no cursor. + const uncaughtExceptionHandler = (error: Error) => this.uncaughtCrash(error); + process.prependListener("uncaughtException", uncaughtExceptionHandler); + this.signalCleanupHandlers.push(() => process.off("uncaughtException", uncaughtExceptionHandler)); } private unregisterSignalHandlers(): void { From 2726e57cda0a799ba98910458f99092eb0829883 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 12 May 2026 20:56:58 +0000 Subject: [PATCH 019/166] chore: approve contributor brianmichel --- .github/APPROVED_CONTRIBUTORS | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/APPROVED_CONTRIBUTORS b/.github/APPROVED_CONTRIBUTORS index 6f2193ab..50d3566e 100644 --- a/.github/APPROVED_CONTRIBUTORS +++ b/.github/APPROVED_CONTRIBUTORS @@ -205,3 +205,5 @@ npupko issue chrisvariety pr maximilianzuern pr + +brianmichel pr From 5ac874c8495e03c2e2da7751e20d01e707a43e15 Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Tue, 12 May 2026 23:25:15 +0200 Subject: [PATCH 020/166] fix(coding-agent): retry Anthropic message_stop stream endings closes #4433 --- packages/coding-agent/CHANGELOG.md | 1 + packages/coding-agent/src/core/agent-session.ts | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 4a9261c4..703c3d59 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -8,6 +8,7 @@ ### Fixed +- Fixed auto-retry for Anthropic streams that end before `message_stop` ([#4433](https://github.com/earendil-works/pi/issues/4433)). - Fixed theme sharing across package scopes so extensions do not crash with `Theme not initialized` ([#4333](https://github.com/earendil-works/pi/issues/4333)). - Fixed keybinding hints to show Option instead of Alt on macOS ([#4289](https://github.com/earendil-works/pi/issues/4289)). - Fixed the interactive update notification to render the changelog as an OSC 8 hyperlink when the terminal supports hyperlinks ([#4280](https://github.com/earendil-works/pi/issues/4280)). diff --git a/packages/coding-agent/src/core/agent-session.ts b/packages/coding-agent/src/core/agent-session.ts index 4985779f..9868e8a6 100644 --- a/packages/coding-agent/src/core/agent-session.ts +++ b/packages/coding-agent/src/core/agent-session.ts @@ -2419,8 +2419,8 @@ export class AgentSession { if (isContextOverflow(message, contextWindow)) return false; const err = message.errorMessage; - // Match: overloaded_error, provider returned error, rate limit, 429, 500, 502, 503, 504, service unavailable, network/connection errors (including connection lost), WebSocket transport closes/errors, fetch failed, request ended without sending chunks, HTTP/2 closed before response, terminated, retry delay exceeded - return /overloaded|provider.?returned.?error|rate.?limit|too many requests|429|500|502|503|504|service.?unavailable|server.?error|internal.?error|network.?error|connection.?error|connection.?refused|connection.?lost|websocket.?closed|websocket.?error|other side closed|fetch failed|upstream.?connect|reset before headers|socket hang up|ended without|http2 request did not get a response|timed? out|timeout|terminated|retry delay/i.test( + // Match: overloaded_error, provider returned error, rate limit, 429, 500, 502, 503, 504, service unavailable, network/connection errors (including connection lost), WebSocket transport closes/errors, fetch failed, premature stream endings, HTTP/2 closed before response, terminated, retry delay exceeded + return /overloaded|provider.?returned.?error|rate.?limit|too many requests|429|500|502|503|504|service.?unavailable|server.?error|internal.?error|network.?error|connection.?error|connection.?refused|connection.?lost|websocket.?closed|websocket.?error|other side closed|fetch failed|upstream.?connect|reset before headers|socket hang up|ended without|stream ended before message_stop|http2 request did not get a response|timed? out|timeout|terminated|retry delay/i.test( err, ); } From d62c416cfdf99ba407cd66b18abe35dd09ec4af9 Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Tue, 12 May 2026 23:34:57 +0200 Subject: [PATCH 021/166] fix(coding-agent): allow tool expansion during extension confirms closes #4429 --- packages/coding-agent/CHANGELOG.md | 1 + .../src/modes/interactive/components/extension-selector.ts | 7 ++++++- .../coding-agent/src/modes/interactive/interactive-mode.ts | 2 +- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 703c3d59..7b86ee14 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -8,6 +8,7 @@ ### Fixed +- Fixed tool output expansion while extension confirmation dialogs are focused ([#4429](https://github.com/earendil-works/pi/issues/4429)). - Fixed auto-retry for Anthropic streams that end before `message_stop` ([#4433](https://github.com/earendil-works/pi/issues/4433)). - Fixed theme sharing across package scopes so extensions do not crash with `Theme not initialized` ([#4333](https://github.com/earendil-works/pi/issues/4333)). - Fixed keybinding hints to show Option instead of Alt on macOS ([#4289](https://github.com/earendil-works/pi/issues/4289)). diff --git a/packages/coding-agent/src/modes/interactive/components/extension-selector.ts b/packages/coding-agent/src/modes/interactive/components/extension-selector.ts index c2fa547e..8b59c2f2 100644 --- a/packages/coding-agent/src/modes/interactive/components/extension-selector.ts +++ b/packages/coding-agent/src/modes/interactive/components/extension-selector.ts @@ -12,6 +12,7 @@ import { keyHint, rawKeyHint } from "./keybinding-hints.js"; export interface ExtensionSelectorOptions { tui?: TUI; timeout?: number; + onToggleToolsExpanded?: () => void; } export class ExtensionSelectorComponent extends Container { @@ -23,6 +24,7 @@ export class ExtensionSelectorComponent extends Container { private titleText: Text; private baseTitle: string; private countdown: CountdownTimer | undefined; + private onToggleToolsExpanded: (() => void) | undefined; constructor( title: string, @@ -36,6 +38,7 @@ export class ExtensionSelectorComponent extends Container { this.options = options; this.onSelectCallback = onSelect; this.onCancelCallback = onCancel; + this.onToggleToolsExpanded = opts?.onToggleToolsExpanded; this.baseTitle = title; this.addChild(new DynamicBorder()); @@ -87,7 +90,9 @@ export class ExtensionSelectorComponent extends Container { handleInput(keyData: string): void { const kb = getKeybindings(); - if (kb.matches(keyData, "tui.select.up") || keyData === "k") { + if (kb.matches(keyData, "app.tools.expand")) { + this.onToggleToolsExpanded?.(); + } else if (kb.matches(keyData, "tui.select.up") || keyData === "k") { this.selectedIndex = Math.max(0, this.selectedIndex - 1); this.updateList(); } else if (kb.matches(keyData, "tui.select.down") || keyData === "j") { diff --git a/packages/coding-agent/src/modes/interactive/interactive-mode.ts b/packages/coding-agent/src/modes/interactive/interactive-mode.ts index 006ee7c5..ee3da0e5 100644 --- a/packages/coding-agent/src/modes/interactive/interactive-mode.ts +++ b/packages/coding-agent/src/modes/interactive/interactive-mode.ts @@ -2024,7 +2024,7 @@ export class InteractiveMode { this.hideExtensionSelector(); resolve(undefined); }, - { tui: this.ui, timeout: opts?.timeout }, + { tui: this.ui, timeout: opts?.timeout, onToggleToolsExpanded: () => this.toggleToolOutputExpansion() }, ); this.editorContainer.clear(); From 64882c6a516c6fe0e207ee3b114704f965e4eef6 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 12 May 2026 23:35:41 +0200 Subject: [PATCH 022/166] chore(deps): remove unused dependencies (#4453) --- package-lock.json | 157 ++++++++++++++++++++------- package.json | 3 +- packages/ai/package.json | 5 +- packages/coding-agent/package.json | 1 - packages/tui/package.json | 8 +- packages/web-ui/example/package.json | 2 +- 6 files changed, 122 insertions(+), 54 deletions(-) diff --git a/package-lock.json b/package-lock.json index 071a4371..19e7021a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,8 +16,7 @@ "packages/coding-agent/examples/extensions/sandbox" ], "dependencies": { - "@earendil-works/pi-coding-agent": "^0.30.2", - "get-east-asian-width": "^1.4.0" + "@earendil-works/pi-coding-agent": "^0.30.2" }, "devDependencies": { "@anthropic-ai/sandbox-runtime": "^0.0.26", @@ -965,6 +964,7 @@ "cpu": [ "ppc64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -981,6 +981,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -997,6 +998,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1013,6 +1015,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1027,6 +1030,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1043,6 +1047,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1059,6 +1064,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1075,6 +1081,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1091,6 +1098,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1107,6 +1115,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1123,6 +1132,7 @@ "cpu": [ "ia32" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1139,6 +1149,7 @@ "cpu": [ "loong64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1155,6 +1166,7 @@ "cpu": [ "mips64el" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1171,6 +1183,7 @@ "cpu": [ "ppc64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1187,6 +1200,7 @@ "cpu": [ "riscv64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1203,6 +1217,7 @@ "cpu": [ "s390x" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1219,6 +1234,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1235,6 +1251,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1251,6 +1268,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1267,6 +1285,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1283,6 +1302,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1299,6 +1319,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1315,6 +1336,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1331,6 +1353,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1347,6 +1370,7 @@ "cpu": [ "ia32" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1363,6 +1387,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1396,6 +1421,7 @@ }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.13", + "dev": true, "license": "MIT", "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", @@ -1404,6 +1430,7 @@ }, "node_modules/@jridgewell/remapping": { "version": "2.3.5", + "dev": true, "license": "MIT", "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", @@ -1412,6 +1439,7 @@ }, "node_modules/@jridgewell/resolve-uri": { "version": "3.1.2", + "dev": true, "license": "MIT", "engines": { "node": ">=6.0.0" @@ -1419,10 +1447,12 @@ }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.5.5", + "dev": true, "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.31", + "dev": true, "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", @@ -2375,6 +2405,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2388,6 +2419,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2399,6 +2431,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2412,6 +2445,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2425,6 +2459,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2438,6 +2473,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2451,6 +2487,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2464,6 +2501,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2477,6 +2515,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2490,6 +2529,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2503,6 +2543,7 @@ "cpu": [ "loong64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2516,6 +2557,7 @@ "cpu": [ "loong64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2529,6 +2571,7 @@ "cpu": [ "ppc64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2542,6 +2585,7 @@ "cpu": [ "ppc64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2555,6 +2599,7 @@ "cpu": [ "riscv64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2568,6 +2613,7 @@ "cpu": [ "riscv64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2581,6 +2627,7 @@ "cpu": [ "s390x" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2594,6 +2641,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2607,6 +2655,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2620,6 +2669,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2633,6 +2683,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2646,6 +2697,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2659,6 +2711,7 @@ "cpu": [ "ia32" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2672,6 +2725,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2685,6 +2739,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3264,6 +3319,7 @@ }, "node_modules/@tailwindcss/node": { "version": "4.2.4", + "dev": true, "license": "MIT", "dependencies": { "@jridgewell/remapping": "^2.3.5", @@ -3277,6 +3333,7 @@ }, "node_modules/@tailwindcss/oxide": { "version": "4.2.4", + "dev": true, "license": "MIT", "engines": { "node": ">= 20" @@ -3303,6 +3360,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3317,6 +3375,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3333,6 +3392,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3349,6 +3409,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3365,6 +3426,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3381,6 +3443,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3397,6 +3460,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3413,6 +3477,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3429,6 +3494,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3453,6 +3519,7 @@ "cpu": [ "wasm32" ], + "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -3469,6 +3536,7 @@ }, "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/core": { "version": "1.8.1", + "dev": true, "inBundle": true, "license": "MIT", "optional": true, @@ -3479,6 +3547,7 @@ }, "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/runtime": { "version": "1.8.1", + "dev": true, "inBundle": true, "license": "MIT", "optional": true, @@ -3488,6 +3557,7 @@ }, "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/wasi-threads": { "version": "1.1.0", + "dev": true, "inBundle": true, "license": "MIT", "optional": true, @@ -3497,6 +3567,7 @@ }, "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": { "version": "1.1.1", + "dev": true, "inBundle": true, "license": "MIT", "optional": true, @@ -3512,6 +3583,7 @@ }, "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@tybys/wasm-util": { "version": "0.10.1", + "dev": true, "inBundle": true, "license": "MIT", "optional": true, @@ -3521,6 +3593,7 @@ }, "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/tslib": { "version": "2.8.1", + "dev": true, "inBundle": true, "license": "0BSD", "optional": true @@ -3532,6 +3605,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3548,6 +3622,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3559,6 +3634,7 @@ }, "node_modules/@tailwindcss/vite": { "version": "4.2.4", + "dev": true, "license": "MIT", "dependencies": { "@tailwindcss/node": "4.2.4", @@ -3634,10 +3710,6 @@ "@types/lodash": "*" } }, - "node_modules/@types/mime-types": { - "version": "2.1.4", - "license": "MIT" - }, "node_modules/@types/ms": { "version": "2.1.0", "dev": true, @@ -4436,6 +4508,7 @@ }, "node_modules/detect-libc": { "version": "2.1.2", + "dev": true, "license": "Apache-2.0", "engines": { "node": ">=8" @@ -4475,6 +4548,7 @@ }, "node_modules/enhanced-resolve": { "version": "5.21.0", + "dev": true, "license": "MIT", "dependencies": { "graceful-fs": "^4.2.4", @@ -4499,6 +4573,7 @@ }, "node_modules/esbuild": { "version": "0.27.7", + "dev": true, "hasInstallScript": true, "license": "MIT", "bin": { @@ -4790,6 +4865,7 @@ "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, "hasInstallScript": true, "license": "MIT", "optional": true, @@ -4864,7 +4940,7 @@ }, "node_modules/get-tsconfig": { "version": "4.14.0", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "resolve-pkg-maps": "^1.0.0" @@ -5269,6 +5345,7 @@ }, "node_modules/lightningcss": { "version": "1.32.0", + "dev": true, "license": "MPL-2.0", "dependencies": { "detect-libc": "^2.0.3" @@ -5301,6 +5378,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -5319,6 +5397,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -5339,6 +5418,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -5359,6 +5439,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -5379,6 +5460,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -5399,6 +5481,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -5419,6 +5502,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -5439,6 +5523,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -5459,6 +5544,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -5479,6 +5565,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -5499,6 +5586,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -5564,6 +5652,7 @@ }, "node_modules/magic-string": { "version": "0.30.21", + "dev": true, "license": "MIT", "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" @@ -5599,27 +5688,6 @@ "node": ">=8.6" } }, - "node_modules/mime-db": { - "version": "1.54.0", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "3.0.2", - "license": "MIT", - "dependencies": { - "mime-db": "^1.54.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, "node_modules/mimic-response": { "version": "3.1.0", "dev": true, @@ -5687,6 +5755,7 @@ }, "node_modules/nanoid": { "version": "3.3.12", + "dev": true, "funding": [ { "type": "github", @@ -5981,6 +6050,7 @@ }, "node_modules/picocolors": { "version": "1.1.1", + "dev": true, "license": "ISC" }, "node_modules/picomatch": { @@ -5996,6 +6066,7 @@ }, "node_modules/postcss": { "version": "8.5.14", + "dev": true, "funding": [ { "type": "opencollective", @@ -6208,7 +6279,7 @@ }, "node_modules/resolve-pkg-maps": { "version": "1.0.0", - "devOptional": true, + "dev": true, "license": "MIT", "funding": { "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" @@ -6232,6 +6303,7 @@ }, "node_modules/rollup": { "version": "4.60.3", + "dev": true, "license": "MIT", "dependencies": { "@types/estree": "1.0.8" @@ -6274,6 +6346,7 @@ }, "node_modules/rollup/node_modules/@types/estree": { "version": "1.0.8", + "dev": true, "license": "MIT" }, "node_modules/run-parallel": { @@ -6495,6 +6568,7 @@ }, "node_modules/source-map-js": { "version": "1.2.1", + "dev": true, "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" @@ -6667,6 +6741,7 @@ }, "node_modules/tapable": { "version": "2.3.3", + "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -6731,6 +6806,7 @@ }, "node_modules/tinyglobby": { "version": "0.2.16", + "dev": true, "license": "MIT", "dependencies": { "fdir": "^6.5.0", @@ -6745,6 +6821,7 @@ }, "node_modules/tinyglobby/node_modules/fdir": { "version": "6.5.0", + "dev": true, "license": "MIT", "engines": { "node": ">=12.0.0" @@ -6760,6 +6837,7 @@ }, "node_modules/tinyglobby/node_modules/picomatch": { "version": "4.0.4", + "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -6837,7 +6915,7 @@ }, "node_modules/tsx": { "version": "4.21.0", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "esbuild": "~0.27.0", @@ -6925,6 +7003,7 @@ }, "node_modules/vite": { "version": "7.3.3", + "dev": true, "license": "MIT", "dependencies": { "esbuild": "^0.27.0", @@ -7018,6 +7097,7 @@ }, "node_modules/vite/node_modules/fdir": { "version": "6.5.0", + "dev": true, "license": "MIT", "engines": { "node": ">=12.0.0" @@ -7033,6 +7113,7 @@ }, "node_modules/vite/node_modules/picomatch": { "version": "4.0.4", + "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -7333,14 +7414,11 @@ "@aws-sdk/client-bedrock-runtime": "^3.1030.0", "@google/genai": "^1.40.0", "@mistralai/mistralai": "^2.2.0", - "chalk": "^5.6.2", "openai": "6.26.0", "partial-json": "^0.1.7", "proxy-agent": "^6.5.0", "proxy-from-env": "^1.1.0", - "typebox": "^1.1.24", - "undici": "^7.19.1", - "zod-to-json-schema": "^3.24.6" + "typebox": "^1.1.24" }, "bin": { "pi-ai": "dist/cli.js" @@ -7385,7 +7463,6 @@ "hosted-git-info": "^9.0.2", "ignore": "^7.0.5", "jiti": "^2.7.0", - "marked": "^15.0.12", "minimatch": "^10.2.3", "proper-lockfile": "^4.1.2", "strip-ansi": "^7.1.0", @@ -7528,15 +7605,13 @@ "version": "0.74.0", "license": "MIT", "dependencies": { - "@types/mime-types": "^2.1.4", - "chalk": "^5.5.0", "get-east-asian-width": "^1.3.0", - "marked": "^15.0.12", - "mime-types": "^3.0.1" + "marked": "^15.0.12" }, "devDependencies": { "@xterm/headless": "^5.5.0", - "@xterm/xterm": "^5.5.0" + "@xterm/xterm": "^5.5.0", + "chalk": "^5.5.0" }, "engines": { "node": ">=20.0.0" @@ -7579,11 +7654,11 @@ "@earendil-works/pi-ai": "file:../../ai", "@earendil-works/pi-web-ui": "file:../", "@mariozechner/mini-lit": "^0.2.0", - "@tailwindcss/vite": "^4.1.17", "lit": "^3.3.1", "lucide": "^0.544.0" }, "devDependencies": { + "@tailwindcss/vite": "^4.1.17", "typescript": "^5.7.3", "vite": "^7.1.6" } diff --git a/package.json b/package.json index 2b5b8cae..288ed3a2 100644 --- a/package.json +++ b/package.json @@ -49,8 +49,7 @@ }, "version": "0.0.3", "dependencies": { - "@earendil-works/pi-coding-agent": "^0.30.2", - "get-east-asian-width": "^1.4.0" + "@earendil-works/pi-coding-agent": "^0.30.2" }, "overrides": { "rimraf": "6.1.2", diff --git a/packages/ai/package.json b/packages/ai/package.json index 1744a2fd..9fce8e35 100644 --- a/packages/ai/package.json +++ b/packages/ai/package.json @@ -73,14 +73,11 @@ "@aws-sdk/client-bedrock-runtime": "^3.1030.0", "@google/genai": "^1.40.0", "@mistralai/mistralai": "^2.2.0", - "chalk": "^5.6.2", "openai": "6.26.0", "partial-json": "^0.1.7", "proxy-agent": "^6.5.0", "proxy-from-env": "^1.1.0", - "typebox": "^1.1.24", - "undici": "^7.19.1", - "zod-to-json-schema": "^3.24.6" + "typebox": "^1.1.24" }, "keywords": [ "ai", diff --git a/packages/coding-agent/package.json b/packages/coding-agent/package.json index a046a3c9..f4aa53da 100644 --- a/packages/coding-agent/package.json +++ b/packages/coding-agent/package.json @@ -51,7 +51,6 @@ "hosted-git-info": "^9.0.2", "ignore": "^7.0.5", "jiti": "^2.7.0", - "marked": "^15.0.12", "minimatch": "^10.2.3", "proper-lockfile": "^4.1.2", "strip-ansi": "^7.1.0", diff --git a/packages/tui/package.json b/packages/tui/package.json index af49435e..c23363e9 100644 --- a/packages/tui/package.json +++ b/packages/tui/package.json @@ -36,17 +36,15 @@ }, "types": "./dist/index.d.ts", "dependencies": { - "@types/mime-types": "^2.1.4", - "chalk": "^5.5.0", "get-east-asian-width": "^1.3.0", - "marked": "^15.0.12", - "mime-types": "^3.0.1" + "marked": "^15.0.12" }, "optionalDependencies": { "koffi": "^2.9.0" }, "devDependencies": { "@xterm/headless": "^5.5.0", - "@xterm/xterm": "^5.5.0" + "@xterm/xterm": "^5.5.0", + "chalk": "^5.5.0" } } diff --git a/packages/web-ui/example/package.json b/packages/web-ui/example/package.json index 06497e47..b3efac1a 100644 --- a/packages/web-ui/example/package.json +++ b/packages/web-ui/example/package.json @@ -14,11 +14,11 @@ "@mariozechner/mini-lit": "^0.2.0", "@earendil-works/pi-ai": "file:../../ai", "@earendil-works/pi-web-ui": "file:../", - "@tailwindcss/vite": "^4.1.17", "lit": "^3.3.1", "lucide": "^0.544.0" }, "devDependencies": { + "@tailwindcss/vite": "^4.1.17", "typescript": "^5.7.3", "vite": "^7.1.6" } From a5cbd8e1aa4b4b31c892d558c0fa3dbf05548e50 Mon Sep 17 00:00:00 2001 From: Brian Michel Date: Fri, 8 May 2026 20:17:31 -0400 Subject: [PATCH 023/166] Add Windows arm64 binary support Update build-binaries workflow and shell script to allow for packaging of both arm64 and x64 versions of the Windows application. --- .github/workflows/build-binaries.yml | 2 ++ scripts/build-binaries.sh | 28 ++++++++++++++++++---------- 2 files changed, 20 insertions(+), 10 deletions(-) diff --git a/.github/workflows/build-binaries.yml b/.github/workflows/build-binaries.yml index fcff2104..59d61ac8 100644 --- a/.github/workflows/build-binaries.yml +++ b/.github/workflows/build-binaries.yml @@ -69,6 +69,7 @@ jobs: pi-linux-x64.tar.gz \ pi-linux-arm64.tar.gz \ pi-windows-x64.zip \ + pi-windows-arm64.zip \ 2>/dev/null || \ gh release upload "${RELEASE_TAG}" \ pi-darwin-arm64.tar.gz \ @@ -76,4 +77,5 @@ jobs: pi-linux-x64.tar.gz \ pi-linux-arm64.tar.gz \ pi-windows-x64.zip \ + pi-windows-arm64.zip \ --clobber diff --git a/scripts/build-binaries.sh b/scripts/build-binaries.sh index 635aca64..29cf0147 100755 --- a/scripts/build-binaries.sh +++ b/scripts/build-binaries.sh @@ -8,7 +8,7 @@ # # Options: # --skip-deps Skip installing cross-platform dependencies -# --platform Build only for specified platform (darwin-arm64, darwin-x64, linux-x64, linux-arm64, windows-x64) +# --platform Build only for specified platform (darwin-arm64, darwin-x64, linux-x64, linux-arm64, windows-x64, windows-arm64) # # Output: # packages/coding-agent/binaries/ @@ -17,6 +17,7 @@ # pi-linux-x64.tar.gz # pi-linux-arm64.tar.gz # pi-windows-x64.zip +# pi-windows-arm64.zip set -euo pipefail @@ -49,7 +50,7 @@ if [[ -n "$PLATFORM" ]]; then ;; *) echo "Invalid platform: $PLATFORM" - echo "Valid platforms: darwin-arm64, darwin-x64, linux-x64, linux-arm64, windows-x64" + echo "Valid platforms: darwin-arm64, darwin-x64, linux-x64, linux-arm64, windows-x64, windows-arm64" exit 1 ;; esac @@ -70,11 +71,13 @@ if [[ "$SKIP_DEPS" == "false" ]]; then @mariozechner/clipboard-linux-x64-gnu@0.3.0 \ @mariozechner/clipboard-linux-arm64-gnu@0.3.0 \ @mariozechner/clipboard-win32-x64-msvc@0.3.0 \ + @mariozechner/clipboard-win32-arm64-msvc@0.3.0 \ @img/sharp-darwin-arm64@0.34.5 \ @img/sharp-darwin-x64@0.34.5 \ @img/sharp-linux-x64@0.34.5 \ @img/sharp-linux-arm64@0.34.5 \ @img/sharp-win32-x64@0.34.5 \ + @img/sharp-win32-arm64@0.34.5 \ @img/sharp-libvips-darwin-arm64@1.2.4 \ @img/sharp-libvips-darwin-x64@1.2.4 \ @img/sharp-libvips-linux-x64@1.2.4 \ @@ -91,13 +94,13 @@ cd packages/coding-agent # Clean previous builds rm -rf binaries -mkdir -p binaries/{darwin-arm64,darwin-x64,linux-x64,linux-arm64,windows-x64} +mkdir -p binaries/{darwin-arm64,darwin-x64,linux-x64,linux-arm64,windows-x64,windows-arm64} # Determine which platforms to build if [[ -n "$PLATFORM" ]]; then PLATFORMS=("$PLATFORM") else - PLATFORMS=(darwin-arm64 darwin-x64 linux-x64 linux-arm64 windows-x64) + PLATFORMS=(darwin-arm64 darwin-x64 linux-x64 linux-arm64 windows-x64 windows-arm64) fi for platform in "${PLATFORMS[@]}"; do @@ -106,7 +109,7 @@ for platform in "${PLATFORMS[@]}"; do # into every binary. Koffi is only used on Windows for VT input and the # call site has a try/catch fallback. For Windows builds, we copy the # appropriate .node file alongside the binary below. - if [[ "$platform" == "windows-x64" ]]; then + if [[ "$platform" == windows-* ]]; then bun build --compile --external koffi --target=bun-$platform ./dist/bun/cli.js --outfile binaries/$platform/pi.exe else bun build --compile --external koffi --target=bun-$platform ./dist/bun/cli.js --outfile binaries/$platform/pi @@ -130,11 +133,16 @@ for platform in "${PLATFORMS[@]}"; do cp -r examples binaries/$platform/ # Copy koffi native module for Windows (needed for VT input support) - if [[ "$platform" == "windows-x64" ]]; then - mkdir -p binaries/$platform/node_modules/koffi/build/koffi/win32_x64 + if [[ "$platform" == windows-* ]]; then + if [[ "$platform" == "windows-arm64" ]]; then + koffi_arch_dir="win32_arm64" + else + koffi_arch_dir="win32_x64" + fi + mkdir -p binaries/$platform/node_modules/koffi/build/koffi/$koffi_arch_dir cp ../../node_modules/koffi/index.js binaries/$platform/node_modules/koffi/ cp ../../node_modules/koffi/package.json binaries/$platform/node_modules/koffi/ - cp ../../node_modules/koffi/build/koffi/win32_x64/koffi.node binaries/$platform/node_modules/koffi/build/koffi/win32_x64/ + cp ../../node_modules/koffi/build/koffi/$koffi_arch_dir/koffi.node binaries/$platform/node_modules/koffi/build/koffi/$koffi_arch_dir/ fi done @@ -142,7 +150,7 @@ done cd binaries for platform in "${PLATFORMS[@]}"; do - if [[ "$platform" == "windows-x64" ]]; then + if [[ "$platform" == windows-* ]]; then # Windows (zip) echo "Creating pi-$platform.zip..." (cd $platform && zip -r ../pi-$platform.zip .) @@ -157,7 +165,7 @@ done echo "==> Extracting archives for testing..." for platform in "${PLATFORMS[@]}"; do rm -rf $platform - if [[ "$platform" == "windows-x64" ]]; then + if [[ "$platform" == windows-* ]]; then mkdir -p $platform && (cd $platform && unzip -q ../pi-$platform.zip) else tar -xzf pi-$platform.tar.gz && mv pi $platform From c786efda9dfa76225e2a1bd17f62d504f5fccf88 Mon Sep 17 00:00:00 2001 From: Brian Michel Date: Mon, 11 May 2026 09:26:28 -0400 Subject: [PATCH 024/166] Update bun version to minimum required version for Windows ARM --- .github/workflows/build-binaries.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-binaries.yml b/.github/workflows/build-binaries.yml index 59d61ac8..342bc8c0 100644 --- a/.github/workflows/build-binaries.yml +++ b/.github/workflows/build-binaries.yml @@ -28,7 +28,7 @@ jobs: - name: Setup Bun uses: oven-sh/setup-bun@4bc047ad259df6fc24a6c9b0f9a0cb08cf17fbe5 # v2.0.1 with: - bun-version: 1.2.20 + bun-version: 1.3.10 - name: Setup Node.js uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0 From 8da1f3d1319bbb1263210befc19cc5497ea919c8 Mon Sep 17 00:00:00 2001 From: Nelson Herrera Date: Wed, 13 May 2026 01:31:09 -0700 Subject: [PATCH 025/166] fix(tui): Make markdown.ts more robust to large markdown files (#4463) --- packages/tui/src/components/markdown.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/tui/src/components/markdown.ts b/packages/tui/src/components/markdown.ts index 0f8ffdfe..8e23e289 100644 --- a/packages/tui/src/components/markdown.ts +++ b/packages/tui/src/components/markdown.ts @@ -145,7 +145,9 @@ export class Markdown implements Component { const token = tokens[i]; const nextToken = tokens[i + 1]; const tokenLines = this.renderToken(token, contentWidth, nextToken?.type); - renderedLines.push(...tokenLines); + for (const tokenLine of tokenLines) { + renderedLines.push(tokenLine); + } } // Wrap lines (NO padding, NO background yet) @@ -154,7 +156,9 @@ export class Markdown implements Component { if (isImageLine(line)) { wrappedLines.push(line); } else { - wrappedLines.push(...wrapTextWithAnsi(line, contentWidth)); + for (const wrappedLine of wrapTextWithAnsi(line, contentWidth)) { + wrappedLines.push(wrappedLine); + } } } @@ -191,7 +195,7 @@ export class Markdown implements Component { } // Combine top padding, content, and bottom padding - const result = [...emptyLines, ...contentLines, ...emptyLines]; + const result = emptyLines.concat(contentLines, emptyLines); // Update cache this.cachedText = this.text; From 2829146dde9b27e7d52f4094a7b3d268a08b52a6 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Wed, 13 May 2026 10:44:56 +0200 Subject: [PATCH 026/166] chore(deps): Kill small dependencies (#4467) --- package-lock.json | 191 +----------------- .../agent/src/harness/session/repo/shared.ts | 2 +- .../src/harness/session/storage/memory.ts | 2 +- packages/agent/src/harness/session/uuid.ts | 44 ++++ packages/coding-agent/package.json | 4 - .../coding-agent/src/core/bash-executor.ts | 2 +- .../coding-agent/src/core/session-manager.ts | 2 +- .../src/core/tools/render-utils.ts | 2 +- .../interactive/components/bash-execution.ts | 2 +- packages/coding-agent/src/utils/ansi.ts | 87 ++++++++ packages/coding-agent/src/utils/mime.ts | 82 ++++++-- .../coding-agent/src/utils/tools-manager.ts | 90 ++++++++- packages/coding-agent/src/utils/uuid.ts | 44 ++++ packages/coding-agent/test/ansi-utils.test.ts | 22 ++ .../coding-agent/test/oauth-selector.test.ts | 2 +- .../3217-scoped-model-order.test.ts | 2 +- ...hinking-toggle-pending-tool-render.test.ts | 2 +- .../test/tool-execution-component.test.ts | 2 +- 18 files changed, 357 insertions(+), 227 deletions(-) create mode 100644 packages/agent/src/harness/session/uuid.ts create mode 100644 packages/coding-agent/src/utils/ansi.ts create mode 100644 packages/coding-agent/src/utils/uuid.ts create mode 100644 packages/coding-agent/test/ansi-utils.test.ts diff --git a/package-lock.json b/package-lock.json index 19e7021a..06d8c051 100644 --- a/package-lock.json +++ b/package-lock.json @@ -929,14 +929,6 @@ "node": ">=14.21.3" } }, - "node_modules/@borewit/text-codec": { - "version": "0.2.2", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Borewit" - } - }, "node_modules/@earendil-works/pi-agent-core": { "resolved": "packages/agent", "link": true @@ -3645,25 +3637,6 @@ "vite": "^5.2.0 || ^6 || ^7 || ^8" } }, - "node_modules/@tokenizer/inflate": { - "version": "0.4.1", - "license": "MIT", - "dependencies": { - "debug": "^4.4.3", - "token-types": "^6.1.1" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Borewit" - } - }, - "node_modules/@tokenizer/token": { - "version": "0.3.0", - "license": "MIT" - }, "node_modules/@tootallnate/quickjs-emscripten": { "version": "0.23.0", "license": "MIT" @@ -3739,14 +3712,6 @@ "version": "2.0.7", "license": "MIT" }, - "node_modules/@types/yauzl": { - "version": "2.10.3", - "license": "MIT", - "optional": true, - "dependencies": { - "@types/node": "*" - } - }, "node_modules/@typescript/native-preview": { "version": "7.0.0-dev.20260120.1", "dev": true, @@ -3989,16 +3954,6 @@ "version": "2.0.8", "license": "MIT" }, - "node_modules/ansi-regex": { - "version": "6.2.2", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, "node_modules/ansi-styles": { "version": "4.3.0", "license": "MIT", @@ -4131,13 +4086,6 @@ "ieee754": "^1.1.13" } }, - "node_modules/buffer-crc32": { - "version": "0.2.13", - "license": "MIT", - "engines": { - "node": "*" - } - }, "node_modules/buffer-equal-constant-time": { "version": "1.0.1", "license": "BSD-3-Clause" @@ -4541,6 +4489,7 @@ }, "node_modules/end-of-stream": { "version": "1.4.5", + "dev": true, "license": "MIT", "dependencies": { "once": "^1.4.0" @@ -4718,24 +4667,6 @@ "version": "3.0.2", "license": "MIT" }, - "node_modules/extract-zip": { - "version": "2.0.1", - "license": "BSD-2-Clause", - "dependencies": { - "debug": "^4.1.1", - "get-stream": "^5.1.0", - "yauzl": "^2.10.0" - }, - "bin": { - "extract-zip": "cli.js" - }, - "engines": { - "node": ">= 10.17.0" - }, - "optionalDependencies": { - "@types/yauzl": "^2.9.1" - } - }, "node_modules/fast-glob": { "version": "3.3.3", "dev": true, @@ -4791,13 +4722,6 @@ "reusify": "^1.0.4" } }, - "node_modules/fd-slicer": { - "version": "1.1.0", - "license": "MIT", - "dependencies": { - "pend": "~1.2.0" - } - }, "node_modules/fetch-blob": { "version": "3.2.0", "funding": [ @@ -4819,22 +4743,6 @@ "node": "^12.20 || >= 14.13" } }, - "node_modules/file-type": { - "version": "21.3.4", - "license": "MIT", - "dependencies": { - "@tokenizer/inflate": "^0.4.1", - "strtok3": "^10.3.4", - "token-types": "^6.1.1", - "uint8array-extras": "^1.4.0" - }, - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/sindresorhus/file-type?sponsor=1" - } - }, "node_modules/fill-range": { "version": "7.1.1", "dev": true, @@ -4925,19 +4833,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/get-stream": { - "version": "5.2.0", - "license": "MIT", - "dependencies": { - "pump": "^3.0.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/get-tsconfig": { "version": "4.14.0", "dev": true, @@ -5102,6 +4997,7 @@ }, "node_modules/ieee754": { "version": "1.2.1", + "dev": true, "funding": [ { "type": "github", @@ -5863,6 +5759,7 @@ }, "node_modules/once": { "version": "1.4.0", + "dev": true, "license": "ISC", "dependencies": { "wrappy": "1" @@ -6024,10 +5921,6 @@ "@napi-rs/canvas": "^0.1.81" } }, - "node_modules/pend": { - "version": "1.2.0", - "license": "MIT" - }, "node_modules/pi-extension-custom-provider-anthropic": { "resolved": "packages/coding-agent/examples/extensions/custom-provider-anthropic", "link": true @@ -6188,6 +6081,7 @@ }, "node_modules/pump": { "version": "3.0.4", + "dev": true, "license": "MIT", "dependencies": { "end-of-stream": "^1.1.0", @@ -6621,19 +6515,6 @@ "node": ">=8" } }, - "node_modules/strip-ansi": { - "version": "7.2.0", - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.2.2" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, "node_modules/strip-eof": { "version": "1.0.0", "dev": true, @@ -6671,20 +6552,6 @@ ], "license": "MIT" }, - "node_modules/strtok3": { - "version": "10.3.5", - "license": "MIT", - "dependencies": { - "@tokenizer/token": "^0.3.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Borewit" - } - }, "node_modules/supports-color": { "version": "8.1.1", "dev": true, @@ -6881,22 +6748,6 @@ "node": ">=8.0" } }, - "node_modules/token-types": { - "version": "6.1.2", - "license": "MIT", - "dependencies": { - "@borewit/text-codec": "^0.2.1", - "@tokenizer/token": "^0.3.0", - "ieee754": "^1.2.1" - }, - "engines": { - "node": ">=14.16" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Borewit" - } - }, "node_modules/tree-kill": { "version": "1.2.2", "dev": true, @@ -6965,16 +6816,6 @@ "@webreflection/alien-signals": "^0.3.2" } }, - "node_modules/uint8array-extras": { - "version": "1.5.0", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/undici": { "version": "7.25.0", "license": "MIT", @@ -6990,17 +6831,6 @@ "version": "1.0.2", "license": "MIT" }, - "node_modules/uuid": { - "version": "14.0.0", - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], - "license": "MIT", - "bin": { - "uuid": "dist-node/bin/uuid" - } - }, "node_modules/vite": { "version": "7.3.3", "dev": true, @@ -7275,6 +7105,7 @@ }, "node_modules/wrappy": { "version": "1.0.2", + "dev": true, "license": "ISC" }, "node_modules/ws": { @@ -7351,14 +7182,6 @@ "node": ">=10" } }, - "node_modules/yauzl": { - "version": "2.10.0", - "license": "MIT", - "dependencies": { - "buffer-crc32": "~0.2.3", - "fd-slicer": "~1.1.0" - } - }, "node_modules/zod": { "version": "4.4.3", "license": "MIT", @@ -7457,18 +7280,14 @@ "chalk": "^5.5.0", "cli-highlight": "^2.1.11", "diff": "^8.0.2", - "extract-zip": "^2.0.1", - "file-type": "^21.1.1", "glob": "^13.0.1", "hosted-git-info": "^9.0.2", "ignore": "^7.0.5", "jiti": "^2.7.0", "minimatch": "^10.2.3", "proper-lockfile": "^4.1.2", - "strip-ansi": "^7.1.0", "typebox": "^1.1.24", "undici": "^7.19.1", - "uuid": "^14.0.0", "yaml": "^2.8.2" }, "bin": { diff --git a/packages/agent/src/harness/session/repo/shared.ts b/packages/agent/src/harness/session/repo/shared.ts index eb359fc2..2c628ca9 100644 --- a/packages/agent/src/harness/session/repo/shared.ts +++ b/packages/agent/src/harness/session/repo/shared.ts @@ -1,6 +1,6 @@ -import { v7 as uuidv7 } from "uuid"; import type { SessionMetadata, SessionStorage, SessionTreeEntry } from "../../types.js"; import { Session } from "../session.js"; +import { uuidv7 } from "../uuid.js"; export function createSessionId(): string { return uuidv7(); diff --git a/packages/agent/src/harness/session/storage/memory.ts b/packages/agent/src/harness/session/storage/memory.ts index 648ae6b0..652f633f 100644 --- a/packages/agent/src/harness/session/storage/memory.ts +++ b/packages/agent/src/harness/session/storage/memory.ts @@ -1,6 +1,6 @@ import { randomUUID } from "node:crypto"; -import { v7 as uuidv7 } from "uuid"; import type { SessionMetadata, SessionStorage, SessionTreeEntry } from "../../types.js"; +import { uuidv7 } from "../uuid.js"; function updateLabelCache(labelsById: Map, entry: SessionTreeEntry): void { if (entry.type !== "label") return; diff --git a/packages/agent/src/harness/session/uuid.ts b/packages/agent/src/harness/session/uuid.ts new file mode 100644 index 00000000..27b660ae --- /dev/null +++ b/packages/agent/src/harness/session/uuid.ts @@ -0,0 +1,44 @@ +import { randomBytes } from "node:crypto"; + +let lastTimestamp = -Infinity; +let sequence = 0; + +export function uuidv7(): string { + const random = randomBytes(16); + const timestamp = Date.now(); + + if (timestamp > lastTimestamp) { + sequence = (random[6] << 23) | (random[7] << 16) | (random[8] << 8) | random[9]; + lastTimestamp = timestamp; + } else { + sequence = (sequence + 1) | 0; + if (sequence === 0) { + lastTimestamp++; + } + } + + const bytes = new Uint8Array(16); + bytes[0] = (lastTimestamp / 0x10000000000) & 0xff; + bytes[1] = (lastTimestamp / 0x100000000) & 0xff; + bytes[2] = (lastTimestamp / 0x1000000) & 0xff; + bytes[3] = (lastTimestamp / 0x10000) & 0xff; + bytes[4] = (lastTimestamp / 0x100) & 0xff; + bytes[5] = lastTimestamp & 0xff; + bytes[6] = 0x70 | ((sequence >>> 28) & 0x0f); + bytes[7] = (sequence >>> 20) & 0xff; + bytes[8] = 0x80 | ((sequence >>> 14) & 0x3f); + bytes[9] = (sequence >>> 6) & 0xff; + bytes[10] = ((sequence << 2) & 0xff) | (random[10] & 0x03); + bytes[11] = random[11]; + bytes[12] = random[12]; + bytes[13] = random[13]; + bytes[14] = random[14]; + bytes[15] = random[15]; + + return formatUuid(bytes); +} + +function formatUuid(bytes: Uint8Array): string { + const hex = Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")); + return `${hex.slice(0, 4).join("")}-${hex.slice(4, 6).join("")}-${hex.slice(6, 8).join("")}-${hex.slice(8, 10).join("")}-${hex.slice(10, 16).join("")}`; +} diff --git a/packages/coding-agent/package.json b/packages/coding-agent/package.json index f4aa53da..5f4ab9e5 100644 --- a/packages/coding-agent/package.json +++ b/packages/coding-agent/package.json @@ -45,18 +45,14 @@ "chalk": "^5.5.0", "cli-highlight": "^2.1.11", "diff": "^8.0.2", - "extract-zip": "^2.0.1", - "file-type": "^21.1.1", "glob": "^13.0.1", "hosted-git-info": "^9.0.2", "ignore": "^7.0.5", "jiti": "^2.7.0", "minimatch": "^10.2.3", "proper-lockfile": "^4.1.2", - "strip-ansi": "^7.1.0", "typebox": "^1.1.24", "undici": "^7.19.1", - "uuid": "^14.0.0", "yaml": "^2.8.2" }, "overrides": { diff --git a/packages/coding-agent/src/core/bash-executor.ts b/packages/coding-agent/src/core/bash-executor.ts index ee55611e..c1818dac 100644 --- a/packages/coding-agent/src/core/bash-executor.ts +++ b/packages/coding-agent/src/core/bash-executor.ts @@ -10,7 +10,7 @@ import { randomBytes } from "node:crypto"; import { createWriteStream, type WriteStream } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; -import stripAnsi from "strip-ansi"; +import { stripAnsi } from "../utils/ansi.js"; import { sanitizeBinaryOutput } from "../utils/shell.js"; import type { BashOperations } from "./tools/bash.js"; import { DEFAULT_MAX_BYTES, truncateTail } from "./tools/truncate.js"; diff --git a/packages/coding-agent/src/core/session-manager.ts b/packages/coding-agent/src/core/session-manager.ts index 8a0d286a..60b111e0 100644 --- a/packages/coding-agent/src/core/session-manager.ts +++ b/packages/coding-agent/src/core/session-manager.ts @@ -15,8 +15,8 @@ import { } from "fs"; import { readdir, readFile, stat } from "fs/promises"; import { join, resolve } from "path"; -import { v7 as uuidv7 } from "uuid"; import { getAgentDir as getDefaultAgentDir, getSessionsDir } from "../config.js"; +import { uuidv7 } from "../utils/uuid.js"; import { type BashExecutionMessage, type CustomMessage, diff --git a/packages/coding-agent/src/core/tools/render-utils.ts b/packages/coding-agent/src/core/tools/render-utils.ts index 8cdfc475..8c7a6fcd 100644 --- a/packages/coding-agent/src/core/tools/render-utils.ts +++ b/packages/coding-agent/src/core/tools/render-utils.ts @@ -1,7 +1,7 @@ import * as os from "node:os"; import type { ImageContent, TextContent } from "@earendil-works/pi-ai"; import { getCapabilities, getImageDimensions, imageFallback } from "@earendil-works/pi-tui"; -import stripAnsi from "strip-ansi"; +import { stripAnsi } from "../../utils/ansi.js"; import { sanitizeBinaryOutput } from "../../utils/shell.js"; export function shortenPath(path: unknown): string { diff --git a/packages/coding-agent/src/modes/interactive/components/bash-execution.ts b/packages/coding-agent/src/modes/interactive/components/bash-execution.ts index bfe733f8..240cd12d 100644 --- a/packages/coding-agent/src/modes/interactive/components/bash-execution.ts +++ b/packages/coding-agent/src/modes/interactive/components/bash-execution.ts @@ -3,13 +3,13 @@ */ import { Container, Loader, Spacer, Text, type TUI } from "@earendil-works/pi-tui"; -import stripAnsi from "strip-ansi"; import { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, type TruncationResult, truncateTail, } from "../../../core/tools/truncate.js"; +import { stripAnsi } from "../../../utils/ansi.js"; import { theme } from "../theme/theme.js"; import { DynamicBorder } from "./dynamic-border.js"; import { keyHint, keyText } from "./keybinding-hints.js"; diff --git a/packages/coding-agent/src/utils/ansi.ts b/packages/coding-agent/src/utils/ansi.ts new file mode 100644 index 00000000..497ab3da --- /dev/null +++ b/packages/coding-agent/src/utils/ansi.ts @@ -0,0 +1,87 @@ +export function stripAnsi(value: string): string { + let output = ""; + let index = 0; + + while (index < value.length) { + const code = value.charCodeAt(index); + + if (code === 0x1b) { + const next = value[index + 1]; + + if (next === "[") { + index = skipCsi(value, index + 2); + continue; + } + + if (next === "]" || next === "P" || next === "^" || next === "_") { + index = skipStringControl(value, index + 2); + continue; + } + + if (next && "()*+-./#".includes(next)) { + index = Math.min(index + 3, value.length); + continue; + } + + if (next && isEscFinalByte(next.charCodeAt(0))) { + index += 2; + continue; + } + } else if (code === 0x9b) { + index = skipCsi(value, index + 1); + continue; + } else if (code === 0x9d || code === 0x90 || code === 0x9e || code === 0x9f) { + index = skipStringControl(value, index + 1); + continue; + } + + output += value[index]; + index++; + } + + return output; +} + +function isEscFinalByte(code: number): boolean { + return ( + (code >= 0x30 && code <= 0x39) || + (code >= 0x41 && code <= 0x50) || + (code >= 0x52 && code <= 0x54) || + code === 0x5a || + code === 0x63 || + (code >= 0x66 && code <= 0x6e) || + (code >= 0x71 && code <= 0x75) || + code === 0x79 || + code === 0x3d || + code === 0x3e || + code === 0x3c || + code === 0x7e + ); +} + +function skipCsi(value: string, start: number): number { + let index = start; + while (index < value.length) { + const code = value.charCodeAt(index); + index++; + if (code >= 0x40 && code <= 0x7e) { + return index; + } + } + return value.length; +} + +function skipStringControl(value: string, start: number): number { + let index = start; + while (index < value.length) { + const code = value.charCodeAt(index); + if (code === 0x07 || code === 0x9c) { + return index + 1; + } + if (code === 0x1b && value[index + 1] === "\\") { + return index + 2; + } + index++; + } + return value.length; +} diff --git a/packages/coding-agent/src/utils/mime.ts b/packages/coding-agent/src/utils/mime.ts index f9ded46e..7381d370 100644 --- a/packages/coding-agent/src/utils/mime.ts +++ b/packages/coding-agent/src/utils/mime.ts @@ -1,30 +1,74 @@ import { open } from "node:fs/promises"; -import { fileTypeFromBuffer } from "file-type"; -const IMAGE_MIME_TYPES = new Set(["image/jpeg", "image/png", "image/gif", "image/webp"]); +const IMAGE_TYPE_SNIFF_BYTES = 4100; +const PNG_SIGNATURE = [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]; -const FILE_TYPE_SNIFF_BYTES = 4100; +export function detectSupportedImageMimeType(buffer: Uint8Array): string | null { + if (startsWith(buffer, [0xff, 0xd8, 0xff])) { + return buffer[3] === 0xf7 ? null : "image/jpeg"; + } + if (startsWith(buffer, PNG_SIGNATURE)) { + return isPng(buffer) && !isAnimatedPng(buffer) ? "image/png" : null; + } + if (startsWithAscii(buffer, 0, "GIF")) { + return "image/gif"; + } + if (startsWithAscii(buffer, 0, "RIFF") && startsWithAscii(buffer, 8, "WEBP")) { + return "image/webp"; + } + return null; +} export async function detectSupportedImageMimeTypeFromFile(filePath: string): Promise { const fileHandle = await open(filePath, "r"); try { - const buffer = Buffer.alloc(FILE_TYPE_SNIFF_BYTES); - const { bytesRead } = await fileHandle.read(buffer, 0, FILE_TYPE_SNIFF_BYTES, 0); - if (bytesRead === 0) { - return null; - } - - const fileType = await fileTypeFromBuffer(buffer.subarray(0, bytesRead)); - if (!fileType) { - return null; - } - - if (!IMAGE_MIME_TYPES.has(fileType.mime)) { - return null; - } - - return fileType.mime; + const buffer = Buffer.alloc(IMAGE_TYPE_SNIFF_BYTES); + const { bytesRead } = await fileHandle.read(buffer, 0, IMAGE_TYPE_SNIFF_BYTES, 0); + return detectSupportedImageMimeType(buffer.subarray(0, bytesRead)); } finally { await fileHandle.close(); } } + +function isPng(buffer: Uint8Array): boolean { + return ( + buffer.length >= 16 && readUint32BE(buffer, PNG_SIGNATURE.length) === 13 && startsWithAscii(buffer, 12, "IHDR") + ); +} + +function isAnimatedPng(buffer: Uint8Array): boolean { + let offset = PNG_SIGNATURE.length; + while (offset + 8 <= buffer.length) { + const chunkLength = readUint32BE(buffer, offset); + const chunkTypeOffset = offset + 4; + if (startsWithAscii(buffer, chunkTypeOffset, "acTL")) return true; + if (startsWithAscii(buffer, chunkTypeOffset, "IDAT")) return false; + + const nextOffset = offset + 8 + chunkLength + 4; + if (nextOffset <= offset || nextOffset > buffer.length) return false; + offset = nextOffset; + } + return false; +} + +function readUint32BE(buffer: Uint8Array, offset: number): number { + return ( + (buffer[offset] ?? 0) * 0x1000000 + + ((buffer[offset + 1] ?? 0) << 16) + + ((buffer[offset + 2] ?? 0) << 8) + + (buffer[offset + 3] ?? 0) + ); +} + +function startsWith(buffer: Uint8Array, bytes: number[]): boolean { + if (buffer.length < bytes.length) return false; + return bytes.every((byte, index) => buffer[index] === byte); +} + +function startsWithAscii(buffer: Uint8Array, offset: number, text: string): boolean { + if (buffer.length < offset + text.length) return false; + for (let index = 0; index < text.length; index++) { + if (buffer[offset + index] !== text.charCodeAt(index)) return false; + } + return true; +} diff --git a/packages/coding-agent/src/utils/tools-manager.ts b/packages/coding-agent/src/utils/tools-manager.ts index ba60341f..19df4ea7 100644 --- a/packages/coding-agent/src/utils/tools-manager.ts +++ b/packages/coding-agent/src/utils/tools-manager.ts @@ -1,6 +1,5 @@ import chalk from "chalk"; -import { spawnSync } from "child_process"; -import extractZip from "extract-zip"; +import { type SpawnSyncReturns, spawnSync } from "child_process"; import { chmodSync, createWriteStream, existsSync, mkdirSync, readdirSync, renameSync, rmSync } from "fs"; import { arch, platform } from "os"; import { join } from "path"; @@ -159,6 +158,85 @@ function findBinaryRecursively(rootDir: string, binaryFileName: string): string return null; } +function formatSpawnFailure(result: SpawnSyncReturns): string { + if (result.error?.message) { + return result.error.message; + } + const stderr = result.stderr?.toString().trim(); + if (stderr) { + return stderr; + } + const stdout = result.stdout?.toString().trim(); + if (stdout) { + return stdout; + } + return `exit status ${result.status ?? "unknown"}`; +} + +function runExtractionCommand(command: string, args: string[]): string | null { + const result = spawnSync(command, args, { stdio: "pipe" }); + if (!result.error && result.status === 0) { + return null; + } + return `${command}: ${formatSpawnFailure(result)}`; +} + +function extractTarGzArchive(archivePath: string, extractDir: string, assetName: string): void { + const failure = runExtractionCommand("tar", ["xzf", archivePath, "-C", extractDir]); + if (failure) { + throw new Error(`Failed to extract ${assetName}: ${failure}`); + } +} + +function getWindowsTarCommand(): string { + const systemRoot = process.env.SystemRoot ?? process.env.WINDIR; + if (systemRoot) { + const systemTar = join(systemRoot, "System32", "tar.exe"); + if (existsSync(systemTar)) { + return systemTar; + } + } + return "tar.exe"; +} + +function extractZipArchive(archivePath: string, extractDir: string, assetName: string): void { + const failures: string[] = []; + + if (platform() === "win32") { + // Windows ships bsdtar as tar.exe, which supports zip files. Prefer the + // System32 binary over Git Bash's GNU tar, which does not handle zip archives. + const tarFailure = runExtractionCommand(getWindowsTarCommand(), ["xf", archivePath, "-C", extractDir]); + if (!tarFailure) return; + failures.push(tarFailure); + + const script = + "& { param($archive, $destination) $ErrorActionPreference = 'Stop'; Expand-Archive -LiteralPath $archive -DestinationPath $destination -Force }"; + const powershellFailure = runExtractionCommand("powershell.exe", [ + "-NoLogo", + "-NoProfile", + "-NonInteractive", + "-ExecutionPolicy", + "Bypass", + "-Command", + script, + archivePath, + extractDir, + ]); + if (!powershellFailure) return; + failures.push(powershellFailure); + } else { + const unzipFailure = runExtractionCommand("unzip", ["-q", archivePath, "-d", extractDir]); + if (!unzipFailure) return; + failures.push(unzipFailure); + + const tarFailure = runExtractionCommand("tar", ["xf", archivePath, "-C", extractDir]); + if (!tarFailure) return; + failures.push(tarFailure); + } + + throw new Error(`Failed to extract ${assetName}: ${failures.join("; ")}`); +} + // Download and install a tool async function downloadTool(tool: "fd" | "rg"): Promise { const config = TOOLS[tool]; @@ -197,13 +275,9 @@ async function downloadTool(tool: "fd" | "rg"): Promise { try { if (assetName.endsWith(".tar.gz")) { - const extractResult = spawnSync("tar", ["xzf", archivePath, "-C", extractDir], { stdio: "pipe" }); - if (extractResult.error || extractResult.status !== 0) { - const errMsg = extractResult.error?.message ?? extractResult.stderr?.toString().trim() ?? "unknown error"; - throw new Error(`Failed to extract ${assetName}: ${errMsg}`); - } + extractTarGzArchive(archivePath, extractDir, assetName); } else if (assetName.endsWith(".zip")) { - await extractZip(archivePath, { dir: extractDir }); + extractZipArchive(archivePath, extractDir, assetName); } else { throw new Error(`Unsupported archive format: ${assetName}`); } diff --git a/packages/coding-agent/src/utils/uuid.ts b/packages/coding-agent/src/utils/uuid.ts new file mode 100644 index 00000000..27b660ae --- /dev/null +++ b/packages/coding-agent/src/utils/uuid.ts @@ -0,0 +1,44 @@ +import { randomBytes } from "node:crypto"; + +let lastTimestamp = -Infinity; +let sequence = 0; + +export function uuidv7(): string { + const random = randomBytes(16); + const timestamp = Date.now(); + + if (timestamp > lastTimestamp) { + sequence = (random[6] << 23) | (random[7] << 16) | (random[8] << 8) | random[9]; + lastTimestamp = timestamp; + } else { + sequence = (sequence + 1) | 0; + if (sequence === 0) { + lastTimestamp++; + } + } + + const bytes = new Uint8Array(16); + bytes[0] = (lastTimestamp / 0x10000000000) & 0xff; + bytes[1] = (lastTimestamp / 0x100000000) & 0xff; + bytes[2] = (lastTimestamp / 0x1000000) & 0xff; + bytes[3] = (lastTimestamp / 0x10000) & 0xff; + bytes[4] = (lastTimestamp / 0x100) & 0xff; + bytes[5] = lastTimestamp & 0xff; + bytes[6] = 0x70 | ((sequence >>> 28) & 0x0f); + bytes[7] = (sequence >>> 20) & 0xff; + bytes[8] = 0x80 | ((sequence >>> 14) & 0x3f); + bytes[9] = (sequence >>> 6) & 0xff; + bytes[10] = ((sequence << 2) & 0xff) | (random[10] & 0x03); + bytes[11] = random[11]; + bytes[12] = random[12]; + bytes[13] = random[13]; + bytes[14] = random[14]; + bytes[15] = random[15]; + + return formatUuid(bytes); +} + +function formatUuid(bytes: Uint8Array): string { + const hex = Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")); + return `${hex.slice(0, 4).join("")}-${hex.slice(4, 6).join("")}-${hex.slice(6, 8).join("")}-${hex.slice(8, 10).join("")}-${hex.slice(10, 16).join("")}`; +} diff --git a/packages/coding-agent/test/ansi-utils.test.ts b/packages/coding-agent/test/ansi-utils.test.ts new file mode 100644 index 00000000..2d4aa27f --- /dev/null +++ b/packages/coding-agent/test/ansi-utils.test.ts @@ -0,0 +1,22 @@ +import { describe, expect, it } from "vitest"; +import { stripAnsi } from "../src/utils/ansi.js"; + +describe("stripAnsi", () => { + it("strips RIS without leaking the final byte", () => { + expect(stripAnsi("\x1bcdone")).toBe("done"); + }); + + it("strips single-byte ESC sequences without leaking final bytes", () => { + for (let code = "g".charCodeAt(0); code <= "m".charCodeAt(0); code++) { + expect(stripAnsi(`\x1b${String.fromCharCode(code)}ok`)).toBe("ok"); + } + for (let code = "r".charCodeAt(0); code <= "t".charCodeAt(0); code++) { + expect(stripAnsi(`\x1b${String.fromCharCode(code)}ok`)).toBe("ok"); + } + }); + + it("strips common ANSI sequences used in tool output", () => { + const input = "a\x1b[31mred\x1b[0m\x1b]8;;https://example.com\x07link\x1b]8;;\x07z"; + expect(stripAnsi(input)).toBe("aredlinkz"); + }); +}); diff --git a/packages/coding-agent/test/oauth-selector.test.ts b/packages/coding-agent/test/oauth-selector.test.ts index 9cd02eeb..26fd993b 100644 --- a/packages/coding-agent/test/oauth-selector.test.ts +++ b/packages/coding-agent/test/oauth-selector.test.ts @@ -1,5 +1,4 @@ import { setKeybindings } from "@earendil-works/pi-tui"; -import stripAnsi from "strip-ansi"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; import { AuthStorage } from "../src/core/auth-storage.js"; import { KeybindingsManager } from "../src/core/keybindings.js"; @@ -7,6 +6,7 @@ import { BUILT_IN_PROVIDER_DISPLAY_NAMES } from "../src/core/provider-display-na import { OAuthSelectorComponent } from "../src/modes/interactive/components/oauth-selector.js"; import { isApiKeyLoginProvider } from "../src/modes/interactive/interactive-mode.js"; import { initTheme } from "../src/modes/interactive/theme/theme.js"; +import { stripAnsi } from "../src/utils/ansi.js"; const originalOpenAiApiKey = process.env.OPENAI_API_KEY; diff --git a/packages/coding-agent/test/suite/regressions/3217-scoped-model-order.test.ts b/packages/coding-agent/test/suite/regressions/3217-scoped-model-order.test.ts index 72df1d95..e1b7cadd 100644 --- a/packages/coding-agent/test/suite/regressions/3217-scoped-model-order.test.ts +++ b/packages/coding-agent/test/suite/regressions/3217-scoped-model-order.test.ts @@ -1,10 +1,10 @@ import { setKeybindings, type TUI } from "@earendil-works/pi-tui"; -import stripAnsi from "strip-ansi"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; import { KeybindingsManager } from "../../../src/core/keybindings.js"; import { ModelSelectorComponent } from "../../../src/modes/interactive/components/model-selector.js"; import { ScopedModelsSelectorComponent } from "../../../src/modes/interactive/components/scoped-models-selector.js"; import { initTheme } from "../../../src/modes/interactive/theme/theme.js"; +import { stripAnsi } from "../../../src/utils/ansi.js"; import { createHarness, type Harness } from "../harness.js"; function createFakeTui(): TUI { diff --git a/packages/coding-agent/test/suite/regressions/4167-thinking-toggle-pending-tool-render.test.ts b/packages/coding-agent/test/suite/regressions/4167-thinking-toggle-pending-tool-render.test.ts index 14f72f7b..0dcd8a95 100644 --- a/packages/coding-agent/test/suite/regressions/4167-thinking-toggle-pending-tool-render.test.ts +++ b/packages/coding-agent/test/suite/regressions/4167-thinking-toggle-pending-tool-render.test.ts @@ -1,13 +1,13 @@ import type { AgentMessage } from "@earendil-works/pi-agent-core"; import type { AssistantMessage, ToolResultMessage, Usage } from "@earendil-works/pi-ai"; import { Container, Text, type TUI } from "@earendil-works/pi-tui"; -import stripAnsi from "strip-ansi"; import { beforeAll, describe, expect, test, vi } from "vitest"; import type { AgentSessionEvent } from "../../../src/core/agent-session.js"; import type { SessionContext } from "../../../src/core/session-manager.js"; import type { ToolExecutionComponent } from "../../../src/modes/interactive/components/tool-execution.js"; import { InteractiveMode } from "../../../src/modes/interactive/interactive-mode.js"; import { initTheme } from "../../../src/modes/interactive/theme/theme.js"; +import { stripAnsi } from "../../../src/utils/ansi.js"; const TOOL_CALL_ID = "tool-4167"; const TOOL_NAME = "slow_tool"; diff --git a/packages/coding-agent/test/tool-execution-component.test.ts b/packages/coding-agent/test/tool-execution-component.test.ts index 1e5d4b00..7013ba2a 100644 --- a/packages/coding-agent/test/tool-execution-component.test.ts +++ b/packages/coding-agent/test/tool-execution-component.test.ts @@ -1,6 +1,5 @@ import { join, resolve } from "node:path"; import { Text, type TUI } from "@earendil-works/pi-tui"; -import stripAnsi from "strip-ansi"; import { Type } from "typebox"; import { beforeAll, describe, expect, test } from "vitest"; import { getReadmePath } from "../src/config.js"; @@ -10,6 +9,7 @@ import { createReadTool, createReadToolDefinition } from "../src/core/tools/read import { createWriteToolDefinition } from "../src/core/tools/write.js"; import { ToolExecutionComponent } from "../src/modes/interactive/components/tool-execution.js"; import { initTheme } from "../src/modes/interactive/theme/theme.js"; +import { stripAnsi } from "../src/utils/ansi.js"; function createBaseToolDefinition(name = "custom_tool"): ToolDefinition { return { From e0b5d27af2fb4dc36301212b9eed35caed6826f3 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Wed, 13 May 2026 12:19:45 +0200 Subject: [PATCH 027/166] chore(deps): replace cli-highlight (#4468) --- package-lock.json | 158 ++---------------- packages/coding-agent/package.json | 2 +- .../src/modes/interactive/theme/theme.ts | 2 +- .../src/utils/highlight-js-lib-index.d.ts | 19 +++ packages/coding-agent/src/utils/html.ts | 51 ++++++ .../src/utils/syntax-highlight.ts | 146 ++++++++++++++++ .../test/syntax-highlight.test.ts | 48 ++++++ packages/coding-agent/tsconfig.build.json | 4 +- 8 files changed, 281 insertions(+), 149 deletions(-) create mode 100644 packages/coding-agent/src/utils/highlight-js-lib-index.d.ts create mode 100644 packages/coding-agent/src/utils/html.ts create mode 100644 packages/coding-agent/src/utils/syntax-highlight.ts create mode 100644 packages/coding-agent/test/syntax-highlight.test.ts diff --git a/package-lock.json b/package-lock.json index 06d8c051..1fba0423 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3967,10 +3967,6 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/any-promise": { - "version": "1.3.0", - "license": "MIT" - }, "node_modules/assertion-error": { "version": "2.0.1", "dev": true, @@ -4159,75 +4155,6 @@ "url": "https://polar.sh/cva" } }, - "node_modules/cli-highlight": { - "version": "2.1.11", - "license": "ISC", - "dependencies": { - "chalk": "^4.0.0", - "highlight.js": "^10.7.1", - "mz": "^2.4.0", - "parse5": "^5.1.1", - "parse5-htmlparser2-tree-adapter": "^6.0.0", - "yargs": "^16.0.0" - }, - "bin": { - "highlight": "bin/highlight" - }, - "engines": { - "node": ">=8.0.0", - "npm": ">=5.0.0" - } - }, - "node_modules/cli-highlight/node_modules/chalk": { - "version": "4.1.2", - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/cli-highlight/node_modules/supports-color": { - "version": "7.2.0", - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/cliui": { - "version": "7.0.4", - "license": "ISC", - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", - "wrap-ansi": "^7.0.0" - } - }, - "node_modules/cliui/node_modules/ansi-regex": { - "version": "5.0.1", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/cliui/node_modules/strip-ansi": { - "version": "6.0.1", - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/clsx": { "version": "2.1.1", "license": "MIT", @@ -4485,6 +4412,7 @@ }, "node_modules/emoji-regex": { "version": "8.0.0", + "dev": true, "license": "MIT" }, "node_modules/end-of-stream": { @@ -4562,6 +4490,7 @@ }, "node_modules/escalade": { "version": "3.2.0", + "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -4818,6 +4747,7 @@ }, "node_modules/get-caller-file": { "version": "2.0.5", + "dev": true, "license": "ISC", "engines": { "node": "6.* || 8.* || >= 10.*" @@ -5073,6 +5003,7 @@ }, "node_modules/is-fullwidth-code-point": { "version": "3.0.0", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -5640,15 +5571,6 @@ "version": "2.1.3", "license": "MIT" }, - "node_modules/mz": { - "version": "2.7.0", - "license": "MIT", - "dependencies": { - "any-promise": "^1.0.0", - "object-assign": "^4.0.1", - "thenify-all": "^1.0.0" - } - }, "node_modules/nanoid": { "version": "3.3.12", "dev": true, @@ -5743,13 +5665,6 @@ "node": ">=4" } }, - "node_modules/object-assign": { - "version": "4.1.1", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/ollama": { "version": "0.6.3", "license": "MIT", @@ -5839,21 +5754,6 @@ "version": "1.0.11", "license": "(MIT AND Zlib)" }, - "node_modules/parse5": { - "version": "5.1.1", - "license": "MIT" - }, - "node_modules/parse5-htmlparser2-tree-adapter": { - "version": "6.0.1", - "license": "MIT", - "dependencies": { - "parse5": "^6.0.1" - } - }, - "node_modules/parse5-htmlparser2-tree-adapter/node_modules/parse5": { - "version": "6.0.1", - "license": "MIT" - }, "node_modules/partial-json": { "version": "0.1.7", "license": "MIT" @@ -6146,6 +6046,7 @@ }, "node_modules/require-directory": { "version": "2.1.1", + "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -6488,6 +6389,7 @@ }, "node_modules/string-width": { "version": "4.2.3", + "dev": true, "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", @@ -6500,6 +6402,7 @@ }, "node_modules/string-width/node_modules/ansi-regex": { "version": "5.0.1", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -6507,6 +6410,7 @@ }, "node_modules/string-width/node_modules/strip-ansi": { "version": "6.0.1", + "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -6644,23 +6548,6 @@ "node": ">=6" } }, - "node_modules/thenify": { - "version": "3.3.1", - "license": "MIT", - "dependencies": { - "any-promise": "^1.0.0" - } - }, - "node_modules/thenify-all": { - "version": "1.6.0", - "license": "MIT", - "dependencies": { - "thenify": ">= 3.1.0 < 4" - }, - "engines": { - "node": ">=0.8" - } - }, "node_modules/tinybench": { "version": "2.9.0", "dev": true, @@ -7073,6 +6960,7 @@ }, "node_modules/wrap-ansi": { "version": "7.0.0", + "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", @@ -7088,6 +6976,7 @@ }, "node_modules/wrap-ansi/node_modules/ansi-regex": { "version": "5.0.1", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -7095,6 +6984,7 @@ }, "node_modules/wrap-ansi/node_modules/strip-ansi": { "version": "6.0.1", + "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -7141,6 +7031,7 @@ }, "node_modules/y18n": { "version": "5.0.8", + "dev": true, "license": "ISC", "engines": { "node": ">=10" @@ -7159,29 +7050,6 @@ "url": "https://github.com/sponsors/eemeli" } }, - "node_modules/yargs": { - "version": "16.2.0", - "license": "MIT", - "dependencies": { - "cliui": "^7.0.2", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.0", - "y18n": "^5.0.5", - "yargs-parser": "^20.2.2" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/yargs-parser": { - "version": "20.2.9", - "license": "ISC", - "engines": { - "node": ">=10" - } - }, "node_modules/zod": { "version": "4.4.3", "license": "MIT", @@ -7278,9 +7146,9 @@ "@earendil-works/pi-tui": "^0.74.0", "@silvia-odwyer/photon-node": "^0.3.4", "chalk": "^5.5.0", - "cli-highlight": "^2.1.11", "diff": "^8.0.2", "glob": "^13.0.1", + "highlight.js": "^10.7.3", "hosted-git-info": "^9.0.2", "ignore": "^7.0.5", "jiti": "^2.7.0", diff --git a/packages/coding-agent/package.json b/packages/coding-agent/package.json index 5f4ab9e5..394eb948 100644 --- a/packages/coding-agent/package.json +++ b/packages/coding-agent/package.json @@ -43,9 +43,9 @@ "@earendil-works/pi-tui": "^0.74.0", "@silvia-odwyer/photon-node": "^0.3.4", "chalk": "^5.5.0", - "cli-highlight": "^2.1.11", "diff": "^8.0.2", "glob": "^13.0.1", + "highlight.js": "^10.7.3", "hosted-git-info": "^9.0.2", "ignore": "^7.0.5", "jiti": "^2.7.0", diff --git a/packages/coding-agent/src/modes/interactive/theme/theme.ts b/packages/coding-agent/src/modes/interactive/theme/theme.ts index 30e4a233..1ff111b2 100644 --- a/packages/coding-agent/src/modes/interactive/theme/theme.ts +++ b/packages/coding-agent/src/modes/interactive/theme/theme.ts @@ -2,12 +2,12 @@ import * as fs from "node:fs"; import * as path from "node:path"; import type { EditorTheme, MarkdownTheme, SelectListTheme } from "@earendil-works/pi-tui"; import chalk from "chalk"; -import { highlight, supportsLanguage } from "cli-highlight"; import { type Static, Type } from "typebox"; import { Compile } from "typebox/compile"; import { getCustomThemesDir, getThemesDir } from "../../../config.js"; import type { SourceInfo } from "../../../core/source-info.js"; import { closeWatcher, watchWithErrorHandler } from "../../../utils/fs-watch.js"; +import { highlight, supportsLanguage } from "../../../utils/syntax-highlight.js"; // ============================================================================ // Types & Schema diff --git a/packages/coding-agent/src/utils/highlight-js-lib-index.d.ts b/packages/coding-agent/src/utils/highlight-js-lib-index.d.ts new file mode 100644 index 00000000..75e31da2 --- /dev/null +++ b/packages/coding-agent/src/utils/highlight-js-lib-index.d.ts @@ -0,0 +1,19 @@ +declare module "highlight.js/lib/index.js" { + interface HighlightResult { + value: string; + } + + interface HighlightOptions { + language: string; + ignoreIllegals?: boolean; + } + + interface HighlightJs { + highlight(code: string, options: HighlightOptions): HighlightResult; + highlightAuto(code: string, languageSubset?: string[]): HighlightResult; + getLanguage(name: string): unknown; + } + + const hljs: HighlightJs; + export default hljs; +} diff --git a/packages/coding-agent/src/utils/html.ts b/packages/coding-agent/src/utils/html.ts new file mode 100644 index 00000000..a13ad46f --- /dev/null +++ b/packages/coding-agent/src/utils/html.ts @@ -0,0 +1,51 @@ +export interface DecodedHtmlEntity { + text: string; + length: number; +} + +function decodeCodePoint(codePoint: number): string | undefined { + if (!Number.isInteger(codePoint) || codePoint < 0 || codePoint > 0x10ffff) { + return undefined; + } + return String.fromCodePoint(codePoint); +} + +export function decodeHtmlEntity(entity: string): string | undefined { + switch (entity) { + case "amp": + return "&"; + case "lt": + return "<"; + case "gt": + return ">"; + case "quot": + return '"'; + case "apos": + return "'"; + } + + if (entity.startsWith("#x") || entity.startsWith("#X")) { + return decodeCodePoint(Number.parseInt(entity.slice(2), 16)); + } + + if (entity.startsWith("#")) { + return decodeCodePoint(Number.parseInt(entity.slice(1), 10)); + } + + return undefined; +} + +export function decodeHtmlEntityAt(html: string, index: number): DecodedHtmlEntity | undefined { + const semicolonIndex = html.indexOf(";", index + 1); + if (semicolonIndex === -1 || semicolonIndex - index > 16) { + return undefined; + } + + const entity = html.slice(index + 1, semicolonIndex); + const decoded = decodeHtmlEntity(entity); + if (decoded === undefined) { + return undefined; + } + + return { text: decoded, length: semicolonIndex - index + 1 }; +} diff --git a/packages/coding-agent/src/utils/syntax-highlight.ts b/packages/coding-agent/src/utils/syntax-highlight.ts new file mode 100644 index 00000000..fc8a0281 --- /dev/null +++ b/packages/coding-agent/src/utils/syntax-highlight.ts @@ -0,0 +1,146 @@ +import hljs from "highlight.js/lib/index.js"; +import { decodeHtmlEntityAt } from "./html.js"; + +export type HighlightFormatter = (text: string) => string; +export type HighlightTheme = Partial>; + +export interface HighlightOptions { + language?: string; + ignoreIllegals?: boolean; + languageSubset?: string[]; + theme?: HighlightTheme; +} + +const SPAN_CLOSE = ""; +const HIGHLIGHT_CLASS_PREFIX = "hljs-"; + +function getScopeFromSpanTag(tag: string): string | undefined { + const match = /\sclass\s*=\s*(?:"([^"]*)"|'([^']*)')/.exec(tag); + const classValue = match?.[1] ?? match?.[2]; + if (!classValue) { + return undefined; + } + + for (const className of classValue.split(/\s+/)) { + if (className.startsWith(HIGHLIGHT_CLASS_PREFIX)) { + return className.slice(HIGHLIGHT_CLASS_PREFIX.length); + } + } + + return undefined; +} + +function getScopeFormatter(scope: string, theme: HighlightTheme): HighlightFormatter | undefined { + const exact = theme[scope]; + if (exact) { + return exact; + } + + const dotIndex = scope.indexOf("."); + if (dotIndex !== -1) { + const prefixFormatter = theme[scope.slice(0, dotIndex)]; + if (prefixFormatter) { + return prefixFormatter; + } + } + + const dashIndex = scope.indexOf("-"); + if (dashIndex !== -1) { + const prefixFormatter = theme[scope.slice(0, dashIndex)]; + if (prefixFormatter) { + return prefixFormatter; + } + } + + return undefined; +} + +function getActiveFormatter(scopes: Array, theme: HighlightTheme): HighlightFormatter | undefined { + for (let i = scopes.length - 1; i >= 0; i--) { + const scope = scopes[i]; + if (!scope) { + continue; + } + const formatter = getScopeFormatter(scope, theme); + if (formatter) { + return formatter; + } + } + return theme.default; +} + +function isSpanOpenTagStart(html: string, index: number): boolean { + if (!html.startsWith("" || nextChar === " " || nextChar === "\t" || nextChar === "\n" || nextChar === "\r"; +} + +export function renderHighlightedHtml(html: string, theme: HighlightTheme = {}): string { + let output = ""; + let textBuffer = ""; + const scopes: Array = []; + + const flushText = () => { + if (!textBuffer) { + return; + } + const formatter = getActiveFormatter(scopes, theme); + output += formatter ? formatter(textBuffer) : textBuffer; + textBuffer = ""; + }; + + let index = 0; + while (index < html.length) { + if (isSpanOpenTagStart(html, index)) { + const tagEndIndex = html.indexOf(">", index + 5); + if (tagEndIndex !== -1) { + flushText(); + const tag = html.slice(index, tagEndIndex + 1); + const scope = getScopeFromSpanTag(tag); + scopes.push(scope); + index = tagEndIndex + 1; + continue; + } + } + + if (html.startsWith(SPAN_CLOSE, index)) { + flushText(); + if (scopes.length > 0) { + scopes.pop(); + } + index += SPAN_CLOSE.length; + continue; + } + + if (html[index] === "&") { + const decoded = decodeHtmlEntityAt(html, index); + if (decoded) { + textBuffer += decoded.text; + index += decoded.length; + continue; + } + } + + textBuffer += html[index]; + index++; + } + + flushText(); + return output; +} + +export function highlight(code: string, options: HighlightOptions = {}): string { + const html = options.language + ? hljs.highlight(code, { + language: options.language, + ignoreIllegals: options.ignoreIllegals, + }).value + : hljs.highlightAuto(code, options.languageSubset).value; + return renderHighlightedHtml(html, options.theme); +} + +export function supportsLanguage(name: string): boolean { + return hljs.getLanguage(name) !== undefined; +} diff --git a/packages/coding-agent/test/syntax-highlight.test.ts b/packages/coding-agent/test/syntax-highlight.test.ts new file mode 100644 index 00000000..c5cc7ab1 --- /dev/null +++ b/packages/coding-agent/test/syntax-highlight.test.ts @@ -0,0 +1,48 @@ +import { describe, expect, it } from "vitest"; +import { highlight, renderHighlightedHtml, supportsLanguage } from "../src/utils/syntax-highlight.js"; + +describe("syntax highlight renderer", () => { + it("renders highlighted spans with the provided theme", () => { + const rendered = renderHighlightedHtml('const value', { + keyword: (text) => `[keyword:${text}]`, + }); + expect(rendered).toBe("[keyword:const] value"); + }); + + it("decodes HTML entities emitted by highlight.js", () => { + const rendered = renderHighlightedHtml("<tag attr="value">&#x41;A</tag>"); + expect(rendered).toBe('AA'); + }); + + it("inherits parent formatting for unmapped nested scopes", () => { + const interpolation = "$" + "{x}"; + const rendered = renderHighlightedHtml( + `a${interpolation}b`, + { + string: (text) => `[string:${text}]`, + }, + ); + expect(rendered).toBe(`[string:a][string:${interpolation}][string:b]`); + }); + + it("keeps parent formatting across unscoped nested spans", () => { + const rendered = renderHighlightedHtml('abc', { + string: (text) => `[string:${text}]`, + }); + expect(rendered).toBe("[string:a][string:b][string:c]"); + }); + + it("highlights code through highlight.js", () => { + expect(supportsLanguage("typescript")).toBe(true); + const rendered = highlight("const value = 1", { + language: "typescript", + ignoreIllegals: true, + theme: { + keyword: (text) => `[keyword:${text}]`, + number: (text) => `[number:${text}]`, + }, + }); + expect(rendered).toContain("[keyword:const]"); + expect(rendered).toContain("[number:1]"); + }); +}); diff --git a/packages/coding-agent/tsconfig.build.json b/packages/coding-agent/tsconfig.build.json index 695dd9ad..3437fd70 100644 --- a/packages/coding-agent/tsconfig.build.json +++ b/packages/coding-agent/tsconfig.build.json @@ -4,6 +4,6 @@ "outDir": "./dist", "rootDir": "./src" }, - "include": ["src/**/*.ts"], - "exclude": ["node_modules", "dist", "**/*.d.ts", "src/**/*.d.ts"] + "include": ["src/**/*.ts", "src/**/*.d.ts"], + "exclude": ["node_modules", "dist"] } From c08c4624619f30eb210a9d1fea16205909e845fd Mon Sep 17 00:00:00 2001 From: xu0o0 Date: Wed, 13 May 2026 20:37:48 +0800 Subject: [PATCH 028/166] fix(tui): place image correctly when viewport height < image height (#4461) fixes #4415 --- packages/tui/src/components/image.ts | 33 +++++++++++++++--------- packages/tui/test/terminal-image.test.ts | 12 ++++----- 2 files changed, 27 insertions(+), 18 deletions(-) diff --git a/packages/tui/src/components/image.ts b/packages/tui/src/components/image.ts index 74992b34..6b25df4e 100644 --- a/packages/tui/src/components/image.ts +++ b/packages/tui/src/components/image.ts @@ -82,19 +82,28 @@ export class Image implements Component { this.imageId = result.imageId; } - // Return `rows` lines so TUI accounts for image height. - // First (rows-1) lines are empty and cleared before the image is drawn. - // Last line: move cursor back up, draw the image, then move back down - // for Kitty (this component disables Kitty's terminal-side cursor movement) - // so TUI cursor accounting stays inside the scroll area. - lines = []; - for (let i = 0; i < result.rows - 1; i++) { - lines.push(""); + if (caps.images === "kitty") { + // For Kitty: C=1 prevents cursor movement. + // Don't need the cursor movement. + lines = [result.sequence]; + + // Return `rows` lines so TUI accounts for image height. + for (let i = 0; i < result.rows - 1; i++) { + lines.push(""); + } + } else { + // Return `rows` lines so TUI accounts for image height. + // First (rows-1) lines are empty and cleared before the image is drawn. + // Last line: move cursor back up, draw the image, then move back down + // so TUI cursor accounting stays inside the scroll area. + lines = []; + for (let i = 0; i < result.rows - 1; i++) { + lines.push(""); + } + const rowOffset = result.rows - 1; + const moveUp = rowOffset > 0 ? `\x1b[${rowOffset}A` : ""; + lines.push(moveUp + result.sequence); } - const rowOffset = result.rows - 1; - const moveUp = rowOffset > 0 ? `\x1b[${rowOffset}A` : ""; - const moveDown = caps.images === "kitty" && rowOffset > 0 ? `\x1b[${rowOffset}B` : ""; - lines.push(moveUp + result.sequence + moveDown); } else { const fallback = imageFallback(this.mimeType, this.dimensions, this.options.filename); lines = [this.theme.fallbackColor(fallback)]; diff --git a/packages/tui/test/terminal-image.test.ts b/packages/tui/test/terminal-image.test.ts index 58e018d2..e483b64f 100644 --- a/packages/tui/test/terminal-image.test.ts +++ b/packages/tui/test/terminal-image.test.ts @@ -312,7 +312,7 @@ describe("Kitty image cursor movement", () => { } }); - it("restores the cursor to the reserved image row after Kitty rendering", () => { + it("places image sequence on first line with empty padding rows", () => { setCapabilities({ images: "kitty", trueColor: true, hyperlinks: true }); setCellDimensions({ widthPx: 10, heightPx: 10 }); try { @@ -326,11 +326,11 @@ describe("Kitty image cursor movement", () => { const lines = image.render(4); const imageId = image.getImageId(); assert.strictEqual(typeof imageId, "number"); - assert.deepStrictEqual(lines.slice(0, -1), [""]); - assert.ok(lines[1].startsWith("\x1b[1A\x1b_G")); - assert.ok(lines[1].includes(",C=1,")); - assert.ok(lines[1].includes(`,i=${imageId}`)); - assert.ok(lines[1].endsWith("\x1b[1B")); + assert.ok(lines[0].startsWith("\x1b_G")); + assert.ok(lines[0].includes(",C=1,")); + assert.ok(lines[0].includes(`,i=${imageId}`)); + assert.ok(lines[0].endsWith("\x1b\\")); + assert.deepStrictEqual(lines.slice(1, lines.length), [""]); } finally { resetCapabilitiesCache(); setCellDimensions({ widthPx: 9, heightPx: 18 }); From e2b69a0bb1cf6ae7a641f478bfa4cfc9979dd37f Mon Sep 17 00:00:00 2001 From: Apoorv Saxena Date: Wed, 13 May 2026 19:16:26 +0530 Subject: [PATCH 029/166] fix(ai): mark inception/mercury-2 thinkingLevelMap.off as null Mercury 2 in instant mode (reasoning_effort: "none") disables tool calling. The openai-completions provider hardcodes {reasoning:{effort:"none"}} when no explicit reasoning level is passed and thinkingLevelMap.off isn't null (openai-completions.ts:575), so every caller that doesn't opt in to a level silently breaks Mercury 2's agentic use cases. Setting thinkingLevelMap.off = null on the Mercury 2 catalog entry causes the provider to omit the reasoning param entirely, letting Mercury 2's own default take over. Low/medium/high pass through verbatim; OpenRouter normalizes them to Mercury's vocabulary. Prefix-matched on "inception/mercury-2" so future Mercury 2 variants on OpenRouter inherit the fix. --- packages/ai/CHANGELOG.md | 1 + packages/ai/scripts/generate-models.ts | 7 +++++++ packages/ai/src/models.generated.ts | 1 + 3 files changed, 9 insertions(+) diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index 2be28e9b..7063ce82 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -10,6 +10,7 @@ - Fixed GitHub Copilot Claude test coverage to use the current Claude Sonnet 4.6 model ID. - Fixed OpenAI Responses requests for models that support disabling reasoning to send `reasoning.effort: "none"` when thinking is off. +- Fixed Inception Mercury 2 tool calling on OpenRouter by marking `off` as unsupported in `thinkingLevelMap`, so the openai-completions provider omits the reasoning param instead of defaulting to `{reasoning:{effort:"none"}}` (which puts Mercury 2 in instant mode, disabling tool calls). ## [0.74.0] - 2026-05-07 diff --git a/packages/ai/scripts/generate-models.ts b/packages/ai/scripts/generate-models.ts index 798d059c..72858c4e 100644 --- a/packages/ai/scripts/generate-models.ts +++ b/packages/ai/scripts/generate-models.ts @@ -240,6 +240,13 @@ function applyThinkingLevelMetadata(model: Model): void { if (model.provider === "openai-codex" && model.id === "gpt-5.1-codex-mini") { mergeThinkingLevelMap(model, { minimal: "medium", low: "medium", medium: "medium", high: "high" }); } + if (model.provider === "openrouter" && model.id.startsWith("inception/mercury-2")) { + // Mercury 2 in instant mode (reasoning_effort: "none") disables tool calling. + // Mark "off" unsupported so the openai-completions provider omits the reasoning param + // instead of defaulting to {reasoning:{effort:"none"}} (see openai-completions.ts:575). + // Pi's low/medium/high pass through verbatim; OpenRouter normalizes to Mercury's vocabulary. + mergeThinkingLevelMap(model, { off: null }); + } } function getAnthropicMessagesCompat(provider: string, modelId: string): AnthropicMessagesCompat | undefined { diff --git a/packages/ai/src/models.generated.ts b/packages/ai/src/models.generated.ts index 69559c78..5c3b6071 100644 --- a/packages/ai/src/models.generated.ts +++ b/packages/ai/src/models.generated.ts @@ -9731,6 +9731,7 @@ export const MODELS = { provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", reasoning: true, + thinkingLevelMap: {"off":null}, input: ["text"], cost: { input: 0.25, From 8bf2bfab58679d4d719c9d97030c0409f59f27a8 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Wed, 13 May 2026 15:52:33 +0200 Subject: [PATCH 030/166] fix(coding-agent): align stripAnsi with strip-ansi --- packages/coding-agent/src/utils/ansi.ts | 135 +++++++----------- packages/coding-agent/test/ansi-utils.test.ts | 88 ++++++++++++ 2 files changed, 142 insertions(+), 81 deletions(-) diff --git a/packages/coding-agent/src/utils/ansi.ts b/packages/coding-agent/src/utils/ansi.ts index 497ab3da..a95ded68 100644 --- a/packages/coding-agent/src/utils/ansi.ts +++ b/packages/coding-agent/src/utils/ansi.ts @@ -1,87 +1,60 @@ +/* + * Portions of this file are derived from: + * - ansi-regex (https://github.com/chalk/ansi-regex) + * - strip-ansi (https://github.com/chalk/strip-ansi) + * + * MIT License + * + * Copyright (c) Sindre Sorhus (https://sindresorhus.com) + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +function ansiRegex({ onlyFirst = false }: { onlyFirst?: boolean } = {}): RegExp { + // Valid string terminator sequences are BEL, ESC\, and 0x9c + const ST = "(?:\\u0007|\\u001B\\u005C|\\u009C)"; + + // OSC sequences only: ESC ] ... ST (non-greedy until the first ST) + const osc = `(?:\\u001B\\][\\s\\S]*?${ST})`; + + // CSI and related: ESC/C1, optional intermediates, optional params (supports ; and :) then final byte + const csi = "[\\u001B\\u009B][[\\]()#;?]*(?:\\d{1,4}(?:[;:]\\d{0,4})*)?[\\dA-PR-TZcf-nq-uy=><~]"; + + const pattern = `${osc}|${csi}`; + + return new RegExp(pattern, onlyFirst ? undefined : "g"); +} + +const regex = ansiRegex(); + export function stripAnsi(value: string): string { - let output = ""; - let index = 0; - - while (index < value.length) { - const code = value.charCodeAt(index); - - if (code === 0x1b) { - const next = value[index + 1]; - - if (next === "[") { - index = skipCsi(value, index + 2); - continue; - } - - if (next === "]" || next === "P" || next === "^" || next === "_") { - index = skipStringControl(value, index + 2); - continue; - } - - if (next && "()*+-./#".includes(next)) { - index = Math.min(index + 3, value.length); - continue; - } - - if (next && isEscFinalByte(next.charCodeAt(0))) { - index += 2; - continue; - } - } else if (code === 0x9b) { - index = skipCsi(value, index + 1); - continue; - } else if (code === 0x9d || code === 0x90 || code === 0x9e || code === 0x9f) { - index = skipStringControl(value, index + 1); - continue; - } - - output += value[index]; - index++; + if (typeof value !== "string") { + throw new TypeError(`Expected a \`string\`, got \`${typeof value}\``); } - return output; -} - -function isEscFinalByte(code: number): boolean { - return ( - (code >= 0x30 && code <= 0x39) || - (code >= 0x41 && code <= 0x50) || - (code >= 0x52 && code <= 0x54) || - code === 0x5a || - code === 0x63 || - (code >= 0x66 && code <= 0x6e) || - (code >= 0x71 && code <= 0x75) || - code === 0x79 || - code === 0x3d || - code === 0x3e || - code === 0x3c || - code === 0x7e - ); -} - -function skipCsi(value: string, start: number): number { - let index = start; - while (index < value.length) { - const code = value.charCodeAt(index); - index++; - if (code >= 0x40 && code <= 0x7e) { - return index; - } + // Fast path: ANSI codes require ESC (7-bit) or CSI (8-bit) introducer + if (!value.includes("\u001B") && !value.includes("\u009B")) { + return value; } - return value.length; -} -function skipStringControl(value: string, start: number): number { - let index = start; - while (index < value.length) { - const code = value.charCodeAt(index); - if (code === 0x07 || code === 0x9c) { - return index + 1; - } - if (code === 0x1b && value[index + 1] === "\\") { - return index + 2; - } - index++; - } - return value.length; + // Even though the regex is global, we don't need to reset the `.lastIndex` + // because unlike `.exec()` and `.test()`, `.replace()` does it automatically + // and doing it manually has a performance penalty. + return value.replace(regex, ""); } diff --git a/packages/coding-agent/test/ansi-utils.test.ts b/packages/coding-agent/test/ansi-utils.test.ts index 2d4aa27f..862f0401 100644 --- a/packages/coding-agent/test/ansi-utils.test.ts +++ b/packages/coding-agent/test/ansi-utils.test.ts @@ -1,7 +1,95 @@ import { describe, expect, it } from "vitest"; import { stripAnsi } from "../src/utils/ansi.js"; +function referenceAnsiRegex(): RegExp { + const ST = "(?:\\u0007|\\u001B\\u005C|\\u009C)"; + const osc = `(?:\\u001B\\][\\s\\S]*?${ST})`; + const csi = "[\\u001B\\u009B][[\\]()#;?]*(?:\\d{1,4}(?:[;:]\\d{0,4})*)?[\\dA-PR-TZcf-nq-uy=><~]"; + return new RegExp(`${osc}|${csi}`, "g"); +} + +const referenceRegex = referenceAnsiRegex(); + +function referenceStripAnsi(value: string): string { + if (!value.includes("\u001B") && !value.includes("\u009B")) { + return value; + } + return value.replace(referenceRegex, ""); +} + +function getCompatibilityInputs(): string[] { + const inputs = [ + "plain", + "a\x1b[31mred\x1b[0mz", + "a\x1b]8;;https://example.com\x07link\x1b]8;;\x07z", + "a\x1b]unterminated", + "a\x1b]funterminated", + "a\x1bPabc\x1b\\z", + "a\x1b^abc\x07z", + "a\x1b_abc\x9cz", + "a\x90abc\x9cz", + "a\x9dabc\x9cz", + "a\x9b31mred", + "a\x1b(0x", + "a\x1b*0x", + "a\x1b+c", + "a\x1b/0x", + "a\x1bcok", + "a\x1b\\ok", + ]; + const chars = [ + "a", + "f", + "0", + "1", + ";", + ":", + "[", + "]", + "(", + ")", + "#", + "?", + "m", + "P", + "_", + "\\", + "\x07", + "\x1b", + "\x9b", + "\x9c", + "\x90", + "\x9d", + ]; + + for (const char of chars) { + inputs.push(`x\x1b${char}y`); + inputs.push(`x\x9b${char}y`); + for (let index = 0; index < chars.length; index += 3) { + inputs.push(`x\x1b${char}${chars[index]}y`); + } + } + + return inputs; +} + describe("stripAnsi", () => { + it("matches chalk strip-ansi for generated compatibility inputs", () => { + for (const input of getCompatibilityInputs()) { + expect(stripAnsi(input)).toBe(referenceStripAnsi(input)); + } + }); + + it("throws the same TypeError as chalk strip-ansi for non-string values", () => { + const stripAnsiUnknown = stripAnsi as (value: unknown) => string; + + for (const value of [undefined, null, 123, {}, Object("x")]) { + const message = `Expected a \`string\`, got \`${typeof value}\``; + expect(() => stripAnsiUnknown(value)).toThrow(TypeError); + expect(() => stripAnsiUnknown(value)).toThrow(message); + } + }); + it("strips RIS without leaking the final byte", () => { expect(stripAnsi("\x1bcdone")).toBe("done"); }); From c00f1cf1302142c90a414e26ecd09f7988d97b6f Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Wed, 13 May 2026 15:53:53 +0200 Subject: [PATCH 031/166] fix(agent): correct uuidv7 sequence handling --- packages/agent/src/harness/session/uuid.ts | 6 +-- packages/agent/src/index.ts | 1 + .../agent/test/harness/session-uuid.test.ts | 50 +++++++++++++++++++ .../coding-agent/src/core/session-manager.ts | 3 +- packages/coding-agent/src/utils/uuid.ts | 44 ---------------- 5 files changed, 55 insertions(+), 49 deletions(-) create mode 100644 packages/agent/test/harness/session-uuid.test.ts delete mode 100644 packages/coding-agent/src/utils/uuid.ts diff --git a/packages/agent/src/harness/session/uuid.ts b/packages/agent/src/harness/session/uuid.ts index 27b660ae..c7e2896e 100644 --- a/packages/agent/src/harness/session/uuid.ts +++ b/packages/agent/src/harness/session/uuid.ts @@ -8,10 +8,10 @@ export function uuidv7(): string { const timestamp = Date.now(); if (timestamp > lastTimestamp) { - sequence = (random[6] << 23) | (random[7] << 16) | (random[8] << 8) | random[9]; + sequence = random[6] * 0x1000000 + random[7] * 0x10000 + random[8] * 0x100 + random[9]; lastTimestamp = timestamp; } else { - sequence = (sequence + 1) | 0; + sequence = (sequence + 1) >>> 0; if (sequence === 0) { lastTimestamp++; } @@ -28,7 +28,7 @@ export function uuidv7(): string { bytes[7] = (sequence >>> 20) & 0xff; bytes[8] = 0x80 | ((sequence >>> 14) & 0x3f); bytes[9] = (sequence >>> 6) & 0xff; - bytes[10] = ((sequence << 2) & 0xff) | (random[10] & 0x03); + bytes[10] = ((sequence & 0x3f) << 2) | (random[10] & 0x03); bytes[11] = random[11]; bytes[12] = random[12]; bytes[13] = random[13]; diff --git a/packages/agent/src/index.ts b/packages/agent/src/index.ts index 7b6018c1..293ce196 100644 --- a/packages/agent/src/index.ts +++ b/packages/agent/src/index.ts @@ -29,6 +29,7 @@ export * from "./harness/session/repo/jsonl.js"; export * from "./harness/session/repo/memory.js"; export * from "./harness/session/repo/shared.js"; export * from "./harness/session/session.js"; +export { uuidv7 } from "./harness/session/uuid.js"; export * from "./harness/skills.js"; export * from "./harness/system-prompt.js"; // Harness diff --git a/packages/agent/test/harness/session-uuid.test.ts b/packages/agent/test/harness/session-uuid.test.ts new file mode 100644 index 00000000..0c7f42e2 --- /dev/null +++ b/packages/agent/test/harness/session-uuid.test.ts @@ -0,0 +1,50 @@ +import { describe, expect, it, vi } from "vitest"; + +const randomBytesMock = vi.hoisted(() => vi.fn<(size: number) => Buffer>()); + +vi.mock("node:crypto", () => ({ + randomBytes: randomBytesMock, +})); + +import { uuidv7 } from "../../src/harness/session/uuid.js"; + +const UUID_V7_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/; +const TIMESTAMP = 0x0123456789ab; + +function parseTimestamp(uuid: string): number { + return Number.parseInt(uuid.replaceAll("-", "").slice(0, 12), 16); +} + +describe("uuidv7", () => { + it("uses the RFC 9562 layout and preserves monotonic order", () => { + randomBytesMock + .mockReturnValueOnce( + Buffer.from([0, 0, 0, 0, 0, 0, 0xff, 0xff, 0xff, 0xfe, 0x01, 0x11, 0x22, 0x33, 0x44, 0x55]), + ) + .mockReturnValueOnce(Buffer.alloc(16)) + .mockReturnValueOnce(Buffer.alloc(16)); + const dateNow = vi.spyOn(Date, "now").mockReturnValue(TIMESTAMP); + + try { + const first = uuidv7(); + const second = uuidv7(); + const third = uuidv7(); + + expect(first).toBe("01234567-89ab-7fff-bfff-f91122334455"); + expect(second).toBe("01234567-89ab-7fff-bfff-fc0000000000"); + expect(third).toBe("01234567-89ac-7000-8000-000000000000"); + expect(first).toMatch(UUID_V7_RE); + expect(second).toMatch(UUID_V7_RE); + expect(third).toMatch(UUID_V7_RE); + expect(parseTimestamp(first)).toBe(TIMESTAMP); + expect(parseTimestamp(second)).toBe(TIMESTAMP); + expect(parseTimestamp(third)).toBe(TIMESTAMP + 1); + expect(first < second).toBe(true); + expect(second < third).toBe(true); + expect(randomBytesMock).toHaveBeenCalledTimes(3); + expect(randomBytesMock).toHaveBeenCalledWith(16); + } finally { + dateNow.mockRestore(); + } + }); +}); diff --git a/packages/coding-agent/src/core/session-manager.ts b/packages/coding-agent/src/core/session-manager.ts index 60b111e0..5d8971c5 100644 --- a/packages/coding-agent/src/core/session-manager.ts +++ b/packages/coding-agent/src/core/session-manager.ts @@ -1,4 +1,4 @@ -import type { AgentMessage } from "@earendil-works/pi-agent-core"; +import { type AgentMessage, uuidv7 } from "@earendil-works/pi-agent-core"; import type { ImageContent, Message, TextContent } from "@earendil-works/pi-ai"; import { randomUUID } from "crypto"; import { @@ -16,7 +16,6 @@ import { import { readdir, readFile, stat } from "fs/promises"; import { join, resolve } from "path"; import { getAgentDir as getDefaultAgentDir, getSessionsDir } from "../config.js"; -import { uuidv7 } from "../utils/uuid.js"; import { type BashExecutionMessage, type CustomMessage, diff --git a/packages/coding-agent/src/utils/uuid.ts b/packages/coding-agent/src/utils/uuid.ts deleted file mode 100644 index 27b660ae..00000000 --- a/packages/coding-agent/src/utils/uuid.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { randomBytes } from "node:crypto"; - -let lastTimestamp = -Infinity; -let sequence = 0; - -export function uuidv7(): string { - const random = randomBytes(16); - const timestamp = Date.now(); - - if (timestamp > lastTimestamp) { - sequence = (random[6] << 23) | (random[7] << 16) | (random[8] << 8) | random[9]; - lastTimestamp = timestamp; - } else { - sequence = (sequence + 1) | 0; - if (sequence === 0) { - lastTimestamp++; - } - } - - const bytes = new Uint8Array(16); - bytes[0] = (lastTimestamp / 0x10000000000) & 0xff; - bytes[1] = (lastTimestamp / 0x100000000) & 0xff; - bytes[2] = (lastTimestamp / 0x1000000) & 0xff; - bytes[3] = (lastTimestamp / 0x10000) & 0xff; - bytes[4] = (lastTimestamp / 0x100) & 0xff; - bytes[5] = lastTimestamp & 0xff; - bytes[6] = 0x70 | ((sequence >>> 28) & 0x0f); - bytes[7] = (sequence >>> 20) & 0xff; - bytes[8] = 0x80 | ((sequence >>> 14) & 0x3f); - bytes[9] = (sequence >>> 6) & 0xff; - bytes[10] = ((sequence << 2) & 0xff) | (random[10] & 0x03); - bytes[11] = random[11]; - bytes[12] = random[12]; - bytes[13] = random[13]; - bytes[14] = random[14]; - bytes[15] = random[15]; - - return formatUuid(bytes); -} - -function formatUuid(bytes: Uint8Array): string { - const hex = Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")); - return `${hex.slice(0, 4).join("")}-${hex.slice(4, 6).join("")}-${hex.slice(6, 8).join("")}-${hex.slice(8, 10).join("")}-${hex.slice(10, 16).join("")}`; -} From e3faf4180114b8de227ab3d5bce76c76a18643e7 Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Wed, 13 May 2026 16:11:40 +0200 Subject: [PATCH 032/166] fix(tui): cap portrait image render height --- packages/tui/CHANGELOG.md | 4 ++ packages/tui/src/components/image.ts | 7 +++- packages/tui/src/terminal-image.ts | 49 ++++++++++++++++++------ packages/tui/test/terminal-image.test.ts | 34 ++++++++++++++++ 4 files changed, 82 insertions(+), 12 deletions(-) diff --git a/packages/tui/CHANGELOG.md b/packages/tui/CHANGELOG.md index feeb9c8e..204d6a39 100644 --- a/packages/tui/CHANGELOG.md +++ b/packages/tui/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Fixed + +- Fixed inline image rendering to cap portrait images by height instead of always scaling them to the configured maximum width. + ## [0.74.0] - 2026-05-07 ## [0.73.1] - 2026-05-07 diff --git a/packages/tui/src/components/image.ts b/packages/tui/src/components/image.ts index 6b25df4e..eb68757d 100644 --- a/packages/tui/src/components/image.ts +++ b/packages/tui/src/components/image.ts @@ -1,6 +1,7 @@ import { allocateImageId, getCapabilities, + getCellDimensions, getImageDimensions, type ImageDimensions, imageFallback, @@ -61,7 +62,10 @@ export class Image implements Component { return this.cachedLines; } - const maxWidth = Math.min(width - 2, this.options.maxWidthCells ?? 60); + const maxWidth = Math.max(1, Math.min(width - 2, this.options.maxWidthCells ?? 60)); + const cellDimensions = getCellDimensions(); + const defaultMaxHeight = Math.max(1, Math.ceil((maxWidth * cellDimensions.widthPx) / cellDimensions.heightPx)); + const maxHeight = this.options.maxHeightCells ?? defaultMaxHeight; const caps = getCapabilities(); let lines: string[]; @@ -72,6 +76,7 @@ export class Image implements Component { } const result = renderImage(this.base64Data, this.dimensions, { maxWidthCells: maxWidth, + maxHeightCells: maxHeight, imageId: this.imageId, moveCursor: false, }); diff --git a/packages/tui/src/terminal-image.ts b/packages/tui/src/terminal-image.ts index 6a84716c..b355f0f3 100644 --- a/packages/tui/src/terminal-image.ts +++ b/packages/tui/src/terminal-image.ts @@ -211,16 +211,43 @@ export function encodeITerm2( return `\x1b]1337;File=${params.join(";")}:${base64Data}\x07`; } +export interface ImageCellSize { + columns: number; + rows: number; +} + +export function calculateImageCellSize( + imageDimensions: ImageDimensions, + maxWidthCells: number, + maxHeightCells?: number, + cellDimensions: CellDimensions = { widthPx: 9, heightPx: 18 }, +): ImageCellSize { + const maxWidth = Math.max(1, Math.floor(maxWidthCells)); + const maxHeight = maxHeightCells === undefined ? undefined : Math.max(1, Math.floor(maxHeightCells)); + const imageWidth = Math.max(1, imageDimensions.widthPx); + const imageHeight = Math.max(1, imageDimensions.heightPx); + + const widthScale = (maxWidth * cellDimensions.widthPx) / imageWidth; + const heightScale = maxHeight === undefined ? widthScale : (maxHeight * cellDimensions.heightPx) / imageHeight; + const scale = Math.min(widthScale, heightScale); + + const scaledWidthPx = imageWidth * scale; + const scaledHeightPx = imageHeight * scale; + const columns = Math.ceil(scaledWidthPx / cellDimensions.widthPx); + const rows = Math.ceil(scaledHeightPx / cellDimensions.heightPx); + + return { + columns: Math.max(1, Math.min(maxWidth, columns)), + rows: Math.max(1, maxHeight === undefined ? rows : Math.min(maxHeight, rows)), + }; +} + export function calculateImageRows( imageDimensions: ImageDimensions, targetWidthCells: number, cellDimensions: CellDimensions = { widthPx: 9, heightPx: 18 }, ): number { - const targetWidthPx = targetWidthCells * cellDimensions.widthPx; - const scale = targetWidthPx / imageDimensions.widthPx; - const scaledHeightPx = imageDimensions.heightPx * scale; - const rows = Math.ceil(scaledHeightPx / cellDimensions.heightPx); - return Math.max(1, rows); + return calculateImageCellSize(imageDimensions, targetWidthCells, undefined, cellDimensions).rows; } export function getPngDimensions(base64Data: string): ImageDimensions | null { @@ -376,25 +403,25 @@ export function renderImage( } const maxWidth = options.maxWidthCells ?? 80; - const rows = calculateImageRows(imageDimensions, maxWidth, getCellDimensions()); + const size = calculateImageCellSize(imageDimensions, maxWidth, options.maxHeightCells, getCellDimensions()); if (caps.images === "kitty") { const sequence = encodeKitty(base64Data, { - columns: maxWidth, - rows, + columns: size.columns, + rows: size.rows, imageId: options.imageId, moveCursor: options.moveCursor, }); - return { sequence, rows, imageId: options.imageId }; + return { sequence, rows: size.rows, imageId: options.imageId }; } if (caps.images === "iterm2") { const sequence = encodeITerm2(base64Data, { - width: maxWidth, + width: size.columns, height: "auto", preserveAspectRatio: options.preserveAspectRatio ?? true, }); - return { sequence, rows }; + return { sequence, rows: size.rows }; } return null; diff --git a/packages/tui/test/terminal-image.test.ts b/packages/tui/test/terminal-image.test.ts index e483b64f..68c576bd 100644 --- a/packages/tui/test/terminal-image.test.ts +++ b/packages/tui/test/terminal-image.test.ts @@ -312,6 +312,40 @@ describe("Kitty image cursor movement", () => { } }); + it("honors maxHeightCells by reducing rendered width", () => { + setCapabilities({ images: "kitty", trueColor: true, hyperlinks: true }); + setCellDimensions({ widthPx: 10, heightPx: 10 }); + try { + const result = renderImage("AAAA", { widthPx: 10, heightPx: 100 }, { maxWidthCells: 10, maxHeightCells: 5 }); + assert.ok(result); + assert.strictEqual(result.rows, 5); + assert.ok(result.sequence.includes(",c=1,r=5")); + } finally { + resetCapabilitiesCache(); + setCellDimensions({ widthPx: 9, heightPx: 18 }); + } + }); + + it("caps Image component height to a square pixel box by default", () => { + setCapabilities({ images: "kitty", trueColor: true, hyperlinks: true }); + setCellDimensions({ widthPx: 10, heightPx: 20 }); + try { + const image = new Image( + "AAAA", + "image/png", + { fallbackColor: (value) => value }, + { maxWidthCells: 10 }, + { widthPx: 10, heightPx: 100 }, + ); + const lines = image.render(12); + assert.strictEqual(lines.length, 5); + assert.ok(lines[0].includes(",c=1,r=5")); + } finally { + resetCapabilitiesCache(); + setCellDimensions({ widthPx: 9, heightPx: 18 }); + } + }); + it("places image sequence on first line with empty padding rows", () => { setCapabilities({ images: "kitty", trueColor: true, hyperlinks: true }); setCellDimensions({ widthPx: 10, heightPx: 10 }); From a5cca409d84a3f592c1e0ac113ecf81ac8c2c862 Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Wed, 13 May 2026 16:12:18 +0200 Subject: [PATCH 033/166] refactor(ai): use HTTP proxy agents for Bedrock --- package-lock.json | 205 +------------------- packages/ai/CHANGELOG.md | 1 + packages/ai/package.json | 3 +- packages/ai/src/providers/amazon-bedrock.ts | 27 +-- packages/ai/src/utils/node-http-proxy.ts | 50 +++++ packages/ai/test/node-http-proxy.test.ts | 65 +++++++ 6 files changed, 127 insertions(+), 224 deletions(-) create mode 100644 packages/ai/src/utils/node-http-proxy.ts create mode 100644 packages/ai/test/node-http-proxy.test.ts diff --git a/package-lock.json b/package-lock.json index 1fba0423..f11dae5f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3637,10 +3637,6 @@ "vite": "^5.2.0 || ^6 || ^7 || ^8" } }, - "node_modules/@tootallnate/quickjs-emscripten": { - "version": "0.23.0", - "license": "MIT" - }, "node_modules/@types/chai": { "version": "5.2.3", "dev": true, @@ -3975,16 +3971,6 @@ "node": ">=12" } }, - "node_modules/ast-types": { - "version": "0.13.4", - "license": "MIT", - "dependencies": { - "tslib": "^2.0.1" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/balanced-match": { "version": "4.0.4", "license": "MIT", @@ -4010,13 +3996,6 @@ ], "license": "MIT" }, - "node_modules/basic-ftp": { - "version": "5.3.1", - "license": "MIT", - "engines": { - "node": ">=10.0.0" - } - }, "node_modules/bignumber.js": { "version": "9.3.1", "license": "MIT", @@ -4369,18 +4348,6 @@ "node": ">=4.0.0" } }, - "node_modules/degenerator": { - "version": "5.0.1", - "license": "MIT", - "dependencies": { - "ast-types": "^0.13.4", - "escodegen": "^2.1.0", - "esprima": "^4.0.1" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/detect-libc": { "version": "2.1.2", "dev": true, @@ -4496,43 +4463,6 @@ "node": ">=6" } }, - "node_modules/escodegen": { - "version": "2.1.0", - "license": "BSD-2-Clause", - "dependencies": { - "esprima": "^4.0.1", - "estraverse": "^5.2.0", - "esutils": "^2.0.2" - }, - "bin": { - "escodegen": "bin/escodegen.js", - "esgenerate": "bin/esgenerate.js" - }, - "engines": { - "node": ">=6.0" - }, - "optionalDependencies": { - "source-map": "~0.6.1" - } - }, - "node_modules/esprima": { - "version": "4.0.1", - "license": "BSD-2-Clause", - "bin": { - "esparse": "bin/esparse.js", - "esvalidate": "bin/esvalidate.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/estraverse": { - "version": "5.3.0", - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" - } - }, "node_modules/estree-walker": { "version": "3.0.3", "dev": true, @@ -4541,13 +4471,6 @@ "@types/estree": "^1.0.0" } }, - "node_modules/esutils": { - "version": "2.0.3", - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/execa": { "version": "1.0.0", "dev": true, @@ -4774,25 +4697,6 @@ "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" } }, - "node_modules/get-uri": { - "version": "6.0.5", - "license": "MIT", - "dependencies": { - "basic-ftp": "^5.0.2", - "data-uri-to-buffer": "^6.0.2", - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/get-uri/node_modules/data-uri-to-buffer": { - "version": "6.0.2", - "license": "MIT", - "engines": { - "node": ">= 14" - } - }, "node_modules/github-from-package": { "version": "0.0.0", "dev": true, @@ -4972,13 +4876,6 @@ "node": ">= 0.10" } }, - "node_modules/ip-address": { - "version": "10.2.0", - "license": "MIT", - "engines": { - "node": ">= 12" - } - }, "node_modules/is-core-module": { "version": "2.16.2", "dev": true, @@ -5593,13 +5490,6 @@ "dev": true, "license": "MIT" }, - "node_modules/netmask": { - "version": "2.1.1", - "license": "MIT", - "engines": { - "node": ">= 0.4.0" - } - }, "node_modules/nice-try": { "version": "1.0.5", "dev": true, @@ -5722,34 +5612,6 @@ "version": "0.12.0", "license": "MIT" }, - "node_modules/pac-proxy-agent": { - "version": "7.2.0", - "license": "MIT", - "dependencies": { - "@tootallnate/quickjs-emscripten": "^0.23.0", - "agent-base": "^7.1.2", - "debug": "^4.3.4", - "get-uri": "^6.0.1", - "http-proxy-agent": "^7.0.0", - "https-proxy-agent": "^7.0.6", - "pac-resolver": "^7.0.1", - "socks-proxy-agent": "^8.0.5" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/pac-resolver": { - "version": "7.0.1", - "license": "MIT", - "dependencies": { - "degenerator": "^5.0.0", - "netmask": "^2.0.2" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/pako": { "version": "1.0.11", "license": "(MIT AND Zlib)" @@ -5951,30 +5813,6 @@ "node": ">=12.0.0" } }, - "node_modules/proxy-agent": { - "version": "6.5.0", - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.2", - "debug": "^4.3.4", - "http-proxy-agent": "^7.0.1", - "https-proxy-agent": "^7.0.6", - "lru-cache": "^7.14.1", - "pac-proxy-agent": "^7.1.0", - "proxy-from-env": "^1.1.0", - "socks-proxy-agent": "^8.0.5" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/proxy-agent/node_modules/lru-cache": { - "version": "7.18.3", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, "node_modules/proxy-from-env": { "version": "1.1.0", "license": "MIT" @@ -6321,46 +6159,6 @@ "simple-concat": "^1.0.0" } }, - "node_modules/smart-buffer": { - "version": "4.2.0", - "license": "MIT", - "engines": { - "node": ">= 6.0.0", - "npm": ">= 3.0.0" - } - }, - "node_modules/socks": { - "version": "2.8.8", - "license": "MIT", - "dependencies": { - "ip-address": "^10.1.1", - "smart-buffer": "^4.2.0" - }, - "engines": { - "node": ">= 10.0.0", - "npm": ">= 3.0.0" - } - }, - "node_modules/socks-proxy-agent": { - "version": "8.0.5", - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.2", - "debug": "^4.3.4", - "socks": "^2.8.3" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/source-map": { - "version": "0.6.1", - "license": "BSD-3-Clause", - "optional": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/source-map-js": { "version": "1.2.1", "dev": true, @@ -7105,9 +6903,10 @@ "@aws-sdk/client-bedrock-runtime": "^3.1030.0", "@google/genai": "^1.40.0", "@mistralai/mistralai": "^2.2.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.6", "openai": "6.26.0", "partial-json": "^0.1.7", - "proxy-agent": "^6.5.0", "proxy-from-env": "^1.1.0", "typebox": "^1.1.24" }, diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index 2be28e9b..47c1b361 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -8,6 +8,7 @@ ### Fixed +- Fixed Bedrock proxy handling to preserve `NO_PROXY` exclusions while using HTTP(S)-only proxy agents. - Fixed GitHub Copilot Claude test coverage to use the current Claude Sonnet 4.6 model ID. - Fixed OpenAI Responses requests for models that support disabling reasoning to send `reasoning.effort: "none"` when thinking is off. diff --git a/packages/ai/package.json b/packages/ai/package.json index 9fce8e35..149da8c3 100644 --- a/packages/ai/package.json +++ b/packages/ai/package.json @@ -73,9 +73,10 @@ "@aws-sdk/client-bedrock-runtime": "^3.1030.0", "@google/genai": "^1.40.0", "@mistralai/mistralai": "^2.2.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.6", "openai": "6.26.0", "partial-json": "^0.1.7", - "proxy-agent": "^6.5.0", "proxy-from-env": "^1.1.0", "typebox": "^1.1.24" }, diff --git a/packages/ai/src/providers/amazon-bedrock.ts b/packages/ai/src/providers/amazon-bedrock.ts index 1812d3c8..7c5ac164 100644 --- a/packages/ai/src/providers/amazon-bedrock.ts +++ b/packages/ai/src/providers/amazon-bedrock.ts @@ -20,6 +20,7 @@ import { type ToolConfiguration, ToolResultStatus, } from "@aws-sdk/client-bedrock-runtime"; +import { NodeHttpHandler } from "@smithy/node-http-handler"; import type { DocumentType } from "@smithy/types"; import { calculateCost } from "../models.js"; import type { @@ -42,6 +43,7 @@ import type { } from "../types.js"; import { AssistantMessageEventStream } from "../utils/event-stream.js"; import { parseStreamingJson } from "../utils/json-parse.js"; +import { createHttpProxyAgentsForTarget } from "../utils/node-http-proxy.js"; import { sanitizeSurrogates } from "../utils/sanitize-unicode.js"; import { adjustMaxTokensForThinking, buildBaseOptions, clampReasoning } from "./simple-options.js"; import { transformMessages } from "./transform-messages.js"; @@ -156,30 +158,15 @@ export const streamBedrock: StreamFunction<"bedrock-converse-stream", BedrockOpt }; } - if ( - process.env.HTTP_PROXY || - process.env.HTTPS_PROXY || - process.env.NO_PROXY || - process.env.http_proxy || - process.env.https_proxy || - process.env.no_proxy - ) { - const nodeHttpHandler = await import("@smithy/node-http-handler"); - const proxyAgent = await import("proxy-agent"); - - const agent = new proxyAgent.ProxyAgent(); - + const proxyAgents = createHttpProxyAgentsForTarget(model.baseUrl); + if (proxyAgents) { // Bedrock runtime uses NodeHttp2Handler by default since v3.798.0, which is based // on `http2` module and has no support for http agent. - // Use NodeHttpHandler to support http agent. - config.requestHandler = new nodeHttpHandler.NodeHttpHandler({ - httpAgent: agent, - httpsAgent: agent, - }); + // Use NodeHttpHandler to support HTTP(S) proxy agents. + config.requestHandler = new NodeHttpHandler(proxyAgents); } else if (process.env.AWS_BEDROCK_FORCE_HTTP1 === "1") { // Some custom endpoints require HTTP/1.1 instead of HTTP/2 - const nodeHttpHandler = await import("@smithy/node-http-handler"); - config.requestHandler = new nodeHttpHandler.NodeHttpHandler(); + config.requestHandler = new NodeHttpHandler(); } } else { // Non-Node environment (browser): fall back to us-east-1 since diff --git a/packages/ai/src/utils/node-http-proxy.ts b/packages/ai/src/utils/node-http-proxy.ts new file mode 100644 index 00000000..22babf60 --- /dev/null +++ b/packages/ai/src/utils/node-http-proxy.ts @@ -0,0 +1,50 @@ +import type { Agent as HttpAgent } from "node:http"; +import type { Agent as HttpsAgent } from "node:https"; +import { createRequire } from "node:module"; +import { HttpProxyAgent } from "http-proxy-agent"; +import { HttpsProxyAgent } from "https-proxy-agent"; + +const require = createRequire(import.meta.url); +const { getProxyForUrl } = require("proxy-from-env") as { getProxyForUrl: (url: string) => string }; + +export interface NodeHttpProxyAgents { + httpAgent: HttpAgent; + httpsAgent: HttpsAgent; +} + +export const UNSUPPORTED_PROXY_PROTOCOL_MESSAGE = + "Unsupported proxy protocol. SOCKS and PAC proxy URLs are not supported; use an HTTP or HTTPS proxy URL."; + +export function resolveHttpProxyUrlForTarget(targetUrl: string | URL): URL | undefined { + const proxy = getProxyForUrl(targetUrl.toString()); + if (!proxy) { + return undefined; + } + + let proxyUrl: URL; + try { + proxyUrl = new URL(proxy); + } catch (error) { + throw new Error( + `Invalid proxy URL ${JSON.stringify(proxy)}: ${error instanceof Error ? error.message : String(error)}`, + ); + } + + if (proxyUrl.protocol !== "http:" && proxyUrl.protocol !== "https:") { + throw new Error(`${UNSUPPORTED_PROXY_PROTOCOL_MESSAGE} Got ${proxyUrl.protocol}`); + } + + return proxyUrl; +} + +export function createHttpProxyAgentsForTarget(targetUrl: string | URL): NodeHttpProxyAgents | undefined { + const proxyUrl = resolveHttpProxyUrlForTarget(targetUrl); + if (!proxyUrl) { + return undefined; + } + + return { + httpAgent: new HttpProxyAgent(proxyUrl), + httpsAgent: new HttpsProxyAgent(proxyUrl) as unknown as HttpsAgent, + }; +} diff --git a/packages/ai/test/node-http-proxy.test.ts b/packages/ai/test/node-http-proxy.test.ts new file mode 100644 index 00000000..0057d9bd --- /dev/null +++ b/packages/ai/test/node-http-proxy.test.ts @@ -0,0 +1,65 @@ +import { afterEach, describe, expect, it } from "vitest"; +import { resolveHttpProxyUrlForTarget, UNSUPPORTED_PROXY_PROTOCOL_MESSAGE } from "../src/utils/node-http-proxy.js"; + +const PROXY_ENV_KEYS = [ + "HTTP_PROXY", + "HTTPS_PROXY", + "NO_PROXY", + "ALL_PROXY", + "http_proxy", + "https_proxy", + "no_proxy", + "all_proxy", + "npm_config_http_proxy", + "npm_config_https_proxy", + "npm_config_proxy", + "npm_config_no_proxy", +] as const; + +const originalEnv = new Map(); +for (const key of PROXY_ENV_KEYS) { + originalEnv.set(key, process.env[key]); +} + +function resetProxyEnv(): void { + for (const key of PROXY_ENV_KEYS) { + delete process.env[key]; + } +} + +afterEach(() => { + resetProxyEnv(); + for (const [key, value] of originalEnv) { + if (value !== undefined) { + process.env[key] = value; + } + } +}); + +describe("node HTTP proxy resolution", () => { + it("respects NO_PROXY exclusions", () => { + resetProxyEnv(); + process.env.HTTPS_PROXY = "http://proxy.example:8080"; + process.env.NO_PROXY = "bedrock-runtime.us-east-1.amazonaws.com"; + + expect(resolveHttpProxyUrlForTarget("https://bedrock-runtime.us-east-1.amazonaws.com")).toBeUndefined(); + }); + + it("resolves HTTP and HTTPS proxy URLs", () => { + resetProxyEnv(); + process.env.HTTPS_PROXY = "http://proxy.example:8080"; + + expect(resolveHttpProxyUrlForTarget("https://bedrock-runtime.us-east-1.amazonaws.com")?.toString()).toBe( + "http://proxy.example:8080/", + ); + }); + + it("rejects SOCKS and PAC proxy URLs explicitly", () => { + resetProxyEnv(); + process.env.HTTPS_PROXY = "socks5://proxy.example:1080"; + + expect(() => resolveHttpProxyUrlForTarget("https://bedrock-runtime.us-east-1.amazonaws.com")).toThrow( + UNSUPPORTED_PROXY_PROTOCOL_MESSAGE, + ); + }); +}); From 7f30fb613623867997d412d754e4301b75e5d9dd Mon Sep 17 00:00:00 2001 From: "Mikhail f. Shiryaev" Date: Wed, 13 May 2026 18:42:05 +0200 Subject: [PATCH 034/166] Address edge-case with kitty protocol in wezterm MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit packages/tui/src/stdin-buffer.ts — in extractCompleteSequences, after finding \x1b\x1b as "complete" (legacy meta-key), check if the next character in the buffer would start a new escape sequence ([, ], O, P, _). If so, emit only the first \x1b and let the second ESC begin a new parse iteration. packages/tui/test/stdin-buffer.test.ts — three new cases in the Kitty section: - \x1b\x1b[27;129:3u (num_lock) splits into ["\x1b", "\x1b[27;129:3u"] - \x1b\x1b[27;1:3u (no num_lock) splits into ["\x1b", "\x1b[27;1:3u"] - \x1b\x1b alone (no following CSI) still emits as ["\x1b\x1b"] — preserves ctrl+alt+[ --- packages/tui/src/stdin-buffer.ts | 23 +++++++++++++++++++++++ packages/tui/test/stdin-buffer.test.ts | 20 ++++++++++++++++++++ 2 files changed, 43 insertions(+) diff --git a/packages/tui/src/stdin-buffer.ts b/packages/tui/src/stdin-buffer.ts index 4aaf4627..a8b0b847 100644 --- a/packages/tui/src/stdin-buffer.ts +++ b/packages/tui/src/stdin-buffer.ts @@ -205,6 +205,29 @@ function extractCompleteSequences(buffer: string): { sequences: string[]; remain const status = isCompleteSequence(candidate); if (status === "complete") { + // WezTerm with enable_kitty_keyboard sends the Escape key press as a + // raw '\x1b' byte (simple text path in encode_kitty, ignoring + // DISAMBIGUATE_ESCAPE_CODES) and the release as a full Kitty CSI-u + // sequence. These arrive concatenated as '\x1b\x1b[27;...u'. + // The buffer would normally treat '\x1b\x1b' as a complete meta-key + // sequence (ESC + single char), leaving '[27;...u' to be typed as + // plain text. If the character immediately following '\x1b\x1b' + // would begin a new escape sequence, emit only the first ESC and + // restart from the second. + if (candidate === "\x1b\x1b") { + const nextChar = remaining[seqEnd]; + if ( + nextChar === "[" || // CSI + nextChar === "]" || // OSC + nextChar === "O" || // SS3 + nextChar === "P" || // DCS + nextChar === "_" // APC + ) { + sequences.push(ESC); + pos += 1; + break; + } + } sequences.push(candidate); pos += seqEnd; break; diff --git a/packages/tui/test/stdin-buffer.test.ts b/packages/tui/test/stdin-buffer.test.ts index 549b15f2..4f4a0061 100644 --- a/packages/tui/test/stdin-buffer.test.ts +++ b/packages/tui/test/stdin-buffer.test.ts @@ -198,6 +198,26 @@ describe("StdinBuffer", () => { assert.deepStrictEqual(emittedSequences, ["\x1b[3;1:3~"]); }); + it("should split ESC+ESC+CSI into standalone ESC and the CSI sequence (WezTerm Escape key regression)", () => { + // WezTerm with enable_kitty_keyboard sends Escape key press as raw \x1b + // and the release as a full Kitty CSI-u sequence, concatenated. + // The buffer must not treat \x1b\x1b as a complete meta-key when the + // following byte starts a new escape sequence. + processInput("\x1b\x1b[27;129:3u"); + assert.deepStrictEqual(emittedSequences, ["\x1b", "\x1b[27;129:3u"]); + }); + + it("should split ESC+ESC+CSI with no modifier (no num_lock)", () => { + processInput("\x1b\x1b[27;1:3u"); + assert.deepStrictEqual(emittedSequences, ["\x1b", "\x1b[27;1:3u"]); + }); + + it("should still emit ESC+ESC as a single sequence when not followed by a new escape", () => { + // \x1b\x1b alone (no following CSI) stays as-is — e.g. ctrl+alt+[ + processInput("\x1b\x1b"); + assert.deepStrictEqual(emittedSequences, ["\x1b\x1b"]); + }); + it("should handle plain characters mixed with Kitty sequences", () => { // Plain 'a' followed by Kitty release processInput("a\x1b[97;1:3u"); From 0ae909316a9ede121e91d9830b17fe204d376ebd Mon Sep 17 00:00:00 2001 From: Ramiz Wachtler Date: Wed, 13 May 2026 19:26:08 +0200 Subject: [PATCH 035/166] fix(ai): honor retry-after for OpenAI Codex SSE retries - honor `retry-after-ms` and `retry-after` for OpenAI Codex SSE retries - add SSE retry coverage for millisecond, seconds, date, and fallback delays --- packages/ai/CHANGELOG.md | 1 + .../src/providers/openai-codex-responses.ts | 24 ++- packages/ai/test/openai-codex-stream.test.ts | 179 ++++++++++++++++-- 3 files changed, 184 insertions(+), 20 deletions(-) diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index 9561ce02..5e3df94c 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -12,6 +12,7 @@ - Fixed GitHub Copilot Claude test coverage to use the current Claude Sonnet 4.6 model ID. - Fixed OpenAI Responses requests for models that support disabling reasoning to send `reasoning.effort: "none"` when thinking is off. - Fixed Inception Mercury 2 tool calling on OpenRouter by marking `off` as unsupported in `thinkingLevelMap`, so the openai-completions provider omits the reasoning param instead of defaulting to `{reasoning:{effort:"none"}}` (which puts Mercury 2 in instant mode, disabling tool calls). +- Fixed OpenAI Codex SSE retries to honor `retry-after-ms` and `retry-after` headers before falling back to exponential backoff. ## [0.74.0] - 2026-05-07 diff --git a/packages/ai/src/providers/openai-codex-responses.ts b/packages/ai/src/providers/openai-codex-responses.ts index 64542b2c..dc8d8ef7 100644 --- a/packages/ai/src/providers/openai-codex-responses.ts +++ b/packages/ai/src/providers/openai-codex-responses.ts @@ -254,7 +254,29 @@ export const streamOpenAICodexResponses: StreamFunction<"openai-codex-responses" const errorText = await response.text(); if (attempt < MAX_RETRIES && isRetryableError(response.status, errorText)) { - const delayMs = BASE_DELAY_MS * 2 ** attempt; + let delayMs = BASE_DELAY_MS * 2 ** attempt; + + const retryAfterMs = response.headers.get("retry-after-ms"); + if (retryAfterMs !== null) { + const millis = Number(retryAfterMs); + if (Number.isFinite(millis)) { + delayMs = Math.max(0, millis); + } + } else { + const retryAfter = response.headers.get("retry-after"); + if (retryAfter) { + const seconds = Number(retryAfter); + if (Number.isFinite(seconds)) { + delayMs = Math.max(0, seconds * 1000); + } else { + const date = Date.parse(retryAfter); + if (!Number.isNaN(date)) { + delayMs = Math.max(0, date - Date.now()); + } + } + } + } + await sleep(delayMs, options?.signal); continue; } diff --git a/packages/ai/test/openai-codex-stream.test.ts b/packages/ai/test/openai-codex-stream.test.ts index f3d19896..65b88315 100644 --- a/packages/ai/test/openai-codex-stream.test.ts +++ b/packages/ai/test/openai-codex-stream.test.ts @@ -10,19 +10,17 @@ import { } from "../src/providers/openai-codex-responses.js"; import type { Context, Model } from "../src/types.js"; -const originalFetch = global.fetch; -const originalWebSocket = globalThis.WebSocket; const originalAgentDir = process.env.PI_CODING_AGENT_DIR; afterEach(() => { - global.fetch = originalFetch; - globalThis.WebSocket = originalWebSocket; + vi.unstubAllGlobals(); if (originalAgentDir === undefined) { delete process.env.PI_CODING_AGENT_DIR; } else { process.env.PI_CODING_AGENT_DIR = originalAgentDir; } resetOpenAICodexWebSocketDebugStats(); + vi.useRealTimers(); vi.restoreAllMocks(); }); @@ -155,7 +153,7 @@ describe("openai-codex streaming", () => { return new Response("not found", { status: 404 }); }); - global.fetch = fetchMock as typeof fetch; + vi.stubGlobal("fetch", fetchMock); const model: Model<"openai-codex-responses"> = { id: "gpt-5.1-codex", @@ -206,7 +204,7 @@ describe("openai-codex streaming", () => { }, }); - global.fetch = vi.fn(async (input: string | URL) => { + const fetchMock = vi.fn(async (input: string | URL) => { const url = typeof input === "string" ? input : input.toString(); if (url === "https://api.github.com/repos/openai/codex/releases/latest") { return new Response(JSON.stringify({ tag_name: "rust-v0.0.0" }), { status: 200 }); @@ -221,7 +219,8 @@ describe("openai-codex streaming", () => { }); } return new Response("not found", { status: 404 }); - }) as typeof fetch; + }); + vi.stubGlobal("fetch", fetchMock); const model: Model<"openai-codex-responses"> = { id: "gpt-5.1-codex", @@ -265,7 +264,7 @@ describe("openai-codex streaming", () => { }, }); - global.fetch = vi.fn(async (input: string | URL) => { + const fetchMock = vi.fn(async (input: string | URL) => { const url = typeof input === "string" ? input : input.toString(); if (url === "https://api.github.com/repos/openai/codex/releases/latest") { return new Response(JSON.stringify({ tag_name: "rust-v0.0.0" }), { status: 200 }); @@ -280,7 +279,8 @@ describe("openai-codex streaming", () => { }); } return new Response("not found", { status: 404 }); - }) as typeof fetch; + }); + vi.stubGlobal("fetch", fetchMock); const model: Model<"openai-codex-responses"> = { id: "gpt-5.1-codex", @@ -387,7 +387,7 @@ describe("openai-codex streaming", () => { return new Response("not found", { status: 404 }); }); - global.fetch = fetchMock as typeof fetch; + vi.stubGlobal("fetch", fetchMock); const model: Model<"openai-codex-responses"> = { id: "gpt-5.1-codex", @@ -425,7 +425,7 @@ describe("openai-codex streaming", () => { }); let requestedReasoning: unknown; - global.fetch = vi.fn(async (input: string | URL, init?: RequestInit) => { + const fetchMock = vi.fn(async (input: string | URL, init?: RequestInit) => { const url = typeof input === "string" ? input : input.toString(); if (url === "https://api.github.com/repos/openai/codex/releases/latest") { return new Response(JSON.stringify({ tag_name: "rust-v0.0.0" }), { status: 200 }); @@ -442,7 +442,8 @@ describe("openai-codex streaming", () => { }); } return new Response("not found", { status: 404 }); - }) as typeof fetch; + }); + vi.stubGlobal("fetch", fetchMock); const model: Model<"openai-codex-responses"> = { id: "gpt-5.5", @@ -536,7 +537,7 @@ describe("openai-codex streaming", () => { return new Response("not found", { status: 404 }); }); - global.fetch = fetchMock as typeof fetch; + vi.stubGlobal("fetch", fetchMock); const model: Model<"openai-codex-responses"> = { id: modelId, @@ -614,7 +615,7 @@ describe("openai-codex streaming", () => { }, }); - global.fetch = vi.fn(async (input: string | URL) => { + const fetchMock = vi.fn(async (input: string | URL) => { const url = typeof input === "string" ? input : input.toString(); if (url === "https://api.github.com/repos/openai/codex/releases/latest") { return new Response(JSON.stringify({ tag_name: "rust-v0.0.0" }), { status: 200 }); @@ -629,7 +630,8 @@ describe("openai-codex streaming", () => { }); } return new Response("not found", { status: 404 }); - }) as typeof fetch; + }); + vi.stubGlobal("fetch", fetchMock); const model: Model<"openai-codex-responses"> = { id: modelId, @@ -728,7 +730,7 @@ describe("openai-codex streaming", () => { return new Response("not found", { status: 404 }); }); - global.fetch = fetchMock as typeof fetch; + vi.stubGlobal("fetch", fetchMock); const model: Model<"openai-codex-responses"> = { id: "gpt-5.1-codex", @@ -756,7 +758,8 @@ describe("openai-codex streaming", () => { const token = mockToken(); const sentBodies: unknown[] = []; - global.fetch = vi.fn(async () => new Response("unexpected fetch", { status: 500 })) as typeof fetch; + const fetchMock = vi.fn(async () => new Response("unexpected fetch", { status: 500 })); + vi.stubGlobal("fetch", fetchMock); class MockWebSocket { private listeners = new Map void>>(); @@ -826,7 +829,7 @@ describe("openai-codex streaming", () => { } } - globalThis.WebSocket = MockWebSocket as unknown as typeof WebSocket; + vi.stubGlobal("WebSocket", MockWebSocket); const model: Model<"openai-codex-responses"> = { id: "gpt-5.1-codex", @@ -949,7 +952,7 @@ describe("openai-codex streaming", () => { } } - globalThis.WebSocket = MockWebSocket as unknown as typeof WebSocket; + vi.stubGlobal("WebSocket", MockWebSocket); const model: Model<"openai-codex-responses"> = { id: "gpt-5.1-codex", @@ -1005,4 +1008,142 @@ describe("openai-codex streaming", () => { lastPreviousResponseId: "resp_1", }); }); + + it.each([ + ["retry-after-ms", () => ({ "content-type": "application/json", "retry-after-ms": "1500" }), 1500], + ["retry-after seconds", () => ({ "content-type": "application/json", "retry-after": "60" }), 60_000], + [ + "retry-after HTTP date", + () => ({ "content-type": "application/json", "retry-after": new Date(Date.now() + 45_000).toUTCString() }), + 45_000, + ], + ] as const)("uses %s for SSE retries", async (_name, makeHeaders, expectedDelay) => { + vi.useFakeTimers(); + vi.setSystemTime(new Date("2026-05-13T00:00:00Z")); + const setTimeoutSpy = vi.spyOn(globalThis, "setTimeout"); + const token = mockToken(); + const encoder = new TextEncoder(); + const sse = buildSSEPayload({ status: "completed" }); + let codexRequests = 0; + + const fetchMock = vi.fn(async (input: string | URL) => { + const url = typeof input === "string" ? input : input.toString(); + if (url !== "https://chatgpt.com/backend-api/codex/responses") { + throw new Error(`Unexpected URL: ${url}`); + } + + codexRequests++; + if (codexRequests === 1) { + return new Response(JSON.stringify({ error: { code: "rate_limit_exceeded", message: "rate limited" } }), { + status: 429, + headers: makeHeaders(), + }); + } + + return new Response( + new ReadableStream({ + start(controller) { + controller.enqueue(encoder.encode(sse)); + controller.close(); + }, + }), + { status: 200, headers: { "content-type": "text/event-stream" } }, + ); + }); + vi.stubGlobal("fetch", fetchMock); + + const model: Model<"openai-codex-responses"> = { + id: "gpt-5.1-codex", + name: "GPT-5.1 Codex", + api: "openai-codex-responses", + provider: "openai-codex", + baseUrl: "https://chatgpt.com/backend-api", + reasoning: true, + input: ["text"], + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, + contextWindow: 400000, + maxTokens: 128000, + }; + const context: Context = { + systemPrompt: "You are a helpful assistant.", + messages: [{ role: "user", content: "Say hello", timestamp: Date.now() }], + }; + + const resultPromise = streamOpenAICodexResponses(model, context, { apiKey: token, transport: "sse" }).result(); + await vi.advanceTimersByTimeAsync(0); + expect(setTimeoutSpy).toHaveBeenCalledWith(expect.any(Function), expectedDelay); + + await vi.advanceTimersToNextTimerAsync(); + const result = await resultPromise; + expect(result.content.find((content) => content.type === "text")?.text).toBe("Hello"); + expect(codexRequests).toBe(2); + }); + + it("uses exponential backoff across repeated SSE retries without retry headers", async () => { + vi.useFakeTimers(); + vi.setSystemTime(new Date("2026-05-13T00:00:00Z")); + const setTimeoutSpy = vi.spyOn(globalThis, "setTimeout"); + const token = mockToken(); + const encoder = new TextEncoder(); + const sse = buildSSEPayload({ status: "completed" }); + let codexRequests = 0; + + const fetchMock = vi.fn(async (input: string | URL) => { + const url = typeof input === "string" ? input : input.toString(); + if (url !== "https://chatgpt.com/backend-api/codex/responses") { + throw new Error(`Unexpected URL: ${url}`); + } + + codexRequests++; + if (codexRequests <= 3) { + return new Response(JSON.stringify({ error: { code: "rate_limit_exceeded", message: "rate limited" } }), { + status: 429, + headers: { "content-type": "application/json" }, + }); + } + + return new Response( + new ReadableStream({ + start(controller) { + controller.enqueue(encoder.encode(sse)); + controller.close(); + }, + }), + { status: 200, headers: { "content-type": "text/event-stream" } }, + ); + }); + vi.stubGlobal("fetch", fetchMock); + + const model: Model<"openai-codex-responses"> = { + id: "gpt-5.1-codex", + name: "GPT-5.1 Codex", + api: "openai-codex-responses", + provider: "openai-codex", + baseUrl: "https://chatgpt.com/backend-api", + reasoning: true, + input: ["text"], + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, + contextWindow: 400000, + maxTokens: 128000, + }; + const context: Context = { + systemPrompt: "You are a helpful assistant.", + messages: [{ role: "user", content: "Say hello", timestamp: Date.now() }], + }; + + const resultPromise = streamOpenAICodexResponses(model, context, { apiKey: token, transport: "sse" }).result(); + await vi.advanceTimersByTimeAsync(0); + expect(setTimeoutSpy).toHaveBeenNthCalledWith(1, expect.any(Function), 1000); + + await vi.advanceTimersToNextTimerAsync(); + expect(setTimeoutSpy).toHaveBeenNthCalledWith(2, expect.any(Function), 2000); + + await vi.advanceTimersToNextTimerAsync(); + expect(setTimeoutSpy).toHaveBeenNthCalledWith(3, expect.any(Function), 4000); + + await vi.advanceTimersToNextTimerAsync(); + const result = await resultPromise; + expect(result.content.find((content) => content.type === "text")?.text).toBe("Hello"); + expect(codexRequests).toBe(4); + }); }); From 9fcf924ac9ec35ec5d1415fd97e3fc1ffae5f980 Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Thu, 14 May 2026 18:03:06 +0200 Subject: [PATCH 036/166] docs: add helper inlining rule --- AGENTS.md | 1 + 1 file changed, 1 insertion(+) diff --git a/AGENTS.md b/AGENTS.md index f9205e03..bf250bc4 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -11,6 +11,7 @@ - Read files in full before making wide-ranging changes, before editing files you have not already fully inspected, and when the user asks you to investigate or audit something. Do not rely only on search snippets for broad changes. - No `any` types unless absolutely necessary +- Single-line helper functions with a single call site are forbidden; inline them instead. - Check node_modules for external API type definitions instead of guessing - **NEVER use inline imports** - no `await import("./foo.js")`, no `import("pkg").Type` in type positions, no dynamic imports for types. Always use standard top-level imports. - NEVER remove or downgrade code to fix type errors from outdated dependencies; upgrade the dependency instead From 846906e4d132435a28aff3772376f5af29bcd607 Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Thu, 14 May 2026 18:26:29 +0200 Subject: [PATCH 037/166] refactor(agent): add result-based execution env --- packages/agent/docs/agent-harness.md | 42 +- packages/agent/src/harness/env/nodejs.ts | 359 +++++++++++++----- packages/agent/src/harness/execution-env.ts | 13 +- .../agent/src/harness/prompt-templates.ts | 103 ++--- packages/agent/src/harness/skills.ts | 149 ++++---- packages/agent/src/harness/types.ts | 122 ++++-- .../agent/src/harness/utils/shell-output.ts | 140 ++++--- .../test/harness/agent-harness-stream.test.ts | 2 +- .../agent/test/harness/agent-harness.test.ts | 2 +- .../agent/test/harness/nodejs-env.test.ts | 228 ++++++++--- .../test/harness/prompt-templates.test.ts | 2 +- packages/agent/test/harness/skills.test.ts | 2 +- packages/agent/test/scratch/simple.ts | 2 +- 13 files changed, 783 insertions(+), 383 deletions(-) diff --git a/packages/agent/docs/agent-harness.md b/packages/agent/docs/agent-harness.md index 5baccdff..37b14cb7 100644 --- a/packages/agent/docs/agent-harness.md +++ b/packages/agent/docs/agent-harness.md @@ -18,6 +18,12 @@ The intended rule is: A final lifecycle hardening pass should prove these guarantees with a broad listener/hook reentrancy test suite. +## Error handling + +The target error-handling model is `Result` for fallible harness operations instead of thrown exceptions. Implementations should catch backend/provider/filesystem exceptions at the boundary and normalize them into typed error results; callers should inspect returned results instead of relying on thrown exceptions. + +This is currently implemented for `ExecutionEnv`, `NodeExecutionEnv`, shell-output capture, and skill/prompt-template resource loading. Older harness/session/compaction APIs still throw in several paths; finishing that migration is tracked in the cleanup todo. + ## State model The harness separates state into four categories. @@ -90,7 +96,7 @@ Structural operations require `phase === "idle"` and synchronously set the phase - `compact` - `navigateTree` -Starting another structural operation while the harness is not idle throws. +Starting another structural operation while the harness is not idle currently throws. This should become a typed result failure as part of the error-handling cleanup. The following operations are allowed during a turn where appropriate: @@ -141,7 +147,7 @@ The low-level loop converts harness `ThinkingLevel` to provider `reasoning` at t No state refresh is needed on `agent_end` except flushing leftover pending session writes and clearing the operation phase. The exact `settled` event timing is still under review. -If the system-prompt callback throws while starting `prompt`, `skill`, or `promptFromTemplate`, the operation throws and the harness returns to idle. If it throws from the save-point snapshot created by `prepareNextTurn`, the low-level agent run records an assistant error message. +If the system-prompt callback throws while starting `prompt`, `skill`, or `promptFromTemplate`, the operation currently throws and the harness returns to idle. This should become a typed result failure as part of the error-handling cleanup. If it throws from the save-point snapshot created by `prepareNextTurn`, the low-level agent run records an assistant error message. ## Hooks and events @@ -307,7 +313,7 @@ Implemented so far: - `setTools(tools, activeToolNames?)` - `setActiveTools(toolNames)` -- invalid active tool names throw +- invalid active tool names currently throw; convert to result errors - generic common app tool shape via `AgentHarness` - `QueueMode` exported from `Agent` - `AgentHarnessOptions.steeringMode` / `followUpMode` @@ -355,7 +361,33 @@ Still needed: - Document or change timing for model/thinking/stream-option events that may fire before queued session entries persist while busy. - Audit `abort()` barrier semantics. -### 7. Later coding-agent migration plan +### 7. Complete `Result`/non-throwing harness cleanup + +Started. + +Implemented so far: + +- Added generic `Result` plus helpers. +- Updated `ExecutionEnv` and `NodeExecutionEnv` to return typed results for filesystem/process operations. +- Added `ExecutionEnv.appendFile()` for streaming append use cases. +- Updated skill and prompt-template loaders to consume `ExecutionEnv` results. +- Updated shell output capture to return a result and use `ExecutionEnv` instead of Node APIs directly, including full-output spill via `appendFile()`. +- Removed `NodeExecutionEnv` from the browser-safe `execution-env.ts` re-export; Node-specific callers import from `harness/env/nodejs.js`. +- Expanded `NodeExecutionEnv` tests for file operations, exec errors, aborts, callbacks, timeouts, and shell-output full-output spill. + +Still needed: + +- Remove remaining throws from `src/harness` APIs and helpers, except explicit adapter/test helpers such as `getOrThrow()`. +- Convert session storage/repo/session APIs to typed result returns. +- Convert structural `AgentHarness` operations to typed result returns for busy, missing-resource, auth, compaction, and branch-summary failures. +- Convert compaction helpers to typed result returns. +- Keep Node-specific APIs isolated under `src/harness/env/nodejs.ts` and Node-backed storage/session implementations, or move those implementations behind explicit Node-only entry points. +- Replace Node globals in generic harness utilities, especially `Buffer` usage in truncation utilities, with runtime-neutral implementations. +- Audit package exports so browser/generic-JS imports do not pull Node-only modules such as `NodeExecutionEnv` or JSONL storage. +- Keep expanding `ExecutionEnv` and shell-output contract tests as the API evolves, especially for non-Node implementations. +- Add tests proving harness APIs return `ok: false` instead of throwing for expected failure paths. + +### 8. Later coding-agent migration plan Not started. @@ -367,7 +399,7 @@ Still needed: - Preserve UI/session behavior outside core. - Move coding-agent stream/auth/retry/header behavior onto the harness stream configuration and provider hooks. -### 8. Final lifecycle hardening suite +### 9. Final lifecycle hardening suite Before treating `AgentHarness` as migration-ready, add a broad test suite that exercises listeners and hooks closing over the harness and calling public APIs during every relevant event. diff --git a/packages/agent/src/harness/env/nodejs.ts b/packages/agent/src/harness/env/nodejs.ts index d4b6c0a9..6e79cf2c 100644 --- a/packages/agent/src/harness/env/nodejs.ts +++ b/packages/agent/src/harness/env/nodejs.ts @@ -1,33 +1,59 @@ import { spawn } from "node:child_process"; import { randomUUID } from "node:crypto"; import { constants } from "node:fs"; -import { access, lstat, mkdir, mkdtemp, readdir, readFile, realpath, rm, writeFile } from "node:fs/promises"; +import { + access, + appendFile, + lstat, + mkdir, + mkdtemp, + readdir, + readFile, + realpath, + rm, + writeFile, +} from "node:fs/promises"; import { tmpdir } from "node:os"; import { isAbsolute, join, resolve } from "node:path"; -import { type ExecutionEnv, FileError, type FileInfo, type FileKind } from "../types.js"; +import { + type ExecutionEnv, + ExecutionError, + err, + FileError, + type FileInfo, + type FileKind, + ok, + type Result, +} from "../types.js"; function resolvePath(cwd: string, path: string): string { return isAbsolute(path) ? path : resolve(cwd, path); } -function fileKindFromStats(stats: { isFile(): boolean; isDirectory(): boolean; isSymbolicLink(): boolean }): FileKind { +function fileKindFromStats(stats: { + isFile(): boolean; + isDirectory(): boolean; + isSymbolicLink(): boolean; +}): FileKind | undefined { if (stats.isFile()) return "file"; if (stats.isDirectory()) return "directory"; if (stats.isSymbolicLink()) return "symlink"; - throw new FileError("invalid", "Unsupported file type"); + return undefined; } function fileInfoFromStats( path: string, stats: { isFile(): boolean; isDirectory(): boolean; isSymbolicLink(): boolean; size: number; mtimeMs: number }, -): FileInfo { - return { +): Result { + const kind = fileKindFromStats(stats); + if (!kind) return err(new FileError("invalid", "Unsupported file type", path)); + return ok({ name: path.replace(/\/+$/, "").split("/").pop() ?? path, path, - kind: fileKindFromStats(stats), + kind, size: stats.size, mtimeMs: stats.mtimeMs, - }; + }); } function isNodeError(error: unknown): error is NodeJS.ErrnoException { @@ -39,20 +65,26 @@ function toFileError(error: unknown, path?: string): FileError { if (isNodeError(error)) { const message = error.message; switch (error.code) { + case "ABORT_ERR": + return new FileError("aborted", message, path, error); case "ENOENT": - return new FileError("not_found", message, path, { cause: error }); + return new FileError("not_found", message, path, error); case "EACCES": case "EPERM": - return new FileError("permission_denied", message, path, { cause: error }); + return new FileError("permission_denied", message, path, error); case "ENOTDIR": - return new FileError("not_directory", message, path, { cause: error }); + return new FileError("not_directory", message, path, error); case "EISDIR": - return new FileError("is_directory", message, path, { cause: error }); + return new FileError("is_directory", message, path, error); case "EINVAL": - return new FileError("invalid", message, path, { cause: error }); + return new FileError("invalid", message, path, error); } } - return new FileError("unknown", error instanceof Error ? error.message : String(error), path, { cause: error }); + return new FileError("unknown", error instanceof Error ? error.message : String(error), path, error); +} + +function abortResult(signal: AbortSignal | undefined, path?: string): Result | undefined { + return signal?.aborted ? err(new FileError("aborted", "aborted", path)) : undefined; } async function pathExists(path: string): Promise { @@ -71,7 +103,13 @@ async function runCommand( ): Promise<{ stdout: string; status: number | null }> { return await new Promise((resolve) => { let stdout = ""; - const child = spawn(command, args, { stdio: ["ignore", "pipe", "ignore"] }); + let child: ReturnType; + try { + child = spawn(command, args, { stdio: ["ignore", "pipe", "ignore"] }); + } catch { + resolve({ stdout: "", status: null }); + return; + } const timeout = setTimeout(() => { if (child.pid) killProcessTree(child.pid); }, timeoutMs); @@ -100,12 +138,14 @@ async function findBashOnPath(): Promise { return firstMatch && (await pathExists(firstMatch)) ? firstMatch : null; } -async function getShellConfig(customShellPath?: string): Promise<{ shell: string; args: string[] }> { +async function getShellConfig( + customShellPath?: string, +): Promise> { if (customShellPath) { if (await pathExists(customShellPath)) { - return { shell: customShellPath, args: ["-c"] }; + return ok({ shell: customShellPath, args: ["-c"] }); } - throw new Error(`Custom shell path not found: ${customShellPath}`); + return err(new ExecutionError("shell_unavailable", `Custom shell path not found: ${customShellPath}`)); } if (process.platform === "win32") { const candidates: string[] = []; @@ -115,24 +155,24 @@ async function getShellConfig(customShellPath?: string): Promise<{ shell: string if (programFilesX86) candidates.push(`${programFilesX86}\\Git\\bin\\bash.exe`); for (const candidate of candidates) { if (await pathExists(candidate)) { - return { shell: candidate, args: ["-c"] }; + return ok({ shell: candidate, args: ["-c"] }); } } const bashOnPath = await findBashOnPath(); if (bashOnPath) { - return { shell: bashOnPath, args: ["-c"] }; + return ok({ shell: bashOnPath, args: ["-c"] }); } - throw new Error("No bash shell found"); + return err(new ExecutionError("shell_unavailable", "No bash shell found")); } if (await pathExists("/bin/bash")) { - return { shell: "/bin/bash", args: ["-c"] }; + return ok({ shell: "/bin/bash", args: ["-c"] }); } const bashOnPath = await findBashOnPath(); if (bashOnPath) { - return { shell: bashOnPath, args: ["-c"] }; + return ok({ shell: bashOnPath, args: ["-c"] }); } - return { shell: "sh", args: ["-c"] }; + return ok({ shell: "sh", args: ["-c"] }); } function getShellEnv(baseEnv?: NodeJS.ProcessEnv, extraEnv?: Record): NodeJS.ProcessEnv { @@ -184,46 +224,69 @@ export class NodeExecutionEnv implements ExecutionEnv { cwd?: string; env?: Record; timeout?: number; - signal?: AbortSignal; + abortSignal?: AbortSignal; onStdout?: (chunk: string) => void; onStderr?: (chunk: string) => void; }, - ): Promise<{ stdout: string; stderr: string; exitCode: number }> { - const cwd = options?.cwd ? resolvePath(this.cwd, options.cwd) : this.cwd; - const { shell, args } = await getShellConfig(this.shellPath); + ): Promise> { + if (options?.abortSignal?.aborted) return err(new ExecutionError("aborted", "aborted")); - return await new Promise((resolvePromise, reject) => { + const cwd = options?.cwd ? resolvePath(this.cwd, options.cwd) : this.cwd; + const shellConfig = await getShellConfig(this.shellPath); + if (!shellConfig.ok) return shellConfig; + + return await new Promise((resolvePromise) => { let stdout = ""; let stderr = ""; let settled = false; let timedOut = false; - const child = spawn(shell, [...args, command], { - cwd, - detached: process.platform !== "win32", - env: getShellEnv(this.shellEnv, options?.env), - stdio: ["ignore", "pipe", "pipe"], - }); + let callbackError: ExecutionError | undefined; + let child: ReturnType | undefined; + let timeoutId: ReturnType | undefined; - const timeoutId = + const onAbort = () => { + if (child?.pid) { + killProcessTree(child.pid); + } + }; + + const settle = (result: Result<{ stdout: string; stderr: string; exitCode: number }, ExecutionError>) => { + if (timeoutId) clearTimeout(timeoutId); + if (options?.abortSignal) options.abortSignal.removeEventListener("abort", onAbort); + if (settled) return; + settled = true; + resolvePromise(result); + }; + + try { + child = spawn(shellConfig.value.shell, [...shellConfig.value.args, command], { + cwd, + detached: process.platform !== "win32", + env: getShellEnv(this.shellEnv, options?.env), + stdio: ["ignore", "pipe", "pipe"], + }); + } catch (error) { + settle( + err(new ExecutionError("spawn_error", error instanceof Error ? error.message : String(error), error)), + ); + return; + } + + timeoutId = typeof options?.timeout === "number" ? setTimeout(() => { timedOut = true; - if (child.pid) { + if (child?.pid) { killProcessTree(child.pid); } }, options.timeout * 1000) : undefined; - const onAbort = () => { - if (child.pid) { - killProcessTree(child.pid); - } - }; - if (options?.signal) { - if (options.signal.aborted) { + if (options?.abortSignal) { + if (options.abortSignal.aborted) { onAbort(); } else { - options.signal.addEventListener("abort", onAbort, { once: true }); + options.abortSignal.addEventListener("abort", onAbort, { once: true }); } } @@ -231,137 +294,231 @@ export class NodeExecutionEnv implements ExecutionEnv { child.stderr?.setEncoding("utf8"); child.stdout?.on("data", (chunk: string) => { stdout += chunk; - options?.onStdout?.(chunk); + try { + options?.onStdout?.(chunk); + } catch (error) { + callbackError = new ExecutionError( + "unknown", + error instanceof Error ? error.message : String(error), + error, + ); + onAbort(); + } }); child.stderr?.on("data", (chunk: string) => { stderr += chunk; - options?.onStderr?.(chunk); + try { + options?.onStderr?.(chunk); + } catch (error) { + callbackError = new ExecutionError( + "unknown", + error instanceof Error ? error.message : String(error), + error, + ); + onAbort(); + } }); child.on("error", (error) => { - if (timeoutId) clearTimeout(timeoutId); - if (options?.signal) options.signal.removeEventListener("abort", onAbort); - if (settled) return; - settled = true; - reject(error); + settle(err(new ExecutionError("spawn_error", error.message, error))); }); child.on("close", (code) => { - if (timeoutId) clearTimeout(timeoutId); - if (options?.signal) options.signal.removeEventListener("abort", onAbort); - if (settled) return; - settled = true; - if (options?.signal?.aborted) { - reject(new Error("aborted")); + if (callbackError) { + settle(err(callbackError)); + return; + } + if (options?.abortSignal?.aborted) { + settle(err(new ExecutionError("aborted", "aborted"))); return; } if (timedOut) { - reject(new Error(`timeout:${options?.timeout}`)); + settle(err(new ExecutionError("timeout", `timeout:${options?.timeout}`))); return; } - resolvePromise({ stdout, stderr, exitCode: code ?? 0 }); + settle(ok({ stdout, stderr, exitCode: code ?? 0 })); }); }); } - async readTextFile(path: string): Promise { + async readTextFile(path: string, abortSignal?: AbortSignal): Promise> { const resolved = resolvePath(this.cwd, path); + const aborted = abortResult(abortSignal, resolved); + if (aborted) return aborted; try { - return await readFile(resolved, "utf8"); + return ok(await readFile(resolved, { encoding: "utf8", signal: abortSignal })); } catch (error) { - throw toFileError(error, resolved); + return err(toFileError(error, resolved)); } } - async readBinaryFile(path: string): Promise { + async readBinaryFile(path: string, abortSignal?: AbortSignal): Promise> { const resolved = resolvePath(this.cwd, path); + const aborted = abortResult(abortSignal, resolved); + if (aborted) return aborted; try { - return await readFile(resolved); + return ok(await readFile(resolved, { signal: abortSignal })); } catch (error) { - throw toFileError(error, resolved); + return err(toFileError(error, resolved)); } } - async writeFile(path: string, content: string | Uint8Array): Promise { + async writeFile( + path: string, + content: string | Uint8Array, + abortSignal?: AbortSignal, + ): Promise> { const resolved = resolvePath(this.cwd, path); + const aborted = abortResult(abortSignal, resolved); + if (aborted) return aborted; try { await mkdir(resolve(resolved, ".."), { recursive: true }); - await writeFile(resolved, content); + const afterMkdirAbort = abortResult(abortSignal, resolved); + if (afterMkdirAbort) return afterMkdirAbort; + await writeFile(resolved, content, { signal: abortSignal }); + return ok(undefined); } catch (error) { - throw toFileError(error, resolved); + return err(toFileError(error, resolved)); } } - async fileInfo(path: string): Promise { + async appendFile( + path: string, + content: string | Uint8Array, + abortSignal?: AbortSignal, + ): Promise> { const resolved = resolvePath(this.cwd, path); + const aborted = abortResult(abortSignal, resolved); + if (aborted) return aborted; + try { + await mkdir(resolve(resolved, ".."), { recursive: true }); + const afterMkdirAbort = abortResult(abortSignal, resolved); + if (afterMkdirAbort) return afterMkdirAbort; + await appendFile(resolved, content); + const afterAppendAbort = abortResult(abortSignal, resolved); + if (afterAppendAbort) return afterAppendAbort; + return ok(undefined); + } catch (error) { + return err(toFileError(error, resolved)); + } + } + + async fileInfo(path: string, abortSignal?: AbortSignal): Promise> { + const resolved = resolvePath(this.cwd, path); + const aborted = abortResult(abortSignal, resolved); + if (aborted) return aborted; try { return fileInfoFromStats(resolved, await lstat(resolved)); } catch (error) { - throw toFileError(error, resolved); + return err(toFileError(error, resolved)); } } - async listDir(path: string): Promise { + async listDir(path: string, abortSignal?: AbortSignal): Promise> { const resolved = resolvePath(this.cwd, path); + const aborted = abortResult(abortSignal, resolved); + if (aborted) return aborted; try { const entries = await readdir(resolved, { withFileTypes: true }); const infos: FileInfo[] = []; for (const entry of entries) { + const loopAbort = abortResult(abortSignal, resolved); + if (loopAbort) return loopAbort; const entryPath = resolve(resolved, entry.name); try { - infos.push(fileInfoFromStats(entryPath, await lstat(entryPath))); + const info = fileInfoFromStats(entryPath, await lstat(entryPath)); + if (info.ok) infos.push(info.value); } catch (error) { - if (error instanceof FileError && error.code === "invalid") continue; - throw error; + return err(toFileError(error, entryPath)); } } - return infos; + return ok(infos); } catch (error) { - throw toFileError(error, resolved); + return err(toFileError(error, resolved)); } } - async realPath(path: string): Promise { + async realPath(path: string, abortSignal?: AbortSignal): Promise> { const resolved = resolvePath(this.cwd, path); + const aborted = abortResult(abortSignal, resolved); + if (aborted) return aborted; try { - return await realpath(resolved); + return ok(await realpath(resolved)); } catch (error) { - throw toFileError(error, resolved); + return err(toFileError(error, resolved)); } } - async exists(path: string): Promise { - try { - await this.fileInfo(path); - return true; - } catch (error) { - if (error instanceof FileError && error.code === "not_found") return false; - throw error; - } + async exists(path: string, abortSignal?: AbortSignal): Promise> { + const result = await this.fileInfo(path, abortSignal); + if (result.ok) return ok(true); + if (result.error.code === "not_found") return ok(false); + return err(result.error); } - async createDir(path: string, options?: { recursive?: boolean }): Promise { - await mkdir(resolvePath(this.cwd, path), { recursive: options?.recursive }); - } - - async remove(path: string, options?: { recursive?: boolean; force?: boolean }): Promise { + async createDir( + path: string, + options?: { recursive?: boolean; abortSignal?: AbortSignal }, + ): Promise> { const resolved = resolvePath(this.cwd, path); + const aborted = abortResult(options?.abortSignal, resolved); + if (aborted) return aborted; + try { + await mkdir(resolved, { recursive: options?.recursive ?? true }); + const afterMkdirAbort = abortResult(options?.abortSignal, resolved); + if (afterMkdirAbort) return afterMkdirAbort; + return ok(undefined); + } catch (error) { + return err(toFileError(error, resolved)); + } + } + + async remove( + path: string, + options?: { recursive?: boolean; force?: boolean; abortSignal?: AbortSignal }, + ): Promise> { + const resolved = resolvePath(this.cwd, path); + const aborted = abortResult(options?.abortSignal, resolved); + if (aborted) return aborted; try { await rm(resolved, { recursive: options?.recursive ?? false, force: options?.force ?? false }); + const afterRemoveAbort = abortResult(options?.abortSignal, resolved); + if (afterRemoveAbort) return afterRemoveAbort; + return ok(undefined); } catch (error) { - throw toFileError(error, resolved); + return err(toFileError(error, resolved)); } } - async createTempDir(prefix: string = "tmp-"): Promise { - return await mkdtemp(join(tmpdir(), prefix)); + async createTempDir(prefix: string = "tmp-", abortSignal?: AbortSignal): Promise> { + const aborted = abortResult(abortSignal); + if (aborted) return aborted; + try { + const path = await mkdtemp(join(tmpdir(), prefix)); + const afterMkdtempAbort = abortResult(abortSignal, path); + if (afterMkdtempAbort) return afterMkdtempAbort; + return ok(path); + } catch (error) { + return err(toFileError(error)); + } } - async createTempFile(options?: { prefix?: string; suffix?: string }): Promise { - const dir = await this.createTempDir("tmp-"); - const filePath = join(dir, `${options?.prefix ?? ""}${randomUUID()}${options?.suffix ?? ""}`); - await writeFile(filePath, ""); - return filePath; + async createTempFile(options?: { + prefix?: string; + suffix?: string; + abortSignal?: AbortSignal; + }): Promise> { + const dir = await this.createTempDir("tmp-", options?.abortSignal); + if (!dir.ok) return dir; + const filePath = join(dir.value, `${options?.prefix ?? ""}${randomUUID()}${options?.suffix ?? ""}`); + const aborted = abortResult(options?.abortSignal, filePath); + if (aborted) return aborted; + try { + await writeFile(filePath, "", { signal: options?.abortSignal }); + return ok(filePath); + } catch (error) { + return err(toFileError(error, filePath)); + } } async cleanup(): Promise { diff --git a/packages/agent/src/harness/execution-env.ts b/packages/agent/src/harness/execution-env.ts index 786586e5..915fa4da 100644 --- a/packages/agent/src/harness/execution-env.ts +++ b/packages/agent/src/harness/execution-env.ts @@ -1,3 +1,10 @@ -export { NodeExecutionEnv } from "./env/nodejs.js"; -export type { ExecutionEnv, ExecutionEnvExecOptions, FileErrorCode, FileInfo, FileKind } from "./types.js"; -export { FileError } from "./types.js"; +export type { + ExecutionEnv, + ExecutionEnvExecOptions, + ExecutionErrorCode, + FileErrorCode, + FileInfo, + FileKind, + Result, +} from "./types.js"; +export { ExecutionError, err, FileError, getOrThrow, getOrUndefined, ok } from "./types.js"; diff --git a/packages/agent/src/harness/prompt-templates.ts b/packages/agent/src/harness/prompt-templates.ts index e77682b8..e627b66c 100644 --- a/packages/agent/src/harness/prompt-templates.ts +++ b/packages/agent/src/harness/prompt-templates.ts @@ -1,5 +1,5 @@ import { parse } from "yaml"; -import type { ExecutionEnv, FileInfo, PromptTemplate } from "./types.js"; +import { type ExecutionEnv, type FileInfo, getOrUndefined, type PromptTemplate, type Result } from "./types.js"; /** Warning produced while loading prompt templates. */ export interface PromptTemplateDiagnostic { @@ -30,7 +30,7 @@ export async function loadPromptTemplates( const promptTemplates: PromptTemplate[] = []; const diagnostics: PromptTemplateDiagnostic[] = []; for (const path of Array.isArray(paths) ? paths : [paths]) { - const info = await safeFileInfo(env, path); + const info = getOrUndefined(await env.fileInfo(path)); if (!info) continue; const kind = await resolveKind(env, info); if (kind === "directory") { @@ -83,17 +83,16 @@ async function loadTemplatesFromDir( ): Promise<{ promptTemplates: PromptTemplate[]; diagnostics: PromptTemplateDiagnostic[] }> { const promptTemplates: PromptTemplate[] = []; const diagnostics: PromptTemplateDiagnostic[] = []; - let entries: FileInfo[]; - try { - entries = await env.listDir(dir); - } catch (error) { + const entriesResult = await env.listDir(dir); + if (!entriesResult.ok) { diagnostics.push({ type: "warning", - message: errorMessage(error, "failed to list prompt template directory"), + message: entriesResult.error.message, path: dir, }); return { promptTemplates, diagnostics }; } + const entries = entriesResult.value; for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) { const kind = await resolveKind(env, entry); @@ -110,60 +109,66 @@ async function loadTemplateFromFile( filePath: string, ): Promise<{ promptTemplate: PromptTemplate | null; diagnostics: PromptTemplateDiagnostic[] }> { const diagnostics: PromptTemplateDiagnostic[] = []; - try { - const rawContent = await env.readTextFile(filePath); - const { frontmatter, body } = parseFrontmatter(rawContent); - const firstLine = body.split("\n").find((line) => line.trim()); - let description = typeof frontmatter.description === "string" ? frontmatter.description : ""; - if (!description && firstLine) { - description = firstLine.slice(0, 60); - if (firstLine.length > 60) description += "..."; - } - return { - promptTemplate: { - name: basenameEnvPath(filePath).replace(/\.md$/i, ""), - description, - content: body, - }, - diagnostics, - }; - } catch (error) { + const rawContent = await env.readTextFile(filePath); + if (!rawContent.ok) { diagnostics.push({ type: "warning", - message: errorMessage(error, "failed to load prompt template"), + message: rawContent.error.message, path: filePath, }); return { promptTemplate: null, diagnostics }; } -} -async function safeFileInfo(env: ExecutionEnv, path: string): Promise { - try { - return await env.fileInfo(path); - } catch { - return undefined; + const parsed = parseFrontmatter(rawContent.value); + if (!parsed.ok) { + diagnostics.push({ + type: "warning", + message: parsed.error.message, + path: filePath, + }); + return { promptTemplate: null, diagnostics }; } + + const { frontmatter, body } = parsed.value; + const firstLine = body.split("\n").find((line) => line.trim()); + let description = typeof frontmatter.description === "string" ? frontmatter.description : ""; + if (!description && firstLine) { + description = firstLine.slice(0, 60); + if (firstLine.length > 60) description += "..."; + } + return { + promptTemplate: { + name: basenameEnvPath(filePath).replace(/\.md$/i, ""), + description, + content: body, + }, + diagnostics, + }; } async function resolveKind(env: ExecutionEnv, info: FileInfo): Promise<"file" | "directory" | undefined> { if (info.kind === "file" || info.kind === "directory") return info.kind; - try { - const realPath = await env.realPath(info.path); - const target = await env.fileInfo(realPath); - return target.kind === "file" || target.kind === "directory" ? target.kind : undefined; - } catch { - return undefined; - } + const realPath = await env.realPath(info.path); + if (!realPath.ok) return undefined; + const target = getOrUndefined(await env.fileInfo(realPath.value)); + if (!target) return undefined; + return target.kind === "file" || target.kind === "directory" ? target.kind : undefined; } -function parseFrontmatter>(content: string): { frontmatter: T; body: string } { - const normalized = content.replace(/\r\n/g, "\n").replace(/\r/g, "\n"); - if (!normalized.startsWith("---")) return { frontmatter: {} as T, body: normalized }; - const endIndex = normalized.indexOf("\n---", 3); - if (endIndex === -1) return { frontmatter: {} as T, body: normalized }; - const yamlString = normalized.slice(4, endIndex); - const body = normalized.slice(endIndex + 4).trim(); - return { frontmatter: (parse(yamlString) ?? {}) as T, body }; +function parseFrontmatter>( + content: string, +): Result<{ frontmatter: T; body: string }, Error> { + try { + const normalized = content.replace(/\r\n/g, "\n").replace(/\r/g, "\n"); + if (!normalized.startsWith("---")) return { ok: true, value: { frontmatter: {} as T, body: normalized } }; + const endIndex = normalized.indexOf("\n---", 3); + if (endIndex === -1) return { ok: true, value: { frontmatter: {} as T, body: normalized } }; + const yamlString = normalized.slice(4, endIndex); + const body = normalized.slice(endIndex + 4).trim(); + return { ok: true, value: { frontmatter: (parse(yamlString) ?? {}) as T, body } }; + } catch (error) { + return { ok: false, error: error instanceof Error ? error : new Error(String(error)) }; + } } function basenameEnvPath(path: string): string { @@ -172,10 +177,6 @@ function basenameEnvPath(path: string): string { return slashIndex === -1 ? normalized : normalized.slice(slashIndex + 1); } -function errorMessage(error: unknown, fallback: string): string { - return error instanceof Error ? error.message : fallback; -} - /** Parse an argument string using simple shell-style single and double quotes. */ export function parseCommandArgs(argsString: string): string[] { const args: string[] = []; diff --git a/packages/agent/src/harness/skills.ts b/packages/agent/src/harness/skills.ts index db03d93c..bb8931f6 100644 --- a/packages/agent/src/harness/skills.ts +++ b/packages/agent/src/harness/skills.ts @@ -1,6 +1,6 @@ import ignore from "ignore"; import { parse } from "yaml"; -import type { ExecutionEnv, Skill } from "./types.js"; +import { type ExecutionEnv, type FileInfo, getOrUndefined, type Result, type Skill } from "./types.js"; const MAX_NAME_LENGTH = 64; const MAX_DESCRIPTION_LENGTH = 1024; @@ -44,7 +44,7 @@ export async function loadSkills( const skills: Skill[] = []; const diagnostics: SkillDiagnostic[] = []; for (const dir of Array.isArray(dirs) ? dirs : [dirs]) { - const rootInfo = await safeFileInfo(env, dir); + const rootInfo = getOrUndefined(await env.fileInfo(dir)); if (!rootInfo || (await resolveKind(env, rootInfo)) !== "directory") continue; const result = await loadSkillsFromDirInternal(env, rootInfo.path, true, ignore(), rootInfo.path); skills.push(...result.skills); @@ -89,18 +89,13 @@ async function loadSkillsFromDirInternal( const skills: Skill[] = []; const diagnostics: SkillDiagnostic[] = []; - if (!(await env.exists(dir))) return { skills, diagnostics }; - const dirInfo = await safeFileInfo(env, dir); + const dirInfo = getOrUndefined(await env.fileInfo(dir)); if (!dirInfo || (await resolveKind(env, dirInfo)) !== "directory") return { skills, diagnostics }; await addIgnoreRules(env, ignoreMatcher, dir, rootDir); - let entries: Awaited>; - try { - entries = await env.listDir(dir); - } catch { - return { skills, diagnostics }; - } + const entries = getOrUndefined(await env.listDir(dir)); + if (!entries) return { skills, diagnostics }; for (const entry of entries) { if (entry.name !== "SKILL.md") continue; @@ -148,16 +143,15 @@ async function addIgnoreRules(env: ExecutionEnv, ig: IgnoreMatcher, dir: string, for (const filename of IGNORE_FILE_NAMES) { const ignorePath = joinEnvPath(dir, filename); - const info = await safeFileInfo(env, ignorePath); + const info = getOrUndefined(await env.fileInfo(ignorePath)); if (info?.kind !== "file") continue; - try { - const content = await env.readTextFile(ignorePath); - const patterns = content - .split(/\r?\n/) - .map((line) => prefixIgnorePattern(line, prefix)) - .filter((line): line is string => Boolean(line)); - if (patterns.length > 0) ig.add(patterns); - } catch {} + const content = await env.readTextFile(ignorePath); + if (!content.ok) continue; + const patterns = content.value + .split(/\r?\n/) + .map((line) => prefixIgnorePattern(line, prefix)) + .filter((line): line is string => Boolean(line)); + if (patterns.length > 0) ig.add(patterns); } } @@ -184,40 +178,47 @@ async function loadSkillFromFile( filePath: string, ): Promise<{ skill: Skill | null; diagnostics: SkillDiagnostic[] }> { const diagnostics: SkillDiagnostic[] = []; - try { - const rawContent = await env.readTextFile(filePath); - const { frontmatter, body } = parseFrontmatter(rawContent); - const skillDir = dirnameEnvPath(filePath); - const parentDirName = basenameEnvPath(skillDir); - - for (const error of validateDescription(frontmatter.description)) { - diagnostics.push({ type: "warning", message: error, path: filePath }); - } - - const name = frontmatter.name || parentDirName; - for (const error of validateName(name, parentDirName)) { - diagnostics.push({ type: "warning", message: error, path: filePath }); - } - - if (!frontmatter.description || frontmatter.description.trim() === "") { - return { skill: null, diagnostics }; - } - - return { - skill: { - name, - description: frontmatter.description, - content: body, - filePath, - disableModelInvocation: frontmatter["disable-model-invocation"] === true, - }, - diagnostics, - }; - } catch (error) { - const message = error instanceof Error ? error.message : "failed to parse skill file"; - diagnostics.push({ type: "warning", message, path: filePath }); + const rawContent = await env.readTextFile(filePath); + if (!rawContent.ok) { + diagnostics.push({ type: "warning", message: rawContent.error.message, path: filePath }); return { skill: null, diagnostics }; } + + const parsed = parseFrontmatter(rawContent.value); + if (!parsed.ok) { + diagnostics.push({ type: "warning", message: parsed.error.message, path: filePath }); + return { skill: null, diagnostics }; + } + + const { frontmatter, body } = parsed.value; + const skillDir = dirnameEnvPath(filePath); + const parentDirName = basenameEnvPath(skillDir); + const description = typeof frontmatter.description === "string" ? frontmatter.description : undefined; + + for (const error of validateDescription(description)) { + diagnostics.push({ type: "warning", message: error, path: filePath }); + } + + const frontmatterName = typeof frontmatter.name === "string" ? frontmatter.name : undefined; + const name = frontmatterName || parentDirName; + for (const error of validateName(name, parentDirName)) { + diagnostics.push({ type: "warning", message: error, path: filePath }); + } + + if (!description || description.trim() === "") { + return { skill: null, diagnostics }; + } + + return { + skill: { + name, + description, + content: body, + filePath, + disableModelInvocation: frontmatter["disable-model-invocation"] === true, + }, + diagnostics, + }; } function validateName(name: string, parentDirName: string): string[] { @@ -242,39 +243,29 @@ function validateDescription(description: string | undefined): string[] { return errors; } -function parseFrontmatter>(content: string): { frontmatter: T; body: string } { - const normalized = content.replace(/\r\n/g, "\n").replace(/\r/g, "\n"); - if (!normalized.startsWith("---")) return { frontmatter: {} as T, body: normalized }; - const endIndex = normalized.indexOf("\n---", 3); - if (endIndex === -1) return { frontmatter: {} as T, body: normalized }; - const yamlString = normalized.slice(4, endIndex); - const body = normalized.slice(endIndex + 4).trim(); - return { frontmatter: (parse(yamlString) ?? {}) as T, body }; -} - -async function safeFileInfo( - env: ExecutionEnv, - path: string, -): Promise> | undefined> { +function parseFrontmatter>( + content: string, +): Result<{ frontmatter: T; body: string }, Error> { try { - return await env.fileInfo(path); - } catch { - return undefined; + const normalized = content.replace(/\r\n/g, "\n").replace(/\r/g, "\n"); + if (!normalized.startsWith("---")) return { ok: true, value: { frontmatter: {} as T, body: normalized } }; + const endIndex = normalized.indexOf("\n---", 3); + if (endIndex === -1) return { ok: true, value: { frontmatter: {} as T, body: normalized } }; + const yamlString = normalized.slice(4, endIndex); + const body = normalized.slice(endIndex + 4).trim(); + return { ok: true, value: { frontmatter: (parse(yamlString) ?? {}) as T, body } }; + } catch (error) { + return { ok: false, error: error instanceof Error ? error : new Error(String(error)) }; } } -async function resolveKind( - env: ExecutionEnv, - info: Awaited>, -): Promise<"file" | "directory" | undefined> { +async function resolveKind(env: ExecutionEnv, info: FileInfo): Promise<"file" | "directory" | undefined> { if (info.kind === "file" || info.kind === "directory") return info.kind; - try { - const realPath = await env.realPath(info.path); - const target = await env.fileInfo(realPath); - return target.kind === "file" || target.kind === "directory" ? target.kind : undefined; - } catch { - return undefined; - } + const realPath = await env.realPath(info.path); + if (!realPath.ok) return undefined; + const target = getOrUndefined(await env.fileInfo(realPath.value)); + if (!target) return undefined; + return target.kind === "file" || target.kind === "directory" ? target.kind : undefined; } function joinEnvPath(base: string, child: string): string { diff --git a/packages/agent/src/harness/types.ts b/packages/agent/src/harness/types.ts index f50337f8..d80f2a21 100644 --- a/packages/agent/src/harness/types.ts +++ b/packages/agent/src/harness/types.ts @@ -3,6 +3,30 @@ import type { QueueMode } from "../agent.js"; import type { AgentEvent, AgentMessage, AgentTool, ThinkingLevel } from "../index.js"; import type { Session } from "./session/session.js"; +/** Result of a fallible operation. Expected failures are returned as `ok: false` instead of thrown. */ +export type Result = { ok: true; value: TValue } | { ok: false; error: TError }; + +/** Create a successful {@link Result}. */ +export function ok(value: TValue): Result { + return { ok: true, value }; +} + +/** Create a failed {@link Result}. */ +export function err(error: TError): Result { + return { ok: false, error }; +} + +/** Return the success value or throw the failure error. Intended for tests and explicit adapter boundaries. */ +export function getOrThrow(result: Result): TValue { + if (!result.ok) throw result.error; + return result.value; +} + +/** Return the success value or `undefined`. Only object values are allowed to avoid truthiness bugs with primitives. */ +export function getOrUndefined(result: Result): TValue | undefined { + return result.ok ? result.value : undefined; +} + /** * Skill loaded from a `SKILL.md` file or provided by an application. * @@ -73,8 +97,9 @@ export interface AgentHarnessStreamOptionsPatch /** Kind of filesystem object as addressed by an {@link ExecutionEnv}. Symlinks are not followed automatically. */ export type FileKind = "file" | "directory" | "symlink"; -/** Stable, backend-independent file error codes thrown by {@link ExecutionEnv} file operations. */ +/** Stable, backend-independent file error codes returned by {@link ExecutionEnv} file operations. */ export type FileErrorCode = + | "aborted" | "not_found" | "permission_denied" | "not_directory" @@ -83,7 +108,7 @@ export type FileErrorCode = | "not_supported" | "unknown"; -/** Error thrown by {@link ExecutionEnv} file operations. */ +/** Error returned by {@link ExecutionEnv} file operations. */ export class FileError extends Error { constructor( /** Backend-independent error code. */ @@ -91,13 +116,29 @@ export class FileError extends Error { message: string, /** Absolute addressed path associated with the failure, when available. */ public path?: string, - options?: ErrorOptions, + cause?: unknown, ) { - super(message, options); + super(message, cause === undefined ? undefined : { cause }); this.name = "FileError"; } } +/** Stable, backend-independent execution error codes returned by {@link ExecutionEnv.exec}. */ +export type ExecutionErrorCode = "aborted" | "timeout" | "shell_unavailable" | "spawn_error" | "unknown"; + +/** Error returned by {@link ExecutionEnv.exec}. */ +export class ExecutionError extends Error { + constructor( + /** Backend-independent error code. */ + public code: ExecutionErrorCode, + message: string, + cause?: unknown, + ) { + super(message, cause === undefined ? undefined : { cause }); + this.name = "ExecutionError"; + } +} + /** Metadata for one filesystem object in an {@link ExecutionEnv}. */ export interface FileInfo { /** Basename of {@link path}. */ @@ -114,14 +155,14 @@ export interface FileInfo { /** Options for {@link ExecutionEnv.exec}. */ export interface ExecutionEnvExecOptions { - /** Working directory for the command. Relative paths are resolved against {@link ExecutionEnv.cwd}. */ + /** Working directory for the command. Relative paths are resolved against {@link ExecutionEnv.cwd}. Defaults to {@link ExecutionEnv.cwd}. */ cwd?: string; - /** Additional environment variables for the command. Values override the environment defaults. */ + /** Additional environment variables for the command. Values override the environment defaults. Defaults to no overrides. */ env?: Record; - /** Timeout in seconds. Implementations should reject when the command exceeds this duration. */ + /** Timeout in seconds. Implementations should return a timeout error when the command exceeds this duration. Defaults to no timeout. */ timeout?: number; - /** Abort signal used to terminate the command. */ - signal?: AbortSignal; + /** Abort signal used to terminate the command. Defaults to no abort signal. */ + abortSignal?: AbortSignal; /** Called with stdout chunks as they are produced. */ onStdout?: (chunk: string) => void; /** Called with stderr chunks as they are produced. */ @@ -134,7 +175,8 @@ export interface ExecutionEnvExecOptions { * Paths passed to methods may be absolute or relative to {@link cwd}. Paths returned by this interface are absolute * addressed paths in the environment, but are not canonicalized through symlinks unless returned by {@link resolvePath}. * - * File operations throw {@link FileError} for expected filesystem failures such as missing paths or permission errors. + * Operation methods must never throw or reject. All filesystem/process failures, including unexpected backend failures, + * must be encoded in the returned {@link Result}. Implementations must preserve this invariant. */ export interface ExecutionEnv { /** Current working directory for relative paths and command execution. */ @@ -144,32 +186,44 @@ export interface ExecutionEnv { exec( command: string, options?: ExecutionEnvExecOptions, - ): Promise<{ stdout: string; stderr: string; exitCode: number }>; + ): Promise>; - /** Read a UTF-8 text file. Throws {@link FileError}. */ - readTextFile(path: string): Promise; - /** Read a binary file. Throws {@link FileError}. */ - readBinaryFile(path: string): Promise; - /** Create or overwrite a file, creating parent directories when supported. Throws {@link FileError}. */ - writeFile(path: string, content: string | Uint8Array): Promise; - /** Return metadata for the addressed path without following symlinks. Throws {@link FileError}. */ - fileInfo(path: string): Promise; - /** List direct children of a directory without following symlinks. Throws {@link FileError}. */ - listDir(path: string): Promise; - /** Return the canonical path for a path, following symlinks. Throws {@link FileError}. */ - realPath(path: string): Promise; - /** Return false for missing paths. Other errors, such as permission failures, may throw {@link FileError}. */ - exists(path: string): Promise; - /** Create a directory. */ - createDir(path: string, options?: { recursive?: boolean }): Promise; - /** Remove a file or directory. */ - remove(path: string, options?: { recursive?: boolean; force?: boolean }): Promise; - /** Create a temporary directory and return its absolute path. */ - createTempDir(prefix?: string): Promise; - /** Create a temporary file and return its absolute path. */ - createTempFile(options?: { prefix?: string; suffix?: string }): Promise; + /** Read a UTF-8 text file. */ + readTextFile(path: string, abortSignal?: AbortSignal): Promise>; + /** Read a binary file. */ + readBinaryFile(path: string, abortSignal?: AbortSignal): Promise>; + /** Create or overwrite a file, creating parent directories when supported. */ + writeFile(path: string, content: string | Uint8Array, abortSignal?: AbortSignal): Promise>; + /** Create or append to a file, creating parent directories when supported. */ + appendFile(path: string, content: string | Uint8Array, abortSignal?: AbortSignal): Promise>; + /** Return metadata for the addressed path without following symlinks. */ + fileInfo(path: string, abortSignal?: AbortSignal): Promise>; + /** List direct children of a directory without following symlinks. */ + listDir(path: string, abortSignal?: AbortSignal): Promise>; + /** Return the canonical path for a path, following symlinks. */ + realPath(path: string, abortSignal?: AbortSignal): Promise>; + /** Return false for missing paths. Other errors, such as permission failures, return a {@link FileError}. */ + exists(path: string, abortSignal?: AbortSignal): Promise>; + /** Create a directory. Defaults: `recursive: true`, no abort signal. */ + createDir( + path: string, + options?: { recursive?: boolean; abortSignal?: AbortSignal }, + ): Promise>; + /** Remove a file or directory. Defaults: `recursive: false`, `force: false`, no abort signal. */ + remove( + path: string, + options?: { recursive?: boolean; force?: boolean; abortSignal?: AbortSignal }, + ): Promise>; + /** Create a temporary directory and return its absolute path. Defaults: `prefix: "tmp-"`, no abort signal. */ + createTempDir(prefix?: string, abortSignal?: AbortSignal): Promise>; + /** Create a temporary file and return its absolute path. Defaults: `prefix: ""`, `suffix: ""`, no abort signal. */ + createTempFile(options?: { + prefix?: string; + suffix?: string; + abortSignal?: AbortSignal; + }): Promise>; - /** Release resources owned by the environment. */ + /** Release resources owned by the environment. Must be best-effort and must not throw or reject. */ cleanup(): Promise; } diff --git a/packages/agent/src/harness/utils/shell-output.ts b/packages/agent/src/harness/utils/shell-output.ts index d31d7a06..fcb8d538 100644 --- a/packages/agent/src/harness/utils/shell-output.ts +++ b/packages/agent/src/harness/utils/shell-output.ts @@ -1,8 +1,4 @@ -import { randomBytes } from "node:crypto"; -import { createWriteStream, type WriteStream } from "node:fs"; -import { tmpdir } from "node:os"; -import { join } from "node:path"; -import type { ExecutionEnv, ExecutionEnvExecOptions } from "../types.js"; +import { type ExecutionEnv, type ExecutionEnvExecOptions, ExecutionError, err, ok, type Result } from "../types.js"; import { DEFAULT_MAX_BYTES, truncateTail } from "./truncate.js"; export interface ShellCaptureOptions extends Omit { @@ -17,6 +13,12 @@ export interface ShellCaptureResult { fullOutputPath?: string; } +function toExecutionError(error: unknown): ExecutionError { + return error instanceof ExecutionError + ? error + : new ExecutionError("unknown", error instanceof Error ? error.message : String(error), error); +} + export function sanitizeBinaryOutput(str: string): string { return Array.from(str) .filter((char) => { @@ -34,41 +36,62 @@ export async function executeShellWithCapture( env: ExecutionEnv, command: string, options?: ShellCaptureOptions, -): Promise { +): Promise> { const outputChunks: string[] = []; let outputBytes = 0; const maxOutputBytes = DEFAULT_MAX_BYTES * 2; + const encoder = new TextEncoder(); - let tempFilePath: string | undefined; - let tempFileStream: WriteStream | undefined; let totalBytes = 0; + let fullOutputPath: string | undefined; + let writeChain: Promise> = Promise.resolve(ok(undefined)); + let captureError: ExecutionError | undefined; - const ensureTempFile = () => { - if (tempFilePath) return; - const id = randomBytes(8).toString("hex"); - tempFilePath = join(tmpdir(), `bash-${id}.log`); - tempFileStream = createWriteStream(tempFilePath); - for (const chunk of outputChunks) { - tempFileStream.write(chunk); - } + const appendFullOutput = (text: string): void => { + if (!fullOutputPath || captureError) return; + const path = fullOutputPath; + writeChain = writeChain.then(async (previous) => { + if (!previous.ok) return previous; + const appendResult = await env.appendFile(path, text, options?.abortSignal); + return appendResult.ok ? ok(undefined) : err(toExecutionError(appendResult.error)); + }); + }; + + const ensureFullOutputFile = (initialContent: string): void => { + if (fullOutputPath || captureError) return; + writeChain = writeChain.then(async (previous) => { + if (!previous.ok) return previous; + const tempFile = await env.createTempFile({ + prefix: "bash-", + suffix: ".log", + abortSignal: options?.abortSignal, + }); + if (!tempFile.ok) return err(toExecutionError(tempFile.error)); + fullOutputPath = tempFile.value; + const appendResult = await env.appendFile(tempFile.value, initialContent, options?.abortSignal); + return appendResult.ok ? ok(undefined) : err(toExecutionError(appendResult.error)); + }); }; const onChunk = (chunk: string) => { - totalBytes += Buffer.byteLength(chunk, "utf-8"); - const text = sanitizeBinaryOutput(chunk).replace(/\r/g, ""); - if (totalBytes > DEFAULT_MAX_BYTES) { - ensureTempFile(); + try { + totalBytes += encoder.encode(chunk).byteLength; + const text = sanitizeBinaryOutput(chunk).replace(/\r/g, ""); + if (totalBytes > DEFAULT_MAX_BYTES && !fullOutputPath) { + ensureFullOutputFile(outputChunks.join("") + text); + } else { + appendFullOutput(text); + } + outputChunks.push(text); + outputBytes += text.length; + while (outputBytes > maxOutputBytes && outputChunks.length > 1) { + const removed = outputChunks.shift()!; + outputBytes -= removed.length; + } + options?.onChunk?.(text); + } catch (error) { + captureError = toExecutionError(error); } - if (tempFileStream) { - tempFileStream.write(text); - } - outputChunks.push(text); - outputBytes += text.length; - while (outputBytes > maxOutputBytes && outputChunks.length > 1) { - const removed = outputChunks.shift()!; - outputBytes -= removed.length; - } - options?.onChunk?.(text); }; try { @@ -77,37 +100,36 @@ export async function executeShellWithCapture( onStdout: onChunk, onStderr: onChunk, }); - const fullOutput = outputChunks.join(""); - const truncationResult = truncateTail(fullOutput); - if (truncationResult.truncated) { - ensureTempFile(); + const tailOutput = outputChunks.join(""); + const truncationResult = truncateTail(tailOutput); + if (truncationResult.truncated && !fullOutputPath) { + ensureFullOutputFile(tailOutput); } - tempFileStream?.end(); - const cancelled = options?.signal?.aborted ?? false; - return { - output: truncationResult.truncated ? truncationResult.content : fullOutput, - exitCode: cancelled ? undefined : result.exitCode, + const writeResult = await writeChain; + if (!writeResult.ok) return err(writeResult.error); + if (captureError) return err(captureError); + + if (!result.ok) { + if (result.error.code === "aborted" || options?.abortSignal?.aborted) { + return ok({ + output: truncationResult.truncated ? truncationResult.content : tailOutput, + exitCode: undefined, + cancelled: true, + truncated: truncationResult.truncated, + fullOutputPath, + }); + } + return err(result.error); + } + const cancelled = options?.abortSignal?.aborted ?? false; + return ok({ + output: truncationResult.truncated ? truncationResult.content : tailOutput, + exitCode: cancelled ? undefined : result.value.exitCode, cancelled, truncated: truncationResult.truncated, - fullOutputPath: tempFilePath, - }; - } catch (err) { - if (options?.signal?.aborted) { - const fullOutput = outputChunks.join(""); - const truncationResult = truncateTail(fullOutput); - if (truncationResult.truncated) { - ensureTempFile(); - } - tempFileStream?.end(); - return { - output: truncationResult.truncated ? truncationResult.content : fullOutput, - exitCode: undefined, - cancelled: true, - truncated: truncationResult.truncated, - fullOutputPath: tempFilePath, - }; - } - tempFileStream?.end(); - throw err; + fullOutputPath, + }); + } catch (error) { + return err(toExecutionError(error)); } } diff --git a/packages/agent/test/harness/agent-harness-stream.test.ts b/packages/agent/test/harness/agent-harness-stream.test.ts index e014b377..e5ca193f 100644 --- a/packages/agent/test/harness/agent-harness-stream.test.ts +++ b/packages/agent/test/harness/agent-harness-stream.test.ts @@ -1,7 +1,7 @@ import { fauxAssistantMessage, fauxToolCall, registerFauxProvider, type StreamOptions } from "@earendil-works/pi-ai"; import { afterEach, describe, expect, it } from "vitest"; import { AgentHarness } from "../../src/harness/agent-harness.js"; -import { NodeExecutionEnv } from "../../src/harness/execution-env.js"; +import { NodeExecutionEnv } from "../../src/harness/env/nodejs.js"; import { Session } from "../../src/harness/session/session.js"; import { InMemorySessionStorage } from "../../src/harness/session/storage/memory.js"; import { calculateTool } from "../utils/calculate.js"; diff --git a/packages/agent/test/harness/agent-harness.test.ts b/packages/agent/test/harness/agent-harness.test.ts index 453b1297..f8111f70 100644 --- a/packages/agent/test/harness/agent-harness.test.ts +++ b/packages/agent/test/harness/agent-harness.test.ts @@ -1,7 +1,7 @@ import { getModel } from "@earendil-works/pi-ai"; import { describe, expect, it } from "vitest"; import { AgentHarness } from "../../src/harness/agent-harness.js"; -import { NodeExecutionEnv } from "../../src/harness/execution-env.js"; +import { NodeExecutionEnv } from "../../src/harness/env/nodejs.js"; import { Session } from "../../src/harness/session/session.js"; import { InMemorySessionStorage } from "../../src/harness/session/storage/memory.js"; import type { PromptTemplate, Skill } from "../../src/harness/types.js"; diff --git a/packages/agent/test/harness/nodejs-env.test.ts b/packages/agent/test/harness/nodejs-env.test.ts index 2059c075..97c5da23 100644 --- a/packages/agent/test/harness/nodejs-env.test.ts +++ b/packages/agent/test/harness/nodejs-env.test.ts @@ -1,7 +1,9 @@ import { access, chmod, realpath, symlink } from "node:fs/promises"; import { join } from "node:path"; import { afterEach, describe, expect, it } from "vitest"; -import { FileError, NodeExecutionEnv } from "../../src/harness/execution-env.js"; +import { NodeExecutionEnv } from "../../src/harness/env/nodejs.js"; +import { FileError, getOrThrow } from "../../src/harness/execution-env.js"; +import { executeShellWithCapture } from "../../src/harness/utils/shell-output.js"; import { createTempDir } from "./session-test-utils.js"; const chmodRestorePaths: string[] = []; @@ -19,61 +21,66 @@ describe("NodeExecutionEnv", () => { it("reads, writes, lists, and removes files and directories", async () => { const root = createTempDir(); const env = new NodeExecutionEnv({ cwd: root }); - await env.createDir("nested", { recursive: true }); - await env.writeFile("nested/file.txt", "hello"); - expect(await env.readTextFile("nested/file.txt")).toBe("hello"); - expect(Buffer.from(await env.readBinaryFile("nested/file.txt")).toString("utf8")).toBe("hello"); + getOrThrow(await env.createDir("nested/child")); + getOrThrow(await env.writeFile("nested/child/file.txt", "hel")); + getOrThrow(await env.appendFile("nested/child/file.txt", "lo")); + expect(getOrThrow(await env.readTextFile("nested/child/file.txt"))).toBe("hello"); + expect(Buffer.from(getOrThrow(await env.readBinaryFile("nested/child/file.txt"))).toString("utf8")).toBe("hello"); - const entries = await env.listDir("nested"); + const entries = getOrThrow(await env.listDir("nested/child")); expect(entries).toHaveLength(1); expect(entries[0]).toMatchObject({ name: "file.txt", - path: join(root, "nested/file.txt"), + path: join(root, "nested/child/file.txt"), kind: "file", size: 5, }); expect(typeof entries[0]!.mtimeMs).toBe("number"); - expect(await env.exists("nested/file.txt")).toBe(true); - await env.remove("nested/file.txt"); - expect(await env.exists("nested/file.txt")).toBe(false); + expect(getOrThrow(await env.exists("nested/child/file.txt"))).toBe(true); + getOrThrow(await env.remove("nested/child/file.txt")); + expect(getOrThrow(await env.exists("nested/child/file.txt"))).toBe(false); }); it("returns fileInfo for files, directories, and symlinks without following symlinks", async () => { const root = createTempDir(); const env = new NodeExecutionEnv({ cwd: root }); - await env.createDir("dir", { recursive: true }); - await env.writeFile("dir/file.txt", "hello"); + getOrThrow(await env.createDir("dir", { recursive: true })); + getOrThrow(await env.writeFile("dir/file.txt", "hello")); await symlink(join(root, "dir/file.txt"), join(root, "file-link")); await symlink(join(root, "dir"), join(root, "dir-link")); - expect(await env.fileInfo("dir")).toMatchObject({ name: "dir", path: join(root, "dir"), kind: "directory" }); - expect(await env.fileInfo("dir/file.txt")).toMatchObject({ + expect(getOrThrow(await env.fileInfo("dir"))).toMatchObject({ + name: "dir", + path: join(root, "dir"), + kind: "directory", + }); + expect(getOrThrow(await env.fileInfo("dir/file.txt"))).toMatchObject({ name: "file.txt", path: join(root, "dir/file.txt"), kind: "file", size: 5, }); - expect(await env.fileInfo("file-link")).toMatchObject({ + expect(getOrThrow(await env.fileInfo("file-link"))).toMatchObject({ name: "file-link", path: join(root, "file-link"), kind: "symlink", }); - expect(await env.fileInfo("dir-link")).toMatchObject({ + expect(getOrThrow(await env.fileInfo("dir-link"))).toMatchObject({ name: "dir-link", path: join(root, "dir-link"), kind: "symlink", }); - expect(await env.realPath("file-link")).toBe(await realpath(join(root, "dir/file.txt"))); + expect(getOrThrow(await env.realPath("file-link"))).toBe(await realpath(join(root, "dir/file.txt"))); }); it("lists symlinks as symlinks", async () => { const root = createTempDir(); const env = new NodeExecutionEnv({ cwd: root }); - await env.writeFile("target.txt", "hello"); + getOrThrow(await env.writeFile("target.txt", "hello")); await symlink(join(root, "target.txt"), join(root, "link.txt")); - const entries = await env.listDir("."); + const entries = getOrThrow(await env.listDir(".")); expect( entries.map((entry) => ({ name: entry.name, kind: entry.kind })).sort((a, b) => a.name.localeCompare(b.name)), ).toEqual([ @@ -82,41 +89,112 @@ describe("NodeExecutionEnv", () => { ]); }); - it("throws FileError for missing paths and keeps exists false for missing paths", async () => { + it("returns FileError for missing paths and keeps exists false for missing paths", async () => { const root = createTempDir(); const env = new NodeExecutionEnv({ cwd: root }); - await expect(env.fileInfo("missing.txt")).rejects.toMatchObject({ - name: "FileError", - code: "not_found", - path: join(root, "missing.txt"), - }); - expect(await env.exists("missing.txt")).toBe(false); + const info = await env.fileInfo("missing.txt"); + expect(info.ok).toBe(false); + if (!info.ok) { + expect(info.error).toBeInstanceOf(FileError); + expect(info.error).toMatchObject({ + name: "FileError", + code: "not_found", + path: join(root, "missing.txt"), + }); + } + expect(getOrThrow(await env.exists("missing.txt"))).toBe(false); }); - it("throws FileError for listing non-directories", async () => { + it("returns FileError for listing non-directories", async () => { const root = createTempDir(); const env = new NodeExecutionEnv({ cwd: root }); - await env.writeFile("file.txt", "hello"); - await expect(env.listDir("file.txt")).rejects.toBeInstanceOf(FileError); - await expect(env.listDir("file.txt")).rejects.toMatchObject({ code: "not_directory" }); + getOrThrow(await env.writeFile("file.txt", "hello")); + const result = await env.listDir("file.txt"); + expect(result.ok).toBe(false); + if (!result.ok) { + expect(result.error).toBeInstanceOf(FileError); + expect(result.error).toMatchObject({ code: "not_directory" }); + } + }); + + it("appends to new files and creates parent directories", async () => { + const root = createTempDir(); + const env = new NodeExecutionEnv({ cwd: root }); + getOrThrow(await env.appendFile("new/nested/file.txt", "a")); + getOrThrow(await env.appendFile("new/nested/file.txt", "b")); + expect(getOrThrow(await env.readTextFile("new/nested/file.txt"))).toBe("ab"); }); it("creates temporary directories and files", async () => { const root = createTempDir(); const env = new NodeExecutionEnv({ cwd: root }); - const tempDir = await env.createTempDir("node-env-test-"); + const tempDir = getOrThrow(await env.createTempDir("node-env-test-")); await expect(access(tempDir)).resolves.toBeUndefined(); - const tempFile = await env.createTempFile({ prefix: "prefix-", suffix: ".txt" }); + const tempFile = getOrThrow(await env.createTempFile({ prefix: "prefix-", suffix: ".txt" })); await expect(access(tempFile)).resolves.toBeUndefined(); expect(tempFile.endsWith(".txt")).toBe(true); }); + it("honors createDir recursive false and remove recursive/force options", async () => { + const root = createTempDir(); + const env = new NodeExecutionEnv({ cwd: root }); + const createResult = await env.createDir("missing/child", { recursive: false }); + expect(createResult.ok).toBe(false); + if (!createResult.ok) expect(createResult.error).toMatchObject({ code: "not_found" }); + + getOrThrow(await env.writeFile("dir/child/file.txt", "hello")); + const removeDirectory = await env.remove("dir", { recursive: false }); + expect(removeDirectory.ok).toBe(false); + getOrThrow(await env.remove("dir", { recursive: true })); + expect(getOrThrow(await env.exists("dir"))).toBe(false); + + const removeMissing = await env.remove("missing", { force: false }); + expect(removeMissing.ok).toBe(false); + getOrThrow(await env.remove("missing", { force: true })); + }); + + it("returns aborted results for pre-aborted file operations", async () => { + const root = createTempDir(); + const env = new NodeExecutionEnv({ cwd: root }); + getOrThrow(await env.writeFile("file.txt", "hello")); + const controller = new AbortController(); + controller.abort(); + const signal = controller.signal; + + const results = await Promise.all([ + env.readTextFile("file.txt", signal), + env.readBinaryFile("file.txt", signal), + env.writeFile("other.txt", "hello", signal), + env.appendFile("other.txt", "hello", signal), + env.fileInfo("file.txt", signal), + env.listDir(".", signal), + env.realPath("file.txt", signal), + env.exists("file.txt", signal), + env.createDir("dir", { abortSignal: signal }), + env.remove("file.txt", { abortSignal: signal }), + env.createTempDir("node-env-test-", signal), + env.createTempFile({ abortSignal: signal }), + ]); + for (const result of results) { + expect(result.ok).toBe(false); + if (!result.ok) expect(result.error).toMatchObject({ code: "aborted" }); + } + }); + + it("cleanup is best-effort", async () => { + const root = createTempDir(); + const env = new NodeExecutionEnv({ cwd: root }); + await expect(env.cleanup()).resolves.toBeUndefined(); + }); + it("executes commands in cwd with env overrides", async () => { const root = createTempDir(); const env = new NodeExecutionEnv({ cwd: root }); - const result = await env.exec('printf \'%s:%s\' "$PWD" "$NODE_ENV_TEST"', { - env: { NODE_ENV_TEST: "ok" }, - }); + const result = getOrThrow( + await env.exec('printf \'%s:%s\' "$PWD" "$NODE_ENV_TEST"', { + env: { NODE_ENV_TEST: "ok" }, + }), + ); expect(result).toEqual({ stdout: `${await realpath(root)}:ok`, stderr: "", exitCode: 0 }); }); @@ -125,25 +203,83 @@ describe("NodeExecutionEnv", () => { const env = new NodeExecutionEnv({ cwd: root }); let stdout = ""; let stderr = ""; - const result = await env.exec("printf out; printf err >&2", { - onStdout: (chunk) => { - stdout += chunk; - }, - onStderr: (chunk) => { - stderr += chunk; - }, - }); + const result = getOrThrow( + await env.exec("printf out; printf err >&2", { + onStdout: (chunk) => { + stdout += chunk; + }, + onStderr: (chunk) => { + stderr += chunk; + }, + }), + ); expect(result).toEqual({ stdout: "out", stderr: "err", exitCode: 0 }); expect(stdout).toBe("out"); expect(stderr).toBe("err"); }); - it("rejects aborted commands", async () => { + it("returns non-zero command exit codes as successful execution results", async () => { + const root = createTempDir(); + const env = new NodeExecutionEnv({ cwd: root }); + const result = getOrThrow(await env.exec("exit 7")); + expect(result).toEqual({ stdout: "", stderr: "", exitCode: 7 }); + }); + + it("returns timeout errors for commands exceeding the timeout", async () => { + const root = createTempDir(); + const env = new NodeExecutionEnv({ cwd: root }); + const result = await env.exec("sleep 5", { timeout: 0.01 }); + expect(result.ok).toBe(false); + if (!result.ok) expect(result.error).toMatchObject({ code: "timeout" }); + }); + + it("returns callback errors from exec stream handlers", async () => { + const root = createTempDir(); + const env = new NodeExecutionEnv({ cwd: root }); + const result = await env.exec("printf out", { + onStdout: () => { + throw new Error("callback failed"); + }, + }); + expect(result.ok).toBe(false); + if (!result.ok) expect(result.error).toMatchObject({ code: "unknown", message: "callback failed" }); + }); + + it("returns shell unavailable and spawn errors", async () => { + const root = createTempDir(); + const missingShellEnv = new NodeExecutionEnv({ cwd: root, shellPath: join(root, "missing-shell") }); + const missingShell = await missingShellEnv.exec("printf ok"); + expect(missingShell.ok).toBe(false); + if (!missingShell.ok) expect(missingShell.error).toMatchObject({ code: "shell_unavailable" }); + + const shellPath = join(root, "not-executable-shell"); + const env = new NodeExecutionEnv({ cwd: root }); + getOrThrow(await env.writeFile(shellPath, "not executable")); + const spawnErrorEnv = new NodeExecutionEnv({ cwd: root, shellPath }); + const spawnError = await spawnErrorEnv.exec("printf ok"); + expect(spawnError.ok).toBe(false); + if (!spawnError.ok) expect(spawnError.error).toMatchObject({ code: "spawn_error" }); + }); + + it("returns an aborted result for aborted commands", async () => { const root = createTempDir(); const env = new NodeExecutionEnv({ cwd: root }); const controller = new AbortController(); - const promise = env.exec("sleep 5", { signal: controller.signal }); + const promise = env.exec("sleep 5", { abortSignal: controller.signal }); controller.abort(); - await expect(promise).rejects.toThrow("aborted"); + const result = await promise; + expect(result.ok).toBe(false); + if (!result.ok) expect(result.error).toMatchObject({ code: "aborted" }); + }); + + it("captures large shell output to a full output file through the execution env", async () => { + const root = createTempDir(); + const env = new NodeExecutionEnv({ cwd: root }); + const result = getOrThrow(await executeShellWithCapture(env, "yes line | head -n 15000")); + expect(result.truncated).toBe(true); + expect(result.fullOutputPath).toBeDefined(); + const fullOutput = getOrThrow(await env.readTextFile(result.fullOutputPath!)); + expect(fullOutput.split("\n").length).toBeGreaterThan(10000); + expect(result.output.length).toBeLessThan(fullOutput.length); }); }); diff --git a/packages/agent/test/harness/prompt-templates.test.ts b/packages/agent/test/harness/prompt-templates.test.ts index 62ae9aee..adb11006 100644 --- a/packages/agent/test/harness/prompt-templates.test.ts +++ b/packages/agent/test/harness/prompt-templates.test.ts @@ -1,7 +1,7 @@ import { symlink } from "node:fs/promises"; import { join } from "node:path"; import { describe, expect, it } from "vitest"; -import { NodeExecutionEnv } from "../../src/harness/execution-env.js"; +import { NodeExecutionEnv } from "../../src/harness/env/nodejs.js"; import { formatPromptTemplateInvocation, loadPromptTemplates, diff --git a/packages/agent/test/harness/skills.test.ts b/packages/agent/test/harness/skills.test.ts index f20c70bd..c4d53cbb 100644 --- a/packages/agent/test/harness/skills.test.ts +++ b/packages/agent/test/harness/skills.test.ts @@ -1,7 +1,7 @@ import { symlink } from "node:fs/promises"; import { join } from "node:path"; import { describe, expect, it } from "vitest"; -import { NodeExecutionEnv } from "../../src/harness/execution-env.js"; +import { NodeExecutionEnv } from "../../src/harness/env/nodejs.js"; import { loadSkills, loadSourcedSkills } from "../../src/harness/skills.js"; import { createTempDir } from "./session-test-utils.js"; diff --git a/packages/agent/test/scratch/simple.ts b/packages/agent/test/scratch/simple.ts index c417740f..296b2160 100644 --- a/packages/agent/test/scratch/simple.ts +++ b/packages/agent/test/scratch/simple.ts @@ -1,13 +1,13 @@ import { homedir } from "node:os"; import { join } from "node:path"; import { getModel } from "@earendil-works/pi-ai"; +import { NodeExecutionEnv } from "../../src/harness/env/nodejs.js"; import { InMemorySessionStorage } from "../../src/harness/session/storage/memory.js"; import { AgentHarness, formatSkillsForSystemPrompt, loadSourcedPromptTemplates, loadSourcedSkills, - NodeExecutionEnv, type PromptTemplate, Session, type Skill, From b7ea82105aefe9b2485834d0f21b08f4ce13d0e2 Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Thu, 14 May 2026 21:33:34 +0200 Subject: [PATCH 038/166] refactor(agent): run harness loop directly --- package-lock.json | 677 ++++++++++++++++++ packages/agent/docs/agent-harness.md | 71 +- packages/agent/package.json | 3 + packages/agent/src/agent.ts | 5 +- packages/agent/src/harness/agent-harness.ts | 465 +++++++----- packages/agent/src/harness/types.ts | 3 +- packages/agent/src/harness/utils/truncate.ts | 118 ++- packages/agent/src/types.ts | 8 + .../agent/test/harness/agent-harness.test.ts | 436 ++++++++++- packages/agent/test/harness/truncate.test.ts | 158 ++++ packages/agent/vitest.harness.config.ts | 18 + 11 files changed, 1711 insertions(+), 251 deletions(-) create mode 100644 packages/agent/test/harness/truncate.test.ts create mode 100644 packages/agent/vitest.harness.config.ts diff --git a/package-lock.json b/package-lock.json index f11dae5f..07555c8a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -34,6 +34,20 @@ "node": ">=20.0.0" } }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/@anthropic-ai/sandbox-runtime": { "version": "0.0.26", "dev": true, @@ -763,6 +777,42 @@ "node": ">=18.0.0" } }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.3", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.3.tgz", + "integrity": "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/@babel/runtime": { "version": "7.29.2", "license": "MIT", @@ -770,6 +820,30 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", + "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/@biomejs/biome": { "version": "2.3.5", "dev": true, @@ -1411,6 +1485,90 @@ } } }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.6.tgz", + "integrity": "sha512-+Sg6GCR/wy1oSmQDFq4LQDAhm3ETKnorxN+y5nbLULOR3P0c14f2Wurzj3/xqPXtasLFfHd5iRFQ7AJt4KH2cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.13", "dev": true, @@ -2333,6 +2491,17 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, "node_modules/@pondwader/socks5-server": { "version": "1.0.10", "dev": true, @@ -3821,6 +3990,40 @@ "win32" ] }, + "node_modules/@vitest/coverage-v8": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-3.2.4.tgz", + "integrity": "sha512-EyF9SXU6kS5Ku/U82E259WSnvg6c8KTjppUncuNdm5QHpe17mwREHnjDzozC8x9MZ0xfBUFSaLkRv4TMA75ALQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.3.0", + "@bcoe/v8-coverage": "^1.0.2", + "ast-v8-to-istanbul": "^0.3.3", + "debug": "^4.4.1", + "istanbul-lib-coverage": "^3.2.2", + "istanbul-lib-report": "^3.0.1", + "istanbul-lib-source-maps": "^5.0.6", + "istanbul-reports": "^3.1.7", + "magic-string": "^0.30.17", + "magicast": "^0.3.5", + "std-env": "^3.9.0", + "test-exclude": "^7.0.1", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@vitest/browser": "3.2.4", + "vitest": "3.2.4" + }, + "peerDependenciesMeta": { + "@vitest/browser": { + "optional": true + } + } + }, "node_modules/@vitest/expect": { "version": "3.2.4", "dev": true, @@ -3950,6 +4153,19 @@ "version": "2.0.8", "license": "MIT" }, + "node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, "node_modules/ansi-styles": { "version": "4.3.0", "license": "MIT", @@ -3971,6 +4187,25 @@ "node": ">=12" } }, + "node_modules/ast-v8-to-istanbul": { + "version": "0.3.12", + "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-0.3.12.tgz", + "integrity": "sha512-BRRC8VRZY2R4Z4lFIL35MwNXmwVqBityvOIwETtsCSwvjl0IdgFsy9NhdaA6j74nUdtJJlIypeRhpDam19Wq3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.31", + "estree-walker": "^3.0.3", + "js-tokens": "^10.0.0" + } + }, + "node_modules/ast-v8-to-istanbul/node_modules/js-tokens": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz", + "integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==", + "dev": true, + "license": "MIT" + }, "node_modules/balanced-match": { "version": "4.0.4", "license": "MIT", @@ -4370,6 +4605,13 @@ "jszip": ">=3.0.0" } }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, "node_modules/ecdsa-sig-formatter": { "version": "1.0.11", "license": "Apache-2.0", @@ -4606,6 +4848,100 @@ "node": ">=8" } }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/foreground-child/node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/foreground-child/node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/foreground-child/node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/foreground-child/node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/foreground-child/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/foreground-child/node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/formdata-polyfill": { "version": "4.0.10", "license": "MIT", @@ -4789,6 +5125,13 @@ "node": "^20.17.0 || >=22.9.0" } }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, "node_modules/html-parse-string": { "version": "0.0.9", "license": "MIT" @@ -4942,6 +5285,89 @@ "dev": true, "license": "ISC" }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz", + "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.23", + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, "node_modules/jiti": { "version": "2.7.0", "license": "MIT", @@ -5382,6 +5808,34 @@ "@jridgewell/sourcemap-codec": "^1.5.5" } }, + "node_modules/magicast": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.3.5.tgz", + "integrity": "sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.25.4", + "@babel/types": "^7.25.4", + "source-map-js": "^1.2.0" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/marked": { "version": "15.0.12", "license": "MIT", @@ -5612,6 +6066,13 @@ "version": "0.12.0", "license": "MIT" }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, "node_modules/pako": { "version": "1.0.11", "license": "(MIT AND Zlib)" @@ -6198,6 +6659,45 @@ "node": ">=8" } }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/string-width/node_modules/ansi-regex": { "version": "5.0.1", "dev": true, @@ -6217,6 +6717,46 @@ "node": ">=8" } }, + "node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/strip-eof": { "version": "1.0.0", "dev": true, @@ -6346,6 +6886,100 @@ "node": ">=6" } }, + "node_modules/test-exclude": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-7.0.2.tgz", + "integrity": "sha512-u9E6A+ZDYdp7a4WnarkXPZOx8Ilz46+kby6p1yZ8zsGTz9gYa6FIS7lj2oezzNKmtdyyJNNmmXDppga5GB7kSw==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^10.4.1", + "minimatch": "^10.2.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/test-exclude/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/test-exclude/node_modules/brace-expansion": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", + "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/test-exclude/node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/test-exclude/node_modules/glob/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/test-exclude/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/test-exclude/node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/tinybench": { "version": "2.9.0", "dev": true, @@ -6772,6 +7406,48 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/wrap-ansi/node_modules/ansi-regex": { "version": "5.0.1", "dev": true, @@ -6874,6 +7550,7 @@ }, "devDependencies": { "@types/node": "^24.3.0", + "@vitest/coverage-v8": "^3.2.4", "typescript": "^5.7.3", "vitest": "^3.2.4" }, diff --git a/packages/agent/docs/agent-harness.md b/packages/agent/docs/agent-harness.md index 37b14cb7..6dc19634 100644 --- a/packages/agent/docs/agent-harness.md +++ b/packages/agent/docs/agent-harness.md @@ -1,6 +1,6 @@ # AgentHarness lifecycle -`AgentHarness` is the orchestration layer above the low-level `Agent`. It owns session persistence, runtime configuration, resource resolution, operation locking, and extension-facing mutation semantics. +`AgentHarness` is the orchestration layer above the low-level agent loop. It owns session persistence, runtime configuration, resource resolution, operation locking, and extension-facing mutation semantics. This document describes the current direction and implemented behavior. Some extension/session-facade details are planned and called out explicitly. @@ -15,6 +15,7 @@ The intended rule is: - runtime config setters update future snapshots without mutating the current provider request - session writes made while busy are durably queued and flushed in deterministic order - getters return latest harness config, not in-flight snapshots +- listeners/hooks currently receive no facade; if they close over the raw harness and call settlement APIs such as `waitForIdle()` during the active run, they can deadlock. A future facade should expose `runWhenIdle()` instead. A final lifecycle hardening pass should prove these guarantees with a broad listener/hook reentrancy test suite. @@ -123,8 +124,8 @@ Phase/settlement semantics are still provisional and need a full lifecycle pass. Queue modes are live, not turn-snapshotted: -- `steeringMode` -- `followUpMode` +- `getSteeringMode()` / `setSteeringMode()` +- `getFollowUpMode()` / `setFollowUpMode()` Changing a queue mode during a run affects the next queue drain. Queue drains happen at safe points. @@ -151,17 +152,17 @@ If the system-prompt callback throws while starting `prompt`, `skill`, or `promp ## Hooks and events -Current hooks receive only the event payload. There is no extension context object yet. +Current hooks and listeners receive only the event payload. There is no extension context object yet. Event payloads describe what is happening. Harness getters describe latest config for future snapshots. The split between harness-specific events (`AgentHarnessOwnEvent`) and the union of low-level plus harness events (`AgentHarnessEvent`) is provisional but useful for distinguishing hookable harness events from public subscription events. -A future extension context may expose the harness and a queued-write session facade. +A future extension context should expose a harness facade plus a queued-write session facade. The facade must not expose APIs that can deadlock the current event dispatch. In particular, listeners/hooks should not call `waitForIdle()` for the active run; expose a `runWhenIdle(() => Promise)` scheduling API instead. This is future extension-context work; current listeners/hooks receive only payloads and no safe harness facade. ## Planned session facade -Extensions should eventually interact with a harness-scoped session facade rather than the raw session. +Extensions should eventually interact with a harness-scoped `HarnessSession` facade rather than the raw session. The facade should wrap the internal session and enforce harness pending-write ordering semantics. Once this exists, hooks and event listeners can receive a context that exposes the full `AgentHarness` plus the session facade without giving direct access to unordered raw session writes. Planned read semantics: @@ -183,7 +184,9 @@ Agent-emitted messages are persisted on `message_end` to preserve transcript ord ## Abort -Abort is allowed during a turn. It aborts the low-level run and clears low-level steering/follow-up queues. +Abort is allowed during a turn. It aborts the low-level run and clears steering/follow-up queues. + +Abort does not clear `nextTurn` messages. Messages queued with `nextTurn()` survive abort and are inserted before the user message on the next user-initiated turn. Abort does not discard pending session writes. Pending writes flush at the next save point if reached, at `agent_end`, or in operation failure cleanup. @@ -205,7 +208,7 @@ Harness tests should stay focused by area instead of growing one large catch-all Current structure: -- `packages/agent/test/harness/agent-harness.test.ts`: basic construction/API smoke tests. +- `packages/agent/test/harness/agent-harness.test.ts`: core lifecycle and public API behavior. - `packages/agent/test/harness/agent-harness-stream.test.ts`: stream options and provider hook semantics. Preferred future structure: @@ -216,27 +219,40 @@ Preferred future structure: Use the `pi-ai` faux provider (`registerFauxProvider`, `fauxAssistantMessage`) for deterministic harness/provider tests. Faux response factories can inspect `StreamOptions`, invoke `options.onPayload`, and return scripted assistant messages without real provider APIs or network access. +Harness coverage is configured separately from the default package test run: + +```bash +npm run test:harness +npm run coverage:harness +``` + +`coverage:harness` runs `test/harness/**/*.test.ts` and reports coverage for `src/harness/**/*.ts` plus the non-harness runtime files it directly exercises (`src/agent.ts` and `src/agent-loop.ts`) into `coverage/harness`. Type-only dependencies such as `src/types.ts` are not included because they have no meaningful runtime coverage. + ## Implementation todo This list tracks the remaining work before treating `AgentHarness` as migration-ready. ### 1. Remove `Agent` dependency from `AgentHarness` -New top priority. +Implemented. -`AgentHarness` should likely call `agentLoop` / `agentLoopContinue` directly instead of owning an internal `Agent` instance. The harness already owns session persistence, runtime config snapshots, queues, provider stream configuration, hooks/events, phase semantics, and abort semantics. Keeping `Agent` in the middle creates duplicated state and adapter seams. +`AgentHarness` now calls `runAgentLoop()` directly instead of owning an internal `Agent` instance. The harness owns active run/abort-controller lifecycle, queue draining, provider stream configuration, event reduction, session persistence, pending write flushing, and save-point snapshot refresh. -Still needed: +Implemented validation: -- Replace internal `new Agent(...)` with direct low-level loop calls. -- Move active run/abort-controller lifecycle into `AgentHarness`. -- Move queue draining into `AgentHarness` only, removing duplicated low-level `Agent` queues. -- Reduce low-level `AgentEvent` state directly in the harness where needed. -- Preserve current public behavior for `prompt`, `skill`, `promptFromTemplate`, `steer`, `followUp`, `nextTurn`, `abort`, and `waitForIdle`. -- Preserve provider hook behavior implemented by the harness stream wrapper. -- Preserve save-point snapshot refresh semantics without side-effecting through `Agent.prepareNextTurn`. -- Decide whether `AgentHarness.agent` remains temporarily for compatibility or is removed before migration. -- Add tests covering parity with the current harness behavior before and after the refactor. +- prompt construction and public runtime config getters/mutators +- steering queue draining and `queue_update` emission +- follow-up queue draining and `queue_update` emission +- `before_agent_start` message ordering and persistence +- abort clearing steer/follow-up queues while preserving `nextTurn` messages +- thrown hook failure cleanup with persisted assistant error messages and settlement +- save-point refresh for model, thinking level, resources, system prompt, and active tools +- pending listener session write ordering after agent-emitted messages +- external `waitForIdle()` waiting for awaited listeners and run settlement +- `tool_call` and `tool_result` hook behavior through the direct loop +- provider stream wrapper behavior in `agent-harness-stream.test.ts` + +Remaining lifecycle hardening beyond this refactor is tracked in the final lifecycle hardening suite. ### 2. Finish curated provider/stream configuration @@ -294,8 +310,10 @@ Current cleanup already done: Still needed: -- Define extension context shape. -- Likely expose a harness facade plus a session facade rather than raw internals. +- Define extension/listener context shape. +- Expose a harness facade plus a session facade rather than raw internals. +- The harness facade should expose safe runtime APIs and `runWhenIdle(() => Promise)`; it should not expose active-run `waitForIdle()` to listeners/hooks. +- The session facade should wrap the internal session and participate in pending session write queue semantics so writes remain ordered with agent-emitted messages. - Decide which public harness APIs are allowed from each hook/event. - Decide whether hooks can mutate turn snapshots directly or only through explicit hook results/public APIs. - Clarify event payload semantics versus harness getter semantics. @@ -315,9 +333,9 @@ Implemented so far: - `setActiveTools(toolNames)` - invalid active tool names currently throw; convert to result errors - generic common app tool shape via `AgentHarness` -- `QueueMode` exported from `Agent` +- `QueueMode` exported from core types - `AgentHarnessOptions.steeringMode` / `followUpMode` -- live `steeringMode` / `followUpMode` getters/setters +- live `getSteeringMode()` / `setSteeringMode()` and `getFollowUpMode()` / `setFollowUpMode()` methods - queue modes are immediate/live, matching coding-agent behavior Still needed: @@ -355,7 +373,7 @@ Still needed: - Implement retry handling. - Ensure structural operations use consistent `try/finally` phase cleanup. - Verify `before_agent_start` hook semantics against coding-agent: - - current behavior prepends returned messages. + - current behavior appends returned messages after the user/next-turn prompt messages. - decide whether replacement, prepend, append, or transform semantics are correct. - Decide if `before_agent_start` needs more turn info such as tools/tool snippets. - Document or change timing for model/thinking/stream-option events that may fire before queued session entries persist while busy. @@ -373,6 +391,7 @@ Implemented so far: - Updated skill and prompt-template loaders to consume `ExecutionEnv` results. - Updated shell output capture to return a result and use `ExecutionEnv` instead of Node APIs directly, including full-output spill via `appendFile()`. - Removed `NodeExecutionEnv` from the browser-safe `execution-env.ts` re-export; Node-specific callers import from `harness/env/nodejs.js`. +- Replaced `Buffer` usage in generic truncation utilities with runtime-neutral UTF-8 handling. - Expanded `NodeExecutionEnv` tests for file operations, exec errors, aborts, callbacks, timeouts, and shell-output full-output spill. Still needed: @@ -382,7 +401,7 @@ Still needed: - Convert structural `AgentHarness` operations to typed result returns for busy, missing-resource, auth, compaction, and branch-summary failures. - Convert compaction helpers to typed result returns. - Keep Node-specific APIs isolated under `src/harness/env/nodejs.ts` and Node-backed storage/session implementations, or move those implementations behind explicit Node-only entry points. -- Replace Node globals in generic harness utilities, especially `Buffer` usage in truncation utilities, with runtime-neutral implementations. +- Audit remaining generic harness utilities for Node globals as new APIs are added. - Audit package exports so browser/generic-JS imports do not pull Node-only modules such as `NodeExecutionEnv` or JSONL storage. - Keep expanding `ExecutionEnv` and shell-output contract tests as the API evolves, especially for non-Node implementations. - Add tests proving harness APIs return `ok: false` instead of throwing for expected failure paths. diff --git a/packages/agent/package.json b/packages/agent/package.json index 853b8180..c2d2695b 100644 --- a/packages/agent/package.json +++ b/packages/agent/package.json @@ -14,6 +14,8 @@ "build": "tsgo -p tsconfig.build.json", "dev": "tsgo -p tsconfig.build.json --watch --preserveWatchOutput", "test": "vitest --run", + "test:harness": "vitest --run --config vitest.harness.config.ts", + "coverage:harness": "vitest --run --config vitest.harness.config.ts --coverage", "prepublishOnly": "npm run clean && npm run build" }, "dependencies": { @@ -41,6 +43,7 @@ }, "devDependencies": { "@types/node": "^24.3.0", + "@vitest/coverage-v8": "^3.2.4", "typescript": "^5.7.3", "vitest": "^3.2.4" } diff --git a/packages/agent/src/agent.ts b/packages/agent/src/agent.ts index 6eafd030..59c06415 100644 --- a/packages/agent/src/agent.ts +++ b/packages/agent/src/agent.ts @@ -21,10 +21,13 @@ import type { AgentTool, BeforeToolCallContext, BeforeToolCallResult, + QueueMode, StreamFn, ToolExecutionMode, } from "./types.js"; +export type { QueueMode } from "./types.js"; + function defaultConvertToLlm(messages: AgentMessage[]): Message[] { return messages.filter( (message) => message.role === "user" || message.role === "assistant" || message.role === "toolResult", @@ -53,8 +56,6 @@ const DEFAULT_MODEL = { maxTokens: 0, } satisfies Model; -export type QueueMode = "all" | "one-at-a-time"; - type MutableAgentState = Omit & { isStreaming: boolean; streamingMessage?: AgentMessage; diff --git a/packages/agent/src/harness/agent-harness.ts b/packages/agent/src/harness/agent-harness.ts index 879a3599..eb009414 100644 --- a/packages/agent/src/harness/agent-harness.ts +++ b/packages/agent/src/harness/agent-harness.ts @@ -5,10 +5,20 @@ import { streamSimple, type UserMessage, } from "@earendil-works/pi-ai"; -import { Agent, type QueueMode } from "../agent.js"; -import type { AgentEvent, AgentMessage, AgentTool, ThinkingLevel } from "../types.js"; +import { runAgentLoop } from "../agent-loop.js"; +import type { + AgentContext, + AgentEvent, + AgentLoopConfig, + AgentMessage, + AgentTool, + QueueMode, + StreamFn, + ThinkingLevel, +} from "../types.js"; import { collectEntriesForBranchSummary, generateBranchSummary } from "./compaction/branch-summarization.js"; import { compact, DEFAULT_COMPACTION_SETTINGS, prepareCompaction } from "./compaction/compaction.js"; +import { convertToLlm } from "./messages.js"; import { formatPromptTemplateInvocation } from "./prompt-templates.js"; import { formatSkillInvocation } from "./skills.js"; import type { @@ -35,6 +45,27 @@ function createUserMessage(text: string, images?: ImageContent[]): UserMessage { return { role: "user", content, timestamp: Date.now() }; } +function createFailureMessage(model: Model, error: unknown, aborted: boolean): AssistantMessage { + return { + role: "assistant", + content: [{ type: "text", text: "" }], + api: model.api, + provider: model.provider, + model: model.id, + stopReason: aborted ? "aborted" : "error", + errorMessage: error instanceof Error ? error.message : String(error), + timestamp: Date.now(), + usage: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + totalTokens: 0, + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 }, + }, + }; +} + function cloneStreamOptions(streamOptions?: AgentHarnessStreamOptions): AgentHarnessStreamOptions { return { ...streamOptions, @@ -54,10 +85,6 @@ function mergeHeaders(...headers: Array | undefined>): Re return hasHeaders ? merged : undefined; } -function hasOwn(object: object, key: PropertyKey): boolean { - return Object.hasOwn(object, key); -} - function applyStreamOptionsPatch( base: AgentHarnessStreamOptions, patch?: AgentHarnessStreamOptionsPatch, @@ -65,13 +92,13 @@ function applyStreamOptionsPatch( const result = cloneStreamOptions(base); if (!patch) return result; - if (hasOwn(patch, "transport")) result.transport = patch.transport; - if (hasOwn(patch, "timeoutMs")) result.timeoutMs = patch.timeoutMs; - if (hasOwn(patch, "maxRetries")) result.maxRetries = patch.maxRetries; - if (hasOwn(patch, "maxRetryDelayMs")) result.maxRetryDelayMs = patch.maxRetryDelayMs; - if (hasOwn(patch, "cacheRetention")) result.cacheRetention = patch.cacheRetention; + if (Object.hasOwn(patch, "transport")) result.transport = patch.transport; + if (Object.hasOwn(patch, "timeoutMs")) result.timeoutMs = patch.timeoutMs; + if (Object.hasOwn(patch, "maxRetries")) result.maxRetries = patch.maxRetries; + if (Object.hasOwn(patch, "maxRetryDelayMs")) result.maxRetryDelayMs = patch.maxRetryDelayMs; + if (Object.hasOwn(patch, "cacheRetention")) result.cacheRetention = patch.cacheRetention; - if (hasOwn(patch, "headers")) { + if (Object.hasOwn(patch, "headers")) { if (patch.headers === undefined) { result.headers = undefined; } else { @@ -84,7 +111,7 @@ function applyStreamOptionsPatch( } } - if (hasOwn(patch, "metadata")) { + if (Object.hasOwn(patch, "metadata")) { if (patch.metadata === undefined) { result.metadata = undefined; } else { @@ -100,6 +127,10 @@ function applyStreamOptionsPatch( return result; } +const SUBSCRIBER_EVENT_TYPE = "*"; + +type AgentHarnessHandler = (event: any, signal?: AbortSignal) => Promise | any; + interface AgentHarnessTurnState< TSkill extends Skill = Skill, TPromptTemplate extends PromptTemplate = PromptTemplate, @@ -121,72 +152,28 @@ export class AgentHarness< TPromptTemplate extends PromptTemplate = PromptTemplate, TTool extends AgentTool = AgentTool, > { - readonly agent: Agent; readonly env: ExecutionEnv; private session: Session; + private phase: AgentHarnessPhase = "idle"; + private runAbortController?: AbortController; + private runPromise?: Promise; + private pendingSessionWrites: PendingSessionWrite[] = []; private model: Model; private thinkingLevel: ThinkingLevel; - private activeToolNames: string[]; - private nextTurnQueue: AgentMessage[] = []; - private phase: AgentHarnessPhase = "idle"; - private steerQueue: UserMessage[] = []; - private followUpQueue: UserMessage[] = []; - private pendingSessionWrites: PendingSessionWrite[] = []; - private resources: AgentHarnessResources; - private streamOptions: AgentHarnessStreamOptions; - private appliedStreamOptions: AgentHarnessStreamOptions = {}; - private appliedSessionId?: string; private systemPrompt: AgentHarnessOptions["systemPrompt"]; + private streamOptions: AgentHarnessStreamOptions; private getApiKeyAndHeaders?: AgentHarnessOptions["getApiKeyAndHeaders"]; + private resources: AgentHarnessResources; private tools = new Map(); - private listeners = new Set< - (event: AgentHarnessEvent, signal?: AbortSignal) => Promise | void - >(); - private hooks = new Map Promise | any>>(); + private activeToolNames: string[]; + private steerQueue: UserMessage[] = []; + private steeringQueueMode: QueueMode; + private followUpQueue: UserMessage[] = []; + private followUpQueueMode: QueueMode; + private nextTurnQueue: AgentMessage[] = []; + private handlers = new Map>(); constructor(options: AgentHarnessOptions) { - this.agent = new Agent({ - initialState: { - model: options.model, - thinkingLevel: options.thinkingLevel, - tools: options.tools ?? [], - }, - streamFn: async (model, context, streamOptions) => { - const auth = await this.getApiKeyAndHeaders?.(model); - const snapshotOptions: AgentHarnessStreamOptions = { - ...this.appliedStreamOptions, - headers: mergeHeaders(this.appliedStreamOptions.headers, auth?.headers), - }; - const requestOptions = await this.emitBeforeProviderRequest( - model, - this.appliedSessionId ?? "", - snapshotOptions, - ); - return streamSimple(model, context, { - cacheRetention: requestOptions.cacheRetention, - headers: requestOptions.headers, - maxRetries: requestOptions.maxRetries, - maxRetryDelayMs: requestOptions.maxRetryDelayMs, - metadata: requestOptions.metadata, - onPayload: async (payload) => await this.emitBeforeProviderPayload(model, payload), - onResponse: async (response) => { - const headers = { ...(response.headers as Record) }; - await this.emitOwn( - { type: "after_provider_response", status: response.status, headers }, - this.agent.signal, - ); - }, - reasoning: streamOptions?.reasoning, - signal: streamOptions?.signal, - sessionId: this.appliedSessionId, - timeoutMs: requestOptions.timeoutMs, - transport: requestOptions.transport, - apiKey: auth?.apiKey, - }); - }, - steeringMode: options.steeringMode, - followUpMode: options.followUpMode, - }); this.env = options.env; this.session = options.session; this.resources = options.resources ?? {}; @@ -197,64 +184,24 @@ export class AgentHarness< this.tools.set(tool.name, tool); } this.model = options.model; - this.thinkingLevel = options.thinkingLevel ?? this.agent.state.thinkingLevel; + this.thinkingLevel = options.thinkingLevel ?? "off"; this.activeToolNames = options.activeToolNames ?? (options.tools ?? []).map((tool) => tool.name); - this.agent.state.model = this.model; - this.agent.state.thinkingLevel = this.thinkingLevel; - this.agent.transformContext = async (messages) => { - const result = await this.emitHook({ type: "context", messages: [...messages] }); - return result?.messages ?? messages; - }; - this.agent.beforeToolCall = async ({ toolCall, args }) => { - const result = await this.emitHook({ - type: "tool_call", - toolCallId: toolCall.id, - toolName: toolCall.name, - input: args as Record, - }); - return result ? { block: result.block, reason: result.reason } : undefined; - }; - this.agent.afterToolCall = async ({ toolCall, args, result, isError }) => { - const patch = await this.emitHook({ - type: "tool_result", - toolCallId: toolCall.id, - toolName: toolCall.name, - input: args as Record, - content: result.content, - details: result.details, - isError, - }); - return patch - ? { content: patch.content, details: patch.details, isError: patch.isError, terminate: patch.terminate } - : undefined; - }; - this.agent.prepareNextTurn = async () => { - await this.flushPendingSessionWrites(); - const turnState = await this.createTurnState(); - this.applyTurnState(turnState); - return { - context: { - systemPrompt: turnState.systemPrompt, - messages: turnState.messages.slice(), - tools: turnState.activeTools.slice(), - }, - model: turnState.model, - thinkingLevel: turnState.thinkingLevel, - }; - }; - this.agent.subscribe(async (event, signal) => { - await this.handleAgentEvent(event, signal); - }); + this.steeringQueueMode = options.steeringMode ?? "one-at-a-time"; + this.followUpQueueMode = options.followUpMode ?? "one-at-a-time"; + } + + private getHandlers(type: string): Set | undefined { + return this.handlers.get(type); } private async emitOwn(event: AgentHarnessOwnEvent, signal?: AbortSignal): Promise { - for (const listener of this.listeners) { + for (const listener of this.getHandlers(SUBSCRIBER_EVENT_TYPE) ?? []) { await listener(event, signal); } } private async emitAny(event: AgentHarnessEvent, signal?: AbortSignal): Promise { - for (const listener of this.listeners) { + for (const listener of this.getHandlers(SUBSCRIBER_EVENT_TYPE) ?? []) { await listener(event, signal); } } @@ -262,7 +209,7 @@ export class AgentHarness< private async emitHook( event: Extract, ): Promise { - const handlers = this.hooks.get(event.type as TType); + const handlers = this.getHandlers(event.type as TType); if (!handlers || handlers.size === 0) return undefined; let lastResult: AgentHarnessEventResultMap[TType] | undefined; for (const handler of handlers) { @@ -279,7 +226,7 @@ export class AgentHarness< sessionId: string, streamOptions: AgentHarnessStreamOptions, ): Promise { - const handlers = this.hooks.get("before_provider_request"); + const handlers = this.getHandlers("before_provider_request"); let current = cloneStreamOptions(streamOptions); if (!handlers || handlers.size === 0) return current; for (const handler of handlers) { @@ -297,7 +244,7 @@ export class AgentHarness< } private async emitBeforeProviderPayload(model: Model, payload: unknown): Promise { - const handlers = this.hooks.get("before_provider_payload"); + const handlers = this.getHandlers("before_provider_payload"); let current = payload; if (!handlers || handlers.size === 0) return current; for (const handler of handlers) { @@ -318,6 +265,17 @@ export class AgentHarness< }); } + private startRunPromise(): () => void { + let finish = () => {}; + this.runPromise = new Promise((resolve) => { + finish = resolve; + }); + return () => { + this.runPromise = undefined; + finish(); + }; + } + private async createTurnState(): Promise> { const context = await this.session.buildContext(); const resources = this.getResources(); @@ -352,14 +310,105 @@ export class AgentHarness< }; } - private applyTurnState(turnState: AgentHarnessTurnState): void { - this.agent.state.messages = turnState.messages; - this.appliedStreamOptions = cloneStreamOptions(turnState.streamOptions); - this.appliedSessionId = turnState.sessionId; - this.agent.state.systemPrompt = turnState.systemPrompt; - this.agent.state.model = turnState.model; - this.agent.state.thinkingLevel = turnState.thinkingLevel; - this.agent.state.tools = turnState.activeTools; + private createContext( + turnState: AgentHarnessTurnState, + systemPrompt?: string, + ): AgentContext { + return { + systemPrompt: systemPrompt ?? turnState.systemPrompt, + messages: turnState.messages.slice(), + tools: turnState.activeTools.slice(), + }; + } + + private createStreamFn(getTurnState: () => AgentHarnessTurnState): StreamFn { + return async (model, context, streamOptions) => { + const turnState = getTurnState(); + const auth = await this.getApiKeyAndHeaders?.(model); + const snapshotOptions: AgentHarnessStreamOptions = { + ...turnState.streamOptions, + headers: mergeHeaders(turnState.streamOptions.headers, auth?.headers), + }; + const requestOptions = await this.emitBeforeProviderRequest(model, turnState.sessionId, snapshotOptions); + return streamSimple(model, context, { + cacheRetention: requestOptions.cacheRetention, + headers: requestOptions.headers, + maxRetries: requestOptions.maxRetries, + maxRetryDelayMs: requestOptions.maxRetryDelayMs, + metadata: requestOptions.metadata, + onPayload: async (payload) => await this.emitBeforeProviderPayload(model, payload), + onResponse: async (response) => { + const headers = { ...(response.headers as Record) }; + await this.emitOwn( + { type: "after_provider_response", status: response.status, headers }, + streamOptions?.signal, + ); + }, + reasoning: streamOptions?.reasoning, + signal: streamOptions?.signal, + sessionId: turnState.sessionId, + timeoutMs: requestOptions.timeoutMs, + transport: requestOptions.transport, + apiKey: auth?.apiKey, + }); + }; + } + + private async drainQueuedMessages(queue: AgentMessage[], mode: QueueMode): Promise { + const messages = mode === "all" ? queue.splice(0) : queue.splice(0, 1); + if (messages.length > 0) await this.emitQueueUpdate(); + return messages; + } + + private createLoopConfig( + getTurnState: () => AgentHarnessTurnState, + setTurnState: (turnState: AgentHarnessTurnState) => void, + ): AgentLoopConfig { + const turnState = getTurnState(); + return { + model: turnState.model, + reasoning: turnState.thinkingLevel === "off" ? undefined : turnState.thinkingLevel, + convertToLlm, + transformContext: async (messages) => { + const result = await this.emitHook({ type: "context", messages: [...messages] }); + return result?.messages ?? messages; + }, + beforeToolCall: async ({ toolCall, args }) => { + const result = await this.emitHook({ + type: "tool_call", + toolCallId: toolCall.id, + toolName: toolCall.name, + input: args as Record, + }); + return result ? { block: result.block, reason: result.reason } : undefined; + }, + afterToolCall: async ({ toolCall, args, result, isError }) => { + const patch = await this.emitHook({ + type: "tool_result", + toolCallId: toolCall.id, + toolName: toolCall.name, + input: args as Record, + content: result.content, + details: result.details, + isError, + }); + return patch + ? { content: patch.content, details: patch.details, isError: patch.isError, terminate: patch.terminate } + : undefined; + }, + prepareNextTurn: async () => { + await this.flushPendingSessionWrites(); + const nextTurnState = await this.createTurnState(); + setTurnState(nextTurnState); + return { + context: this.createContext(nextTurnState), + model: nextTurnState.model, + thinkingLevel: nextTurnState.thinkingLevel, + }; + }, + getSteeringMessages: async () => this.drainQueuedMessages(this.steerQueue, this.steeringQueueMode), + getFollowUpMessages: async () => this.drainQueuedMessages(this.followUpQueue, this.followUpQueueMode), + }; } private validateToolNames(toolNames: string[]): void { @@ -391,19 +440,6 @@ export class AgentHarness< private async handleAgentEvent(event: AgentEvent, signal?: AbortSignal): Promise { await this.emitAny(event, signal); - if (event.type === "message_start" && event.message.role === "user") { - const steerIndex = this.steerQueue.indexOf(event.message); - if (steerIndex !== -1) { - this.steerQueue.splice(steerIndex, 1); - await this.emitQueueUpdate(); - } else { - const followUpIndex = this.followUpQueue.indexOf(event.message); - if (followUpIndex !== -1) { - this.followUpQueue.splice(followUpIndex, 1); - await this.emitQueueUpdate(); - } - } - } if (event.type === "message_end") { await this.session.appendMessage(event.message); } @@ -422,13 +458,26 @@ export class AgentHarness< } } + private async emitRunFailure( + model: Model, + error: unknown, + aborted: boolean, + signal: AbortSignal, + ): Promise { + const failureMessage = createFailureMessage(model, error, aborted); + await this.handleAgentEvent({ type: "message_start", message: failureMessage }, signal); + await this.handleAgentEvent({ type: "message_end", message: failureMessage }, signal); + await this.handleAgentEvent({ type: "turn_end", message: failureMessage, toolResults: [] }, signal); + await this.handleAgentEvent({ type: "agent_end", messages: [failureMessage] }, signal); + return [failureMessage]; + } + private async executeTurn( turnState: AgentHarnessTurnState, text: string, options?: { images?: ImageContent[] }, ): Promise { - this.applyTurnState(turnState); - const beforeLength = this.agent.state.messages.length; + let activeTurnState = turnState; let messages: AgentMessage[] = [createUserMessage(text, options?.images)]; if (this.nextTurnQueue.length > 0) { messages = [...this.nextTurnQueue, messages[0]!]; @@ -442,41 +491,70 @@ export class AgentHarness< systemPrompt: turnState.systemPrompt, resources: turnState.resources, }); - if (beforeResult?.messages) messages = [...beforeResult.messages, ...messages]; - if (beforeResult?.systemPrompt) this.agent.state.systemPrompt = beforeResult.systemPrompt; + if (beforeResult?.messages) messages = [...messages, ...beforeResult.messages]; + + const abortController = new AbortController(); + const getTurnState = () => activeTurnState; + const setTurnState = (nextTurnState: AgentHarnessTurnState) => { + activeTurnState = nextTurnState; + }; + this.runAbortController = abortController; + const runResultPromise = (async () => { + try { + return await runAgentLoop( + messages, + this.createContext(turnState, beforeResult?.systemPrompt), + this.createLoopConfig(getTurnState, setTurnState), + (event) => this.handleAgentEvent(event, abortController.signal), + abortController.signal, + this.createStreamFn(getTurnState), + ); + } catch (error) { + return await this.emitRunFailure( + activeTurnState.model, + error, + abortController.signal.aborted, + abortController.signal, + ); + } + })(); try { - await this.agent.prompt(messages); + const newMessages = await runResultPromise; + for (let i = newMessages.length - 1; i >= 0; i--) { + const message = newMessages[i]!; + if (message.role === "assistant") { + return message; + } + } + throw new Error("AgentHarness prompt completed without an assistant message"); } finally { - await this.flushPendingSessionWrites(); - } - let response: AssistantMessage | undefined; - const newMessages = this.agent.state.messages.slice(beforeLength); - for (let i = newMessages.length - 1; i >= 0; i--) { - const message = newMessages[i]!; - if (message.role === "assistant") { - response = message; - break; + try { + await this.flushPendingSessionWrites(); + } finally { + this.runAbortController = undefined; } } - if (!response) throw new Error("AgentHarness prompt completed without an assistant message"); - return response; } async prompt(text: string, options?: { images?: ImageContent[] }): Promise { if (this.phase !== "idle") throw new Error("AgentHarness is busy"); this.phase = "turn"; + const finishRunPromise = this.startRunPromise(); try { const turnState = await this.createTurnState(); return await this.executeTurn(turnState, text, options); } catch (error) { this.phase = "idle"; throw error; + } finally { + finishRunPromise(); } } async skill(name: string, additionalInstructions?: string): Promise { if (this.phase !== "idle") throw new Error("AgentHarness is busy"); this.phase = "turn"; + const finishRunPromise = this.startRunPromise(); try { const turnState = await this.createTurnState(); const skill = (turnState.resources.skills ?? []).find((candidate) => candidate.name === name); @@ -485,12 +563,15 @@ export class AgentHarness< } catch (error) { this.phase = "idle"; throw error; + } finally { + finishRunPromise(); } } async promptFromTemplate(name: string, args: string[] = []): Promise { if (this.phase !== "idle") throw new Error("AgentHarness is busy"); this.phase = "turn"; + const finishRunPromise = this.startRunPromise(); try { const turnState = await this.createTurnState(); const template = (turnState.resources.promptTemplates ?? []).find((candidate) => candidate.name === name); @@ -499,22 +580,20 @@ export class AgentHarness< } catch (error) { this.phase = "idle"; throw error; + } finally { + finishRunPromise(); } } steer(text: string, options?: { images?: ImageContent[] }): void { if (this.phase === "idle") throw new Error("Cannot steer while idle"); - const message = createUserMessage(text, options?.images); - this.steerQueue.push(message); - this.agent.steer(message); + this.steerQueue.push(createUserMessage(text, options?.images)); void this.emitQueueUpdate(); } followUp(text: string, options?: { images?: ImageContent[] }): void { if (this.phase === "idle") throw new Error("Cannot follow up while idle"); - const message = createUserMessage(text, options?.images); - this.followUpQueue.push(message); - this.agent.followUp(message); + this.followUpQueue.push(createUserMessage(text, options?.images)); void this.emitQueueUpdate(); } @@ -690,11 +769,18 @@ export class AgentHarness< return { cancelled: false, editorText, summaryEntry }; } + getModel(): Model { + return this.model; + } + + getThinkingLevel(): ThinkingLevel { + return this.thinkingLevel; + } + async setModel(model: Model): Promise { const previousModel = this.model; this.model = model; if (this.phase === "idle") { - this.agent.state.model = model; await this.session.appendModelChange(model.provider, model.id); } else { this.pendingSessionWrites.push({ type: "model_change", provider: model.provider, modelId: model.id }); @@ -706,7 +792,6 @@ export class AgentHarness< const previousLevel = this.thinkingLevel; this.thinkingLevel = level; if (this.phase === "idle") { - this.agent.state.thinkingLevel = level; await this.session.appendThinkingLevelChange(level); } else { this.pendingSessionWrites.push({ type: "thinking_level_change", thinkingLevel: level }); @@ -717,25 +802,22 @@ export class AgentHarness< async setActiveTools(toolNames: string[]): Promise { this.validateToolNames(toolNames); this.activeToolNames = [...toolNames]; - if (this.phase === "idle") { - this.agent.state.tools = this.activeToolNames.map((name) => this.tools.get(name)!); - } } - get steeringMode(): QueueMode { - return this.agent.steeringMode; + getSteeringMode(): QueueMode { + return this.steeringQueueMode; } - set steeringMode(mode: QueueMode) { - this.agent.steeringMode = mode; + setSteeringMode(mode: QueueMode): void { + this.steeringQueueMode = mode; } - get followUpMode(): QueueMode { - return this.agent.followUpMode; + getFollowUpMode(): QueueMode { + return this.followUpQueueMode; } - set followUpMode(mode: QueueMode) { - this.agent.followUpMode = mode; + setFollowUpMode(mode: QueueMode): void { + this.followUpQueueMode = mode; } getResources(): AgentHarnessResources { @@ -770,9 +852,6 @@ export class AgentHarness< } else { this.validateToolNames(this.activeToolNames); } - if (this.phase === "idle") { - this.agent.state.tools = this.activeToolNames.map((name) => this.tools.get(name)!); - } } async abort(): Promise { @@ -780,23 +859,27 @@ export class AgentHarness< const clearedFollowUp = [...this.followUpQueue]; this.steerQueue = []; this.followUpQueue = []; - this.agent.clearAllQueues(); await this.emitQueueUpdate(); - this.agent.abort(); - await this.agent.waitForIdle(); + this.runAbortController?.abort(); + await this.waitForIdle(); await this.emitOwn({ type: "abort", clearedSteer, clearedFollowUp }); return { clearedSteer, clearedFollowUp }; } async waitForIdle(): Promise { - await this.agent.waitForIdle(); + await this.runPromise; } subscribe( listener: (event: AgentHarnessEvent, signal?: AbortSignal) => Promise | void, ): () => void { - this.listeners.add(listener); - return () => this.listeners.delete(listener); + let handlers = this.handlers.get(SUBSCRIBER_EVENT_TYPE); + if (!handlers) { + handlers = new Set(); + this.handlers.set(SUBSCRIBER_EVENT_TYPE, handlers); + } + handlers.add(listener as AgentHarnessHandler); + return () => handlers!.delete(listener as AgentHarnessHandler); } on( @@ -805,12 +888,12 @@ export class AgentHarness< event: Extract, ) => Promise | AgentHarnessEventResultMap[TType], ): () => void { - let handlers = this.hooks.get(type); + let handlers = this.handlers.get(type); if (!handlers) { handlers = new Set(); - this.hooks.set(type, handlers); + this.handlers.set(type, handlers); } - handlers.add(handler as any); - return () => handlers!.delete(handler as any); + handlers.add(handler as AgentHarnessHandler); + return () => handlers!.delete(handler as AgentHarnessHandler); } } diff --git a/packages/agent/src/harness/types.ts b/packages/agent/src/harness/types.ts index d80f2a21..9df2a563 100644 --- a/packages/agent/src/harness/types.ts +++ b/packages/agent/src/harness/types.ts @@ -1,6 +1,5 @@ import type { ImageContent, Model, SimpleStreamOptions, TextContent, Transport } from "@earendil-works/pi-ai"; -import type { QueueMode } from "../agent.js"; -import type { AgentEvent, AgentMessage, AgentTool, ThinkingLevel } from "../index.js"; +import type { AgentEvent, AgentMessage, AgentTool, QueueMode, ThinkingLevel } from "../index.js"; import type { Session } from "./session/session.js"; /** Result of a fallible operation. Expected failures are returned as `ok: false` instead of thrown. */ diff --git a/packages/agent/src/harness/utils/truncate.ts b/packages/agent/src/harness/utils/truncate.ts index 18ac5d74..1870f423 100644 --- a/packages/agent/src/harness/utils/truncate.ts +++ b/packages/agent/src/harness/utils/truncate.ts @@ -44,6 +44,64 @@ export interface TruncationOptions { maxBytes?: number; } +interface RuntimeBuffer { + byteLength(content: string, encoding: "utf8"): number; +} + +const runtimeBuffer = (globalThis as { Buffer?: RuntimeBuffer }).Buffer; +const nonAsciiPattern = /[^\x00-\x7f]/; + +function utf8ByteLength(content: string): number { + if (runtimeBuffer) return runtimeBuffer.byteLength(content, "utf8"); + + const firstNonAscii = content.search(nonAsciiPattern); + if (firstNonAscii === -1) return content.length; + + let bytes = firstNonAscii; + for (let i = firstNonAscii; i < content.length; i++) { + const code = content.charCodeAt(i); + if (code <= 0x7f) { + bytes += 1; + } else if (code <= 0x7ff) { + bytes += 2; + } else if (code >= 0xd800 && code <= 0xdbff && i + 1 < content.length) { + const next = content.charCodeAt(i + 1); + if (next >= 0xdc00 && next <= 0xdfff) { + bytes += 4; + i++; + } else { + bytes += 3; + } + } else { + bytes += 3; + } + } + return bytes; +} + +function replaceUnpairedSurrogates(content: string): string { + let output = ""; + for (let i = 0; i < content.length; i++) { + const code = content.charCodeAt(i); + if (code >= 0xd800 && code <= 0xdbff) { + if (i + 1 < content.length) { + const next = content.charCodeAt(i + 1); + if (next >= 0xdc00 && next <= 0xdfff) { + output += content[i] + content[i + 1]; + i++; + continue; + } + } + output += "�"; + } else if (code >= 0xdc00 && code <= 0xdfff) { + output += "�"; + } else { + output += content[i]; + } + } + return output; +} + /** * Format bytes as human-readable size. */ @@ -68,7 +126,7 @@ export function truncateHead(content: string, options: TruncationOptions = {}): const maxLines = options.maxLines ?? DEFAULT_MAX_LINES; const maxBytes = options.maxBytes ?? DEFAULT_MAX_BYTES; - const totalBytes = Buffer.byteLength(content, "utf-8"); + const totalBytes = utf8ByteLength(content); const lines = content.split("\n"); const totalLines = lines.length; @@ -90,7 +148,7 @@ export function truncateHead(content: string, options: TruncationOptions = {}): } // Check if first line alone exceeds byte limit - const firstLineBytes = Buffer.byteLength(lines[0], "utf-8"); + const firstLineBytes = utf8ByteLength(lines[0]); if (firstLineBytes > maxBytes) { return { content: "", @@ -114,7 +172,7 @@ export function truncateHead(content: string, options: TruncationOptions = {}): for (let i = 0; i < lines.length && i < maxLines; i++) { const line = lines[i]; - const lineBytes = Buffer.byteLength(line, "utf-8") + (i > 0 ? 1 : 0); // +1 for newline + const lineBytes = utf8ByteLength(line) + (i > 0 ? 1 : 0); // +1 for newline if (outputBytesCount + lineBytes > maxBytes) { truncatedBy = "bytes"; @@ -131,7 +189,7 @@ export function truncateHead(content: string, options: TruncationOptions = {}): } const outputContent = outputLinesArr.join("\n"); - const finalOutputBytes = Buffer.byteLength(outputContent, "utf-8"); + const finalOutputBytes = utf8ByteLength(outputContent); return { content: outputContent, @@ -158,7 +216,7 @@ export function truncateTail(content: string, options: TruncationOptions = {}): const maxLines = options.maxLines ?? DEFAULT_MAX_LINES; const maxBytes = options.maxBytes ?? DEFAULT_MAX_BYTES; - const totalBytes = Buffer.byteLength(content, "utf-8"); + const totalBytes = utf8ByteLength(content); const lines = content.split("\n"); const totalLines = lines.length; @@ -187,7 +245,7 @@ export function truncateTail(content: string, options: TruncationOptions = {}): for (let i = lines.length - 1; i >= 0 && outputLinesArr.length < maxLines; i--) { const line = lines[i]; - const lineBytes = Buffer.byteLength(line, "utf-8") + (outputLinesArr.length > 0 ? 1 : 0); // +1 for newline + const lineBytes = utf8ByteLength(line) + (outputLinesArr.length > 0 ? 1 : 0); // +1 for newline if (outputBytesCount + lineBytes > maxBytes) { truncatedBy = "bytes"; @@ -196,7 +254,7 @@ export function truncateTail(content: string, options: TruncationOptions = {}): if (outputLinesArr.length === 0) { const truncatedLine = truncateStringToBytesFromEnd(line, maxBytes); outputLinesArr.unshift(truncatedLine); - outputBytesCount = Buffer.byteLength(truncatedLine, "utf-8"); + outputBytesCount = utf8ByteLength(truncatedLine); lastLinePartial = true; } break; @@ -212,7 +270,7 @@ export function truncateTail(content: string, options: TruncationOptions = {}): } const outputContent = outputLinesArr.join("\n"); - const finalOutputBytes = Buffer.byteLength(outputContent, "utf-8"); + const finalOutputBytes = utf8ByteLength(outputContent); return { content: outputContent, @@ -234,20 +292,40 @@ export function truncateTail(content: string, options: TruncationOptions = {}): * Handles multi-byte UTF-8 characters correctly. */ function truncateStringToBytesFromEnd(str: string, maxBytes: number): string { - const buf = Buffer.from(str, "utf-8"); - if (buf.length <= maxBytes) { - return str; + if (maxBytes <= 0) return ""; + + let outputBytes = 0; + let start = str.length; + let needsReplacement = false; + for (let i = str.length; i > 0; ) { + let characterStart = i - 1; + const code = str.charCodeAt(characterStart); + let characterBytes: number; + let unpairedSurrogate = false; + if (code >= 0xdc00 && code <= 0xdfff && characterStart > 0) { + const previous = str.charCodeAt(characterStart - 1); + if (previous >= 0xd800 && previous <= 0xdbff) { + characterStart--; + characterBytes = 4; + } else { + characterBytes = 3; + unpairedSurrogate = true; + } + } else if (code >= 0xd800 && code <= 0xdfff) { + characterBytes = 3; + unpairedSurrogate = true; + } else { + characterBytes = code <= 0x7f ? 1 : code <= 0x7ff ? 2 : 3; + } + if (outputBytes + characterBytes > maxBytes) break; + outputBytes += characterBytes; + start = characterStart; + needsReplacement ||= unpairedSurrogate; + i = characterStart; } - // Start from the end, skip maxBytes back - let start = buf.length - maxBytes; - - // Find a valid UTF-8 boundary (start of a character) - while (start < buf.length && (buf[start] & 0xc0) === 0x80) { - start++; - } - - return buf.slice(start).toString("utf-8"); + const output = str.slice(start); + return needsReplacement ? replaceUnpairedSurrogates(output) : output; } /** diff --git a/packages/agent/src/types.ts b/packages/agent/src/types.ts index 285c1b02..f24d2496 100644 --- a/packages/agent/src/types.ts +++ b/packages/agent/src/types.ts @@ -35,6 +35,14 @@ export type StreamFn = ( */ export type ToolExecutionMode = "sequential" | "parallel"; +/** + * Controls how many queued user messages are injected when the agent loop reaches a queue drain point. + * + * - "all": drain and inject every queued message at that point. + * - "one-at-a-time": drain and inject only the oldest queued message, leaving the rest queued for later drain points. + */ +export type QueueMode = "all" | "one-at-a-time"; + /** A single tool call content block emitted by an assistant message. */ export type AgentToolCall = Extract; diff --git a/packages/agent/test/harness/agent-harness.test.ts b/packages/agent/test/harness/agent-harness.test.ts index f8111f70..e0b4497e 100644 --- a/packages/agent/test/harness/agent-harness.test.ts +++ b/packages/agent/test/harness/agent-harness.test.ts @@ -1,11 +1,13 @@ -import { getModel } from "@earendil-works/pi-ai"; -import { describe, expect, it } from "vitest"; +import { fauxAssistantMessage, fauxToolCall, getModel, registerFauxProvider } from "@earendil-works/pi-ai"; +import { afterEach, describe, expect, it } from "vitest"; import { AgentHarness } from "../../src/harness/agent-harness.js"; import { NodeExecutionEnv } from "../../src/harness/env/nodejs.js"; import { Session } from "../../src/harness/session/session.js"; import { InMemorySessionStorage } from "../../src/harness/session/storage/memory.js"; import type { PromptTemplate, Skill } from "../../src/harness/types.js"; -import type { AgentTool } from "../../src/types.js"; +import type { AgentMessage, AgentTool } from "../../src/types.js"; +import { calculateTool } from "../utils/calculate.js"; +import { getCurrentTimeTool } from "../utils/get-current-time.js"; interface AppSkill extends Skill { source: "project" | "user"; @@ -19,6 +21,39 @@ interface AppTool extends AgentTool { source: "builtin" | "extension"; } +const registrations: Array<{ unregister(): void }> = []; + +function textFromUserMessages(messages: Array<{ role: string; content: unknown }>): string[] { + return messages.flatMap((message) => { + if (message.role !== "user") return []; + if (typeof message.content === "string") return [message.content]; + if (!Array.isArray(message.content)) return []; + return message.content.flatMap((part) => { + if (!part || typeof part !== "object" || !("type" in part) || part.type !== "text") return []; + return "text" in part && typeof part.text === "string" ? [part.text] : []; + }); + }); +} + +function deferred(): { promise: Promise; resolve: () => void } { + let resolve = () => {}; + const promise = new Promise((resolvePromise) => { + resolve = resolvePromise; + }); + return { promise, resolve }; +} + +function getReasoning(options: unknown): unknown { + if (!options || typeof options !== "object" || !("reasoning" in options)) return undefined; + return options.reasoning; +} + +afterEach(() => { + for (const registration of registrations.splice(0)) { + registration.unregister(); + } +}); + describe("AgentHarness", () => { it("constructs directly and exposes queue modes", () => { const session = new Session(new InMemorySessionStorage()); @@ -28,18 +63,399 @@ describe("AgentHarness", () => { env, session, model: initialModel, + thinkingLevel: "high", systemPrompt: "You are helpful.", steeringMode: "all", followUpMode: "all", }); expect(harness.env).toBe(env); - expect(harness.agent.state.model).toBe(initialModel); - expect(harness.steeringMode).toBe("all"); - expect(harness.followUpMode).toBe("all"); - harness.steeringMode = "one-at-a-time"; - harness.followUpMode = "one-at-a-time"; - expect(harness.agent.steeringMode).toBe("one-at-a-time"); - expect(harness.agent.followUpMode).toBe("one-at-a-time"); + expect(harness.getModel()).toBe(initialModel); + expect(harness.getThinkingLevel()).toBe("high"); + expect(harness.getSteeringMode()).toBe("all"); + expect(harness.getFollowUpMode()).toBe("all"); + harness.setSteeringMode("one-at-a-time"); + harness.setFollowUpMode("one-at-a-time"); + expect(harness.getSteeringMode()).toBe("one-at-a-time"); + expect(harness.getFollowUpMode()).toBe("one-at-a-time"); + }); + + it("drains one queued steering message at a time and emits queue updates", async () => { + const registration = registerFauxProvider(); + registrations.push(registration); + const userCounts: number[] = []; + registration.setResponses([ + (context) => { + userCounts.push(context.messages.filter((message) => message.role === "user").length); + return fauxAssistantMessage("first"); + }, + (context) => { + userCounts.push(context.messages.filter((message) => message.role === "user").length); + return fauxAssistantMessage("second"); + }, + (context) => { + userCounts.push(context.messages.filter((message) => message.role === "user").length); + return fauxAssistantMessage("third"); + }, + ]); + const harness = new AgentHarness({ + env: new NodeExecutionEnv({ cwd: process.cwd() }), + session: new Session(new InMemorySessionStorage()), + model: registration.getModel(), + steeringMode: "one-at-a-time", + }); + const steerQueueLengths: number[] = []; + let queued = false; + harness.subscribe((event) => { + if (event.type === "queue_update") { + steerQueueLengths.push(event.steer.length); + } + if (event.type === "message_start" && event.message.role === "assistant" && !queued) { + queued = true; + harness.steer("one"); + harness.steer("two"); + } + }); + + await harness.prompt("hello"); + + expect(userCounts).toEqual([1, 2, 3]); + expect(steerQueueLengths).toEqual([1, 2, 1, 0]); + }); + + it("appends before_agent_start messages and persists them", async () => { + const registration = registerFauxProvider(); + registrations.push(registration); + let requestText: string[] = []; + registration.setResponses([ + (context) => { + requestText = textFromUserMessages(context.messages); + return fauxAssistantMessage("ok"); + }, + ]); + const session = new Session(new InMemorySessionStorage()); + const harness = new AgentHarness({ + env: new NodeExecutionEnv({ cwd: process.cwd() }), + session, + model: registration.getModel(), + }); + harness.on("before_agent_start", () => ({ + messages: [{ role: "user", content: [{ type: "text", text: "hook" }], timestamp: Date.now() }], + })); + + await harness.prompt("hello"); + + const persistedText = (await session.getEntries()).flatMap((entry) => { + if (entry.type !== "message" || entry.message.role !== "user") return []; + const content = entry.message.content; + if (typeof content === "string") return [content]; + return content.flatMap((part) => (part.type === "text" ? [part.text] : [])); + }); + expect(requestText).toEqual(["hello", "hook"]); + expect(persistedText).toEqual(["hello", "hook"]); + }); + + it("abort clears steer and follow-up queues but preserves next-turn messages", async () => { + const registration = registerFauxProvider(); + registrations.push(registration); + let releaseFirstResponse: (() => void) | undefined; + let abortedSignal: AbortSignal | undefined; + const firstResponseReleased = new Promise((resolve) => { + releaseFirstResponse = resolve; + }); + const secondRequestText: string[] = []; + registration.setResponses([ + async (_context, options) => { + abortedSignal = options?.signal; + await firstResponseReleased; + return fauxAssistantMessage("aborted-ish"); + }, + (context) => { + secondRequestText.push(...textFromUserMessages(context.messages)); + return fauxAssistantMessage("second"); + }, + ]); + const harness = new AgentHarness({ + env: new NodeExecutionEnv({ cwd: process.cwd() }), + session: new Session(new InMemorySessionStorage()), + model: registration.getModel(), + }); + const queueUpdates: Array<{ steer: number; followUp: number; nextTurn: number }> = []; + harness.subscribe((event) => { + if (event.type === "queue_update") { + queueUpdates.push({ + steer: event.steer.length, + followUp: event.followUp.length, + nextTurn: event.nextTurn.length, + }); + } + }); + + const firstPrompt = harness.prompt("first"); + await new Promise((resolve) => setTimeout(resolve, 0)); + harness.steer("steer"); + harness.followUp("follow"); + harness.nextTurn("next"); + const abortResultPromise = harness.abort(); + await new Promise((resolve) => setTimeout(resolve, 0)); + expect(abortedSignal?.aborted).toBe(true); + releaseFirstResponse?.(); + const abortResult = await abortResultPromise; + await firstPrompt; + await harness.prompt("second"); + + expect(abortResult.clearedSteer).toHaveLength(1); + expect(abortResult.clearedFollowUp).toHaveLength(1); + expect(queueUpdates).toContainEqual({ steer: 0, followUp: 0, nextTurn: 1 }); + expect(secondRequestText).toEqual(["first", "next", "second"]); + }); + + it("drains follow-up messages one at a time after the agent would otherwise stop", async () => { + const registration = registerFauxProvider(); + registrations.push(registration); + const userCounts: number[] = []; + registration.setResponses([ + (context) => { + userCounts.push(context.messages.filter((message) => message.role === "user").length); + return fauxAssistantMessage("first"); + }, + (context) => { + userCounts.push(context.messages.filter((message) => message.role === "user").length); + return fauxAssistantMessage("second"); + }, + (context) => { + userCounts.push(context.messages.filter((message) => message.role === "user").length); + return fauxAssistantMessage("third"); + }, + ]); + const harness = new AgentHarness({ + env: new NodeExecutionEnv({ cwd: process.cwd() }), + session: new Session(new InMemorySessionStorage()), + model: registration.getModel(), + followUpMode: "one-at-a-time", + }); + const followUpQueueLengths: number[] = []; + let queued = false; + harness.subscribe((event) => { + if (event.type === "queue_update") { + followUpQueueLengths.push(event.followUp.length); + } + if (event.type === "message_start" && event.message.role === "assistant" && !queued) { + queued = true; + harness.followUp("one"); + harness.followUp("two"); + } + }); + + await harness.prompt("hello"); + + expect(userCounts).toEqual([1, 2, 3]); + expect(followUpQueueLengths).toEqual([1, 2, 1, 0]); + }); + + it("settles thrown hook failures with persisted assistant error messages", async () => { + const registration = registerFauxProvider(); + registrations.push(registration); + registration.setResponses([() => fauxAssistantMessage("should not be used")]); + const session = new Session(new InMemorySessionStorage()); + const harness = new AgentHarness({ + env: new NodeExecutionEnv({ cwd: process.cwd() }), + session, + model: registration.getModel(), + }); + const events: string[] = []; + harness.subscribe((event) => { + events.push(event.type); + }); + harness.on("context", () => { + throw new Error("context exploded"); + }); + + const response = await harness.prompt("hello"); + await expect(harness.prompt("after failure")).resolves.toMatchObject({ role: "assistant" }); + + const entries = await session.getEntries(); + const messages = entries.flatMap((entry) => (entry.type === "message" ? [entry.message] : [])); + expect(response.stopReason).toBe("error"); + expect(response.errorMessage).toBe("context exploded"); + expect(messages[0]?.role).toBe("user"); + expect(messages[1]).toMatchObject({ role: "assistant", stopReason: "error", errorMessage: "context exploded" }); + expect(events).toContain("agent_end"); + expect(events).toContain("settled"); + }); + + it("refreshes model, thinking level, resources, system prompt, and active tools at save points", async () => { + const registration = registerFauxProvider({ + models: [ + { id: "first", reasoning: true }, + { id: "second", reasoning: true }, + ], + }); + registrations.push(registration); + const secondModel = registration.getModel("second"); + if (!secondModel) throw new Error("missing second faux model"); + const captured: Array<{ modelId: string; reasoning: unknown; systemPrompt: string; tools: string[] }> = []; + registration.setResponses([ + (context, options, _state, model) => { + captured.push({ + modelId: model.id, + reasoning: getReasoning(options), + systemPrompt: context.systemPrompt ?? "", + tools: context.tools?.map((tool) => tool.name) ?? [], + }); + return fauxAssistantMessage(fauxToolCall("calculate", { expression: "1 + 1" }, { id: "call-1" }), { + stopReason: "toolUse", + }); + }, + (context, options, _state, model) => { + captured.push({ + modelId: model.id, + reasoning: getReasoning(options), + systemPrompt: context.systemPrompt ?? "", + tools: context.tools?.map((tool) => tool.name) ?? [], + }); + return fauxAssistantMessage("done"); + }, + ]); + const harness = new AgentHarness({ + env: new NodeExecutionEnv({ cwd: process.cwd() }), + session: new Session(new InMemorySessionStorage()), + model: registration.getModel(), + thinkingLevel: "off", + resources: { + skills: [{ name: "prompt", description: "prompt", content: "first prompt", filePath: "/skills/prompt" }], + }, + systemPrompt: ({ resources }) => resources.skills?.[0]?.content ?? "missing prompt", + tools: [calculateTool], + }); + harness.subscribe((event) => { + if (event.type === "tool_execution_start") { + void harness.setModel(secondModel); + void harness.setThinkingLevel("high"); + void harness.setResources({ + skills: [ + { name: "prompt", description: "prompt", content: "second prompt", filePath: "/skills/prompt" }, + ], + }); + void harness.setTools([calculateTool, getCurrentTimeTool], [getCurrentTimeTool.name]); + } + }); + + await harness.prompt("hello"); + + expect(captured).toEqual([ + { modelId: "first", reasoning: undefined, systemPrompt: "first prompt", tools: ["calculate"] }, + { modelId: "second", reasoning: "high", systemPrompt: "second prompt", tools: ["get_current_time"] }, + ]); + }); + + it("orders pending listener session writes after agent-emitted messages", async () => { + const registration = registerFauxProvider(); + registrations.push(registration); + registration.setResponses([() => fauxAssistantMessage("ok")]); + const session = new Session(new InMemorySessionStorage()); + const harness = new AgentHarness({ + env: new NodeExecutionEnv({ cwd: process.cwd() }), + session, + model: registration.getModel(), + }); + let wrotePendingMessage = false; + harness.subscribe(async (event) => { + if (event.type === "message_end" && event.message.role === "assistant" && !wrotePendingMessage) { + wrotePendingMessage = true; + await harness.appendMessage({ + role: "custom", + customType: "listener", + content: "listener write", + display: true, + timestamp: Date.now(), + } as AgentMessage); + } + }); + + await harness.prompt("hello"); + + const entries = await session.getEntries(); + const roles = entries.flatMap((entry) => (entry.type === "message" ? [entry.message.role] : [])); + expect(roles).toEqual(["user", "assistant", "custom"]); + }); + + it("waitForIdle waits for external run settlement and awaited listeners", async () => { + const registration = registerFauxProvider(); + registrations.push(registration); + registration.setResponses([() => fauxAssistantMessage("ok")]); + const barrier = deferred(); + const harness = new AgentHarness({ + env: new NodeExecutionEnv({ cwd: process.cwd() }), + session: new Session(new InMemorySessionStorage()), + model: registration.getModel(), + }); + let listenerFinished = false; + harness.subscribe(async (event) => { + if (event.type === "agent_end") { + await barrier.promise; + listenerFinished = true; + } + }); + + const promptPromise = harness.prompt("hello"); + let idleResolved = false; + const idlePromise = harness.waitForIdle().then(() => { + idleResolved = true; + }); + await new Promise((resolve) => setTimeout(resolve, 10)); + expect(idleResolved).toBe(false); + expect(listenerFinished).toBe(false); + barrier.resolve(); + await Promise.all([promptPromise, idlePromise]); + expect(idleResolved).toBe(true); + expect(listenerFinished).toBe(true); + }); + + it("runs tool_call and tool_result hooks through the direct loop", async () => { + const registration = registerFauxProvider(); + registrations.push(registration); + registration.setResponses([ + () => + fauxAssistantMessage(fauxToolCall("calculate", { expression: "2 + 2" }, { id: "call-1" }), { + stopReason: "toolUse", + }), + ]); + const session = new Session(new InMemorySessionStorage()); + const harness = new AgentHarness({ + env: new NodeExecutionEnv({ cwd: process.cwd() }), + session, + model: registration.getModel(), + tools: [calculateTool], + }); + const seenToolCalls: Array<{ id: string; name: string; expression: unknown }> = []; + harness.on("tool_call", (event) => { + seenToolCalls.push({ id: event.toolCallId, name: event.toolName, expression: event.input.expression }); + return undefined; + }); + harness.on("tool_result", (event) => { + expect(event.toolCallId).toBe("call-1"); + expect(event.toolName).toBe("calculate"); + return { + content: [{ type: "text", text: "patched result" }], + details: { patched: true }, + terminate: true, + }; + }); + + await harness.prompt("hello"); + + const toolResult = (await session.getEntries()).find( + (entry) => entry.type === "message" && entry.message.role === "toolResult", + ); + expect(seenToolCalls).toEqual([{ id: "call-1", name: "calculate", expression: "2 + 2" }]); + expect(toolResult).toMatchObject({ + type: "message", + message: { + role: "toolResult", + content: [{ type: "text", text: "patched result" }], + details: { patched: true }, + }, + }); }); it("preserves app resource types for getters and update events", async () => { diff --git a/packages/agent/test/harness/truncate.test.ts b/packages/agent/test/harness/truncate.test.ts new file mode 100644 index 00000000..a859b321 --- /dev/null +++ b/packages/agent/test/harness/truncate.test.ts @@ -0,0 +1,158 @@ +import { describe, expect, it } from "vitest"; +import { truncateHead, truncateTail } from "../../src/harness/utils/truncate.js"; + +const encoder = new TextEncoder(); + +function byteLength(content: string): number { + return encoder.encode(content).length; +} + +function bufferTail(content: string, maxBytes: number): string { + const bytes = Buffer.from(content, "utf8"); + if (bytes.length <= maxBytes) return content; + let start = bytes.length - maxBytes; + while (start < bytes.length && (bytes[start] & 0xc0) === 0x80) start++; + return bytes.subarray(start).toString("utf8"); +} + +function assertMatchesBufferTail(input: string, maxByteValues?: readonly number[]): void { + const totalBytes = Buffer.byteLength(input, "utf8"); + const values = maxByteValues ?? Array.from({ length: totalBytes + 5 }, (_, maxBytes) => maxBytes); + for (const maxBytes of values) { + const result = truncateTail(input, { maxBytes, maxLines: 10 }); + const expected = bufferTail(input, maxBytes); + if (result.content !== expected) { + throw new Error( + `tail mismatch input=${JSON.stringify(input)} maxBytes=${maxBytes} expected=${JSON.stringify(expected)} actual=${JSON.stringify(result.content)}`, + ); + } + const outputBytes = Buffer.byteLength(result.content, "utf8"); + if (outputBytes > maxBytes) { + throw new Error( + `tail output exceeded byte limit input=${JSON.stringify(input)} maxBytes=${maxBytes} outputBytes=${outputBytes}`, + ); + } + } +} + +function sampledByteLimits(input: string): number[] { + const totalBytes = Buffer.byteLength(input, "utf8"); + const candidates = [ + 0, + 1, + 2, + 3, + 4, + 5, + 8, + Math.floor(totalBytes / 2) - 1, + Math.floor(totalBytes / 2), + Math.floor(totalBytes / 2) + 1, + totalBytes - 8, + totalBytes - 5, + totalBytes - 4, + totalBytes - 3, + totalBytes - 2, + totalBytes - 1, + totalBytes, + totalBytes + 1, + totalBytes + 4, + ]; + return [...new Set(candidates.filter((value) => value >= 0))].sort((a, b) => a - b); +} + +describe("truncate utilities", () => { + it("counts UTF-8 bytes without Node Buffer", () => { + const content = "aé🙂\nb"; + const result = truncateHead(content, { maxBytes: 100, maxLines: 10 }); + + expect(result.truncated).toBe(false); + expect(result.totalBytes).toBe(byteLength(content)); + expect(result.outputBytes).toBe(byteLength(content)); + expect(result.totalBytes).toBe(9); + }); + + it("truncates head on UTF-8 byte limits without partial lines", () => { + const content = "éé\nabc"; + const result = truncateHead(content, { maxBytes: 4, maxLines: 10 }); + + expect(result.content).toBe("éé"); + expect(result.truncated).toBe(true); + expect(result.truncatedBy).toBe("bytes"); + expect(result.outputBytes).toBe(4); + expect(result.firstLineExceedsLimit).toBe(false); + }); + + it("reports head truncation when the first line exceeds the byte limit", () => { + const result = truncateHead("éé\nabc", { maxBytes: 3, maxLines: 10 }); + + expect(result.content).toBe(""); + expect(result.truncated).toBe(true); + expect(result.truncatedBy).toBe("bytes"); + expect(result.firstLineExceedsLimit).toBe(true); + }); + + it("truncates tail on UTF-8 boundaries when only a partial last line fits", () => { + const result = truncateTail("aé🙂b", { maxBytes: 5, maxLines: 10 }); + + expect(result.content).toBe("🙂b"); + expect(result.truncated).toBe(true); + expect(result.truncatedBy).toBe("bytes"); + expect(result.lastLinePartial).toBe(true); + expect(result.outputBytes).toBe(5); + }); + + it("drops an oversized trailing character when it cannot fit in tail byte limit", () => { + const result = truncateTail("abc🙂", { maxBytes: 3, maxLines: 10 }); + + expect(result.content).toBe(""); + expect(result.truncated).toBe(true); + expect(result.truncatedBy).toBe("bytes"); + expect(result.lastLinePartial).toBe(true); + expect(result.outputBytes).toBe(0); + }); + + it("matches Buffer tail truncation semantics for surrogate edge cases", () => { + const inputs = ["a\ud83d", "\ude42b", "a\ude42b", "\ud83d\ud83d\ude42", "\ud83d\ude42\ude42", "👩‍💻"]; + for (const input of inputs) assertMatchesBufferTail(input); + }); + + it("matches Buffer tail truncation semantics across deterministic fuzz cases", () => { + const alphabet = [ + "a", + "\u007f", + "\u0080", + "é", + "\u07ff", + "\u0800", + "中", + "\ud7ff", + "\ud800", + "\ud83d", + "\udc00", + "\ude42", + "🙂", + "\ue000", + "\uffff", + ]; + + function checkExhaustive(prefix: string, depth: number): void { + assertMatchesBufferTail(prefix, sampledByteLimits(prefix)); + if (depth === 0) return; + for (const character of alphabet) checkExhaustive(prefix + character, depth - 1); + } + checkExhaustive("", 3); + + let seed = 0x12345678; + function random(): number { + seed = (seed * 1664525 + 1013904223) >>> 0; + return seed / 0x100000000; + } + for (let i = 0; i < 1_000; i++) { + let input = ""; + const length = Math.floor(random() * 80); + for (let j = 0; j < length; j++) input += alphabet[Math.floor(random() * alphabet.length)]; + assertMatchesBufferTail(input, sampledByteLimits(input)); + } + }); +}); diff --git a/packages/agent/vitest.harness.config.ts b/packages/agent/vitest.harness.config.ts new file mode 100644 index 00000000..9421e5a9 --- /dev/null +++ b/packages/agent/vitest.harness.config.ts @@ -0,0 +1,18 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + globals: true, + environment: "node", + testTimeout: 30000, + include: ["test/harness/**/*.test.ts"], + coverage: { + provider: "v8", + all: true, + include: ["src/harness/**/*.ts", "src/agent.ts", "src/agent-loop.ts"], + exclude: ["src/**/*.d.ts"], + reporter: ["text", "html", "lcov"], + reportsDirectory: "coverage/harness", + }, + }, +}); From a31ce0f47fb6f37980f20808c8510482728881dc Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Thu, 14 May 2026 21:56:45 +0200 Subject: [PATCH 039/166] refactor(agent): return results from compaction helpers --- packages/agent/docs/agent-harness.md | 2 +- packages/agent/src/harness/agent-harness.ts | 24 +- .../src/harness/compaction/compaction.ts | 85 ++-- packages/agent/src/harness/types.ts | 16 + .../agent/test/harness/compaction.test.ts | 388 ++++++++++++++++-- 5 files changed, 452 insertions(+), 63 deletions(-) diff --git a/packages/agent/docs/agent-harness.md b/packages/agent/docs/agent-harness.md index 6dc19634..e6a56db6 100644 --- a/packages/agent/docs/agent-harness.md +++ b/packages/agent/docs/agent-harness.md @@ -392,6 +392,7 @@ Implemented so far: - Updated shell output capture to return a result and use `ExecutionEnv` instead of Node APIs directly, including full-output spill via `appendFile()`. - Removed `NodeExecutionEnv` from the browser-safe `execution-env.ts` re-export; Node-specific callers import from `harness/env/nodejs.js`. - Replaced `Buffer` usage in generic truncation utilities with runtime-neutral UTF-8 handling. +- Converted compaction summary helpers to typed result returns and added error-path coverage. - Expanded `NodeExecutionEnv` tests for file operations, exec errors, aborts, callbacks, timeouts, and shell-output full-output spill. Still needed: @@ -399,7 +400,6 @@ Still needed: - Remove remaining throws from `src/harness` APIs and helpers, except explicit adapter/test helpers such as `getOrThrow()`. - Convert session storage/repo/session APIs to typed result returns. - Convert structural `AgentHarness` operations to typed result returns for busy, missing-resource, auth, compaction, and branch-summary failures. -- Convert compaction helpers to typed result returns. - Keep Node-specific APIs isolated under `src/harness/env/nodejs.ts` and Node-backed storage/session implementations, or move those implementations behind explicit Node-only entry points. - Audit remaining generic harness utilities for Node globals as new APIs are added. - Audit package exports so browser/generic-JS imports do not pull Node-only modules such as `NodeExecutionEnv` or JSONL storage. diff --git a/packages/agent/src/harness/agent-harness.ts b/packages/agent/src/harness/agent-harness.ts index eb009414..5b78316e 100644 --- a/packages/agent/src/harness/agent-harness.ts +++ b/packages/agent/src/harness/agent-harness.ts @@ -634,17 +634,19 @@ export class AgentHarness< throw new Error("Compaction cancelled"); } const provided = hookResult?.compaction; - const result = - provided ?? - (await compact( - preparation, - model, - auth.apiKey, - auth.headers, - customInstructions, - undefined, - this.thinkingLevel, - )); + const compactResult = provided + ? { ok: true as const, value: provided } + : await compact( + preparation, + model, + auth.apiKey, + auth.headers, + customInstructions, + undefined, + this.thinkingLevel, + ); + if (!compactResult.ok) throw compactResult.error; + const result = compactResult.value; const entryId = await this.session.appendCompaction( result.summary, result.firstKeptEntryId, diff --git a/packages/agent/src/harness/compaction/compaction.ts b/packages/agent/src/harness/compaction/compaction.ts index 298dc86d..79a76ef1 100644 --- a/packages/agent/src/harness/compaction/compaction.ts +++ b/packages/agent/src/harness/compaction/compaction.ts @@ -15,7 +15,7 @@ import { createCustomMessage, } from "../messages.js"; import { buildSessionContext } from "../session/session.js"; -import type { CompactionEntry, SessionTreeEntry } from "../types.js"; +import { type CompactionEntry, CompactionError, err, ok, type Result, type SessionTreeEntry } from "../types.js"; import { computeFileLists, createFileOps, @@ -544,7 +544,7 @@ export async function generateSummary( customInstructions?: string, previousSummary?: string, thinkingLevel?: ThinkingLevel, -): Promise { +): Promise> { const maxTokens = Math.min( Math.floor(0.8 * reserveTokens), model.maxTokens > 0 ? model.maxTokens : Number.POSITIVE_INFINITY, @@ -581,14 +581,28 @@ export async function generateSummary( ? { maxTokens, signal, apiKey, headers, reasoning: thinkingLevel } : { maxTokens, signal, apiKey, headers }; - const response = await completeSimple( + const responseResult: Result = await completeSimple( model, { systemPrompt: SUMMARIZATION_SYSTEM_PROMPT, messages: summarizationMessages }, completionOptions, + ).then( + (response) => ok(response), + (error: unknown) => + err(new CompactionError("unknown", "Summarization request failed", error)), ); + if (!responseResult.ok) return err(responseResult.error); + const response = responseResult.value; + if (response.stopReason === "aborted") { + return err(new CompactionError("aborted", response.errorMessage || "Summarization aborted")); + } if (response.stopReason === "error") { - throw new Error(`Summarization failed: ${response.errorMessage || "Unknown error"}`); + return err( + new CompactionError( + "summarization_failed", + `Summarization failed: ${response.errorMessage || "Unknown error"}`, + ), + ); } const textContent = response.content @@ -596,7 +610,7 @@ export async function generateSummary( .map((c) => c.text) .join("\n"); - return textContent; + return ok(textContent); } // ============================================================================ @@ -734,7 +748,7 @@ export async function compact( customInstructions?: string, signal?: AbortSignal, thinkingLevel?: ThinkingLevel, -): Promise { +): Promise> { const { firstKeptEntryId, messagesToSummarize, @@ -746,11 +760,13 @@ export async function compact( settings, } = preparation; - // Generate summaries (can be parallel if both needed) and merge into one + if (!firstKeptEntryId) { + return err(new CompactionError("invalid_session", "First kept entry has no UUID - session may need migration")); + } + let summary: string; if (isSplitTurn && turnPrefixMessages.length > 0) { - // Generate both summaries in parallel const [historyResult, turnPrefixResult] = await Promise.all([ messagesToSummarize.length > 0 ? generateSummary( @@ -764,7 +780,7 @@ export async function compact( previousSummary, thinkingLevel, ) - : Promise.resolve("No prior history."), + : Promise.resolve(ok("No prior history.")), generateTurnPrefixSummary( turnPrefixMessages, model, @@ -775,11 +791,11 @@ export async function compact( thinkingLevel, ), ]); - // Merge into single summary - summary = `${historyResult}\n\n---\n\n**Turn Context (split turn):**\n\n${turnPrefixResult}`; + if (!historyResult.ok) return err(historyResult.error); + if (!turnPrefixResult.ok) return err(turnPrefixResult.error); + summary = `${historyResult.value}\n\n---\n\n**Turn Context (split turn):**\n\n${turnPrefixResult.value}`; } else { - // Just generate history summary - summary = await generateSummary( + const summaryResult = await generateSummary( messagesToSummarize, model, settings.reserveTokens, @@ -790,22 +806,19 @@ export async function compact( previousSummary, thinkingLevel, ); + if (!summaryResult.ok) return err(summaryResult.error); + summary = summaryResult.value; } - // Compute file lists and append to summary const { readFiles, modifiedFiles } = computeFileLists(fileOps); summary += formatFileOperations(readFiles, modifiedFiles); - if (!firstKeptEntryId) { - throw new Error("First kept entry has no UUID - session may need migration"); - } - - return { + return ok({ summary, firstKeptEntryId, tokensBefore, details: { readFiles, modifiedFiles } as CompactionDetails, - }; + }); } /** @@ -819,7 +832,7 @@ async function generateTurnPrefixSummary( headers?: Record, signal?: AbortSignal, thinkingLevel?: ThinkingLevel, -): Promise { +): Promise> { const maxTokens = Math.min( Math.floor(0.5 * reserveTokens), model.maxTokens > 0 ? model.maxTokens : Number.POSITIVE_INFINITY, @@ -835,20 +848,38 @@ async function generateTurnPrefixSummary( }, ]; - const response = await completeSimple( + const responseResult: Result = await completeSimple( model, { systemPrompt: SUMMARIZATION_SYSTEM_PROMPT, messages: summarizationMessages }, model.reasoning && thinkingLevel && thinkingLevel !== "off" ? { maxTokens, signal, apiKey, headers, reasoning: thinkingLevel } : { maxTokens, signal, apiKey, headers }, + ).then( + (response) => ok(response), + (error: unknown) => + err( + new CompactionError("unknown", "Turn prefix summarization request failed", error), + ), ); + if (!responseResult.ok) return err(responseResult.error); + const response = responseResult.value; + if (response.stopReason === "aborted") { + return err(new CompactionError("aborted", response.errorMessage || "Turn prefix summarization aborted")); + } if (response.stopReason === "error") { - throw new Error(`Turn prefix summarization failed: ${response.errorMessage || "Unknown error"}`); + return err( + new CompactionError( + "summarization_failed", + `Turn prefix summarization failed: ${response.errorMessage || "Unknown error"}`, + ), + ); } - return response.content - .filter((c): c is { type: "text"; text: string } => c.type === "text") - .map((c) => c.text) - .join("\n"); + return ok( + response.content + .filter((c): c is { type: "text"; text: string } => c.type === "text") + .map((c) => c.text) + .join("\n"), + ); } diff --git a/packages/agent/src/harness/types.ts b/packages/agent/src/harness/types.ts index 9df2a563..91e2d87a 100644 --- a/packages/agent/src/harness/types.ts +++ b/packages/agent/src/harness/types.ts @@ -138,6 +138,22 @@ export class ExecutionError extends Error { } } +/** Stable compaction error codes returned by compaction helpers. */ +export type CompactionErrorCode = "aborted" | "summarization_failed" | "invalid_session" | "unknown"; + +/** Error returned by compaction helpers. */ +export class CompactionError extends Error { + constructor( + /** Backend-independent error code. */ + public code: CompactionErrorCode, + message: string, + cause?: unknown, + ) { + super(message, cause === undefined ? undefined : { cause }); + this.name = "CompactionError"; + } +} + /** Metadata for one filesystem object in an {@link ExecutionEnv}. */ export interface FileInfo { /** Basename of {@link path}. */ diff --git a/packages/agent/test/harness/compaction.test.ts b/packages/agent/test/harness/compaction.test.ts index d39be48b..a9ec08fa 100644 --- a/packages/agent/test/harness/compaction.test.ts +++ b/packages/agent/test/harness/compaction.test.ts @@ -2,6 +2,7 @@ import { type AssistantMessage, type FauxProviderRegistration, fauxAssistantMessage, + type Message, type Model, registerFauxProvider, type Usage, @@ -13,21 +14,27 @@ import { compact, DEFAULT_COMPACTION_SETTINGS, estimateContextTokens, + estimateTokens, findCutPoint, + findTurnStartIndex, generateSummary, + getLastAssistantUsage, prepareCompaction, serializeConversation, shouldCompact, } from "../../src/harness/compaction/compaction.js"; import { buildSessionContext } from "../../src/harness/session/session.js"; import type { + BranchSummaryEntry, CompactionEntry, CompactionSettings, + CustomMessageEntry, MessageEntry, ModelChangeEntry, SessionTreeEntry, ThinkingLevelChangeEntry, } from "../../src/harness/types.js"; +import { getOrThrow } from "../../src/harness/types.js"; import type { AgentMessage } from "../../src/types.js"; let nextId = 0; @@ -179,6 +186,133 @@ describe("harness compaction", () => { expect(entries[result.firstKeptEntryIndex]?.type).toBe("message"); }); + it("covers cut-point and turn-start edge cases", () => { + const thinking = createThinkingLevelEntry("high"); + const modelChange = createModelChangeEntry("openai", "gpt-4", thinking.id); + expect(findCutPoint([thinking, modelChange], 0, 2, 1)).toEqual({ + firstKeptEntryIndex: 0, + turnStartIndex: -1, + isSplitTurn: false, + }); + + const branchSummary: BranchSummaryEntry = { + type: "branch_summary", + id: createId(), + parentId: modelChange.id, + timestamp: new Date().toISOString(), + fromId: "branch", + summary: "branch summary", + }; + const customMessage: CustomMessageEntry = { + type: "custom_message", + id: createId(), + parentId: branchSummary.id, + timestamp: new Date().toISOString(), + customType: "note", + content: "custom content", + display: true, + }; + expect(findTurnStartIndex([thinking, branchSummary], 1, 0)).toBe(1); + expect(findTurnStartIndex([thinking, customMessage], 1, 0)).toBe(1); + expect(findTurnStartIndex([thinking, modelChange], 1, 0)).toBe(-1); + + const result = findCutPoint([thinking, branchSummary, customMessage], 0, 3, 1); + expect(result.firstKeptEntryIndex).toBe(0); + + const toolResult = createMessageEntry({ + role: "toolResult", + toolCallId: "call-1", + toolName: "read", + content: [{ type: "text", text: "tool output" }], + isError: false, + timestamp: Date.now(), + }); + expect(findCutPoint([toolResult], 0, 1, 1)).toEqual({ + firstKeptEntryIndex: 0, + turnStartIndex: -1, + isSplitTurn: false, + }); + + const user = createMessageEntry(createUserMessage("user")); + const compaction = createCompactionEntry("summary", user.id, user.id); + const assistant = createMessageEntry(createAssistantMessage("assistant"), compaction.id); + expect(findCutPoint([user, compaction, assistant], 0, 3, 1).firstKeptEntryIndex).toBe(2); + }); + + it("estimates tokens and context usage across supported message roles", () => { + const usage = createMockUsage(10, 5, 3, 2); + const assistant = createAssistantMessage("assistant", usage); + const assistantWithThinkingAndTool: AssistantMessage = { + ...assistant, + content: [ + { type: "thinking", thinking: "thinking" }, + { type: "toolCall", id: "call-1", name: "read", arguments: { path: "file.ts" } }, + ], + }; + const customString: AgentMessage = { + role: "custom", + customType: "note", + content: "custom text", + display: true, + timestamp: Date.now(), + }; + const toolResultWithImage: AgentMessage = { + role: "toolResult", + toolCallId: "call-1", + toolName: "read", + content: [ + { type: "text", text: "tool text" }, + { type: "image", mimeType: "image/png", data: "abc" }, + ], + isError: false, + timestamp: Date.now(), + }; + const bashExecution: AgentMessage = { + role: "bashExecution", + command: "npm run check", + output: "ok", + exitCode: 0, + cancelled: false, + truncated: false, + timestamp: Date.now(), + }; + const branchSummaryMessage: AgentMessage = { + role: "branchSummary", + summary: "branch", + fromId: "x", + timestamp: Date.now(), + }; + const compactionSummaryMessage: AgentMessage = { + role: "compactionSummary", + summary: "compact", + tokensBefore: 123, + timestamp: Date.now(), + }; + + expect(estimateTokens({ role: "user", content: "plain user", timestamp: Date.now() })).toBeGreaterThan(0); + expect(estimateTokens(assistantWithThinkingAndTool)).toBeGreaterThan(0); + expect(estimateTokens(customString)).toBeGreaterThan(0); + expect(estimateTokens(toolResultWithImage)).toBeGreaterThan(1000); + expect(estimateTokens(bashExecution)).toBeGreaterThan(0); + expect(estimateTokens(branchSummaryMessage)).toBeGreaterThan(0); + expect(estimateTokens(compactionSummaryMessage)).toBeGreaterThan(0); + expect(estimateTokens({ role: "unknown", timestamp: Date.now() } as unknown as AgentMessage)).toBe(0); + expect( + getLastAssistantUsage([createMessageEntry(createUserMessage("user")), createMessageEntry(assistant)]), + ).toBe(usage); + expect( + getLastAssistantUsage([ + createMessageEntry({ ...assistant, stopReason: "aborted" }), + createMessageEntry({ ...assistant, stopReason: "error" }), + ]), + ).toBeUndefined(); + expect(estimateContextTokens([createUserMessage("no usage")]).lastUsageIndex).toBeNull(); + expect(estimateContextTokens([assistant, createUserMessage("tail")])).toMatchObject({ + usageTokens: 20, + lastUsageIndex: 0, + }); + }); + it("builds session context with a compaction entry", () => { const u1 = createMessageEntry(createUserMessage("1")); const a1 = createMessageEntry(createAssistantMessage("a"), u1.id); @@ -218,6 +352,67 @@ describe("harness compaction", () => { expect(preparation?.tokensBefore).toBe(estimateContextTokens(buildSessionContext(pathEntries).messages).tokens); }); + it("prepares split-turn compaction with prior file-operation details", () => { + const u1 = createMessageEntry(createUserMessage("user msg 1")); + const assistantMessage: AssistantMessage = { + ...createAssistantMessage("assistant msg 1"), + content: [{ type: "toolCall", id: "tool-1", name: "write", arguments: { path: "written.ts" } }], + }; + const a1 = createMessageEntry(assistantMessage, u1.id); + const compaction1: CompactionEntry = { + ...createCompactionEntry("First summary", u1.id, a1.id), + details: { readFiles: ["old-read.ts"], modifiedFiles: ["old-edit.ts"] }, + }; + const u2 = createMessageEntry(createUserMessage("large turn"), compaction1.id); + const a2 = createMessageEntry(createAssistantMessage("large assistant message"), u2.id); + const preparation = prepareCompaction([u1, a1, compaction1, u2, a2], { + enabled: true, + reserveTokens: 100, + keepRecentTokens: 1, + }); + + expect(preparation).toMatchObject({ previousSummary: "First summary", isSplitTurn: true }); + expect(preparation?.turnPrefixMessages.map((message) => message.role)).toEqual(["user"]); + expect([...preparation!.fileOps.read]).toContain("old-read.ts"); + expect([...preparation!.fileOps.edited]).toContain("old-edit.ts"); + expect([...preparation!.fileOps.written]).toContain("written.ts"); + }); + + it("prepares custom and branch summary entries for summarization", () => { + const branchSummary: BranchSummaryEntry = { + type: "branch_summary", + id: createId(), + parentId: null, + timestamp: new Date().toISOString(), + fromId: "branch", + summary: "branch summary", + }; + const customMessage: CustomMessageEntry = { + type: "custom_message", + id: createId(), + parentId: branchSummary.id, + timestamp: new Date().toISOString(), + customType: "note", + content: "custom content", + display: true, + }; + const user = createMessageEntry(createUserMessage("keep"), customMessage.id); + const assistant = createMessageEntry(createAssistantMessage("assistant"), user.id); + const preparation = prepareCompaction([branchSummary, customMessage, user, assistant], { + enabled: true, + reserveTokens: 100, + keepRecentTokens: 1, + }); + + expect(preparation?.messagesToSummarize.map((message) => message.role)).toEqual(["branchSummary", "custom"]); + }); + + it("does not prepare compaction when there is nothing valid to compact", () => { + const compaction = createCompactionEntry("already compacted", "entry-keep"); + expect(prepareCompaction([compaction], DEFAULT_COMPACTION_SETTINGS)).toBeUndefined(); + expect(prepareCompaction([], DEFAULT_COMPACTION_SETTINGS)).toBeUndefined(); + }); + it("serializes conversation with truncated tool results", () => { const longContent = "x".repeat(5000); const messages = convertMessages([ @@ -245,16 +440,18 @@ describe("harness compaction", () => { return fauxAssistantMessage("## Goal\nTest summary"); }, ]); - await generateSummary( - messages, - reasoningModel, - 2000, - "test-key", - undefined, - undefined, - undefined, - undefined, - "medium", + getOrThrow( + await generateSummary( + messages, + reasoningModel, + 2000, + "test-key", + undefined, + undefined, + undefined, + undefined, + "medium", + ), ); expect(seenOptions[0]).toMatchObject({ reasoning: "medium", apiKey: "test-key" }); @@ -265,7 +462,9 @@ describe("harness compaction", () => { return fauxAssistantMessage("## Goal\nTest summary"); }, ]); - await generateSummary(messages, offModel, 2000, "test-key", undefined, undefined, undefined, undefined, "off"); + getOrThrow( + await generateSummary(messages, offModel, 2000, "test-key", undefined, undefined, undefined, undefined, "off"), + ); expect(seenOptions[1]).not.toHaveProperty("reasoning"); const { faux: fauxNonReasoning, model: nonReasoningModel } = createFauxModel(false); @@ -275,20 +474,76 @@ describe("harness compaction", () => { return fauxAssistantMessage("## Goal\nTest summary"); }, ]); - await generateSummary( - messages, - nonReasoningModel, - 2000, - "test-key", - undefined, - undefined, - undefined, - undefined, - "medium", + getOrThrow( + await generateSummary( + messages, + nonReasoningModel, + 2000, + "test-key", + undefined, + undefined, + undefined, + undefined, + "medium", + ), ); expect(seenOptions[2]).not.toHaveProperty("reasoning"); }); + it("includes previous summaries and custom instructions in generateSummary prompts", async () => { + const messages: AgentMessage[] = [createUserMessage("Summarize this.")]; + let promptText = ""; + const { faux, model } = createFauxModel(false); + faux.setResponses([ + (context) => { + const message = context.messages[0]; + const content = message?.role === "user" ? message.content : []; + promptText = Array.isArray(content) && content[0]?.type === "text" ? content[0].text : ""; + return fauxAssistantMessage("## Goal\nTest summary"); + }, + ]); + + const summary = getOrThrow( + await generateSummary( + messages, + model, + 2000, + "test-key", + { "x-test": "yes" }, + undefined, + "focus", + "old summary", + ), + ); + + expect(summary).toContain("Test summary"); + expect(promptText).toContain("\nold summary\n"); + expect(promptText).toContain("Additional focus: focus"); + }); + + it("returns error results for failed or aborted summary generations", async () => { + const messages: AgentMessage[] = [createUserMessage("Summarize this.")]; + const { faux: errorFaux, model: errorModel } = createFauxModel(false); + errorFaux.setResponses([fauxAssistantMessage("", { stopReason: "error", errorMessage: "boom" })]); + const errorResult = await generateSummary(messages, errorModel, 2000, "test-key"); + expect(errorResult).toMatchObject({ + ok: false, + error: { code: "summarization_failed", message: "Summarization failed: boom" }, + }); + + const { faux: abortedFaux, model: abortedModel } = createFauxModel(false); + abortedFaux.setResponses([fauxAssistantMessage("", { stopReason: "aborted", errorMessage: "stopped" })]); + const abortedResult = await generateSummary(messages, abortedModel, 2000, "test-key"); + expect(abortedResult).toMatchObject({ ok: false, error: { code: "aborted", message: "stopped" } }); + + const missingProviderModel = { ...abortedModel, api: "missing-api" } as Model; + const unknownResult = await generateSummary(messages, missingProviderModel, 2000, "test-key"); + expect(unknownResult).toMatchObject({ + ok: false, + error: { code: "unknown", message: "Summarization request failed" }, + }); + }); + it("clamps compaction summary maxTokens to the model output cap", async () => { const messages: AgentMessage[] = [createUserMessage("Summarize this.")]; const seenOptions: Array | undefined> = []; @@ -313,11 +568,96 @@ describe("harness compaction", () => { settings: { enabled: true, reserveTokens: 500000, keepRecentTokens: 20000 }, }; - await compact(preparation, model, "test-key"); + getOrThrow(await compact(preparation, model, "test-key")); expect(seenOptions.map((options) => options?.maxTokens)).toEqual([128000, 128000]); }); + it("returns compaction error results without throwing", async () => { + const messages: AgentMessage[] = [createUserMessage("Summarize this.")]; + const preparation: CompactionPreparation = { + firstKeptEntryId: "entry-keep", + messagesToSummarize: messages, + turnPrefixMessages: [], + isSplitTurn: false, + tokensBefore: 100, + fileOps: { read: new Set(), written: new Set(), edited: new Set() }, + settings: { enabled: true, reserveTokens: 2000, keepRecentTokens: 20 }, + }; + const { faux: historyFaux, model: historyModel } = createFauxModel(false); + historyFaux.setResponses([fauxAssistantMessage("", { stopReason: "error", errorMessage: "history failed" })]); + expect(await compact(preparation, historyModel, "test-key")).toMatchObject({ + ok: false, + error: { code: "summarization_failed", message: "Summarization failed: history failed" }, + }); + + const { model: invalidModel } = createFauxModel(false); + const invalidResult = await compact( + { ...preparation, messagesToSummarize: [], firstKeptEntryId: "" }, + invalidModel, + "test-key", + ); + expect(invalidResult).toMatchObject({ ok: false, error: { code: "invalid_session" } }); + }); + + it("passes reasoning through turn-prefix summaries when enabled", async () => { + const messages: AgentMessage[] = [createUserMessage("Summarize this.")]; + const seenOptions: Array | undefined> = []; + const { faux, model } = createFauxModel(true); + faux.setResponses([ + (_context, options) => { + seenOptions.push(options as Record | undefined); + return fauxAssistantMessage("## Original Request\nTest summary"); + }, + ]); + const preparation: CompactionPreparation = { + firstKeptEntryId: "entry-keep", + messagesToSummarize: [], + turnPrefixMessages: messages, + isSplitTurn: true, + tokensBefore: 100, + fileOps: { read: new Set(), written: new Set(), edited: new Set() }, + settings: { enabled: true, reserveTokens: 2000, keepRecentTokens: 20 }, + }; + + getOrThrow(await compact(preparation, model, "test-key", undefined, undefined, undefined, "high")); + + expect(seenOptions[0]).toMatchObject({ reasoning: "high" }); + }); + + it("returns turn-prefix compaction errors without throwing", async () => { + const messages: AgentMessage[] = [createUserMessage("Summarize this.")]; + const preparation: CompactionPreparation = { + firstKeptEntryId: "entry-keep", + messagesToSummarize: [], + turnPrefixMessages: messages, + isSplitTurn: true, + tokensBefore: 100, + fileOps: { read: new Set(), written: new Set(), edited: new Set() }, + settings: { enabled: true, reserveTokens: 2000, keepRecentTokens: 20 }, + }; + const { faux, model } = createFauxModel(false); + faux.setResponses([fauxAssistantMessage("", { stopReason: "error", errorMessage: "prefix failed" })]); + + expect(await compact(preparation, model, "test-key")).toMatchObject({ + ok: false, + error: { code: "summarization_failed", message: "Turn prefix summarization failed: prefix failed" }, + }); + + const { faux: abortedFaux, model: abortedModel } = createFauxModel(false); + abortedFaux.setResponses([fauxAssistantMessage("", { stopReason: "aborted", errorMessage: "prefix stopped" })]); + expect(await compact(preparation, abortedModel, "test-key")).toMatchObject({ + ok: false, + error: { code: "aborted", message: "prefix stopped" }, + }); + + const missingProviderModel = { ...abortedModel, api: "missing-api" } as Model; + expect(await compact(preparation, missingProviderModel, "test-key")).toMatchObject({ + ok: false, + error: { code: "unknown", message: "Turn prefix summarization request failed" }, + }); + }); + it("returns a compaction result with file details", async () => { const u1 = createMessageEntry(createUserMessage("read a file")); const assistantMessage: AssistantMessage = { @@ -331,13 +671,13 @@ describe("harness compaction", () => { expect(preparation).toBeDefined(); const { faux, model } = createFauxModel(false); faux.setResponses([fauxAssistantMessage("## Goal\nTest summary")]); - const result = await compact(preparation!, model, "test-key"); + const result = getOrThrow(await compact(preparation!, model, "test-key")); expect(result.summary.length).toBeGreaterThan(0); expect(result.firstKeptEntryId).toBeTruthy(); expect(result.details).toBeDefined(); }); }); -function convertMessages(messages: any[]): any[] { +function convertMessages(messages: Message[]): Message[] { return messages; } From 0b54c87e24ba94b0a2ac43501959fd35c48deb1b Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Thu, 14 May 2026 22:44:28 +0200 Subject: [PATCH 040/166] fix(release): finalize Windows ARM64 binary support closes #4410 --- .github/workflows/build-binaries.yml | 1 + packages/coding-agent/CHANGELOG.md | 1 + scripts/build-binaries.sh | 22 +++++++++++++--------- 3 files changed, 15 insertions(+), 9 deletions(-) diff --git a/.github/workflows/build-binaries.yml b/.github/workflows/build-binaries.yml index 342bc8c0..ccc4eab8 100644 --- a/.github/workflows/build-binaries.yml +++ b/.github/workflows/build-binaries.yml @@ -24,6 +24,7 @@ jobs: uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: ref: ${{ env.RELEASE_TAG }} + persist-credentials: false - name: Setup Bun uses: oven-sh/setup-bun@4bc047ad259df6fc24a6c9b0f9a0cb08cf17fbe5 # v2.0.1 diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 7b86ee14..4de7ea76 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -5,6 +5,7 @@ ### Added - Added Together AI to built-in provider setup, `/login` API-key auth, and default model resolution ([#3624](https://github.com/earendil-works/pi-mono/pull/3624) by [@Nutlope](https://github.com/Nutlope)). +- Added Windows ARM64 standalone binary release artifacts ([#4458](https://github.com/earendil-works/pi/pull/4458) by [@brianmichel](https://github.com/brianmichel)). ### Fixed diff --git a/scripts/build-binaries.sh b/scripts/build-binaries.sh index 29cf0147..e66e4390 100755 --- a/scripts/build-binaries.sh +++ b/scripts/build-binaries.sh @@ -46,7 +46,7 @@ done # Validate platform if specified if [[ -n "$PLATFORM" ]]; then case "$PLATFORM" in - darwin-arm64|darwin-x64|linux-x64|linux-arm64|windows-x64) + darwin-arm64|darwin-x64|linux-x64|linux-arm64|windows-x64|windows-arm64) ;; *) echo "Invalid platform: $PLATFORM" @@ -65,13 +65,13 @@ if [[ "$SKIP_DEPS" == "false" ]]; then # We need all platform bindings for bun cross-compilation # Use --force to bypass platform checks (os/cpu restrictions in package.json) # Install all in one command to avoid npm removing packages from previous installs - npm install --no-save --force \ - @mariozechner/clipboard-darwin-arm64@0.3.0 \ - @mariozechner/clipboard-darwin-x64@0.3.0 \ - @mariozechner/clipboard-linux-x64-gnu@0.3.0 \ - @mariozechner/clipboard-linux-arm64-gnu@0.3.0 \ - @mariozechner/clipboard-win32-x64-msvc@0.3.0 \ - @mariozechner/clipboard-win32-arm64-msvc@0.3.0 \ + npm install --no-save --force --ignore-scripts \ + @mariozechner/clipboard-darwin-arm64@0.3.2 \ + @mariozechner/clipboard-darwin-x64@0.3.2 \ + @mariozechner/clipboard-linux-x64-gnu@0.3.2 \ + @mariozechner/clipboard-linux-arm64-gnu@0.3.2 \ + @mariozechner/clipboard-win32-x64-msvc@0.3.2 \ + @mariozechner/clipboard-win32-arm64-msvc@0.3.2 \ @img/sharp-darwin-arm64@0.34.5 \ @img/sharp-darwin-x64@0.34.5 \ @img/sharp-linux-x64@0.34.5 \ @@ -179,5 +179,9 @@ ls -lh *.tar.gz *.zip 2>/dev/null || true echo "" echo "Extracted directories for testing:" for platform in "${PLATFORMS[@]}"; do - echo " binaries/$platform/pi" + if [[ "$platform" == windows-* ]]; then + echo " binaries/$platform/pi.exe" + else + echo " binaries/$platform/pi" + fi done From 80c918c2478c0e48a6f1773668f3bef82518ff0f Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Fri, 15 May 2026 00:53:33 +0200 Subject: [PATCH 041/166] refactor(agent): isolate node filesystem session dependencies --- packages/agent/docs/agent-harness.md | 5 +- packages/agent/package.json | 11 ++ .../compaction/branch-summarization.ts | 12 +- packages/agent/src/harness/env/nodejs.ts | 16 +- packages/agent/src/harness/execution-env.ts | 10 -- .../agent/src/harness/prompt-templates.ts | 6 +- .../agent/src/harness/session/jsonl-repo.ts | 138 ++++++++++++++++++ .../{storage/jsonl.ts => jsonl-storage.ts} | 79 +++++----- .../{repo/memory.ts => memory-repo.ts} | 6 +- .../{storage/memory.ts => memory-storage.ts} | 9 +- .../session/{repo/shared.ts => repo-utils.ts} | 6 +- .../agent/src/harness/session/repo/jsonl.ts | 109 -------------- packages/agent/src/harness/session/uuid.ts | 16 +- packages/agent/src/harness/skills.ts | 6 +- packages/agent/src/harness/types.ts | 56 ++++--- packages/agent/src/index.ts | 7 +- packages/agent/src/node.ts | 2 + .../test/harness/agent-harness-stream.test.ts | 2 +- .../agent/test/harness/agent-harness.test.ts | 2 +- .../agent/test/harness/nodejs-env.test.ts | 8 +- packages/agent/test/harness/repo.test.ts | 11 +- .../agent/test/harness/session-uuid.test.ts | 32 ++-- packages/agent/test/harness/session.test.ts | 8 +- packages/agent/test/harness/storage.test.ts | 38 +++-- packages/agent/test/scratch/simple.ts | 2 +- 25 files changed, 340 insertions(+), 257 deletions(-) delete mode 100644 packages/agent/src/harness/execution-env.ts create mode 100644 packages/agent/src/harness/session/jsonl-repo.ts rename packages/agent/src/harness/session/{storage/jsonl.ts => jsonl-storage.ts} (69%) rename packages/agent/src/harness/session/{repo/memory.ts => memory-repo.ts} (91%) rename packages/agent/src/harness/session/{storage/memory.ts => memory-storage.ts} (94%) rename packages/agent/src/harness/session/{repo/shared.ts => repo-utils.ts} (91%) delete mode 100644 packages/agent/src/harness/session/repo/jsonl.ts create mode 100644 packages/agent/src/node.ts diff --git a/packages/agent/docs/agent-harness.md b/packages/agent/docs/agent-harness.md index e6a56db6..3ab1faf0 100644 --- a/packages/agent/docs/agent-harness.md +++ b/packages/agent/docs/agent-harness.md @@ -387,10 +387,11 @@ Implemented so far: - Added generic `Result` plus helpers. - Updated `ExecutionEnv` and `NodeExecutionEnv` to return typed results for filesystem/process operations. +- Split filesystem/shell capabilities and moved JSONL session storage/repo onto filesystem picks instead of direct Node imports. - Added `ExecutionEnv.appendFile()` for streaming append use cases. - Updated skill and prompt-template loaders to consume `ExecutionEnv` results. - Updated shell output capture to return a result and use `ExecutionEnv` instead of Node APIs directly, including full-output spill via `appendFile()`. -- Removed `NodeExecutionEnv` from the browser-safe `execution-env.ts` re-export; Node-specific callers import from `harness/env/nodejs.js`. +- Removed `NodeExecutionEnv` from browser-safe root exports; Node-specific callers use the `node` entry point or `harness/env/nodejs.js`. - Replaced `Buffer` usage in generic truncation utilities with runtime-neutral UTF-8 handling. - Converted compaction summary helpers to typed result returns and added error-path coverage. - Expanded `NodeExecutionEnv` tests for file operations, exec errors, aborts, callbacks, timeouts, and shell-output full-output spill. @@ -402,7 +403,7 @@ Still needed: - Convert structural `AgentHarness` operations to typed result returns for busy, missing-resource, auth, compaction, and branch-summary failures. - Keep Node-specific APIs isolated under `src/harness/env/nodejs.ts` and Node-backed storage/session implementations, or move those implementations behind explicit Node-only entry points. - Audit remaining generic harness utilities for Node globals as new APIs are added. -- Audit package exports so browser/generic-JS imports do not pull Node-only modules such as `NodeExecutionEnv` or JSONL storage. +- Audit package exports so browser/generic-JS imports do not pull Node-only modules such as `NodeExecutionEnv`. - Keep expanding `ExecutionEnv` and shell-output contract tests as the API evolves, especially for non-Node implementations. - Add tests proving harness APIs return `ok: false` instead of throwing for expected failure paths. diff --git a/packages/agent/package.json b/packages/agent/package.json index c2d2695b..4fe6d2fc 100644 --- a/packages/agent/package.json +++ b/packages/agent/package.json @@ -5,6 +5,17 @@ "type": "module", "main": "./dist/index.js", "types": "./dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + }, + "./node": { + "types": "./dist/node.d.ts", + "import": "./dist/node.js" + }, + "./package.json": "./package.json" + }, "files": [ "dist", "README.md" diff --git a/packages/agent/src/harness/compaction/branch-summarization.ts b/packages/agent/src/harness/compaction/branch-summarization.ts index e44bf458..38d3a18e 100644 --- a/packages/agent/src/harness/compaction/branch-summarization.ts +++ b/packages/agent/src/harness/compaction/branch-summarization.ts @@ -5,7 +5,7 @@ * a summary of the branch being left so context isn't lost. */ -import type { ImageContent, Model, TextContent } from "@earendil-works/pi-ai"; +import type { Model } from "@earendil-works/pi-ai"; import { completeSimple } from "@earendil-works/pi-ai"; import type { AgentMessage } from "../../types.js"; import { @@ -148,16 +148,10 @@ function getMessageFromEntry(entry: SessionTreeEntry): AgentMessage | undefined case "message": // Skip tool results - context is in assistant's tool call if (entry.message.role === "toolResult") return undefined; - return entry.message as AgentMessage; + return entry.message; case "custom_message": - return createCustomMessage( - entry.customType, - entry.content as string | (TextContent | ImageContent)[], - entry.display, - entry.details, - entry.timestamp, - ); + return createCustomMessage(entry.customType, entry.content, entry.display, entry.details, entry.timestamp); case "branch_summary": return createBranchSummaryMessage(entry.summary, entry.fromId, entry.timestamp); diff --git a/packages/agent/src/harness/env/nodejs.ts b/packages/agent/src/harness/env/nodejs.ts index 6e79cf2c..0b23a2d1 100644 --- a/packages/agent/src/harness/env/nodejs.ts +++ b/packages/agent/src/harness/env/nodejs.ts @@ -218,6 +218,20 @@ export class NodeExecutionEnv implements ExecutionEnv { this.shellEnv = options.shellEnv; } + async absolutePath(path: string, abortSignal?: AbortSignal): Promise> { + const resolved = resolvePath(this.cwd, path); + const aborted = abortResult(abortSignal, resolved); + if (aborted) return aborted; + return ok(resolved); + } + + async joinPath(parts: string[], abortSignal?: AbortSignal): Promise> { + const joined = join(...parts); + const aborted = abortResult(abortSignal, joined); + if (aborted) return aborted; + return ok(joined); + } + async exec( command: string, options?: { @@ -438,7 +452,7 @@ export class NodeExecutionEnv implements ExecutionEnv { } } - async realPath(path: string, abortSignal?: AbortSignal): Promise> { + async canonicalPath(path: string, abortSignal?: AbortSignal): Promise> { const resolved = resolvePath(this.cwd, path); const aborted = abortResult(abortSignal, resolved); if (aborted) return aborted; diff --git a/packages/agent/src/harness/execution-env.ts b/packages/agent/src/harness/execution-env.ts deleted file mode 100644 index 915fa4da..00000000 --- a/packages/agent/src/harness/execution-env.ts +++ /dev/null @@ -1,10 +0,0 @@ -export type { - ExecutionEnv, - ExecutionEnvExecOptions, - ExecutionErrorCode, - FileErrorCode, - FileInfo, - FileKind, - Result, -} from "./types.js"; -export { ExecutionError, err, FileError, getOrThrow, getOrUndefined, ok } from "./types.js"; diff --git a/packages/agent/src/harness/prompt-templates.ts b/packages/agent/src/harness/prompt-templates.ts index e627b66c..abcaad5a 100644 --- a/packages/agent/src/harness/prompt-templates.ts +++ b/packages/agent/src/harness/prompt-templates.ts @@ -148,9 +148,9 @@ async function loadTemplateFromFile( async function resolveKind(env: ExecutionEnv, info: FileInfo): Promise<"file" | "directory" | undefined> { if (info.kind === "file" || info.kind === "directory") return info.kind; - const realPath = await env.realPath(info.path); - if (!realPath.ok) return undefined; - const target = getOrUndefined(await env.fileInfo(realPath.value)); + const canonicalPath = await env.canonicalPath(info.path); + if (!canonicalPath.ok) return undefined; + const target = getOrUndefined(await env.fileInfo(canonicalPath.value)); if (!target) return undefined; return target.kind === "file" || target.kind === "directory" ? target.kind : undefined; } diff --git a/packages/agent/src/harness/session/jsonl-repo.ts b/packages/agent/src/harness/session/jsonl-repo.ts new file mode 100644 index 00000000..e41f0fb3 --- /dev/null +++ b/packages/agent/src/harness/session/jsonl-repo.ts @@ -0,0 +1,138 @@ +import type { + FileSystem, + JsonlSessionCreateOptions, + JsonlSessionListOptions, + JsonlSessionMetadata, + JsonlSessionRepoApi, + Session, +} from "../types.js"; +import { getOrThrow } from "../types.js"; +import { JsonlSessionStorage, loadJsonlSessionMetadata } from "./jsonl-storage.js"; +import { createSessionId, createTimestamp, getEntriesToFork, toSession } from "./repo-utils.js"; + +type JsonlSessionRepoFileSystem = Pick< + FileSystem, + | "cwd" + | "absolutePath" + | "joinPath" + | "readTextFile" + | "writeFile" + | "appendFile" + | "listDir" + | "exists" + | "createDir" + | "remove" +>; + +function encodeCwd(cwd: string): string { + return `--${cwd.replace(/^[/\\]/, "").replace(/[/\\:]/g, "-")}--`; +} + +export class JsonlSessionRepo implements JsonlSessionRepoApi { + private readonly fs: JsonlSessionRepoFileSystem; + private readonly sessionsRootInput: string; + private sessionsRoot: string | undefined; + + constructor(options: { fs: JsonlSessionRepoFileSystem; sessionsRoot: string }) { + this.fs = options.fs; + this.sessionsRootInput = options.sessionsRoot; + } + + private async getSessionsRoot(): Promise { + if (!this.sessionsRoot) { + this.sessionsRoot = getOrThrow(await this.fs.absolutePath(this.sessionsRootInput)); + } + return this.sessionsRoot; + } + + private async getSessionDir(cwd: string): Promise { + return getOrThrow(await this.fs.joinPath([await this.getSessionsRoot(), encodeCwd(cwd)])); + } + + private async createSessionFilePath(cwd: string, sessionId: string, timestamp: string): Promise { + return getOrThrow( + await this.fs.joinPath([ + await this.getSessionDir(cwd), + `${timestamp.replace(/[:.]/g, "-")}_${sessionId}.jsonl`, + ]), + ); + } + + async create(options: JsonlSessionCreateOptions): Promise> { + const id = options.id ?? createSessionId(); + const createdAt = createTimestamp(); + const sessionDir = await this.getSessionDir(options.cwd); + getOrThrow(await this.fs.createDir(sessionDir, { recursive: true })); + const filePath = await this.createSessionFilePath(options.cwd, id, createdAt); + const storage = await JsonlSessionStorage.create(this.fs, filePath, { + cwd: options.cwd, + sessionId: id, + parentSessionPath: options.parentSessionPath, + }); + return toSession(storage); + } + + async open(metadata: JsonlSessionMetadata): Promise> { + if (!getOrThrow(await this.fs.exists(metadata.path))) { + throw new Error(`Session not found: ${metadata.path}`); + } + const storage = await JsonlSessionStorage.open(this.fs, metadata.path); + return toSession(storage); + } + + async list(options: JsonlSessionListOptions = {}): Promise { + const dirs = options.cwd ? [await this.getSessionDir(options.cwd)] : await this.listSessionDirs(); + const sessions: JsonlSessionMetadata[] = []; + for (const dir of dirs) { + if (!getOrThrow(await this.fs.exists(dir))) continue; + const files = getOrThrow(await this.fs.listDir(dir)).filter( + (file) => file.kind !== "directory" && file.name.endsWith(".jsonl"), + ); + for (const file of files) { + try { + sessions.push(await loadJsonlSessionMetadata(this.fs, file.path)); + } catch { + // Ignore invalid session files when listing a directory. + } + } + } + sessions.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()); + return sessions; + } + + async delete(metadata: JsonlSessionMetadata): Promise { + getOrThrow(await this.fs.remove(metadata.path, { force: true })); + } + + async fork( + sourceMetadata: JsonlSessionMetadata, + options: JsonlSessionCreateOptions & { entryId?: string; position?: "before" | "at"; id?: string }, + ): Promise> { + const source = await this.open(sourceMetadata); + const forkedEntries = await getEntriesToFork(source.getStorage(), options); + const id = options.id ?? createSessionId(); + const createdAt = createTimestamp(); + const sessionDir = await this.getSessionDir(options.cwd); + getOrThrow(await this.fs.createDir(sessionDir, { recursive: true })); + const storage = await JsonlSessionStorage.create( + this.fs, + await this.createSessionFilePath(options.cwd, id, createdAt), + { + cwd: options.cwd, + sessionId: id, + parentSessionPath: options.parentSessionPath ?? sourceMetadata.path, + }, + ); + for (const entry of forkedEntries) { + await storage.appendEntry(entry); + } + return toSession(storage); + } + + private async listSessionDirs(): Promise { + const sessionsRoot = await this.getSessionsRoot(); + if (!getOrThrow(await this.fs.exists(sessionsRoot))) return []; + const entries = getOrThrow(await this.fs.listDir(sessionsRoot)); + return entries.filter((entry) => entry.kind === "directory").map((entry) => entry.path); + } +} diff --git a/packages/agent/src/harness/session/storage/jsonl.ts b/packages/agent/src/harness/session/jsonl-storage.ts similarity index 69% rename from packages/agent/src/harness/session/storage/jsonl.ts rename to packages/agent/src/harness/session/jsonl-storage.ts index 72ce6ce3..a6f370f7 100644 --- a/packages/agent/src/harness/session/storage/jsonl.ts +++ b/packages/agent/src/harness/session/jsonl-storage.ts @@ -1,9 +1,8 @@ -import { randomUUID } from "node:crypto"; -import { createReadStream } from "node:fs"; -import { appendFile, mkdir, readFile, writeFile } from "node:fs/promises"; -import { dirname, resolve } from "node:path"; -import { createInterface } from "node:readline"; -import type { JsonlSessionMetadata, SessionStorage, SessionTreeEntry } from "../../types.js"; +import type { FileSystem, JsonlSessionMetadata, SessionStorage, SessionTreeEntry } from "../types.js"; +import { getOrThrow } from "../types.js"; +import { uuidv7 } from "./uuid.js"; + +type JsonlSessionStorageFileSystem = Pick; interface SessionHeader { type: "session"; @@ -34,10 +33,10 @@ function buildLabelsById(entries: SessionTreeEntry[]): Map { function generateEntryId(byId: { has(id: string): boolean }): string { for (let i = 0; i < 100; i++) { - const id = randomUUID().slice(0, 8); + const id = uuidv7().slice(0, 8); if (!byId.has(id)) return id; } - return randomUUID(); + return uuidv7(); } function headerToSessionMetadata(header: SessionHeader, path: string): JsonlSessionMetadata { @@ -50,32 +49,32 @@ function headerToSessionMetadata(header: SessionHeader, path: string): JsonlSess }; } -export async function loadJsonlSessionMetadata(filePath: string): Promise { - const stream = createReadStream(filePath, { encoding: "utf8" }); - const lines = createInterface({ input: stream, crlfDelay: Infinity }); - try { - for await (const line of lines) { - if (!line.trim()) break; - try { - const header = JSON.parse(line) as SessionHeader; - return headerToSessionMetadata(header, resolve(filePath)); - } catch { - throw new Error(`Invalid JSONL session file ${filePath}: first line is not a valid session header`); - } +export async function loadJsonlSessionMetadata( + fs: JsonlSessionStorageFileSystem, + filePath: string, +): Promise { + const content = getOrThrow(await fs.readTextFile(filePath)); + for (const line of content.split("\n")) { + if (!line.trim()) break; + try { + const header = JSON.parse(line) as SessionHeader; + return headerToSessionMetadata(header, filePath); + } catch { + throw new Error(`Invalid JSONL session file ${filePath}: first line is not a valid session header`); } - throw new Error(`Invalid JSONL session file ${filePath}: missing session header`); - } finally { - lines.close(); - stream.destroy(); } + throw new Error(`Invalid JSONL session file ${filePath}: missing session header`); } -async function loadJsonlStorage(filePath: string): Promise<{ +async function loadJsonlStorage( + fs: JsonlSessionStorageFileSystem, + filePath: string, +): Promise<{ header: SessionHeader; entries: SessionTreeEntry[]; leafId: string | null; }> { - const content = await readFile(filePath, "utf8"); + const content = getOrThrow(await fs.readTextFile(filePath)); const lines = content.split("\n").filter((line) => line.trim()); if (lines.length === 0) { throw new Error(`Invalid JSONL session file ${filePath}: missing session header`); @@ -103,6 +102,7 @@ async function loadJsonlStorage(filePath: string): Promise<{ } export class JsonlSessionStorage implements SessionStorage { + private readonly fs: JsonlSessionStorageFileSystem; private readonly filePath: string; private readonly metadata: JsonlSessionMetadata; private entries: SessionTreeEntry[]; @@ -110,8 +110,15 @@ export class JsonlSessionStorage implements SessionStorage private labelsById: Map; private currentLeafId: string | null; - private constructor(filePath: string, header: SessionHeader, entries: SessionTreeEntry[], leafId: string | null) { - this.filePath = resolve(filePath); + private constructor( + fs: JsonlSessionStorageFileSystem, + filePath: string, + header: SessionHeader, + entries: SessionTreeEntry[], + leafId: string | null, + ) { + this.fs = fs; + this.filePath = filePath; this.metadata = headerToSessionMetadata(header, this.filePath); this.entries = entries; this.byId = new Map(entries.map((entry) => [entry.id, entry])); @@ -119,13 +126,13 @@ export class JsonlSessionStorage implements SessionStorage this.currentLeafId = leafId; } - static async open(filePath: string): Promise { - const resolvedPath = resolve(filePath); - const loaded = await loadJsonlStorage(resolvedPath); - return new JsonlSessionStorage(resolvedPath, loaded.header, loaded.entries, loaded.leafId); + static async open(fs: JsonlSessionStorageFileSystem, filePath: string): Promise { + const loaded = await loadJsonlStorage(fs, filePath); + return new JsonlSessionStorage(fs, filePath, loaded.header, loaded.entries, loaded.leafId); } static async create( + fs: JsonlSessionStorageFileSystem, filePath: string, options: { cwd: string; @@ -133,7 +140,6 @@ export class JsonlSessionStorage implements SessionStorage parentSessionPath?: string; }, ): Promise { - const resolvedPath = resolve(filePath); const header: SessionHeader = { type: "session", version: 3, @@ -142,9 +148,8 @@ export class JsonlSessionStorage implements SessionStorage cwd: options.cwd, parentSession: options.parentSessionPath, }; - await mkdir(dirname(resolvedPath), { recursive: true }); - await writeFile(resolvedPath, `${JSON.stringify(header)}\n`); - return new JsonlSessionStorage(resolvedPath, header, [], null); + getOrThrow(await fs.writeFile(filePath, `${JSON.stringify(header)}\n`)); + return new JsonlSessionStorage(fs, filePath, header, [], null); } async getMetadata(): Promise { @@ -167,7 +172,7 @@ export class JsonlSessionStorage implements SessionStorage } async appendEntry(entry: SessionTreeEntry): Promise { - await appendFile(this.filePath, `${JSON.stringify(entry)}\n`); + getOrThrow(await this.fs.appendFile(this.filePath, `${JSON.stringify(entry)}\n`)); this.entries.push(entry); this.byId.set(entry.id, entry); updateLabelCache(this.labelsById, entry); diff --git a/packages/agent/src/harness/session/repo/memory.ts b/packages/agent/src/harness/session/memory-repo.ts similarity index 91% rename from packages/agent/src/harness/session/repo/memory.ts rename to packages/agent/src/harness/session/memory-repo.ts index 3846ae2a..85401801 100644 --- a/packages/agent/src/harness/session/repo/memory.ts +++ b/packages/agent/src/harness/session/memory-repo.ts @@ -1,6 +1,6 @@ -import type { Session, SessionMetadata, SessionRepo } from "../../types.js"; -import { InMemorySessionStorage } from "../storage/memory.js"; -import { createSessionId, createTimestamp, getEntriesToFork, toSession } from "./shared.js"; +import type { Session, SessionMetadata, SessionRepo } from "../types.js"; +import { InMemorySessionStorage } from "./memory-storage.js"; +import { createSessionId, createTimestamp, getEntriesToFork, toSession } from "./repo-utils.js"; export class InMemorySessionRepo implements SessionRepo { private sessions = new Map>(); diff --git a/packages/agent/src/harness/session/storage/memory.ts b/packages/agent/src/harness/session/memory-storage.ts similarity index 94% rename from packages/agent/src/harness/session/storage/memory.ts rename to packages/agent/src/harness/session/memory-storage.ts index 652f633f..bf519b38 100644 --- a/packages/agent/src/harness/session/storage/memory.ts +++ b/packages/agent/src/harness/session/memory-storage.ts @@ -1,6 +1,5 @@ -import { randomUUID } from "node:crypto"; -import type { SessionMetadata, SessionStorage, SessionTreeEntry } from "../../types.js"; -import { uuidv7 } from "../uuid.js"; +import type { SessionMetadata, SessionStorage, SessionTreeEntry } from "../types.js"; +import { uuidv7 } from "./uuid.js"; function updateLabelCache(labelsById: Map, entry: SessionTreeEntry): void { if (entry.type !== "label") return; @@ -22,10 +21,10 @@ function buildLabelsById(entries: SessionTreeEntry[]): Map { function generateEntryId(byId: { has(id: string): boolean }): string { for (let i = 0; i < 100; i++) { - const id = randomUUID().slice(0, 8); + const id = uuidv7().slice(0, 8); if (!byId.has(id)) return id; } - return randomUUID(); + return uuidv7(); } export class InMemorySessionStorage implements SessionStorage { diff --git a/packages/agent/src/harness/session/repo/shared.ts b/packages/agent/src/harness/session/repo-utils.ts similarity index 91% rename from packages/agent/src/harness/session/repo/shared.ts rename to packages/agent/src/harness/session/repo-utils.ts index 2c628ca9..e842a718 100644 --- a/packages/agent/src/harness/session/repo/shared.ts +++ b/packages/agent/src/harness/session/repo-utils.ts @@ -1,6 +1,6 @@ -import type { SessionMetadata, SessionStorage, SessionTreeEntry } from "../../types.js"; -import { Session } from "../session.js"; -import { uuidv7 } from "../uuid.js"; +import type { SessionMetadata, SessionStorage, SessionTreeEntry } from "../types.js"; +import { Session } from "./session.js"; +import { uuidv7 } from "./uuid.js"; export function createSessionId(): string { return uuidv7(); diff --git a/packages/agent/src/harness/session/repo/jsonl.ts b/packages/agent/src/harness/session/repo/jsonl.ts deleted file mode 100644 index a4c5ec5b..00000000 --- a/packages/agent/src/harness/session/repo/jsonl.ts +++ /dev/null @@ -1,109 +0,0 @@ -import { constants } from "node:fs"; -import { access, mkdir, readdir, rm } from "node:fs/promises"; -import { join, resolve } from "node:path"; -import type { - JsonlSessionCreateOptions, - JsonlSessionListOptions, - JsonlSessionMetadata, - JsonlSessionRepoApi, - Session, -} from "../../types.js"; -import { JsonlSessionStorage, loadJsonlSessionMetadata } from "../storage/jsonl.js"; -import { createSessionId, createTimestamp, getEntriesToFork, toSession } from "./shared.js"; - -async function exists(path: string): Promise { - try { - await access(path, constants.F_OK); - return true; - } catch { - return false; - } -} - -function encodeCwd(cwd: string): string { - return `--${cwd.replace(/^[/\\]/, "").replace(/[/\\:]/g, "-")}--`; -} - -export class JsonlSessionRepo implements JsonlSessionRepoApi { - private sessionsRoot: string; - - constructor(options: { sessionsRoot: string }) { - this.sessionsRoot = resolve(options.sessionsRoot); - } - - private getSessionDir(cwd: string): string { - return join(this.sessionsRoot, encodeCwd(cwd)); - } - - private createSessionFilePath(cwd: string, sessionId: string, timestamp: string): string { - return join(this.getSessionDir(cwd), `${timestamp.replace(/[:.]/g, "-")}_${sessionId}.jsonl`); - } - - async create(options: JsonlSessionCreateOptions): Promise> { - await mkdir(this.sessionsRoot, { recursive: true }); - const id = options.id ?? createSessionId(); - const createdAt = createTimestamp(); - const filePath = this.createSessionFilePath(options.cwd, id, createdAt); - const storage = await JsonlSessionStorage.create(filePath, { - cwd: options.cwd, - sessionId: id, - parentSessionPath: options.parentSessionPath, - }); - return toSession(storage); - } - - async open(metadata: JsonlSessionMetadata): Promise> { - if (!(await exists(metadata.path))) { - throw new Error(`Session not found: ${metadata.path}`); - } - const storage = await JsonlSessionStorage.open(metadata.path); - return toSession(storage); - } - - async list(options: JsonlSessionListOptions = {}): Promise { - const dirs = options.cwd ? [this.getSessionDir(options.cwd)] : await this.listSessionDirs(); - const sessions: JsonlSessionMetadata[] = []; - for (const dir of dirs) { - if (!(await exists(dir))) continue; - const files = (await readdir(dir)).filter((file) => file.endsWith(".jsonl")).map((file) => join(dir, file)); - for (const filePath of files) { - try { - sessions.push(await loadJsonlSessionMetadata(filePath)); - } catch { - // Ignore invalid session files when listing a directory. - } - } - } - sessions.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()); - return sessions; - } - - async delete(metadata: JsonlSessionMetadata): Promise { - await rm(metadata.path, { force: true }); - } - - async fork( - sourceMetadata: JsonlSessionMetadata, - options: JsonlSessionCreateOptions & { entryId?: string; position?: "before" | "at"; id?: string }, - ): Promise> { - const source = await this.open(sourceMetadata); - const forkedEntries = await getEntriesToFork(source.getStorage(), options); - const id = options.id ?? createSessionId(); - const createdAt = createTimestamp(); - const storage = await JsonlSessionStorage.create(this.createSessionFilePath(options.cwd, id, createdAt), { - cwd: options.cwd, - sessionId: id, - parentSessionPath: options.parentSessionPath ?? sourceMetadata.path, - }); - for (const entry of forkedEntries) { - await storage.appendEntry(entry); - } - return toSession(storage); - } - - private async listSessionDirs(): Promise { - if (!(await exists(this.sessionsRoot))) return []; - const entries = await readdir(this.sessionsRoot, { withFileTypes: true }); - return entries.filter((entry) => entry.isDirectory()).map((entry) => join(this.sessionsRoot, entry.name)); - } -} diff --git a/packages/agent/src/harness/session/uuid.ts b/packages/agent/src/harness/session/uuid.ts index c7e2896e..0c4cef8e 100644 --- a/packages/agent/src/harness/session/uuid.ts +++ b/packages/agent/src/harness/session/uuid.ts @@ -1,10 +1,20 @@ -import { randomBytes } from "node:crypto"; - let lastTimestamp = -Infinity; let sequence = 0; +function fillRandomBytes(bytes: Uint8Array): void { + const crypto = globalThis.crypto; + if (crypto?.getRandomValues) { + crypto.getRandomValues(bytes); + return; + } + for (let i = 0; i < bytes.length; i++) { + bytes[i] = Math.floor(Math.random() * 256); + } +} + export function uuidv7(): string { - const random = randomBytes(16); + const random = new Uint8Array(16); + fillRandomBytes(random); const timestamp = Date.now(); if (timestamp > lastTimestamp) { diff --git a/packages/agent/src/harness/skills.ts b/packages/agent/src/harness/skills.ts index bb8931f6..d26c8627 100644 --- a/packages/agent/src/harness/skills.ts +++ b/packages/agent/src/harness/skills.ts @@ -261,9 +261,9 @@ function parseFrontmatter>( async function resolveKind(env: ExecutionEnv, info: FileInfo): Promise<"file" | "directory" | undefined> { if (info.kind === "file" || info.kind === "directory") return info.kind; - const realPath = await env.realPath(info.path); - if (!realPath.ok) return undefined; - const target = getOrUndefined(await env.fileInfo(realPath.value)); + const canonicalPath = await env.canonicalPath(info.path); + if (!canonicalPath.ok) return undefined; + const target = getOrUndefined(await env.fileInfo(canonicalPath.value)); if (!target) return undefined; return target.kind === "file" || target.kind === "directory" ? target.kind : undefined; } diff --git a/packages/agent/src/harness/types.ts b/packages/agent/src/harness/types.ts index 91e2d87a..d25f181c 100644 --- a/packages/agent/src/harness/types.ts +++ b/packages/agent/src/harness/types.ts @@ -93,10 +93,10 @@ export interface AgentHarnessStreamOptionsPatch metadata?: Record; } -/** Kind of filesystem object as addressed by an {@link ExecutionEnv}. Symlinks are not followed automatically. */ +/** Kind of filesystem object as addressed by a {@link FileSystem}. Symlinks are not followed automatically. */ export type FileKind = "file" | "directory" | "symlink"; -/** Stable, backend-independent file error codes returned by {@link ExecutionEnv} file operations. */ +/** Stable, backend-independent file error codes returned by {@link FileSystem} file operations. */ export type FileErrorCode = | "aborted" | "not_found" @@ -107,7 +107,7 @@ export type FileErrorCode = | "not_supported" | "unknown"; -/** Error returned by {@link ExecutionEnv} file operations. */ +/** Error returned by {@link FileSystem} file operations. */ export class FileError extends Error { constructor( /** Backend-independent error code. */ @@ -154,13 +154,13 @@ export class CompactionError extends Error { } } -/** Metadata for one filesystem object in an {@link ExecutionEnv}. */ +/** Metadata for one filesystem object in a {@link FileSystem}. */ export interface FileInfo { /** Basename of {@link path}. */ name: string; /** Absolute, syntactically normalized addressed path in the execution environment. Symlinks are not followed. */ path: string; - /** Object kind. Symlink targets are not followed; use {@link ExecutionEnv.resolvePath} explicitly. */ + /** Object kind. Symlink targets are not followed; use {@link FileSystem.canonicalPath} explicitly. */ kind: FileKind; /** Size in bytes for the addressed filesystem object. */ size: number; @@ -168,7 +168,7 @@ export interface FileInfo { mtimeMs: number; } -/** Options for {@link ExecutionEnv.exec}. */ +/** Options for {@link Shell.exec}. */ export interface ExecutionEnvExecOptions { /** Working directory for the command. Relative paths are resolved against {@link ExecutionEnv.cwd}. Defaults to {@link ExecutionEnv.cwd}. */ cwd?: string; @@ -185,24 +185,22 @@ export interface ExecutionEnvExecOptions { } /** - * Filesystem and process execution environment used by the harness. + * Filesystem capability used by the harness. * - * Paths passed to methods may be absolute or relative to {@link cwd}. Paths returned by this interface are absolute - * addressed paths in the environment, but are not canonicalized through symlinks unless returned by {@link resolvePath}. + * Paths passed to methods may be absolute or relative to {@link cwd}. Paths returned by file operations are addressed paths + * in the filesystem namespace, but are not canonicalized through symlinks unless returned by {@link canonicalPath}. * - * Operation methods must never throw or reject. All filesystem/process failures, including unexpected backend failures, - * must be encoded in the returned {@link Result}. Implementations must preserve this invariant. + * Operation methods must never throw or reject. All filesystem failures, including unexpected backend failures, must be + * encoded in the returned {@link Result}. Implementations must preserve this invariant. */ -export interface ExecutionEnv { - /** Current working directory for relative paths and command execution. */ +export interface FileSystem { + /** Current working directory for relative paths. */ cwd: string; - /** Execute a shell command in {@link cwd} unless `options.cwd` is provided. */ - exec( - command: string, - options?: ExecutionEnvExecOptions, - ): Promise>; - + /** Return an absolute addressed path without requiring it to exist and without resolving symlinks. */ + absolutePath(path: string, abortSignal?: AbortSignal): Promise>; + /** Join path segments in the filesystem namespace without requiring the result to exist. */ + joinPath(parts: string[], abortSignal?: AbortSignal): Promise>; /** Read a UTF-8 text file. */ readTextFile(path: string, abortSignal?: AbortSignal): Promise>; /** Read a binary file. */ @@ -215,8 +213,8 @@ export interface ExecutionEnv { fileInfo(path: string, abortSignal?: AbortSignal): Promise>; /** List direct children of a directory without following symlinks. */ listDir(path: string, abortSignal?: AbortSignal): Promise>; - /** Return the canonical path for a path, following symlinks. */ - realPath(path: string, abortSignal?: AbortSignal): Promise>; + /** Return the canonical path for an existing path, resolving symlinks where supported. */ + canonicalPath(path: string, abortSignal?: AbortSignal): Promise>; /** Return false for missing paths. Other errors, such as permission failures, return a {@link FileError}. */ exists(path: string, abortSignal?: AbortSignal): Promise>; /** Create a directory. Defaults: `recursive: true`, no abort signal. */ @@ -238,10 +236,24 @@ export interface ExecutionEnv { abortSignal?: AbortSignal; }): Promise>; - /** Release resources owned by the environment. Must be best-effort and must not throw or reject. */ + /** Release filesystem resources. Must be best-effort and must not throw or reject. */ cleanup(): Promise; } +/** Shell execution capability used by the harness. */ +export interface Shell { + /** Execute a shell command in {@link FileSystem.cwd} unless `options.cwd` is provided. */ + exec( + command: string, + options?: ExecutionEnvExecOptions, + ): Promise>; + /** Release shell resources. Must be best-effort and must not throw or reject. */ + cleanup(): Promise; +} + +/** Filesystem and process execution environment used by the harness. */ +export interface ExecutionEnv extends FileSystem, Shell {} + export interface SessionTreeEntryBase { type: string; id: string; diff --git a/packages/agent/src/index.ts b/packages/agent/src/index.ts index 293ce196..345def15 100644 --- a/packages/agent/src/index.ts +++ b/packages/agent/src/index.ts @@ -22,12 +22,11 @@ export { serializeConversation, shouldCompact, } from "./harness/compaction/compaction.js"; -export * from "./harness/execution-env.js"; export * from "./harness/messages.js"; export * from "./harness/prompt-templates.js"; -export * from "./harness/session/repo/jsonl.js"; -export * from "./harness/session/repo/memory.js"; -export * from "./harness/session/repo/shared.js"; +export * from "./harness/session/jsonl-repo.js"; +export * from "./harness/session/memory-repo.js"; +export * from "./harness/session/repo-utils.js"; export * from "./harness/session/session.js"; export { uuidv7 } from "./harness/session/uuid.js"; export * from "./harness/skills.js"; diff --git a/packages/agent/src/node.ts b/packages/agent/src/node.ts new file mode 100644 index 00000000..23ccae43 --- /dev/null +++ b/packages/agent/src/node.ts @@ -0,0 +1,2 @@ +export { NodeExecutionEnv } from "./harness/env/nodejs.js"; +export * from "./index.js"; diff --git a/packages/agent/test/harness/agent-harness-stream.test.ts b/packages/agent/test/harness/agent-harness-stream.test.ts index e5ca193f..dba98f91 100644 --- a/packages/agent/test/harness/agent-harness-stream.test.ts +++ b/packages/agent/test/harness/agent-harness-stream.test.ts @@ -2,8 +2,8 @@ import { fauxAssistantMessage, fauxToolCall, registerFauxProvider, type StreamOp import { afterEach, describe, expect, it } from "vitest"; import { AgentHarness } from "../../src/harness/agent-harness.js"; import { NodeExecutionEnv } from "../../src/harness/env/nodejs.js"; +import { InMemorySessionStorage } from "../../src/harness/session/memory-storage.js"; import { Session } from "../../src/harness/session/session.js"; -import { InMemorySessionStorage } from "../../src/harness/session/storage/memory.js"; import { calculateTool } from "../utils/calculate.js"; const registrations: Array<{ unregister(): void }> = []; diff --git a/packages/agent/test/harness/agent-harness.test.ts b/packages/agent/test/harness/agent-harness.test.ts index e0b4497e..6ded341c 100644 --- a/packages/agent/test/harness/agent-harness.test.ts +++ b/packages/agent/test/harness/agent-harness.test.ts @@ -2,8 +2,8 @@ import { fauxAssistantMessage, fauxToolCall, getModel, registerFauxProvider } fr import { afterEach, describe, expect, it } from "vitest"; import { AgentHarness } from "../../src/harness/agent-harness.js"; import { NodeExecutionEnv } from "../../src/harness/env/nodejs.js"; +import { InMemorySessionStorage } from "../../src/harness/session/memory-storage.js"; import { Session } from "../../src/harness/session/session.js"; -import { InMemorySessionStorage } from "../../src/harness/session/storage/memory.js"; import type { PromptTemplate, Skill } from "../../src/harness/types.js"; import type { AgentMessage, AgentTool } from "../../src/types.js"; import { calculateTool } from "../utils/calculate.js"; diff --git a/packages/agent/test/harness/nodejs-env.test.ts b/packages/agent/test/harness/nodejs-env.test.ts index 97c5da23..fa3d6aa9 100644 --- a/packages/agent/test/harness/nodejs-env.test.ts +++ b/packages/agent/test/harness/nodejs-env.test.ts @@ -2,7 +2,7 @@ import { access, chmod, realpath, symlink } from "node:fs/promises"; import { join } from "node:path"; import { afterEach, describe, expect, it } from "vitest"; import { NodeExecutionEnv } from "../../src/harness/env/nodejs.js"; -import { FileError, getOrThrow } from "../../src/harness/execution-env.js"; +import { FileError, getOrThrow } from "../../src/harness/types.js"; import { executeShellWithCapture } from "../../src/harness/utils/shell-output.js"; import { createTempDir } from "./session-test-utils.js"; @@ -21,6 +21,8 @@ describe("NodeExecutionEnv", () => { it("reads, writes, lists, and removes files and directories", async () => { const root = createTempDir(); const env = new NodeExecutionEnv({ cwd: root }); + expect(getOrThrow(await env.absolutePath("nested/child"))).toBe(join(root, "nested/child")); + expect(getOrThrow(await env.joinPath([root, "nested", "child"]))).toBe(join(root, "nested", "child")); getOrThrow(await env.createDir("nested/child")); getOrThrow(await env.writeFile("nested/child/file.txt", "hel")); getOrThrow(await env.appendFile("nested/child/file.txt", "lo")); @@ -71,7 +73,7 @@ describe("NodeExecutionEnv", () => { path: join(root, "dir-link"), kind: "symlink", }); - expect(getOrThrow(await env.realPath("file-link"))).toBe(await realpath(join(root, "dir/file.txt"))); + expect(getOrThrow(await env.canonicalPath("file-link"))).toBe(await realpath(join(root, "dir/file.txt"))); }); it("lists symlinks as symlinks", async () => { @@ -168,7 +170,7 @@ describe("NodeExecutionEnv", () => { env.appendFile("other.txt", "hello", signal), env.fileInfo("file.txt", signal), env.listDir(".", signal), - env.realPath("file.txt", signal), + env.canonicalPath("file.txt", signal), env.exists("file.txt", signal), env.createDir("dir", { abortSignal: signal }), env.remove("file.txt", { abortSignal: signal }), diff --git a/packages/agent/test/harness/repo.test.ts b/packages/agent/test/harness/repo.test.ts index 310d8898..fda51b03 100644 --- a/packages/agent/test/harness/repo.test.ts +++ b/packages/agent/test/harness/repo.test.ts @@ -1,7 +1,8 @@ import { existsSync } from "node:fs"; import { describe, expect, it } from "vitest"; -import { JsonlSessionRepo } from "../../src/harness/session/repo/jsonl.js"; -import { InMemorySessionRepo } from "../../src/harness/session/repo/memory.js"; +import { NodeExecutionEnv } from "../../src/harness/env/nodejs.js"; +import { JsonlSessionRepo } from "../../src/harness/session/jsonl-repo.js"; +import { InMemorySessionRepo } from "../../src/harness/session/memory-repo.js"; import { createAssistantMessage, createTempDir, createUserMessage } from "./session-test-utils.js"; describe("InMemorySessionRepo", () => { @@ -26,9 +27,10 @@ describe("InMemorySessionRepo", () => { describe("JsonlSessionRepo", () => { it("stores sessions below encoded cwd directories and lists by cwd", async () => { const root = createTempDir(); + const env = new NodeExecutionEnv({ cwd: root }); const cwd = "/tmp/my-project"; const otherCwd = "/tmp/other-project"; - const repo = new JsonlSessionRepo({ sessionsRoot: root }); + const repo = new JsonlSessionRepo({ fs: env, sessionsRoot: root }); const session = await repo.create({ cwd, id: "019de8c2-de29-73e9-ae0c-e134db34c447" }); const otherSession = await repo.create({ cwd: otherCwd, id: "other-session" }); const metadata = await session.getMetadata(); @@ -44,7 +46,8 @@ describe("JsonlSessionRepo", () => { it("opens, deletes, and forks by metadata", async () => { const root = createTempDir(); - const repo = new JsonlSessionRepo({ sessionsRoot: root }); + const env = new NodeExecutionEnv({ cwd: root }); + const repo = new JsonlSessionRepo({ fs: env, sessionsRoot: root }); const source = await repo.create({ cwd: "/tmp/source", id: "source-session" }); const sourceMetadata = await source.getMetadata(); const user1 = await source.appendMessage(createUserMessage("one")); diff --git a/packages/agent/test/harness/session-uuid.test.ts b/packages/agent/test/harness/session-uuid.test.ts index 0c7f42e2..247f3177 100644 --- a/packages/agent/test/harness/session-uuid.test.ts +++ b/packages/agent/test/harness/session-uuid.test.ts @@ -1,11 +1,4 @@ -import { describe, expect, it, vi } from "vitest"; - -const randomBytesMock = vi.hoisted(() => vi.fn<(size: number) => Buffer>()); - -vi.mock("node:crypto", () => ({ - randomBytes: randomBytesMock, -})); - +import { afterEach, describe, expect, it, vi } from "vitest"; import { uuidv7 } from "../../src/harness/session/uuid.js"; const UUID_V7_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/; @@ -15,14 +8,22 @@ function parseTimestamp(uuid: string): number { return Number.parseInt(uuid.replaceAll("-", "").slice(0, 12), 16); } +afterEach(() => { + vi.unstubAllGlobals(); +}); + describe("uuidv7", () => { it("uses the RFC 9562 layout and preserves monotonic order", () => { - randomBytesMock - .mockReturnValueOnce( - Buffer.from([0, 0, 0, 0, 0, 0, 0xff, 0xff, 0xff, 0xfe, 0x01, 0x11, 0x22, 0x33, 0x44, 0x55]), - ) - .mockReturnValueOnce(Buffer.alloc(16)) - .mockReturnValueOnce(Buffer.alloc(16)); + const randomValues = [ + new Uint8Array([0, 0, 0, 0, 0, 0, 0xff, 0xff, 0xff, 0xfe, 0x01, 0x11, 0x22, 0x33, 0x44, 0x55]), + new Uint8Array(16), + new Uint8Array(16), + ]; + const getRandomValues = vi.fn((bytes: Uint8Array) => { + bytes.set(randomValues.shift() ?? new Uint8Array(bytes.length)); + return bytes; + }); + vi.stubGlobal("crypto", { getRandomValues }); const dateNow = vi.spyOn(Date, "now").mockReturnValue(TIMESTAMP); try { @@ -41,8 +42,7 @@ describe("uuidv7", () => { expect(parseTimestamp(third)).toBe(TIMESTAMP + 1); expect(first < second).toBe(true); expect(second < third).toBe(true); - expect(randomBytesMock).toHaveBeenCalledTimes(3); - expect(randomBytesMock).toHaveBeenCalledWith(16); + expect(getRandomValues).toHaveBeenCalledTimes(3); } finally { dateNow.mockRestore(); } diff --git a/packages/agent/test/harness/session.test.ts b/packages/agent/test/harness/session.test.ts index d6b7f365..bfb0a6b3 100644 --- a/packages/agent/test/harness/session.test.ts +++ b/packages/agent/test/harness/session.test.ts @@ -1,9 +1,10 @@ import { readFileSync } from "node:fs"; import { join } from "node:path"; import { describe, expect, it } from "vitest"; +import { NodeExecutionEnv } from "../../src/harness/env/nodejs.js"; +import { JsonlSessionStorage } from "../../src/harness/session/jsonl-storage.js"; +import { InMemorySessionStorage } from "../../src/harness/session/memory-storage.js"; import { Session } from "../../src/harness/session/session.js"; -import { JsonlSessionStorage } from "../../src/harness/session/storage/jsonl.js"; -import { InMemorySessionStorage } from "../../src/harness/session/storage/memory.js"; import type { SessionStorage } from "../../src/harness/types.js"; import { createAssistantMessage, createTempDir, createUserMessage, getLatestTempDir } from "./session-test-utils.js"; @@ -128,7 +129,8 @@ runSessionSuite( "Session with JSONL storage", async () => { const dir = createTempDir(); - return await JsonlSessionStorage.create(join(dir, "session.jsonl"), { cwd: dir, sessionId: "session-1" }); + const env = new NodeExecutionEnv({ cwd: dir }); + return await JsonlSessionStorage.create(env, join(dir, "session.jsonl"), { cwd: dir, sessionId: "session-1" }); }, () => { const dir = getLatestTempDir(); diff --git a/packages/agent/test/harness/storage.test.ts b/packages/agent/test/harness/storage.test.ts index 8c05702b..20f7ac8d 100644 --- a/packages/agent/test/harness/storage.test.ts +++ b/packages/agent/test/harness/storage.test.ts @@ -1,8 +1,9 @@ import { existsSync, readFileSync, writeFileSync } from "node:fs"; import { join } from "node:path"; import { describe, expect, it } from "vitest"; -import { JsonlSessionStorage, loadJsonlSessionMetadata } from "../../src/harness/session/storage/jsonl.js"; -import { InMemorySessionStorage } from "../../src/harness/session/storage/memory.js"; +import { NodeExecutionEnv } from "../../src/harness/env/nodejs.js"; +import { JsonlSessionStorage, loadJsonlSessionMetadata } from "../../src/harness/session/jsonl-storage.js"; +import { InMemorySessionStorage } from "../../src/harness/session/memory-storage.js"; import type { MessageEntry, SessionMetadata } from "../../src/harness/types.js"; import { createAssistantMessage, createTempDir, createUserMessage } from "./session-test-utils.js"; @@ -102,14 +103,16 @@ describe("InMemorySessionStorage", () => { describe("JsonlSessionStorage", () => { it("throws for missing files when opening", async () => { const dir = createTempDir(); + const env = new NodeExecutionEnv({ cwd: dir }); const filePath = join(dir, "session.jsonl"); - await expect(JsonlSessionStorage.open(filePath)).rejects.toMatchObject({ code: "ENOENT" }); + await expect(JsonlSessionStorage.open(env, filePath)).rejects.toMatchObject({ code: "not_found" }); }); it("writes the header on create", async () => { const dir = createTempDir(); + const env = new NodeExecutionEnv({ cwd: dir }); const filePath = join(dir, "session.jsonl"); - const storage = await JsonlSessionStorage.create(filePath, { cwd: dir, sessionId: "session-1" }); + const storage = await JsonlSessionStorage.create(env, filePath, { cwd: dir, sessionId: "session-1" }); expect(existsSync(filePath)).toBe(true); expect(readFileSync(filePath, "utf8").trim().split("\n")).toHaveLength(1); expect(await storage.getLeafId()).toBeNull(); @@ -129,13 +132,15 @@ describe("JsonlSessionStorage", () => { it("throws for malformed session headers", async () => { const dir = createTempDir(); + const env = new NodeExecutionEnv({ cwd: dir }); const filePath = join(dir, "session.jsonl"); writeFileSync(filePath, "not json\n"); - await expect(JsonlSessionStorage.open(filePath)).rejects.toThrow("first line is not a valid session header"); + await expect(JsonlSessionStorage.open(env, filePath)).rejects.toThrow("first line is not a valid session header"); }); it("ignores malformed entry lines", async () => { const dir = createTempDir(); + const env = new NodeExecutionEnv({ cwd: dir }); const filePath = join(dir, "session.jsonl"); const header = { type: "session", @@ -152,15 +157,16 @@ describe("JsonlSessionStorage", () => { message: createUserMessage("one"), }; writeFileSync(filePath, `${JSON.stringify(header)}\nnot json\n${JSON.stringify(entry)}\n`); - const storage = await JsonlSessionStorage.open(filePath); + const storage = await JsonlSessionStorage.open(env, filePath); expect((await storage.getEntries()).map((loadedEntry) => loadedEntry.id)).toEqual(["entry-1"]); expect(await storage.getLeafId()).toBe("entry-1"); }); it("creates and reads session metadata from the header", async () => { const dir = createTempDir(); + const env = new NodeExecutionEnv({ cwd: dir }); const filePath = join(dir, "session.jsonl"); - const storage = await JsonlSessionStorage.create(filePath, { + const storage = await JsonlSessionStorage.create(env, filePath, { cwd: dir, sessionId: "session-1", parentSessionPath: "/tmp/parent.jsonl", @@ -179,13 +185,14 @@ describe("JsonlSessionStorage", () => { timestamp: "2026-01-01T00:00:00.000Z", message: createUserMessage("one"), }); - expect(await loadJsonlSessionMetadata(filePath)).toEqual(metadata); + expect(await loadJsonlSessionMetadata(env, filePath)).toEqual(metadata); }); it("loads existing entries and reconstructs leaf", async () => { const dir = createTempDir(); + const env = new NodeExecutionEnv({ cwd: dir }); const filePath = join(dir, "session.jsonl"); - const storage = await JsonlSessionStorage.create(filePath, { cwd: dir, sessionId: "session-1" }); + const storage = await JsonlSessionStorage.create(env, filePath, { cwd: dir, sessionId: "session-1" }); const root: MessageEntry = { type: "message", id: "root", @@ -201,7 +208,7 @@ describe("JsonlSessionStorage", () => { }; await storage.appendEntry(root); await storage.appendEntry(child); - const loaded = await JsonlSessionStorage.open(filePath); + const loaded = await JsonlSessionStorage.open(env, filePath); expect(await loaded.getLeafId()).toBe("child"); expect((await loaded.getEntries()).map((entry) => entry.id)).toEqual(["root", "child"]); expect((await loaded.getPathToRoot("child")).map((entry) => entry.id)).toEqual(["root", "child"]); @@ -209,8 +216,9 @@ describe("JsonlSessionStorage", () => { it("finds entries by type", async () => { const dir = createTempDir(); + const env = new NodeExecutionEnv({ cwd: dir }); const filePath = join(dir, "session.jsonl"); - const storage = await JsonlSessionStorage.create(filePath, { cwd: dir, sessionId: "session-1" }); + const storage = await JsonlSessionStorage.create(env, filePath, { cwd: dir, sessionId: "session-1" }); await storage.appendEntry({ type: "message", id: "entry-1", @@ -224,8 +232,9 @@ describe("JsonlSessionStorage", () => { it("maintains label lookup", async () => { const dir = createTempDir(); + const env = new NodeExecutionEnv({ cwd: dir }); const filePath = join(dir, "session.jsonl"); - const storage = await JsonlSessionStorage.create(filePath, { cwd: dir, sessionId: "session-1" }); + const storage = await JsonlSessionStorage.create(env, filePath, { cwd: dir, sessionId: "session-1" }); await storage.appendEntry({ type: "message", id: "entry-1", @@ -252,12 +261,13 @@ describe("JsonlSessionStorage", () => { label: undefined, }); expect(await storage.getLabel("entry-1")).toBeUndefined(); - const loaded = await JsonlSessionStorage.open(filePath); + const loaded = await JsonlSessionStorage.open(env, filePath); expect(await loaded.getLabel("entry-1")).toBeUndefined(); }); it("reads session metadata from only the first JSONL line", async () => { const dir = createTempDir(); + const env = new NodeExecutionEnv({ cwd: dir }); const filePath = join(dir, "session.jsonl"); const header = { type: "session", @@ -268,7 +278,7 @@ describe("JsonlSessionStorage", () => { }; const malformedSecondLine = "{".repeat(10000); writeFileSync(filePath, `${JSON.stringify(header)}\n${malformedSecondLine}\n`); - expect(await loadJsonlSessionMetadata(filePath)).toEqual({ + expect(await loadJsonlSessionMetadata(env, filePath)).toEqual({ id: "session-1", createdAt: "2026-01-01T00:00:00.000Z", cwd: dir, diff --git a/packages/agent/test/scratch/simple.ts b/packages/agent/test/scratch/simple.ts index 296b2160..2b8fed11 100644 --- a/packages/agent/test/scratch/simple.ts +++ b/packages/agent/test/scratch/simple.ts @@ -2,7 +2,7 @@ import { homedir } from "node:os"; import { join } from "node:path"; import { getModel } from "@earendil-works/pi-ai"; import { NodeExecutionEnv } from "../../src/harness/env/nodejs.js"; -import { InMemorySessionStorage } from "../../src/harness/session/storage/memory.js"; +import { InMemorySessionStorage } from "../../src/harness/session/memory-storage.js"; import { AgentHarness, formatSkillsForSystemPrompt, From 3ca474b15b2df46cf61d5a1383a993f49b741b17 Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Fri, 15 May 2026 01:02:36 +0200 Subject: [PATCH 042/166] fix(coding-agent): fix Termux docs code fence closes #4503 --- packages/coding-agent/CHANGELOG.md | 1 + packages/coding-agent/docs/termux.md | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 4de7ea76..ca020073 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -9,6 +9,7 @@ ### Fixed +- Fixed nested code fences in the Termux setup documentation so the example AGENTS.md renders correctly ([#4503](https://github.com/earendil-works/pi/issues/4503)). - Fixed tool output expansion while extension confirmation dialogs are focused ([#4429](https://github.com/earendil-works/pi/issues/4429)). - Fixed auto-retry for Anthropic streams that end before `message_stop` ([#4433](https://github.com/earendil-works/pi/issues/4433)). - Fixed theme sharing across package scopes so extensions do not crash with `Theme not initialized` ([#4333](https://github.com/earendil-works/pi/issues/4333)). diff --git a/packages/coding-agent/docs/termux.md b/packages/coding-agent/docs/termux.md index cb16a283..c9b9973c 100644 --- a/packages/coding-agent/docs/termux.md +++ b/packages/coding-agent/docs/termux.md @@ -36,7 +36,7 @@ Image clipboard is not supported on Termux (the `ctrl+v` image paste feature wil Create `~/.pi/agent/AGENTS.md` to help the agent understand the Termux environment: -```markdown +````markdown # Agent Environment: Termux on Android ## Location @@ -91,7 +91,7 @@ termux-camera-photo out.jpg # Take photo - Termux:API app must be installed for `termux-*` commands - Use `pkg install termux-api` for the command-line tools - Storage permission needed for `/storage/emulated/0` access -``` +```` ## Limitations From 7ddc7dd1ec9b2a32e54f8d23b45b37e5f886510a Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Fri, 15 May 2026 01:10:56 +0200 Subject: [PATCH 043/166] fix(coding-agent): space interactive errors closes #4510 --- packages/coding-agent/CHANGELOG.md | 1 + packages/coding-agent/src/modes/interactive/interactive-mode.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index ca020073..5e6d23d8 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -9,6 +9,7 @@ ### Fixed +- Fixed interactive error messages to render with trailing spacing so reload errors do not run into resource listings ([#4510](https://github.com/earendil-works/pi/issues/4510)). - Fixed nested code fences in the Termux setup documentation so the example AGENTS.md renders correctly ([#4503](https://github.com/earendil-works/pi/issues/4503)). - Fixed tool output expansion while extension confirmation dialogs are focused ([#4429](https://github.com/earendil-works/pi/issues/4429)). - Fixed auto-retry for Anthropic streams that end before `message_stop` ([#4433](https://github.com/earendil-works/pi/issues/4433)). diff --git a/packages/coding-agent/src/modes/interactive/interactive-mode.ts b/packages/coding-agent/src/modes/interactive/interactive-mode.ts index 8db98111..be1c1cb7 100644 --- a/packages/coding-agent/src/modes/interactive/interactive-mode.ts +++ b/packages/coding-agent/src/modes/interactive/interactive-mode.ts @@ -3542,6 +3542,7 @@ export class InteractiveMode { showError(errorMessage: string): void { this.chatContainer.addChild(new Spacer(1)); this.chatContainer.addChild(new Text(theme.fg("error", `Error: ${errorMessage}`), 1, 0)); + this.chatContainer.addChild(new Spacer(1)); this.ui.requestRender(); } From a8af0b5e99e3309c5cbc3301c276302512789ca5 Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Fri, 15 May 2026 01:26:31 +0200 Subject: [PATCH 044/166] fix(ai): ignore generic GitHub tokens for Copilot auth closes #4485 --- packages/ai/CHANGELOG.md | 1 + packages/ai/README.md | 2 +- packages/ai/src/env-api-keys.ts | 2 +- packages/ai/test/env-api-keys.test.ts | 46 +++++++++++++++++++++++++++ 4 files changed, 49 insertions(+), 2 deletions(-) create mode 100644 packages/ai/test/env-api-keys.test.ts diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index 5e3df94c..01a63393 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -8,6 +8,7 @@ ### Fixed +- Fixed GitHub Copilot model availability to ignore generic `GH_TOKEN` and `GITHUB_TOKEN` environment variables, requiring OAuth login or `COPILOT_GITHUB_TOKEN` instead ([#4485](https://github.com/earendil-works/pi/issues/4485)). - Fixed Bedrock proxy handling to preserve `NO_PROXY` exclusions while using HTTP(S)-only proxy agents. - Fixed GitHub Copilot Claude test coverage to use the current Claude Sonnet 4.6 model ID. - Fixed OpenAI Responses requests for models that support disabling reasoning to send `reasoning.effort: "none"` when thinking is off. diff --git a/packages/ai/README.md b/packages/ai/README.md index e88de28e..1790a64c 100644 --- a/packages/ai/README.md +++ b/packages/ai/README.md @@ -1122,7 +1122,7 @@ In Node.js environments, you can set environment variables to avoid passing API | Xiaomi MiMo Token Plan (China) | `XIAOMI_TOKEN_PLAN_CN_API_KEY` | | Xiaomi MiMo Token Plan (Amsterdam) | `XIAOMI_TOKEN_PLAN_AMS_API_KEY` | | Xiaomi MiMo Token Plan (Singapore) | `XIAOMI_TOKEN_PLAN_SGP_API_KEY` | -| GitHub Copilot | `COPILOT_GITHUB_TOKEN` or `GH_TOKEN` or `GITHUB_TOKEN` | +| GitHub Copilot | `COPILOT_GITHUB_TOKEN` | When set, the library automatically uses these keys: diff --git a/packages/ai/src/env-api-keys.ts b/packages/ai/src/env-api-keys.ts index 846623aa..6ed76d41 100644 --- a/packages/ai/src/env-api-keys.ts +++ b/packages/ai/src/env-api-keys.ts @@ -90,7 +90,7 @@ function hasVertexAdcCredentials(): boolean { function getApiKeyEnvVars(provider: string): readonly string[] | undefined { if (provider === "github-copilot") { - return ["COPILOT_GITHUB_TOKEN", "GH_TOKEN", "GITHUB_TOKEN"]; + return ["COPILOT_GITHUB_TOKEN"]; } // ANTHROPIC_OAUTH_TOKEN takes precedence over ANTHROPIC_API_KEY diff --git a/packages/ai/test/env-api-keys.test.ts b/packages/ai/test/env-api-keys.test.ts new file mode 100644 index 00000000..6889743d --- /dev/null +++ b/packages/ai/test/env-api-keys.test.ts @@ -0,0 +1,46 @@ +import { afterEach, describe, expect, it } from "vitest"; +import { findEnvKeys, getEnvApiKey } from "../src/env-api-keys.js"; + +const originalCopilotGitHubToken = process.env.COPILOT_GITHUB_TOKEN; +const originalGhToken = process.env.GH_TOKEN; +const originalGitHubToken = process.env.GITHUB_TOKEN; + +afterEach(() => { + if (originalCopilotGitHubToken === undefined) { + delete process.env.COPILOT_GITHUB_TOKEN; + } else { + process.env.COPILOT_GITHUB_TOKEN = originalCopilotGitHubToken; + } + + if (originalGhToken === undefined) { + delete process.env.GH_TOKEN; + } else { + process.env.GH_TOKEN = originalGhToken; + } + + if (originalGitHubToken === undefined) { + delete process.env.GITHUB_TOKEN; + } else { + process.env.GITHUB_TOKEN = originalGitHubToken; + } +}); + +describe("environment API keys", () => { + it("does not treat generic GitHub tokens as GitHub Copilot credentials", () => { + delete process.env.COPILOT_GITHUB_TOKEN; + process.env.GH_TOKEN = "gh-token"; + process.env.GITHUB_TOKEN = "github-token"; + + expect(findEnvKeys("github-copilot")).toBeUndefined(); + expect(getEnvApiKey("github-copilot")).toBeUndefined(); + }); + + it("resolves GitHub Copilot credentials from COPILOT_GITHUB_TOKEN", () => { + process.env.COPILOT_GITHUB_TOKEN = "copilot-token"; + process.env.GH_TOKEN = "gh-token"; + process.env.GITHUB_TOKEN = "github-token"; + + expect(findEnvKeys("github-copilot")).toEqual(["COPILOT_GITHUB_TOKEN"]); + expect(getEnvApiKey("github-copilot")).toBe("copilot-token"); + }); +}); From 98ffad0437c8b4bcbd56244be129899b4cc4e283 Mon Sep 17 00:00:00 2001 From: Ramiz Wachtler Date: Fri, 15 May 2026 15:11:43 +0200 Subject: [PATCH 045/166] fix(ai): openai-completions - throw error on missing finish-reason - require \ before treating \ streams as successful - add regression coverage for truncated streams without \ - closes #4345 --- packages/ai/CHANGELOG.md | 1 + .../ai/src/providers/openai-completions.ts | 5 +++ .../openai-completions-tool-choice.test.ts | 32 +++++++++++++++++++ 3 files changed, 38 insertions(+) diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index 01a63393..7fa3a37d 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -9,6 +9,7 @@ ### Fixed - Fixed GitHub Copilot model availability to ignore generic `GH_TOKEN` and `GITHUB_TOKEN` environment variables, requiring OAuth login or `COPILOT_GITHUB_TOKEN` instead ([#4485](https://github.com/earendil-works/pi/issues/4485)). +- Fixed `openai-completions` streams to surface an error when the stream ends before any terminal `finish_reason`, so truncated responses can retry instead of being accepted as success ([#4345](https://github.com/earendil-works/pi/issues/4345)). - Fixed Bedrock proxy handling to preserve `NO_PROXY` exclusions while using HTTP(S)-only proxy agents. - Fixed GitHub Copilot Claude test coverage to use the current Claude Sonnet 4.6 model ID. - Fixed OpenAI Responses requests for models that support disabling reasoning to send `reasoning.effort: "none"` when thinking is off. diff --git a/packages/ai/src/providers/openai-completions.ts b/packages/ai/src/providers/openai-completions.ts index 01bc6e78..0d753363 100644 --- a/packages/ai/src/providers/openai-completions.ts +++ b/packages/ai/src/providers/openai-completions.ts @@ -165,6 +165,7 @@ export const streamOpenAICompletions: StreamFunction<"openai-completions", OpenA let textBlock: TextContent | null = null; let thinkingBlock: ThinkingContent | null = null; + let hasFinishReason = false; const toolCallBlocksByIndex = new Map(); const toolCallBlocksById = new Map(); const blocks = output.content as StreamingBlock[]; @@ -288,6 +289,7 @@ export const streamOpenAICompletions: StreamFunction<"openai-completions", OpenA if (finishReasonResult.errorMessage) { output.errorMessage = finishReasonResult.errorMessage; } + hasFinishReason = true; } if (choice.delta) { @@ -390,6 +392,9 @@ export const streamOpenAICompletions: StreamFunction<"openai-completions", OpenA if (output.stopReason === "error") { throw new Error(output.errorMessage || "Provider returned an error stop reason"); } + if (!hasFinishReason) { + throw new Error("Stream ended without finish_reason"); + } stream.push({ type: "done", reason: output.stopReason, message: output }); stream.end(); diff --git a/packages/ai/test/openai-completions-tool-choice.test.ts b/packages/ai/test/openai-completions-tool-choice.test.ts index c9c8ec9f..8ed6165d 100644 --- a/packages/ai/test/openai-completions-tool-choice.test.ts +++ b/packages/ai/test/openai-completions-tool-choice.test.ts @@ -441,6 +441,38 @@ describe("openai-completions tool_choice", () => { expect(response.content).toEqual([{ type: "text", text: "OK" }]); }); + it("errors when a stream ends after only null finish_reason chunks", async () => { + mockState.chunks = [ + { + id: "chatcmpl-truncated", + choices: [{ delta: { content: "partial answer" }, finish_reason: null }], + }, + { + id: "chatcmpl-truncated", + choices: [{ delta: { content: "partial answer" }, finish_reason: null }], + }, + ]; + + const { compat: _compat, ...baseModel } = getModel("openai", "gpt-4o-mini")!; + const model = { ...baseModel, api: "openai-completions" } as const; + const response = await streamSimple( + model, + { + messages: [ + { + role: "user", + content: "Reply with a longer sentence", + timestamp: Date.now(), + }, + ], + }, + { apiKey: "test" }, + ).result(); + + expect(response.stopReason).toBe("error"); + expect(response.errorMessage).toBe("Stream ended without finish_reason"); + }); + it("coalesces tool call deltas by stable index when provider mutates ids mid-stream", async () => { mockState.chunks = [ { From 656bd2cadf0fed5858747328f776ede128a7224b Mon Sep 17 00:00:00 2001 From: Julien Chaumond Date: Fri, 15 May 2026 20:23:34 +0200 Subject: [PATCH 046/166] docs(coding-agent): fix invalid notify type in extensions example MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The notify() API only accepts "info" | "warning" | "error" — "success" was a copy-paste error in the example. Co-Authored-By: julien-agent --- packages/coding-agent/docs/extensions.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/coding-agent/docs/extensions.md b/packages/coding-agent/docs/extensions.md index 998a650d..ce922f2a 100644 --- a/packages/coding-agent/docs/extensions.md +++ b/packages/coding-agent/docs/extensions.md @@ -162,7 +162,7 @@ export default function (pi: ExtensionAPI) { pi.on("event_name", async (event, ctx) => { // ctx.ui for user interaction const ok = await ctx.ui.confirm("Title", "Are you sure?"); - ctx.ui.notify("Done!", "success"); + ctx.ui.notify("Done!", "info"); ctx.ui.setStatus("my-ext", "Processing..."); // Footer status ctx.ui.setWidget("my-ext", ["Line 1", "Line 2"]); // Widget above editor (default) }); From 4f40f62b7b63c13b3a249719394d474ba58d65cb Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Sat, 16 May 2026 00:32:16 +0200 Subject: [PATCH 047/166] refactor(agent): harden harness session semantics --- AGENTS.md | 1 + packages/agent/docs/agent-harness.md | 47 +- packages/agent/src/harness/agent-harness.ts | 556 ++++++++++-------- .../compaction/branch-summarization.ts | 167 ++---- .../src/harness/compaction/compaction.ts | 258 ++------ .../agent/src/harness/compaction/utils.ts | 62 +- packages/agent/src/harness/env/nodejs.ts | 154 +++-- .../agent/src/harness/prompt-templates.ts | 64 +- .../agent/src/harness/session/jsonl-repo.ts | 75 ++- .../src/harness/session/jsonl-storage.ts | 155 +++-- .../agent/src/harness/session/memory-repo.ts | 7 +- .../src/harness/session/memory-storage.ts | 51 +- .../agent/src/harness/session/repo-utils.ts | 21 +- packages/agent/src/harness/session/session.ts | 5 +- packages/agent/src/harness/skills.ts | 129 +++- packages/agent/src/harness/types.ts | 107 +++- .../agent/src/harness/utils/shell-output.ts | 16 +- packages/agent/src/index.ts | 3 + .../agent/test/harness/compaction.test.ts | 45 +- .../agent/test/harness/nodejs-env.test.ts | 21 +- packages/agent/test/harness/session.test.ts | 6 +- packages/agent/test/harness/skills.test.ts | 1 + packages/agent/test/harness/storage.test.ts | 34 +- 23 files changed, 1112 insertions(+), 873 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index bf250bc4..e3880791 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -6,6 +6,7 @@ - No emojis in commits, issues, PR comments, or code - No fluff or cheerful filler text - Technical prose only, be kind but direct (e.g., "Thanks @user" not "Thanks so much @user!") +- When the user asks a question, answer it first before making edits or running implementation commands. ## Code Quality diff --git a/packages/agent/docs/agent-harness.md b/packages/agent/docs/agent-harness.md index 3ab1faf0..5a092fa2 100644 --- a/packages/agent/docs/agent-harness.md +++ b/packages/agent/docs/agent-harness.md @@ -21,9 +21,13 @@ A final lifecycle hardening pass should prove these guarantees with a broad list ## Error handling -The target error-handling model is `Result` for fallible harness operations instead of thrown exceptions. Implementations should catch backend/provider/filesystem exceptions at the boundary and normalize them into typed error results; callers should inspect returned results instead of relying on thrown exceptions. +The current split is: -This is currently implemented for `ExecutionEnv`, `NodeExecutionEnv`, shell-output capture, and skill/prompt-template resource loading. Older harness/session/compaction APIs still throw in several paths; finishing that migration is tracked in the cleanup todo. +- low-level capabilities and helpers use `Result` where expected failures are contained and must not throw, such as `ExecutionEnv`, filesystem/shell operations, shell-output capture, resource loading, and compaction helpers +- high-level mutation/orchestration APIs such as `Session` and `AgentHarness` reject/throw instead of returning bare results that can be ignored +- public `AgentHarness` failures are normalized to `AgentHarnessError` where practical; subsystem errors are preserved as `cause` + +Harness events observe committed state. Public mutators validate required input and persistence before committing when practical, then await notifications. If a hook or subscriber fails after commit, the state change is not rolled back and the public method rejects with `AgentHarnessError` code `"hook"`. ## State model @@ -73,6 +77,8 @@ Stream options are shallow-copied when a snapshot is created. `headers` and `met The session contains persisted entries only. Session reads return persisted state and do not include queued writes. +Session storage implementations must persist leaf changes as `leaf` entries. `setLeafId()` is not an in-memory-only cursor update; it appends a durable entry whose `targetId` is the active tree leaf or `null` for root. Reopening storage must reconstruct the current leaf from the latest persisted leaf-affecting entry. + ### Pending session writes Session writes requested while an operation is active are queued as pending session writes. Pending writes are based on session-entry shapes without generated fields (`id`, `parentId`, `timestamp`). @@ -97,7 +103,7 @@ Structural operations require `phase === "idle"` and synchronously set the phase - `compact` - `navigateTree` -Starting another structural operation while the harness is not idle currently throws. This should become a typed result failure as part of the error-handling cleanup. +Starting another structural operation while the harness is not idle rejects with `AgentHarnessError` code `"busy"`. The following operations are allowed during a turn where appropriate: @@ -148,7 +154,7 @@ The low-level loop converts harness `ThinkingLevel` to provider `reasoning` at t No state refresh is needed on `agent_end` except flushing leftover pending session writes and clearing the operation phase. The exact `settled` event timing is still under review. -If the system-prompt callback throws while starting `prompt`, `skill`, or `promptFromTemplate`, the operation currently throws and the harness returns to idle. This should become a typed result failure as part of the error-handling cleanup. If it throws from the save-point snapshot created by `prepareNextTurn`, the low-level agent run records an assistant error message. +If the system-prompt callback throws while starting `prompt`, `skill`, or `promptFromTemplate`, the operation rejects with `AgentHarnessError` and the harness returns to idle. If it throws from the save-point snapshot created by `prepareNextTurn`, the low-level agent run records an assistant error message. ## Hooks and events @@ -331,7 +337,7 @@ Implemented so far: - `setTools(tools, activeToolNames?)` - `setActiveTools(toolNames)` -- invalid active tool names currently throw; convert to result errors +- invalid active tool names reject with `AgentHarnessError` - generic common app tool shape via `AgentHarness` - `QueueMode` exported from core types - `AgentHarnessOptions.steeringMode` / `followUpMode` @@ -363,6 +369,17 @@ Implemented so far: - Removed `liveOperationId`. - Removed `shell()`; use `harness.env`. +Implemented in the hardening pass: + +- Structural compaction/tree operations restore phase with `finally`. +- Public harness failures normalize subsystem causes to `AgentHarnessError`. +- Pending session writes flush one-by-one and are not dropped on failure. +- Queue drains roll back if queue-update notification fails. +- `message_end` persistence happens before subscriber notification. +- `abort()` signals cancellation before notifications and still waits for idle through notification errors. +- Idle model/thinking/tool updates validate and persist before committing in-memory state. +- `setLeafId()` persists durable `leaf` entries so tree navigation survives storage reopen. + Still needed: - Finalize phase/idle semantics. @@ -371,7 +388,6 @@ Still needed: - Audit follow-up behavior around `agent_end`. - Implement auto-compaction decision point. - Implement retry handling. -- Ensure structural operations use consistent `try/finally` phase cleanup. - Verify `before_agent_start` hook semantics against coding-agent: - current behavior appends returned messages after the user/next-turn prompt messages. - decide whether replacement, prepend, append, or transform semantics are correct. @@ -379,9 +395,9 @@ Still needed: - Document or change timing for model/thinking/stream-option events that may fire before queued session entries persist while busy. - Audit `abort()` barrier semantics. -### 7. Complete `Result`/non-throwing harness cleanup +### 7. Complete low-level `Result` cleanup -Started. +Current hardening pass complete; future items remain as the API evolves. Implemented so far: @@ -395,17 +411,22 @@ Implemented so far: - Replaced `Buffer` usage in generic truncation utilities with runtime-neutral UTF-8 handling. - Converted compaction summary helpers to typed result returns and added error-path coverage. - Expanded `NodeExecutionEnv` tests for file operations, exec errors, aborts, callbacks, timeouts, and shell-output full-output spill. +- Added `readTextLines()` so JSONL metadata loading reads only the header line instead of whole session files. +- Removed no-op abort handling from Node filesystem methods where cancellation is not meaningful while keeping the `FileSystem` interface unchanged. +- Mapped filesystem errors crossing the session boundary to typed `SessionError`. +- Added typed branch-summary errors and cause-aware public harness error normalization. +- Made resource loaders report structured diagnostics for non-`not_found` filesystem failures. -Still needed: +Ongoing guardrails: -- Remove remaining throws from `src/harness` APIs and helpers, except explicit adapter/test helpers such as `getOrThrow()`. -- Convert session storage/repo/session APIs to typed result returns. -- Convert structural `AgentHarness` operations to typed result returns for busy, missing-resource, auth, compaction, and branch-summary failures. +- Keep low-level capability/helper APIs non-throwing where they return `Result`. +- Keep session storage/repo/session APIs throwing typed `SessionError`. +- Keep structural `AgentHarness` operations rejecting with `AgentHarnessError` for busy, missing-resource, auth, compaction, and branch-summary failures. - Keep Node-specific APIs isolated under `src/harness/env/nodejs.ts` and Node-backed storage/session implementations, or move those implementations behind explicit Node-only entry points. - Audit remaining generic harness utilities for Node globals as new APIs are added. - Audit package exports so browser/generic-JS imports do not pull Node-only modules such as `NodeExecutionEnv`. - Keep expanding `ExecutionEnv` and shell-output contract tests as the API evolves, especially for non-Node implementations. -- Add tests proving harness APIs return `ok: false` instead of throwing for expected failure paths. +- Add tests proving public harness failures reject with `AgentHarnessError` where expected. ### 8. Later coding-agent migration plan diff --git a/packages/agent/src/harness/agent-harness.ts b/packages/agent/src/harness/agent-harness.ts index 5b78316e..6ab5dbef 100644 --- a/packages/agent/src/harness/agent-harness.ts +++ b/packages/agent/src/harness/agent-harness.ts @@ -38,6 +38,7 @@ import type { Session, Skill, } from "./types.js"; +import { AgentHarnessError, BranchSummaryError, CompactionError, SessionError, toError } from "./types.js"; function createUserMessage(text: string, images?: ImageContent[]): UserMessage { const content: Array<{ type: "text"; text: string } | ImageContent> = [{ type: "text", text }]; @@ -131,6 +132,19 @@ const SUBSCRIBER_EVENT_TYPE = "*"; type AgentHarnessHandler = (event: any, signal?: AbortSignal) => Promise | any; +function normalizeHarnessError(error: unknown, fallbackCode: AgentHarnessError["code"]): AgentHarnessError { + if (error instanceof AgentHarnessError) return error; + const cause = toError(error); + if (cause instanceof SessionError) return new AgentHarnessError("session", cause.message, cause); + if (cause instanceof CompactionError) return new AgentHarnessError("compaction", cause.message, cause); + if (cause instanceof BranchSummaryError) return new AgentHarnessError("branch_summary", cause.message, cause); + return new AgentHarnessError(fallbackCode, cause.message, cause); +} + +function normalizeHookError(error: unknown): AgentHarnessError { + return normalizeHarnessError(error, "hook"); +} + interface AgentHarnessTurnState< TSkill extends Skill = Skill, TPromptTemplate extends PromptTemplate = PromptTemplate, @@ -196,13 +210,21 @@ export class AgentHarness< private async emitOwn(event: AgentHarnessOwnEvent, signal?: AbortSignal): Promise { for (const listener of this.getHandlers(SUBSCRIBER_EVENT_TYPE) ?? []) { - await listener(event, signal); + try { + await listener(event, signal); + } catch (error) { + throw normalizeHookError(error); + } } } private async emitAny(event: AgentHarnessEvent, signal?: AbortSignal): Promise { for (const listener of this.getHandlers(SUBSCRIBER_EVENT_TYPE) ?? []) { - await listener(event, signal); + try { + await listener(event, signal); + } catch (error) { + throw normalizeHookError(error); + } } } @@ -213,9 +235,13 @@ export class AgentHarness< if (!handlers || handlers.size === 0) return undefined; let lastResult: AgentHarnessEventResultMap[TType] | undefined; for (const handler of handlers) { - const result = await handler(event); - if (result !== undefined) { - lastResult = result; + try { + const result = await handler(event); + if (result !== undefined) { + lastResult = result; + } + } catch (error) { + throw normalizeHookError(error); } } return lastResult; @@ -230,14 +256,18 @@ export class AgentHarness< let current = cloneStreamOptions(streamOptions); if (!handlers || handlers.size === 0) return current; for (const handler of handlers) { - const result = await handler({ - type: "before_provider_request", - model, - sessionId, - streamOptions: cloneStreamOptions(current), - }); - if (result?.streamOptions) { - current = applyStreamOptionsPatch(current, result.streamOptions); + try { + const result = await handler({ + type: "before_provider_request", + model, + sessionId, + streamOptions: cloneStreamOptions(current), + }); + if (result?.streamOptions) { + current = applyStreamOptionsPatch(current, result.streamOptions); + } + } catch (error) { + throw normalizeHookError(error); } } return current; @@ -248,9 +278,13 @@ export class AgentHarness< let current = payload; if (!handlers || handlers.size === 0) return current; for (const handler of handlers) { - const result = await handler({ type: "before_provider_payload", model, payload: current }); - if (result !== undefined) { - current = result.payload; + try { + const result = await handler({ type: "before_provider_payload", model, payload: current }); + if (result !== undefined) { + current = result.payload; + } + } catch (error) { + throw normalizeHookError(error); } } return current; @@ -356,8 +390,14 @@ export class AgentHarness< private async drainQueuedMessages(queue: AgentMessage[], mode: QueueMode): Promise { const messages = mode === "all" ? queue.splice(0) : queue.splice(0, 1); - if (messages.length > 0) await this.emitQueueUpdate(); - return messages; + if (messages.length === 0) return messages; + try { + await this.emitQueueUpdate(); + return messages; + } catch (error) { + queue.unshift(...messages); + throw normalizeHookError(error); + } } private createLoopConfig( @@ -411,15 +451,14 @@ export class AgentHarness< }; } - private validateToolNames(toolNames: string[]): void { - const missing = toolNames.filter((name) => !this.tools.has(name)); - if (missing.length > 0) throw new Error(`Unknown tool(s): ${missing.join(", ")}`); + private validateToolNames(toolNames: string[], tools: Map = this.tools): void { + const missing = toolNames.filter((name) => !tools.has(name)); + if (missing.length > 0) throw new AgentHarnessError("invalid_argument", `Unknown tool(s): ${missing.join(", ")}`); } private async flushPendingSessionWrites(): Promise { - const writes = this.pendingSessionWrites; - this.pendingSessionWrites = []; - for (const write of writes) { + while (this.pendingSessionWrites.length > 0) { + const write = this.pendingSessionWrites[0]!; if (write.type === "message") { await this.session.appendMessage(write.message); } else if (write.type === "model_change") { @@ -434,28 +473,40 @@ export class AgentHarness< await this.session.appendLabel(write.targetId, write.label); } else if (write.type === "session_info") { await this.session.appendSessionName(write.name ?? ""); + } else if (write.type === "leaf") { + await this.session.getStorage().setLeafId(write.targetId); } + this.pendingSessionWrites.shift(); } } private async handleAgentEvent(event: AgentEvent, signal?: AbortSignal): Promise { - await this.emitAny(event, signal); if (event.type === "message_end") { await this.session.appendMessage(event.message); + await this.emitAny(event, signal); + return; } if (event.type === "turn_end") { + let eventError: unknown; + try { + await this.emitAny(event, signal); + } catch (error) { + eventError = error; + } const hadPendingMutations = this.pendingSessionWrites.length > 0; await this.flushPendingSessionWrites(); - await this.emitOwn({ - type: "save_point", - hadPendingMutations, - }); + if (eventError) throw eventError; + await this.emitOwn({ type: "save_point", hadPendingMutations }); + return; } if (event.type === "agent_end") { await this.flushPendingSessionWrites(); this.phase = "idle"; + await this.emitAny(event, signal); await this.emitOwn({ type: "settled", nextTurnCount: this.nextTurnQueue.length }, signal); + return; } + await this.emitAny(event, signal); } private async emitRunFailure( @@ -480,9 +531,14 @@ export class AgentHarness< let activeTurnState = turnState; let messages: AgentMessage[] = [createUserMessage(text, options?.images)]; if (this.nextTurnQueue.length > 0) { - messages = [...this.nextTurnQueue, messages[0]!]; - this.nextTurnQueue = []; - await this.emitQueueUpdate(); + const queuedMessages = this.nextTurnQueue.splice(0); + try { + await this.emitQueueUpdate(); + } catch (error) { + this.nextTurnQueue.unshift(...queuedMessages); + throw normalizeHookError(error); + } + messages = [...queuedMessages, messages[0]!]; } const beforeResult = await this.emitHook({ type: "before_agent_start", @@ -510,12 +566,20 @@ export class AgentHarness< this.createStreamFn(getTurnState), ); } catch (error) { - return await this.emitRunFailure( - activeTurnState.model, - error, - abortController.signal.aborted, - abortController.signal, - ); + try { + return await this.emitRunFailure( + activeTurnState.model, + error, + abortController.signal.aborted, + abortController.signal, + ); + } catch (failureError) { + const cause = new AggregateError( + [toError(error), toError(failureError)], + "Agent run failed and failure reporting failed", + ); + throw new AgentHarnessError("unknown", cause.message, cause); + } } })(); try { @@ -526,7 +590,7 @@ export class AgentHarness< return message; } } - throw new Error("AgentHarness prompt completed without an assistant message"); + throw new AgentHarnessError("invalid_state", "AgentHarness prompt completed without an assistant message"); } finally { try { await this.flushPendingSessionWrites(); @@ -537,7 +601,7 @@ export class AgentHarness< } async prompt(text: string, options?: { images?: ImageContent[] }): Promise { - if (this.phase !== "idle") throw new Error("AgentHarness is busy"); + if (this.phase !== "idle") throw new AgentHarnessError("busy", "AgentHarness is busy"); this.phase = "turn"; const finishRunPromise = this.startRunPromise(); try { @@ -545,230 +609,229 @@ export class AgentHarness< return await this.executeTurn(turnState, text, options); } catch (error) { this.phase = "idle"; - throw error; + throw normalizeHarnessError(error, "unknown"); } finally { finishRunPromise(); } } async skill(name: string, additionalInstructions?: string): Promise { - if (this.phase !== "idle") throw new Error("AgentHarness is busy"); + if (this.phase !== "idle") throw new AgentHarnessError("busy", "AgentHarness is busy"); this.phase = "turn"; const finishRunPromise = this.startRunPromise(); try { const turnState = await this.createTurnState(); const skill = (turnState.resources.skills ?? []).find((candidate) => candidate.name === name); - if (!skill) throw new Error(`Unknown skill: ${name}`); + if (!skill) throw new AgentHarnessError("invalid_argument", `Unknown skill: ${name}`); return await this.executeTurn(turnState, formatSkillInvocation(skill, additionalInstructions)); } catch (error) { this.phase = "idle"; - throw error; + throw normalizeHarnessError(error, "unknown"); } finally { finishRunPromise(); } } async promptFromTemplate(name: string, args: string[] = []): Promise { - if (this.phase !== "idle") throw new Error("AgentHarness is busy"); + if (this.phase !== "idle") throw new AgentHarnessError("busy", "AgentHarness is busy"); this.phase = "turn"; const finishRunPromise = this.startRunPromise(); try { const turnState = await this.createTurnState(); const template = (turnState.resources.promptTemplates ?? []).find((candidate) => candidate.name === name); - if (!template) throw new Error(`Unknown prompt template: ${name}`); + if (!template) throw new AgentHarnessError("invalid_argument", `Unknown prompt template: ${name}`); return await this.executeTurn(turnState, formatPromptTemplateInvocation(template, args)); } catch (error) { this.phase = "idle"; - throw error; + throw normalizeHarnessError(error, "unknown"); } finally { finishRunPromise(); } } - steer(text: string, options?: { images?: ImageContent[] }): void { - if (this.phase === "idle") throw new Error("Cannot steer while idle"); + async steer(text: string, options?: { images?: ImageContent[] }): Promise { + if (this.phase === "idle") throw new AgentHarnessError("invalid_state", "Cannot steer while idle"); this.steerQueue.push(createUserMessage(text, options?.images)); - void this.emitQueueUpdate(); + await this.emitQueueUpdate(); } - followUp(text: string, options?: { images?: ImageContent[] }): void { - if (this.phase === "idle") throw new Error("Cannot follow up while idle"); + async followUp(text: string, options?: { images?: ImageContent[] }): Promise { + if (this.phase === "idle") throw new AgentHarnessError("invalid_state", "Cannot follow up while idle"); this.followUpQueue.push(createUserMessage(text, options?.images)); - void this.emitQueueUpdate(); + await this.emitQueueUpdate(); } - nextTurn(text: string, options?: { images?: ImageContent[] }): void { + async nextTurn(text: string, options?: { images?: ImageContent[] }): Promise { this.nextTurnQueue.push(createUserMessage(text, options?.images)); - void this.emitQueueUpdate(); + await this.emitQueueUpdate(); } async appendMessage(message: AgentMessage): Promise { - if (this.phase === "idle") { - await this.session.appendMessage(message); - } else { - this.pendingSessionWrites.push({ type: "message", message }); + try { + if (this.phase === "idle") { + await this.session.appendMessage(message); + } else { + this.pendingSessionWrites.push({ type: "message", message }); + } + } catch (error) { + throw normalizeHarnessError(error, "session"); } } async compact( customInstructions?: string, ): Promise<{ summary: string; firstKeptEntryId: string; tokensBefore: number; details?: unknown }> { - if (this.phase !== "idle") throw new Error("compact() requires idle harness"); + if (this.phase !== "idle") throw new AgentHarnessError("busy", "compact() requires idle harness"); this.phase = "compaction"; - const model = this.model; - if (!model) throw new Error("No model set for compaction"); - const auth = await this.getApiKeyAndHeaders?.(model); - if (!auth) throw new Error("No auth available for compaction"); - const branchEntries = await this.session.getBranch(); - const preparation = prepareCompaction(branchEntries, DEFAULT_COMPACTION_SETTINGS); - if (!preparation) throw new Error("Nothing to compact"); - const hookResult = await this.emitHook({ - type: "session_before_compact", - preparation, - branchEntries, - customInstructions, - signal: new AbortController().signal, - }); - if (hookResult?.cancel) { + try { + const model = this.model; + if (!model) throw new AgentHarnessError("invalid_state", "No model set for compaction"); + const auth = await this.getApiKeyAndHeaders?.(model); + if (!auth) throw new AgentHarnessError("auth", "No auth available for compaction"); + const branchEntries = await this.session.getBranch(); + const preparationResult = prepareCompaction(branchEntries, DEFAULT_COMPACTION_SETTINGS); + if (!preparationResult.ok) throw preparationResult.error; + const preparation = preparationResult.value; + if (!preparation) throw new AgentHarnessError("compaction", "Nothing to compact"); + const hookResult = await this.emitHook({ + type: "session_before_compact", + preparation, + branchEntries, + customInstructions, + signal: new AbortController().signal, + }); + if (hookResult?.cancel) throw new AgentHarnessError("compaction", "Compaction cancelled"); + const provided = hookResult?.compaction; + const compactResult = provided + ? { ok: true as const, value: provided } + : await compact( + preparation, + model, + auth.apiKey, + auth.headers, + customInstructions, + undefined, + this.thinkingLevel, + ); + if (!compactResult.ok) throw compactResult.error; + const result = compactResult.value; + const entryId = await this.session.appendCompaction( + result.summary, + result.firstKeptEntryId, + result.tokensBefore, + result.details, + provided !== undefined, + ); + const entry = await this.session.getEntry(entryId); + if (entry?.type === "compaction") { + await this.emitOwn({ type: "session_compact", compactionEntry: entry, fromHook: provided !== undefined }); + } + return result; + } catch (error) { + throw normalizeHarnessError(error, "compaction"); + } finally { this.phase = "idle"; - throw new Error("Compaction cancelled"); } - const provided = hookResult?.compaction; - const compactResult = provided - ? { ok: true as const, value: provided } - : await compact( - preparation, - model, - auth.apiKey, - auth.headers, - customInstructions, - undefined, - this.thinkingLevel, - ); - if (!compactResult.ok) throw compactResult.error; - const result = compactResult.value; - const entryId = await this.session.appendCompaction( - result.summary, - result.firstKeptEntryId, - result.tokensBefore, - result.details, - provided !== undefined, - ); - const entry = await this.session.getEntry(entryId); - if (entry?.type === "compaction") { - await this.emitOwn({ type: "session_compact", compactionEntry: entry, fromHook: provided !== undefined }); - } - this.phase = "idle"; - return result; } async navigateTree( targetId: string, options?: { summarize?: boolean; customInstructions?: string; replaceInstructions?: boolean; label?: string }, ): Promise { - if (this.phase !== "idle") throw new Error("navigateTree() requires idle harness"); + if (this.phase !== "idle") throw new AgentHarnessError("busy", "navigateTree() requires idle harness"); this.phase = "branch_summary"; - const oldLeafId = await this.session.getLeafId(); - if (oldLeafId === targetId) { - this.phase = "idle"; - return { cancelled: false }; - } - const targetEntry = await this.session.getEntry(targetId); - if (!targetEntry) throw new Error(`Entry ${targetId} not found`); - const { entries, commonAncestorId } = await collectEntriesForBranchSummary(this.session, oldLeafId, targetId); - const preparation = { - targetId, - oldLeafId, - commonAncestorId, - entriesToSummarize: entries, - userWantsSummary: options?.summarize ?? false, - customInstructions: options?.customInstructions, - replaceInstructions: options?.replaceInstructions, - label: options?.label, - }; - const signal = new AbortController().signal; - const hookResult = await this.emitHook({ - type: "session_before_tree", - preparation, - signal, - }); - if (hookResult?.cancel) { - this.phase = "idle"; - return { cancelled: true }; - } - let summaryEntry: any | undefined; - let summaryText: string | undefined = hookResult?.summary?.summary; - let summaryDetails: unknown = hookResult?.summary?.details; - if (!summaryText && options?.summarize && entries.length > 0) { - const model = this.model; - if (!model) throw new Error("No model set for branch summary"); - const auth = await this.getApiKeyAndHeaders?.(model); - if (!auth) throw new Error("No auth available for branch summary"); - const branchSummary = await generateBranchSummary(entries, { - model, - apiKey: auth.apiKey, - headers: auth.headers, - signal: new AbortController().signal, - customInstructions: hookResult?.customInstructions ?? options?.customInstructions, - replaceInstructions: hookResult?.replaceInstructions ?? options?.replaceInstructions, - }); - if (branchSummary.aborted) { - this.phase = "idle"; - return { cancelled: true }; - } - if (branchSummary.error) throw new Error(branchSummary.error); - summaryText = branchSummary.summary; - summaryDetails = { - readFiles: branchSummary.readFiles ?? [], - modifiedFiles: branchSummary.modifiedFiles ?? [], + try { + const oldLeafId = await this.session.getLeafId(); + if (oldLeafId === targetId) return { cancelled: false }; + const targetEntry = await this.session.getEntry(targetId); + if (!targetEntry) throw new AgentHarnessError("invalid_argument", `Entry ${targetId} not found`); + const { entries, commonAncestorId } = await collectEntriesForBranchSummary(this.session, oldLeafId, targetId); + const preparation = { + targetId, + oldLeafId, + commonAncestorId, + entriesToSummarize: entries, + userWantsSummary: options?.summarize ?? false, + customInstructions: options?.customInstructions, + replaceInstructions: options?.replaceInstructions, + label: options?.label, }; + const signal = new AbortController().signal; + const hookResult = await this.emitHook({ type: "session_before_tree", preparation, signal }); + if (hookResult?.cancel) return { cancelled: true }; + let summaryEntry: NavigateTreeResult["summaryEntry"]; + let summaryText: string | undefined = hookResult?.summary?.summary; + let summaryDetails: unknown = hookResult?.summary?.details; + if (!summaryText && options?.summarize && entries.length > 0) { + const model = this.model; + if (!model) throw new AgentHarnessError("invalid_state", "No model set for branch summary"); + const auth = await this.getApiKeyAndHeaders?.(model); + if (!auth) throw new AgentHarnessError("auth", "No auth available for branch summary"); + const branchSummary = await generateBranchSummary(entries, { + model, + apiKey: auth.apiKey, + headers: auth.headers, + signal: new AbortController().signal, + customInstructions: hookResult?.customInstructions ?? options?.customInstructions, + replaceInstructions: hookResult?.replaceInstructions ?? options?.replaceInstructions, + }); + if (!branchSummary.ok) { + if (branchSummary.error.code === "aborted") return { cancelled: true }; + throw new AgentHarnessError("branch_summary", branchSummary.error.message, branchSummary.error); + } + summaryText = branchSummary.value.summary; + summaryDetails = { + readFiles: branchSummary.value.readFiles, + modifiedFiles: branchSummary.value.modifiedFiles, + }; + } + let editorText: string | undefined; + let newLeafId: string | null; + if (targetEntry.type === "message" && targetEntry.message.role === "user") { + newLeafId = targetEntry.parentId; + const content = targetEntry.message.content; + editorText = + typeof content === "string" + ? content + : content + .filter((c): c is { readonly type: "text"; readonly text: string } => c.type === "text") + .map((c) => c.text) + .join(""); + } else if (targetEntry.type === "custom_message") { + newLeafId = targetEntry.parentId; + editorText = + typeof targetEntry.content === "string" + ? targetEntry.content + : targetEntry.content + .filter((c): c is { readonly type: "text"; readonly text: string } => c.type === "text") + .map((c) => c.text) + .join(""); + } else { + newLeafId = targetId; + } + const summaryId = await this.session.moveTo( + newLeafId, + summaryText + ? { summary: summaryText, details: summaryDetails, fromHook: hookResult?.summary !== undefined } + : undefined, + ); + if (summaryId) { + const entry = await this.session.getEntry(summaryId); + if (entry?.type === "branch_summary") summaryEntry = entry; + } + await this.emitOwn({ + type: "session_tree", + newLeafId: await this.session.getLeafId(), + oldLeafId, + summaryEntry, + fromHook: hookResult?.summary !== undefined, + }); + return { cancelled: false, editorText, summaryEntry }; + } catch (error) { + throw normalizeHarnessError(error, "branch_summary"); + } finally { + this.phase = "idle"; } - let editorText: string | undefined; - let newLeafId: string | null; - if (targetEntry.type === "message" && targetEntry.message.role === "user") { - newLeafId = targetEntry.parentId; - const content = targetEntry.message.content; - editorText = - typeof content === "string" - ? content - : content - .filter((c): c is { readonly type: "text"; readonly text: string } => c.type === "text") - .map((c) => c.text) - .join(""); - } else if (targetEntry.type === "custom_message") { - newLeafId = targetEntry.parentId; - editorText = - typeof targetEntry.content === "string" - ? targetEntry.content - : targetEntry.content - .filter((c): c is { readonly type: "text"; readonly text: string } => c.type === "text") - .map((c) => c.text) - .join(""); - } else { - newLeafId = targetId; - } - const summaryId = await this.session.moveTo( - newLeafId, - summaryText - ? { - summary: summaryText, - details: summaryDetails, - fromHook: hookResult?.summary !== undefined, - } - : undefined, - ); - if (summaryId) { - summaryEntry = await this.session.getEntry(summaryId); - } - await this.emitOwn({ - type: "session_tree", - newLeafId: await this.session.getLeafId(), - oldLeafId, - summaryEntry, - fromHook: hookResult?.summary !== undefined, - }); - this.phase = "idle"; - return { cancelled: false, editorText, summaryEntry }; } getModel(): Model { @@ -780,37 +843,49 @@ export class AgentHarness< } async setModel(model: Model): Promise { - const previousModel = this.model; - this.model = model; - if (this.phase === "idle") { - await this.session.appendModelChange(model.provider, model.id); - } else { - this.pendingSessionWrites.push({ type: "model_change", provider: model.provider, modelId: model.id }); + try { + const previousModel = this.model; + if (this.phase === "idle") { + await this.session.appendModelChange(model.provider, model.id); + } else { + this.pendingSessionWrites.push({ type: "model_change", provider: model.provider, modelId: model.id }); + } + this.model = model; + await this.emitOwn({ type: "model_select", model, previousModel, source: "set" }); + } catch (error) { + throw normalizeHarnessError(error, "session"); } - await this.emitOwn({ type: "model_select", model, previousModel, source: "set" }); } async setThinkingLevel(level: ThinkingLevel): Promise { - const previousLevel = this.thinkingLevel; - this.thinkingLevel = level; - if (this.phase === "idle") { - await this.session.appendThinkingLevelChange(level); - } else { - this.pendingSessionWrites.push({ type: "thinking_level_change", thinkingLevel: level }); + try { + const previousLevel = this.thinkingLevel; + if (this.phase === "idle") { + await this.session.appendThinkingLevelChange(level); + } else { + this.pendingSessionWrites.push({ type: "thinking_level_change", thinkingLevel: level }); + } + this.thinkingLevel = level; + await this.emitOwn({ type: "thinking_level_select", level, previousLevel }); + } catch (error) { + throw normalizeHarnessError(error, "session"); } - await this.emitOwn({ type: "thinking_level_select", level, previousLevel }); } async setActiveTools(toolNames: string[]): Promise { - this.validateToolNames(toolNames); - this.activeToolNames = [...toolNames]; + try { + this.validateToolNames(toolNames); + this.activeToolNames = [...toolNames]; + } catch (error) { + throw normalizeHarnessError(error, "invalid_argument"); + } } getSteeringMode(): QueueMode { return this.steeringQueueMode; } - setSteeringMode(mode: QueueMode): void { + async setSteeringMode(mode: QueueMode): Promise { this.steeringQueueMode = mode; } @@ -818,7 +893,7 @@ export class AgentHarness< return this.followUpQueueMode; } - setFollowUpMode(mode: QueueMode): void { + async setFollowUpMode(mode: QueueMode): Promise { this.followUpQueueMode = mode; } @@ -842,17 +917,19 @@ export class AgentHarness< return cloneStreamOptions(this.streamOptions); } - setStreamOptions(streamOptions: AgentHarnessStreamOptions): void { + async setStreamOptions(streamOptions: AgentHarnessStreamOptions): Promise { this.streamOptions = cloneStreamOptions(streamOptions); } async setTools(tools: TTool[], activeToolNames?: string[]): Promise { - this.tools = new Map(tools.map((tool) => [tool.name, tool])); - if (activeToolNames) { - this.validateToolNames(activeToolNames); - this.activeToolNames = [...activeToolNames]; - } else { - this.validateToolNames(this.activeToolNames); + try { + const nextTools = new Map(tools.map((tool) => [tool.name, tool])); + const nextActiveToolNames = activeToolNames ? [...activeToolNames] : this.activeToolNames; + this.validateToolNames(nextActiveToolNames, nextTools); + this.tools = nextTools; + this.activeToolNames = [...nextActiveToolNames]; + } catch (error) { + throw normalizeHarnessError(error, "invalid_argument"); } } @@ -861,10 +938,27 @@ export class AgentHarness< const clearedFollowUp = [...this.followUpQueue]; this.steerQueue = []; this.followUpQueue = []; - await this.emitQueueUpdate(); this.runAbortController?.abort(); - await this.waitForIdle(); - await this.emitOwn({ type: "abort", clearedSteer, clearedFollowUp }); + const errors: Error[] = []; + try { + await this.emitQueueUpdate(); + } catch (error) { + errors.push(toError(error)); + } + try { + await this.waitForIdle(); + } catch (error) { + errors.push(toError(error)); + } + try { + await this.emitOwn({ type: "abort", clearedSteer, clearedFollowUp }); + } catch (error) { + errors.push(toError(error)); + } + if (errors.length > 0) { + const cause = errors.length === 1 ? errors[0]! : new AggregateError(errors, "Abort completed with errors"); + throw normalizeHarnessError(cause, "hook"); + } return { clearedSteer, clearedFollowUp }; } diff --git a/packages/agent/src/harness/compaction/branch-summarization.ts b/packages/agent/src/harness/compaction/branch-summarization.ts index 38d3a18e..37af3b15 100644 --- a/packages/agent/src/harness/compaction/branch-summarization.ts +++ b/packages/agent/src/harness/compaction/branch-summarization.ts @@ -1,10 +1,3 @@ -/** - * Branch summarization for tree navigation. - * - * When navigating to a different point in the session tree, this generates - * a summary of the branch being left so context isn't lost. - */ - import type { Model } from "@earendil-works/pi-ai"; import { completeSimple } from "@earendil-works/pi-ai"; import type { AgentMessage } from "../../types.js"; @@ -14,102 +7,75 @@ import { createCompactionSummaryMessage, createCustomMessage, } from "../messages.js"; -import type { Session, SessionTreeEntry } from "../types.js"; -import { estimateTokens } from "./compaction.js"; +import type { BranchSummaryResult, Session, SessionTreeEntry } from "../types.js"; +import { BranchSummaryError, err, ok, type Result, SessionError } from "../types.js"; +import { estimateTokens, SUMMARIZATION_SYSTEM_PROMPT } from "./compaction.js"; import { computeFileLists, createFileOps, extractFileOpsFromMessage, type FileOperations, formatFileOperations, - SUMMARIZATION_SYSTEM_PROMPT, serializeConversation, } from "./utils.js"; -// ============================================================================ -// Types -// ============================================================================ - -export interface BranchSummaryResult { - summary?: string; - readFiles?: string[]; - modifiedFiles?: string[]; - aborted?: boolean; - error?: string; -} - -/** Details stored in BranchSummaryEntry.details for file tracking */ +/** File-operation details stored on generated branch summary entries. */ export interface BranchSummaryDetails { + /** Files read while exploring the summarized branch. */ readFiles: string[]; + /** Files modified while exploring the summarized branch. */ modifiedFiles: string[]; } export type { FileOperations } from "./utils.js"; +/** Prepared branch content for summarization. */ export interface BranchPreparation { - /** Messages extracted for summarization, in chronological order */ + /** Messages selected for the branch summary. */ messages: AgentMessage[]; - /** File operations extracted from tool calls */ + /** File operations extracted from the branch. */ fileOps: FileOperations; - /** Total estimated tokens in messages */ + /** Estimated token count for selected messages. */ totalTokens: number; } +/** Entries selected for branch summarization. */ export interface CollectEntriesResult { - /** Entries to summarize, in chronological order */ + /** Entries to summarize in chronological order. */ entries: SessionTreeEntry[]; - /** Common ancestor between old and new position, if any */ + /** Deepest common ancestor between the previous leaf and target entry. */ commonAncestorId: string | null; } +/** Options for generating a branch summary. */ export interface GenerateBranchSummaryOptions { - /** Model to use for summarization */ + /** Model used for summarization. */ model: Model; - /** API key for the model */ + /** API key forwarded to the provider. */ apiKey: string; - /** Request headers for the model */ + /** Optional request headers forwarded to the provider. */ headers?: Record; - /** Abort signal for cancellation */ + /** Abort signal for the summarization request. */ signal: AbortSignal; - /** Optional custom instructions for summarization */ + /** Optional instructions appended to or replacing the default prompt. */ customInstructions?: string; - /** If true, customInstructions replaces the default prompt instead of being appended */ + /** Replace the default prompt with custom instructions instead of appending them. */ replaceInstructions?: boolean; - /** Tokens reserved for prompt + LLM response (default 16384) */ + /** Tokens reserved for prompt and model output. Defaults to 16384. */ reserveTokens?: number; } -// ============================================================================ -// Entry Collection -// ============================================================================ - -/** - * Collect entries that should be summarized when navigating from one position to another. - * - * Walks from oldLeafId back to the common ancestor with targetId, collecting entries - * along the way. Does NOT stop at compaction boundaries - those are included and their - * summaries become context. - * - * @param session - Session manager (read-only access) - * @param oldLeafId - Current position (where we're navigating from) - * @param targetId - Target position (where we're navigating to) - * @returns Entries to summarize and the common ancestor - */ +/** Collect entries that should be summarized before navigating to a different session tree entry. */ export async function collectEntriesForBranchSummary( session: Session, oldLeafId: string | null, targetId: string, ): Promise { - // If no old position, nothing to summarize if (!oldLeafId) { return { entries: [], commonAncestorId: null }; } - - // Find common ancestor (deepest node that's on both paths) const oldPath = new Set((await session.getBranch(oldLeafId)).map((e) => e.id)); const targetPath = await session.getBranch(targetId); - - // targetPath is root-first, so iterate backwards to find deepest common ancestor let commonAncestorId: string | null = null; for (let i = targetPath.length - 1; i >= 0; i--) { if (oldPath.has(targetPath[i].id)) { @@ -117,36 +83,22 @@ export async function collectEntriesForBranchSummary( break; } } - - // Collect entries from old leaf back to common ancestor const entries: SessionTreeEntry[] = []; let current: string | null = oldLeafId; while (current && current !== commonAncestorId) { const entry = await session.getEntry(current); - if (!entry) break; + if (!entry) throw new SessionError("invalid_session", `Entry ${current} not found`); entries.push(entry as SessionTreeEntry); current = entry.parentId; } - - // Reverse to get chronological order entries.reverse(); return { entries, commonAncestorId }; } - -// ============================================================================ -// Entry to Message Conversion -// ============================================================================ - -/** - * Extract AgentMessage from a session entry. - * Similar to getMessageFromEntry in compaction.ts but also handles compaction entries. - */ function getMessageFromEntry(entry: SessionTreeEntry): AgentMessage | undefined { switch (entry.type) { case "message": - // Skip tool results - context is in assistant's tool call if (entry.message.role === "toolResult") return undefined; return entry.message; @@ -158,38 +110,21 @@ function getMessageFromEntry(entry: SessionTreeEntry): AgentMessage | undefined case "compaction": return createCompactionSummaryMessage(entry.summary, entry.tokensBefore, entry.timestamp); - - // These don't contribute to conversation content case "thinking_level_change": case "model_change": case "custom": case "label": case "session_info": + case "leaf": return undefined; } } -/** - * Prepare entries for summarization with token budget. - * - * Walks entries from NEWEST to OLDEST, adding messages until we hit the token budget. - * This ensures we keep the most recent context when the branch is too long. - * - * Also collects file operations from: - * - Tool calls in assistant messages - * - Existing branch_summary entries' details (for cumulative tracking) - * - * @param entries - Entries in chronological order - * @param tokenBudget - Maximum tokens to include (0 = no limit) - */ +/** Prepare branch entries for summarization within an optional token budget. */ export function prepareBranchEntries(entries: SessionTreeEntry[], tokenBudget: number = 0): BranchPreparation { const messages: AgentMessage[] = []; const fileOps = createFileOps(); let totalTokens = 0; - - // First pass: collect file ops from ALL entries (even if they don't fit in token budget) - // This ensures we capture cumulative file tracking from nested branch summaries - // Only extract from pi-generated summaries (fromHook !== true), not extension-generated ones for (const entry of entries) { if (entry.type === "branch_summary" && !entry.fromHook && entry.details) { const details = entry.details as BranchSummaryDetails; @@ -197,35 +132,26 @@ export function prepareBranchEntries(entries: SessionTreeEntry[], tokenBudget: n for (const f of details.readFiles) fileOps.read.add(f); } if (Array.isArray(details.modifiedFiles)) { - // Modified files go into both edited and written for proper deduplication for (const f of details.modifiedFiles) { fileOps.edited.add(f); } } } } - - // Second pass: walk from newest to oldest, adding messages until token budget for (let i = entries.length - 1; i >= 0; i--) { const entry = entries[i]; const message = getMessageFromEntry(entry); if (!message) continue; - - // Extract file ops from assistant messages (tool calls) extractFileOpsFromMessage(message, fileOps); const tokens = estimateTokens(message); - - // Check budget before adding if (tokenBudget > 0 && totalTokens + tokens > tokenBudget) { - // If this is a summary entry, try to fit it anyway as it's important context if (entry.type === "compaction" || entry.type === "branch_summary") { if (totalTokens < tokenBudget * 0.9) { messages.unshift(message); totalTokens += tokens; } } - // Stop - we've hit the budget break; } @@ -236,10 +162,6 @@ export function prepareBranchEntries(entries: SessionTreeEntry[], tokenBudget: n return { messages, fileOps, totalTokens }; } -// ============================================================================ -// Summary Generation -// ============================================================================ - const BRANCH_SUMMARY_PREAMBLE = `The user explored a different conversation branch before returning here. Summary of that exploration: @@ -274,34 +196,22 @@ Use this EXACT format: Keep each section concise. Preserve exact file paths, function names, and error messages.`; -/** - * Generate a summary of abandoned branch entries. - * - * @param entries - Session entries to summarize (chronological order) - * @param options - Generation options - */ +/** Generate a summary for abandoned branch entries. */ export async function generateBranchSummary( entries: SessionTreeEntry[], options: GenerateBranchSummaryOptions, -): Promise { +): Promise> { const { model, apiKey, headers, signal, customInstructions, replaceInstructions, reserveTokens = 16384 } = options; - - // Token budget = context window minus reserved space for prompt + response const contextWindow = model.contextWindow || 128000; const tokenBudget = contextWindow - reserveTokens; const { messages, fileOps } = prepareBranchEntries(entries, tokenBudget); if (messages.length === 0) { - return { summary: "No content to summarize" }; + return ok({ summary: "No content to summarize", readFiles: [], modifiedFiles: [] }); } - - // Transform to LLM-compatible messages, then serialize to text - // Serialization prevents the model from treating it as a conversation to continue const llmMessages = convertToLlm(messages); const conversationText = serializeConversation(llmMessages); - - // Build prompt let instructions: string; if (replaceInstructions && customInstructions) { instructions = customInstructions; @@ -319,37 +229,34 @@ export async function generateBranchSummary( timestamp: Date.now(), }, ]; - - // Call LLM for summarization const response = await completeSimple( model, { systemPrompt: SUMMARIZATION_SYSTEM_PROMPT, messages: summarizationMessages }, { apiKey, headers, signal, maxTokens: 2048 }, ); - - // Check if aborted or errored if (response.stopReason === "aborted") { - return { aborted: true }; + return err(new BranchSummaryError("aborted", response.errorMessage || "Branch summary aborted")); } if (response.stopReason === "error") { - return { error: response.errorMessage || "Summarization failed" }; + return err( + new BranchSummaryError( + "summarization_failed", + `Branch summary failed: ${response.errorMessage || "Unknown error"}`, + ), + ); } let summary = response.content .filter((c): c is { type: "text"; text: string } => c.type === "text") .map((c) => c.text) .join("\n"); - - // Prepend preamble to provide context about the branch summary summary = BRANCH_SUMMARY_PREAMBLE + summary; - - // Compute file lists and append to summary const { readFiles, modifiedFiles } = computeFileLists(fileOps); summary += formatFileOperations(readFiles, modifiedFiles); - return { + return ok({ summary: summary || "No summary generated", readFiles, modifiedFiles, - }; + }); } diff --git a/packages/agent/src/harness/compaction/compaction.ts b/packages/agent/src/harness/compaction/compaction.ts index 79a76ef1..a04ccd07 100644 --- a/packages/agent/src/harness/compaction/compaction.ts +++ b/packages/agent/src/harness/compaction/compaction.ts @@ -1,10 +1,3 @@ -/** - * Context compaction for long sessions. - * - * Pure functions for compaction logic. The session manager handles I/O, - * and after compaction the session is reloaded. - */ - import type { AssistantMessage, ImageContent, Model, TextContent, Usage } from "@earendil-works/pi-ai"; import { completeSimple } from "@earendil-works/pi-ai"; import type { AgentMessage, ThinkingLevel } from "../../types.js"; @@ -22,35 +15,33 @@ import { extractFileOpsFromMessage, type FileOperations, formatFileOperations, - SUMMARIZATION_SYSTEM_PROMPT, serializeConversation, } from "./utils.js"; -// ============================================================================ -// File Operation Tracking -// ============================================================================ - -/** Details stored in CompactionEntry.details for file tracking */ +/** File-operation details stored on generated compaction entries. */ export interface CompactionDetails { + /** Files read in the compacted history. */ readFiles: string[]; + /** Files modified in the compacted history. */ modifiedFiles: string[]; } +function safeJsonStringify(value: unknown): string { + try { + return JSON.stringify(value) ?? "undefined"; + } catch { + return "[unserializable]"; + } +} -/** - * Extract file operations from messages and previous compaction entries. - */ function extractFileOperations( messages: AgentMessage[], entries: SessionTreeEntry[], prevCompactionIndex: number, ): FileOperations { const fileOps = createFileOps(); - - // Collect from previous compaction's details (if pi-generated) if (prevCompactionIndex >= 0) { const prevCompaction = entries[prevCompactionIndex] as CompactionEntry; if (!prevCompaction.fromHook && prevCompaction.details) { - // fromHook field kept for session file compatibility const details = prevCompaction.details as CompactionDetails; if (Array.isArray(details.readFiles)) { for (const f of details.readFiles) fileOps.read.add(f); @@ -60,23 +51,12 @@ function extractFileOperations( } } } - - // Extract from tool calls in messages for (const msg of messages) { extractFileOpsFromMessage(msg, fileOps); } return fileOps; } - -// ============================================================================ -// Message Extraction -// ============================================================================ - -/** - * Extract AgentMessage from an entry if it produces one. - * Returns undefined for entries that don't contribute to LLM context. - */ function getMessageFromEntry(entry: SessionTreeEntry): AgentMessage | undefined { if (entry.type === "message") { return entry.message as AgentMessage; @@ -106,47 +86,39 @@ function getMessageFromEntryForCompaction(entry: SessionTreeEntry): AgentMessage return getMessageFromEntry(entry); } -/** Result from compact() - SessionManager adds uuid/parentUuid when saving */ +/** Generated compaction data ready to be persisted as a compaction entry. */ export interface CompactionResult { + /** Summary text that replaces compacted history in future context. */ summary: string; + /** Entry id where retained history starts. */ firstKeptEntryId: string; + /** Estimated context tokens before compaction. */ tokensBefore: number; - /** Extension-specific data (e.g., ArtifactIndex, version markers for structured compaction) */ + /** Optional implementation-specific details stored with the compaction entry. */ details?: T; } -// ============================================================================ -// Types -// ============================================================================ - +/** Compaction thresholds and retention settings. */ export interface CompactionSettings { + /** Enable automatic compaction decisions. */ enabled: boolean; + /** Tokens reserved for summary prompt and output. */ reserveTokens: number; + /** Approximate recent-context tokens to keep after compaction. */ keepRecentTokens: number; } +/** Default compaction settings used by the harness. */ export const DEFAULT_COMPACTION_SETTINGS: CompactionSettings = { enabled: true, reserveTokens: 16384, keepRecentTokens: 20000, }; -// ============================================================================ -// Token calculation -// ============================================================================ - -/** - * Calculate total context tokens from usage. - * Uses the native totalTokens field when available, falls back to computing from components. - */ +/** Calculate total context tokens from provider usage. */ export function calculateContextTokens(usage: Usage): number { return usage.totalTokens || usage.input + usage.output + usage.cacheRead + usage.cacheWrite; } - -/** - * Get usage from an assistant message if available. - * Skips aborted and error messages as they don't have valid usage data. - */ function getAssistantUsage(msg: AgentMessage): Usage | undefined { if (msg.role === "assistant" && "usage" in msg) { const assistantMsg = msg as AssistantMessage; @@ -157,9 +129,7 @@ function getAssistantUsage(msg: AgentMessage): Usage | undefined { return undefined; } -/** - * Find the last non-aborted assistant message usage from session entries. - */ +/** Return usage from the last successful assistant message in session entries. */ export function getLastAssistantUsage(entries: SessionTreeEntry[]): Usage | undefined { for (let i = entries.length - 1; i >= 0; i--) { const entry = entries[i]; @@ -171,10 +141,15 @@ export function getLastAssistantUsage(entries: SessionTreeEntry[]): Usage | unde return undefined; } +/** Estimated context-token usage for a message list. */ export interface ContextUsageEstimate { + /** Estimated total context tokens. */ tokens: number; + /** Tokens reported by the most recent assistant usage block. */ usageTokens: number; + /** Estimated tokens after the most recent assistant usage block. */ trailingTokens: number; + /** Index of the message that provided usage, or null when none exists. */ lastUsageIndex: number | null; } @@ -186,10 +161,7 @@ function getLastAssistantUsageInfo(messages: AgentMessage[]): { usage: Usage; in return undefined; } -/** - * Estimate context tokens from messages, using the last assistant usage when available. - * If there are messages after the last usage, estimate their tokens with estimateTokens. - */ +/** Estimate context tokens for messages using provider usage when available. */ export function estimateContextTokens(messages: AgentMessage[]): ContextUsageEstimate { const usageInfo = getLastAssistantUsageInfo(messages); @@ -220,22 +192,13 @@ export function estimateContextTokens(messages: AgentMessage[]): ContextUsageEst }; } -/** - * Check if compaction should trigger based on context usage. - */ +/** Return whether context usage exceeds the configured compaction threshold. */ export function shouldCompact(contextTokens: number, contextWindow: number, settings: CompactionSettings): boolean { if (!settings.enabled) return false; return contextTokens > contextWindow - settings.reserveTokens; } -// ============================================================================ -// Cut point detection -// ============================================================================ - -/** - * Estimate token count for a message using chars/4 heuristic. - * This is conservative (overestimates tokens). - */ +/** Estimate token count for one message using a conservative character heuristic. */ export function estimateTokens(message: AgentMessage): number { let chars = 0; @@ -261,7 +224,7 @@ export function estimateTokens(message: AgentMessage): number { } else if (block.type === "thinking") { chars += block.thinking.length; } else if (block.type === "toolCall") { - chars += block.name.length + JSON.stringify(block.arguments).length; + chars += block.name.length + safeJsonStringify(block.arguments).length; } } return Math.ceil(chars / 4); @@ -276,7 +239,7 @@ export function estimateTokens(message: AgentMessage): number { chars += block.text.length; } if (block.type === "image") { - chars += 4800; // Estimate images as 4000 chars, or 1200 tokens + chars += 4800; } } } @@ -295,14 +258,6 @@ export function estimateTokens(message: AgentMessage): number { return 0; } - -/** - * Find valid cut points: indices of user, assistant, custom, or bashExecution messages. - * Never cut at tool results (they must follow their tool call). - * When we cut at an assistant message with tool calls, its tool results follow it - * and will be kept. - * BashExecutionMessage is treated like a user message (user-initiated context). - */ function findValidCutPoints(entries: SessionTreeEntry[], startIndex: number, endIndex: number): number[] { const cutPoints: number[] = []; for (let i = startIndex; i < endIndex; i++) { @@ -332,10 +287,9 @@ function findValidCutPoints(entries: SessionTreeEntry[], startIndex: number, end case "custom_message": case "label": case "session_info": + case "leaf": break; } - - // branch_summary and custom_message are user-role messages, valid cut points if (entry.type === "branch_summary" || entry.type === "custom_message") { cutPoints.push(i); } @@ -343,15 +297,10 @@ function findValidCutPoints(entries: SessionTreeEntry[], startIndex: number, end return cutPoints; } -/** - * Find the user message (or bashExecution) that starts the turn containing the given entry index. - * Returns -1 if no turn start found before the index. - * BashExecutionMessage is treated like a user message for turn boundaries. - */ +/** Find the user-visible message that starts the turn containing an entry. */ export function findTurnStartIndex(entries: SessionTreeEntry[], entryIndex: number, startIndex: number): number { for (let i = entryIndex; i >= startIndex; i--) { const entry = entries[i]; - // branch_summary and custom_message are user-role messages, can start a turn if (entry.type === "branch_summary" || entry.type === "custom_message") { return i; } @@ -365,31 +314,17 @@ export function findTurnStartIndex(entries: SessionTreeEntry[], entryIndex: numb return -1; } +/** Cut point selected for compaction. */ export interface CutPointResult { - /** Index of first entry to keep */ + /** Index of the first entry retained after compaction. */ firstKeptEntryIndex: number; - /** Index of user message that starts the turn being split, or -1 if not splitting */ + /** Index of the turn-start entry when the cut splits a turn, otherwise -1. */ turnStartIndex: number; - /** Whether this cut splits a turn (cut point is not a user message) */ + /** Whether the selected cut point splits an in-progress turn. */ isSplitTurn: boolean; } -/** - * Find the cut point in session entries that keeps approximately `keepRecentTokens`. - * - * Algorithm: Walk backwards from newest, accumulating estimated message sizes. - * Stop when we've accumulated >= keepRecentTokens. Cut at that point. - * - * Can cut at user OR assistant messages (never tool results). When cutting at an - * assistant message with tool calls, its tool results come after and will be kept. - * - * Returns CutPointResult with: - * - firstKeptEntryIndex: the entry index to start keeping from - * - turnStartIndex: if cutting mid-turn, the user message that started that turn - * - isSplitTurn: whether we're cutting in the middle of a turn - * - * Only considers entries between `startIndex` and `endIndex` (exclusive). - */ +/** Find the compaction cut point that keeps approximately the requested recent-token budget. */ export function findCutPoint( entries: SessionTreeEntry[], startIndex: number, @@ -401,22 +336,15 @@ export function findCutPoint( if (cutPoints.length === 0) { return { firstKeptEntryIndex: startIndex, turnStartIndex: -1, isSplitTurn: false }; } - - // Walk backwards from newest, accumulating estimated message sizes let accumulatedTokens = 0; - let cutIndex = cutPoints[0]; // Default: keep from first message (not header) + let cutIndex = cutPoints[0]; for (let i = endIndex - 1; i >= startIndex; i--) { const entry = entries[i]; if (entry.type !== "message") continue; - - // Estimate this message's size const messageTokens = estimateTokens(entry.message as AgentMessage); accumulatedTokens += messageTokens; - - // Check if we've exceeded the budget if (accumulatedTokens >= keepRecentTokens) { - // Find the closest valid cut point at or after this entry for (let c = 0; c < cutPoints.length; c++) { if (cutPoints[c] >= i) { cutIndex = cutPoints[c]; @@ -426,23 +354,16 @@ export function findCutPoint( break; } } - - // Scan backwards from cutIndex to include any non-message entries (bash, settings, etc.) while (cutIndex > startIndex) { const prevEntry = entries[cutIndex - 1]; - // Stop at session header or compaction boundaries if (prevEntry.type === "compaction") { break; } if (prevEntry.type === "message") { - // Stop if we hit any message break; } - // Include this non-message entry (bash, settings change, etc.) cutIndex--; } - - // Determine if this is a split turn const cutEntry = entries[cutIndex]; const isUserMessage = cutEntry.type === "message" && cutEntry.message.role === "user"; const turnStartIndex = isUserMessage ? -1 : findTurnStartIndex(entries, cutIndex, startIndex); @@ -454,9 +375,9 @@ export function findCutPoint( }; } -// ============================================================================ -// Summarization -// ============================================================================ +export const SUMMARIZATION_SYSTEM_PROMPT = `You are a context summarization assistant. Your task is to read a conversation between a user and an AI coding assistant, then produce a structured summary following the exact format specified. + +Do NOT continue the conversation. Do NOT respond to any questions in the conversation. ONLY output the structured summary.`; const SUMMARIZATION_PROMPT = `The messages above are a conversation to summarize. Create a structured context checkpoint summary that another LLM will use to continue the work. @@ -530,10 +451,7 @@ Use this EXACT format: Keep each section concise. Preserve exact file paths, function names, and error messages.`; -/** - * Generate a summary of the conversation using the LLM. - * If previousSummary is provided, uses the update prompt to merge. - */ +/** Generate or update a conversation summary for compaction. */ export async function generateSummary( currentMessages: AgentMessage[], model: Model, @@ -549,19 +467,12 @@ export async function generateSummary( Math.floor(0.8 * reserveTokens), model.maxTokens > 0 ? model.maxTokens : Number.POSITIVE_INFINITY, ); - - // Use update prompt if we have a previous summary, otherwise initial prompt let basePrompt = previousSummary ? UPDATE_SUMMARIZATION_PROMPT : SUMMARIZATION_PROMPT; if (customInstructions) { basePrompt = `${basePrompt}\n\nAdditional focus: ${customInstructions}`; } - - // Serialize conversation to text so model doesn't try to continue it - // Convert to LLM messages first (handles custom types like bashExecution, custom, etc.) const llmMessages = convertToLlm(currentMessages); const conversationText = serializeConversation(llmMessages); - - // Build the prompt with conversation wrapped in tags let promptText = `\n${conversationText}\n\n\n`; if (previousSummary) { promptText += `\n${previousSummary}\n\n\n`; @@ -581,18 +492,11 @@ export async function generateSummary( ? { maxTokens, signal, apiKey, headers, reasoning: thinkingLevel } : { maxTokens, signal, apiKey, headers }; - const responseResult: Result = await completeSimple( + const response = await completeSimple( model, { systemPrompt: SUMMARIZATION_SYSTEM_PROMPT, messages: summarizationMessages }, completionOptions, - ).then( - (response) => ok(response), - (error: unknown) => - err(new CompactionError("unknown", "Summarization request failed", error)), ); - if (!responseResult.ok) return err(responseResult.error); - - const response = responseResult.value; if (response.stopReason === "aborted") { return err(new CompactionError("aborted", response.errorMessage || "Summarization aborted")); } @@ -613,34 +517,33 @@ export async function generateSummary( return ok(textContent); } -// ============================================================================ -// Compaction Preparation (for extensions) -// ============================================================================ - +/** Prepared inputs for a compaction run. */ export interface CompactionPreparation { - /** UUID of first entry to keep */ + /** Entry id where retained history starts. */ firstKeptEntryId: string; - /** Messages that will be summarized and discarded */ + /** Messages summarized into the history summary. */ messagesToSummarize: AgentMessage[]; - /** Messages that will be turned into turn prefix summary (if splitting) */ + /** Prefix messages summarized separately when compaction splits a turn. */ turnPrefixMessages: AgentMessage[]; - /** Whether this is a split turn (cut point in middle of turn) */ + /** Whether compaction splits a turn. */ isSplitTurn: boolean; + /** Estimated context tokens before compaction. */ tokensBefore: number; - /** Summary from previous compaction, for iterative update */ + /** Previous compaction summary used for iterative updates. */ previousSummary?: string; - /** File operations extracted from messagesToSummarize */ + /** File operations extracted from summarized history. */ fileOps: FileOperations; - /** Compaction settions from settings.jsonl */ + /** Settings used to prepare compaction. */ settings: CompactionSettings; } +/** Prepare session entries for compaction, or return undefined when compaction is not applicable. */ export function prepareCompaction( pathEntries: SessionTreeEntry[], settings: CompactionSettings, -): CompactionPreparation | undefined { - if (pathEntries.length > 0 && pathEntries[pathEntries.length - 1].type === "compaction") { - return undefined; +): Result { + if (pathEntries.length === 0 || pathEntries[pathEntries.length - 1].type === "compaction") { + return ok(undefined); } let prevCompactionIndex = -1; @@ -664,24 +567,18 @@ export function prepareCompaction( const tokensBefore = estimateContextTokens(buildSessionContext(pathEntries).messages).tokens; const cutPoint = findCutPoint(pathEntries, boundaryStart, boundaryEnd, settings.keepRecentTokens); - - // Get UUID of first kept entry const firstKeptEntry = pathEntries[cutPoint.firstKeptEntryIndex]; if (!firstKeptEntry?.id) { - return undefined; // Session needs migration + return err(new CompactionError("invalid_session", "First kept entry has no UUID - session may need migration")); } const firstKeptEntryId = firstKeptEntry.id; const historyEnd = cutPoint.isSplitTurn ? cutPoint.turnStartIndex : cutPoint.firstKeptEntryIndex; - - // Messages to summarize (will be discarded after summary) const messagesToSummarize: AgentMessage[] = []; for (let i = boundaryStart; i < historyEnd; i++) { const msg = getMessageFromEntryForCompaction(pathEntries[i]); if (msg) messagesToSummarize.push(msg); } - - // Messages for turn prefix summary (if splitting a turn) const turnPrefixMessages: AgentMessage[] = []; if (cutPoint.isSplitTurn) { for (let i = cutPoint.turnStartIndex; i < cutPoint.firstKeptEntryIndex; i++) { @@ -689,18 +586,14 @@ export function prepareCompaction( if (msg) turnPrefixMessages.push(msg); } } - - // Extract file operations from messages and previous compaction const fileOps = extractFileOperations(messagesToSummarize, pathEntries, prevCompactionIndex); - - // Also extract file ops from turn prefix if splitting if (cutPoint.isSplitTurn) { for (const msg of turnPrefixMessages) { extractFileOpsFromMessage(msg, fileOps); } } - return { + return ok({ firstKeptEntryId, messagesToSummarize, turnPrefixMessages, @@ -709,13 +602,9 @@ export function prepareCompaction( previousSummary, fileOps, settings, - }; + }); } -// ============================================================================ -// Main compaction function -// ============================================================================ - const TURN_PREFIX_SUMMARIZATION_PROMPT = `This is the PREFIX of a turn that was too large to keep. The SUFFIX (recent work) is retained. Summarize the prefix to provide context for the retained suffix: @@ -731,15 +620,9 @@ Summarize the prefix to provide context for the retained suffix: Be concise. Focus on what's needed to understand the kept suffix.`; -/** - * Generate summaries for compaction using prepared data. - * Returns CompactionResult - SessionManager adds uuid/parentUuid when saving. - * - * @param preparation - Pre-calculated preparation from prepareCompaction() - * @param customInstructions - Optional custom focus for the summary - */ export { serializeConversation } from "./utils.js"; +/** Generate compaction summary data from prepared session history. */ export async function compact( preparation: CompactionPreparation, model: Model, @@ -820,10 +703,6 @@ export async function compact( details: { readFiles, modifiedFiles } as CompactionDetails, }); } - -/** - * Generate a summary for a turn prefix (when splitting a turn). - */ async function generateTurnPrefixSummary( messages: AgentMessage[], model: Model, @@ -836,7 +715,7 @@ async function generateTurnPrefixSummary( const maxTokens = Math.min( Math.floor(0.5 * reserveTokens), model.maxTokens > 0 ? model.maxTokens : Number.POSITIVE_INFINITY, - ); // Smaller budget for turn prefix + ); const llmMessages = convertToLlm(messages); const conversationText = serializeConversation(llmMessages); const promptText = `\n${conversationText}\n\n\n${TURN_PREFIX_SUMMARIZATION_PROMPT}`; @@ -848,22 +727,13 @@ async function generateTurnPrefixSummary( }, ]; - const responseResult: Result = await completeSimple( + const response = await completeSimple( model, { systemPrompt: SUMMARIZATION_SYSTEM_PROMPT, messages: summarizationMessages }, model.reasoning && thinkingLevel && thinkingLevel !== "off" ? { maxTokens, signal, apiKey, headers, reasoning: thinkingLevel } : { maxTokens, signal, apiKey, headers }, - ).then( - (response) => ok(response), - (error: unknown) => - err( - new CompactionError("unknown", "Turn prefix summarization request failed", error), - ), ); - if (!responseResult.ok) return err(responseResult.error); - - const response = responseResult.value; if (response.stopReason === "aborted") { return err(new CompactionError("aborted", response.errorMessage || "Turn prefix summarization aborted")); } diff --git a/packages/agent/src/harness/compaction/utils.ts b/packages/agent/src/harness/compaction/utils.ts index 35cd1938..2b852a46 100644 --- a/packages/agent/src/harness/compaction/utils.ts +++ b/packages/agent/src/harness/compaction/utils.ts @@ -1,20 +1,17 @@ -/** - * Shared utilities for compaction and branch summarization. - */ - import type { Message } from "@earendil-works/pi-ai"; import type { AgentMessage } from "../../types.js"; -// ============================================================================ -// File Operation Tracking -// ============================================================================ - +/** File paths touched by a session branch or compaction range. */ export interface FileOperations { + /** Files read but not necessarily modified. */ read: Set; + /** Files written by full-file write operations. */ written: Set; + /** Files modified by edit operations. */ edited: Set; } +/** Create an empty file-operation accumulator. */ export function createFileOps(): FileOperations { return { read: new Set(), @@ -23,9 +20,7 @@ export function createFileOps(): FileOperations { }; } -/** - * Extract file operations from tool calls in an assistant message. - */ +/** Add file operations from assistant tool calls to an accumulator. */ export function extractFileOpsFromMessage(message: AgentMessage, fileOps: FileOperations): void { if (message.role !== "assistant") return; if (!("content" in message) || !Array.isArray(message.content)) return; @@ -55,10 +50,7 @@ export function extractFileOpsFromMessage(message: AgentMessage, fileOps: FileOp } } -/** - * Compute final file lists from file operations. - * Returns readFiles (files only read, not modified) and modifiedFiles. - */ +/** Compute sorted read-only and modified file lists from accumulated operations. */ export function computeFileLists(fileOps: FileOperations): { readFiles: string[]; modifiedFiles: string[] } { const modified = new Set([...fileOps.edited, ...fileOps.written]); const readOnly = [...fileOps.read].filter((f) => !modified.has(f)).sort(); @@ -66,9 +58,7 @@ export function computeFileLists(fileOps: FileOperations): { readFiles: string[] return { readFiles: readOnly, modifiedFiles }; } -/** - * Format file operations as XML tags for summary. - */ +/** Format file lists as summary metadata tags. */ export function formatFileOperations(readFiles: string[], modifiedFiles: string[]): string { const sections: string[] = []; if (readFiles.length > 0) { @@ -81,31 +71,23 @@ export function formatFileOperations(readFiles: string[], modifiedFiles: string[ return `\n\n${sections.join("\n\n")}`; } -// ============================================================================ -// Message Serialization -// ============================================================================ - -/** Maximum characters for a tool result in serialized summaries. */ const TOOL_RESULT_MAX_CHARS = 2000; -/** - * Truncate text to a maximum character length for summarization. - * Keeps the beginning and appends a truncation marker. - */ +function safeJsonStringify(value: unknown): string { + try { + return JSON.stringify(value) ?? "undefined"; + } catch { + return "[unserializable]"; + } +} + function truncateForSummary(text: string, maxChars: number): string { if (text.length <= maxChars) return text; const truncatedChars = text.length - maxChars; return `${text.slice(0, maxChars)}\n\n[... ${truncatedChars} more characters truncated]`; } -/** - * Serialize LLM messages to text for summarization. - * This prevents the model from treating it as a conversation to continue. - * Call convertToLlm() first to handle custom message types. - * - * Tool results are truncated to keep the summarization request within - * reasonable token budgets. Full content is not needed for summarization. - */ +/** Serialize LLM messages to plain text for summarization prompts. */ export function serializeConversation(messages: Message[]): string { const parts: string[] = []; @@ -132,7 +114,7 @@ export function serializeConversation(messages: Message[]): string { } else if (block.type === "toolCall") { const args = block.arguments as Record; const argsStr = Object.entries(args) - .map(([k, v]) => `${k}=${JSON.stringify(v)}`) + .map(([k, v]) => `${k}=${safeJsonStringify(v)}`) .join(", "); toolCalls.push(`${block.name}(${argsStr})`); } @@ -160,11 +142,3 @@ export function serializeConversation(messages: Message[]): string { return parts.join("\n\n"); } - -// ============================================================================ -// Summarization System Prompt -// ============================================================================ - -export const SUMMARIZATION_SYSTEM_PROMPT = `You are a context summarization assistant. Your task is to read a conversation between a user and an AI coding assistant, then produce a structured summary following the exact format specified. - -Do NOT continue the conversation. Do NOT respond to any questions in the conversation. ONLY output the structured summary.`; diff --git a/packages/agent/src/harness/env/nodejs.ts b/packages/agent/src/harness/env/nodejs.ts index 0b23a2d1..b86f6fe8 100644 --- a/packages/agent/src/harness/env/nodejs.ts +++ b/packages/agent/src/harness/env/nodejs.ts @@ -1,6 +1,6 @@ import { spawn } from "node:child_process"; import { randomUUID } from "node:crypto"; -import { constants } from "node:fs"; +import { constants, createReadStream } from "node:fs"; import { access, appendFile, @@ -15,6 +15,7 @@ import { } from "node:fs/promises"; import { tmpdir } from "node:os"; import { isAbsolute, join, resolve } from "node:path"; +import { createInterface } from "node:readline"; import { type ExecutionEnv, ExecutionError, @@ -24,6 +25,7 @@ import { type FileKind, ok, type Result, + toError, } from "../types.js"; function resolvePath(cwd: string, path: string): string { @@ -62,25 +64,26 @@ function isNodeError(error: unknown): error is NodeJS.ErrnoException { function toFileError(error: unknown, path?: string): FileError { if (error instanceof FileError) return error; + const cause = toError(error); if (isNodeError(error)) { const message = error.message; switch (error.code) { case "ABORT_ERR": - return new FileError("aborted", message, path, error); + return new FileError("aborted", message, path, cause); case "ENOENT": - return new FileError("not_found", message, path, error); + return new FileError("not_found", message, path, cause); case "EACCES": case "EPERM": - return new FileError("permission_denied", message, path, error); + return new FileError("permission_denied", message, path, cause); case "ENOTDIR": - return new FileError("not_directory", message, path, error); + return new FileError("not_directory", message, path, cause); case "EISDIR": - return new FileError("is_directory", message, path, error); + return new FileError("is_directory", message, path, cause); case "EINVAL": - return new FileError("invalid", message, path, error); + return new FileError("invalid", message, path, cause); } } - return new FileError("unknown", error instanceof Error ? error.message : String(error), path, error); + return new FileError("unknown", cause.message, path, cause); } function abortResult(signal: AbortSignal | undefined, path?: string): Result | undefined { @@ -218,18 +221,12 @@ export class NodeExecutionEnv implements ExecutionEnv { this.shellEnv = options.shellEnv; } - async absolutePath(path: string, abortSignal?: AbortSignal): Promise> { - const resolved = resolvePath(this.cwd, path); - const aborted = abortResult(abortSignal, resolved); - if (aborted) return aborted; - return ok(resolved); + async absolutePath(path: string): Promise> { + return ok(resolvePath(this.cwd, path)); } - async joinPath(parts: string[], abortSignal?: AbortSignal): Promise> { - const joined = join(...parts); - const aborted = abortResult(abortSignal, joined); - if (aborted) return aborted; - return ok(joined); + async joinPath(parts: string[]): Promise> { + return ok(join(...parts)); } async exec( @@ -280,9 +277,8 @@ export class NodeExecutionEnv implements ExecutionEnv { stdio: ["ignore", "pipe", "pipe"], }); } catch (error) { - settle( - err(new ExecutionError("spawn_error", error instanceof Error ? error.message : String(error), error)), - ); + const cause = toError(error); + settle(err(new ExecutionError("spawn_error", cause.message, cause))); return; } @@ -311,11 +307,8 @@ export class NodeExecutionEnv implements ExecutionEnv { try { options?.onStdout?.(chunk); } catch (error) { - callbackError = new ExecutionError( - "unknown", - error instanceof Error ? error.message : String(error), - error, - ); + const cause = toError(error); + callbackError = new ExecutionError("callback_error", cause.message, cause); onAbort(); } }); @@ -324,11 +317,8 @@ export class NodeExecutionEnv implements ExecutionEnv { try { options?.onStderr?.(chunk); } catch (error) { - callbackError = new ExecutionError( - "unknown", - error instanceof Error ? error.message : String(error), - error, - ); + const cause = toError(error); + callbackError = new ExecutionError("callback_error", cause.message, cause); onAbort(); } }); @@ -342,14 +332,14 @@ export class NodeExecutionEnv implements ExecutionEnv { settle(err(callbackError)); return; } - if (options?.abortSignal?.aborted) { - settle(err(new ExecutionError("aborted", "aborted"))); - return; - } if (timedOut) { settle(err(new ExecutionError("timeout", `timeout:${options?.timeout}`))); return; } + if (options?.abortSignal?.aborted) { + settle(err(new ExecutionError("aborted", "aborted"))); + return; + } settle(ok({ stdout, stderr, exitCode: code ?? 0 })); }); }); @@ -366,6 +356,37 @@ export class NodeExecutionEnv implements ExecutionEnv { } } + async readTextLines( + path: string, + options?: { maxLines?: number; abortSignal?: AbortSignal }, + ): Promise> { + const resolved = resolvePath(this.cwd, path); + const aborted = abortResult(options?.abortSignal, resolved); + if (aborted) return aborted; + if (options?.maxLines !== undefined && options.maxLines <= 0) return ok([]); + let stream: ReturnType | undefined; + let lineReader: ReturnType | undefined; + try { + stream = createReadStream(resolved, { encoding: "utf8", signal: options?.abortSignal }); + lineReader = createInterface({ input: stream, crlfDelay: Infinity }); + const lines: string[] = []; + for await (const line of lineReader) { + const loopAbort = abortResult(options?.abortSignal, resolved); + if (loopAbort) return loopAbort; + lines.push(line); + if (options?.maxLines !== undefined && lines.length >= options.maxLines) break; + } + const afterReadAbort = abortResult(options?.abortSignal, resolved); + if (afterReadAbort) return afterReadAbort; + return ok(lines); + } catch (error) { + return err(toFileError(error, resolved)); + } finally { + lineReader?.close(); + stream?.destroy(); + } + } + async readBinaryFile(path: string, abortSignal?: AbortSignal): Promise> { const resolved = resolvePath(this.cwd, path); const aborted = abortResult(abortSignal, resolved); @@ -396,31 +417,19 @@ export class NodeExecutionEnv implements ExecutionEnv { } } - async appendFile( - path: string, - content: string | Uint8Array, - abortSignal?: AbortSignal, - ): Promise> { + async appendFile(path: string, content: string | Uint8Array): Promise> { const resolved = resolvePath(this.cwd, path); - const aborted = abortResult(abortSignal, resolved); - if (aborted) return aborted; try { await mkdir(resolve(resolved, ".."), { recursive: true }); - const afterMkdirAbort = abortResult(abortSignal, resolved); - if (afterMkdirAbort) return afterMkdirAbort; await appendFile(resolved, content); - const afterAppendAbort = abortResult(abortSignal, resolved); - if (afterAppendAbort) return afterAppendAbort; return ok(undefined); } catch (error) { return err(toFileError(error, resolved)); } } - async fileInfo(path: string, abortSignal?: AbortSignal): Promise> { + async fileInfo(path: string): Promise> { const resolved = resolvePath(this.cwd, path); - const aborted = abortResult(abortSignal, resolved); - if (aborted) return aborted; try { return fileInfoFromStats(resolved, await lstat(resolved)); } catch (error) { @@ -452,10 +461,8 @@ export class NodeExecutionEnv implements ExecutionEnv { } } - async canonicalPath(path: string, abortSignal?: AbortSignal): Promise> { + async canonicalPath(path: string): Promise> { const resolved = resolvePath(this.cwd, path); - const aborted = abortResult(abortSignal, resolved); - if (aborted) return aborted; try { return ok(await realpath(resolved)); } catch (error) { @@ -463,72 +470,47 @@ export class NodeExecutionEnv implements ExecutionEnv { } } - async exists(path: string, abortSignal?: AbortSignal): Promise> { - const result = await this.fileInfo(path, abortSignal); + async exists(path: string): Promise> { + const result = await this.fileInfo(path); if (result.ok) return ok(true); if (result.error.code === "not_found") return ok(false); return err(result.error); } - async createDir( - path: string, - options?: { recursive?: boolean; abortSignal?: AbortSignal }, - ): Promise> { + async createDir(path: string, options?: { recursive?: boolean }): Promise> { const resolved = resolvePath(this.cwd, path); - const aborted = abortResult(options?.abortSignal, resolved); - if (aborted) return aborted; try { await mkdir(resolved, { recursive: options?.recursive ?? true }); - const afterMkdirAbort = abortResult(options?.abortSignal, resolved); - if (afterMkdirAbort) return afterMkdirAbort; return ok(undefined); } catch (error) { return err(toFileError(error, resolved)); } } - async remove( - path: string, - options?: { recursive?: boolean; force?: boolean; abortSignal?: AbortSignal }, - ): Promise> { + async remove(path: string, options?: { recursive?: boolean; force?: boolean }): Promise> { const resolved = resolvePath(this.cwd, path); - const aborted = abortResult(options?.abortSignal, resolved); - if (aborted) return aborted; try { await rm(resolved, { recursive: options?.recursive ?? false, force: options?.force ?? false }); - const afterRemoveAbort = abortResult(options?.abortSignal, resolved); - if (afterRemoveAbort) return afterRemoveAbort; return ok(undefined); } catch (error) { return err(toFileError(error, resolved)); } } - async createTempDir(prefix: string = "tmp-", abortSignal?: AbortSignal): Promise> { - const aborted = abortResult(abortSignal); - if (aborted) return aborted; + async createTempDir(prefix: string = "tmp-"): Promise> { try { - const path = await mkdtemp(join(tmpdir(), prefix)); - const afterMkdtempAbort = abortResult(abortSignal, path); - if (afterMkdtempAbort) return afterMkdtempAbort; - return ok(path); + return ok(await mkdtemp(join(tmpdir(), prefix))); } catch (error) { return err(toFileError(error)); } } - async createTempFile(options?: { - prefix?: string; - suffix?: string; - abortSignal?: AbortSignal; - }): Promise> { - const dir = await this.createTempDir("tmp-", options?.abortSignal); + async createTempFile(options?: { prefix?: string; suffix?: string }): Promise> { + const dir = await this.createTempDir("tmp-"); if (!dir.ok) return dir; const filePath = join(dir.value, `${options?.prefix ?? ""}${randomUUID()}${options?.suffix ?? ""}`); - const aborted = abortResult(options?.abortSignal, filePath); - if (aborted) return aborted; try { - await writeFile(filePath, "", { signal: options?.abortSignal }); + await writeFile(filePath, ""); return ok(filePath); } catch (error) { return err(toFileError(error, filePath)); diff --git a/packages/agent/src/harness/prompt-templates.ts b/packages/agent/src/harness/prompt-templates.ts index abcaad5a..3e678cfc 100644 --- a/packages/agent/src/harness/prompt-templates.ts +++ b/packages/agent/src/harness/prompt-templates.ts @@ -1,10 +1,14 @@ import { parse } from "yaml"; -import { type ExecutionEnv, type FileInfo, getOrUndefined, type PromptTemplate, type Result } from "./types.js"; +import { type ExecutionEnv, type FileInfo, type PromptTemplate, type Result, toError } from "./types.js"; + +export type PromptTemplateDiagnosticCode = "file_info_failed" | "list_failed" | "read_failed" | "parse_failed"; /** Warning produced while loading prompt templates. */ export interface PromptTemplateDiagnostic { /** Diagnostic severity. Currently only warnings are emitted. */ type: "warning"; + /** Stable diagnostic code. */ + code: PromptTemplateDiagnosticCode; /** Human-readable diagnostic message. */ message: string; /** Path associated with the diagnostic. */ @@ -30,9 +34,20 @@ export async function loadPromptTemplates( const promptTemplates: PromptTemplate[] = []; const diagnostics: PromptTemplateDiagnostic[] = []; for (const path of Array.isArray(paths) ? paths : [paths]) { - const info = getOrUndefined(await env.fileInfo(path)); - if (!info) continue; - const kind = await resolveKind(env, info); + const infoResult = await env.fileInfo(path); + if (!infoResult.ok) { + if (infoResult.error.code !== "not_found") { + diagnostics.push({ + type: "warning", + code: "file_info_failed", + message: infoResult.error.message, + path, + }); + } + continue; + } + const info = infoResult.value; + const kind = await resolveKind(env, info, diagnostics); if (kind === "directory") { const result = await loadTemplatesFromDir(env, info.path); promptTemplates.push(...result.promptTemplates); @@ -87,6 +102,7 @@ async function loadTemplatesFromDir( if (!entriesResult.ok) { diagnostics.push({ type: "warning", + code: "list_failed", message: entriesResult.error.message, path: dir, }); @@ -95,7 +111,7 @@ async function loadTemplatesFromDir( const entries = entriesResult.value; for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) { - const kind = await resolveKind(env, entry); + const kind = await resolveKind(env, entry, diagnostics); if (kind !== "file" || !entry.name.endsWith(".md")) continue; const result = await loadTemplateFromFile(env, entry.path); if (result.promptTemplate) promptTemplates.push(result.promptTemplate); @@ -113,6 +129,7 @@ async function loadTemplateFromFile( if (!rawContent.ok) { diagnostics.push({ type: "warning", + code: "read_failed", message: rawContent.error.message, path: filePath, }); @@ -123,6 +140,7 @@ async function loadTemplateFromFile( if (!parsed.ok) { diagnostics.push({ type: "warning", + code: "parse_failed", message: parsed.error.message, path: filePath, }); @@ -146,13 +164,37 @@ async function loadTemplateFromFile( }; } -async function resolveKind(env: ExecutionEnv, info: FileInfo): Promise<"file" | "directory" | undefined> { +async function resolveKind( + env: ExecutionEnv, + info: FileInfo, + diagnostics: PromptTemplateDiagnostic[], +): Promise<"file" | "directory" | undefined> { if (info.kind === "file" || info.kind === "directory") return info.kind; const canonicalPath = await env.canonicalPath(info.path); - if (!canonicalPath.ok) return undefined; - const target = getOrUndefined(await env.fileInfo(canonicalPath.value)); - if (!target) return undefined; - return target.kind === "file" || target.kind === "directory" ? target.kind : undefined; + if (!canonicalPath.ok) { + if (canonicalPath.error.code !== "not_found") { + diagnostics.push({ + type: "warning", + code: "file_info_failed", + message: canonicalPath.error.message, + path: info.path, + }); + } + return undefined; + } + const target = await env.fileInfo(canonicalPath.value); + if (!target.ok) { + if (target.error.code !== "not_found") { + diagnostics.push({ + type: "warning", + code: "file_info_failed", + message: target.error.message, + path: info.path, + }); + } + return undefined; + } + return target.value.kind === "file" || target.value.kind === "directory" ? target.value.kind : undefined; } function parseFrontmatter>( @@ -167,7 +209,7 @@ function parseFrontmatter>( const body = normalized.slice(endIndex + 4).trim(); return { ok: true, value: { frontmatter: (parse(yamlString) ?? {}) as T, body } }; } catch (error) { - return { ok: false, error: error instanceof Error ? error : new Error(String(error)) }; + return { ok: false, error: toError(error) }; } } diff --git a/packages/agent/src/harness/session/jsonl-repo.ts b/packages/agent/src/harness/session/jsonl-repo.ts index e41f0fb3..cd6ac1d8 100644 --- a/packages/agent/src/harness/session/jsonl-repo.ts +++ b/packages/agent/src/harness/session/jsonl-repo.ts @@ -6,9 +6,15 @@ import type { JsonlSessionRepoApi, Session, } from "../types.js"; -import { getOrThrow } from "../types.js"; +import { SessionError, toError } from "../types.js"; import { JsonlSessionStorage, loadJsonlSessionMetadata } from "./jsonl-storage.js"; -import { createSessionId, createTimestamp, getEntriesToFork, toSession } from "./repo-utils.js"; +import { + createSessionId, + createTimestamp, + getEntriesToFork, + getFileSystemResultOrThrow, + toSession, +} from "./repo-utils.js"; type JsonlSessionRepoFileSystem = Pick< FileSystem, @@ -16,6 +22,7 @@ type JsonlSessionRepoFileSystem = Pick< | "absolutePath" | "joinPath" | "readTextFile" + | "readTextLines" | "writeFile" | "appendFile" | "listDir" @@ -40,21 +47,28 @@ export class JsonlSessionRepo implements JsonlSessionRepoApi { private async getSessionsRoot(): Promise { if (!this.sessionsRoot) { - this.sessionsRoot = getOrThrow(await this.fs.absolutePath(this.sessionsRootInput)); + this.sessionsRoot = getFileSystemResultOrThrow( + await this.fs.absolutePath(this.sessionsRootInput), + `Failed to resolve sessions root ${this.sessionsRootInput}`, + ); } return this.sessionsRoot; } private async getSessionDir(cwd: string): Promise { - return getOrThrow(await this.fs.joinPath([await this.getSessionsRoot(), encodeCwd(cwd)])); + return getFileSystemResultOrThrow( + await this.fs.joinPath([await this.getSessionsRoot(), encodeCwd(cwd)]), + `Failed to resolve session directory for ${cwd}`, + ); } private async createSessionFilePath(cwd: string, sessionId: string, timestamp: string): Promise { - return getOrThrow( + return getFileSystemResultOrThrow( await this.fs.joinPath([ await this.getSessionDir(cwd), `${timestamp.replace(/[:.]/g, "-")}_${sessionId}.jsonl`, ]), + `Failed to resolve session file path for ${sessionId}`, ); } @@ -62,7 +76,10 @@ export class JsonlSessionRepo implements JsonlSessionRepoApi { const id = options.id ?? createSessionId(); const createdAt = createTimestamp(); const sessionDir = await this.getSessionDir(options.cwd); - getOrThrow(await this.fs.createDir(sessionDir, { recursive: true })); + getFileSystemResultOrThrow( + await this.fs.createDir(sessionDir, { recursive: true }), + `Failed to create session directory ${sessionDir}`, + ); const filePath = await this.createSessionFilePath(options.cwd, id, createdAt); const storage = await JsonlSessionStorage.create(this.fs, filePath, { cwd: options.cwd, @@ -73,8 +90,10 @@ export class JsonlSessionRepo implements JsonlSessionRepoApi { } async open(metadata: JsonlSessionMetadata): Promise> { - if (!getOrThrow(await this.fs.exists(metadata.path))) { - throw new Error(`Session not found: ${metadata.path}`); + if ( + !getFileSystemResultOrThrow(await this.fs.exists(metadata.path), `Failed to check session ${metadata.path}`) + ) { + throw new SessionError("not_found", `Session not found: ${metadata.path}`); } const storage = await JsonlSessionStorage.open(this.fs, metadata.path); return toSession(storage); @@ -84,15 +103,19 @@ export class JsonlSessionRepo implements JsonlSessionRepoApi { const dirs = options.cwd ? [await this.getSessionDir(options.cwd)] : await this.listSessionDirs(); const sessions: JsonlSessionMetadata[] = []; for (const dir of dirs) { - if (!getOrThrow(await this.fs.exists(dir))) continue; - const files = getOrThrow(await this.fs.listDir(dir)).filter( - (file) => file.kind !== "directory" && file.name.endsWith(".jsonl"), - ); + if (!getFileSystemResultOrThrow(await this.fs.exists(dir), `Failed to check session directory ${dir}`)) { + continue; + } + const files = getFileSystemResultOrThrow( + await this.fs.listDir(dir), + `Failed to list sessions in ${dir}`, + ).filter((file) => file.kind !== "directory" && file.name.endsWith(".jsonl")); for (const file of files) { try { sessions.push(await loadJsonlSessionMetadata(this.fs, file.path)); - } catch { - // Ignore invalid session files when listing a directory. + } catch (error) { + const cause = toError(error); + if (!(cause instanceof SessionError) || cause.code !== "invalid_session") throw cause; } } } @@ -101,7 +124,10 @@ export class JsonlSessionRepo implements JsonlSessionRepoApi { } async delete(metadata: JsonlSessionMetadata): Promise { - getOrThrow(await this.fs.remove(metadata.path, { force: true })); + getFileSystemResultOrThrow( + await this.fs.remove(metadata.path, { force: true }), + `Failed to delete session ${metadata.path}`, + ); } async fork( @@ -113,7 +139,10 @@ export class JsonlSessionRepo implements JsonlSessionRepoApi { const id = options.id ?? createSessionId(); const createdAt = createTimestamp(); const sessionDir = await this.getSessionDir(options.cwd); - getOrThrow(await this.fs.createDir(sessionDir, { recursive: true })); + getFileSystemResultOrThrow( + await this.fs.createDir(sessionDir, { recursive: true }), + `Failed to create session directory ${sessionDir}`, + ); const storage = await JsonlSessionStorage.create( this.fs, await this.createSessionFilePath(options.cwd, id, createdAt), @@ -131,8 +160,18 @@ export class JsonlSessionRepo implements JsonlSessionRepoApi { private async listSessionDirs(): Promise { const sessionsRoot = await this.getSessionsRoot(); - if (!getOrThrow(await this.fs.exists(sessionsRoot))) return []; - const entries = getOrThrow(await this.fs.listDir(sessionsRoot)); + if ( + !getFileSystemResultOrThrow( + await this.fs.exists(sessionsRoot), + `Failed to check sessions root ${sessionsRoot}`, + ) + ) { + return []; + } + const entries = getFileSystemResultOrThrow( + await this.fs.listDir(sessionsRoot), + `Failed to list sessions root ${sessionsRoot}`, + ); return entries.filter((entry) => entry.kind === "directory").map((entry) => entry.path); } } diff --git a/packages/agent/src/harness/session/jsonl-storage.ts b/packages/agent/src/harness/session/jsonl-storage.ts index a6f370f7..eac8334d 100644 --- a/packages/agent/src/harness/session/jsonl-storage.ts +++ b/packages/agent/src/harness/session/jsonl-storage.ts @@ -1,8 +1,9 @@ -import type { FileSystem, JsonlSessionMetadata, SessionStorage, SessionTreeEntry } from "../types.js"; -import { getOrThrow } from "../types.js"; +import type { FileSystem, JsonlSessionMetadata, LeafEntry, SessionStorage, SessionTreeEntry } from "../types.js"; +import { SessionError, toError } from "../types.js"; +import { getFileSystemResultOrThrow } from "./repo-utils.js"; import { uuidv7 } from "./uuid.js"; -type JsonlSessionStorageFileSystem = Pick; +type JsonlSessionStorageFileSystem = Pick; interface SessionHeader { type: "session"; @@ -39,6 +40,76 @@ function generateEntryId(byId: { has(id: string): boolean }): string { return uuidv7(); } +function isRecord(value: unknown): value is Record { + return typeof value === "object" && value !== null; +} + +function invalidSession(filePath: string, message: string, cause?: Error): SessionError { + return new SessionError("invalid_session", `Invalid JSONL session file ${filePath}: ${message}`, cause); +} + +function invalidEntry(filePath: string, lineNumber: number, message: string, cause?: Error): SessionError { + return new SessionError( + "invalid_entry", + `Invalid JSONL session file ${filePath}: line ${lineNumber} ${message}`, + cause, + ); +} + +function parseHeaderLine(line: string, filePath: string): SessionHeader { + let parsed: unknown; + try { + parsed = JSON.parse(line); + } catch (error) { + throw invalidSession(filePath, "first line is not a valid session header", toError(error)); + } + if (!isRecord(parsed)) throw invalidSession(filePath, "first line is not a valid session header"); + if (parsed.type !== "session") throw invalidSession(filePath, "first line is not a valid session header"); + if (parsed.version !== 3) throw invalidSession(filePath, "unsupported session version"); + if (typeof parsed.id !== "string" || !parsed.id) throw invalidSession(filePath, "session header is missing id"); + if (typeof parsed.timestamp !== "string" || !parsed.timestamp) { + throw invalidSession(filePath, "session header is missing timestamp"); + } + if (typeof parsed.cwd !== "string" || !parsed.cwd) throw invalidSession(filePath, "session header is missing cwd"); + if (parsed.parentSession !== undefined && typeof parsed.parentSession !== "string") { + throw invalidSession(filePath, "session header parentSession must be a string"); + } + return { + type: "session", + version: 3, + id: parsed.id, + timestamp: parsed.timestamp, + cwd: parsed.cwd, + parentSession: parsed.parentSession, + }; +} + +function parseEntryLine(line: string, filePath: string, lineNumber: number): SessionTreeEntry { + let parsed: unknown; + try { + parsed = JSON.parse(line); + } catch (error) { + throw invalidEntry(filePath, lineNumber, "is not valid JSON", toError(error)); + } + if (!isRecord(parsed)) throw invalidEntry(filePath, lineNumber, "is not a valid session entry"); + if (typeof parsed.type !== "string") throw invalidEntry(filePath, lineNumber, "is missing entry type"); + if (typeof parsed.id !== "string" || !parsed.id) throw invalidEntry(filePath, lineNumber, "is missing entry id"); + if (parsed.parentId !== null && typeof parsed.parentId !== "string") { + throw invalidEntry(filePath, lineNumber, "has invalid parentId"); + } + if (typeof parsed.timestamp !== "string" || !parsed.timestamp) { + throw invalidEntry(filePath, lineNumber, "is missing timestamp"); + } + if (parsed.type === "leaf" && parsed.targetId !== null && typeof parsed.targetId !== "string") { + throw invalidEntry(filePath, lineNumber, "has invalid targetId"); + } + return parsed as unknown as SessionTreeEntry; +} + +function leafIdAfterEntry(entry: SessionTreeEntry): string | null { + return entry.type === "leaf" ? entry.targetId : entry.id; +} + function headerToSessionMetadata(header: SessionHeader, path: string): JsonlSessionMetadata { return { id: header.id, @@ -53,17 +124,13 @@ export async function loadJsonlSessionMetadata( fs: JsonlSessionStorageFileSystem, filePath: string, ): Promise { - const content = getOrThrow(await fs.readTextFile(filePath)); - for (const line of content.split("\n")) { - if (!line.trim()) break; - try { - const header = JSON.parse(line) as SessionHeader; - return headerToSessionMetadata(header, filePath); - } catch { - throw new Error(`Invalid JSONL session file ${filePath}: first line is not a valid session header`); - } - } - throw new Error(`Invalid JSONL session file ${filePath}: missing session header`); + const lines = getFileSystemResultOrThrow( + await fs.readTextLines(filePath, { maxLines: 1 }), + `Failed to read session header ${filePath}`, + ); + const line = lines[0]; + if (line?.trim()) return headerToSessionMetadata(parseHeaderLine(line, filePath), filePath); + throw invalidSession(filePath, "missing session header"); } async function loadJsonlStorage( @@ -74,29 +141,19 @@ async function loadJsonlStorage( entries: SessionTreeEntry[]; leafId: string | null; }> { - const content = getOrThrow(await fs.readTextFile(filePath)); + const content = getFileSystemResultOrThrow(await fs.readTextFile(filePath), `Failed to read session ${filePath}`); const lines = content.split("\n").filter((line) => line.trim()); if (lines.length === 0) { - throw new Error(`Invalid JSONL session file ${filePath}: missing session header`); - } - - let header: SessionHeader; - try { - header = JSON.parse(lines[0]!) as SessionHeader; - } catch { - throw new Error(`Invalid JSONL session file ${filePath}: first line is not a valid session header`); + throw invalidSession(filePath, "missing session header"); } + const header = parseHeaderLine(lines[0]!, filePath); const entries: SessionTreeEntry[] = []; let leafId: string | null = null; - for (const line of lines.slice(1)) { - try { - const entry = JSON.parse(line) as SessionTreeEntry; - entries.push(entry); - leafId = entry.id; - } catch { - // ignore malformed entry lines - } + for (let i = 1; i < lines.length; i++) { + const entry = parseEntryLine(lines[i]!, filePath, i + 1); + entries.push(entry); + leafId = leafIdAfterEntry(entry); } return { header, entries, leafId }; } @@ -148,7 +205,10 @@ export class JsonlSessionStorage implements SessionStorage cwd: options.cwd, parentSession: options.parentSessionPath, }; - getOrThrow(await fs.writeFile(filePath, `${JSON.stringify(header)}\n`)); + getFileSystemResultOrThrow( + await fs.writeFile(filePath, `${JSON.stringify(header)}\n`), + `Failed to create session ${filePath}`, + ); return new JsonlSessionStorage(fs, filePath, header, [], null); } @@ -157,13 +217,29 @@ export class JsonlSessionStorage implements SessionStorage } async getLeafId(): Promise { + if (this.currentLeafId !== null && !this.byId.has(this.currentLeafId)) { + throw new SessionError("invalid_session", `Entry ${this.currentLeafId} not found`); + } return this.currentLeafId; } async setLeafId(leafId: string | null): Promise { if (leafId !== null && !this.byId.has(leafId)) { - throw new Error(`Entry ${leafId} not found`); + throw new SessionError("not_found", `Entry ${leafId} not found`); } + const entry: LeafEntry = { + type: "leaf", + id: generateEntryId(this.byId), + parentId: this.currentLeafId, + timestamp: new Date().toISOString(), + targetId: leafId, + }; + getFileSystemResultOrThrow( + await this.fs.appendFile(this.filePath, `${JSON.stringify(entry)}\n`), + `Failed to append session leaf ${entry.id}`, + ); + this.entries.push(entry); + this.byId.set(entry.id, entry); this.currentLeafId = leafId; } @@ -172,11 +248,14 @@ export class JsonlSessionStorage implements SessionStorage } async appendEntry(entry: SessionTreeEntry): Promise { - getOrThrow(await this.fs.appendFile(this.filePath, `${JSON.stringify(entry)}\n`)); + getFileSystemResultOrThrow( + await this.fs.appendFile(this.filePath, `${JSON.stringify(entry)}\n`), + `Failed to append session entry ${entry.id}`, + ); this.entries.push(entry); this.byId.set(entry.id, entry); updateLabelCache(this.labelsById, entry); - this.currentLeafId = entry.id; + this.currentLeafId = leafIdAfterEntry(entry); } async getEntry(id: string): Promise { @@ -197,9 +276,13 @@ export class JsonlSessionStorage implements SessionStorage if (leafId === null) return []; const path: SessionTreeEntry[] = []; let current = this.byId.get(leafId); + if (!current) throw new SessionError("not_found", `Entry ${leafId} not found`); while (current) { path.unshift(current); - current = current.parentId ? this.byId.get(current.parentId) : undefined; + if (!current.parentId) break; + const parent = this.byId.get(current.parentId); + if (!parent) throw new SessionError("invalid_session", `Entry ${current.parentId} not found`); + current = parent; } return path; } diff --git a/packages/agent/src/harness/session/memory-repo.ts b/packages/agent/src/harness/session/memory-repo.ts index 85401801..54095514 100644 --- a/packages/agent/src/harness/session/memory-repo.ts +++ b/packages/agent/src/harness/session/memory-repo.ts @@ -1,4 +1,4 @@ -import type { Session, SessionMetadata, SessionRepo } from "../types.js"; +import { type Session, SessionError, type SessionMetadata, type SessionRepo } from "../types.js"; import { InMemorySessionStorage } from "./memory-storage.js"; import { createSessionId, createTimestamp, getEntriesToFork, toSession } from "./repo-utils.js"; @@ -19,7 +19,7 @@ export class InMemorySessionRepo implements SessionRepo> { const session = this.sessions.get(metadata.id); if (!session) { - throw new Error(`Session not found: ${metadata.id}`); + throw new SessionError("not_found", `Session not found: ${metadata.id}`); } return session; } @@ -42,8 +42,7 @@ export class InMemorySessionRepo implements SessionRepo, entry: SessionTreeEntry): void { @@ -27,36 +33,55 @@ function generateEntryId(byId: { has(id: string): boolean }): string { return uuidv7(); } -export class InMemorySessionStorage implements SessionStorage { - private readonly metadata: SessionMetadata; +function leafIdAfterEntry(entry: SessionTreeEntry): string | null { + return entry.type === "leaf" ? entry.targetId : entry.id; +} + +export class InMemorySessionStorage + implements SessionStorage +{ + private readonly metadata: TMetadata; private entries: SessionTreeEntry[]; private byId: Map; private labelsById: Map; private leafId: string | null; - constructor(options?: { entries?: SessionTreeEntry[]; leafId?: string | null; metadata?: SessionMetadata }) { + constructor(options?: { entries?: SessionTreeEntry[]; metadata?: TMetadata }) { this.entries = options?.entries ? [...options.entries] : []; this.byId = new Map(this.entries.map((entry) => [entry.id, entry])); this.labelsById = buildLabelsById(this.entries); - this.leafId = options?.leafId ?? this.entries[this.entries.length - 1]?.id ?? null; + this.leafId = null; + for (const entry of this.entries) this.leafId = leafIdAfterEntry(entry); if (this.leafId !== null && !this.byId.has(this.leafId)) { - throw new Error(`Entry ${this.leafId} not found`); + throw new SessionError("invalid_session", `Entry ${this.leafId} not found`); } - this.metadata = options?.metadata ?? { id: uuidv7(), createdAt: new Date().toISOString() }; + this.metadata = options?.metadata ?? ({ id: uuidv7(), createdAt: new Date().toISOString() } as TMetadata); } - async getMetadata(): Promise { + async getMetadata(): Promise { return this.metadata; } async getLeafId(): Promise { + if (this.leafId !== null && !this.byId.has(this.leafId)) { + throw new SessionError("invalid_session", `Entry ${this.leafId} not found`); + } return this.leafId; } async setLeafId(leafId: string | null): Promise { if (leafId !== null && !this.byId.has(leafId)) { - throw new Error(`Entry ${leafId} not found`); + throw new SessionError("not_found", `Entry ${leafId} not found`); } + const entry: LeafEntry = { + type: "leaf", + id: generateEntryId(this.byId), + parentId: this.leafId, + timestamp: new Date().toISOString(), + targetId: leafId, + }; + this.entries.push(entry); + this.byId.set(entry.id, entry); this.leafId = leafId; } @@ -68,7 +93,7 @@ export class InMemorySessionStorage implements SessionStorage { this.entries.push(entry); this.byId.set(entry.id, entry); updateLabelCache(this.labelsById, entry); - this.leafId = entry.id; + this.leafId = leafIdAfterEntry(entry); } async getEntry(id: string): Promise { @@ -89,9 +114,13 @@ export class InMemorySessionStorage implements SessionStorage { if (leafId === null) return []; const path: SessionTreeEntry[] = []; let current = this.byId.get(leafId); + if (!current) throw new SessionError("not_found", `Entry ${leafId} not found`); while (current) { path.unshift(current); - current = current.parentId ? this.byId.get(current.parentId) : undefined; + if (!current.parentId) break; + const parent = this.byId.get(current.parentId); + if (!parent) throw new SessionError("invalid_session", `Entry ${current.parentId} not found`); + current = parent; } return path; } diff --git a/packages/agent/src/harness/session/repo-utils.ts b/packages/agent/src/harness/session/repo-utils.ts index e842a718..f98b1256 100644 --- a/packages/agent/src/harness/session/repo-utils.ts +++ b/packages/agent/src/harness/session/repo-utils.ts @@ -1,4 +1,11 @@ -import type { SessionMetadata, SessionStorage, SessionTreeEntry } from "../types.js"; +import { + type FileError, + type Result, + SessionError, + type SessionMetadata, + type SessionStorage, + type SessionTreeEntry, +} from "../types.js"; import { Session } from "./session.js"; import { uuidv7 } from "./uuid.js"; @@ -14,6 +21,14 @@ export function toSession(storage: SessionSto return new Session(storage); } +export function getFileSystemResultOrThrow(result: Result, message: string): TValue { + if (!result.ok) { + const code = result.error.code === "not_found" ? "not_found" : "storage"; + throw new SessionError(code, `${message}: ${result.error.message}`, result.error); + } + return result.value; +} + export async function getEntriesToFork( storage: SessionStorage, options: { entryId?: string; position?: "before" | "at" }, @@ -21,14 +36,14 @@ export async function getEntriesToFork( if (!options.entryId) return storage.getEntries(); const target = await storage.getEntry(options.entryId); if (!target) { - throw new Error(`Entry ${options.entryId} not found`); + throw new SessionError("invalid_fork_target", `Entry ${options.entryId} not found`); } let effectiveLeafId: string | null; if ((options.position ?? "before") === "at") { effectiveLeafId = target.id; } else { if (target.type !== "message" || target.message.role !== "user") { - throw new Error(`Entry ${options.entryId} is not a user message`); + throw new SessionError("invalid_fork_target", `Entry ${options.entryId} is not a user message`); } effectiveLeafId = target.parentId; } diff --git a/packages/agent/src/harness/session/session.ts b/packages/agent/src/harness/session/session.ts index f0b42ecf..0b0b3e1e 100644 --- a/packages/agent/src/harness/session/session.ts +++ b/packages/agent/src/harness/session/session.ts @@ -16,6 +16,7 @@ import type { SessionTreeEntry, ThinkingLevelChangeEntry, } from "../types.js"; +import { SessionError } from "../types.js"; export function buildSessionContext(pathEntries: SessionTreeEntry[]): SessionContext { let thinkingLevel = "off"; @@ -206,7 +207,7 @@ export class Session { async appendLabel(targetId: string, label: string | undefined): Promise { if (!(await this.storage.getEntry(targetId))) { - throw new Error(`Entry ${targetId} not found`); + throw new SessionError("not_found", `Entry ${targetId} not found`); } return this.appendTypedEntry({ type: "label", @@ -233,7 +234,7 @@ export class Session { summary?: { summary: string; details?: unknown; fromHook?: boolean }, ): Promise { if (entryId !== null && !(await this.storage.getEntry(entryId))) { - throw new Error(`Entry ${entryId} not found`); + throw new SessionError("not_found", `Entry ${entryId} not found`); } await this.storage.setLeafId(entryId); if (!summary) return undefined; diff --git a/packages/agent/src/harness/skills.ts b/packages/agent/src/harness/skills.ts index d26c8627..7269857e 100644 --- a/packages/agent/src/harness/skills.ts +++ b/packages/agent/src/harness/skills.ts @@ -1,6 +1,6 @@ import ignore from "ignore"; import { parse } from "yaml"; -import { type ExecutionEnv, type FileInfo, getOrUndefined, type Result, type Skill } from "./types.js"; +import { type ExecutionEnv, type FileInfo, type Result, type Skill, toError } from "./types.js"; const MAX_NAME_LENGTH = 64; const MAX_DESCRIPTION_LENGTH = 1024; @@ -8,10 +8,19 @@ const IGNORE_FILE_NAMES = [".gitignore", ".ignore", ".fdignore"]; type IgnoreMatcher = ReturnType; +export type SkillDiagnosticCode = + | "file_info_failed" + | "list_failed" + | "read_failed" + | "parse_failed" + | "invalid_metadata"; + /** Warning produced while loading skills. */ export interface SkillDiagnostic { /** Diagnostic severity. Currently only warnings are emitted. */ type: "warning"; + /** Stable diagnostic code. */ + code: SkillDiagnosticCode; /** Human-readable diagnostic message. */ message: string; /** Path associated with the diagnostic. */ @@ -44,8 +53,20 @@ export async function loadSkills( const skills: Skill[] = []; const diagnostics: SkillDiagnostic[] = []; for (const dir of Array.isArray(dirs) ? dirs : [dirs]) { - const rootInfo = getOrUndefined(await env.fileInfo(dir)); - if (!rootInfo || (await resolveKind(env, rootInfo)) !== "directory") continue; + const rootInfoResult = await env.fileInfo(dir); + if (!rootInfoResult.ok) { + if (rootInfoResult.error.code !== "not_found") { + diagnostics.push({ + type: "warning", + code: "file_info_failed", + message: rootInfoResult.error.message, + path: dir, + }); + } + continue; + } + const rootInfo = rootInfoResult.value; + if ((await resolveKind(env, rootInfo, diagnostics)) !== "directory") continue; const result = await loadSkillsFromDirInternal(env, rootInfo.path, true, ignore(), rootInfo.path); skills.push(...result.skills); diagnostics.push(...result.diagnostics); @@ -89,18 +110,34 @@ async function loadSkillsFromDirInternal( const skills: Skill[] = []; const diagnostics: SkillDiagnostic[] = []; - const dirInfo = getOrUndefined(await env.fileInfo(dir)); - if (!dirInfo || (await resolveKind(env, dirInfo)) !== "directory") return { skills, diagnostics }; + const dirInfoResult = await env.fileInfo(dir); + if (!dirInfoResult.ok) { + if (dirInfoResult.error.code !== "not_found") { + diagnostics.push({ + type: "warning", + code: "file_info_failed", + message: dirInfoResult.error.message, + path: dir, + }); + } + return { skills, diagnostics }; + } + const dirInfo = dirInfoResult.value; + if ((await resolveKind(env, dirInfo, diagnostics)) !== "directory") return { skills, diagnostics }; - await addIgnoreRules(env, ignoreMatcher, dir, rootDir); + await addIgnoreRules(env, ignoreMatcher, dir, rootDir, diagnostics); - const entries = getOrUndefined(await env.listDir(dir)); - if (!entries) return { skills, diagnostics }; + const entriesResult = await env.listDir(dir); + if (!entriesResult.ok) { + diagnostics.push({ type: "warning", code: "list_failed", message: entriesResult.error.message, path: dir }); + return { skills, diagnostics }; + } + const entries = entriesResult.value; for (const entry of entries) { if (entry.name !== "SKILL.md") continue; const fullPath = entry.path; - const kind = await resolveKind(env, entry); + const kind = await resolveKind(env, entry, diagnostics); if (kind !== "file") continue; const relPath = relativeEnvPath(rootDir, fullPath); if (ignoreMatcher.ignores(relPath)) continue; @@ -114,7 +151,7 @@ async function loadSkillsFromDirInternal( for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) { if (entry.name.startsWith(".") || entry.name === "node_modules") continue; const fullPath = entry.path; - const kind = await resolveKind(env, entry); + const kind = await resolveKind(env, entry, diagnostics); if (!kind) continue; const relPath = relativeEnvPath(rootDir, fullPath); @@ -137,16 +174,36 @@ async function loadSkillsFromDirInternal( return { skills, diagnostics }; } -async function addIgnoreRules(env: ExecutionEnv, ig: IgnoreMatcher, dir: string, rootDir: string): Promise { +async function addIgnoreRules( + env: ExecutionEnv, + ig: IgnoreMatcher, + dir: string, + rootDir: string, + diagnostics: SkillDiagnostic[], +): Promise { const relativeDir = relativeEnvPath(rootDir, dir); const prefix = relativeDir ? `${relativeDir}/` : ""; for (const filename of IGNORE_FILE_NAMES) { const ignorePath = joinEnvPath(dir, filename); - const info = getOrUndefined(await env.fileInfo(ignorePath)); - if (info?.kind !== "file") continue; + const info = await env.fileInfo(ignorePath); + if (!info.ok) { + if (info.error.code !== "not_found") { + diagnostics.push({ + type: "warning", + code: "file_info_failed", + message: info.error.message, + path: ignorePath, + }); + } + continue; + } + if (info.value.kind !== "file") continue; const content = await env.readTextFile(ignorePath); - if (!content.ok) continue; + if (!content.ok) { + diagnostics.push({ type: "warning", code: "read_failed", message: content.error.message, path: ignorePath }); + continue; + } const patterns = content.value .split(/\r?\n/) .map((line) => prefixIgnorePattern(line, prefix)) @@ -180,13 +237,13 @@ async function loadSkillFromFile( const diagnostics: SkillDiagnostic[] = []; const rawContent = await env.readTextFile(filePath); if (!rawContent.ok) { - diagnostics.push({ type: "warning", message: rawContent.error.message, path: filePath }); + diagnostics.push({ type: "warning", code: "read_failed", message: rawContent.error.message, path: filePath }); return { skill: null, diagnostics }; } const parsed = parseFrontmatter(rawContent.value); if (!parsed.ok) { - diagnostics.push({ type: "warning", message: parsed.error.message, path: filePath }); + diagnostics.push({ type: "warning", code: "parse_failed", message: parsed.error.message, path: filePath }); return { skill: null, diagnostics }; } @@ -196,13 +253,13 @@ async function loadSkillFromFile( const description = typeof frontmatter.description === "string" ? frontmatter.description : undefined; for (const error of validateDescription(description)) { - diagnostics.push({ type: "warning", message: error, path: filePath }); + diagnostics.push({ type: "warning", code: "invalid_metadata", message: error, path: filePath }); } const frontmatterName = typeof frontmatter.name === "string" ? frontmatter.name : undefined; const name = frontmatterName || parentDirName; for (const error of validateName(name, parentDirName)) { - diagnostics.push({ type: "warning", message: error, path: filePath }); + diagnostics.push({ type: "warning", code: "invalid_metadata", message: error, path: filePath }); } if (!description || description.trim() === "") { @@ -255,17 +312,41 @@ function parseFrontmatter>( const body = normalized.slice(endIndex + 4).trim(); return { ok: true, value: { frontmatter: (parse(yamlString) ?? {}) as T, body } }; } catch (error) { - return { ok: false, error: error instanceof Error ? error : new Error(String(error)) }; + return { ok: false, error: toError(error) }; } } -async function resolveKind(env: ExecutionEnv, info: FileInfo): Promise<"file" | "directory" | undefined> { +async function resolveKind( + env: ExecutionEnv, + info: FileInfo, + diagnostics: SkillDiagnostic[], +): Promise<"file" | "directory" | undefined> { if (info.kind === "file" || info.kind === "directory") return info.kind; const canonicalPath = await env.canonicalPath(info.path); - if (!canonicalPath.ok) return undefined; - const target = getOrUndefined(await env.fileInfo(canonicalPath.value)); - if (!target) return undefined; - return target.kind === "file" || target.kind === "directory" ? target.kind : undefined; + if (!canonicalPath.ok) { + if (canonicalPath.error.code !== "not_found") { + diagnostics.push({ + type: "warning", + code: "file_info_failed", + message: canonicalPath.error.message, + path: info.path, + }); + } + return undefined; + } + const target = await env.fileInfo(canonicalPath.value); + if (!target.ok) { + if (target.error.code !== "not_found") { + diagnostics.push({ + type: "warning", + code: "file_info_failed", + message: target.error.message, + path: info.path, + }); + } + return undefined; + } + return target.value.kind === "file" || target.value.kind === "directory" ? target.value.kind : undefined; } function joinEnvPath(base: string, child: string): string { diff --git a/packages/agent/src/harness/types.ts b/packages/agent/src/harness/types.ts index d25f181c..96b60f63 100644 --- a/packages/agent/src/harness/types.ts +++ b/packages/agent/src/harness/types.ts @@ -26,6 +26,17 @@ export function getOrUndefined(result: Result>; /** Read a UTF-8 text file. */ readTextFile(path: string, abortSignal?: AbortSignal): Promise>; + /** Read UTF-8 text lines. Implementations should stop once `maxLines` lines have been read. */ + readTextLines( + path: string, + options?: { maxLines?: number; abortSignal?: AbortSignal }, + ): Promise>; /** Read a binary file. */ readBinaryFile(path: string, abortSignal?: AbortSignal): Promise>; /** Create or overwrite a file, creating parent directories when supported. */ @@ -319,6 +401,11 @@ export interface SessionInfoEntry extends SessionTreeEntryBase { name?: string; } +export interface LeafEntry extends SessionTreeEntryBase { + type: "leaf"; + targetId: string | null; +} + export type SessionTreeEntry = | MessageEntry | ThinkingLevelChangeEntry @@ -328,7 +415,8 @@ export type SessionTreeEntry = | CustomEntry | CustomMessageEntry | LabelEntry - | SessionInfoEntry; + | SessionInfoEntry + | LeafEntry; export interface SessionContext { messages: AgentMessage[]; @@ -350,6 +438,7 @@ export interface JsonlSessionMetadata extends SessionMetadata { export interface SessionStorage { getMetadata(): Promise; getLeafId(): Promise; + /** Persist a leaf entry that records the active session-tree leaf. */ setLeafId(leafId: string | null): Promise; createEntryId(): Promise; appendEntry(entry: SessionTreeEntry): Promise; @@ -688,11 +777,9 @@ export interface GenerateBranchSummaryOptions { } export interface BranchSummaryResult { - summary?: string; - readFiles?: string[]; - modifiedFiles?: string[]; - aborted?: boolean; - error?: string; + summary: string; + readFiles: string[]; + modifiedFiles: string[]; } export interface AgentHarnessOptions< diff --git a/packages/agent/src/harness/utils/shell-output.ts b/packages/agent/src/harness/utils/shell-output.ts index fcb8d538..23f1362c 100644 --- a/packages/agent/src/harness/utils/shell-output.ts +++ b/packages/agent/src/harness/utils/shell-output.ts @@ -1,4 +1,12 @@ -import { type ExecutionEnv, type ExecutionEnvExecOptions, ExecutionError, err, ok, type Result } from "../types.js"; +import { + type ExecutionEnv, + type ExecutionEnvExecOptions, + ExecutionError, + err, + ok, + type Result, + toError, +} from "../types.js"; import { DEFAULT_MAX_BYTES, truncateTail } from "./truncate.js"; export interface ShellCaptureOptions extends Omit { @@ -14,9 +22,9 @@ export interface ShellCaptureResult { } function toExecutionError(error: unknown): ExecutionError { - return error instanceof ExecutionError - ? error - : new ExecutionError("unknown", error instanceof Error ? error.message : String(error), error); + if (error instanceof ExecutionError) return error; + const cause = toError(error); + return new ExecutionError("unknown", cause.message, cause); } export function sanitizeBinaryOutput(str: string): string { diff --git a/packages/agent/src/index.ts b/packages/agent/src/index.ts index 345def15..04275403 100644 --- a/packages/agent/src/index.ts +++ b/packages/agent/src/index.ts @@ -4,6 +4,9 @@ export * from "./agent.js"; export * from "./agent-loop.js"; export * from "./harness/agent-harness.js"; export { + type BranchPreparation, + type BranchSummaryDetails, + type CollectEntriesResult, collectEntriesForBranchSummary, generateBranchSummary, prepareBranchEntries, diff --git a/packages/agent/test/harness/compaction.test.ts b/packages/agent/test/harness/compaction.test.ts index a9ec08fa..25d740ca 100644 --- a/packages/agent/test/harness/compaction.test.ts +++ b/packages/agent/test/harness/compaction.test.ts @@ -345,7 +345,7 @@ describe("harness compaction", () => { const u3 = createMessageEntry(createUserMessage("user msg 3"), compaction1.id); const a3 = createMessageEntry(createAssistantMessage("assistant msg 3", createMockUsage(8000, 2000)), u3.id); const pathEntries = [u1, a1, u2, a2, compaction1, u3, a3]; - const preparation = prepareCompaction(pathEntries, DEFAULT_COMPACTION_SETTINGS); + const preparation = getOrThrow(prepareCompaction(pathEntries, DEFAULT_COMPACTION_SETTINGS)); expect(preparation).toBeDefined(); expect(preparation?.previousSummary).toBe("First summary"); expect(preparation?.firstKeptEntryId).toBeTruthy(); @@ -365,11 +365,13 @@ describe("harness compaction", () => { }; const u2 = createMessageEntry(createUserMessage("large turn"), compaction1.id); const a2 = createMessageEntry(createAssistantMessage("large assistant message"), u2.id); - const preparation = prepareCompaction([u1, a1, compaction1, u2, a2], { - enabled: true, - reserveTokens: 100, - keepRecentTokens: 1, - }); + const preparation = getOrThrow( + prepareCompaction([u1, a1, compaction1, u2, a2], { + enabled: true, + reserveTokens: 100, + keepRecentTokens: 1, + }), + ); expect(preparation).toMatchObject({ previousSummary: "First summary", isSplitTurn: true }); expect(preparation?.turnPrefixMessages.map((message) => message.role)).toEqual(["user"]); @@ -398,19 +400,21 @@ describe("harness compaction", () => { }; const user = createMessageEntry(createUserMessage("keep"), customMessage.id); const assistant = createMessageEntry(createAssistantMessage("assistant"), user.id); - const preparation = prepareCompaction([branchSummary, customMessage, user, assistant], { - enabled: true, - reserveTokens: 100, - keepRecentTokens: 1, - }); + const preparation = getOrThrow( + prepareCompaction([branchSummary, customMessage, user, assistant], { + enabled: true, + reserveTokens: 100, + keepRecentTokens: 1, + }), + ); expect(preparation?.messagesToSummarize.map((message) => message.role)).toEqual(["branchSummary", "custom"]); }); it("does not prepare compaction when there is nothing valid to compact", () => { const compaction = createCompactionEntry("already compacted", "entry-keep"); - expect(prepareCompaction([compaction], DEFAULT_COMPACTION_SETTINGS)).toBeUndefined(); - expect(prepareCompaction([], DEFAULT_COMPACTION_SETTINGS)).toBeUndefined(); + expect(getOrThrow(prepareCompaction([compaction], DEFAULT_COMPACTION_SETTINGS))).toBeUndefined(); + expect(getOrThrow(prepareCompaction([], DEFAULT_COMPACTION_SETTINGS))).toBeUndefined(); }); it("serializes conversation with truncated tool results", () => { @@ -535,13 +539,6 @@ describe("harness compaction", () => { abortedFaux.setResponses([fauxAssistantMessage("", { stopReason: "aborted", errorMessage: "stopped" })]); const abortedResult = await generateSummary(messages, abortedModel, 2000, "test-key"); expect(abortedResult).toMatchObject({ ok: false, error: { code: "aborted", message: "stopped" } }); - - const missingProviderModel = { ...abortedModel, api: "missing-api" } as Model; - const unknownResult = await generateSummary(messages, missingProviderModel, 2000, "test-key"); - expect(unknownResult).toMatchObject({ - ok: false, - error: { code: "unknown", message: "Summarization request failed" }, - }); }); it("clamps compaction summary maxTokens to the model output cap", async () => { @@ -650,12 +647,6 @@ describe("harness compaction", () => { ok: false, error: { code: "aborted", message: "prefix stopped" }, }); - - const missingProviderModel = { ...abortedModel, api: "missing-api" } as Model; - expect(await compact(preparation, missingProviderModel, "test-key")).toMatchObject({ - ok: false, - error: { code: "unknown", message: "Turn prefix summarization request failed" }, - }); }); it("returns a compaction result with file details", async () => { @@ -667,7 +658,7 @@ describe("harness compaction", () => { const a1 = createMessageEntry(assistantMessage, u1.id); const u2 = createMessageEntry(createUserMessage("continue"), a1.id); const a2 = createMessageEntry(createAssistantMessage("done", createMockUsage(4000, 500)), u2.id); - const preparation = prepareCompaction([u1, a1, u2, a2], DEFAULT_COMPACTION_SETTINGS); + const preparation = getOrThrow(prepareCompaction([u1, a1, u2, a2], DEFAULT_COMPACTION_SETTINGS)); expect(preparation).toBeDefined(); const { faux, model } = createFauxModel(false); faux.setResponses([fauxAssistantMessage("## Goal\nTest summary")]); diff --git a/packages/agent/test/harness/nodejs-env.test.ts b/packages/agent/test/harness/nodejs-env.test.ts index fa3d6aa9..aef098be 100644 --- a/packages/agent/test/harness/nodejs-env.test.ts +++ b/packages/agent/test/harness/nodejs-env.test.ts @@ -27,6 +27,7 @@ describe("NodeExecutionEnv", () => { getOrThrow(await env.writeFile("nested/child/file.txt", "hel")); getOrThrow(await env.appendFile("nested/child/file.txt", "lo")); expect(getOrThrow(await env.readTextFile("nested/child/file.txt"))).toBe("hello"); + expect(getOrThrow(await env.readTextLines("nested/child/file.txt", { maxLines: 1 }))).toEqual(["hello"]); expect(Buffer.from(getOrThrow(await env.readBinaryFile("nested/child/file.txt"))).toString("utf8")).toBe("hello"); const entries = getOrThrow(await env.listDir("nested/child")); @@ -91,6 +92,13 @@ describe("NodeExecutionEnv", () => { ]); }); + it("stops reading text lines at the requested limit", async () => { + const root = createTempDir(); + const env = new NodeExecutionEnv({ cwd: root }); + getOrThrow(await env.writeFile("file.txt", "one\ntwo\nthree")); + expect(getOrThrow(await env.readTextLines("file.txt", { maxLines: 1 }))).toEqual(["one"]); + }); + it("returns FileError for missing paths and keeps exists false for missing paths", async () => { const root = createTempDir(); const env = new NodeExecutionEnv({ cwd: root }); @@ -155,7 +163,7 @@ describe("NodeExecutionEnv", () => { getOrThrow(await env.remove("missing", { force: true })); }); - it("returns aborted results for pre-aborted file operations", async () => { + it("returns aborted results for pre-aborted cancellable file operations", async () => { const root = createTempDir(); const env = new NodeExecutionEnv({ cwd: root }); getOrThrow(await env.writeFile("file.txt", "hello")); @@ -165,17 +173,10 @@ describe("NodeExecutionEnv", () => { const results = await Promise.all([ env.readTextFile("file.txt", signal), + env.readTextLines("file.txt", { abortSignal: signal }), env.readBinaryFile("file.txt", signal), env.writeFile("other.txt", "hello", signal), - env.appendFile("other.txt", "hello", signal), - env.fileInfo("file.txt", signal), env.listDir(".", signal), - env.canonicalPath("file.txt", signal), - env.exists("file.txt", signal), - env.createDir("dir", { abortSignal: signal }), - env.remove("file.txt", { abortSignal: signal }), - env.createTempDir("node-env-test-", signal), - env.createTempFile({ abortSignal: signal }), ]); for (const result of results) { expect(result.ok).toBe(false); @@ -244,7 +245,7 @@ describe("NodeExecutionEnv", () => { }, }); expect(result.ok).toBe(false); - if (!result.ok) expect(result.error).toMatchObject({ code: "unknown", message: "callback failed" }); + if (!result.ok) expect(result.error).toMatchObject({ code: "callback_error", message: "callback failed" }); }); it("returns shell unavailable and spawn errors", async () => { diff --git a/packages/agent/test/harness/session.test.ts b/packages/agent/test/harness/session.test.ts index bfb0a6b3..96510ea4 100644 --- a/packages/agent/test/harness/session.test.ts +++ b/packages/agent/test/harness/session.test.ts @@ -140,10 +140,10 @@ runSessionSuite( const header = JSON.parse(lines[0]!); expect(header.type).toBe("session"); expect(header.version).toBe(3); - for (const line of lines.slice(1)) { - const entry = JSON.parse(line); + const entries = lines.slice(1).map((line) => JSON.parse(line)); + expect(entries.some((entry) => entry.type === "leaf")).toBe(true); + for (const entry of entries) { expect(entry.type).not.toBe("entry"); - expect(entry.type).not.toBe("leaf"); expect(typeof entry.id).toBe("string"); } }, diff --git a/packages/agent/test/harness/skills.test.ts b/packages/agent/test/harness/skills.test.ts index c4d53cbb..41840a3e 100644 --- a/packages/agent/test/harness/skills.test.ts +++ b/packages/agent/test/harness/skills.test.ts @@ -93,6 +93,7 @@ Use this skill. expect(diagnostics).toEqual([ { type: "warning", + code: "invalid_metadata", message: "description is required", path: join(root, "user/broken/SKILL.md"), source: { type: "user" }, diff --git a/packages/agent/test/harness/storage.test.ts b/packages/agent/test/harness/storage.test.ts index 20f7ac8d..ff6a6a95 100644 --- a/packages/agent/test/harness/storage.test.ts +++ b/packages/agent/test/harness/storage.test.ts @@ -4,7 +4,7 @@ import { describe, expect, it } from "vitest"; import { NodeExecutionEnv } from "../../src/harness/env/nodejs.js"; import { JsonlSessionStorage, loadJsonlSessionMetadata } from "../../src/harness/session/jsonl-storage.js"; import { InMemorySessionStorage } from "../../src/harness/session/memory-storage.js"; -import type { MessageEntry, SessionMetadata } from "../../src/harness/types.js"; +import { type MessageEntry, ok, type SessionMetadata } from "../../src/harness/types.js"; import { createAssistantMessage, createTempDir, createUserMessage } from "./session-test-utils.js"; describe("InMemorySessionStorage", () => { @@ -14,7 +14,7 @@ describe("InMemorySessionStorage", () => { expect(await storage.getMetadata()).toEqual(metadata); }); - it("copies initial entries and tracks leaf independently", async () => { + it("copies initial entries and persists leaf changes", async () => { const entry: MessageEntry = { type: "message", id: "entry-1", @@ -29,12 +29,12 @@ describe("InMemorySessionStorage", () => { expect(await storage.getLeafId()).toBe("entry-1"); await storage.setLeafId(null); expect(await storage.getLeafId()).toBeNull(); + expect((await storage.getEntries()).at(-1)).toMatchObject({ type: "leaf", targetId: null }); }); it("rejects invalid leaf ids", async () => { const storage = new InMemorySessionStorage(); await expect(storage.setLeafId("missing")).rejects.toThrow("Entry missing not found"); - expect(() => new InMemorySessionStorage({ leafId: "missing" })).toThrow("Entry missing not found"); }); it("finds entries by type", async () => { @@ -138,7 +138,7 @@ describe("JsonlSessionStorage", () => { await expect(JsonlSessionStorage.open(env, filePath)).rejects.toThrow("first line is not a valid session header"); }); - it("ignores malformed entry lines", async () => { + it("throws for malformed entry lines", async () => { const dir = createTempDir(); const env = new NodeExecutionEnv({ cwd: dir }); const filePath = join(dir, "session.jsonl"); @@ -157,9 +157,7 @@ describe("JsonlSessionStorage", () => { message: createUserMessage("one"), }; writeFileSync(filePath, `${JSON.stringify(header)}\nnot json\n${JSON.stringify(entry)}\n`); - const storage = await JsonlSessionStorage.open(env, filePath); - expect((await storage.getEntries()).map((loadedEntry) => loadedEntry.id)).toEqual(["entry-1"]); - expect(await storage.getLeafId()).toBe("entry-1"); + await expect(JsonlSessionStorage.open(env, filePath)).rejects.toMatchObject({ code: "invalid_entry" }); }); it("creates and reads session metadata from the header", async () => { @@ -211,6 +209,10 @@ describe("JsonlSessionStorage", () => { const loaded = await JsonlSessionStorage.open(env, filePath); expect(await loaded.getLeafId()).toBe("child"); expect((await loaded.getEntries()).map((entry) => entry.id)).toEqual(["root", "child"]); + await loaded.setLeafId("root"); + const reloaded = await JsonlSessionStorage.open(env, filePath); + expect(await reloaded.getLeafId()).toBe("root"); + expect((await reloaded.getEntries()).at(-1)).toMatchObject({ type: "leaf", targetId: "root" }); expect((await loaded.getPathToRoot("child")).map((entry) => entry.id)).toEqual(["root", "child"]); }); @@ -265,9 +267,8 @@ describe("JsonlSessionStorage", () => { expect(await loaded.getLabel("entry-1")).toBeUndefined(); }); - it("reads session metadata from only the first JSONL line", async () => { + it("reads session metadata through the line-reading filesystem operation", async () => { const dir = createTempDir(); - const env = new NodeExecutionEnv({ cwd: dir }); const filePath = join(dir, "session.jsonl"); const header = { type: "session", @@ -276,9 +277,18 @@ describe("JsonlSessionStorage", () => { timestamp: "2026-01-01T00:00:00.000Z", cwd: dir, }; - const malformedSecondLine = "{".repeat(10000); - writeFileSync(filePath, `${JSON.stringify(header)}\n${malformedSecondLine}\n`); - expect(await loadJsonlSessionMetadata(env, filePath)).toEqual({ + const metadata = await loadJsonlSessionMetadata( + { + readTextLines: async () => ok([JSON.stringify(header)]), + readTextFile: async () => { + throw new Error("readTextFile should not be called for metadata"); + }, + writeFile: async () => ok(undefined), + appendFile: async () => ok(undefined), + }, + filePath, + ); + expect(metadata).toEqual({ id: "session-1", createdAt: "2026-01-01T00:00:00.000Z", cwd: dir, From b4ee3aaeea183b793d320d71eb91ee80c6a1c583 Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Sat, 16 May 2026 01:06:27 +0200 Subject: [PATCH 048/166] docs(agent): document harness hook design --- packages/agent/docs/agent-harness.md | 334 ++++++++++--------- packages/agent/docs/hooks.md | 465 +++++++++++++++++++++++++++ 2 files changed, 629 insertions(+), 170 deletions(-) create mode 100644 packages/agent/docs/hooks.md diff --git a/packages/agent/docs/agent-harness.md b/packages/agent/docs/agent-harness.md index 5a092fa2..18749d71 100644 --- a/packages/agent/docs/agent-harness.md +++ b/packages/agent/docs/agent-harness.md @@ -158,14 +158,19 @@ If the system-prompt callback throws while starting `prompt`, `skill`, or `promp ## Hooks and events -Current hooks and listeners receive only the event payload. There is no extension context object yet. +The target hook system is described in [hooks.md](./hooks.md). + +Summary: + +- `AgentHarness` emits typed hook events and consumes typed results. +- A single hooks implementation owns registration, cleanup, provenance, and result reducers. +- Observational and mutation hooks use one event-specific `on()` API; the event result type determines whether a handler may return a result. +- Result-producing events are reduced by typed reducer tables; app-specific hooks add reducers only for app-specific result-producing events. +- Hook registration provenance is sidecar metadata on the registration. Resource and tool provenance belongs on app-specific concrete value types. +- Hook context should be a plain object of facades, not raw internals or late-bound getter mazes. Event payloads describe what is happening. Harness getters describe latest config for future snapshots. -The split between harness-specific events (`AgentHarnessOwnEvent`) and the union of low-level plus harness events (`AgentHarnessEvent`) is provisional but useful for distinguishing hookable harness events from public subscription events. - -A future extension context should expose a harness facade plus a queued-write session facade. The facade must not expose APIs that can deadlock the current event dispatch. In particular, listeners/hooks should not call `waitForIdle()` for the active run; expose a `runWhenIdle(() => Promise)` scheduling API instead. This is future extension-context work; current listeners/hooks receive only payloads and no safe harness facade. - ## Planned session facade Extensions should eventually interact with a harness-scoped `HarnessSession` facade rather than the raw session. The facade should wrap the internal session and enforce harness pending-write ordering semantics. Once this exists, hooks and event listeners can receive a context that exposes the full `AgentHarness` plus the session facade without giving direct access to unordered raw session writes. @@ -236,141 +241,58 @@ npm run coverage:harness ## Implementation todo -This list tracks the remaining work before treating `AgentHarness` as migration-ready. +This list tracks the remaining work before treating `AgentHarness` as migration-ready. Active/planned items are ordered from easiest to hardest. Completed items are archived at the bottom. -### 1. Remove `Agent` dependency from `AgentHarness` +### 1. Add explicit tool registry read/update semantics -Implemented. +Status: In progress -`AgentHarness` now calls `runAgentLoop()` directly instead of owning an internal `Agent` instance. The harness owns active run/abort-controller lifecycle, queue draining, provider stream configuration, event reduction, session persistence, pending write flushing, and save-point snapshot refresh. +Done: -Implemented validation: +- Added `setTools(tools, activeToolNames?)`. +- Added `setActiveTools(toolNames)`. +- Invalid active tool names reject with `AgentHarnessError`. +- Added generic app tool shape via `AgentHarness`. +- Exported `QueueMode` from core types. +- Added `AgentHarnessOptions.steeringMode` and `followUpMode`. +- Added live `getSteeringMode()` / `setSteeringMode()` and `getFollowUpMode()` / `setFollowUpMode()`. -- prompt construction and public runtime config getters/mutators -- steering queue draining and `queue_update` emission -- follow-up queue draining and `queue_update` emission -- `before_agent_start` message ordering and persistence -- abort clearing steer/follow-up queues while preserving `nextTurn` messages -- thrown hook failure cleanup with persisted assistant error messages and settlement -- save-point refresh for model, thinking level, resources, system prompt, and active tools -- pending listener session write ordering after agent-emitted messages -- external `waitForIdle()` waiting for awaited listeners and run settlement -- `tool_call` and `tool_result` hook behavior through the direct loop -- provider stream wrapper behavior in `agent-harness-stream.test.ts` +Remaining: -Remaining lifecycle hardening beyond this refactor is tracked in the final lifecycle hardening suite. +- Add `getTools()` semantics. +- Add `getActiveTools()` semantics. +- Decide and implement tool update observability events. +- Include active-tool-only updates in the runtime config observability plan. -### 2. Finish curated provider/stream configuration +### 2. Design per-`AgentHarness` model registry -Implemented so far: +Status: Planned -- `AgentHarnessOptions.streamOptions` provides curated request configuration. -- `getStreamOptions()` returns a shallow copy of current harness config. -- `setStreamOptions()` replaces current harness config. -- Stream options are snapshotted in `createTurnState()` and applied with `applyTurnState()`. -- `headers` and `metadata` maps are shallow-copied when stream options are copied. -- `sessionId` is derived from `session.getMetadata().id` in the turn snapshot. -- The harness installs its own internal stream wrapper and calls `streamSimple()`. -- The wrapper ignores raw incoming provider options except lifecycle-owned fields that must come from the low-level loop: `signal` and `reasoning`. -- Credentials and auth headers from `getApiKeyAndHeaders()` are resolved per provider request. +Done: -Implemented provider hook behavior: +- Current `setModel()` behavior is preserved. -- `before_provider_request` runs before `streamSimple()` and can patch curated stream options for the current request only. -- `before_provider_payload` maps to the underlying `pi-ai` `onPayload` and can inspect/replace provider-specific payloads. -- `after_provider_response` maps to the underlying `pi-ai` `onResponse` and observes response status/headers before body consumption. -- `AgentHarnessStreamOptionsPatch` has explicit deletion semantics: - - top-level fields present with `undefined` clear that option. - - `headers` and `metadata` patches may set individual keys to `undefined` to delete them. - - `headers: undefined` or `metadata: undefined`, when explicitly present, clears the whole map. -- Current-request stream option merge order is: - 1. snapshotted `streamOptions` - 2. auth headers from `getApiKeyAndHeaders()` - 3. `before_provider_request` patches, in hook registration order -- `before_provider_request` does not patch `reasoning`; add that only if a concrete use case appears. - -Implemented validation: - -- `packages/agent/test/harness/agent-harness-stream.test.ts` uses the `pi-ai` faux provider. -- Tests cover stream option forwarding, auth header merge, request hook patching, request hook deletion semantics, request hook chaining, payload hook chaining, and busy/save-point snapshot behavior. - -### 3. Design per-`AgentHarness` model registry - -Not started. - -Still needed: +Remaining: - Decide how applications supply the model registry. - Decide whether the harness stores concrete `Model` objects, model references, or both. - Validate model selection against the registry. - Define model change semantics during active turns and save points. -- Preserve current `setModel()` behavior until the registry model is designed. -### 4. Design generic hook/event extension mechanism +### 3. Full `AgentHarness` lifecycle/state pass -Current cleanup already done: +Status: In progress -- Removed `AgentHarnessContext`. -- Hooks receive only event payloads. -- `emitHook(event)` derives the hook type from `event.type`. +Done: -Still needed: - -- Define extension/listener context shape. -- Expose a harness facade plus a session facade rather than raw internals. -- The harness facade should expose safe runtime APIs and `runWhenIdle(() => Promise)`; it should not expose active-run `waitForIdle()` to listeners/hooks. -- The session facade should wrap the internal session and participate in pending session write queue semantics so writes remain ordered with agent-emitted messages. -- Decide which public harness APIs are allowed from each hook/event. -- Decide whether hooks can mutate turn snapshots directly or only through explicit hook results/public APIs. -- Clarify event payload semantics versus harness getter semantics. -- Revisit `AgentHarnessOwnEvent` versus `AgentHarnessEvent`. -- Define hook result chaining where it has clean transform semantics: - - `before_provider_request`: each hook receives the stream options produced by previous hooks. - - `before_provider_payload`: each hook receives the payload produced by previous hooks. - - possibly `context`: each hook receives the messages produced by previous hooks. - - possibly `tool_result`: each hook receives the result fields produced by previous hooks. -- Do not chain hooks where semantics are policy-based or ambiguous until explicitly designed, such as `tool_call`, `session_before_compact`, `session_before_tree`, and `before_agent_start`. - -### 5. Add explicit tool registry read/update semantics - -Implemented so far: - -- `setTools(tools, activeToolNames?)` -- `setActiveTools(toolNames)` -- invalid active tool names reject with `AgentHarnessError` -- generic common app tool shape via `AgentHarness` -- `QueueMode` exported from core types -- `AgentHarnessOptions.steeringMode` / `followUpMode` -- live `getSteeringMode()` / `setSteeringMode()` and `getFollowUpMode()` / `setFollowUpMode()` methods -- queue modes are immediate/live, matching coding-agent behavior - -Still needed: - -- Add `getTools()` semantics. -- Add `getActiveTools()` semantics. -- Decide and implement tool update observability events. -- Include active-tool-only updates in the uniform runtime config observability plan. - -### 6. Full `AgentHarness` lifecycle/state pass - -Implemented so far: - -- Removed constructor `void syncFromTree()`. -- Removed `syncFromTree()`. +- Removed constructor `void syncFromTree()`, `syncFromTree()`, `liveOperationId`, and `shell()`. - Added `createTurnState()`, `applyTurnState()`, and `executeTurn()`. -- Low-level `AgentLoopConfig.prepareNextTurn` save-point update exists. -- `prepareNextTurn` updates low-level context/model/thinking-level and harness-applied stream/session snapshot state. -- The loop converts `ThinkingLevel` to provider `reasoning` internally. -- `phase` replaces boolean idle. -- Pending session writes are based on session-entry shapes without generated fields. +- Added explicit `phase` in place of boolean idle state. +- Save points refresh context, model, thinking level, stream options, and session snapshot state. +- Pending session writes use session-entry shapes without generated fields. - Pending session writes flush at save points, settlement, and failure cleanup. -- `steer`, `followUp`, and `nextTurn` accept text plus optional images and create `UserMessage` internally. -- `nextTurn` ordering is fixed: queued messages before the new user message. -- Removed `liveOperationId`. -- Removed `shell()`; use `harness.env`. - -Implemented in the hardening pass: - +- `steer`, `followUp`, and `nextTurn` create user messages from text plus optional images. +- `nextTurn` messages are inserted before the new user prompt. - Structural compaction/tree operations restore phase with `finally`. - Public harness failures normalize subsystem causes to `AgentHarnessError`. - Pending session writes flush one-by-one and are not dropped on failure. @@ -380,7 +302,7 @@ Implemented in the hardening pass: - Idle model/thinking/tool updates validate and persist before committing in-memory state. - `setLeafId()` persists durable `leaf` entries so tree navigation survives storage reopen. -Still needed: +Remaining: - Finalize phase/idle semantics. - Audit whether `settled` can fire too early. @@ -388,72 +310,144 @@ Still needed: - Audit follow-up behavior around `agent_end`. - Implement auto-compaction decision point. - Implement retry handling. -- Verify `before_agent_start` hook semantics against coding-agent: - - current behavior appends returned messages after the user/next-turn prompt messages. - - decide whether replacement, prepend, append, or transform semantics are correct. -- Decide if `before_agent_start` needs more turn info such as tools/tool snippets. -- Document or change timing for model/thinking/stream-option events that may fire before queued session entries persist while busy. +- Verify `before_agent_start` hook semantics against coding-agent. +- Decide whether `before_agent_start` needs more turn info such as tools/tool snippets. +- Document or change runtime config event timing while busy. - Audit `abort()` barrier semantics. -### 7. Complete low-level `Result` cleanup +### 4. Implement generic hook/event extension mechanism -Current hardening pass complete; future items remain as the API evolves. +Status: Designed in [hooks.md](./hooks.md), not implemented -Implemented so far: +Done: -- Added generic `Result` plus helpers. -- Updated `ExecutionEnv` and `NodeExecutionEnv` to return typed results for filesystem/process operations. -- Split filesystem/shell capabilities and moved JSONL session storage/repo onto filesystem picks instead of direct Node imports. -- Added `ExecutionEnv.appendFile()` for streaming append use cases. -- Updated skill and prompt-template loaders to consume `ExecutionEnv` results. -- Updated shell output capture to return a result and use `ExecutionEnv` instead of Node APIs directly, including full-output spill via `appendFile()`. -- Removed `NodeExecutionEnv` from browser-safe root exports; Node-specific callers use the `node` entry point or `harness/env/nodejs.js`. -- Replaced `Buffer` usage in generic truncation utilities with runtime-neutral UTF-8 handling. -- Converted compaction summary helpers to typed result returns and added error-path coverage. -- Expanded `NodeExecutionEnv` tests for file operations, exec errors, aborts, callbacks, timeouts, and shell-output full-output spill. -- Added `readTextLines()` so JSONL metadata loading reads only the header line instead of whole session files. -- Removed no-op abort handling from Node filesystem methods where cancellation is not meaningful while keeping the `FileSystem` interface unchanged. -- Mapped filesystem errors crossing the session boundary to typed `SessionError`. -- Added typed branch-summary errors and cause-aware public harness error normalization. -- Made resource loaders report structured diagnostics for non-`not_found` filesystem failures. +- Removed `AgentHarnessContext`. +- Hooks receive only event payloads. +- `emitHook(event)` derives the hook type from `event.type`. +- Provider request/payload hooks have ordered transform semantics. -Ongoing guardrails: +Remaining: -- Keep low-level capability/helper APIs non-throwing where they return `Result`. -- Keep session storage/repo/session APIs throwing typed `SessionError`. -- Keep structural `AgentHarness` operations rejecting with `AgentHarnessError` for busy, missing-resource, auth, compaction, and branch-summary failures. -- Keep Node-specific APIs isolated under `src/harness/env/nodejs.ts` and Node-backed storage/session implementations, or move those implementations behind explicit Node-only entry points. -- Audit remaining generic harness utilities for Node globals as new APIs are added. -- Audit package exports so browser/generic-JS imports do not pull Node-only modules such as `NodeExecutionEnv`. -- Keep expanding `ExecutionEnv` and shell-output contract tests as the API evolves, especially for non-Node implementations. -- Add tests proving public harness failures reject with `AgentHarnessError` where expected. +- Add `HookEvent`, `ResultOf`, registration options with generic source metadata, and the single `AgentHarnessHooks` implementation. +- Move result chaining out of `AgentHarness` into reducer functions. +- Type-check base harness reducers so every result-producing `AgentHarnessEvent` has reducer semantics. +- Make `AgentHarness` accept and expose the concrete hooks instance with constructor inference for app-specific hooks. +- Define the initial harness/context facades exposed through hook context. +- Preserve current provider hook behavior, including stream option patch deletion semantics. +- Add parity tests for reducer semantics: transform chaining, patch chaining, early block/cancel, cleanup, source metadata, and typed app-specific reducer coverage. -### 8. Later coding-agent migration plan +### 5. Final lifecycle hardening suite -Not started. +Status: Planned -Still needed: +Done: + +- None. + +Remaining: + +- Add broad listener/hook reentrancy tests across relevant events. +- Test runtime config setters from low-level lifecycle events and harness events. +- Test runtime config observability for model, thinking, resources, tools, active tools, and stream options. +- Test resource/tool/model/thinking/stream-option updates during active turns and save points. +- Test session writes from listeners and hooks, including `settled` writes. +- Test queue operations from turn events, tool events, and provider hooks. +- Test rejected structural operations while busy. +- Test abort from listeners/hooks. +- Test getter behavior during active operations. +- Test deterministic ordering of agent-emitted messages and pending listener writes. +- Test no deadlocks when async listeners call harness APIs and await them. +- Test phase cleanup through success, provider error, hook error, abort, compaction, and tree navigation. + +### 6. Later coding-agent migration plan + +Status: Planned + +Done: + +- None. + +Remaining: - Map coding-agent resources to sourced loaders. - Keep app-level resource dedupe/provenance outside the harness. -- Adapt extension loader to the future hook/session facade. +- Adapt extension loading to the future hook/session facade. - Preserve UI/session behavior outside core. -- Move coding-agent stream/auth/retry/header behavior onto the harness stream configuration and provider hooks. +- Move coding-agent stream/auth/retry/header behavior onto harness stream configuration and provider hooks. -### 9. Final lifecycle hardening suite +--- -Before treating `AgentHarness` as migration-ready, add a broad test suite that exercises listeners and hooks closing over the harness and calling public APIs during every relevant event. +## Completed implementation todo -Needs broad tests for: +### 7. Remove `Agent` dependency from `AgentHarness` -- runtime config setters from low-level lifecycle events and harness events -- uniform runtime config observability events for model, thinking, resources, tools, active tools, and stream options -- resource/tool/model/thinking/stream-option updates during active turns and save points -- session writes from listeners and hooks, including writes from `settled` -- queue operations from turn events, tool events, and provider hooks -- rejected structural operations while busy -- abort from listeners/hooks -- getter behavior during active operations -- deterministic ordering of agent-emitted messages and pending listener writes -- no deadlocks when async listeners call harness APIs and await them -- phase cleanup through success, provider error, hook error, abort, compaction, and tree navigation +Status: Done + +Done: + +- `AgentHarness` calls `runAgentLoop()` directly. +- Harness owns run lifecycle, abort controller, queue draining, provider stream config, event reduction, session persistence, pending write flushing, and save-point snapshots. +- Harness tests cover prompt construction, queue draining, abort behavior, save-point refresh, pending write ordering, awaited listener settlement, tool hooks, and provider stream wrapping. + +Remaining: + +- None. + +Notes: + +- Broader listener/hook reentrancy coverage is tracked in item 5. + +### 8. Finish curated provider/stream configuration + +Status: Done + +Done: + +- Added curated `AgentHarnessOptions.streamOptions`, `getStreamOptions()`, and `setStreamOptions()`. +- Stream options, headers, metadata, and derived session id are snapshotted per turn. +- Harness-owned stream wrapper calls `streamSimple()` and keeps lifecycle-owned `signal` and `reasoning` from the low-level loop. +- `getApiKeyAndHeaders()` resolves credentials per provider request. +- `before_provider_request`, `before_provider_payload`, and `after_provider_response` hooks are implemented. +- Stream option patching supports explicit field deletion and ordered hook chaining. +- `agent-harness-stream.test.ts` covers forwarding, auth merge, hook patching/deletion/chaining, payload hooks, and busy/save-point snapshot behavior. + +Remaining: + +- None. + +### 9. Complete low-level `Result` cleanup + +Status: Done + +Done: + +- Added generic `Result` plus helpers. +- Updated `ExecutionEnv` and `NodeExecutionEnv` to return typed results for filesystem/process operations. +- Split filesystem and shell capabilities. +- Moved JSONL session storage/repo onto filesystem picks instead of direct Node imports. +- Added `ExecutionEnv.appendFile()` for streaming append use cases. +- Updated skill and prompt-template loaders to consume `ExecutionEnv` results. +- Updated shell output capture to return a result and use `ExecutionEnv`, including full-output spill via `appendFile()`. +- Removed `NodeExecutionEnv` from browser-safe root exports. +- Replaced `Buffer` usage in generic truncation utilities with runtime-neutral UTF-8 handling. +- Converted compaction and branch-summary helpers to typed result returns. +- Added `readTextLines()` so JSONL metadata loading reads only the header line. +- Removed no-op abort handling from Node filesystem methods where cancellation is not meaningful. +- Mapped filesystem errors crossing the session boundary to typed `SessionError`. +- Added typed branch-summary errors and cause-aware public harness error normalization. +- Resource loaders report structured diagnostics for non-`not_found` filesystem failures. +- Expanded `NodeExecutionEnv` tests for file operations, exec errors, aborts, callbacks, timeouts, and shell-output spill. + +Remaining: + +- None. + +Notes: + +- Keep low-level capability/helper APIs non-throwing where they return `Result`. +- Keep session storage/repo/session APIs throwing typed `SessionError`. +- Keep public structural harness failures normalized to `AgentHarnessError`. +- Keep Node-specific APIs isolated under `src/harness/env/nodejs.ts`, Node-backed storage/session implementations, or explicit Node-only entry points. +- Audit generic harness utilities for Node globals as APIs are added. +- Audit package exports so browser/generic imports do not pull Node-only modules. +- Keep expanding `ExecutionEnv` and shell-output contract tests as APIs evolve. diff --git a/packages/agent/docs/hooks.md b/packages/agent/docs/hooks.md new file mode 100644 index 00000000..c15bb5dc --- /dev/null +++ b/packages/agent/docs/hooks.md @@ -0,0 +1,465 @@ +# AgentHarness hooks design + +This document describes the target hook system for `AgentHarness` and app-specific harness integrations. + +## Goals + +- `AgentHarness` emits hook events and consumes typed results. +- Hook registration, provenance, cleanup, and mutation-chain semantics live in the hooks implementation. +- There is one registration API and one emission API. +- Observational and mutation hooks use the same registration API; the event result type determines whether a handler can return a result. +- Apps can extend the event union, context type, source/provenance type, and reducers without changing `AgentHarness`. +- Resources and tools carry provenance on their app-specific concrete value types. Hook handlers carry provenance as registration sidecar metadata. + +## Value provenance + +For non-hook values, provenance belongs on the app-specific concrete type. + +```ts +interface AppSource { + path: string; + scope: "user" | "project" | "temporary"; +} + +type AppSkill = Skill & { source: AppSource }; +type AppPromptTemplate = PromptTemplate & { source: AppSource }; +type AppTool = AgentTool & { source: AppSource }; +``` + +The harness already accepts generic resource/tool types, so no wrapper such as `{ value, source }` is needed. + +```ts +const harness = new AgentHarness({ + resources: { skills, promptTemplates }, + tools, + // ... +}); +``` + +Loaders such as `loadSourcedSkills()` and `loadSourcedPromptTemplates()` can map source metadata onto the concrete app value type before passing values to the harness. + +## Hook event typing + +Each hook event owns its handler result type through a type-only phantom field. + +```ts +declare const HookResult: unique symbol; + +export interface HookEvent { + type: TType; + readonly [HookResult]?: TResult; +} + +export type ResultOf = TEvent extends { readonly [HookResult]?: infer TResult } ? TResult : void; +``` + +Observational events omit the result type: + +```ts +interface MessageStartEvent extends HookEvent<"message_start"> { + type: "message_start"; + message: AgentMessage; +} +``` + +Mutation/policy events declare their result type: + +```ts +interface ContextEvent extends HookEvent<"context", { messages?: AgentMessage[] }> { + type: "context"; + messages: AgentMessage[]; +} + +interface ToolCallEvent extends HookEvent<"tool_call", { block?: boolean; reason?: string }> { + type: "tool_call"; + toolName: string; + input: Record; +} +``` + +There is no central result map and no event spec table. The event type itself defines the return type handlers may produce. + +## Hook handlers and registration options + +Handlers are plain functions. Provenance and cleanup live on the registration. + +```ts +export type HookCleanup = () => void | Promise; + +export type HookHandler = ( + event: TEvent, + context: TContext, + signal?: AbortSignal, +) => ResultOf | void | Promise | void>; + +export interface HookRegistrationOptions { + source?: TSource; + cleanup?: HookCleanup; +} +``` + +Example: + +```ts +hooks.on( + "context", + (event, context) => ({ messages: injectContext(event.messages, context) }), + { + source: extensionSource, + cleanup: () => cache.dispose(), + }, +); +``` + +The cleanup runs once, either when the returned unregister function is called, or when `clear()` / `dispose()` clears the registration. + +## Reducers + +Result-producing events need reducers. Observational events do not. + +```ts +type ResultfulEvent = TEvent extends HookEvent + ? [TResult] extends [void] + ? never + : TEvent + : never; + +type HookRegistration = { + handler: HookHandler; + source?: TSource; + cleanup?: HookCleanup; + disposed: boolean; + order: number; +}; + +type Reducer = ( + event: TEvent, + registrations: readonly HookRegistration[], + context: TContext, + signal?: AbortSignal, +) => Promise | undefined>; + +type Reducers = { + [TType in ResultfulEvent["type"]]: Reducer< + Extract, { type: TType }>, + TContext, + TSource + >; +}; +``` + +Reducers encode hook semantics, for example: + +- `context`: sequential transform; each handler sees current messages. +- `before_provider_request`: sequential patch/transform; each handler sees current request state. +- `before_provider_payload`: sequential payload transform. +- `before_agent_start`: chain `systemPrompt`; collect injected messages. +- `tool_call`: same mutable event/input visible to later handlers; first `{ block: true }` stops. +- `tool_result`: sequential patch accumulation; each handler sees current patched result. +- `message_end`: sequential message replacement; replacement must keep the original role. +- `session_before_*`: first `{ cancel: true }` stops; otherwise return the last meaningful result. + +Base harness reducers are defined once: + +```ts +const agentHarnessReducers = { + context: reduceContext, + before_provider_request: reduceBeforeProviderRequest, + before_provider_payload: reduceBeforeProviderPayload, + before_agent_start: reduceBeforeAgentStart, + tool_call: reduceToolCall, + tool_result: reduceToolResult, + message_end: reduceMessageEnd, + session_before_compact: reduceFirstCancelOrLast, + session_before_tree: reduceFirstCancelOrLast, +} satisfies Reducers; +``` + +If `AgentHarnessEvent` gains a new result-producing event, TypeScript forces the reducer table to be updated. + +## Single hooks implementation + +The hooks implementation stores registrations and runs reducers. + +```ts +class AgentHarnessHooks< + TEvent extends HookEvent, + TContext, + TSource = unknown, +> { + context: TContext; + + constructor( + context: TContext, + extraReducers?: ExtraReducers, + ) { + this.context = context; + this.reducers = { + ...agentHarnessReducers, + ...extraReducers, + } as Reducers; + } + + setContext(context: TContext): void { + this.context = context; + } + + on( + type: TType, + handler: HookHandler, TContext>, + options?: HookRegistrationOptions, + ): () => Promise { + // Store the registration and return unregister. + } + + async emit( + event: TEmittedEvent, + signal?: AbortSignal, + ): Promise | undefined> { + const registrations = this.getRegistrations(event.type); + const reducer = this.reducers[event.type as keyof typeof this.reducers]; + + if (reducer) { + return reducer(event as never, registrations as never, this.context, signal) as Promise< + ResultOf | undefined + >; + } + + for (const registration of registrations) { + await registration.handler(event, this.context, signal); + } + + return undefined; + } + + async clear(): Promise { + // Remove all registrations and run remaining cleanups once in reverse registration order. + } + + dispose(): Promise { + return this.clear(); + } +} +``` + +Public API: + +```ts +hooks.on(...); +hooks.emit(...); +hooks.clear(); +hooks.dispose(); +``` + +There is no wildcard subscription and no separate observer API. + +## App-specific events and reducers + +Apps extend the event union. + +Observational app events need no reducer: + +```ts +interface SessionStartEvent extends HookEvent<"session_start"> { + type: "session_start"; + reason: "startup" | "reload" | "new" | "resume" | "fork"; +} +``` + +Result-producing app events need an extra reducer: + +```ts +type InputResult = + | { action: "continue" } + | { action: "transform"; text: string; images?: ImageContent[] } + | { action: "handled" }; + +interface InputEvent extends HookEvent<"input", InputResult> { + type: "input"; + text: string; + images?: ImageContent[]; + source: "interactive" | "rpc" | "extension"; +} +``` + +```ts +const codingAgentExtraReducers = { + input: reduceInput, + user_bash: reduceFirstResult, + resources_discover: reduceResourcesDiscover, + session_before_switch: reduceFirstCancelOrLast, + session_before_fork: reduceFirstCancelOrLast, +} satisfies ExtraReducers; +``` + +Base reducers are included by the hooks constructor. Apps only provide reducers for app-specific result-producing events. + +```ts +type CodingAgentEvent = + | AgentHarnessEvent + | SessionStartEvent + | SessionShutdownEvent + | InputEvent + | UserBashEvent + | ResourcesDiscoverEvent; + +const hooks = new AgentHarnessHooks( + context, + codingAgentExtraReducers, +); +``` + +## Harness typing + +`AgentHarness` stores and exposes the concrete hooks object. + +```ts +type DefaultHooks = AgentHarnessHooks< + AgentHarnessEvent, + undefined, + unknown +>; + +class AgentHarness< + TSkill extends Skill = Skill, + TPromptTemplate extends PromptTemplate = PromptTemplate, + TTool extends AgentTool = AgentTool, + THooks = DefaultHooks, +> { + readonly hooks: THooks; + + constructor(options: AgentHarnessOptions) { + this.hooks = options.hooks ?? createDefaultHooks(); + } +} +``` + +When custom hooks are passed, TypeScript infers `THooks` from `options.hooks`. + +```ts +const hooks = new CodingAgentHooks(context, codingAgentExtraReducers); + +const harness = new AgentHarness({ + model, + session, + hooks, + resources, + tools, +}); + +harness.hooks; // CodingAgentHooks +``` + +Custom app APIs live on `harness.hooks`; they are not proxied onto `AgentHarness`. + +## Harness usage + +The harness only emits events and uses typed results. + +```ts +await this.hooks.emit({ type: "message_start", message }, signal); +``` + +```ts +const result = await this.hooks.emit({ type: "context", messages }, signal); +messages = result?.messages ?? messages; +``` + +```ts +const result = await this.hooks.emit({ type: "tool_call", toolName, input }, signal); +if (result?.block) return blockedToolResult(result.reason); +``` + +`AgentHarness` does not store handlers and does not implement hook chaining semantics. + +## Context model + +Context is a plain object owned by the hooks implementation. + +```ts +hooks.setContext(nextContext); +``` + +Per-run `AbortSignal` is passed separately to `emit()` and handlers. + +Dynamic app state should be exposed through small facades instead of late-bound getter mazes. + +Example app context: + +```ts +interface CodingAgentContext { + harness: HarnessFacade; + session: SessionFacade; + ui: UiFacade; + models: ModelFacade; +} +``` + +The hook context should not expose `waitForIdle()` to hook handlers. A future facade can expose `runWhenIdle(() => Promise)` for safe deferred work. + +## Cleanup semantics + +Each registration owns at most one cleanup. + +- Manual unregister removes the registration and runs its cleanup once. +- `clear()` removes all remaining registrations and runs their cleanups once. +- `dispose()` calls `clear()`. +- Cleanup order is reverse registration order. +- Cleanup errors are collected; cleanup continues; `clear()` throws an aggregate error if any cleanup failed. + +## Error policy + +The base hooks implementation can throw handler errors by default. + +App-specific hooks that load untrusted/user extensions should use a continue-and-report policy. Reducers receive registration source metadata so they can report errors with provenance: + +```ts +for (const registration of registrations) { + try { + const result = await registration.handler(event, context, signal); + // apply result + } catch (error) { + reportHookError({ + event: event.type, + source: registration.source, + error, + }); + } +} +``` + +## Extension loading sketch + +An app-level extension host owns extension loading and non-hook registries. The harness only receives hooks. + +```ts +class ExtensionHost { + constructor(private readonly hooks: AgentHarnessHooks) {} + + async load(paths: string[]): Promise { + for (const path of paths) { + const extension = await loadExtension(path); + const source = createExtensionSource(path); + + const api = { + on: (type, handler, cleanup) => { + this.hooks.on(type, handler, { source, cleanup }); + }, + registerTool: (tool) => { + this.tools.set(tool.name, { ...tool, source }); + }, + }; + + await extension(api); + } + } + + async clear(): Promise { + this.tools.clear(); + this.commands.clear(); + await this.hooks.clear(); + } +} +``` + +Non-hook registries, such as tools, commands, flags, shortcuts, message renderers, providers, and OAuth providers, remain app-level concerns. From b9ae2590f51415edf2b5a13b98ab10132a8aa4cf Mon Sep 17 00:00:00 2001 From: Aliou Diallo Date: Sat, 16 May 2026 01:49:04 +0200 Subject: [PATCH 049/166] docs(coding-agent): document overflow normalization for custom providers Adds a Context Overflow Errors subsection to custom-provider.md showing how a custom provider extension can rewrite unrecognized overflow errors via a message_end handler so pi's native compact-and-retry path triggers. Includes guard rails for provider scoping, idempotency, and avoiding rate-limit phrases. --- packages/coding-agent/docs/custom-provider.md | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/packages/coding-agent/docs/custom-provider.md b/packages/coding-agent/docs/custom-provider.md index be80ade4..09eb6e6e 100644 --- a/packages/coding-agent/docs/custom-provider.md +++ b/packages/coding-agent/docs/custom-provider.md @@ -23,6 +23,7 @@ See these complete provider examples: - [Unregister Provider](#unregister-provider) - [OAuth Support](#oauth-support) - [Custom Streaming API](#custom-streaming-api) +- [Context Overflow Errors](#context-overflow-errors) - [Testing Your Implementation](#testing-your-implementation) - [Config Reference](#config-reference) - [Model Definition Reference](#model-definition-reference) @@ -506,6 +507,60 @@ output.usage.totalTokens = output.usage.input + output.usage.output + calculateCost(model, output.usage); ``` +### Context Overflow Errors + +When a request exceeds the model's context window, pi can recover automatically by compacting the conversation and retrying. This recovery only kicks in if pi recognizes the failure as an overflow. + +Detection runs on the finalized assistant message: + +- `stopReason === "error"` +- `errorMessage` matches one of pi's known overflow patterns (see [`packages/ai/src/utils/overflow.ts`](https://github.com/earendil-works/pi-mono/blob/main/packages/ai/src/utils/overflow.ts)) + +If your provider returns overflow errors with a message pi does not recognize, normalize the error from the same extension that registers the provider. Use a `message_end` handler to rewrite the assistant message so its `errorMessage` starts with a phrase pi recognizes. The generic fallback `context_length_exceeded` is the safest choice. + +```typescript +const MY_PROVIDER_OVERFLOW_PATTERN = /your provider's overflow phrase/i; + +export default function (pi: ExtensionAPI) { + pi.registerProvider("my-provider", { /* ... */ }); + + pi.on("message_end", (event, ctx) => { + const message = event.message; + if (message.role !== "assistant") return; + if (message.stopReason !== "error") return; + if ( + message.provider !== "my-provider" && + ctx.model?.provider !== "my-provider" + ) + return; + + const errorMessage = message.errorMessage ?? ""; + if (errorMessage.includes("context_length_exceeded")) return; + if (!MY_PROVIDER_OVERFLOW_PATTERN.test(errorMessage)) return; + + return { + message: { + ...message, + errorMessage: `context_length_exceeded: ${errorMessage}`, + }, + }; + }); +} +``` + +`message_end` runs before pi tracks the assistant message for auto-compaction, so the rewritten `errorMessage` is what pi checks. With this in place, pi will: + +1. Detect the overflow from `errorMessage`. +2. Drop the failed assistant message from live context. +3. Run compaction. +4. Retry the request once. + +Guard the rewrite carefully: + +- Scope it to your provider (`message.provider` and `ctx.model?.provider`) so unrelated errors from other providers are untouched. +- Match a provider-specific pattern, not pi's generic overflow patterns. Rewriting rate-limit or throttling errors (`rate limit`, `too many requests`) would falsely trigger compaction instead of pi's normal retry-with-backoff path. +- Skip when `errorMessage` already includes `context_length_exceeded` so the handler is idempotent. + ### Registration Register your stream function: From 87881ca686a49004df7671125626bf9314e60469 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sat, 16 May 2026 11:50:24 +0200 Subject: [PATCH 050/166] fix(ai): preserve OpenRouter cached token semantics --- .../ai/src/providers/openai-completions.ts | 18 +++++++++--------- .../openai-completions-tool-choice.test.ts | 14 ++++++++------ 2 files changed, 17 insertions(+), 15 deletions(-) diff --git a/packages/ai/src/providers/openai-completions.ts b/packages/ai/src/providers/openai-completions.ts index 01bc6e78..de4c5239 100644 --- a/packages/ai/src/providers/openai-completions.ts +++ b/packages/ai/src/providers/openai-completions.ts @@ -997,17 +997,17 @@ function parseChunkUsage( model: Model<"openai-completions">, ): AssistantMessage["usage"] { const promptTokens = rawUsage.prompt_tokens || 0; - const reportedCachedTokens = rawUsage.prompt_tokens_details?.cached_tokens ?? rawUsage.prompt_cache_hit_tokens ?? 0; + const cacheReadTokens = rawUsage.prompt_tokens_details?.cached_tokens ?? rawUsage.prompt_cache_hit_tokens ?? 0; const cacheWriteTokens = rawUsage.prompt_tokens_details?.cache_write_tokens || 0; - // Normalize to pi-ai semantics: - // - cacheRead: hits from cache created by previous requests only - // - cacheWrite: tokens written to cache in this request - // Some OpenAI-compatible providers (observed on OpenRouter) report cached_tokens - // as (previous hits + current writes). In that case, remove cacheWrite from cacheRead. - const cacheReadTokens = - cacheWriteTokens > 0 ? Math.max(0, reportedCachedTokens - cacheWriteTokens) : reportedCachedTokens; - + // Follow documented OpenAI/OpenRouter semantics: cached_tokens is cache-read + // tokens (hits). OpenAI does not document or emit cache_write_tokens, but + // OpenRouter-compatible providers can include it as a separate write count. + // OpenRouter's own provider/tests affirm the separate mapping: + // https://github.com/OpenRouterTeam/ai-sdk-provider/pull/409 + // Do not subtract writes from cached_tokens, otherwise spec-compliant + // providers are under-reported. DS4 mirrors this contract too: + // https://github.com/antirez/ds4/pull/29 const input = Math.max(0, promptTokens - cacheReadTokens - cacheWriteTokens); // OpenAI completion_tokens already includes reasoning_tokens. const outputTokens = rawUsage.completion_tokens || 0; diff --git a/packages/ai/test/openai-completions-tool-choice.test.ts b/packages/ai/test/openai-completions-tool-choice.test.ts index c9c8ec9f..7b79b2da 100644 --- a/packages/ai/test/openai-completions-tool-choice.test.ts +++ b/packages/ai/test/openai-completions-tool-choice.test.ts @@ -819,7 +819,7 @@ describe("openai-completions tool_choice", () => { expect(response.usage.totalTokens).toBe(43); }); - it("preserves prompt_tokens_details.cache_write_tokens from chunk usage", async () => { + it("preserves prompt_tokens_details cache read/write fields from chunk usage", async () => { mockState.chunks = [ { id: "chatcmpl-cache-write", @@ -853,13 +853,14 @@ describe("openai-completions tool_choice", () => { { apiKey: "test" }, ).result(); - expect(response.usage.input).toBe(50); - expect(response.usage.cacheRead).toBe(20); + // cached_tokens is documented as cache reads; cache_write_tokens is separate. + expect(response.usage.input).toBe(20); + expect(response.usage.cacheRead).toBe(50); expect(response.usage.cacheWrite).toBe(30); expect(response.usage.totalTokens).toBe(105); }); - it("preserves prompt_tokens_details.cache_write_tokens from choice usage fallback", async () => { + it("preserves prompt_tokens_details cache read/write fields from choice usage fallback", async () => { mockState.chunks = [ { id: "chatcmpl-cache-write-choice", @@ -898,8 +899,9 @@ describe("openai-completions tool_choice", () => { { apiKey: "test" }, ).result(); - expect(response.usage.input).toBe(50); - expect(response.usage.cacheRead).toBe(20); + // cached_tokens is documented as cache reads; cache_write_tokens is separate. + expect(response.usage.input).toBe(20); + expect(response.usage.cacheRead).toBe(50); expect(response.usage.cacheWrite).toBe(30); expect(response.usage.totalTokens).toBe(105); }); From f8d2c8aa6ec33974c2ac9efcd436dc47508fdc18 Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Sat, 16 May 2026 22:07:04 +0200 Subject: [PATCH 051/166] docs(agent): organize harness design todos --- packages/agent/docs/agent-harness.md | 42 +- packages/agent/docs/durable-harness.md | 181 +++++++ packages/agent/docs/hooks.md | 704 ++++++++++++------------- packages/agent/docs/observability.md | 376 +++++++++++++ 4 files changed, 935 insertions(+), 368 deletions(-) create mode 100644 packages/agent/docs/durable-harness.md create mode 100644 packages/agent/docs/observability.md diff --git a/packages/agent/docs/agent-harness.md b/packages/agent/docs/agent-harness.md index 18749d71..7ac55457 100644 --- a/packages/agent/docs/agent-harness.md +++ b/packages/agent/docs/agent-harness.md @@ -264,6 +264,10 @@ Remaining: - Decide and implement tool update observability events. - Include active-tool-only updates in the runtime config observability plan. +Notes: + +- Observability design: [observability.md](./observability.md) + ### 2. Design per-`AgentHarness` model registry Status: Planned @@ -336,7 +340,33 @@ Remaining: - Preserve current provider hook behavior, including stream option patch deletion semantics. - Add parity tests for reducer semantics: transform chaining, patch chaining, early block/cancel, cleanup, source metadata, and typed app-specific reducer coverage. -### 5. Final lifecycle hardening suite +Notes: + +- Hook design: [hooks.md](./hooks.md) + +### 5. Spike semi-durable harness/session recovery + +Status: Planned + +Done: + +- Wrote durability design: [durable-harness.md](./durable-harness.md) + +Remaining: + +- Decide whether session owns all durable harness state or whether any sidecars are needed for large blobs. +- Define durable entries for queues, pending writes, operations, turns, provider requests, and tool calls. +- Define resume requirements for app-provided tools, models, extensions, resources, hooks, and auth providers. +- Define conservative recovery policy for unfinished agent turns, provider requests, tool calls, compaction, and tree navigation. +- Prototype reducer-based recovery from session entries. +- Decide whether interrupted operations append user-visible messages or only internal operation entries. + +Notes: + +- Provider streams are not resumable; recovery should restart from durable boundaries or mark operations interrupted. +- Unfinished tool calls are unsafe to retry unless tools declare idempotent/retry-safe behavior. + +### 6. Final lifecycle hardening suite Status: Planned @@ -359,7 +389,7 @@ Remaining: - Test no deadlocks when async listeners call harness APIs and await them. - Test phase cleanup through success, provider error, hook error, abort, compaction, and tree navigation. -### 6. Later coding-agent migration plan +### 7. Later coding-agent migration plan Status: Planned @@ -379,7 +409,7 @@ Remaining: ## Completed implementation todo -### 7. Remove `Agent` dependency from `AgentHarness` +### 8. Remove `Agent` dependency from `AgentHarness` Status: Done @@ -395,9 +425,9 @@ Remaining: Notes: -- Broader listener/hook reentrancy coverage is tracked in item 5. +- Broader listener/hook reentrancy coverage is tracked in item 6. -### 8. Finish curated provider/stream configuration +### 9. Finish curated provider/stream configuration Status: Done @@ -415,7 +445,7 @@ Remaining: - None. -### 9. Complete low-level `Result` cleanup +### 10. Complete low-level `Result` cleanup Status: Done diff --git a/packages/agent/docs/durable-harness.md b/packages/agent/docs/durable-harness.md new file mode 100644 index 00000000..64e313df --- /dev/null +++ b/packages/agent/docs/durable-harness.md @@ -0,0 +1,181 @@ +# Durable AgentHarness and session design + + + +Durable AgentHarness / session design notes. + +## Framing + +A fully durable `AgentHarness` is not realistic by itself because important dependencies are runtime JS supplied by the host app: + +- tool implementations +- model/auth providers +- extensions and hook handlers +- resource loaders +- system-prompt callbacks/modifiers + +The practical target is a semi-durable harness: + +- session is the durable append-only state tree +- harness persists the state it owns into session entries +- the host app is responsible for recreating compatible non-persistable dependencies on resume +- recovery restarts from durable boundaries, not from an in-flight provider stream + +## Session owns durable state + +Treat session as all durable agent state, not just transcript history. + +Existing session state already includes harness state: + +- model changes +- thinking-level changes +- leaf entries +- labels +- compactions and branch summaries +- custom messages and custom entries + +That suggests continuing with one durable session log rather than adding harness sidecars. Sidecars may still be useful for large blobs, but the session entry should remain the source-of-truth reference. + +## What the app must provide on resume + +The app must recreate compatible runtime dependencies: + +- model registry / model objects +- tool registry +- extension set, versions, and ordering +- resource loaders +- system prompt providers/hooks +- auth providers +- app-specific hooks + +Harness can validate stable IDs/versions/hashes when available, but it cannot serialize these dependencies itself. + +## What harness should persist + +Minimum useful durability entries: + +- queued steer/followUp/nextTurn messages +- queue consumption tied to a turn +- pending session writes accepted during active operations +- pending write application status +- operation start/finish/interruption +- turn start/finish +- provider request start/finish, if needed for recovery diagnostics +- tool call start/finish, if we want safe tool recovery + +Potential entries: + +```ts +type DurableHarnessEntry = + | QueueEnqueuedEntry + | QueueConsumedEntry + | PendingWriteEnqueuedEntry + | PendingWriteAppliedEntry + | OperationStartedEntry + | OperationFinishedEntry + | OperationInterruptedEntry + | TurnStartedEntry + | TurnFinishedEntry + | ProviderRequestStartedEntry + | ProviderRequestFinishedEntry + | ToolCallStartedEntry + | ToolCallFinishedEntry; +``` + +Every accepted mutation must be durable before the public API resolves. + +## Recovery model + +On startup: + +1. Host app registers tools/models/extensions/resources/auth/hooks. +2. Harness opens session. +3. Harness reduces session entries into: + - current leaf + - conversation branch + - harness config + - queues + - pending writes + - active operation/turn/tool state +4. Harness validates required runtime dependencies. +5. Harness reconciles unfinished operation state. + +Provider streams are not resumable. Recovery can only retry from a durable boundary or mark the operation interrupted. + +## Recovery policies + +Default conservative policy: + +- unfinished agent turn: mark interrupted, preserve durable queues/pending writes, return idle +- unfinished provider request: mark interrupted; do not retry automatically +- unfinished tool call: append interrupted/error tool result; retry only if the tool declares retry-safe/idempotent +- unfinished compaction: rerun if no compaction entry exists +- unfinished branch summary/tree navigation: rerun/apply missing summary or leaf entries if safe + +Optional policy: + +```ts +recovery: "mark_interrupted" | "retry_unfinished" +``` + +`retry_unfinished` must be guarded around non-idempotent tool calls. + +## Critical scenarios + +### Queues + +- Crash before `queue_enqueued`: message was not accepted. +- Crash after `queue_enqueued`: message is restored. +- Crash after queue drain but before durable turn record: risk of loss/duplication. +- Required invariant: consumed queue IDs must be recorded in `turn_started` or equivalent before they are considered consumed. + +### Pending writes + +- Crash before `pending_write_enqueued`: write was not accepted. +- Crash after enqueue before apply: recovery applies it. +- Crash after apply before applied marker: deterministic target entry IDs let recovery detect the entry already exists and mark it applied. + +### Agent loop turn + +- Crash before provider request: retry or mark interrupted. +- Crash during provider request: mark interrupted by default. +- Crash after provider response before assistant message persisted: response is lost unless provider result was journaled. +- Crash after assistant message persisted: recover from durable message. + +### Tool calls + +- Crash after tool call starts but before result: external side effects may already have happened. +- Default recovery should not rerun non-idempotent tools. +- Tool calls need stable IDs and retry-safety metadata for automatic recovery. + +### Compaction + +- Crash before summary generation: rerun preparation/summary. +- Crash after generated summary but before compaction entry: rerun unless summary was journaled. +- Crash after compaction entry: operation is complete; append finish marker if missing. + +### Branch summary / tree navigation + +- Crash before summary: rerun or mark interrupted. +- Crash after summary entry before leaf entry: append missing leaf entry. +- Crash after leaf entry: operation is complete; append finish marker if missing. + +## Minimum viable spike + +1. Add durable queue entries. +2. Add durable pending write entries with deterministic target IDs. +3. Add operation start/finish/interrupted entries. +4. Add turn start with consumed queue IDs. +5. Recover by reducing the session log. +6. Mark unfinished agent turns interrupted by default. +7. Rerun unfinished compaction/tree operations only when no final entry exists. +8. Do not retry unfinished tool calls unless tool metadata says retry-safe. + +## Open questions + +- Which harness config entries should move into session first: tools, active tools, resources, stream options, system prompt refs? +- Should resolved system prompt text be snapshotted per turn for audit/debug? +- Do we require strict dependency ID/version matching on resume? +- How much provider request data should be journaled? +- Should recovery append user-visible assistant interruption messages or only internal operation entries? +- Should storage support truncating a final partial JSONL line during recovery? diff --git a/packages/agent/docs/hooks.md b/packages/agent/docs/hooks.md index c15bb5dc..de7230e3 100644 --- a/packages/agent/docs/hooks.md +++ b/packages/agent/docs/hooks.md @@ -1,68 +1,37 @@ # AgentHarness hooks design -This document describes the target hook system for `AgentHarness` and app-specific harness integrations. + -## Goals +Final design. -- `AgentHarness` emits hook events and consumes typed results. -- Hook registration, provenance, cleanup, and mutation-chain semantics live in the hooks implementation. -- There is one registration API and one emission API. -- Observational and mutation hooks use the same registration API; the event result type determines whether a handler can return a result. -- Apps can extend the event union, context type, source/provenance type, and reducers without changing `AgentHarness`. -- Resources and tools carry provenance on their app-specific concrete value types. Hook handlers carry provenance as registration sidecar metadata. +## Core model -## Value provenance - -For non-hook values, provenance belongs on the app-specific concrete type. - -```ts -interface AppSource { - path: string; - scope: "user" | "project" | "temporary"; -} - -type AppSkill = Skill & { source: AppSource }; -type AppPromptTemplate = PromptTemplate & { source: AppSource }; -type AppTool = AgentTool & { source: AppSource }; -``` - -The harness already accepts generic resource/tool types, so no wrapper such as `{ value, source }` is needed. - -```ts -const harness = new AgentHarness({ - resources: { skills, promptTemplates }, - tools, - // ... -}); -``` - -Loaders such as `loadSourcedSkills()` and `loadSourcedPromptTemplates()` can map source metadata onto the concrete app value type before passing values to the harness. - -## Hook event typing - -Each hook event owns its handler result type through a type-only phantom field. +Events carry their result type as a type-only phantom: ```ts declare const HookResult: unique symbol; -export interface HookEvent { +interface HookEvent { type: TType; readonly [HookResult]?: TResult; } -export type ResultOf = TEvent extends { readonly [HookResult]?: infer TResult } ? TResult : void; +type ResultOf = E extends { readonly [HookResult]?: infer R } ? R : void; + +type HookHandler = ( + event: E, + ctx: Ctx, + signal?: AbortSignal, +) => ResultOf | void | Promise | void>; + +type HookObserver = ( + event: E, + ctx: Ctx, + signal?: AbortSignal, +) => void | Promise; ``` -Observational events omit the result type: - -```ts -interface MessageStartEvent extends HookEvent<"message_start"> { - type: "message_start"; - message: AgentMessage; -} -``` - -Mutation/policy events declare their result type: +Example: ```ts interface ContextEvent extends HookEvent<"context", { messages?: AgentMessage[] }> { @@ -75,319 +44,274 @@ interface ToolCallEvent extends HookEvent<"tool_call", { block?: boolean; reason toolName: string; input: Record; } -``` -There is no central result map and no event spec table. The event type itself defines the return type handlers may produce. - -## Hook handlers and registration options - -Handlers are plain functions. Provenance and cleanup live on the registration. - -```ts -export type HookCleanup = () => void | Promise; - -export type HookHandler = ( - event: TEvent, - context: TContext, - signal?: AbortSignal, -) => ResultOf | void | Promise | void>; - -export interface HookRegistrationOptions { - source?: TSource; - cleanup?: HookCleanup; +interface MessageEndEvent extends HookEvent<"message_end"> { + type: "message_end"; + message: AgentMessage; } ``` -Example: +No result map. No spec table. The event type defines its own result. + +## Hooks interface ```ts -hooks.on( - "context", - (event, context) => ({ messages: injectContext(event.messages, context) }), - { - source: extensionSource, - cleanup: () => cache.dispose(), - }, -); -``` +interface AgentHarnessHooks, Ctx> { + context: Ctx; -The cleanup runs once, either when the returned unregister function is called, or when `clear()` / `dispose()` clears the registration. + setContext(ctx: Ctx): void; -## Reducers + observe(handler: HookObserver): () => void; -Result-producing events need reducers. Observational events do not. - -```ts -type ResultfulEvent = TEvent extends HookEvent - ? [TResult] extends [void] - ? never - : TEvent - : never; - -type HookRegistration = { - handler: HookHandler; - source?: TSource; - cleanup?: HookCleanup; - disposed: boolean; - order: number; -}; - -type Reducer = ( - event: TEvent, - registrations: readonly HookRegistration[], - context: TContext, - signal?: AbortSignal, -) => Promise | undefined>; - -type Reducers = { - [TType in ResultfulEvent["type"]]: Reducer< - Extract, { type: TType }>, - TContext, - TSource - >; -}; -``` - -Reducers encode hook semantics, for example: - -- `context`: sequential transform; each handler sees current messages. -- `before_provider_request`: sequential patch/transform; each handler sees current request state. -- `before_provider_payload`: sequential payload transform. -- `before_agent_start`: chain `systemPrompt`; collect injected messages. -- `tool_call`: same mutable event/input visible to later handlers; first `{ block: true }` stops. -- `tool_result`: sequential patch accumulation; each handler sees current patched result. -- `message_end`: sequential message replacement; replacement must keep the original role. -- `session_before_*`: first `{ cancel: true }` stops; otherwise return the last meaningful result. - -Base harness reducers are defined once: - -```ts -const agentHarnessReducers = { - context: reduceContext, - before_provider_request: reduceBeforeProviderRequest, - before_provider_payload: reduceBeforeProviderPayload, - before_agent_start: reduceBeforeAgentStart, - tool_call: reduceToolCall, - tool_result: reduceToolResult, - message_end: reduceMessageEnd, - session_before_compact: reduceFirstCancelOrLast, - session_before_tree: reduceFirstCancelOrLast, -} satisfies Reducers; -``` - -If `AgentHarnessEvent` gains a new result-producing event, TypeScript forces the reducer table to be updated. - -## Single hooks implementation - -The hooks implementation stores registrations and runs reducers. - -```ts -class AgentHarnessHooks< - TEvent extends HookEvent, - TContext, - TSource = unknown, -> { - context: TContext; - - constructor( - context: TContext, - extraReducers?: ExtraReducers, - ) { - this.context = context; - this.reducers = { - ...agentHarnessReducers, - ...extraReducers, - } as Reducers; - } - - setContext(context: TContext): void { - this.context = context; - } - - on( + on( type: TType, - handler: HookHandler, TContext>, - options?: HookRegistrationOptions, - ): () => Promise { - // Store the registration and return unregister. - } + handler: HookHandler, Ctx>, + ): () => void; - async emit( - event: TEmittedEvent, + emit( + event: TEvent, signal?: AbortSignal, - ): Promise | undefined> { - const registrations = this.getRegistrations(event.type); - const reducer = this.reducers[event.type as keyof typeof this.reducers]; + ): Promise | undefined>; - if (reducer) { - return reducer(event as never, registrations as never, this.context, signal) as Promise< - ResultOf | undefined - >; + addCleanup(cleanup: () => void | Promise): () => void; + + clear(): Promise; + dispose(): Promise; +} +``` + +Important split: + +- `observe()` sees all events, read-only, return ignored. +- `on(type, handler)` participates in that event’s semantics. +- `emit(event)` is the only thing `AgentHarness` calls. +- `clear()` removes observers/handlers and runs cleanups. + +## Default implementation internals + +```ts +class DefaultAgentHarnessHooks, Ctx> + implements AgentHarnessHooks { + context: Ctx; + + private observers = new Set>(); + private handlers = new Map>>(); + private cleanups = new Set<() => void | Promise>(); + + constructor(ctx: Ctx) { + this.context = ctx; + } + + setContext(ctx: Ctx): void { + this.context = ctx; + } + + observe(handler: HookObserver): () => void { + this.observers.add(handler); + return () => this.observers.delete(handler); + } + + on(type, handler): () => void { + let handlers = this.handlers.get(type); + if (!handlers) { + handlers = new Set(); + this.handlers.set(type, handlers); + } + handlers.add(handler); + return () => handlers.delete(handler); + } + + async emit(event, signal?) { + for (const observer of this.observers) { + await observer(event, this.context, signal); } - for (const registration of registrations) { - await registration.handler(event, this.context, signal); + switch (event.type) { + case "context": + return this.emitContext(event, signal); + case "before_provider_request": + return this.emitBeforeProviderRequest(event, signal); + case "before_provider_payload": + return this.emitBeforeProviderPayload(event, signal); + case "before_agent_start": + return this.emitBeforeAgentStart(event, signal); + case "tool_call": + return this.emitToolCall(event, signal); + case "tool_result": + return this.emitToolResult(event, signal); + case "session_before_compact": + case "session_before_tree": + return this.emitFirstCancelOrLast(event, signal); + default: + await this.emitObservationHandlers(event, signal); + return undefined; } - - return undefined; - } - - async clear(): Promise { - // Remove all registrations and run remaining cleanups once in reverse registration order. - } - - dispose(): Promise { - return this.clear(); } } ``` -Public API: +Internal casts are acceptable inside the implementation because `Map` loses specificity. Public API remains typed. + +## Mutation semantics + +### Observation ```ts -hooks.on(...); -hooks.emit(...); -hooks.clear(); -hooks.dispose(); +await hooks.emit({ type: "message_end", message }, signal); ``` -There is no wildcard subscription and no separate observer API. +Observers run. `message_end` handlers run. Return ignored unless that event later gets a result type. -## App-specific events and reducers +### Context transform -Apps extend the event union. - -Observational app events need no reducer: +Handlers run in order. Each sees current messages. ```ts -interface SessionStartEvent extends HookEvent<"session_start"> { - type: "session_start"; - reason: "startup" | "reload" | "new" | "resume" | "fork"; -} -``` +let current = event; -Result-producing app events need an extra reducer: - -```ts -type InputResult = - | { action: "continue" } - | { action: "transform"; text: string; images?: ImageContent[] } - | { action: "handled" }; - -interface InputEvent extends HookEvent<"input", InputResult> { - type: "input"; - text: string; - images?: ImageContent[]; - source: "interactive" | "rpc" | "extension"; -} -``` - -```ts -const codingAgentExtraReducers = { - input: reduceInput, - user_bash: reduceFirstResult, - resources_discover: reduceResourcesDiscover, - session_before_switch: reduceFirstCancelOrLast, - session_before_fork: reduceFirstCancelOrLast, -} satisfies ExtraReducers; -``` - -Base reducers are included by the hooks constructor. Apps only provide reducers for app-specific result-producing events. - -```ts -type CodingAgentEvent = - | AgentHarnessEvent - | SessionStartEvent - | SessionShutdownEvent - | InputEvent - | UserBashEvent - | ResourcesDiscoverEvent; - -const hooks = new AgentHarnessHooks( - context, - codingAgentExtraReducers, -); -``` - -## Harness typing - -`AgentHarness` stores and exposes the concrete hooks object. - -```ts -type DefaultHooks = AgentHarnessHooks< - AgentHarnessEvent, - undefined, - unknown ->; - -class AgentHarness< - TSkill extends Skill = Skill, - TPromptTemplate extends PromptTemplate = PromptTemplate, - TTool extends AgentTool = AgentTool, - THooks = DefaultHooks, -> { - readonly hooks: THooks; - - constructor(options: AgentHarnessOptions) { - this.hooks = options.hooks ?? createDefaultHooks(); +for (const handler of handlers("context")) { + const result = await handler(current, ctx, signal); + if (result?.messages) { + current = { ...current, messages: result.messages }; } } + +return current.messages === event.messages ? undefined : { messages: current.messages }; ``` -When custom hooks are passed, TypeScript infers `THooks` from `options.hooks`. +### Provider request / payload + +Sequential transform. Each handler sees previous output. ```ts -const hooks = new CodingAgentHooks(context, codingAgentExtraReducers); +let current = event; -const harness = new AgentHarness({ - model, - session, - hooks, - resources, - tools, -}); +for (const handler of handlers("before_provider_payload")) { + const result = await handler(current, ctx, signal); + if (result !== undefined) { + current = { ...current, payload: result.payload }; + } +} -harness.hooks; // CodingAgentHooks +return changed ? { payload: current.payload } : undefined; ``` -Custom app APIs live on `harness.hooks`; they are not proxied onto `AgentHarness`. +### Before agent start + +Collect injected messages, chain system prompt. + +```ts +let systemPrompt = event.systemPrompt; +const messages = []; + +for (const handler of handlers("before_agent_start")) { + const result = await handler({ ...event, systemPrompt }, ctx, signal); + if (result?.messages) messages.push(...result.messages); + if (result?.systemPrompt !== undefined) systemPrompt = result.systemPrompt; +} + +return messages.length || systemPrompt !== event.systemPrompt + ? { messages, systemPrompt } + : undefined; +``` + +### Tool call + +Sequential, early exit on block. + +```ts +for (const handler of handlers("tool_call")) { + const result = await handler(event, ctx, signal); + if (result?.block) return result; +} +``` + +### Tool result + +Sequential patch accumulation. Each handler sees current patched result. + +```ts +let current = event; +let modified = false; + +for (const handler of handlers("tool_result")) { + const result = await handler(current, ctx, signal); + if (!result) continue; + + current = { + ...current, + content: result.content ?? current.content, + details: result.details ?? current.details, + isError: result.isError ?? current.isError, + }; + + modified = true; +} + +return modified + ? { content: current.content, details: current.details, isError: current.isError } + : undefined; +``` + +### Session-before events + +Sequential, early exit on cancel. + +```ts +let last; + +for (const handler of handlers(event.type)) { + const result = await handler(event, ctx, signal); + if (!result) continue; + last = result; + if (result.cancel) return result; +} + +return last; +``` ## Harness usage -The harness only emits events and uses typed results. +Harness only does this: ```ts -await this.hooks.emit({ type: "message_start", message }, signal); +await this.hooks.emit(event, signal); ``` +or: + ```ts const result = await this.hooks.emit({ type: "context", messages }, signal); -messages = result?.messages ?? messages; +return result?.messages ?? messages; ``` +Harness does not store handlers, chain listeners, or know extension policy. + +## Context + +Context is a normal object, not rebuilt per emit. + ```ts -const result = await this.hooks.emit({ type: "tool_call", toolName, input }, signal); -if (result?.block) return blockedToolResult(result.reason); +const hooks = new CodingAgentHooks({ + harness: harnessFacade, + session: sessionFacade, + ui: noUiFacade, +}); ``` -`AgentHarness` does not store handlers and does not implement hook chaining semantics. - -## Context model - -Context is a plain object owned by the hooks implementation. +Later: ```ts -hooks.setContext(nextContext); +hooks.setContext({ + ...hooks.context, + ui: tuiFacade, +}); ``` -Per-run `AbortSignal` is passed separately to `emit()` and handlers. - -Dynamic app state should be exposed through small facades instead of late-bound getter mazes. - -Example app context: +For dynamic state, prefer stable facades/methods over getter maze: ```ts -interface CodingAgentContext { +interface CodingAgentHookContext { harness: HarnessFacade; session: SessionFacade; ui: UiFacade; @@ -395,71 +319,127 @@ interface CodingAgentContext { } ``` -The hook context should not expose `waitForIdle()` to hook handlers. A future facade can expose `runWhenIdle(() => Promise)` for safe deferred work. +Per-run `signal` is passed as the third handler arg. -## Cleanup semantics +## Extension loading later -Each registration owns at most one cleanup. - -- Manual unregister removes the registration and runs its cleanup once. -- `clear()` removes all remaining registrations and runs their cleanups once. -- `dispose()` calls `clear()`. -- Cleanup order is reverse registration order. -- Cleanup errors are collected; cleanup continues; `clear()` throws an aggregate error if any cleanup failed. - -## Error policy - -The base hooks implementation can throw handler errors by default. - -App-specific hooks that load untrusted/user extensions should use a continue-and-report policy. Reducers receive registration source metadata so they can report errors with provenance: +Extension loading can live next to harness and construct hooks: ```ts -for (const registration of registrations) { - try { - const result = await registration.handler(event, context, signal); - // apply result - } catch (error) { - reportHookError({ - event: event.type, - source: registration.source, - error, - }); - } -} +const hooks = await loadExtensions({ + paths, + context, + hooks: new CodingAgentHooks(context), +}); +const harness = new AgentHarness({ ..., hooks }); ``` -## Extension loading sketch - -An app-level extension host owns extension loading and non-hook registries. The harness only receives hooks. +The loader registers into hooks: ```ts -class ExtensionHost { - constructor(private readonly hooks: AgentHarnessHooks) {} - - async load(paths: string[]): Promise { - for (const path of paths) { - const extension = await loadExtension(path); - const source = createExtensionSource(path); - - const api = { - on: (type, handler, cleanup) => { - this.hooks.on(type, handler, { source, cleanup }); - }, - registerTool: (tool) => { - this.tools.set(tool.name, { ...tool, source }); - }, - }; - - await extension(api); - } - } - - async clear(): Promise { - this.tools.clear(); - this.commands.clear(); - await this.hooks.clear(); - } -} +hooks.on("context", handler); +hooks.on("tool_call", handler); +hooks.addCleanup(cleanup); ``` -Non-hook registries, such as tools, commands, flags, shortcuts, message renderers, providers, and OAuth providers, remain app-level concerns. +For reload: + +```ts +await hooks.clear(); +const nextHooks = await loadExtensions(...); +harness.setHooks(nextHooks); // idle-only if supported +``` + +## Poking holes + +### 1. Error policy must be explicit + +Existing coding-agent catches extension errors, reports them, and continues. New hooks need the same policy, likely: + +```ts +errorMode: "continue" | "throw" +onError(error) +``` + +For coding-agent, default should be `"continue"`. + +### 2. Source metadata matters + +Existing runner knows which extension produced an error/resource/tool. Plain `on()` loses that unless we add registration metadata or scopes. + +Probably needed: + +```ts +const scope = hooks.createScope({ sourceInfo }); +scope.on("context", handler); +scope.addCleanup(...); +``` + +Or `on(type, handler, { sourceInfo })`. + +### 3. Some extension capabilities are registries, not hooks + +These are not covered by `emit()` and should stay as registries on `CodingAgentHooks` or an extension host: + +- tools +- commands +- shortcuts +- flags +- message renderers +- provider registrations +- OAuth providers +- custom model providers + +That is fine. They do not belong in `AgentHarness`. + +### 4. Existing coding-agent events can be represented + +No blocker for: + +- `context` +- `before_provider_request` +- `after_provider_response` +- `before_agent_start` +- `message_end` +- `tool_call` +- `tool_result` +- `input` +- `user_bash` +- `resources_discover` +- `session_before_*` +- `session_*` +- model/thinking selection events +- agent/turn/message/tool lifecycle events + +They become additional event types handled by `CodingAgentHooks`. + +### 5. Need to preserve exact old semantics + +When porting coding-agent, special cases must be copied: + +- `input`: transform chain, `handled` short-circuits. +- `user_bash`: first meaningful result wins. +- `message_end`: replacement must keep same role. +- `before_agent_start`: `ctx.getSystemPrompt()` must reflect current chained prompt. +- `resources_discover`: aggregate paths and keep extension source. +- `tool_call`: argument mutation remains visible to later handlers. +- `tool_result`: later handlers see prior patches. + +The design allows all of that, but the default/coding hooks implementation must encode it. + +### 6. `emit()` switch can miss custom mutation events + +If a subclass adds a result-producing event but forgets to override `emit()`, it will behave observationally. Tests should catch this. Could add a protected strategy registry later if this becomes error-prone, but not initially. + +### 7. Observer semantics are intentionally limited + +Observers see the original emitted event once. They do not see every intermediate mutation. If something needs final transformed state, emit a separate final event or use an event-specific handler. + +## Verdict + +This design can implement a new coding-agent. It is simpler than the current runner, keeps harness clean, and preserves the important extension capabilities as long as `CodingAgentHooks` adds source-aware scopes, registries, cleanup, and the exact old event semantics. + +--- Comments --- + +Thread hn2xk0tzhj on "addCleanup(cleanup" + [tmluyaub9v] Owner (2026-05-14T12:55:45.500Z): cleanup should be passed along optionally to on/observe diff --git a/packages/agent/docs/observability.md b/packages/agent/docs/observability.md new file mode 100644 index 00000000..2f77b3fc --- /dev/null +++ b/packages/agent/docs/observability.md @@ -0,0 +1,376 @@ + + +# Pi Observability Design Notes + +## Goal + +Make `packages/ai` and `packages/agent`/harness observable without depending on OpenTelemetry, Sentry, or any APM vendor. + +Pi should emit stable, structured lifecycle events. External listeners can convert those events into OTel spans, Sentry spans, logs, metrics, or custom telemetry. + +## Mental model + +A trace is one causal tree of work, e.g. one user turn. + +A span is one timed operation in that tree. It is normally represented by IDs, not object pointers: + +```ts +interface SpanRecord { + traceId: string; + spanId: string; + parentSpanId?: string; + name: string; + startTime: number; + endTime?: number; + attributes: Record; + status: "ok" | "error"; +} +``` + +Example tree: + +```text +traceId=t1 spanId=s1 parent=- name=pi.agent.prompt +traceId=t1 spanId=s2 parent=s1 name=pi.agent.turn +traceId=t1 spanId=s3 parent=s2 name=pi.ai.provider.request +traceId=t1 spanId=s4 parent=s2 name=pi.agent.tool_call +traceId=t1 spanId=s5 parent=s4 name=pi.session.append_entry +``` + +## Async context + +JavaScript has one event loop but multiple async chains can interleave. A single global `currentContext` breaks under concurrency. + +`AsyncLocalStorage` is the Node equivalent of `ThreadLocal` for async continuations. It lets concurrent operations keep distinct current contexts: + +```ts +await Promise.all([ + runWithPiContext({ userId: "alice" }, () => harness.prompt("A")), + runWithPiContext({ userId: "bob" }, () => harness.prompt("B")), +]); +``` + +Deep code can then read the correct current context for the active async chain. + +Pi must run in Node, Bun, browser, workers, and other JS runtimes, so ALS cannot be the core abstraction. It should be a runtime adapter. + +## Core design + +Pi owns a small runtime-agnostic observability abstraction: + +```ts +export interface PiObservabilityContext { + traceId?: string; + currentSpanId?: string; + userContext?: Record; +} + +export interface PiObservabilityEvent { + type: "start" | "end" | "error" | "event"; + name: string; + traceId: string; + spanId?: string; + parentSpanId?: string; + timestamp: number; + durationMs?: number; + context?: Record; + payload?: Record; + error?: { name: string; message: string }; +} + +export interface PiObservability { + getContext(): PiObservabilityContext | undefined; + runWithContext(context: PiObservabilityContext, fn: () => T): T; + emit(event: PiObservabilityEvent): void; + hasSubscribers(): boolean; +} +``` + +Public API: + +```ts +export function configurePiObservability(observability: PiObservability): void; +export function subscribePiObservability(listener: (event: PiObservabilityEvent) => void): () => void; +export function runWithPiContext(userContext: Record, fn: () => T): T; +export function traceOperation(name: string, payload: Record, fn: () => T): T; +``` + +`traceOperation()`: + +1. reads the current context +2. creates `traceId` if missing +3. creates a new `spanId` +4. uses current span as `parentSpanId` +5. emits `start` +6. runs callback under child context +7. emits `end` or `error` +8. rethrows on error + +Pseudo-code: + +```ts +function traceOperation(name: string, payload: Record, fn: () => T): T { + const parent = getContext(); + const traceId = parent?.traceId ?? createId(); + const spanId = createId(); + const parentSpanId = parent?.currentSpanId; + + const child = { ...parent, traceId, currentSpanId: spanId }; + + emit({ type: "start", name, traceId, spanId, parentSpanId, timestamp: Date.now(), context: parent?.userContext, payload }); + + return runWithContext(child, () => { + try { + const result = fn(); + // Promise-aware implementation emits end/error after settlement. + emit({ type: "end", name, traceId, spanId, parentSpanId, timestamp: Date.now(), context: child.userContext, payload }); + return result; + } catch (error) { + emit({ type: "error", name, traceId, spanId, parentSpanId, timestamp: Date.now(), context: child.userContext, payload, error: serializeError(error) }); + throw error; + } + }); +} +``` + +## Runtime adapters + +Core packages should not import Node-only APIs. + +Possible implementations: + +- Node adapter: `AsyncLocalStorage` for context, optional `diagnostics_channel` publishing. +- Browser/workers fallback: local subscriber set and limited/manual context propagation. +- Bun/Deno adapters: use runtime-specific async context if available. + +For Node, diagnostics channels can be used as a passive event bus: + +```ts +import { channel } from "diagnostics_channel"; +channel("pi.observability").publish(event); +``` + +Subscribers can create OTel/Sentry spans without monkey-patching pi. + +## What pi emits + +Pi emits what happened. It does not create OTel/Sentry spans directly. + +Initial minimal event names: + +```text +pi.agent.prompt +pi.agent.skill +pi.agent.prompt_template +pi.agent.compaction +pi.agent.branch_navigation +pi.agent.session.append_entry +pi.ai.provider.request +``` + +Each operation emits: + +```text +start +end +error +``` + +Later additions: + +```text +pi.agent.turn +pi.agent.tool_call +pi.agent.queue_update +pi.ai.provider.retry +pi.ai.provider.first_token +pi.ai.provider.usage +pi.session.read +pi.session.write +``` + +## Minimal instrumentation points + +### packages/agent + +Wrap: + +- `AgentHarness.prompt()` +- `AgentHarness.skill()` +- `AgentHarness.promptFromTemplate()` +- `AgentHarness.compact()` +- `AgentHarness.navigateTree()` +- `Session.appendTypedEntry()` or storage append facade + +Example: + +```ts +return traceOperation( + "pi.agent.prompt", + { + sessionId: turnState.sessionId, + provider: turnState.model.provider, + model: turnState.model.id, + promptLength: text.length, + imageCount: options?.images?.length ?? 0, + }, + () => this.executeTurn(turnState, text, options), +); +``` + +Session write: + +```ts +return traceOperation( + "pi.agent.session.append_entry", + { entryType: entry.type }, + async () => { + await this.unwrap(this.storage.appendEntry(entry)); + return entry.id; + }, +); +``` + +### packages/ai + +Wrap common provider boundaries: + +- `streamSimple()` +- `completeSimple()` + +Example: + +```ts +return traceOperation( + "pi.ai.provider.request", + { + api: model.api, + provider: model.provider, + model: model.id, + sessionId: options.sessionId, + reasoning: options.reasoning, + }, + () => actualStreamSimple(model, context, options), +); +``` + +End/error payloads can include safe metadata: + +- stop reason +- status code +- retry count +- input/output/total tokens +- cost total +- aborted/timeout flag + +## Safety and redaction + +Default payloads must be safe. + +Safe by default: + +- provider +- model +- API identifier +- session id +- entry type +- tool name +- status code +- stop reason +- token counts +- costs +- durations + +Unsafe by default: + +- prompts +- completions +- tool args +- tool results +- shell output +- file contents +- provider request payloads +- provider response bodies +- API keys +- headers + +Content capture can be opt-in later with explicit redaction hooks. + +## Listener behavior + +Observability must never affect pi execution. + +Subscriber errors should be swallowed or isolated. Harness hooks are control-plane and may affect execution; observability subscribers are passive and must not. + +## User context + +Users can associate arbitrary context with a turn: + +```ts +await runWithPiContext( + { + userId: "u123", + orgId: "acme", + region: "eu", + }, + () => harness.prompt("fix this"), +); +``` + +Every emitted event inside that async chain includes the context: + +```ts +{ + type: "start", + name: "pi.ai.provider.request", + traceId: "t1", + spanId: "s3", + parentSpanId: "s1", + context: { + userId: "u123", + orgId: "acme", + region: "eu", + }, + payload: { + provider: "anthropic", + model: "claude-sonnet-4", + }, +} +``` + +An OTel adapter can map this to span attributes. A Sentry adapter can map it to Sentry context/spans. A custom user can log JSON. + +## Package story + +Minimal initial package: + +```text +packages/observability + runtime-agnostic context + traceOperation + subscribe +``` + +Then: + +```text +packages/ai + emits pi.ai.* events + +packages/agent + emits pi.agent.* / pi.session.* events +``` + +Optional later: + +```text +packages/observability-node + AsyncLocalStorage + diagnostics_channel bridge + +packages/otel + subscribes to pi events and creates OpenTelemetry spans +``` + +## Thesis + +Pi defines a stable, safe event contract. Adapters define where events go. + +This makes ai/harness observable without binding core packages to OTel, Sentry, Node-only APIs, or monkey-patching. From 163fd35fd8ce9d44c084b44ae2bab49b2256ae1c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 16 May 2026 21:06:50 +0000 Subject: [PATCH 052/166] chore: approve contributor abhinavmathur-atlan --- .github/APPROVED_CONTRIBUTORS | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/APPROVED_CONTRIBUTORS b/.github/APPROVED_CONTRIBUTORS index 50d3566e..2845d9d8 100644 --- a/.github/APPROVED_CONTRIBUTORS +++ b/.github/APPROVED_CONTRIBUTORS @@ -207,3 +207,5 @@ chrisvariety pr maximilianzuern pr brianmichel pr + +abhinavmathur-atlan pr From 758004d1bb1488998b3b752bfb039d517d9c2b16 Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Sat, 16 May 2026 23:07:55 +0200 Subject: [PATCH 053/166] fix(coding-agent): cap resume session metadata loads closes #4583 closes #4591 --- packages/coding-agent/CHANGELOG.md | 1 + .../coding-agent/src/core/session-manager.ts | 66 ++++++++++++++----- 2 files changed, 51 insertions(+), 16 deletions(-) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 5e6d23d8..739a7353 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -9,6 +9,7 @@ ### Fixed +- Fixed `--resume` session listing to cap in-flight session metadata loads and avoid OOM on large session histories ([#4583](https://github.com/earendil-works/pi/issues/4583)). - Fixed interactive error messages to render with trailing spacing so reload errors do not run into resource listings ([#4510](https://github.com/earendil-works/pi/issues/4510)). - Fixed nested code fences in the Termux setup documentation so the example AGENTS.md renders correctly ([#4503](https://github.com/earendil-works/pi/issues/4503)). - Fixed tool output expansion while extension confirmation dialogs are focused ([#4429](https://github.com/earendil-works/pi/issues/4429)). diff --git a/packages/coding-agent/src/core/session-manager.ts b/packages/coding-agent/src/core/session-manager.ts index 5d8971c5..0c6e2334 100644 --- a/packages/coding-agent/src/core/session-manager.ts +++ b/packages/coding-agent/src/core/session-manager.ts @@ -617,6 +617,48 @@ async function buildSessionInfo(filePath: string): Promise { export type SessionListProgress = (loaded: number, total: number) => void; +const MAX_CONCURRENT_SESSION_INFO_LOADS = 10; + +async function buildSessionInfosWithConcurrency( + files: string[], + onLoaded: () => void, +): Promise<(SessionInfo | null)[]> { + const results: (SessionInfo | null)[] = new Array(files.length).fill(null); + const inFlight = new Set>(); + let nextIndex = 0; + + const startNext = (): void => { + const index = nextIndex++; + const file = files[index]; + if (!file) return; + + let task: Promise; + task = buildSessionInfo(file) + .then((info) => { + results[index] = info; + }) + .catch(() => { + results[index] = null; + }) + .finally(() => { + inFlight.delete(task); + onLoaded(); + }); + inFlight.add(task); + }; + + while (nextIndex < files.length || inFlight.size > 0) { + while (nextIndex < files.length && inFlight.size < MAX_CONCURRENT_SESSION_INFO_LOADS) { + startNext(); + } + if (inFlight.size > 0) { + await Promise.race(inFlight); + } + } + + return results; +} + async function listSessionsFromDir( dir: string, onProgress?: SessionListProgress, @@ -634,14 +676,10 @@ async function listSessionsFromDir( const total = progressTotal ?? files.length; let loaded = 0; - const results = await Promise.all( - files.map(async (file) => { - const info = await buildSessionInfo(file); - loaded++; - onProgress?.(progressOffset + loaded, total); - return info; - }), - ); + const results = await buildSessionInfosWithConcurrency(files, () => { + loaded++; + onProgress?.(progressOffset + loaded, total); + }); for (const info of results) { if (info) { sessions.push(info); @@ -1400,14 +1438,10 @@ export class SessionManager { const sessions: SessionInfo[] = []; const allFiles = dirFiles.flat(); - const results = await Promise.all( - allFiles.map(async (file) => { - const info = await buildSessionInfo(file); - loaded++; - onProgress?.(loaded, totalFiles); - return info; - }), - ); + const results = await buildSessionInfosWithConcurrency(allFiles, () => { + loaded++; + onProgress?.(loaded, totalFiles); + }); for (const info of results) { if (info) { From 7c5c3d6fd60a0a3d8a3d40d9320c32975b01b215 Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Sat, 16 May 2026 23:29:22 +0200 Subject: [PATCH 054/166] fix(ai): detect litellm context overflow errors closes #4563 --- packages/ai/CHANGELOG.md | 1 + packages/ai/src/utils/overflow.ts | 4 +++- packages/ai/test/overflow.test.ts | 7 +++++++ 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index 7fa3a37d..f962fcee 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -15,6 +15,7 @@ - Fixed OpenAI Responses requests for models that support disabling reasoning to send `reasoning.effort: "none"` when thinking is off. - Fixed Inception Mercury 2 tool calling on OpenRouter by marking `off` as unsupported in `thinkingLevelMap`, so the openai-completions provider omits the reasoning param instead of defaulting to `{reasoning:{effort:"none"}}` (which puts Mercury 2 in instant mode, disabling tool calls). - Fixed OpenAI Codex SSE retries to honor `retry-after-ms` and `retry-after` headers before falling back to exponential backoff. +- Fixed context overflow detection for LiteLLM-wrapped OpenAI-compatible errors using `exceeds the model's maximum context length of ... tokens` wording ([#4563](https://github.com/earendil-works/pi/issues/4563)). ## [0.74.0] - 2026-05-07 diff --git a/packages/ai/src/utils/overflow.ts b/packages/ai/src/utils/overflow.ts index bf879dc4..7eef4a75 100644 --- a/packages/ai/src/utils/overflow.ts +++ b/packages/ai/src/utils/overflow.ts @@ -11,6 +11,7 @@ import type { AssistantMessage } from "../types.js"; * - Anthropic: "prompt is too long: 213462 tokens > 200000 maximum" * - Anthropic: "413 {\"error\":{\"type\":\"request_too_large\",\"message\":\"Request exceeds the maximum size\"}}" * - OpenAI: "Your input exceeds the context window of this model" + * - OpenAI/LiteLLM: "Requested token count exceeds the model's maximum context length of 131072 tokens" * - Google: "The input token count (1196265) exceeds the maximum number of tokens allowed (1048575)" * - xAI: "This model's maximum prompt length is 131072 but the request contains 537812 tokens" * - Groq: "Please reduce the length of the messages or completion" @@ -34,6 +35,7 @@ const OVERFLOW_PATTERNS = [ /request_too_large/i, // Anthropic request byte-size overflow (HTTP 413) /input is too long for requested model/i, // Amazon Bedrock /exceeds the context window/i, // OpenAI (Completions & Responses API) + /exceeds (?:the )?(?:model'?s )?maximum context length of [\d,]+ tokens?/i, // OpenAI-compatible proxies (LiteLLM) /input token count.*exceeds the maximum/i, // Google (Gemini) /maximum prompt length is \d+/i, // xAI (Grok) /reduce the length of the messages/i, // Groq @@ -81,7 +83,7 @@ const NON_OVERFLOW_PATTERNS = [ * * **Reliable detection (returns error with detectable message):** * - Anthropic: "prompt is too long: X tokens > Y maximum" or "request_too_large" - * - OpenAI (Completions & Responses): "exceeds the context window" + * - OpenAI (Completions & Responses): "exceeds the context window" or "exceeds the model's maximum context length of X tokens" * - Google Gemini: "input token count exceeds the maximum" * - xAI (Grok): "maximum prompt length is X but request contains Y" * - Groq: "reduce the length of the messages" diff --git a/packages/ai/test/overflow.test.ts b/packages/ai/test/overflow.test.ts index 1b50b831..ca6fde24 100644 --- a/packages/ai/test/overflow.test.ts +++ b/packages/ai/test/overflow.test.ts @@ -42,6 +42,13 @@ describe("isContextOverflow", () => { expect(isContextOverflow(message, 262144)).toBe(true); }); + it("detects LiteLLM-wrapped OpenAI maximum context length errors", () => { + const message = createErrorMessage( + "Error: 503 litellm.ServiceUnavailableError: litellm.MidStreamFallbackError: litellm.APIConnectionError: APIConnectionError: OpenAIException - Requested token count exceeds the model's maximum context length of 131072 tokens.", + ); + expect(isContextOverflow(message, 131072)).toBe(true); + }); + it("does not treat generic non-overflow Ollama errors as overflow", () => { const message = createErrorMessage("500 `model runner crashed unexpectedly`"); expect(isContextOverflow(message, 32768)).toBe(false); From 22a9c484e73506777e8e678913bdd275f00d7951 Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Sat, 16 May 2026 23:33:06 +0200 Subject: [PATCH 055/166] fix(ai): respect model output token limits closes #4539 --- packages/ai/CHANGELOG.md | 1 + packages/ai/src/providers/simple-options.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index f962fcee..cec9ae7c 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -16,6 +16,7 @@ - Fixed Inception Mercury 2 tool calling on OpenRouter by marking `off` as unsupported in `thinkingLevelMap`, so the openai-completions provider omits the reasoning param instead of defaulting to `{reasoning:{effort:"none"}}` (which puts Mercury 2 in instant mode, disabling tool calls). - Fixed OpenAI Codex SSE retries to honor `retry-after-ms` and `retry-after` headers before falling back to exponential backoff. - Fixed context overflow detection for LiteLLM-wrapped OpenAI-compatible errors using `exceeds the model's maximum context length of ... tokens` wording ([#4563](https://github.com/earendil-works/pi/issues/4563)). +- Fixed `streamSimple()` defaults to respect model output limits above 32000 tokens instead of clamping provider requests to 32000 ([#4539](https://github.com/earendil-works/pi/issues/4539)). ## [0.74.0] - 2026-05-07 diff --git a/packages/ai/src/providers/simple-options.ts b/packages/ai/src/providers/simple-options.ts index 914ba5e7..288f2c52 100644 --- a/packages/ai/src/providers/simple-options.ts +++ b/packages/ai/src/providers/simple-options.ts @@ -3,7 +3,7 @@ import type { Api, Model, SimpleStreamOptions, StreamOptions, ThinkingBudgets, T export function buildBaseOptions(model: Model, options?: SimpleStreamOptions, apiKey?: string): StreamOptions { return { temperature: options?.temperature, - maxTokens: options?.maxTokens ?? (model.maxTokens > 0 ? Math.min(model.maxTokens, 32000) : undefined), + maxTokens: options?.maxTokens ?? (model.maxTokens > 0 ? model.maxTokens : undefined), signal: options?.signal, apiKey: apiKey || options?.apiKey, transport: options?.transport, From d0fe8570e93e737c74e30223e91adb4248ac49bc Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Sat, 16 May 2026 23:36:25 +0200 Subject: [PATCH 056/166] fix(coding-agent): parse multiline prompt template args closes #4553 --- packages/coding-agent/CHANGELOG.md | 1 + .../coding-agent/src/core/prompt-templates.ts | 10 ++-- .../test/prompt-templates.test.ts | 58 ++++++++++++++++++- 3 files changed, 63 insertions(+), 6 deletions(-) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 739a7353..95525d0c 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -9,6 +9,7 @@ ### Fixed +- Fixed prompt template argument parsing to split unquoted multiline input on newlines ([#4553](https://github.com/earendil-works/pi/issues/4553)). - Fixed `--resume` session listing to cap in-flight session metadata loads and avoid OOM on large session histories ([#4583](https://github.com/earendil-works/pi/issues/4583)). - Fixed interactive error messages to render with trailing spacing so reload errors do not run into resource listings ([#4510](https://github.com/earendil-works/pi/issues/4510)). - Fixed nested code fences in the Termux setup documentation so the example AGENTS.md renders correctly ([#4503](https://github.com/earendil-works/pi/issues/4503)). diff --git a/packages/coding-agent/src/core/prompt-templates.ts b/packages/coding-agent/src/core/prompt-templates.ts index 8527f911..45e779a1 100644 --- a/packages/coding-agent/src/core/prompt-templates.ts +++ b/packages/coding-agent/src/core/prompt-templates.ts @@ -37,7 +37,7 @@ export function parseCommandArgs(argsString: string): string[] { } } else if (char === '"' || char === "'") { inQuote = char; - } else if (char === " " || char === "\t") { + } else if (/\s/.test(char)) { if (current) { args.push(current); current = ""; @@ -282,9 +282,11 @@ export function loadPromptTemplates(options: LoadPromptTemplatesOptions): Prompt export function expandPromptTemplate(text: string, templates: PromptTemplate[]): string { if (!text.startsWith("/")) return text; - const spaceIndex = text.indexOf(" "); - const templateName = spaceIndex === -1 ? text.slice(1) : text.slice(1, spaceIndex); - const argsString = spaceIndex === -1 ? "" : text.slice(spaceIndex + 1); + const match = text.match(/^\/([^\s]+)(?:\s+([\s\S]*))?$/); + if (!match) return text; + + const templateName = match[1]; + const argsString = match[2] ?? ""; const template = templates.find((t) => t.name === templateName); if (template) { diff --git a/packages/coding-agent/test/prompt-templates.test.ts b/packages/coding-agent/test/prompt-templates.test.ts index 67be0da4..10cecaca 100644 --- a/packages/coding-agent/test/prompt-templates.test.ts +++ b/packages/coding-agent/test/prompt-templates.test.ts @@ -13,7 +13,12 @@ import { tmpdir } from "os"; import { join } from "path"; import { afterAll, describe, expect, test } from "vitest"; import { getAgentDir } from "../src/config.js"; -import { loadPromptTemplates, parseCommandArgs, substituteArgs } from "../src/core/prompt-templates.js"; +import { + expandPromptTemplate, + loadPromptTemplates, + parseCommandArgs, + substituteArgs, +} from "../src/core/prompt-templates.js"; // ============================================================================ // substituteArgs @@ -335,10 +340,25 @@ describe("parseCommandArgs", () => { expect(parseCommandArgs("日本語 🎉 café")).toEqual(["日本語", "🎉", "café"]); }); - test("should handle newlines in arguments", () => { + test("should handle newlines in quoted arguments", () => { expect(parseCommandArgs('"line1\nline2" second')).toEqual(["line1\nline2", "second"]); }); + test("should treat unquoted newlines as separators", () => { + expect(parseCommandArgs("label-2\n\nHere is some description #2.")).toEqual([ + "label-2", + "Here", + "is", + "some", + "description", + "#2.", + ]); + }); + + test("should collapse mixed unquoted whitespace", () => { + expect(parseCommandArgs("a\n\n\tb c")).toEqual(["a", "b", "c"]); + }); + test("should handle escaped quotes inside quoted strings", () => { // Note: This implementation doesn't handle escaped quotes - backslash is literal expect(parseCommandArgs('"quoted \\"text\\""')).toEqual(["quoted \\text\\"]); @@ -357,6 +377,40 @@ describe("parseCommandArgs", () => { // Integration // ============================================================================ +describe("expandPromptTemplate", () => { + test("should split template arguments on unquoted newlines", () => { + const result = expandPromptTemplate("/arg-test label-2\n\nHere is some description #2.", [ + { + name: "arg-test", + description: "test", + content: `- arg1: $1\n- rest: \${@:2}`, + sourceInfo: { path: "/tmp/arg-test.md", source: "local", scope: "temporary", origin: "top-level" }, + filePath: "/tmp/arg-test.md", + }, + ]); + + expect(result).toBe("- arg1: label-2\n- rest: Here is some description #2."); + }); + + test("should support template command separated from args by newline", () => { + const result = expandPromptTemplate("/arg-test\nlabel-2", [ + { + name: "arg-test", + description: "test", + content: "arg1: $1", + sourceInfo: { path: "/tmp/arg-test.md", source: "local", scope: "temporary", origin: "top-level" }, + filePath: "/tmp/arg-test.md", + }, + ]); + + expect(result).toBe("arg1: label-2"); + }); +}); + +// ============================================================================ +// Integration +// ============================================================================ + describe("parseCommandArgs + substituteArgs integration", () => { test("should parse and substitute together correctly", () => { const input = 'Button "onClick handler" "disabled support"'; From 5d31e70b8a23008a745aa0315eeb6226694d481f Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Sat, 16 May 2026 23:38:46 +0200 Subject: [PATCH 057/166] fix(coding-agent): allow skill names to differ from directories closes #4534 --- packages/coding-agent/CHANGELOG.md | 1 + packages/coding-agent/docs/skills.md | 7 +++---- packages/coding-agent/src/core/skills.ts | 8 ++------ packages/coding-agent/test/skills.test.ts | 4 ++-- 4 files changed, 8 insertions(+), 12 deletions(-) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 95525d0c..b1a7a765 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -9,6 +9,7 @@ ### Fixed +- Fixed skill diagnostics to stop warning when a skill name differs from its parent directory ([#4534](https://github.com/earendil-works/pi/issues/4534)). - Fixed prompt template argument parsing to split unquoted multiline input on newlines ([#4553](https://github.com/earendil-works/pi/issues/4553)). - Fixed `--resume` session listing to cap in-flight session metadata loads and avoid OOM on large session histories ([#4583](https://github.com/earendil-works/pi/issues/4583)). - Fixed interactive error messages to render with trailing spacing so reload errors do not run into resource listings ([#4510](https://github.com/earendil-works/pi/issues/4510)). diff --git a/packages/coding-agent/docs/skills.md b/packages/coding-agent/docs/skills.md index 746d549e..cb96dd97 100644 --- a/packages/coding-agent/docs/skills.md +++ b/packages/coding-agent/docs/skills.md @@ -4,7 +4,7 @@ Skills are self-contained capability packages that the agent loads on-demand. A skill provides specialized workflows, setup instructions, helper scripts, and reference documentation for specific tasks. -Pi implements the [Agent Skills standard](https://agentskills.io/specification), warning about violations but remaining lenient. +Pi implements the [Agent Skills standard](https://agentskills.io/specification), warning about most violations but remaining lenient. Pi allows skill names to differ from their parent directory even though the standard disallows it; that rule is suboptimal for shared skill directories used across multiple agent harnesses. ## Table of Contents @@ -140,7 +140,7 @@ Per the [Agent Skills specification](https://agentskills.io/specification#frontm | Field | Required | Description | |-------|----------|-------------| -| `name` | Yes | Max 64 chars. Lowercase a-z, 0-9, hyphens. Must match parent directory. | +| `name` | Yes | Max 64 chars. Lowercase a-z, 0-9, hyphens. Unlike the standard, Pi does not require this to match the parent directory because that standard requirement is suboptimal for shared skill directories. | | `description` | Yes | Max 1024 chars. What the skill does and when to use it. | | `license` | No | License name or reference to bundled file. | | `compatibility` | No | Max 500 chars. Environment requirements. | @@ -154,7 +154,7 @@ Per the [Agent Skills specification](https://agentskills.io/specification#frontm - Lowercase letters, numbers, hyphens only - No leading/trailing hyphens - No consecutive hyphens -- Must match parent directory name +Pi does not require the name to match the parent directory. The Agent Skills standard does, but that requirement is suboptimal for shared skill directories used by multiple tools. Valid: `pdf-processing`, `data-analysis`, `code-review` Invalid: `PDF-Processing`, `-pdf`, `pdf--processing` @@ -177,7 +177,6 @@ description: Helps with PDFs. Pi validates skills against the Agent Skills standard. Most issues produce warnings but still load the skill: -- Name doesn't match parent directory - Name exceeds 64 characters or contains invalid characters - Name starts/ends with hyphen or has consecutive hyphens - Description exceeds 1024 characters diff --git a/packages/coding-agent/src/core/skills.ts b/packages/coding-agent/src/core/skills.ts index c0647473..82dc9a63 100644 --- a/packages/coding-agent/src/core/skills.ts +++ b/packages/coding-agent/src/core/skills.ts @@ -90,13 +90,9 @@ export interface LoadSkillsResult { * Validate skill name per Agent Skills spec. * Returns array of validation error messages (empty if valid). */ -function validateName(name: string, parentDirName: string): string[] { +function validateName(name: string): string[] { const errors: string[] = []; - if (name !== parentDirName) { - errors.push(`name "${name}" does not match parent directory "${parentDirName}"`); - } - if (name.length > MAX_NAME_LENGTH) { errors.push(`name exceeds ${MAX_NAME_LENGTH} characters (${name.length})`); } @@ -301,7 +297,7 @@ function loadSkillFromFile( const name = frontmatter.name || parentDirName; // Validate name - const nameErrors = validateName(name, parentDirName); + const nameErrors = validateName(name); for (const error of nameErrors) { diagnostics.push({ type: "warning", message: error, path: filePath }); } diff --git a/packages/coding-agent/test/skills.test.ts b/packages/coding-agent/test/skills.test.ts index 84803e6b..36c02e87 100644 --- a/packages/coding-agent/test/skills.test.ts +++ b/packages/coding-agent/test/skills.test.ts @@ -41,7 +41,7 @@ describe("skills", () => { expect(diagnostics).toHaveLength(0); }); - it("should warn when name doesn't match parent directory", () => { + it("should allow names that don't match parent directory", () => { const { skills, diagnostics } = loadSkillsFromDir({ dir: join(fixturesDir, "name-mismatch"), source: "test", @@ -51,7 +51,7 @@ describe("skills", () => { expect(skills[0].name).toBe("different-name"); expect( diagnostics.some((d: ResourceDiagnostic) => d.message.includes("does not match parent directory")), - ).toBe(true); + ).toBe(false); }); it("should warn when name contains invalid characters", () => { From 7afd80d78801cdd5a9be8c0e018d556605d90784 Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Sat, 16 May 2026 23:39:35 +0200 Subject: [PATCH 058/166] fix(coding-agent): pin fd for macos x64 closes #4559 --- packages/coding-agent/CHANGELOG.md | 1 + packages/coding-agent/src/utils/tools-manager.ts | 5 ++++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index b1a7a765..3a857254 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -9,6 +9,7 @@ ### Fixed +- Fixed `fd` auto-download on macOS x86_64 by pinning the last release that ships an Intel macOS binary ([#4559](https://github.com/earendil-works/pi/issues/4559)). - Fixed skill diagnostics to stop warning when a skill name differs from its parent directory ([#4534](https://github.com/earendil-works/pi/issues/4534)). - Fixed prompt template argument parsing to split unquoted multiline input on newlines ([#4553](https://github.com/earendil-works/pi/issues/4553)). - Fixed `--resume` session listing to cap in-flight session metadata loads and avoid OOM on large session histories ([#4583](https://github.com/earendil-works/pi/issues/4583)). diff --git a/packages/coding-agent/src/utils/tools-manager.ts b/packages/coding-agent/src/utils/tools-manager.ts index 19df4ea7..2ee60a0d 100644 --- a/packages/coding-agent/src/utils/tools-manager.ts +++ b/packages/coding-agent/src/utils/tools-manager.ts @@ -246,7 +246,10 @@ async function downloadTool(tool: "fd" | "rg"): Promise { const architecture = arch(); // Get latest version - const version = await getLatestVersion(config.repo); + let version = await getLatestVersion(config.repo); + if (tool === "fd" && plat === "darwin" && architecture === "x64") { + version = "10.3.0"; + } // Get asset name for this platform const assetName = config.getAssetName(version, plat, architecture); From c5831df689dd651753c90887d08a4b9e1c0ec7e4 Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Sun, 17 May 2026 00:02:37 +0200 Subject: [PATCH 059/166] fix(ai): vendor proxy env resolution closes #4513 --- package-lock.json | 5 -- packages/ai/CHANGELOG.md | 1 + packages/ai/package.json | 1 - packages/ai/src/utils/node-http-proxy.ts | 81 ++++++++++++++++++++++-- 4 files changed, 78 insertions(+), 10 deletions(-) diff --git a/package-lock.json b/package-lock.json index 07555c8a..37a2d05d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6274,10 +6274,6 @@ "node": ">=12.0.0" } }, - "node_modules/proxy-from-env": { - "version": "1.1.0", - "license": "MIT" - }, "node_modules/pump": { "version": "3.0.4", "dev": true, @@ -7584,7 +7580,6 @@ "https-proxy-agent": "^7.0.6", "openai": "6.26.0", "partial-json": "^0.1.7", - "proxy-from-env": "^1.1.0", "typebox": "^1.1.24" }, "bin": { diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index cec9ae7c..f3b72aff 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -11,6 +11,7 @@ - Fixed GitHub Copilot model availability to ignore generic `GH_TOKEN` and `GITHUB_TOKEN` environment variables, requiring OAuth login or `COPILOT_GITHUB_TOKEN` instead ([#4485](https://github.com/earendil-works/pi/issues/4485)). - Fixed `openai-completions` streams to surface an error when the stream ends before any terminal `finish_reason`, so truncated responses can retry instead of being accepted as success ([#4345](https://github.com/earendil-works/pi/issues/4345)). - Fixed Bedrock proxy handling to preserve `NO_PROXY` exclusions while using HTTP(S)-only proxy agents. +- Fixed compiled Bun binaries failing to start outside the repo when Bedrock proxy support tried to resolve `proxy-from-env` from external `node_modules` ([#4513](https://github.com/earendil-works/pi/issues/4513)). - Fixed GitHub Copilot Claude test coverage to use the current Claude Sonnet 4.6 model ID. - Fixed OpenAI Responses requests for models that support disabling reasoning to send `reasoning.effort: "none"` when thinking is off. - Fixed Inception Mercury 2 tool calling on OpenRouter by marking `off` as unsupported in `thinkingLevelMap`, so the openai-completions provider omits the reasoning param instead of defaulting to `{reasoning:{effort:"none"}}` (which puts Mercury 2 in instant mode, disabling tool calls). diff --git a/packages/ai/package.json b/packages/ai/package.json index 149da8c3..4f4d5bf7 100644 --- a/packages/ai/package.json +++ b/packages/ai/package.json @@ -77,7 +77,6 @@ "https-proxy-agent": "^7.0.6", "openai": "6.26.0", "partial-json": "^0.1.7", - "proxy-from-env": "^1.1.0", "typebox": "^1.1.24" }, "keywords": [ diff --git a/packages/ai/src/utils/node-http-proxy.ts b/packages/ai/src/utils/node-http-proxy.ts index 22babf60..7b5f4750 100644 --- a/packages/ai/src/utils/node-http-proxy.ts +++ b/packages/ai/src/utils/node-http-proxy.ts @@ -1,11 +1,16 @@ import type { Agent as HttpAgent } from "node:http"; import type { Agent as HttpsAgent } from "node:https"; -import { createRequire } from "node:module"; import { HttpProxyAgent } from "http-proxy-agent"; import { HttpsProxyAgent } from "https-proxy-agent"; -const require = createRequire(import.meta.url); -const { getProxyForUrl } = require("proxy-from-env") as { getProxyForUrl: (url: string) => string }; +const DEFAULT_PROXY_PORTS: Record = { + ftp: 21, + gopher: 70, + http: 80, + https: 443, + ws: 80, + wss: 443, +}; export interface NodeHttpProxyAgents { httpAgent: HttpAgent; @@ -15,8 +20,76 @@ export interface NodeHttpProxyAgents { export const UNSUPPORTED_PROXY_PROTOCOL_MESSAGE = "Unsupported proxy protocol. SOCKS and PAC proxy URLs are not supported; use an HTTP or HTTPS proxy URL."; +function getProxyEnv(key: string): string { + return process.env[key.toLowerCase()] || process.env[key.toUpperCase()] || ""; +} + +function parseProxyTargetUrl(targetUrl: string | URL): URL | undefined { + if (targetUrl instanceof URL) { + return targetUrl; + } + + try { + return new URL(targetUrl); + } catch { + return undefined; + } +} + +function shouldProxyHostname(hostname: string, port: number): boolean { + const noProxy = getProxyEnv("no_proxy").toLowerCase(); + if (!noProxy) { + return true; + } + if (noProxy === "*") { + return false; + } + + return noProxy.split(/[,\s]/).every((proxy) => { + if (!proxy) { + return true; + } + + const parsedProxy = proxy.match(/^(.+):(\d+)$/); + let proxyHostname = parsedProxy ? parsedProxy[1] : proxy; + const proxyPort = parsedProxy ? Number.parseInt(parsedProxy[2]!, 10) : 0; + if (proxyPort && proxyPort !== port) { + return true; + } + + if (!/^[.*]/.test(proxyHostname)) { + return hostname !== proxyHostname; + } + + if (proxyHostname.startsWith("*")) { + proxyHostname = proxyHostname.slice(1); + } + return !hostname.endsWith(proxyHostname); + }); +} + +function getProxyForUrl(targetUrl: string | URL): string { + const parsedUrl = parseProxyTargetUrl(targetUrl); + if (!parsedUrl?.protocol || !parsedUrl.host) { + return ""; + } + + const protocol = parsedUrl.protocol.split(":", 1)[0]!; + const hostname = parsedUrl.host.replace(/:\d*$/, ""); + const port = Number.parseInt(parsedUrl.port, 10) || DEFAULT_PROXY_PORTS[protocol] || 0; + if (!shouldProxyHostname(hostname, port)) { + return ""; + } + + let proxy = getProxyEnv(`${protocol}_proxy`) || getProxyEnv("all_proxy"); + if (proxy && !proxy.includes("://")) { + proxy = `${protocol}://${proxy}`; + } + return proxy; +} + export function resolveHttpProxyUrlForTarget(targetUrl: string | URL): URL | undefined { - const proxy = getProxyForUrl(targetUrl.toString()); + const proxy = getProxyForUrl(targetUrl); if (!proxy) { return undefined; } From 43d3cdd48a62809d253e354123b72215f4cb16b4 Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Sun, 17 May 2026 00:03:03 +0200 Subject: [PATCH 060/166] fix(coding-agent): use configured model scope cycle hint closes #4508 --- packages/coding-agent/CHANGELOG.md | 1 + packages/coding-agent/src/main.ts | 11 ----------- .../src/modes/interactive/interactive-mode.ts | 15 +++++++++++++++ 3 files changed, 16 insertions(+), 11 deletions(-) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 3a857254..5f7a6d45 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -9,6 +9,7 @@ ### Fixed +- Fixed the scoped model startup hint to show the configured model-cycle keybinding ([#4508](https://github.com/earendil-works/pi/issues/4508)). - Fixed `fd` auto-download on macOS x86_64 by pinning the last release that ships an Intel macOS binary ([#4559](https://github.com/earendil-works/pi/issues/4559)). - Fixed skill diagnostics to stop warning when a skill name differs from its parent directory ([#4534](https://github.com/earendil-works/pi/issues/4534)). - Fixed prompt template argument parsing to split unquoted multiline input on newlines ([#4553](https://github.com/earendil-works/pi/issues/4553)). diff --git a/packages/coding-agent/src/main.ts b/packages/coding-agent/src/main.ts index 5a29a1fc..72e502be 100644 --- a/packages/coding-agent/src/main.ts +++ b/packages/coding-agent/src/main.ts @@ -651,7 +651,6 @@ export async function main(args: string[], options?: MainOptions) { await showDeprecationWarnings(deprecationWarnings); } - const scopedModels = [...session.scopedModels]; time("resolveModelScope"); reportDiagnostics(runtime.diagnostics); if (runtime.diagnostics.some((diagnostic) => diagnostic.type === "error")) { @@ -674,16 +673,6 @@ export async function main(args: string[], options?: MainOptions) { printTimings(); await runRpcMode(runtime); } else if (appMode === "interactive") { - if (scopedModels.length > 0 && (parsed.verbose || !settingsManager.getQuietStartup())) { - const modelList = scopedModels - .map((sm) => { - const thinkingStr = sm.thinkingLevel ? `:${sm.thinkingLevel}` : ""; - return `${sm.model.id}${thinkingStr}`; - }) - .join(", "); - console.log(chalk.dim(`Model scope: ${modelList} ${chalk.gray("(Ctrl+P to cycle)")}`)); - } - const interactiveMode = new InteractiveMode(runtime, { migratedProviders, modelFallbackMessage, diff --git a/packages/coding-agent/src/modes/interactive/interactive-mode.ts b/packages/coding-agent/src/modes/interactive/interactive-mode.ts index be1c1cb7..a493bf6f 100644 --- a/packages/coding-agent/src/modes/interactive/interactive-mode.ts +++ b/packages/coding-agent/src/modes/interactive/interactive-mode.ts @@ -572,6 +572,21 @@ export class InteractiveMode { const [fdPath] = await Promise.all([ensureTool("fd"), ensureTool("rg")]); this.fdPath = fdPath; + if (this.session.scopedModels.length > 0 && (this.options.verbose || !this.settingsManager.getQuietStartup())) { + const modelList = this.session.scopedModels + .map((sm) => { + const thinkingStr = sm.thinkingLevel ? `:${sm.thinkingLevel}` : ""; + return `${sm.model.id}${thinkingStr}`; + }) + .join(", "); + const cycleKeys = this.keybindings.getKeys("app.model.cycleForward"); + const cycleHint = + cycleKeys.length > 0 + ? theme.fg("muted", ` (${formatKeyText(cycleKeys.join("/"), { capitalize: true })} to cycle)`) + : ""; + console.log(theme.fg("dim", `Model scope: ${modelList}${cycleHint}`)); + } + // Add header container as first child this.ui.addChild(this.headerContainer); From 27a193070d73f9beb5a0b08786878cbf6e1a2eea Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 17 May 2026 00:08:51 +0200 Subject: [PATCH 061/166] fix(coding-agent): resolve pnpm global packages Fixes #4501 --- .../coding-agent/src/core/package-manager.ts | 26 ++++- .../coding-agent/test/package-manager.test.ts | 99 +++++++++++++++++++ 2 files changed, 123 insertions(+), 2 deletions(-) diff --git a/packages/coding-agent/src/core/package-manager.ts b/packages/coding-agent/src/core/package-manager.ts index 8cd1baa9..27c18fd4 100644 --- a/packages/coding-agent/src/core/package-manager.ts +++ b/packages/coding-agent/src/core/package-manager.ts @@ -1221,13 +1221,14 @@ export class DefaultPackageManager implements PackageManager { }; if (parsed.type === "npm") { - const installedPath = this.getNpmInstallPath(parsed, scope); + let installedPath = this.getNpmInstallPath(parsed, scope); const needsInstall = !existsSync(installedPath) || (parsed.pinned && !(await this.installedNpmMatchesPinnedVersion(parsed, installedPath))); if (needsInstall) { const installed = await installMissing(); if (!installed) continue; + installedPath = this.getNpmInstallPath(parsed, scope); } metadata.baseDir = installedPath; this.collectPackageResources(installedPath, accumulator, filter, metadata); @@ -1855,6 +1856,27 @@ export class DefaultPackageManager implements PackageManager { return this.globalNpmRoot; } + private getPnpmGlobalPackagePath(packageName: string): string | undefined { + const npmCommand = this.getNpmCommand(); + const commandParts = [npmCommand.command, ...npmCommand.args]; + const separatorIndex = commandParts.lastIndexOf("--"); + const packageManagerCommand = separatorIndex >= 0 ? commandParts[separatorIndex + 1] : npmCommand.command; + const packageManagerName = packageManagerCommand + ? basename(packageManagerCommand).replace(/\.(cmd|exe)$/i, "") + : ""; + if (packageManagerName !== "pnpm") { + return undefined; + } + + const output = this.runNpmCommandSync(["list", "-g", "--depth", "0", "--json"]); + const entries = JSON.parse(output) as Array<{ dependencies?: Record }>; + for (const entry of entries) { + const path = entry.dependencies?.[packageName]?.path; + if (path) return path; + } + return undefined; + } + private getNpmInstallPath(source: NpmSource, scope: SourceScope): string { if (scope === "temporary") { return join(this.getTemporaryDir("npm"), "node_modules", source.name); @@ -1862,7 +1884,7 @@ export class DefaultPackageManager implements PackageManager { if (scope === "project") { return join(this.cwd, CONFIG_DIR_NAME, "npm", "node_modules", source.name); } - return join(this.getGlobalNpmRoot(), source.name); + return this.getPnpmGlobalPackagePath(source.name) ?? join(this.getGlobalNpmRoot(), source.name); } private getGitInstallPath(source: GitSource, scope: SourceScope): string { diff --git a/packages/coding-agent/test/package-manager.test.ts b/packages/coding-agent/test/package-manager.test.ts index 18128a83..7c0fef67 100644 --- a/packages/coding-agent/test/package-manager.test.ts +++ b/packages/coding-agent/test/package-manager.test.ts @@ -798,6 +798,105 @@ Content`, expect(packageManager.getInstalledPath("npm:@scope/pkg", "user")).toBeUndefined(); expect(runCommandSyncSpy).toHaveBeenNthCalledWith(2, "mise", ["exec", "node@22", "--", "npm", "root", "-g"]); }); + + it("should resolve pnpm global package paths from pnpm list output", async () => { + settingsManager = SettingsManager.inMemory({ + npmCommand: ["pnpm"], + packages: ["npm:pnpm-pkg"], + }); + packageManager = new DefaultPackageManager({ + cwd: tempDir, + agentDir, + settingsManager, + }); + + const pnpmRoot = join(tempDir, "pnpm", "global", "v11"); + const packagePath = join(pnpmRoot, "20-hash", "node_modules", "pnpm-pkg"); + let installed = false; + + vi.spyOn(packageManager as any, "runCommandSync").mockImplementation((...callArgs: unknown[]) => { + const [command, args] = callArgs as [string, string[]]; + if (command !== "pnpm") { + throw new Error(`unexpected command ${command}`); + } + if (args.join(" ") === "list -g --depth 0 --json") { + return JSON.stringify([ + { + path: pnpmRoot, + dependencies: installed ? { "pnpm-pkg": { version: "1.0.0", path: packagePath } } : {}, + }, + ]); + } + if (args.join(" ") === "root -g") { + return pnpmRoot; + } + throw new Error(`unexpected args ${args.join(" ")}`); + }); + const runCommandSpy = vi + .spyOn(packageManager as any, "runCommand") + .mockImplementation(async (...callArgs: unknown[]) => { + const [command, args] = callArgs as [string, string[]]; + expect(command).toBe("pnpm"); + expect(args).toEqual(["install", "-g", "pnpm-pkg"]); + mkdirSync(join(packagePath, "extensions"), { recursive: true }); + writeFileSync(join(packagePath, "package.json"), JSON.stringify({ name: "pnpm-pkg", version: "1.0.0" })); + writeFileSync(join(packagePath, "extensions", "index.ts"), "export default function() {};"); + installed = true; + }); + + const first = await packageManager.resolve(); + const second = await packageManager.resolve(); + + expect(first.extensions.some((r) => r.path === join(packagePath, "extensions", "index.ts") && r.enabled)).toBe( + true, + ); + expect( + second.extensions.some((r) => r.path === join(packagePath, "extensions", "index.ts") && r.enabled), + ).toBe(true); + expect(runCommandSpy).toHaveBeenCalledTimes(1); + expect(packageManager.getInstalledPath("npm:pnpm-pkg", "user")).toBe(packagePath); + }); + + it("should resolve wrapped pnpm global package paths from pnpm list output", () => { + settingsManager = SettingsManager.inMemory({ + npmCommand: ["mise", "exec", "node@20", "--", "pnpm"], + }); + packageManager = new DefaultPackageManager({ + cwd: tempDir, + agentDir, + settingsManager, + }); + + const pnpmRoot = join(tempDir, "pnpm", "global", "v11"); + const packagePath = join(pnpmRoot, "20-hash", "node_modules", "pnpm-pkg"); + mkdirSync(packagePath, { recursive: true }); + + vi.spyOn(packageManager as any, "runCommandSync").mockImplementation((...callArgs: unknown[]) => { + const [command, args] = callArgs as [string, string[]]; + expect(command).toBe("mise"); + if (args.join(" ") === "exec node@20 -- pnpm list -g --depth 0 --json") { + return JSON.stringify([{ path: pnpmRoot, dependencies: { "pnpm-pkg": { path: packagePath } } }]); + } + throw new Error(`unexpected args ${args.join(" ")}`); + }); + + expect(packageManager.getInstalledPath("npm:pnpm-pkg", "user")).toBe(packagePath); + }); + + it("should fail when pnpm global package list is malformed", () => { + settingsManager = SettingsManager.inMemory({ + npmCommand: ["pnpm"], + }); + packageManager = new DefaultPackageManager({ + cwd: tempDir, + agentDir, + settingsManager, + }); + + vi.spyOn(packageManager as any, "runCommandSync").mockReturnValue("not json"); + + expect(() => packageManager.getInstalledPath("npm:pnpm-pkg", "user")).toThrow(); + }); }); describe("source parsing", () => { From d803768f6a283969b8f9c178d3822e9da9b7c703 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 17 May 2026 00:30:32 +0200 Subject: [PATCH 062/166] fix(deps): restore lockfile registry metadata Fixes #4315 --- package-lock.json | 832 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 832 insertions(+) diff --git a/package-lock.json b/package-lock.json index 37a2d05d..dce6b3b7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -50,6 +50,8 @@ }, "node_modules/@anthropic-ai/sandbox-runtime": { "version": "0.0.26", + "resolved": "https://registry.npmjs.org/@anthropic-ai/sandbox-runtime/-/sandbox-runtime-0.0.26.tgz", + "integrity": "sha512-DYV5LSsVMnzq0lbfaYMSpxZPUMAx4+hy343dRss+pVCLIfF62qOhxpYfZ5TmOk1GTDQm5f9wPprMNSStmnsV4w==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -69,6 +71,8 @@ }, "node_modules/@anthropic-ai/sandbox-runtime/node_modules/zod": { "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", "dev": true, "license": "MIT", "funding": { @@ -77,6 +81,8 @@ }, "node_modules/@anthropic-ai/sdk": { "version": "0.91.1", + "resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.91.1.tgz", + "integrity": "sha512-LAmu761tSN9r66ixvmciswUj/ZC+1Q4iAfpedTfSVLeswRwnY3n2Nb6Tsk+cLPP28aLOPWeMgIuTuCcMC6W/iw==", "license": "MIT", "dependencies": { "json-schema-to-ts": "^3.1.1" @@ -95,6 +101,8 @@ }, "node_modules/@aws-crypto/crc32": { "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/crc32/-/crc32-5.2.0.tgz", + "integrity": "sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/util": "^5.2.0", @@ -107,6 +115,8 @@ }, "node_modules/@aws-crypto/sha256-browser": { "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-browser/-/sha256-browser-5.2.0.tgz", + "integrity": "sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-js": "^5.2.0", @@ -120,6 +130,8 @@ }, "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/is-array-buffer": { "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -130,6 +142,8 @@ }, "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/util-buffer-from": { "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", "license": "Apache-2.0", "dependencies": { "@smithy/is-array-buffer": "^2.2.0", @@ -141,6 +155,8 @@ }, "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/util-utf8": { "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", "license": "Apache-2.0", "dependencies": { "@smithy/util-buffer-from": "^2.2.0", @@ -152,6 +168,8 @@ }, "node_modules/@aws-crypto/sha256-js": { "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-5.2.0.tgz", + "integrity": "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/util": "^5.2.0", @@ -164,6 +182,8 @@ }, "node_modules/@aws-crypto/supports-web-crypto": { "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/supports-web-crypto/-/supports-web-crypto-5.2.0.tgz", + "integrity": "sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -171,6 +191,8 @@ }, "node_modules/@aws-crypto/util": { "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-5.2.0.tgz", + "integrity": "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/types": "^3.222.0", @@ -180,6 +202,8 @@ }, "node_modules/@aws-crypto/util/node_modules/@smithy/is-array-buffer": { "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -190,6 +214,8 @@ }, "node_modules/@aws-crypto/util/node_modules/@smithy/util-buffer-from": { "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", "license": "Apache-2.0", "dependencies": { "@smithy/is-array-buffer": "^2.2.0", @@ -201,6 +227,8 @@ }, "node_modules/@aws-crypto/util/node_modules/@smithy/util-utf8": { "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", "license": "Apache-2.0", "dependencies": { "@smithy/util-buffer-from": "^2.2.0", @@ -212,6 +240,8 @@ }, "node_modules/@aws-sdk/client-bedrock-runtime": { "version": "3.1044.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-bedrock-runtime/-/client-bedrock-runtime-3.1044.0.tgz", + "integrity": "sha512-uwJB0pVZIey+kYag8xeUirMvuaDhhEHuYgSsapOKT5XCAije5pLNlg160eECJdhX9JEf0IthIM3IiHKSEkFtMw==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", @@ -268,6 +298,8 @@ }, "node_modules/@aws-sdk/core": { "version": "3.974.8", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.974.8.tgz", + "integrity": "sha512-njR2qoG6ZuB0kvAS2FyICsFZJ6gmCcf2X/7JcD14sUvGDm26wiZ5BrA6LOiUxKFEF+IVe7kdroxyE00YlkiYsw==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/types": "^3.973.8", @@ -291,6 +323,8 @@ }, "node_modules/@aws-sdk/credential-provider-env": { "version": "3.972.34", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.34.tgz", + "integrity": "sha512-XT0jtf8Fw9JE6ppsQeoNnZRiG+jqRixMT1v1ZR17G60UvVdsQmTG8nbEyHuEPfMxDXEhfdARaM/XiEhca4lGHQ==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/core": "^3.974.8", @@ -305,6 +339,8 @@ }, "node_modules/@aws-sdk/credential-provider-http": { "version": "3.972.36", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.36.tgz", + "integrity": "sha512-DPoGWfy7J7RKxvbf5kOKIGQkD2ek3dbKgzKIGrnLuvZBz5myU+Im/H6pmc14QcnFbqHMqxvtWSgRDSJW3qXLQg==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/core": "^3.974.8", @@ -324,6 +360,8 @@ }, "node_modules/@aws-sdk/credential-provider-ini": { "version": "3.972.38", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.972.38.tgz", + "integrity": "sha512-oDzUBu2MGJFgoar05sPMCwSrhw44ASyccrHzj66vO69OZqi7I6hZZxXfuPLC8OCzW7C+sU+bI73XHij41yekgQ==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/core": "^3.974.8", @@ -347,6 +385,8 @@ }, "node_modules/@aws-sdk/credential-provider-login": { "version": "3.972.38", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.38.tgz", + "integrity": "sha512-g1NosS8qe4OF++G2UFCM5ovSkgipC7YYor5KCWatG0UoMSO5YFj9C8muePlyVmOBV/WTI16Jo3/s1NUo/o1Bww==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/core": "^3.974.8", @@ -364,6 +404,8 @@ }, "node_modules/@aws-sdk/credential-provider-node": { "version": "3.972.39", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.39.tgz", + "integrity": "sha512-HEswDQyxUtadoZ/bJsPPENHg7R0Lzym5LuMksJeHvqhCOpP+rtkDLKI4/ZChH4w3cf5kG8n6bZuI8PzajoiqMg==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/credential-provider-env": "^3.972.34", @@ -385,6 +427,8 @@ }, "node_modules/@aws-sdk/credential-provider-process": { "version": "3.972.34", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.34.tgz", + "integrity": "sha512-T3IFs4EVmVi1dVN5RciFnklCANSzvrQd/VuHY9ThHSQmYkTogjcGkoJEr+oNUPQZnso52183088NqysMPji1/Q==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/core": "^3.974.8", @@ -400,6 +444,8 @@ }, "node_modules/@aws-sdk/credential-provider-sso": { "version": "3.972.38", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.972.38.tgz", + "integrity": "sha512-5ZxG+t0+3Q3QPh8KEjX6syskhgNf7I0MN7oGioTf6Lm1NTjfP7sIcYGNsthXC2qR8vcD3edNZwCr2ovfSSWuRA==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/core": "^3.974.8", @@ -417,6 +463,8 @@ }, "node_modules/@aws-sdk/credential-provider-sso/node_modules/@aws-sdk/token-providers": { "version": "3.1041.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1041.0.tgz", + "integrity": "sha512-Th7kPI6YPtvJUcdznooXJMy+9rQWjmEF81LxaJssngBzuysK4a/x+l8kjm1zb7nYsUPbndnBdUnwng/3PLvtGw==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/core": "^3.974.8", @@ -433,6 +481,8 @@ }, "node_modules/@aws-sdk/credential-provider-web-identity": { "version": "3.972.38", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.38.tgz", + "integrity": "sha512-lYHFF30DGI20jZcYX8cm6Ns0V7f1dDN6g/MBDLTyD/5iw+bXs3yBr2iAiHDkx4RFU5JgsnZvCHYKiRVPRdmOgw==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/core": "^3.974.8", @@ -449,6 +499,8 @@ }, "node_modules/@aws-sdk/eventstream-handler-node": { "version": "3.972.14", + "resolved": "https://registry.npmjs.org/@aws-sdk/eventstream-handler-node/-/eventstream-handler-node-3.972.14.tgz", + "integrity": "sha512-m4X56gxG76/CKfxNVbOFuYwnAZcHgS6HOH8lgp15HoGHIAVTcZfZrXvcYzJFOMLEJgVn+JHBu6EiNV+xSNXXFg==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/types": "^3.973.8", @@ -462,6 +514,8 @@ }, "node_modules/@aws-sdk/middleware-eventstream": { "version": "3.972.10", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-eventstream/-/middleware-eventstream-3.972.10.tgz", + "integrity": "sha512-QUqLs7Af1II9X4fCRAu+EGHG3KHyOp4RkuLhRKoA3NuFlh6TL8i+zXBl8w2LUxqm44B/Kom45hgSlwA1SpTsXQ==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/types": "^3.973.8", @@ -475,6 +529,8 @@ }, "node_modules/@aws-sdk/middleware-host-header": { "version": "3.972.10", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.972.10.tgz", + "integrity": "sha512-IJSsIMeVQ8MMCPbuh1AbltkFhLBLXn7aejzfX5YKT/VLDHn++Dcz8886tXckE+wQssyPUhaXrJhdakO2VilRhg==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/types": "^3.973.8", @@ -488,6 +544,8 @@ }, "node_modules/@aws-sdk/middleware-logger": { "version": "3.972.10", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.972.10.tgz", + "integrity": "sha512-OOuGvvz1Dm20SjZo5oEBePFqxt5nf8AwkNDSyUHvD9/bfNASmstcYxFAHUowy4n6Io7mWUZ04JURZwSBvyQanQ==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/types": "^3.973.8", @@ -500,6 +558,8 @@ }, "node_modules/@aws-sdk/middleware-recursion-detection": { "version": "3.972.11", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.972.11.tgz", + "integrity": "sha512-+zz6f79Kj9V5qFK2P+D8Ehjnw4AhphAlCAsPjUqEcInA9umtSSKMrHbSagEeOIsDNuvVrH98bjRHcyQukTrhaQ==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/types": "^3.973.8", @@ -514,6 +574,8 @@ }, "node_modules/@aws-sdk/middleware-sdk-s3": { "version": "3.972.37", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.972.37.tgz", + "integrity": "sha512-Km7M+i8DrLArVzrid1gfxeGhYHBd3uxvE77g0s5a52zPSVosxzQBnJ0gwWb6NIp/DOk8gsBMhi7V+cpJG0ndTA==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/core": "^3.974.8", @@ -537,6 +599,8 @@ }, "node_modules/@aws-sdk/middleware-user-agent": { "version": "3.972.38", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.972.38.tgz", + "integrity": "sha512-iz+B29TXcAZsJpwB+AwG/TTGA5l/VnmMZ2UxtiySOZjI6gCdmviXPwdgzcmuazMy16rXoPY4mYCGe7zdNKfx5A==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/core": "^3.974.8", @@ -554,6 +618,8 @@ }, "node_modules/@aws-sdk/middleware-websocket": { "version": "3.972.16", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-websocket/-/middleware-websocket-3.972.16.tgz", + "integrity": "sha512-86+S9oCyRVGzoMRpQhxkArp7kD2K75GPmaNevd9B6EyNhWoNvnCZZ3WbgN4j7ZT+jvtvBCGZvI2XHsWZJ+BRIg==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/types": "^3.973.8", @@ -575,6 +641,8 @@ }, "node_modules/@aws-sdk/nested-clients": { "version": "3.997.6", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.997.6.tgz", + "integrity": "sha512-WBDnqatJl+kGObpfmfSxqnXeYTu3Me8wx8WCtvoxX3pfWrrTv8I4WTMSSs7PZqcRcVh8WeUKMgGFjMG+52SR1w==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", @@ -623,6 +691,8 @@ }, "node_modules/@aws-sdk/region-config-resolver": { "version": "3.972.13", + "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.972.13.tgz", + "integrity": "sha512-CvJ2ZIjK/jVD/lbOpowBVElJyC1YxLTIJ13yM0AEo0t2v7swOzGjSA6lJGH+DwZXQhcjUjoYwc8bVYCX5MDr1A==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/types": "^3.973.8", @@ -637,6 +707,8 @@ }, "node_modules/@aws-sdk/signature-v4-multi-region": { "version": "3.996.25", + "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.996.25.tgz", + "integrity": "sha512-+CMIt3e1VzlklAECmG+DtP1sV8iKq25FuA0OKpnJ4KA0kxUtd7CgClY7/RU6VzJBQwbN4EJ9Ue6plvqx1qGadw==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/middleware-sdk-s3": "^3.972.37", @@ -652,6 +724,8 @@ }, "node_modules/@aws-sdk/token-providers": { "version": "3.1044.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1044.0.tgz", + "integrity": "sha512-uvpWTfpzOM9qVrR0kdAjzBbvv2ERW7S1CKeBeRkHyHy21Ext5fNReIVrbAzhjuVC7UxRyZQ8PHYW/3T83hWlbg==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/core": "^3.974.8", @@ -668,6 +742,8 @@ }, "node_modules/@aws-sdk/types": { "version": "3.973.8", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.973.8.tgz", + "integrity": "sha512-gjlAdtHMbtR9X5iIhVUvbVcy55KnznpC6bkDUWW9z915bi0ckdUr5cjf16Kp6xq0bP5HBD2xzgbL9F9Quv5vUw==", "license": "Apache-2.0", "dependencies": { "@smithy/types": "^4.14.1", @@ -679,6 +755,8 @@ }, "node_modules/@aws-sdk/util-arn-parser": { "version": "3.972.3", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-arn-parser/-/util-arn-parser-3.972.3.tgz", + "integrity": "sha512-HzSD8PMFrvgi2Kserxuff5VitNq2sgf3w9qxmskKDiDTThWfVteJxuCS9JXiPIPtmCrp+7N9asfIaVhBFORllA==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -689,6 +767,8 @@ }, "node_modules/@aws-sdk/util-endpoints": { "version": "3.996.8", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.996.8.tgz", + "integrity": "sha512-oOZHcRDihk5iEe5V25NVWg45b3qEA8OpHWVdU/XQh8Zj4heVPAJqWvMphQnU7LkufmUo10EpvFPZuQMiFLJK3g==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/types": "^3.973.8", @@ -703,6 +783,8 @@ }, "node_modules/@aws-sdk/util-format-url": { "version": "3.972.10", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-format-url/-/util-format-url-3.972.10.tgz", + "integrity": "sha512-DEKiHNJVtNxdyTeQspzY+15Po/kHm6sF0Cs4HV9Q2+lplB63+DrvdeiSoOSdWEWAoO2RcY1veoXVDz2tWxWCgQ==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/types": "^3.973.8", @@ -716,6 +798,8 @@ }, "node_modules/@aws-sdk/util-locate-window": { "version": "3.965.5", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.965.5.tgz", + "integrity": "sha512-WhlJNNINQB+9qtLtZJcpQdgZw3SCDCpXdUJP7cToGwHbCWCnRckGlc6Bx/OhWwIYFNAn+FIydY8SZ0QmVu3xTQ==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -726,6 +810,8 @@ }, "node_modules/@aws-sdk/util-user-agent-browser": { "version": "3.972.10", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.972.10.tgz", + "integrity": "sha512-FAzqXvfEssGdSIz8ejatan0bOdx1qefBWKF/gWmVBXIP1HkS7v/wjjaqrAGGKvyihrXTXW00/2/1nTJtxpXz7g==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/types": "^3.973.8", @@ -736,6 +822,8 @@ }, "node_modules/@aws-sdk/util-user-agent-node": { "version": "3.973.24", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.973.24.tgz", + "integrity": "sha512-ZWwlkjcIp7cEL8ZfTpTAPNkwx25p7xol0xlKoWVVf22+nsjwmLcHYtTPjIV1cSpmB/b6DaK4cb1fSkvCXHgRdw==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/middleware-user-agent": "^3.972.38", @@ -759,6 +847,8 @@ }, "node_modules/@aws-sdk/xml-builder": { "version": "3.972.22", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.22.tgz", + "integrity": "sha512-PMYKKtJd70IsSG0yHrdAbxBr+ZWBKLvzFZfD3/urxgf6hXVMzuU5M+3MJ5G67RpOmLBu1fAUN65SbWuKUCOlAA==", "license": "Apache-2.0", "dependencies": { "@nodable/entities": "2.1.0", @@ -772,6 +862,8 @@ }, "node_modules/@aws/lambda-invoke-store": { "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.2.4.tgz", + "integrity": "sha512-iY8yvjE0y651BixKNPgmv1WrQc+GZ142sb0z4gYnChDDY2YqI4P/jsSopBWrKfAt7LOJAkOXt7rC/hms+WclQQ==", "license": "Apache-2.0", "engines": { "node": ">=18.0.0" @@ -815,6 +907,8 @@ }, "node_modules/@babel/runtime": { "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz", + "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==", "license": "MIT", "engines": { "node": ">=6.9.0" @@ -846,6 +940,8 @@ }, "node_modules/@biomejs/biome": { "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-2.3.5.tgz", + "integrity": "sha512-HvLhNlIlBIbAV77VysRIBEwp55oM/QAjQEin74QQX9Xb259/XP/D5AGGnZMOyF1el4zcvlNYYR3AyTMUV3ILhg==", "dev": true, "license": "MIT OR Apache-2.0", "bin": { @@ -871,6 +967,8 @@ }, "node_modules/@biomejs/cli-darwin-arm64": { "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.3.5.tgz", + "integrity": "sha512-fLdTur8cJU33HxHUUsii3GLx/TR0BsfQx8FkeqIiW33cGMtUD56fAtrh+2Fx1uhiCsVZlFh6iLKUU3pniZREQw==", "cpu": [ "arm64" ], @@ -1093,6 +1191,8 @@ }, "node_modules/@esbuild/darwin-arm64": { "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", + "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", "cpu": [ "arm64" ], @@ -1465,6 +1565,8 @@ }, "node_modules/@google/genai": { "version": "1.52.0", + "resolved": "https://registry.npmjs.org/@google/genai/-/genai-1.52.0.tgz", + "integrity": "sha512-gwSvbpiN/17O9TbsqSsE/OzZcpv5Fo4RQjdngGgogtuB9RsyJ8ZHhX5KjHj1bp5N9snN2eK8LDGXSaWW2hof8Q==", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { @@ -1571,6 +1673,8 @@ }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", "dev": true, "license": "MIT", "dependencies": { @@ -1580,6 +1684,8 @@ }, "node_modules/@jridgewell/remapping": { "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1589,6 +1695,8 @@ }, "node_modules/@jridgewell/resolve-uri": { "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", "dev": true, "license": "MIT", "engines": { @@ -1597,11 +1705,15 @@ }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", "dev": true, "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", "dev": true, "license": "MIT", "dependencies": { @@ -1611,10 +1723,14 @@ }, "node_modules/@lit-labs/ssr-dom-shim": { "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@lit-labs/ssr-dom-shim/-/ssr-dom-shim-1.5.1.tgz", + "integrity": "sha512-Aou5UdlSpr5whQe8AA/bZG0jMj96CoJIWbGfZ91qieWu5AWUMKw8VR/pAkQkJYvBNhmCcWnZlyyk5oze8JIqYA==", "license": "BSD-3-Clause" }, "node_modules/@lit/reactive-element": { "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@lit/reactive-element/-/reactive-element-2.1.2.tgz", + "integrity": "sha512-pbCDiVMnne1lYUIaYNN5wrwQXDtHaYtg7YEFPeW+hws6U47WeFvISGUWekPGKWOP1ygrs0ef0o1VJMk1exos5A==", "license": "BSD-3-Clause", "dependencies": { "@lit-labs/ssr-dom-shim": "^1.5.0" @@ -1622,6 +1738,8 @@ }, "node_modules/@lmstudio/lms-isomorphic": { "version": "0.4.6", + "resolved": "https://registry.npmjs.org/@lmstudio/lms-isomorphic/-/lms-isomorphic-0.4.6.tgz", + "integrity": "sha512-v0LIjXKnDe3Ff3XZO5eQjlVxTjleUHXaom14MV7QU9bvwaoo3l5p71+xJ3mmSaqZq370CQ6pTKCn1Bb7Jf+VwQ==", "license": "Apache-2.0", "dependencies": { "ws": "^8.16.0" @@ -1629,6 +1747,8 @@ }, "node_modules/@lmstudio/sdk": { "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@lmstudio/sdk/-/sdk-1.5.0.tgz", + "integrity": "sha512-fdY12x4hb14PEjYijh7YeCqT1ZDY5Ok6VR4l4+E/dI+F6NW8oB+P83Sxed5vqE4XgTzbgyPuSR2ZbMNxxF+6jA==", "license": "Apache-2.0", "dependencies": { "@lmstudio/lms-isomorphic": "^0.4.6", @@ -1640,6 +1760,8 @@ }, "node_modules/@lmstudio/sdk/node_modules/chalk": { "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "license": "MIT", "dependencies": { "ansi-styles": "^4.1.0", @@ -1654,6 +1776,8 @@ }, "node_modules/@lmstudio/sdk/node_modules/supports-color": { "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "license": "MIT", "dependencies": { "has-flag": "^4.0.0" @@ -1664,6 +1788,8 @@ }, "node_modules/@lmstudio/sdk/node_modules/zod": { "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" @@ -1671,6 +1797,8 @@ }, "node_modules/@mariozechner/clipboard": { "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard/-/clipboard-0.3.5.tgz", + "integrity": "sha512-D3F+UrU9CR7roJt0zDLp6Oc+4/KlLDIrN4frH+6V90SJNW2KKUec1oCQIPaaDjCqeOsQyX9dyqYbImIQIM45PA==", "license": "MIT", "optional": true, "engines": { @@ -1691,6 +1819,8 @@ }, "node_modules/@mariozechner/clipboard-darwin-arm64": { "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-darwin-arm64/-/clipboard-darwin-arm64-0.3.2.tgz", + "integrity": "sha512-uBf6K7Je1ihsgvmWxA8UCGCeI+nbRVRXoarZdLjl6slz94Zs1tNKFZqx7aCI5O1i3e0B6ja82zZ06BWrl0MCVw==", "cpu": [ "arm64" ], @@ -1705,6 +1835,8 @@ }, "node_modules/@mariozechner/clipboard-darwin-universal": { "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-darwin-universal/-/clipboard-darwin-universal-0.3.2.tgz", + "integrity": "sha512-mxSheKTW2U9LsBdXy0SdmdCAE5HqNS9QUmpNHLnfJ+SsbFKALjEZc5oRrVMXxGQSirDvYf5bjmRyT0QYYonnlg==", "license": "MIT", "optional": true, "os": [ @@ -1844,6 +1976,8 @@ }, "node_modules/@mariozechner/mini-lit": { "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@mariozechner/mini-lit/-/mini-lit-0.2.1.tgz", + "integrity": "sha512-u300euLgCsDDlb8o2Wbz+55eSJga5X2vB58s9XBuFIr2Bi3iI+GMR7t/NYo/O6Vr6obXShXgYjR3SRUJVgo+kQ==", "dependencies": { "@preact/signals-core": "^1.12.1", "class-variance-authority": "^0.7.1", @@ -1863,6 +1997,8 @@ }, "node_modules/@mariozechner/mini-lit/node_modules/highlight.js": { "version": "11.11.1", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.11.1.tgz", + "integrity": "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==", "license": "BSD-3-Clause", "engines": { "node": ">=12.0.0" @@ -1870,6 +2006,8 @@ }, "node_modules/@mariozechner/mini-lit/node_modules/marked": { "version": "16.4.2", + "resolved": "https://registry.npmjs.org/marked/-/marked-16.4.2.tgz", + "integrity": "sha512-TI3V8YYWvkVf3KJe1dRkpnjs68JUPyEa5vjKrp1XEEJUAOaQc+Qj+L1qWbPd0SJuAdQkFU0h73sXXqwDYxsiDA==", "license": "MIT", "bin": { "marked": "bin/marked.js" @@ -1880,6 +2018,8 @@ }, "node_modules/@mistralai/mistralai": { "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@mistralai/mistralai/-/mistralai-2.2.1.tgz", + "integrity": "sha512-uKU8CZmL2RzYKmplsU01hii4p3pe4HqJefpWNRWXm1Tcm0Sm4xXfwSLIy4k7ZCPlbETCGcp69E7hZs+WOJ5itQ==", "license": "Apache-2.0", "dependencies": { "ws": "^8.18.0", @@ -1889,6 +2029,8 @@ }, "node_modules/@napi-rs/canvas": { "version": "0.1.100", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas/-/canvas-0.1.100.tgz", + "integrity": "sha512-xglYA6q3XO5P3BNJYxVZ1IV7DLVjp1Py6nwag88YntrS+3vKHyYcMqXVS4ZztJmwz2uGvz1FWhI/4LgbR5uQDA==", "license": "MIT", "optional": true, "workspaces": [ @@ -1937,6 +2079,8 @@ }, "node_modules/@napi-rs/canvas-darwin-arm64": { "version": "0.1.100", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-arm64/-/canvas-darwin-arm64-0.1.100.tgz", + "integrity": "sha512-2PcswRaC7Ly645DGt88///zuFDhJxJYdKAs1uU3mfk1atYkXufgcgLfBpk6Tm12nCQBaNt1wpybuPZ4qOhTo8A==", "cpu": [ "arm64" ], @@ -2135,6 +2279,8 @@ }, "node_modules/@nodable/entities": { "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@nodable/entities/-/entities-2.1.0.tgz", + "integrity": "sha512-nyT7T3nbMyBI/lvr6L5TyWbFJAI9FTgVRakNoBqCD+PmID8DzFrrNdLLtHMwMszOtqZa8PAOV24ZqDnQrhQINA==", "funding": [ { "type": "github", @@ -2145,6 +2291,8 @@ }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", "dev": true, "license": "MIT", "dependencies": { @@ -2157,6 +2305,8 @@ }, "node_modules/@nodelib/fs.stat": { "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", "dev": true, "license": "MIT", "engines": { @@ -2165,6 +2315,8 @@ }, "node_modules/@nodelib/fs.walk": { "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", "dev": true, "license": "MIT", "dependencies": { @@ -2177,6 +2329,8 @@ }, "node_modules/@parcel/watcher": { "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.6.tgz", + "integrity": "sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -2232,6 +2386,8 @@ }, "node_modules/@parcel/watcher-darwin-arm64": { "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.6.tgz", + "integrity": "sha512-Z2ZdrnwyXvvvdtRHLmM4knydIdU9adO3D4n/0cVipF3rRiwP+3/sfzpAwA/qKFL6i1ModaabkU7IbpeMBgiVEA==", "cpu": [ "arm64" ], @@ -2482,6 +2638,8 @@ }, "node_modules/@parcel/watcher/node_modules/picomatch": { "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", "engines": { @@ -2504,11 +2662,15 @@ }, "node_modules/@pondwader/socks5-server": { "version": "1.0.10", + "resolved": "https://registry.npmjs.org/@pondwader/socks5-server/-/socks5-server-1.0.10.tgz", + "integrity": "sha512-bQY06wzzR8D2+vVCUoBsr5QS2U6UgPUQRmErNwtsuI6vLcyRKkafjkr3KxbtGFf9aBBIV2mcvlsKD1UYaIV+sg==", "dev": true, "license": "MIT" }, "node_modules/@preact/signals-core": { "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@preact/signals-core/-/signals-core-1.14.1.tgz", + "integrity": "sha512-vxPpfXqrwUe9lpjqfYNjAF/0RF/eFGeLgdJzdmIIZjpOnTmGmAB4BjWone562mJGMRP4frU6iZ6ei3PDsu52Ng==", "license": "MIT", "funding": { "type": "opencollective", @@ -2517,22 +2679,32 @@ }, "node_modules/@protobufjs/aspromise": { "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", "license": "BSD-3-Clause" }, "node_modules/@protobufjs/base64": { "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", "license": "BSD-3-Clause" }, "node_modules/@protobufjs/codegen": { "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.5.tgz", + "integrity": "sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==", "license": "BSD-3-Clause" }, "node_modules/@protobufjs/eventemitter": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==", "license": "BSD-3-Clause" }, "node_modules/@protobufjs/fetch": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", "license": "BSD-3-Clause", "dependencies": { "@protobufjs/aspromise": "^1.1.1", @@ -2541,22 +2713,32 @@ }, "node_modules/@protobufjs/float": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", "license": "BSD-3-Clause" }, "node_modules/@protobufjs/inquire": { "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.1.tgz", + "integrity": "sha512-mnzgDV26ueAvk7rsbt9L7bE0SuAoqyuys/sMMrmVcN5x9VsxpcG3rqAUSgDyLp0UZlmNfIbQ4fHfCtreVBk8Ew==", "license": "BSD-3-Clause" }, "node_modules/@protobufjs/path": { "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", "license": "BSD-3-Clause" }, "node_modules/@protobufjs/pool": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", "license": "BSD-3-Clause" }, "node_modules/@protobufjs/utf8": { "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.1.tgz", + "integrity": "sha512-oOAWABowe8EAbMyWKM0tYDKi8Yaox52D+HWZhAIJqQXbqe0xI/GV7FhLWqlEKreMkfDjshR5FKgi3mnle0h6Eg==", "license": "BSD-3-Clause" }, "node_modules/@rollup/rollup-android-arm-eabi": { @@ -2589,6 +2771,8 @@ }, "node_modules/@rollup/rollup-darwin-arm64": { "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.3.tgz", + "integrity": "sha512-vo6Y5Qfpx7/5EaamIwi0WqW2+zfiusVihKatLvtN1VFVy3D13uERk/6gZLU1UiHRL6fDXqj/ELIeVRGnvcTE1g==", "cpu": [ "arm64" ], @@ -2909,10 +3093,14 @@ }, "node_modules/@silvia-odwyer/photon-node": { "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@silvia-odwyer/photon-node/-/photon-node-0.3.4.tgz", + "integrity": "sha512-bnly4BKB3KDTFxrUIcgCLbaeVVS8lrAkri1pEzskpmxu9MdfGQTy8b8EgcD83ywD3RPMsIulY8xJH5Awa+t9fA==", "license": "Apache-2.0" }, "node_modules/@smithy/config-resolver": { "version": "4.4.17", + "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-4.4.17.tgz", + "integrity": "sha512-TzDZcAnhTyAHbXVxWZo7/tEcrIeFq20IBk8So3OLOetWpR8EwY/yEqBMBFaJMeyEiREDq4NfEl+qO3OAUD+vbQ==", "license": "Apache-2.0", "dependencies": { "@smithy/node-config-provider": "^4.3.14", @@ -2928,6 +3116,8 @@ }, "node_modules/@smithy/core": { "version": "3.23.17", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.23.17.tgz", + "integrity": "sha512-x7BlLbUFL8NWCGjMF9C+1N5cVCxcPa7g6Tv9B4A2luWx3be3oU8hQ96wIwxe/s7OhIzvoJH73HAUSg5JXVlEtQ==", "license": "Apache-2.0", "dependencies": { "@smithy/protocol-http": "^5.3.14", @@ -2947,6 +3137,8 @@ }, "node_modules/@smithy/credential-provider-imds": { "version": "4.2.14", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.2.14.tgz", + "integrity": "sha512-Au28zBN48ZAoXdooGUHemuVBrkE+Ie6RPmGNIAJsFqj33Vhb6xAgRifUydZ2aY+M+KaMAETAlKk5NC5h1G7wpg==", "license": "Apache-2.0", "dependencies": { "@smithy/node-config-provider": "^4.3.14", @@ -2961,6 +3153,8 @@ }, "node_modules/@smithy/eventstream-codec": { "version": "4.2.14", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-codec/-/eventstream-codec-4.2.14.tgz", + "integrity": "sha512-erZq0nOIpzfeZdCyzZjdJb4nVSKLUmSkaQUVkRGQTXs30gyUGeKnrYEg+Xe1W5gE3aReS7IgsvANwVPxSzY6Pw==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/crc32": "5.2.0", @@ -2974,6 +3168,8 @@ }, "node_modules/@smithy/eventstream-serde-browser": { "version": "4.2.14", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-browser/-/eventstream-serde-browser-4.2.14.tgz", + "integrity": "sha512-8IelTCtTctWRbb+0Dcy+C0aICh1qa0qWXqgjcXDmMuCvPJRnv26hiDZoAau2ILOniki65mCPKqOQs/BaWvO4CQ==", "license": "Apache-2.0", "dependencies": { "@smithy/eventstream-serde-universal": "^4.2.14", @@ -2986,6 +3182,8 @@ }, "node_modules/@smithy/eventstream-serde-config-resolver": { "version": "4.3.14", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-config-resolver/-/eventstream-serde-config-resolver-4.3.14.tgz", + "integrity": "sha512-sqHiHpYRYo3FJlaIxD1J8PhbcmJAm7IuM16mVnwSkCToD7g00IBZzKuiLNMGmftULmEUX6/UAz8/NN5uMP8bVA==", "license": "Apache-2.0", "dependencies": { "@smithy/types": "^4.14.1", @@ -2997,6 +3195,8 @@ }, "node_modules/@smithy/eventstream-serde-node": { "version": "4.2.14", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-node/-/eventstream-serde-node-4.2.14.tgz", + "integrity": "sha512-Ht/8BuGlKfFTy0H3+8eEu0vdpwGztCnaLLXtpXNdQqiR7Hj4vFScU3T436vRAjATglOIPjJXronY+1WxxNLSiw==", "license": "Apache-2.0", "dependencies": { "@smithy/eventstream-serde-universal": "^4.2.14", @@ -3009,6 +3209,8 @@ }, "node_modules/@smithy/eventstream-serde-universal": { "version": "4.2.14", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-universal/-/eventstream-serde-universal-4.2.14.tgz", + "integrity": "sha512-lWyt4T2XQZUZgK3tQ3Wn0w3XBvZsK/vjTuJl6bXbnGZBHH0ZUSONTYiK9TgjTTzU54xQr3DRFwpjmhp0oLm3gg==", "license": "Apache-2.0", "dependencies": { "@smithy/eventstream-codec": "^4.2.14", @@ -3021,6 +3223,8 @@ }, "node_modules/@smithy/fetch-http-handler": { "version": "5.3.17", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.3.17.tgz", + "integrity": "sha512-bXOvQzaSm6MnmLaWA1elgfQcAtN4UP3vXqV97bHuoOrHQOJiLT3ds6o9eo5bqd0TJfRFpzdGnDQdW3FACiAVdw==", "license": "Apache-2.0", "dependencies": { "@smithy/protocol-http": "^5.3.14", @@ -3035,6 +3239,8 @@ }, "node_modules/@smithy/hash-node": { "version": "4.2.14", + "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-4.2.14.tgz", + "integrity": "sha512-8ZBDY2DD4wr+GGjTpPtiglEsqr0lUP+KHqgZcWczFf6qeZ/YRjMIOoQWVQlmwu7EtxKTd8YXD8lblmYcpBIA1g==", "license": "Apache-2.0", "dependencies": { "@smithy/types": "^4.14.1", @@ -3048,6 +3254,8 @@ }, "node_modules/@smithy/invalid-dependency": { "version": "4.2.14", + "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-4.2.14.tgz", + "integrity": "sha512-c21qJiTSb25xvvOp+H2TNZzPCngrvl5vIPqPB8zQ/DmJF4QWXO19x1dWfMJZ6wZuuWUPPm0gV8C0cU3+ifcWuw==", "license": "Apache-2.0", "dependencies": { "@smithy/types": "^4.14.1", @@ -3059,6 +3267,8 @@ }, "node_modules/@smithy/is-array-buffer": { "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-4.2.2.tgz", + "integrity": "sha512-n6rQ4N8Jj4YTQO3YFrlgZuwKodf4zUFs7EJIWH86pSCWBaAtAGBFfCM7Wx6D2bBJ2xqFNxGBSrUWswT3M0VJow==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -3069,6 +3279,8 @@ }, "node_modules/@smithy/middleware-content-length": { "version": "4.2.14", + "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-4.2.14.tgz", + "integrity": "sha512-xhHq7fX4/3lv5NHxLUk3OeEvl0xZ+Ek3qIbWaCL4f9JwgDZEclPBElljaZCAItdGPQl/kSM4LPMOpy1MYgprpw==", "license": "Apache-2.0", "dependencies": { "@smithy/protocol-http": "^5.3.14", @@ -3081,6 +3293,8 @@ }, "node_modules/@smithy/middleware-endpoint": { "version": "4.4.32", + "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.4.32.tgz", + "integrity": "sha512-ZZkgyjnJppiZbIm6Qbx92pbXYi1uzenIvGhBSCDlc7NwuAkiqSgS75j1czAD25ZLs2FjMjYy1q7gyRVWG6JA0Q==", "license": "Apache-2.0", "dependencies": { "@smithy/core": "^3.23.17", @@ -3098,6 +3312,8 @@ }, "node_modules/@smithy/middleware-retry": { "version": "4.5.7", + "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.5.7.tgz", + "integrity": "sha512-bRt6ZImqVSeTk39Nm81K20ObIiAZ3WefY7G6+iz/0tZjs4dgRRjvRX2sgsH+zi6iDCRR/aQvQofLKxxz4rPBZg==", "license": "Apache-2.0", "dependencies": { "@smithy/core": "^3.23.17", @@ -3117,6 +3333,8 @@ }, "node_modules/@smithy/middleware-serde": { "version": "4.2.20", + "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-4.2.20.tgz", + "integrity": "sha512-Lx9JMO9vArPtiChE3wbEZ5akMIDQpWQtlu90lhACQmNOXcGXRbaDywMHDzuDZ2OkZzP+9wQfZi3YJT9F67zTQQ==", "license": "Apache-2.0", "dependencies": { "@smithy/core": "^3.23.17", @@ -3130,6 +3348,8 @@ }, "node_modules/@smithy/middleware-stack": { "version": "4.2.14", + "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-4.2.14.tgz", + "integrity": "sha512-2dvkUKLuFdKsCRmOE4Mn63co0Djtsm+JMh0bYZQupN1pJwMeE8FmQmRLLzzEMN0dnNi7CDCYYH8F0EVwWiPBeA==", "license": "Apache-2.0", "dependencies": { "@smithy/types": "^4.14.1", @@ -3141,6 +3361,8 @@ }, "node_modules/@smithy/node-config-provider": { "version": "4.3.14", + "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-4.3.14.tgz", + "integrity": "sha512-S+gFjyo/weSVL0P1b9Ts8C/CwIfNCgUPikk3sl6QVsfE/uUuO+QsF+NsE/JkpvWqqyz1wg7HFdiaZuj5CoBMRg==", "license": "Apache-2.0", "dependencies": { "@smithy/property-provider": "^4.2.14", @@ -3154,6 +3376,8 @@ }, "node_modules/@smithy/node-http-handler": { "version": "4.6.1", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.6.1.tgz", + "integrity": "sha512-iB+orM4x3xrr57X3YaXazfKnntl0LHlZB1kcXSGzMV1Tt0+YwEjGlbjk/44qEGtBzXAz6yFDzkYTKSV6Pj2HUg==", "license": "Apache-2.0", "dependencies": { "@smithy/protocol-http": "^5.3.14", @@ -3167,6 +3391,8 @@ }, "node_modules/@smithy/property-provider": { "version": "4.2.14", + "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-4.2.14.tgz", + "integrity": "sha512-WuM31CgfsnQ/10i7NYr0PyxqknD72Y5uMfUMVSniPjbEPceiTErb4eIqJQ+pdxNEAUEWrewrGjIRjVbVHsxZiQ==", "license": "Apache-2.0", "dependencies": { "@smithy/types": "^4.14.1", @@ -3178,6 +3404,8 @@ }, "node_modules/@smithy/protocol-http": { "version": "5.3.14", + "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.3.14.tgz", + "integrity": "sha512-dN5F8kHx8RNU0r+pCwNmFZyz6ChjMkzShy/zup6MtkRmmix4vZzJdW+di7x//b1LiynIev88FM18ie+wwPcQtQ==", "license": "Apache-2.0", "dependencies": { "@smithy/types": "^4.14.1", @@ -3189,6 +3417,8 @@ }, "node_modules/@smithy/querystring-builder": { "version": "4.2.14", + "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-4.2.14.tgz", + "integrity": "sha512-XYA5Z0IqTeF+5XDdh4BBmSA0HvbgVZIyv4cmOoUheDNR57K1HgBp9ukUMx3Cr3XpDHHpLBnexPE3LAtDsZkj2A==", "license": "Apache-2.0", "dependencies": { "@smithy/types": "^4.14.1", @@ -3201,6 +3431,8 @@ }, "node_modules/@smithy/querystring-parser": { "version": "4.2.14", + "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-4.2.14.tgz", + "integrity": "sha512-hr+YyqBD23GVvRxGGrcc/oOeNlK3PzT5Fu4dzrDXxzS1LpFiuL2PQQqKPs87M79aW7ziMs+nvB3qdw77SqE7Lw==", "license": "Apache-2.0", "dependencies": { "@smithy/types": "^4.14.1", @@ -3212,6 +3444,8 @@ }, "node_modules/@smithy/service-error-classification": { "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-4.3.1.tgz", + "integrity": "sha512-aUQuDGh760ts/8MU+APjIZhlLPKhIIfqyzZaJikLEIMrdxFvxuLYD0WxWzaYWpmLbQlXDe9p7EWM3HsBe0K6Gw==", "license": "Apache-2.0", "dependencies": { "@smithy/types": "^4.14.1" @@ -3222,6 +3456,8 @@ }, "node_modules/@smithy/shared-ini-file-loader": { "version": "4.4.9", + "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-4.4.9.tgz", + "integrity": "sha512-495/V2I15SHgedSJoDPD23JuSfKAp726ZI1V0wtjB07Wh7q/0tri/0e0DLefZCHgxZonrGKt/OCTpAtP1wE1kQ==", "license": "Apache-2.0", "dependencies": { "@smithy/types": "^4.14.1", @@ -3233,6 +3469,8 @@ }, "node_modules/@smithy/signature-v4": { "version": "5.3.14", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.3.14.tgz", + "integrity": "sha512-1D9Y/nmlVjCeSivCbhZ7hgEpmHyY1h0GvpSZt3l0xcD9JjmjVC1CHOozS6+Gh+/ldMH8JuJ6cujObQqfayAVFA==", "license": "Apache-2.0", "dependencies": { "@smithy/is-array-buffer": "^4.2.2", @@ -3250,6 +3488,8 @@ }, "node_modules/@smithy/smithy-client": { "version": "4.12.13", + "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.12.13.tgz", + "integrity": "sha512-y/Pcj1V9+qG98gyu1gvftHB7rDpdh+7kIBIggs55yGm3JdtBV8GT8IFF3a1qxZ79QnaJHX9GXzvBG6tAd+czJA==", "license": "Apache-2.0", "dependencies": { "@smithy/core": "^3.23.17", @@ -3266,6 +3506,8 @@ }, "node_modules/@smithy/types": { "version": "4.14.1", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.14.1.tgz", + "integrity": "sha512-59b5HtSVrVR/eYNei3BUj3DCPKD/G7EtDDe7OEJE7i7FtQFugYo6MxbotS8mVJkLNVf8gYaAlEBwwtJ9HzhWSg==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -3276,6 +3518,8 @@ }, "node_modules/@smithy/url-parser": { "version": "4.2.14", + "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-4.2.14.tgz", + "integrity": "sha512-p06BiBigJ8bTA3MgnOfCtDUWnAMY0YfedO/GRpmc7p+wg3KW8vbXy1xwSu5ASy0wV7rRYtlfZOIKH4XqfhjSQQ==", "license": "Apache-2.0", "dependencies": { "@smithy/querystring-parser": "^4.2.14", @@ -3288,6 +3532,8 @@ }, "node_modules/@smithy/util-base64": { "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@smithy/util-base64/-/util-base64-4.3.2.tgz", + "integrity": "sha512-XRH6b0H/5A3SgblmMa5ErXQ2XKhfbQB+Fm/oyLZ2O2kCUrwgg55bU0RekmzAhuwOjA9qdN5VU2BprOvGGUkOOQ==", "license": "Apache-2.0", "dependencies": { "@smithy/util-buffer-from": "^4.2.2", @@ -3300,6 +3546,8 @@ }, "node_modules/@smithy/util-body-length-browser": { "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-browser/-/util-body-length-browser-4.2.2.tgz", + "integrity": "sha512-JKCrLNOup3OOgmzeaKQwi4ZCTWlYR5H4Gm1r2uTMVBXoemo1UEghk5vtMi1xSu2ymgKVGW631e2fp9/R610ZjQ==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -3310,6 +3558,8 @@ }, "node_modules/@smithy/util-body-length-node": { "version": "4.2.3", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-node/-/util-body-length-node-4.2.3.tgz", + "integrity": "sha512-ZkJGvqBzMHVHE7r/hcuCxlTY8pQr1kMtdsVPs7ex4mMU+EAbcXppfo5NmyxMYi2XU49eqaz56j2gsk4dHHPG/g==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -3320,6 +3570,8 @@ }, "node_modules/@smithy/util-buffer-from": { "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-4.2.2.tgz", + "integrity": "sha512-FDXD7cvUoFWwN6vtQfEta540Y/YBe5JneK3SoZg9bThSoOAC/eGeYEua6RkBgKjGa/sz6Y+DuBZj3+YEY21y4Q==", "license": "Apache-2.0", "dependencies": { "@smithy/is-array-buffer": "^4.2.2", @@ -3331,6 +3583,8 @@ }, "node_modules/@smithy/util-config-provider": { "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/util-config-provider/-/util-config-provider-4.2.2.tgz", + "integrity": "sha512-dWU03V3XUprJwaUIFVv4iOnS1FC9HnMHDfUrlNDSh4315v0cWyaIErP8KiqGVbf5z+JupoVpNM7ZB3jFiTejvQ==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -3341,6 +3595,8 @@ }, "node_modules/@smithy/util-defaults-mode-browser": { "version": "4.3.49", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.3.49.tgz", + "integrity": "sha512-a5bNrdiONYB/qE2BuKegvUMd/+ZDwdg4vsNuuSzYE8qs2EYAdK9CynL+Rzn29PbPiUqoz/cbpRbcLzD5lEevHw==", "license": "Apache-2.0", "dependencies": { "@smithy/property-provider": "^4.2.14", @@ -3354,6 +3610,8 @@ }, "node_modules/@smithy/util-defaults-mode-node": { "version": "4.2.54", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.2.54.tgz", + "integrity": "sha512-g1cvrJvOnzeJgEdf7AE4luI7gp6L8weE0y9a9wQUSGtjb8QRHDbCJYuE4Sy0SD9N8RrnNPFsPltAz/OSoBR9Zw==", "license": "Apache-2.0", "dependencies": { "@smithy/config-resolver": "^4.4.17", @@ -3370,6 +3628,8 @@ }, "node_modules/@smithy/util-endpoints": { "version": "3.4.2", + "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-3.4.2.tgz", + "integrity": "sha512-a55Tr+3OKld4TTtnT+RhKOQHyPxm3j/xL4OR83WBUhLJaKDS9dnJ7arRMOp3t31dcLhApwG9bgvrRXBHlLdIkg==", "license": "Apache-2.0", "dependencies": { "@smithy/node-config-provider": "^4.3.14", @@ -3382,6 +3642,8 @@ }, "node_modules/@smithy/util-hex-encoding": { "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/util-hex-encoding/-/util-hex-encoding-4.2.2.tgz", + "integrity": "sha512-Qcz3W5vuHK4sLQdyT93k/rfrUwdJ8/HZ+nMUOyGdpeGA1Wxt65zYwi3oEl9kOM+RswvYq90fzkNDahPS8K0OIg==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -3392,6 +3654,8 @@ }, "node_modules/@smithy/util-middleware": { "version": "4.2.14", + "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-4.2.14.tgz", + "integrity": "sha512-1Su2vj9RYNDEv/V+2E+jXkkwGsgR7dc4sfHn9Z7ruzQHJIEni9zzw5CauvRXlFJfmgcqYP8fWa0dkh2Q2YaQyw==", "license": "Apache-2.0", "dependencies": { "@smithy/types": "^4.14.1", @@ -3403,6 +3667,8 @@ }, "node_modules/@smithy/util-retry": { "version": "4.3.8", + "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-4.3.8.tgz", + "integrity": "sha512-LUIxbTBi+OpvXpg91poGA6BdyoleMDLnfXjVDqyi2RvZmTveY5loE/FgYUBCR5LU2BThW2SoZRh8dTIIy38IPw==", "license": "Apache-2.0", "dependencies": { "@smithy/service-error-classification": "^4.3.1", @@ -3415,6 +3681,8 @@ }, "node_modules/@smithy/util-stream": { "version": "4.5.25", + "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.5.25.tgz", + "integrity": "sha512-/PFpG4k8Ze8Ei+mMKj3oiPICYekthuzePZMgZbCqMiXIHHf4n2aZ4Ps0aSRShycFTGuj/J6XldmC0x0DwednIA==", "license": "Apache-2.0", "dependencies": { "@smithy/fetch-http-handler": "^5.3.17", @@ -3432,6 +3700,8 @@ }, "node_modules/@smithy/util-uri-escape": { "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/util-uri-escape/-/util-uri-escape-4.2.2.tgz", + "integrity": "sha512-2kAStBlvq+lTXHyAZYfJRb/DfS3rsinLiwb+69SstC9Vb0s9vNWkRwpnj918Pfi85mzi42sOqdV72OLxWAISnw==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -3442,6 +3712,8 @@ }, "node_modules/@smithy/util-utf8": { "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.2.2.tgz", + "integrity": "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw==", "license": "Apache-2.0", "dependencies": { "@smithy/util-buffer-from": "^4.2.2", @@ -3453,6 +3725,8 @@ }, "node_modules/@smithy/uuid": { "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@smithy/uuid/-/uuid-1.1.2.tgz", + "integrity": "sha512-O/IEdcCUKkubz60tFbGA7ceITTAJsty+lBjNoorP4Z6XRqaFb/OjQjZODophEcuq68nKm6/0r+6/lLQ+XVpk8g==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -3463,6 +3737,8 @@ }, "node_modules/@tailwindcss/cli": { "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/cli/-/cli-4.2.4.tgz", + "integrity": "sha512-e87GGhuXxnyQPyA0TS8an/3wNpj+OUmx8u0F4BicYr48TF72032AIu5917rRYaWm7HorXi3GSZ/uG+ohqP6AKA==", "dev": true, "license": "MIT", "dependencies": { @@ -3480,6 +3756,8 @@ }, "node_modules/@tailwindcss/node": { "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.2.4.tgz", + "integrity": "sha512-Ai7+yQPxz3ddrDQzFfBKdHEVBg0w3Zl83jnjuwxnZOsnH9pGn93QHQtpU0p/8rYWxvbFZHneni6p1BSLK4DkGA==", "dev": true, "license": "MIT", "dependencies": { @@ -3494,6 +3772,8 @@ }, "node_modules/@tailwindcss/oxide": { "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.2.4.tgz", + "integrity": "sha512-9El/iI069DKDSXwTvB9J4BwdO5JhRrOweGaK25taBAvBXyXqJAX+Jqdvs8r8gKpsI/1m0LeJLyQYTf/WLrBT1Q==", "dev": true, "license": "MIT", "engines": { @@ -3533,6 +3813,8 @@ }, "node_modules/@tailwindcss/oxide-darwin-arm64": { "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.2.4.tgz", + "integrity": "sha512-tSC/Kbqpz/5/o/C2sG7QvOxAKqyd10bq+ypZNf+9Fi2TvbVbv1zNpcEptcsU7DPROaSbVgUXmrzKhurFvo5eDg==", "cpu": [ "arm64" ], @@ -3795,6 +4077,8 @@ }, "node_modules/@tailwindcss/vite": { "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.2.4.tgz", + "integrity": "sha512-pCvohwOCspk3ZFn6eJzrrX3g4n2JY73H6MmYC87XfGPyTty4YsCjYTMArRZm/zOI8dIt3+EcrLHAFPe5A4bgtw==", "dev": true, "license": "MIT", "dependencies": { @@ -3808,6 +4092,8 @@ }, "node_modules/@types/chai": { "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", "dev": true, "license": "MIT", "dependencies": { @@ -3817,31 +4103,43 @@ }, "node_modules/@types/deep-eql": { "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", "dev": true, "license": "MIT" }, "node_modules/@types/diff": { "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@types/diff/-/diff-7.0.2.tgz", + "integrity": "sha512-JSWRMozjFKsGlEjiiKajUjIJVKuKdE3oVy2DNtK+fUo8q82nhFZ2CPQwicAIkXrofahDXrWJ7mjelvZphMS98Q==", "dev": true, "license": "MIT" }, "node_modules/@types/estree": { "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", "dev": true, "license": "MIT" }, "node_modules/@types/hosted-git-info": { "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/hosted-git-info/-/hosted-git-info-3.0.5.tgz", + "integrity": "sha512-Dmngh7U003cOHPhKGyA7LWqrnvcTyILNgNPmNCxlx7j8MIi54iBliiT8XqVLIQ3GchoOjVAyBzNJVyuaJjqokg==", "dev": true, "license": "MIT" }, "node_modules/@types/lodash": { "version": "4.17.24", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.24.tgz", + "integrity": "sha512-gIW7lQLZbue7lRSWEFql49QJJWThrTFFeIMJdp3eH4tKoxm1OvEPg02rm4wCCSHS0cL3/Fizimb35b7k8atwsQ==", "dev": true, "license": "MIT" }, "node_modules/@types/lodash-es": { "version": "4.17.12", + "resolved": "https://registry.npmjs.org/@types/lodash-es/-/lodash-es-4.17.12.tgz", + "integrity": "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==", "dev": true, "license": "MIT", "dependencies": { @@ -3850,11 +4148,15 @@ }, "node_modules/@types/ms": { "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", "dev": true, "license": "MIT" }, "node_modules/@types/node": { "version": "22.19.17", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.17.tgz", + "integrity": "sha512-wGdMcf+vPYM6jikpS/qhg6WiqSV/OhG+jeeHT/KlVqxYfD40iYJf9/AE1uQxVWFvU7MipKRkRv8NSHiCGgPr8Q==", "license": "MIT", "dependencies": { "undici-types": "~6.21.0" @@ -3862,6 +4164,8 @@ }, "node_modules/@types/proper-lockfile": { "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@types/proper-lockfile/-/proper-lockfile-4.1.4.tgz", + "integrity": "sha512-uo2ABllncSqg9F1D4nugVl9v93RmjxF6LJzQLMLDdPaXCUIDPeOJ21Gbqi43xNKzBi/WQ0Q0dICqufzQbMjipQ==", "dev": true, "license": "MIT", "dependencies": { @@ -3870,15 +4174,21 @@ }, "node_modules/@types/retry": { "version": "0.12.5", + "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.5.tgz", + "integrity": "sha512-3xSjTp3v03X/lSQLkczaN9UIEwJMoMCA1+Nb5HfbJEQWogdeQIyVtTvxPXDQjZ5zws8rFQfVfRdz03ARihPJgw==", "dev": true, "license": "MIT" }, "node_modules/@types/trusted-types": { "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", "license": "MIT" }, "node_modules/@typescript/native-preview": { "version": "7.0.0-dev.20260120.1", + "resolved": "https://registry.npmjs.org/@typescript/native-preview/-/native-preview-7.0.0-dev.20260120.1.tgz", + "integrity": "sha512-nnEf37C9ue7OBRnF2zmV/OCBmV5Y7T/K4mCHa+nxgiXcF/1w8sA0cgdFl+gHQ0mysqUJ+Bu5btAMeWgpLyjrgg==", "dev": true, "license": "Apache-2.0", "bin": { @@ -3896,6 +4206,8 @@ }, "node_modules/@typescript/native-preview-darwin-arm64": { "version": "7.0.0-dev.20260120.1", + "resolved": "https://registry.npmjs.org/@typescript/native-preview-darwin-arm64/-/native-preview-darwin-arm64-7.0.0-dev.20260120.1.tgz", + "integrity": "sha512-r3pWFuR2H7mn6ScwpH5jJljKQqKto0npVuJSk6pRwFwexpTyxOGmJTZJ1V0AWiisaNxU2+CNAqWFJSJYIE/QTg==", "cpu": [ "arm64" ], @@ -4026,6 +4338,8 @@ }, "node_modules/@vitest/expect": { "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz", + "integrity": "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==", "dev": true, "license": "MIT", "dependencies": { @@ -4041,6 +4355,8 @@ }, "node_modules/@vitest/mocker": { "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.4.tgz", + "integrity": "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==", "dev": true, "license": "MIT", "dependencies": { @@ -4066,6 +4382,8 @@ }, "node_modules/@vitest/pretty-format": { "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.4.tgz", + "integrity": "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==", "dev": true, "license": "MIT", "dependencies": { @@ -4077,6 +4395,8 @@ }, "node_modules/@vitest/runner": { "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.4.tgz", + "integrity": "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==", "dev": true, "license": "MIT", "dependencies": { @@ -4090,6 +4410,8 @@ }, "node_modules/@vitest/snapshot": { "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.4.tgz", + "integrity": "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==", "dev": true, "license": "MIT", "dependencies": { @@ -4103,6 +4425,8 @@ }, "node_modules/@vitest/spy": { "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.4.tgz", + "integrity": "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==", "dev": true, "license": "MIT", "dependencies": { @@ -4114,6 +4438,8 @@ }, "node_modules/@vitest/utils": { "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.4.tgz", + "integrity": "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==", "dev": true, "license": "MIT", "dependencies": { @@ -4127,6 +4453,8 @@ }, "node_modules/@webreflection/alien-signals": { "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@webreflection/alien-signals/-/alien-signals-0.3.2.tgz", + "integrity": "sha512-DmNjD8Kq5iM+Toirp3llS/izAiI3Dwav5nHRvKdR/YJBTgun3y4xK76rs9CFYD2bZwZJN/rP+HjEqKTteGK+Yw==", "license": "MIT", "dependencies": { "alien-signals": "^2.0.6" @@ -4134,16 +4462,22 @@ }, "node_modules/@xterm/headless": { "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@xterm/headless/-/headless-5.5.0.tgz", + "integrity": "sha512-5xXB7kdQlFBP82ViMJTwwEc3gKCLGKR/eoxQm4zge7GPBl86tCdI0IdPJjoKd8mUSFXz5V7i/25sfsEkP4j46g==", "dev": true, "license": "MIT" }, "node_modules/@xterm/xterm": { "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.5.0.tgz", + "integrity": "sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A==", "dev": true, "license": "MIT" }, "node_modules/agent-base": { "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", "license": "MIT", "engines": { "node": ">= 14" @@ -4151,6 +4485,8 @@ }, "node_modules/alien-signals": { "version": "2.0.8", + "resolved": "https://registry.npmjs.org/alien-signals/-/alien-signals-2.0.8.tgz", + "integrity": "sha512-844G1VLkk0Pe2SJjY0J8vp8ADI73IM4KliNu2OGlYzWpO28NexEUvjHTcFjFX3VXoiUtwTbHxLNI9ImkcoBqzA==", "license": "MIT" }, "node_modules/ansi-regex": { @@ -4168,6 +4504,8 @@ }, "node_modules/ansi-styles": { "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -4181,6 +4519,8 @@ }, "node_modules/assertion-error": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", "dev": true, "license": "MIT", "engines": { @@ -4208,6 +4548,8 @@ }, "node_modules/balanced-match": { "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", "license": "MIT", "engines": { "node": "18 || 20 || >=22" @@ -4215,6 +4557,8 @@ }, "node_modules/base64-js": { "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", "funding": [ { "type": "github", @@ -4233,6 +4577,8 @@ }, "node_modules/bignumber.js": { "version": "9.3.1", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz", + "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==", "license": "MIT", "engines": { "node": "*" @@ -4240,6 +4586,8 @@ }, "node_modules/bl": { "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", "dev": true, "license": "MIT", "dependencies": { @@ -4250,10 +4598,14 @@ }, "node_modules/bowser": { "version": "2.14.1", + "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.14.1.tgz", + "integrity": "sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==", "license": "MIT" }, "node_modules/brace-expansion": { "version": "5.0.5", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", + "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", "license": "MIT", "dependencies": { "balanced-match": "^4.0.2" @@ -4264,6 +4616,8 @@ }, "node_modules/braces": { "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dev": true, "license": "MIT", "dependencies": { @@ -4275,6 +4629,8 @@ }, "node_modules/buffer": { "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", "dev": true, "funding": [ { @@ -4298,10 +4654,14 @@ }, "node_modules/buffer-equal-constant-time": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", "license": "BSD-3-Clause" }, "node_modules/cac": { "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", "dev": true, "license": "MIT", "engines": { @@ -4310,6 +4670,8 @@ }, "node_modules/canvas": { "version": "3.2.3", + "resolved": "https://registry.npmjs.org/canvas/-/canvas-3.2.3.tgz", + "integrity": "sha512-PzE5nJZPz72YUAfo8oTp0u3fqqY7IzlTubneAihqDYAUcBk7ryeCmBbdJBEdaH0bptSOe2VT2Zwcb3UaFyaSWw==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -4323,6 +4685,8 @@ }, "node_modules/chai": { "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", "dev": true, "license": "MIT", "dependencies": { @@ -4338,6 +4702,8 @@ }, "node_modules/chalk": { "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", "license": "MIT", "engines": { "node": "^12.17.0 || ^14.13 || >=16.0.0" @@ -4348,6 +4714,8 @@ }, "node_modules/check-error": { "version": "2.1.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", "dev": true, "license": "MIT", "engines": { @@ -4356,11 +4724,15 @@ }, "node_modules/chownr": { "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", "dev": true, "license": "ISC" }, "node_modules/class-variance-authority": { "version": "0.7.1", + "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz", + "integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==", "license": "Apache-2.0", "dependencies": { "clsx": "^2.1.1" @@ -4371,6 +4743,8 @@ }, "node_modules/clsx": { "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", "license": "MIT", "engines": { "node": ">=6" @@ -4378,6 +4752,8 @@ }, "node_modules/color-convert": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -4388,10 +4764,14 @@ }, "node_modules/color-name": { "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "license": "MIT" }, "node_modules/commander": { "version": "12.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", "dev": true, "license": "MIT", "engines": { @@ -4400,6 +4780,8 @@ }, "node_modules/concurrently": { "version": "9.2.1", + "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.2.1.tgz", + "integrity": "sha512-fsfrO0MxV64Znoy8/l1vVIjjHa29SZyyqPgQBwhiDcaW8wJc2W3XWVOGx4M3oJBnv/zdUZIIp1gDeS98GzP8Ng==", "dev": true, "license": "MIT", "dependencies": { @@ -4423,6 +4805,8 @@ }, "node_modules/concurrently/node_modules/ansi-regex": { "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true, "license": "MIT", "engines": { @@ -4431,6 +4815,8 @@ }, "node_modules/concurrently/node_modules/chalk": { "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dev": true, "license": "MIT", "dependencies": { @@ -4446,6 +4832,8 @@ }, "node_modules/concurrently/node_modules/chalk/node_modules/supports-color": { "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, "license": "MIT", "dependencies": { @@ -4457,6 +4845,8 @@ }, "node_modules/concurrently/node_modules/cliui": { "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", "dev": true, "license": "ISC", "dependencies": { @@ -4470,6 +4860,8 @@ }, "node_modules/concurrently/node_modules/strip-ansi": { "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, "license": "MIT", "dependencies": { @@ -4481,6 +4873,8 @@ }, "node_modules/concurrently/node_modules/yargs": { "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", "dev": true, "license": "MIT", "dependencies": { @@ -4498,6 +4892,8 @@ }, "node_modules/concurrently/node_modules/yargs-parser": { "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", "dev": true, "license": "ISC", "engines": { @@ -4506,10 +4902,14 @@ }, "node_modules/core-util-is": { "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", "license": "MIT" }, "node_modules/cross-spawn": { "version": "6.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.6.tgz", + "integrity": "sha512-VqCUuhcd1iB+dsv8gxPttb5iZh/D0iubSP21g36KXdEuf6I5JiioesUVjpCdHV9MZRUfVFlvwtIUyPfxo5trtw==", "dev": true, "license": "MIT", "dependencies": { @@ -4525,6 +4925,8 @@ }, "node_modules/cross-spawn/node_modules/semver": { "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", "dev": true, "license": "ISC", "bin": { @@ -4533,6 +4935,8 @@ }, "node_modules/data-uri-to-buffer": { "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", "license": "MIT", "engines": { "node": ">= 12" @@ -4540,6 +4944,8 @@ }, "node_modules/debug": { "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -4555,6 +4961,8 @@ }, "node_modules/decompress-response": { "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", "dev": true, "license": "MIT", "dependencies": { @@ -4569,6 +4977,8 @@ }, "node_modules/deep-eql": { "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", "dev": true, "license": "MIT", "engines": { @@ -4577,6 +4987,8 @@ }, "node_modules/deep-extend": { "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", "dev": true, "license": "MIT", "engines": { @@ -4585,6 +4997,8 @@ }, "node_modules/detect-libc": { "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", "dev": true, "license": "Apache-2.0", "engines": { @@ -4593,6 +5007,8 @@ }, "node_modules/diff": { "version": "8.0.4", + "resolved": "https://registry.npmjs.org/diff/-/diff-8.0.4.tgz", + "integrity": "sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw==", "license": "BSD-3-Clause", "engines": { "node": ">=0.3.1" @@ -4600,6 +5016,8 @@ }, "node_modules/docx-preview": { "version": "0.3.7", + "resolved": "https://registry.npmjs.org/docx-preview/-/docx-preview-0.3.7.tgz", + "integrity": "sha512-Lav69CTA/IYZPJTsKH7oYeoZjyg96N0wEJMNslGJnZJ+dMUZK85Lt5ASC79yUlD48ecWjuv+rkcmFt6EVPV0Xg==", "license": "Apache-2.0", "dependencies": { "jszip": ">=3.0.0" @@ -4614,6 +5032,8 @@ }, "node_modules/ecdsa-sig-formatter": { "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", "license": "Apache-2.0", "dependencies": { "safe-buffer": "^5.0.1" @@ -4621,11 +5041,15 @@ }, "node_modules/emoji-regex": { "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "dev": true, "license": "MIT" }, "node_modules/end-of-stream": { "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", "dev": true, "license": "MIT", "dependencies": { @@ -4634,6 +5058,8 @@ }, "node_modules/enhanced-resolve": { "version": "5.21.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.21.0.tgz", + "integrity": "sha512-otxSQPw4lkOZWkHpB3zaEQs6gWYEsmX4xQF68ElXC/TWvGxGMSGOvoNbaLXm6/cS/fSfHtsEdw90y20PCd+sCA==", "dev": true, "license": "MIT", "dependencies": { @@ -4646,6 +5072,8 @@ }, "node_modules/es-errors": { "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", "dev": true, "license": "MIT", "engines": { @@ -4654,11 +5082,15 @@ }, "node_modules/es-module-lexer": { "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", "dev": true, "license": "MIT" }, "node_modules/esbuild": { "version": "0.27.7", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", + "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -4699,6 +5131,8 @@ }, "node_modules/escalade": { "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", "dev": true, "license": "MIT", "engines": { @@ -4707,6 +5141,8 @@ }, "node_modules/estree-walker": { "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", "dev": true, "license": "MIT", "dependencies": { @@ -4715,6 +5151,8 @@ }, "node_modules/execa": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", + "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", "dev": true, "license": "MIT", "dependencies": { @@ -4732,6 +5170,8 @@ }, "node_modules/execa/node_modules/get-stream": { "version": "4.1.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", + "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", "dev": true, "license": "MIT", "dependencies": { @@ -4743,6 +5183,8 @@ }, "node_modules/expand-template": { "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", "dev": true, "license": "(MIT OR WTFPL)", "engines": { @@ -4751,6 +5193,8 @@ }, "node_modules/expect-type": { "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", "dev": true, "license": "Apache-2.0", "engines": { @@ -4759,10 +5203,14 @@ }, "node_modules/extend": { "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", "license": "MIT" }, "node_modules/fast-glob": { "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", "dev": true, "license": "MIT", "dependencies": { @@ -4778,6 +5226,8 @@ }, "node_modules/fast-xml-builder": { "version": "1.1.9", + "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.1.9.tgz", + "integrity": "sha512-jcyKVSEX13iseJqg7n/KWw+xnu/7fdrZ333Fac54KjHDIELVCfDDJXYIm6DTJ0Su4gSzrhqiK0DzY/wZbF40mw==", "funding": [ { "type": "github", @@ -4791,6 +5241,8 @@ }, "node_modules/fast-xml-parser": { "version": "5.7.2", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.7.2.tgz", + "integrity": "sha512-P7oW7tLbYnhOLQk/Gv7cZgzgMPP/XN03K02/Jy6Y/NHzyIAIpxuZIM/YqAkfiXFPxA2CTm7NtCijK9EDu09u2w==", "funding": [ { "type": "github", @@ -4810,6 +5262,8 @@ }, "node_modules/fastq": { "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", "dev": true, "license": "ISC", "dependencies": { @@ -4818,6 +5272,8 @@ }, "node_modules/fetch-blob": { "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", "funding": [ { "type": "github", @@ -4839,6 +5295,8 @@ }, "node_modules/fill-range": { "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dev": true, "license": "MIT", "dependencies": { @@ -4944,6 +5402,8 @@ }, "node_modules/formdata-polyfill": { "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", "license": "MIT", "dependencies": { "fetch-blob": "^3.1.2" @@ -4954,6 +5414,8 @@ }, "node_modules/fs-constants": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", "dev": true, "license": "MIT" }, @@ -4974,6 +5436,8 @@ }, "node_modules/function-bind": { "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", "dev": true, "license": "MIT", "funding": { @@ -4982,6 +5446,8 @@ }, "node_modules/gaxios": { "version": "7.1.4", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.1.4.tgz", + "integrity": "sha512-bTIgTsM2bWn3XklZISBTQX7ZSddGW+IO3bMdGaemHZ3tbqExMENHLx6kKZ/KlejgrMtj8q7wBItt51yegqalrA==", "license": "Apache-2.0", "dependencies": { "extend": "^3.0.2", @@ -4994,6 +5460,8 @@ }, "node_modules/gcp-metadata": { "version": "8.1.2", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-8.1.2.tgz", + "integrity": "sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg==", "license": "Apache-2.0", "dependencies": { "gaxios": "^7.0.0", @@ -5006,6 +5474,8 @@ }, "node_modules/get-caller-file": { "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", "dev": true, "license": "ISC", "engines": { @@ -5014,6 +5484,8 @@ }, "node_modules/get-east-asian-width": { "version": "1.5.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.5.0.tgz", + "integrity": "sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA==", "license": "MIT", "engines": { "node": ">=18" @@ -5024,6 +5496,8 @@ }, "node_modules/get-tsconfig": { "version": "4.14.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.14.0.tgz", + "integrity": "sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==", "dev": true, "license": "MIT", "dependencies": { @@ -5035,11 +5509,15 @@ }, "node_modules/github-from-package": { "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", "dev": true, "license": "MIT" }, "node_modules/glob": { "version": "13.0.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", + "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", "license": "BlueOak-1.0.0", "dependencies": { "minimatch": "^10.2.2", @@ -5055,6 +5533,8 @@ }, "node_modules/glob-parent": { "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", "dev": true, "license": "ISC", "dependencies": { @@ -5066,6 +5546,8 @@ }, "node_modules/google-auth-library": { "version": "10.6.2", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-10.6.2.tgz", + "integrity": "sha512-e27Z6EThmVNNvtYASwQxose/G57rkRuaRbQyxM2bvYLLX/GqWZ5chWq2EBoUchJbCc57eC9ArzO5wMsEmWftCw==", "license": "Apache-2.0", "dependencies": { "base64-js": "^1.3.0", @@ -5081,6 +5563,8 @@ }, "node_modules/google-logging-utils": { "version": "1.1.3", + "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-1.1.3.tgz", + "integrity": "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA==", "license": "Apache-2.0", "engines": { "node": ">=14" @@ -5088,10 +5572,14 @@ }, "node_modules/graceful-fs": { "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", "license": "ISC" }, "node_modules/has-flag": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "license": "MIT", "engines": { "node": ">=8" @@ -5099,6 +5587,8 @@ }, "node_modules/hasown": { "version": "2.0.3", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz", + "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", "dev": true, "license": "MIT", "dependencies": { @@ -5110,6 +5600,8 @@ }, "node_modules/highlight.js": { "version": "10.7.3", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz", + "integrity": "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==", "license": "BSD-3-Clause", "engines": { "node": "*" @@ -5117,6 +5609,8 @@ }, "node_modules/hosted-git-info": { "version": "9.0.3", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-9.0.3.tgz", + "integrity": "sha512-Hc+ghLoSt6QaYZUv0WBiIvmMDZuZZ7oaDvdH8MbfOO4lOsxdXLEvuC6ePoGs9H1X9oCLyq6+NVN0MKqD+ydxyg==", "license": "ISC", "dependencies": { "lru-cache": "^11.1.0" @@ -5134,10 +5628,14 @@ }, "node_modules/html-parse-string": { "version": "0.0.9", + "resolved": "https://registry.npmjs.org/html-parse-string/-/html-parse-string-0.0.9.tgz", + "integrity": "sha512-wyGnsOolHbNrcb8N6bdJF4EHyzd3zVGCb9/mBxeNjAYBDOZqD7YkqLBz7kXtdgHwNnV8lN/BpSDpsI1zm8Sd8g==", "license": "MIT" }, "node_modules/http-proxy-agent": { "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", "license": "MIT", "dependencies": { "agent-base": "^7.1.0", @@ -5149,6 +5647,8 @@ }, "node_modules/https-proxy-agent": { "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", "license": "MIT", "dependencies": { "agent-base": "^7.1.2", @@ -5160,6 +5660,8 @@ }, "node_modules/husky": { "version": "9.1.7", + "resolved": "https://registry.npmjs.org/husky/-/husky-9.1.7.tgz", + "integrity": "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==", "dev": true, "license": "MIT", "bin": { @@ -5174,6 +5676,8 @@ }, "node_modules/ieee754": { "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", "dev": true, "funding": [ { @@ -5193,6 +5697,8 @@ }, "node_modules/ignore": { "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", "license": "MIT", "engines": { "node": ">= 4" @@ -5200,19 +5706,27 @@ }, "node_modules/immediate": { "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", "license": "MIT" }, "node_modules/inherits": { "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "license": "ISC" }, "node_modules/ini": { "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", "dev": true, "license": "ISC" }, "node_modules/interpret": { "version": "1.4.0", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.4.0.tgz", + "integrity": "sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==", "dev": true, "license": "MIT", "engines": { @@ -5221,6 +5735,8 @@ }, "node_modules/is-core-module": { "version": "2.16.2", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz", + "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==", "dev": true, "license": "MIT", "dependencies": { @@ -5235,6 +5751,8 @@ }, "node_modules/is-extglob": { "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", "dev": true, "license": "MIT", "engines": { @@ -5243,6 +5761,8 @@ }, "node_modules/is-fullwidth-code-point": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", "dev": true, "license": "MIT", "engines": { @@ -5251,6 +5771,8 @@ }, "node_modules/is-glob": { "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", "dev": true, "license": "MIT", "dependencies": { @@ -5262,6 +5784,8 @@ }, "node_modules/is-number": { "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "dev": true, "license": "MIT", "engines": { @@ -5270,6 +5794,8 @@ }, "node_modules/is-stream": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==", "dev": true, "license": "MIT", "engines": { @@ -5278,10 +5804,14 @@ }, "node_modules/isarray": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", "license": "MIT" }, "node_modules/isexe": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "dev": true, "license": "ISC" }, @@ -5370,6 +5900,8 @@ }, "node_modules/jiti": { "version": "2.7.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", + "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", "license": "MIT", "bin": { "jiti": "lib/jiti-cli.mjs" @@ -5377,11 +5909,15 @@ }, "node_modules/js-tokens": { "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", "dev": true, "license": "MIT" }, "node_modules/json-bigint": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", "license": "MIT", "dependencies": { "bignumber.js": "^9.0.0" @@ -5389,6 +5925,8 @@ }, "node_modules/json-schema-to-ts": { "version": "3.1.1", + "resolved": "https://registry.npmjs.org/json-schema-to-ts/-/json-schema-to-ts-3.1.1.tgz", + "integrity": "sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.18.3", @@ -5400,6 +5938,8 @@ }, "node_modules/jsonschema": { "version": "1.5.0", + "resolved": "https://registry.npmjs.org/jsonschema/-/jsonschema-1.5.0.tgz", + "integrity": "sha512-K+A9hhqbn0f3pJX17Q/7H6yQfD/5OXgdrR5UE12gMXCiN9D5Xq2o5mddV2QEcX/bjla99ASsAAQUyMCCRWAEhw==", "license": "MIT", "engines": { "node": "*" @@ -5407,6 +5947,8 @@ }, "node_modules/jszip": { "version": "3.10.1", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", + "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", "license": "(MIT OR GPL-3.0-or-later)", "dependencies": { "lie": "~3.3.0", @@ -5417,6 +5959,8 @@ }, "node_modules/jszip/node_modules/readable-stream": { "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", "license": "MIT", "dependencies": { "core-util-is": "~1.0.0", @@ -5430,10 +5974,14 @@ }, "node_modules/jszip/node_modules/safe-buffer": { "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", "license": "MIT" }, "node_modules/jszip/node_modules/string_decoder": { "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "license": "MIT", "dependencies": { "safe-buffer": "~5.1.0" @@ -5441,6 +5989,8 @@ }, "node_modules/jwa": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", "license": "MIT", "dependencies": { "buffer-equal-constant-time": "^1.0.1", @@ -5450,6 +6000,8 @@ }, "node_modules/jws": { "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", "license": "MIT", "dependencies": { "jwa": "^2.0.1", @@ -5458,6 +6010,8 @@ }, "node_modules/katex": { "version": "0.16.45", + "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.45.tgz", + "integrity": "sha512-pQpZbdBu7wCTmQUh7ufPmLr0pFoObnGUoL/yhtwJDgmmQpbkg/0HSVti25Fu4rmd1oCR6NGWe9vqTWuWv3GcNA==", "funding": [ "https://opencollective.com/katex", "https://github.com/sponsors/katex" @@ -5472,6 +6026,8 @@ }, "node_modules/katex/node_modules/commander": { "version": "8.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", "license": "MIT", "engines": { "node": ">= 12" @@ -5479,6 +6035,8 @@ }, "node_modules/koffi": { "version": "2.16.2", + "resolved": "https://registry.npmjs.org/koffi/-/koffi-2.16.2.tgz", + "integrity": "sha512-owU0MRwv6xkrVqCd+33uw6BaYppkTRXbO/rVdJNI2dvZG0gzyRhYwW25eWtc5pauwK8TGh3AbkFONSezdykfSA==", "hasInstallScript": true, "license": "MIT", "optional": true, @@ -5488,6 +6046,8 @@ }, "node_modules/lie": { "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", "license": "MIT", "dependencies": { "immediate": "~3.0.5" @@ -5495,6 +6055,8 @@ }, "node_modules/lightningcss": { "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", "dev": true, "license": "MPL-2.0", "dependencies": { @@ -5544,6 +6106,8 @@ }, "node_modules/lightningcss-darwin-arm64": { "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", "cpu": [ "arm64" ], @@ -5752,6 +6316,8 @@ }, "node_modules/lit": { "version": "3.3.2", + "resolved": "https://registry.npmjs.org/lit/-/lit-3.3.2.tgz", + "integrity": "sha512-NF9zbsP79l4ao2SNrH3NkfmFgN/hBYSQo90saIVI1o5GpjAdCPVstVzO1MrLOakHoEhYkrtRjPK6Ob521aoYWQ==", "license": "BSD-3-Clause", "dependencies": { "@lit/reactive-element": "^2.1.0", @@ -5761,6 +6327,8 @@ }, "node_modules/lit-element": { "version": "4.2.2", + "resolved": "https://registry.npmjs.org/lit-element/-/lit-element-4.2.2.tgz", + "integrity": "sha512-aFKhNToWxoyhkNDmWZwEva2SlQia+jfG0fjIWV//YeTaWrVnOxD89dPKfigCUspXFmjzOEUQpOkejH5Ly6sG0w==", "license": "BSD-3-Clause", "dependencies": { "@lit-labs/ssr-dom-shim": "^1.5.0", @@ -5770,6 +6338,8 @@ }, "node_modules/lit-html": { "version": "3.3.2", + "resolved": "https://registry.npmjs.org/lit-html/-/lit-html-3.3.2.tgz", + "integrity": "sha512-Qy9hU88zcmaxBXcc10ZpdK7cOLXvXpRoBxERdtqV9QOrfpMZZ6pSYP91LhpPtap3sFMUiL7Tw2RImbe0Al2/kw==", "license": "BSD-3-Clause", "dependencies": { "@types/trusted-types": "^2.0.2" @@ -5777,20 +6347,28 @@ }, "node_modules/lodash-es": { "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.18.1.tgz", + "integrity": "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==", "dev": true, "license": "MIT" }, "node_modules/long": { "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", "license": "Apache-2.0" }, "node_modules/loupe": { "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", "dev": true, "license": "MIT" }, "node_modules/lru-cache": { "version": "11.3.6", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.3.6.tgz", + "integrity": "sha512-Gf/KoL3C/MlI7Bt0PGI9I+TeTC/I6r/csU58N4BSNc4lppLBeKsOdFYkK+dX0ABDUMJNfCHTyPpzwwO21Awd3A==", "license": "BlueOak-1.0.0", "engines": { "node": "20 || >=22" @@ -5798,10 +6376,14 @@ }, "node_modules/lucide": { "version": "0.544.0", + "resolved": "https://registry.npmjs.org/lucide/-/lucide-0.544.0.tgz", + "integrity": "sha512-U5ORwr5z9Sx7bNTDFaW55RbjVdQEnAcT3vws9uz3vRT1G4XXJUDAhRZdxhFoIyHEvjmTkzzlEhjSLYM5n4mb5w==", "license": "ISC" }, "node_modules/magic-string": { "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", "dev": true, "license": "MIT", "dependencies": { @@ -5838,6 +6420,8 @@ }, "node_modules/marked": { "version": "15.0.12", + "resolved": "https://registry.npmjs.org/marked/-/marked-15.0.12.tgz", + "integrity": "sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA==", "license": "MIT", "bin": { "marked": "bin/marked.js" @@ -5848,6 +6432,8 @@ }, "node_modules/merge2": { "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", "dev": true, "license": "MIT", "engines": { @@ -5856,6 +6442,8 @@ }, "node_modules/micromatch": { "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "dev": true, "license": "MIT", "dependencies": { @@ -5868,6 +6456,8 @@ }, "node_modules/mimic-response": { "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", "dev": true, "license": "MIT", "engines": { @@ -5879,6 +6469,8 @@ }, "node_modules/minimatch": { "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", "license": "BlueOak-1.0.0", "dependencies": { "brace-expansion": "^5.0.5" @@ -5892,6 +6484,8 @@ }, "node_modules/minimist": { "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", "dev": true, "license": "MIT", "funding": { @@ -5900,6 +6494,8 @@ }, "node_modules/minipass": { "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", "license": "BlueOak-1.0.0", "engines": { "node": ">=16 || 14 >=14.17" @@ -5907,11 +6503,15 @@ }, "node_modules/mkdirp-classic": { "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", "dev": true, "license": "MIT" }, "node_modules/mri": { "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", + "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==", "dev": true, "license": "MIT", "engines": { @@ -5920,10 +6520,14 @@ }, "node_modules/ms": { "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, "node_modules/nanoid": { "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", "dev": true, "funding": [ { @@ -5941,16 +6545,22 @@ }, "node_modules/napi-build-utils": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", "dev": true, "license": "MIT" }, "node_modules/nice-try": { "version": "1.0.5", + "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", + "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", "dev": true, "license": "MIT" }, "node_modules/node-abi": { "version": "3.92.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.92.0.tgz", + "integrity": "sha512-KdHvFWZjEKDf0cakgFjebl371GPsISX2oZHcuyKqM7DtogIsHrqKeLTo8wBHxaXRAQlY2PsPlZmfo+9ZCxEREQ==", "dev": true, "license": "MIT", "dependencies": { @@ -5962,11 +6572,15 @@ }, "node_modules/node-addon-api": { "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", "dev": true, "license": "MIT" }, "node_modules/node-domexception": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", "funding": [ { "type": "github", @@ -5984,6 +6598,8 @@ }, "node_modules/node-fetch": { "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", "license": "MIT", "dependencies": { "data-uri-to-buffer": "^4.0.0", @@ -6000,6 +6616,8 @@ }, "node_modules/npm-run-path": { "version": "2.0.2", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", + "integrity": "sha512-lJxZYlT4DW/bRUtFh1MQIWqmLwQfAxnqWG4HhEdjMlkrJYnJn0Jrr2u3mgxqaWsdiBc76TYkTG/mhrnYTuzfHw==", "dev": true, "license": "MIT", "dependencies": { @@ -6011,6 +6629,8 @@ }, "node_modules/ollama": { "version": "0.6.3", + "resolved": "https://registry.npmjs.org/ollama/-/ollama-0.6.3.tgz", + "integrity": "sha512-KEWEhIqE5wtfzEIZbDCLH51VFZ6Z3ZSa6sIOg/E/tBV8S51flyqBOXi+bRxlOYKDf8i327zG9eSTb8IJxvm3Zg==", "license": "MIT", "dependencies": { "whatwg-fetch": "^3.6.20" @@ -6018,6 +6638,8 @@ }, "node_modules/once": { "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", "dev": true, "license": "ISC", "dependencies": { @@ -6026,6 +6648,8 @@ }, "node_modules/openai": { "version": "6.26.0", + "resolved": "https://registry.npmjs.org/openai/-/openai-6.26.0.tgz", + "integrity": "sha512-zd23dbWTjiJ6sSAX6s0HrCZi41JwTA1bQVs0wLQPZ2/5o2gxOJA5wh7yOAUgwYybfhDXyhwlpeQf7Mlgx8EOCA==", "license": "Apache-2.0", "bin": { "openai": "bin/cli" @@ -6045,6 +6669,8 @@ }, "node_modules/p-finally": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", + "integrity": "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==", "dev": true, "license": "MIT", "engines": { @@ -6053,6 +6679,8 @@ }, "node_modules/p-retry": { "version": "4.6.2", + "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz", + "integrity": "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==", "license": "MIT", "dependencies": { "@types/retry": "0.12.0", @@ -6064,6 +6692,8 @@ }, "node_modules/p-retry/node_modules/@types/retry": { "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==", "license": "MIT" }, "node_modules/package-json-from-dist": { @@ -6075,14 +6705,20 @@ }, "node_modules/pako": { "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", "license": "(MIT AND Zlib)" }, "node_modules/partial-json": { "version": "0.1.7", + "resolved": "https://registry.npmjs.org/partial-json/-/partial-json-0.1.7.tgz", + "integrity": "sha512-Njv/59hHaokb/hRUjce3Hdv12wd60MtM9Z5Olmn+nehe0QDAsRtRbJPvJ0Z91TusF0SuZRIvnM+S4l6EIP8leA==", "license": "MIT" }, "node_modules/path-expression-matcher": { "version": "1.5.0", + "resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.5.0.tgz", + "integrity": "sha512-cbrerZV+6rvdQrrD+iGMcZFEiiSrbv9Tfdkvnusy6y0x0GKBXREFg/Y65GhIfm0tnLntThhzCnfKwp1WRjeCyQ==", "funding": [ { "type": "github", @@ -6096,6 +6732,8 @@ }, "node_modules/path-key": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==", "dev": true, "license": "MIT", "engines": { @@ -6104,11 +6742,15 @@ }, "node_modules/path-parse": { "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "dev": true, "license": "MIT" }, "node_modules/path-scurry": { "version": "2.0.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", + "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", "license": "BlueOak-1.0.0", "dependencies": { "lru-cache": "^11.0.0", @@ -6123,11 +6765,15 @@ }, "node_modules/pathe": { "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", "dev": true, "license": "MIT" }, "node_modules/pathval": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", "dev": true, "license": "MIT", "engines": { @@ -6136,6 +6782,8 @@ }, "node_modules/pdfjs-dist": { "version": "5.4.394", + "resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-5.4.394.tgz", + "integrity": "sha512-9ariAYGqUJzx+V/1W4jHyiyCep6IZALmDzoaTLZ6VNu8q9LWi1/ukhzHgE2Xsx96AZi0mbZuK4/ttIbqSbLypg==", "license": "Apache-2.0", "engines": { "node": ">=20.16.0 || >=22.3.0" @@ -6166,11 +6814,15 @@ }, "node_modules/picocolors": { "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", "dev": true, "license": "ISC" }, "node_modules/picomatch": { "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", "dev": true, "license": "MIT", "engines": { @@ -6182,6 +6834,8 @@ }, "node_modules/postcss": { "version": "8.5.14", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz", + "integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==", "dev": true, "funding": [ { @@ -6209,6 +6863,8 @@ }, "node_modules/prebuild-install": { "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", "dev": true, "license": "MIT", "dependencies": { @@ -6234,10 +6890,14 @@ }, "node_modules/process-nextick-args": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", "license": "MIT" }, "node_modules/proper-lockfile": { "version": "4.1.2", + "resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz", + "integrity": "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==", "license": "MIT", "dependencies": { "graceful-fs": "^4.2.4", @@ -6247,6 +6907,8 @@ }, "node_modules/proper-lockfile/node_modules/retry": { "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", "license": "MIT", "engines": { "node": ">= 4" @@ -6254,6 +6916,8 @@ }, "node_modules/protobufjs": { "version": "7.5.6", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.6.tgz", + "integrity": "sha512-M71sTMB146U3u0di3yup8iM+zv8yPRNQVr1KK4tyBitl3qFvEGucq/rGDRShD2rsJhtN02RJaJ7j5X5hmy8SJg==", "hasInstallScript": true, "license": "BSD-3-Clause", "dependencies": { @@ -6276,6 +6940,8 @@ }, "node_modules/pump": { "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", "dev": true, "license": "MIT", "dependencies": { @@ -6285,6 +6951,8 @@ }, "node_modules/queue-microtask": { "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", "dev": true, "funding": [ { @@ -6304,6 +6972,8 @@ }, "node_modules/rc": { "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", "dev": true, "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", "dependencies": { @@ -6318,6 +6988,8 @@ }, "node_modules/readable-stream": { "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", "dev": true, "license": "MIT", "dependencies": { @@ -6331,6 +7003,8 @@ }, "node_modules/rechoir": { "version": "0.6.2", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz", + "integrity": "sha512-HFM8rkZ+i3zrV+4LQjwQ0W+ez98pApMGM3HUrN04j3CqzPOzl9nmP15Y8YXNm8QHGv/eacOVEjqhmWpkRV0NAw==", "dev": true, "dependencies": { "resolve": "^1.1.6" @@ -6341,6 +7015,8 @@ }, "node_modules/require-directory": { "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", "dev": true, "license": "MIT", "engines": { @@ -6349,6 +7025,8 @@ }, "node_modules/resolve": { "version": "1.22.12", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", + "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", "dev": true, "license": "MIT", "dependencies": { @@ -6369,6 +7047,8 @@ }, "node_modules/resolve-pkg-maps": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", "dev": true, "license": "MIT", "funding": { @@ -6377,6 +7057,8 @@ }, "node_modules/retry": { "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", "license": "MIT", "engines": { "node": ">= 4" @@ -6384,6 +7066,8 @@ }, "node_modules/reusify": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", "dev": true, "license": "MIT", "engines": { @@ -6393,6 +7077,8 @@ }, "node_modules/rollup": { "version": "4.60.3", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.3.tgz", + "integrity": "sha512-pAQK9HalE84QSm4Po3EmWIZPd3FnjkShVkiMlz1iligWYkWQ7wHYd1PF/T7QZ5TVSD6uSTon5gBVMSM4JfBV+A==", "dev": true, "license": "MIT", "dependencies": { @@ -6436,11 +7122,15 @@ }, "node_modules/rollup/node_modules/@types/estree": { "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", "dev": true, "license": "MIT" }, "node_modules/run-parallel": { "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", "dev": true, "funding": [ { @@ -6463,6 +7153,8 @@ }, "node_modules/rxjs": { "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -6471,6 +7163,8 @@ }, "node_modules/safe-buffer": { "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", "funding": [ { "type": "github", @@ -6489,6 +7183,8 @@ }, "node_modules/semver": { "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", "dev": true, "license": "ISC", "bin": { @@ -6500,10 +7196,14 @@ }, "node_modules/setimmediate": { "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", "license": "MIT" }, "node_modules/shebang-command": { "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==", "dev": true, "license": "MIT", "dependencies": { @@ -6515,6 +7215,8 @@ }, "node_modules/shebang-regex": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==", "dev": true, "license": "MIT", "engines": { @@ -6523,6 +7225,8 @@ }, "node_modules/shell-quote": { "version": "1.8.3", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz", + "integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==", "dev": true, "license": "MIT", "engines": { @@ -6534,6 +7238,8 @@ }, "node_modules/shelljs": { "version": "0.9.2", + "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.9.2.tgz", + "integrity": "sha512-S3I64fEiKgTZzKCC46zT/Ib9meqofLrQVbpSswtjFfAVDW+AZ54WTnAM/3/yENoxz/V1Cy6u3kiiEbQ4DNphvw==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -6551,6 +7257,8 @@ }, "node_modules/shx": { "version": "0.4.0", + "resolved": "https://registry.npmjs.org/shx/-/shx-0.4.0.tgz", + "integrity": "sha512-Z0KixSIlGPpijKgcH6oCMCbltPImvaKy0sGH8AkLRXw1KyzpKtaCTizP2xen+hNDqVF4xxgvA0KXSb9o4Q6hnA==", "dev": true, "license": "MIT", "dependencies": { @@ -6566,15 +7274,21 @@ }, "node_modules/siginfo": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", "dev": true, "license": "ISC" }, "node_modules/signal-exit": { "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", "license": "ISC" }, "node_modules/simple-concat": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", "dev": true, "funding": [ { @@ -6594,6 +7308,8 @@ }, "node_modules/simple-get": { "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", "dev": true, "funding": [ { @@ -6618,6 +7334,8 @@ }, "node_modules/source-map-js": { "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", "dev": true, "license": "BSD-3-Clause", "engines": { @@ -6626,16 +7344,22 @@ }, "node_modules/stackback": { "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", "dev": true, "license": "MIT" }, "node_modules/std-env": { "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", "dev": true, "license": "MIT" }, "node_modules/string_decoder": { "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", "dev": true, "license": "MIT", "dependencies": { @@ -6644,6 +7368,8 @@ }, "node_modules/string-width": { "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "dev": true, "license": "MIT", "dependencies": { @@ -6696,6 +7422,8 @@ }, "node_modules/string-width/node_modules/ansi-regex": { "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true, "license": "MIT", "engines": { @@ -6704,6 +7432,8 @@ }, "node_modules/string-width/node_modules/strip-ansi": { "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, "license": "MIT", "dependencies": { @@ -6755,6 +7485,8 @@ }, "node_modules/strip-eof": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", + "integrity": "sha512-7FCwGGmx8mD5xQd3RPUvnSpUXHM3BWuzjtpD4TXsfcZ9EL4azvVVUscFYwD9nx8Kh+uCBC00XBtAykoMHwTh8Q==", "dev": true, "license": "MIT", "engines": { @@ -6763,6 +7495,8 @@ }, "node_modules/strip-json-comments": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", "dev": true, "license": "MIT", "engines": { @@ -6771,6 +7505,8 @@ }, "node_modules/strip-literal": { "version": "3.1.0", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz", + "integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==", "dev": true, "license": "MIT", "dependencies": { @@ -6782,6 +7518,8 @@ }, "node_modules/strnum": { "version": "2.3.0", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.3.0.tgz", + "integrity": "sha512-ums3KNd42PGyx5xaoVTO1mjU1bH3NpY4vsrVlnv9PNGqQj8wd7rJ6nEypLrJ7z5vxK5RP0yMLo6J/Gsm62DI5Q==", "funding": [ { "type": "github", @@ -6792,6 +7530,8 @@ }, "node_modules/supports-color": { "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", "dev": true, "license": "MIT", "dependencies": { @@ -6806,6 +7546,8 @@ }, "node_modules/supports-preserve-symlinks-flag": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", "dev": true, "license": "MIT", "engines": { @@ -6817,6 +7559,8 @@ }, "node_modules/tailwind-merge": { "version": "3.5.0", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.5.0.tgz", + "integrity": "sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A==", "license": "MIT", "funding": { "type": "github", @@ -6825,6 +7569,8 @@ }, "node_modules/tailwind-variants": { "version": "3.2.2", + "resolved": "https://registry.npmjs.org/tailwind-variants/-/tailwind-variants-3.2.2.tgz", + "integrity": "sha512-Mi4kHeMTLvKlM98XPnK+7HoBPmf4gygdFmqQPaDivc3DpYS6aIY6KiG/PgThrGvii5YZJqRsPz0aPyhoFzmZgg==", "license": "MIT", "engines": { "node": ">=16.x", @@ -6842,10 +7588,14 @@ }, "node_modules/tailwindcss": { "version": "4.2.4", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.4.tgz", + "integrity": "sha512-HhKppgO81FQof5m6TEnuBWCZGgfRAWbaeOaGT00KOy/Pf/j6oUihdvBpA7ltCeAvZpFhW3j0PTclkxsd4IXYDA==", "license": "MIT" }, "node_modules/tapable": { "version": "2.3.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", + "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", "dev": true, "license": "MIT", "engines": { @@ -6858,6 +7608,8 @@ }, "node_modules/tar-fs": { "version": "2.1.4", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", + "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", "dev": true, "license": "MIT", "dependencies": { @@ -6869,6 +7621,8 @@ }, "node_modules/tar-stream": { "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", "dev": true, "license": "MIT", "dependencies": { @@ -6978,16 +7732,22 @@ }, "node_modules/tinybench": { "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", "dev": true, "license": "MIT" }, "node_modules/tinyexec": { "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", "dev": true, "license": "MIT" }, "node_modules/tinyglobby": { "version": "0.2.16", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", + "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", "dev": true, "license": "MIT", "dependencies": { @@ -7003,6 +7763,8 @@ }, "node_modules/tinyglobby/node_modules/fdir": { "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", "dev": true, "license": "MIT", "engines": { @@ -7019,6 +7781,8 @@ }, "node_modules/tinyglobby/node_modules/picomatch": { "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", "engines": { @@ -7030,6 +7794,8 @@ }, "node_modules/tinypool": { "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", "dev": true, "license": "MIT", "engines": { @@ -7038,6 +7804,8 @@ }, "node_modules/tinyrainbow": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", "dev": true, "license": "MIT", "engines": { @@ -7046,6 +7814,8 @@ }, "node_modules/tinyspy": { "version": "4.0.4", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.4.tgz", + "integrity": "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==", "dev": true, "license": "MIT", "engines": { @@ -7054,6 +7824,8 @@ }, "node_modules/to-regex-range": { "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", "dev": true, "license": "MIT", "dependencies": { @@ -7065,6 +7837,8 @@ }, "node_modules/tree-kill": { "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", "dev": true, "license": "MIT", "bin": { @@ -7073,14 +7847,20 @@ }, "node_modules/ts-algebra": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ts-algebra/-/ts-algebra-2.0.0.tgz", + "integrity": "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==", "license": "MIT" }, "node_modules/tslib": { "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD" }, "node_modules/tsx": { "version": "4.21.0", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", + "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", "dev": true, "license": "MIT", "dependencies": { @@ -7099,6 +7879,8 @@ }, "node_modules/tunnel-agent": { "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -7110,10 +7892,14 @@ }, "node_modules/typebox": { "version": "1.1.38", + "resolved": "https://registry.npmjs.org/typebox/-/typebox-1.1.38.tgz", + "integrity": "sha512-pZ0aQPmMmXoUvSbeuWf/Hzsc+avNw/Zd6VeE8CFgkVGWyuHPJvqeJJDeJqLve+K70LvjYIoleGcoJHPT17cWoA==", "license": "MIT" }, "node_modules/typescript": { "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", "bin": { @@ -7126,6 +7912,8 @@ }, "node_modules/uhtml": { "version": "5.0.9", + "resolved": "https://registry.npmjs.org/uhtml/-/uhtml-5.0.9.tgz", + "integrity": "sha512-qPyu3vGilaLe6zrjOCD/xezWEHLwdevxmbY3hzyhT25KBDF4F7YYW3YZcL3kylD/6dMoVISHjn8ggV3+9FY+5g==", "license": "MIT", "dependencies": { "@webreflection/alien-signals": "^0.3.2" @@ -7133,6 +7921,8 @@ }, "node_modules/undici": { "version": "7.25.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.25.0.tgz", + "integrity": "sha512-xXnp4kTyor2Zq+J1FfPI6Eq3ew5h6Vl0F/8d9XU5zZQf1tX9s2Su1/3PiMmUANFULpmksxkClamIZcaUqryHsQ==", "license": "MIT", "engines": { "node": ">=20.18.1" @@ -7140,14 +7930,20 @@ }, "node_modules/undici-types": { "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", "license": "MIT" }, "node_modules/util-deprecate": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "license": "MIT" }, "node_modules/vite": { "version": "7.3.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.3.tgz", + "integrity": "sha512-/4XH147Ui7OGTjg3HbdWe5arnZQSbfuRzdr9Ec7TQi5I7R+ir0Rlc9GIvD4v0XZurELqA035KVXJXpR61xhiTA==", "dev": true, "license": "MIT", "dependencies": { @@ -7221,6 +8017,8 @@ }, "node_modules/vite-node": { "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", + "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", "dev": true, "license": "MIT", "dependencies": { @@ -7242,6 +8040,8 @@ }, "node_modules/vite/node_modules/fdir": { "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", "dev": true, "license": "MIT", "engines": { @@ -7258,6 +8058,8 @@ }, "node_modules/vite/node_modules/picomatch": { "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", "engines": { @@ -7269,6 +8071,8 @@ }, "node_modules/vitest": { "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz", + "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==", "dev": true, "license": "MIT", "dependencies": { @@ -7340,6 +8144,8 @@ }, "node_modules/vitest/node_modules/picomatch": { "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", "engines": { @@ -7351,6 +8157,8 @@ }, "node_modules/web-streams-polyfill": { "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", "license": "MIT", "engines": { "node": ">= 8" @@ -7358,10 +8166,14 @@ }, "node_modules/whatwg-fetch": { "version": "3.6.20", + "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.20.tgz", + "integrity": "sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==", "license": "MIT" }, "node_modules/which": { "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", "dev": true, "license": "ISC", "dependencies": { @@ -7373,6 +8185,8 @@ }, "node_modules/why-is-node-running": { "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", "dev": true, "license": "MIT", "dependencies": { @@ -7388,6 +8202,8 @@ }, "node_modules/wrap-ansi": { "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", "dev": true, "license": "MIT", "dependencies": { @@ -7446,6 +8262,8 @@ }, "node_modules/wrap-ansi/node_modules/ansi-regex": { "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true, "license": "MIT", "engines": { @@ -7454,6 +8272,8 @@ }, "node_modules/wrap-ansi/node_modules/strip-ansi": { "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, "license": "MIT", "dependencies": { @@ -7465,11 +8285,15 @@ }, "node_modules/wrappy": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "dev": true, "license": "ISC" }, "node_modules/ws": { "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz", + "integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==", "license": "MIT", "engines": { "node": ">=10.0.0" @@ -7501,6 +8325,8 @@ }, "node_modules/y18n": { "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", "dev": true, "license": "ISC", "engines": { @@ -7509,6 +8335,8 @@ }, "node_modules/yaml": { "version": "2.8.4", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.4.tgz", + "integrity": "sha512-ml/JPOj9fOQK8RNnWojA67GbZ0ApXAUlN2UQclwv2eVgTgn7O9gg9o7paZWKMp4g0H3nTLtS9LVzhkpOFIKzog==", "license": "ISC", "bin": { "yaml": "bin.mjs" @@ -7522,6 +8350,8 @@ }, "node_modules/zod": { "version": "4.4.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", + "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" @@ -7529,6 +8359,8 @@ }, "node_modules/zod-to-json-schema": { "version": "3.25.2", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.2.tgz", + "integrity": "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==", "license": "ISC", "peerDependencies": { "zod": "^3.25.28 || ^4" From bfd6535f7babc2f98d043794e2f705d208b046fb Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Sun, 17 May 2026 01:19:01 +0200 Subject: [PATCH 063/166] fix(coding-agent): update clipboard native package closes #4492 --- package-lock.json | 88 +++++++++++++++--------------- packages/coding-agent/CHANGELOG.md | 1 + packages/coding-agent/package.json | 2 +- 3 files changed, 46 insertions(+), 45 deletions(-) diff --git a/package-lock.json b/package-lock.json index dce6b3b7..fcc43689 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1796,31 +1796,31 @@ } }, "node_modules/@mariozechner/clipboard": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/@mariozechner/clipboard/-/clipboard-0.3.5.tgz", - "integrity": "sha512-D3F+UrU9CR7roJt0zDLp6Oc+4/KlLDIrN4frH+6V90SJNW2KKUec1oCQIPaaDjCqeOsQyX9dyqYbImIQIM45PA==", + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard/-/clipboard-0.3.6.tgz", + "integrity": "sha512-MXdtr+6+ntlIVHdrZYuZNQydu6o8yZswFJ2Ln81j2O/Y9B/LDHvEaIm95xWNPkjGTWriSOeLnQJRFs6dYb60bg==", "license": "MIT", "optional": true, "engines": { "node": ">= 10" }, "optionalDependencies": { - "@mariozechner/clipboard-darwin-arm64": "0.3.2", - "@mariozechner/clipboard-darwin-universal": "0.3.2", - "@mariozechner/clipboard-darwin-x64": "0.3.2", - "@mariozechner/clipboard-linux-arm64-gnu": "0.3.2", - "@mariozechner/clipboard-linux-arm64-musl": "0.3.2", - "@mariozechner/clipboard-linux-riscv64-gnu": "0.3.2", - "@mariozechner/clipboard-linux-x64-gnu": "0.3.2", - "@mariozechner/clipboard-linux-x64-musl": "0.3.2", - "@mariozechner/clipboard-win32-arm64-msvc": "0.3.2", - "@mariozechner/clipboard-win32-x64-msvc": "0.3.2" + "@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" } }, "node_modules/@mariozechner/clipboard-darwin-arm64": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-darwin-arm64/-/clipboard-darwin-arm64-0.3.2.tgz", - "integrity": "sha512-uBf6K7Je1ihsgvmWxA8UCGCeI+nbRVRXoarZdLjl6slz94Zs1tNKFZqx7aCI5O1i3e0B6ja82zZ06BWrl0MCVw==", + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-darwin-arm64/-/clipboard-darwin-arm64-0.3.6.tgz", + "integrity": "sha512-HjaisYCAbHi/1+N1yDAQHc8ZXGffufIUT5NSOSVR3f3AuMDusxTtnbK8tZ7JFDkShua1oNGZoNwQHsc8MPtE0Q==", "cpu": [ "arm64" ], @@ -1834,9 +1834,9 @@ } }, "node_modules/@mariozechner/clipboard-darwin-universal": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-darwin-universal/-/clipboard-darwin-universal-0.3.2.tgz", - "integrity": "sha512-mxSheKTW2U9LsBdXy0SdmdCAE5HqNS9QUmpNHLnfJ+SsbFKALjEZc5oRrVMXxGQSirDvYf5bjmRyT0QYYonnlg==", + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-darwin-universal/-/clipboard-darwin-universal-0.3.6.tgz", + "integrity": "sha512-8BWtPjOtJOJoykml3w0fx0zRrfWP31mXrJwfoA7xzNprkZw1uolCNfgmjDiVBseoKjp16EGITz7bN+61qn8dWA==", "license": "MIT", "optional": true, "os": [ @@ -1847,9 +1847,9 @@ } }, "node_modules/@mariozechner/clipboard-darwin-x64": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-darwin-x64/-/clipboard-darwin-x64-0.3.2.tgz", - "integrity": "sha512-U1BcVEoidvwIp95+HJswSW+xr28EQiHR7rZjH6pn8Sja5yO4Yoe3yCN0Zm8Lo72BbSOK/fTSq0je7CJpaPCspg==", + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-darwin-x64/-/clipboard-darwin-x64-0.3.6.tgz", + "integrity": "sha512-p9syiZD1kU4I+1ya7f7g+zD1GiUvR8fdlRlNmgsZNWlyjtc8rlV2EjTLd/35x1LsdBq020GVvtzp0ZmPgBI09Q==", "cpu": [ "x64" ], @@ -1863,9 +1863,9 @@ } }, "node_modules/@mariozechner/clipboard-linux-arm64-gnu": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-linux-arm64-gnu/-/clipboard-linux-arm64-gnu-0.3.2.tgz", - "integrity": "sha512-BsinwG3yWTIjdgNCxsFlip7LkfwPk+ruw/aFCXHUg/fb5XC/Ksp+YMQ7u0LUtiKzIv/7LMXgZInJQH6gxbAaqQ==", + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-linux-arm64-gnu/-/clipboard-linux-arm64-gnu-0.3.6.tgz", + "integrity": "sha512-5JFf5rGofrm+V29HNF+wLthXphHdQpMbKDUYJ5tML6/Z5DLlLOV/9Ak4kDPtYyZ+Dzf+kAusE0VsFg4+tfP1IA==", "cpu": [ "arm64" ], @@ -1879,9 +1879,9 @@ } }, "node_modules/@mariozechner/clipboard-linux-arm64-musl": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-linux-arm64-musl/-/clipboard-linux-arm64-musl-0.3.2.tgz", - "integrity": "sha512-0/Gi5Xq2V6goXBop19ePoHvXsmJD9SzFlO3S+d6+T2b+BlPcpOu3Oa0wTjl+cZrLAAEzA86aPNBI+VVAFDFPKw==", + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-linux-arm64-musl/-/clipboard-linux-arm64-musl-0.3.6.tgz", + "integrity": "sha512-JlVjxxw0GbGC0djXYWRIqyteO3J1KZ/QG3udlEFaOD5TLOM1FnmXXAPDQBqr+aBVr720ef9K00dirYnJ0LDCtw==", "cpu": [ "arm64" ], @@ -1895,9 +1895,9 @@ } }, "node_modules/@mariozechner/clipboard-linux-riscv64-gnu": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-linux-riscv64-gnu/-/clipboard-linux-riscv64-gnu-0.3.2.tgz", - "integrity": "sha512-2AFFiXB24qf0zOZsxI1GJGb9wQGlOJyN6UwoXqmKS3dpQi/l6ix30IzDDA4c4ZcCcx4D+9HLYXhC1w7Sov8pXA==", + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-linux-riscv64-gnu/-/clipboard-linux-riscv64-gnu-0.3.6.tgz", + "integrity": "sha512-4t8BUi5zZ+L77otFQVnVSlaTyAX4TVk9EqQm4syMrEQp96trFEHEwwNHcNEBGzYv5+K7mxay50TthYkz47OWzQ==", "cpu": [ "riscv64" ], @@ -1911,9 +1911,9 @@ } }, "node_modules/@mariozechner/clipboard-linux-x64-gnu": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-linux-x64-gnu/-/clipboard-linux-x64-gnu-0.3.2.tgz", - "integrity": "sha512-v6fVnsn7WMGg73Dab8QMwyFce7tzGfgEixKgzLP8f1GJqkJZi5zO4k4FOHzSgUufgLil63gnxvMpjWkgfeQN7A==", + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-linux-x64-gnu/-/clipboard-linux-x64-gnu-0.3.6.tgz", + "integrity": "sha512-trtPwcNLW37irwQCJLtCxLw757jjJZk3TSnY/MU9bhtWtA3K9b/eLW0e4RGhUXDoFRds9opNWWaUDuFLa8dm0w==", "cpu": [ "x64" ], @@ -1927,9 +1927,9 @@ } }, "node_modules/@mariozechner/clipboard-linux-x64-musl": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-linux-x64-musl/-/clipboard-linux-x64-musl-0.3.2.tgz", - "integrity": "sha512-xVUtnoMQ8v2JVyfJLKKXACA6avdnchdbBkTsZs8BgJQo29qwCp5NIHAUO8gbJ40iaEGToW5RlmVk2M9V0HsHEw==", + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-linux-x64-musl/-/clipboard-linux-x64-musl-0.3.6.tgz", + "integrity": "sha512-WfnzIvOCCWQiN0MmltCEo6cLceUDbYe+I7xyFZjaps5A+2Op/M2CY7Rey+C4ucQhrvmpoHmTSFgY9ODWk7snoA==", "cpu": [ "x64" ], @@ -1943,9 +1943,9 @@ } }, "node_modules/@mariozechner/clipboard-win32-arm64-msvc": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-win32-arm64-msvc/-/clipboard-win32-arm64-msvc-0.3.2.tgz", - "integrity": "sha512-AEgg95TNi8TGgak2wSXZkXKCvAUTjWoU1Pqb0ON7JHrX78p616XUFNTJohtIon3e0w6k0pYPZeCuqRCza/Tqeg==", + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-win32-arm64-msvc/-/clipboard-win32-arm64-msvc-0.3.6.tgz", + "integrity": "sha512-+8+1aHYsBPUjmW3otmWlg+Hijt0iJvoBBs5e0mxFeUd4gDaKMB8Bn6x7c6KVtscg7E5j5NFXnwQqNSIAO4p8zQ==", "cpu": [ "arm64" ], @@ -1959,9 +1959,9 @@ } }, "node_modules/@mariozechner/clipboard-win32-x64-msvc": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-win32-x64-msvc/-/clipboard-win32-x64-msvc-0.3.2.tgz", - "integrity": "sha512-tGRuYpZwDOD7HBrCpyRuhGnHHSCknELvqwKKUG4JSfSB7JIU7LKRh6zx6fMUOQd8uISK35TjFg5UcNih+vJhFA==", + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-win32-x64-msvc/-/clipboard-win32-x64-msvc-0.3.6.tgz", + "integrity": "sha512-S4xfPmERC8ZkiLHe3vekZCjdDwNEETCuvCgQK2kP6/TnvmUkq1y2Pk+DjM4t8uh9KMX9bH4zs5ePcKa8GTXmfg==", "cpu": [ "x64" ], @@ -8478,7 +8478,7 @@ "node": ">=20.6.0" }, "optionalDependencies": { - "@mariozechner/clipboard": "^0.3.5" + "@mariozechner/clipboard": "^0.3.6" } }, "packages/coding-agent/examples/extensions/custom-provider-anthropic": { diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 5f7a6d45..d27ff42b 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -9,6 +9,7 @@ ### Fixed +- Fixed macOS clipboard access errors under sandboxed pasteboard denial so they do not abort the process ([#4492](https://github.com/earendil-works/pi/issues/4492)). - Fixed the scoped model startup hint to show the configured model-cycle keybinding ([#4508](https://github.com/earendil-works/pi/issues/4508)). - Fixed `fd` auto-download on macOS x86_64 by pinning the last release that ships an Intel macOS binary ([#4559](https://github.com/earendil-works/pi/issues/4559)). - Fixed skill diagnostics to stop warning when a skill name differs from its parent directory ([#4534](https://github.com/earendil-works/pi/issues/4534)). diff --git a/packages/coding-agent/package.json b/packages/coding-agent/package.json index 394eb948..460c365b 100644 --- a/packages/coding-agent/package.json +++ b/packages/coding-agent/package.json @@ -62,7 +62,7 @@ } }, "optionalDependencies": { - "@mariozechner/clipboard": "^0.3.5" + "@mariozechner/clipboard": "^0.3.6" }, "devDependencies": { "@types/diff": "^7.0.2", From e26fbb3d4604d518a39cb16291d4061d4b5146d5 Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Sun, 17 May 2026 01:22:09 +0200 Subject: [PATCH 064/166] fix(coding-agent): route global fetch through undici dispatcher closes #4519 --- packages/coding-agent/CHANGELOG.md | 1 + packages/coding-agent/src/cli.ts | 13 +++++++++++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index d27ff42b..4e4c9e52 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -9,6 +9,7 @@ ### Fixed +- Fixed Node 26 OpenAI-compatible streams timing out after five idle minutes by routing global fetch through pi's undici dispatcher ([#4519](https://github.com/earendil-works/pi/issues/4519)). - Fixed macOS clipboard access errors under sandboxed pasteboard denial so they do not abort the process ([#4492](https://github.com/earendil-works/pi/issues/4492)). - Fixed the scoped model startup hint to show the configured model-cycle keybinding ([#4508](https://github.com/earendil-works/pi/issues/4508)). - Fixed `fd` auto-download on macOS x86_64 by pinning the last release that ships an Intel macOS binary ([#4559](https://github.com/earendil-works/pi/issues/4559)). diff --git a/packages/coding-agent/src/cli.ts b/packages/coding-agent/src/cli.ts index 6e0e8403..dcfb4bba 100644 --- a/packages/coding-agent/src/cli.ts +++ b/packages/coding-agent/src/cli.ts @@ -5,7 +5,7 @@ * * Test with: npx tsx src/cli-new.ts [args...] */ -import { EnvHttpProxyAgent, setGlobalDispatcher } from "undici"; +import { EnvHttpProxyAgent, setGlobalDispatcher, fetch as undiciFetch } from "undici"; import { APP_NAME } from "./config.js"; import { main } from "./main.js"; @@ -17,6 +17,15 @@ process.emitWarning = (() => {}) as typeof process.emitWarning; // (e.g. vLLM buffering a large tool call) exceed that and abort the SSE stream // with UND_ERR_BODY_TIMEOUT. Disable both — provider SDKs enforce their own // AbortController-based deadlines via retry.provider.timeoutMs. -setGlobalDispatcher(new EnvHttpProxyAgent({ bodyTimeout: 0, headersTimeout: 0 })); +// Node 26 uses an internal undici for globalThis.fetch that does not honor npm +// undici's global dispatcher, so route global fetch through npm undici as well. +const dispatcher = new EnvHttpProxyAgent({ bodyTimeout: 0, headersTimeout: 0 }); +setGlobalDispatcher(dispatcher); +const fetchWithDispatcher = undiciFetch as unknown as typeof fetch; +globalThis.fetch = (input, init) => + fetchWithDispatcher(input, { + ...init, + dispatcher, + } as unknown as RequestInit); main(process.argv.slice(2)); From 72104d88f96b4b31211048d4474b141924258fa7 Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Sun, 17 May 2026 01:29:35 +0200 Subject: [PATCH 065/166] docs: audit unreleased changelogs --- packages/ai/CHANGELOG.md | 4 ++++ packages/coding-agent/CHANGELOG.md | 17 +++++++++++++++++ packages/tui/CHANGELOG.md | 8 ++++++++ 3 files changed, 29 insertions(+) diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index f3b72aff..0e7176f5 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -4,12 +4,16 @@ ### Added +- Added image generation APIs, image model metadata, and built-in OpenRouter image generation support ([#3887](https://github.com/earendil-works/pi-mono/pull/3887) by [@cristinaponcela](https://github.com/cristinaponcela)). - Added Together AI as a built-in OpenAI-compatible provider with generated model metadata and `TOGETHER_API_KEY` authentication ([#3624](https://github.com/earendil-works/pi-mono/pull/3624) by [@Nutlope](https://github.com/Nutlope)). ### Fixed - Fixed GitHub Copilot model availability to ignore generic `GH_TOKEN` and `GITHUB_TOKEN` environment variables, requiring OAuth login or `COPILOT_GITHUB_TOKEN` instead ([#4485](https://github.com/earendil-works/pi/issues/4485)). - Fixed `openai-completions` streams to surface an error when the stream ends before any terminal `finish_reason`, so truncated responses can retry instead of being accepted as success ([#4345](https://github.com/earendil-works/pi/issues/4345)). +- Fixed Fireworks provider caching compatibility by adding session affinity headers and model metadata compat settings ([#4358](https://github.com/earendil-works/pi-mono/pull/4358) by [@yanirz](https://github.com/yanirz)). +- Fixed OpenAI Codex WebSocket transport to respect proxy environment variables under Bun ([#4354](https://github.com/earendil-works/pi-mono/pull/4354) by [@haoqixu](https://github.com/haoqixu)). +- Fixed OpenRouter cache usage normalization to preserve cached-token semantics without treating cached tokens as cache writes. - Fixed Bedrock proxy handling to preserve `NO_PROXY` exclusions while using HTTP(S)-only proxy agents. - Fixed compiled Bun binaries failing to start outside the repo when Bedrock proxy support tried to resolve `proxy-from-env` from external `node_modules` ([#4513](https://github.com/earendil-works/pi/issues/4513)). - Fixed GitHub Copilot Claude test coverage to use the current Claude Sonnet 4.6 model ID. diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 4e4c9e52..bed43bf3 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -2,24 +2,41 @@ ## [Unreleased] +### New Features + +- **Image generation support** - Added image generation APIs, generated image model metadata, and built-in OpenRouter image generation support inherited from `@earendil-works/pi-ai`. +- **Together AI provider** - Added Together AI as a built-in provider with `/login` API-key auth, default model resolution, and setup docs. See [README.md#providers--models](README.md#providers--models) and [docs/providers.md](docs/providers.md). +- **Windows ARM64 standalone binaries** - Added standalone release artifacts for Windows ARM64. +- **Improved terminal and markdown rendering** - Added markdown list indentation, task-list checkbox rendering, large markdown robustness, and inline image placement fixes inherited from `@earendil-works/pi-tui`. + ### Added +- Added image generation support from `@earendil-works/pi-ai`, including image generation APIs, image model metadata, and built-in OpenRouter image generation support ([#3887](https://github.com/earendil-works/pi-mono/pull/3887) by [@cristinaponcela](https://github.com/cristinaponcela)). - Added Together AI to built-in provider setup, `/login` API-key auth, and default model resolution ([#3624](https://github.com/earendil-works/pi-mono/pull/3624) by [@Nutlope](https://github.com/Nutlope)). - Added Windows ARM64 standalone binary release artifacts ([#4458](https://github.com/earendil-works/pi/pull/4458) by [@brianmichel](https://github.com/brianmichel)). ### Fixed - Fixed Node 26 OpenAI-compatible streams timing out after five idle minutes by routing global fetch through pi's undici dispatcher ([#4519](https://github.com/earendil-works/pi/issues/4519)). +- Fixed pnpm global package installs by resolving the global package root from pnpm's layout. - Fixed macOS clipboard access errors under sandboxed pasteboard denial so they do not abort the process ([#4492](https://github.com/earendil-works/pi/issues/4492)). - Fixed the scoped model startup hint to show the configured model-cycle keybinding ([#4508](https://github.com/earendil-works/pi/issues/4508)). +- Fixed resource path display to disambiguate package/resource names that collide across package locations. - Fixed `fd` auto-download on macOS x86_64 by pinning the last release that ships an Intel macOS binary ([#4559](https://github.com/earendil-works/pi/issues/4559)). - Fixed skill diagnostics to stop warning when a skill name differs from its parent directory ([#4534](https://github.com/earendil-works/pi/issues/4534)). - Fixed prompt template argument parsing to split unquoted multiline input on newlines ([#4553](https://github.com/earendil-works/pi/issues/4553)). - Fixed `--resume` session listing to cap in-flight session metadata loads and avoid OOM on large session histories ([#4583](https://github.com/earendil-works/pi/issues/4583)). - Fixed interactive error messages to render with trailing spacing so reload errors do not run into resource listings ([#4510](https://github.com/earendil-works/pi/issues/4510)). +- Fixed `.agents` package provenance metadata to survive package-manager scans. - Fixed nested code fences in the Termux setup documentation so the example AGENTS.md renders correctly ([#4503](https://github.com/earendil-works/pi/issues/4503)). - Fixed tool output expansion while extension confirmation dialogs are focused ([#4429](https://github.com/earendil-works/pi/issues/4429)). - Fixed auto-retry for Anthropic streams that end before `message_stop` ([#4433](https://github.com/earendil-works/pi/issues/4433)). +- Fixed compaction summary calls to clamp requested output tokens to model limits. +- Fixed uncaught interactive-mode exceptions to restore the terminal before exiting ([#4426](https://github.com/earendil-works/pi-mono/pull/4426) by [@ofa1](https://github.com/ofa1)). +- Fixed ANSI stripping to match `strip-ansi` behavior after dependency removal. +- Fixed UUIDv7 sequence generation shared by session IDs after dependency removal. +- Fixed OpenRouter cached-token usage accounting, Fireworks caching compatibility, and OpenAI Codex WebSocket proxy handling inherited from `@earendil-works/pi-ai`. +- Fixed markdown list wrapping, task-list checkboxes, large markdown rendering, WezTerm Kitty keyboard escape handling, and short-viewport inline image placement inherited from `@earendil-works/pi-tui`. - Fixed theme sharing across package scopes so extensions do not crash with `Theme not initialized` ([#4333](https://github.com/earendil-works/pi/issues/4333)). - Fixed keybinding hints to show Option instead of Alt on macOS ([#4289](https://github.com/earendil-works/pi/issues/4289)). - Fixed the interactive update notification to render the changelog as an OSC 8 hyperlink when the terminal supports hyperlinks ([#4280](https://github.com/earendil-works/pi/issues/4280)). diff --git a/packages/tui/CHANGELOG.md b/packages/tui/CHANGELOG.md index 204d6a39..90d57382 100644 --- a/packages/tui/CHANGELOG.md +++ b/packages/tui/CHANGELOG.md @@ -2,8 +2,16 @@ ## [Unreleased] +### Added + +- Added markdown list-item wrapping that preserves indentation for wrapped continuation lines ([#4327](https://github.com/earendil-works/pi-mono/pull/4327) by [@Perlence](https://github.com/Perlence)). + ### Fixed +- Fixed markdown task-list checkbox rendering ([#4379](https://github.com/earendil-works/pi-mono/pull/4379) by [@Perlence](https://github.com/Perlence)). +- Fixed markdown rendering robustness for very large markdown files ([#4463](https://github.com/earendil-works/pi-mono/pull/4463) by [@ndanielherrera](https://github.com/ndanielherrera)). +- Fixed Kitty image placement when the viewport is shorter than the rendered image ([#4461](https://github.com/earendil-works/pi-mono/pull/4461) by [@xu0o0](https://github.com/xu0o0)). +- Fixed WezTerm Kitty keyboard protocol edge cases so escape handling remains correct ([#4482](https://github.com/earendil-works/pi-mono/pull/4482) by [@Felixoid](https://github.com/Felixoid)). - Fixed inline image rendering to cap portrait images by height instead of always scaling them to the configured maximum width. ## [0.74.0] - 2026-05-07 From 2c708492e30a3e9577075f2145d411cd95713033 Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Sun, 17 May 2026 01:32:48 +0200 Subject: [PATCH 066/166] Release v0.74.1 --- package-lock.json | 2917 +++++++---------- packages/agent/CHANGELOG.md | 2 +- packages/agent/package.json | 4 +- packages/ai/CHANGELOG.md | 2 +- packages/ai/package.json | 2 +- packages/ai/scripts/generate-models.ts | 35 +- packages/ai/src/image-models.generated.ts | 165 + packages/ai/src/models.generated.ts | 1608 +++------ packages/coding-agent/CHANGELOG.md | 2 +- .../package-lock.json | 4 +- .../custom-provider-anthropic/package.json | 2 +- .../custom-provider-gitlab-duo/package.json | 2 +- .../extensions/sandbox/package-lock.json | 4 +- .../examples/extensions/sandbox/package.json | 2 +- .../extensions/with-deps/package-lock.json | 4 +- .../extensions/with-deps/package.json | 2 +- packages/coding-agent/package.json | 8 +- packages/tui/CHANGELOG.md | 2 +- packages/tui/package.json | 2 +- packages/web-ui/CHANGELOG.md | 2 +- packages/web-ui/example/package.json | 2 +- packages/web-ui/package.json | 6 +- 22 files changed, 1731 insertions(+), 3048 deletions(-) diff --git a/package-lock.json b/package-lock.json index fcc43689..08d0d5ae 100644 --- a/package-lock.json +++ b/package-lock.json @@ -52,7 +52,6 @@ "version": "0.0.26", "resolved": "https://registry.npmjs.org/@anthropic-ai/sandbox-runtime/-/sandbox-runtime-0.0.26.tgz", "integrity": "sha512-DYV5LSsVMnzq0lbfaYMSpxZPUMAx4+hy343dRss+pVCLIfF62qOhxpYfZ5TmOk1GTDQm5f9wPprMNSStmnsV4w==", - "dev": true, "license": "Apache-2.0", "dependencies": { "@pondwader/socks5-server": "^1.0.10", @@ -69,16 +68,6 @@ "node": ">=18.0.0" } }, - "node_modules/@anthropic-ai/sandbox-runtime/node_modules/zod": { - "version": "3.25.76", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", - "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/colinhacks" - } - }, "node_modules/@anthropic-ai/sdk": { "version": "0.91.1", "resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.91.1.tgz", @@ -128,44 +117,6 @@ "tslib": "^2.6.2" } }, - "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/is-array-buffer": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", - "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/util-buffer-from": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", - "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/is-array-buffer": "^2.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/util-utf8": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", - "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/util-buffer-from": "^2.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=14.0.0" - } - }, "node_modules/@aws-crypto/sha256-js": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-5.2.0.tgz", @@ -200,96 +151,25 @@ "tslib": "^2.6.2" } }, - "node_modules/@aws-crypto/util/node_modules/@smithy/is-array-buffer": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", - "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-crypto/util/node_modules/@smithy/util-buffer-from": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", - "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/is-array-buffer": "^2.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-crypto/util/node_modules/@smithy/util-utf8": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", - "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/util-buffer-from": "^2.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=14.0.0" - } - }, "node_modules/@aws-sdk/client-bedrock-runtime": { - "version": "3.1044.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-bedrock-runtime/-/client-bedrock-runtime-3.1044.0.tgz", - "integrity": "sha512-uwJB0pVZIey+kYag8xeUirMvuaDhhEHuYgSsapOKT5XCAije5pLNlg160eECJdhX9JEf0IthIM3IiHKSEkFtMw==", + "version": "3.1048.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-bedrock-runtime/-/client-bedrock-runtime-3.1048.0.tgz", + "integrity": "sha512-u+NT61JZEkRFtpL0CAw1N1dwxnaLgwVXQl/zjJxTGgLyS/jTIdg2SdoEoCTHxgDyCnqa1HEi9QOoE9/pYRNpOQ==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "^3.974.8", - "@aws-sdk/credential-provider-node": "^3.972.39", - "@aws-sdk/eventstream-handler-node": "^3.972.14", - "@aws-sdk/middleware-eventstream": "^3.972.10", - "@aws-sdk/middleware-host-header": "^3.972.10", - "@aws-sdk/middleware-logger": "^3.972.10", - "@aws-sdk/middleware-recursion-detection": "^3.972.11", - "@aws-sdk/middleware-user-agent": "^3.972.38", - "@aws-sdk/middleware-websocket": "^3.972.16", - "@aws-sdk/region-config-resolver": "^3.972.13", - "@aws-sdk/token-providers": "3.1044.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", - "@aws-sdk/util-endpoints": "^3.996.8", - "@aws-sdk/util-user-agent-browser": "^3.972.10", - "@aws-sdk/util-user-agent-node": "^3.973.24", - "@smithy/config-resolver": "^4.4.17", - "@smithy/core": "^3.23.17", - "@smithy/eventstream-serde-browser": "^4.2.14", - "@smithy/eventstream-serde-config-resolver": "^4.3.14", - "@smithy/eventstream-serde-node": "^4.2.14", - "@smithy/fetch-http-handler": "^5.3.17", - "@smithy/hash-node": "^4.2.14", - "@smithy/invalid-dependency": "^4.2.14", - "@smithy/middleware-content-length": "^4.2.14", - "@smithy/middleware-endpoint": "^4.4.32", - "@smithy/middleware-retry": "^4.5.7", - "@smithy/middleware-serde": "^4.2.20", - "@smithy/middleware-stack": "^4.2.14", - "@smithy/node-config-provider": "^4.3.14", - "@smithy/node-http-handler": "^4.6.1", - "@smithy/protocol-http": "^5.3.14", - "@smithy/smithy-client": "^4.12.13", + "@smithy/core": "^3.24.2", + "@smithy/fetch-http-handler": "^5.4.2", + "@smithy/node-http-handler": "^4.7.2", "@smithy/types": "^4.14.1", - "@smithy/url-parser": "^4.2.14", - "@smithy/util-base64": "^4.3.2", - "@smithy/util-body-length-browser": "^4.2.2", - "@smithy/util-body-length-node": "^4.2.3", - "@smithy/util-defaults-mode-browser": "^4.3.49", - "@smithy/util-defaults-mode-node": "^4.2.54", - "@smithy/util-endpoints": "^3.4.2", - "@smithy/util-middleware": "^4.2.14", - "@smithy/util-retry": "^4.3.6", - "@smithy/util-stream": "^4.5.25", - "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" }, "engines": { @@ -297,24 +177,18 @@ } }, "node_modules/@aws-sdk/core": { - "version": "3.974.8", - "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.974.8.tgz", - "integrity": "sha512-njR2qoG6ZuB0kvAS2FyICsFZJ6gmCcf2X/7JcD14sUvGDm26wiZ5BrA6LOiUxKFEF+IVe7kdroxyE00YlkiYsw==", + "version": "3.974.11", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.974.11.tgz", + "integrity": "sha512-QpnINq5FZH6EOaDEkmHdT7eUunbvD27pDNQypaWjFyYz7Zl1q3UCMQErBZxpmfGfI7MvI2TlK8KTkgNpv8b1ug==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/types": "^3.973.8", - "@aws-sdk/xml-builder": "^3.972.22", - "@smithy/core": "^3.23.17", - "@smithy/node-config-provider": "^4.3.14", - "@smithy/property-provider": "^4.2.14", - "@smithy/protocol-http": "^5.3.14", - "@smithy/signature-v4": "^5.3.14", - "@smithy/smithy-client": "^4.12.13", + "@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", - "@smithy/util-base64": "^4.3.2", - "@smithy/util-middleware": "^4.2.14", - "@smithy/util-retry": "^4.3.6", - "@smithy/util-utf8": "^4.2.2", + "bowser": "^2.11.0", "tslib": "^2.6.2" }, "engines": { @@ -322,14 +196,14 @@ } }, "node_modules/@aws-sdk/credential-provider-env": { - "version": "3.972.34", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.34.tgz", - "integrity": "sha512-XT0jtf8Fw9JE6ppsQeoNnZRiG+jqRixMT1v1ZR17G60UvVdsQmTG8nbEyHuEPfMxDXEhfdARaM/XiEhca4lGHQ==", + "version": "3.972.37", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.37.tgz", + "integrity": "sha512-/jpPvEh6f7ntmIzf7dNxoNX6Q8vt8UpesCjbW6mFfk4V1NW6bIy9qxcQ6WbA8As5yQhsZOe+xeNd4xHX8kdY2Q==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.974.8", + "@aws-sdk/core": "^3.974.11", "@aws-sdk/types": "^3.973.8", - "@smithy/property-provider": "^4.2.14", + "@smithy/core": "^3.24.2", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, @@ -338,20 +212,17 @@ } }, "node_modules/@aws-sdk/credential-provider-http": { - "version": "3.972.36", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.36.tgz", - "integrity": "sha512-DPoGWfy7J7RKxvbf5kOKIGQkD2ek3dbKgzKIGrnLuvZBz5myU+Im/H6pmc14QcnFbqHMqxvtWSgRDSJW3qXLQg==", + "version": "3.972.39", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.39.tgz", + "integrity": "sha512-pIgTpisWyWg7X1bUbzSjuUYosYTD0Ghz2M0hkSTmb3a6i3qV3uU+NYJPI/E2XSC0HcsZh5rsLPzeXrkb2DS0Cg==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.974.8", + "@aws-sdk/core": "^3.974.11", "@aws-sdk/types": "^3.973.8", - "@smithy/fetch-http-handler": "^5.3.17", - "@smithy/node-http-handler": "^4.6.1", - "@smithy/property-provider": "^4.2.14", - "@smithy/protocol-http": "^5.3.14", - "@smithy/smithy-client": "^4.12.13", + "@smithy/core": "^3.24.2", + "@smithy/fetch-http-handler": "^5.4.2", + "@smithy/node-http-handler": "^4.7.2", "@smithy/types": "^4.14.1", - "@smithy/util-stream": "^4.5.25", "tslib": "^2.6.2" }, "engines": { @@ -359,23 +230,22 @@ } }, "node_modules/@aws-sdk/credential-provider-ini": { - "version": "3.972.38", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.972.38.tgz", - "integrity": "sha512-oDzUBu2MGJFgoar05sPMCwSrhw44ASyccrHzj66vO69OZqi7I6hZZxXfuPLC8OCzW7C+sU+bI73XHij41yekgQ==", + "version": "3.972.41", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.972.41.tgz", + "integrity": "sha512-u2tyjaxJJzW8UtW4SM1ZcPMDwO6y+kV+llvou+Adts0FAKyzes5jG4izQN+KX3yE8ZROpS5y1LJ//xL2iSf76w==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.974.8", - "@aws-sdk/credential-provider-env": "^3.972.34", - "@aws-sdk/credential-provider-http": "^3.972.36", - "@aws-sdk/credential-provider-login": "^3.972.38", - "@aws-sdk/credential-provider-process": "^3.972.34", - "@aws-sdk/credential-provider-sso": "^3.972.38", - "@aws-sdk/credential-provider-web-identity": "^3.972.38", - "@aws-sdk/nested-clients": "^3.997.6", + "@aws-sdk/core": "^3.974.11", + "@aws-sdk/credential-provider-env": "^3.972.37", + "@aws-sdk/credential-provider-http": "^3.972.39", + "@aws-sdk/credential-provider-login": "^3.972.41", + "@aws-sdk/credential-provider-process": "^3.972.37", + "@aws-sdk/credential-provider-sso": "^3.972.41", + "@aws-sdk/credential-provider-web-identity": "^3.972.41", + "@aws-sdk/nested-clients": "^3.997.9", "@aws-sdk/types": "^3.973.8", - "@smithy/credential-provider-imds": "^4.2.14", - "@smithy/property-provider": "^4.2.14", - "@smithy/shared-ini-file-loader": "^4.4.9", + "@smithy/core": "^3.24.2", + "@smithy/credential-provider-imds": "^4.3.2", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, @@ -384,17 +254,15 @@ } }, "node_modules/@aws-sdk/credential-provider-login": { - "version": "3.972.38", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.38.tgz", - "integrity": "sha512-g1NosS8qe4OF++G2UFCM5ovSkgipC7YYor5KCWatG0UoMSO5YFj9C8muePlyVmOBV/WTI16Jo3/s1NUo/o1Bww==", + "version": "3.972.41", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.41.tgz", + "integrity": "sha512-0LBitxXiAiaE5nlFPfpNIww/8FRY/I7WIndWsc9GmNFOM7cE1wNpVNQEGEk9Outg5l8xl+3vybxFyUy4l9q/LQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.974.8", - "@aws-sdk/nested-clients": "^3.997.6", + "@aws-sdk/core": "^3.974.11", + "@aws-sdk/nested-clients": "^3.997.9", "@aws-sdk/types": "^3.973.8", - "@smithy/property-provider": "^4.2.14", - "@smithy/protocol-http": "^5.3.14", - "@smithy/shared-ini-file-loader": "^4.4.9", + "@smithy/core": "^3.24.2", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, @@ -403,21 +271,20 @@ } }, "node_modules/@aws-sdk/credential-provider-node": { - "version": "3.972.39", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.39.tgz", - "integrity": "sha512-HEswDQyxUtadoZ/bJsPPENHg7R0Lzym5LuMksJeHvqhCOpP+rtkDLKI4/ZChH4w3cf5kG8n6bZuI8PzajoiqMg==", + "version": "3.972.42", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.42.tgz", + "integrity": "sha512-D4oon2zbqqsWOJUM99Gm3/ZyJ0IJvTXVN3PyloGb3kQEyI36fjCZheZj422lAgTWWd6TSHgiImLt3RIaLdv3dQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/credential-provider-env": "^3.972.34", - "@aws-sdk/credential-provider-http": "^3.972.36", - "@aws-sdk/credential-provider-ini": "^3.972.38", - "@aws-sdk/credential-provider-process": "^3.972.34", - "@aws-sdk/credential-provider-sso": "^3.972.38", - "@aws-sdk/credential-provider-web-identity": "^3.972.38", + "@aws-sdk/credential-provider-env": "^3.972.37", + "@aws-sdk/credential-provider-http": "^3.972.39", + "@aws-sdk/credential-provider-ini": "^3.972.41", + "@aws-sdk/credential-provider-process": "^3.972.37", + "@aws-sdk/credential-provider-sso": "^3.972.41", + "@aws-sdk/credential-provider-web-identity": "^3.972.41", "@aws-sdk/types": "^3.973.8", - "@smithy/credential-provider-imds": "^4.2.14", - "@smithy/property-provider": "^4.2.14", - "@smithy/shared-ini-file-loader": "^4.4.9", + "@smithy/core": "^3.24.2", + "@smithy/credential-provider-imds": "^4.3.2", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, @@ -426,15 +293,14 @@ } }, "node_modules/@aws-sdk/credential-provider-process": { - "version": "3.972.34", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.34.tgz", - "integrity": "sha512-T3IFs4EVmVi1dVN5RciFnklCANSzvrQd/VuHY9ThHSQmYkTogjcGkoJEr+oNUPQZnso52183088NqysMPji1/Q==", + "version": "3.972.37", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.37.tgz", + "integrity": "sha512-7nVaHBUaWIddASYfVaA9O4D5ZVjewU3sCol9WqZPGfW0nR+0WqE0xHZnD/U2L33PlOB8KNXGKZ6wOES/QijKzg==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.974.8", + "@aws-sdk/core": "^3.974.11", "@aws-sdk/types": "^3.973.8", - "@smithy/property-provider": "^4.2.14", - "@smithy/shared-ini-file-loader": "^4.4.9", + "@smithy/core": "^3.24.2", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, @@ -443,35 +309,16 @@ } }, "node_modules/@aws-sdk/credential-provider-sso": { - "version": "3.972.38", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.972.38.tgz", - "integrity": "sha512-5ZxG+t0+3Q3QPh8KEjX6syskhgNf7I0MN7oGioTf6Lm1NTjfP7sIcYGNsthXC2qR8vcD3edNZwCr2ovfSSWuRA==", + "version": "3.972.41", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.972.41.tgz", + "integrity": "sha512-IOWAWEHe5LkjSKkkUUX9ciV6Y1scHTsnfEkdt5yyC4Slrc7AGbkLPrpntjqh18ksJAMOaVhoBsO8p2WyTcY2wQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.974.8", - "@aws-sdk/nested-clients": "^3.997.6", - "@aws-sdk/token-providers": "3.1041.0", + "@aws-sdk/core": "^3.974.11", + "@aws-sdk/nested-clients": "^3.997.9", + "@aws-sdk/token-providers": "3.1048.0", "@aws-sdk/types": "^3.973.8", - "@smithy/property-provider": "^4.2.14", - "@smithy/shared-ini-file-loader": "^4.4.9", - "@smithy/types": "^4.14.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws-sdk/credential-provider-sso/node_modules/@aws-sdk/token-providers": { - "version": "3.1041.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1041.0.tgz", - "integrity": "sha512-Th7kPI6YPtvJUcdznooXJMy+9rQWjmEF81LxaJssngBzuysK4a/x+l8kjm1zb7nYsUPbndnBdUnwng/3PLvtGw==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "^3.974.8", - "@aws-sdk/nested-clients": "^3.997.6", - "@aws-sdk/types": "^3.973.8", - "@smithy/property-provider": "^4.2.14", - "@smithy/shared-ini-file-loader": "^4.4.9", + "@smithy/core": "^3.24.2", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, @@ -480,16 +327,15 @@ } }, "node_modules/@aws-sdk/credential-provider-web-identity": { - "version": "3.972.38", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.38.tgz", - "integrity": "sha512-lYHFF30DGI20jZcYX8cm6Ns0V7f1dDN6g/MBDLTyD/5iw+bXs3yBr2iAiHDkx4RFU5JgsnZvCHYKiRVPRdmOgw==", + "version": "3.972.41", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.41.tgz", + "integrity": "sha512-mbACk9Yypa8nm4iGZLs0PofOXEcTDOUw6wDnsPXNDNSd2WNXs1tSo+6nc/fh0jLYdfVZThhBL98PHW4aXFsG5A==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.974.8", - "@aws-sdk/nested-clients": "^3.997.6", + "@aws-sdk/core": "^3.974.11", + "@aws-sdk/nested-clients": "^3.997.9", "@aws-sdk/types": "^3.973.8", - "@smithy/property-provider": "^4.2.14", - "@smithy/shared-ini-file-loader": "^4.4.9", + "@smithy/core": "^3.24.2", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, @@ -498,13 +344,13 @@ } }, "node_modules/@aws-sdk/eventstream-handler-node": { - "version": "3.972.14", - "resolved": "https://registry.npmjs.org/@aws-sdk/eventstream-handler-node/-/eventstream-handler-node-3.972.14.tgz", - "integrity": "sha512-m4X56gxG76/CKfxNVbOFuYwnAZcHgS6HOH8lgp15HoGHIAVTcZfZrXvcYzJFOMLEJgVn+JHBu6EiNV+xSNXXFg==", + "version": "3.972.16", + "resolved": "https://registry.npmjs.org/@aws-sdk/eventstream-handler-node/-/eventstream-handler-node-3.972.16.tgz", + "integrity": "sha512-yedpPgKftqjU5SlPFHfqWpOw6xSCRieWRG1euWOlXn4WJxt2VX92VprCa2PpSOXjVCAeK6dTjW9eJRXVig9yGA==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/types": "^3.973.8", - "@smithy/eventstream-codec": "^4.2.14", + "@smithy/core": "^3.24.2", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, @@ -513,13 +359,13 @@ } }, "node_modules/@aws-sdk/middleware-eventstream": { - "version": "3.972.10", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-eventstream/-/middleware-eventstream-3.972.10.tgz", - "integrity": "sha512-QUqLs7Af1II9X4fCRAu+EGHG3KHyOp4RkuLhRKoA3NuFlh6TL8i+zXBl8w2LUxqm44B/Kom45hgSlwA1SpTsXQ==", + "version": "3.972.12", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-eventstream/-/middleware-eventstream-3.972.12.tgz", + "integrity": "sha512-tHTHHCHNrq6XklQvlzHBDJG4Iuhh7NVPRdtmvP+nHFA+5sxPlIDzlAHHgfoYHGvT3NXP1yVP/L5c3opUn6T3Qg==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/types": "^3.973.8", - "@smithy/protocol-http": "^5.3.14", + "@smithy/core": "^3.24.2", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, @@ -527,112 +373,18 @@ "node": ">=20.0.0" } }, - "node_modules/@aws-sdk/middleware-host-header": { - "version": "3.972.10", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.972.10.tgz", - "integrity": "sha512-IJSsIMeVQ8MMCPbuh1AbltkFhLBLXn7aejzfX5YKT/VLDHn++Dcz8886tXckE+wQssyPUhaXrJhdakO2VilRhg==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "^3.973.8", - "@smithy/protocol-http": "^5.3.14", - "@smithy/types": "^4.14.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws-sdk/middleware-logger": { - "version": "3.972.10", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.972.10.tgz", - "integrity": "sha512-OOuGvvz1Dm20SjZo5oEBePFqxt5nf8AwkNDSyUHvD9/bfNASmstcYxFAHUowy4n6Io7mWUZ04JURZwSBvyQanQ==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "^3.973.8", - "@smithy/types": "^4.14.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws-sdk/middleware-recursion-detection": { - "version": "3.972.11", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.972.11.tgz", - "integrity": "sha512-+zz6f79Kj9V5qFK2P+D8Ehjnw4AhphAlCAsPjUqEcInA9umtSSKMrHbSagEeOIsDNuvVrH98bjRHcyQukTrhaQ==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "^3.973.8", - "@aws/lambda-invoke-store": "^0.2.2", - "@smithy/protocol-http": "^5.3.14", - "@smithy/types": "^4.14.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws-sdk/middleware-sdk-s3": { - "version": "3.972.37", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.972.37.tgz", - "integrity": "sha512-Km7M+i8DrLArVzrid1gfxeGhYHBd3uxvE77g0s5a52zPSVosxzQBnJ0gwWb6NIp/DOk8gsBMhi7V+cpJG0ndTA==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "^3.974.8", - "@aws-sdk/types": "^3.973.8", - "@aws-sdk/util-arn-parser": "^3.972.3", - "@smithy/core": "^3.23.17", - "@smithy/node-config-provider": "^4.3.14", - "@smithy/protocol-http": "^5.3.14", - "@smithy/signature-v4": "^5.3.14", - "@smithy/smithy-client": "^4.12.13", - "@smithy/types": "^4.14.1", - "@smithy/util-config-provider": "^4.2.2", - "@smithy/util-middleware": "^4.2.14", - "@smithy/util-stream": "^4.5.25", - "@smithy/util-utf8": "^4.2.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws-sdk/middleware-user-agent": { - "version": "3.972.38", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.972.38.tgz", - "integrity": "sha512-iz+B29TXcAZsJpwB+AwG/TTGA5l/VnmMZ2UxtiySOZjI6gCdmviXPwdgzcmuazMy16rXoPY4mYCGe7zdNKfx5A==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "^3.974.8", - "@aws-sdk/types": "^3.973.8", - "@aws-sdk/util-endpoints": "^3.996.8", - "@smithy/core": "^3.23.17", - "@smithy/protocol-http": "^5.3.14", - "@smithy/types": "^4.14.1", - "@smithy/util-retry": "^4.3.6", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, "node_modules/@aws-sdk/middleware-websocket": { - "version": "3.972.16", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-websocket/-/middleware-websocket-3.972.16.tgz", - "integrity": "sha512-86+S9oCyRVGzoMRpQhxkArp7kD2K75GPmaNevd9B6EyNhWoNvnCZZ3WbgN4j7ZT+jvtvBCGZvI2XHsWZJ+BRIg==", + "version": "3.972.19", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-websocket/-/middleware-websocket-3.972.19.tgz", + "integrity": "sha512-mkEhOGYozqKQkbFaVrjwr0faiwwZza1v5/jSY6Tucm3bD+uKTazIUH/4Yo6aMnQD2ua2W9cMP6s8mvwTcjtqHw==", "license": "Apache-2.0", "dependencies": { + "@aws-sdk/core": "^3.974.11", "@aws-sdk/types": "^3.973.8", - "@aws-sdk/util-format-url": "^3.972.10", - "@smithy/eventstream-codec": "^4.2.14", - "@smithy/eventstream-serde-browser": "^4.2.14", - "@smithy/fetch-http-handler": "^5.3.17", - "@smithy/protocol-http": "^5.3.14", - "@smithy/signature-v4": "^5.3.14", + "@smithy/core": "^3.24.2", + "@smithy/fetch-http-handler": "^5.4.2", + "@smithy/signature-v4": "^5.4.2", "@smithy/types": "^4.14.1", - "@smithy/util-base64": "^4.3.2", - "@smithy/util-hex-encoding": "^4.2.2", - "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" }, "engines": { @@ -640,64 +392,19 @@ } }, "node_modules/@aws-sdk/nested-clients": { - "version": "3.997.6", - "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.997.6.tgz", - "integrity": "sha512-WBDnqatJl+kGObpfmfSxqnXeYTu3Me8wx8WCtvoxX3pfWrrTv8I4WTMSSs7PZqcRcVh8WeUKMgGFjMG+52SR1w==", + "version": "3.997.9", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.997.9.tgz", + "integrity": "sha512-jPR3rnmRI4hWYyzfmTGBr7NblMp8QYYeflHXba1H6+7CGrWVqWKQzaXFQ4qbExqPRsXN3T3L3JxFhr6aouXUGQ==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "^3.974.8", - "@aws-sdk/middleware-host-header": "^3.972.10", - "@aws-sdk/middleware-logger": "^3.972.10", - "@aws-sdk/middleware-recursion-detection": "^3.972.11", - "@aws-sdk/middleware-user-agent": "^3.972.38", - "@aws-sdk/region-config-resolver": "^3.972.13", - "@aws-sdk/signature-v4-multi-region": "^3.996.25", + "@aws-sdk/core": "^3.974.11", + "@aws-sdk/signature-v4-multi-region": "^3.996.27", "@aws-sdk/types": "^3.973.8", - "@aws-sdk/util-endpoints": "^3.996.8", - "@aws-sdk/util-user-agent-browser": "^3.972.10", - "@aws-sdk/util-user-agent-node": "^3.973.24", - "@smithy/config-resolver": "^4.4.17", - "@smithy/core": "^3.23.17", - "@smithy/fetch-http-handler": "^5.3.17", - "@smithy/hash-node": "^4.2.14", - "@smithy/invalid-dependency": "^4.2.14", - "@smithy/middleware-content-length": "^4.2.14", - "@smithy/middleware-endpoint": "^4.4.32", - "@smithy/middleware-retry": "^4.5.7", - "@smithy/middleware-serde": "^4.2.20", - "@smithy/middleware-stack": "^4.2.14", - "@smithy/node-config-provider": "^4.3.14", - "@smithy/node-http-handler": "^4.6.1", - "@smithy/protocol-http": "^5.3.14", - "@smithy/smithy-client": "^4.12.13", - "@smithy/types": "^4.14.1", - "@smithy/url-parser": "^4.2.14", - "@smithy/util-base64": "^4.3.2", - "@smithy/util-body-length-browser": "^4.2.2", - "@smithy/util-body-length-node": "^4.2.3", - "@smithy/util-defaults-mode-browser": "^4.3.49", - "@smithy/util-defaults-mode-node": "^4.2.54", - "@smithy/util-endpoints": "^3.4.2", - "@smithy/util-middleware": "^4.2.14", - "@smithy/util-retry": "^4.3.6", - "@smithy/util-utf8": "^4.2.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws-sdk/region-config-resolver": { - "version": "3.972.13", - "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.972.13.tgz", - "integrity": "sha512-CvJ2ZIjK/jVD/lbOpowBVElJyC1YxLTIJ13yM0AEo0t2v7swOzGjSA6lJGH+DwZXQhcjUjoYwc8bVYCX5MDr1A==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "^3.973.8", - "@smithy/config-resolver": "^4.4.17", - "@smithy/node-config-provider": "^4.3.14", + "@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" }, @@ -706,15 +413,14 @@ } }, "node_modules/@aws-sdk/signature-v4-multi-region": { - "version": "3.996.25", - "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.996.25.tgz", - "integrity": "sha512-+CMIt3e1VzlklAECmG+DtP1sV8iKq25FuA0OKpnJ4KA0kxUtd7CgClY7/RU6VzJBQwbN4EJ9Ue6plvqx1qGadw==", + "version": "3.996.27", + "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.996.27.tgz", + "integrity": "sha512-0Phbz4t6HI3D3skxvG2uI+VWU034/nSIw1T8d+FPzzQG9EQTrw94o9mOKO2Gv3n3Oc8P7JD7RAUxkoneLWv5Eg==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/middleware-sdk-s3": "^3.972.37", "@aws-sdk/types": "^3.973.8", - "@smithy/protocol-http": "^5.3.14", - "@smithy/signature-v4": "^5.3.14", + "@smithy/core": "^3.24.2", + "@smithy/signature-v4": "^5.4.2", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, @@ -723,16 +429,15 @@ } }, "node_modules/@aws-sdk/token-providers": { - "version": "3.1044.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1044.0.tgz", - "integrity": "sha512-uvpWTfpzOM9qVrR0kdAjzBbvv2ERW7S1CKeBeRkHyHy21Ext5fNReIVrbAzhjuVC7UxRyZQ8PHYW/3T83hWlbg==", + "version": "3.1048.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1048.0.tgz", + "integrity": "sha512-k0y/GcuesuSfWyUM0WamrGyeZmltRYaPbHO82UDA6mZ/doB+FOHKutikPAtSXMn/hDz970cF+iRuuiYO9VEbAA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.974.8", - "@aws-sdk/nested-clients": "^3.997.6", + "@aws-sdk/core": "^3.974.11", + "@aws-sdk/nested-clients": "^3.997.9", "@aws-sdk/types": "^3.973.8", - "@smithy/property-provider": "^4.2.14", - "@smithy/shared-ini-file-loader": "^4.4.9", + "@smithy/core": "^3.24.2", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, @@ -753,49 +458,6 @@ "node": ">=20.0.0" } }, - "node_modules/@aws-sdk/util-arn-parser": { - "version": "3.972.3", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-arn-parser/-/util-arn-parser-3.972.3.tgz", - "integrity": "sha512-HzSD8PMFrvgi2Kserxuff5VitNq2sgf3w9qxmskKDiDTThWfVteJxuCS9JXiPIPtmCrp+7N9asfIaVhBFORllA==", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws-sdk/util-endpoints": { - "version": "3.996.8", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.996.8.tgz", - "integrity": "sha512-oOZHcRDihk5iEe5V25NVWg45b3qEA8OpHWVdU/XQh8Zj4heVPAJqWvMphQnU7LkufmUo10EpvFPZuQMiFLJK3g==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "^3.973.8", - "@smithy/types": "^4.14.1", - "@smithy/url-parser": "^4.2.14", - "@smithy/util-endpoints": "^3.4.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws-sdk/util-format-url": { - "version": "3.972.10", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-format-url/-/util-format-url-3.972.10.tgz", - "integrity": "sha512-DEKiHNJVtNxdyTeQspzY+15Po/kHm6sF0Cs4HV9Q2+lplB63+DrvdeiSoOSdWEWAoO2RcY1veoXVDz2tWxWCgQ==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "^3.973.8", - "@smithy/querystring-builder": "^4.2.14", - "@smithy/types": "^4.14.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, "node_modules/@aws-sdk/util-locate-window": { "version": "3.965.5", "resolved": "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.965.5.tgz", @@ -808,52 +470,15 @@ "node": ">=20.0.0" } }, - "node_modules/@aws-sdk/util-user-agent-browser": { - "version": "3.972.10", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.972.10.tgz", - "integrity": "sha512-FAzqXvfEssGdSIz8ejatan0bOdx1qefBWKF/gWmVBXIP1HkS7v/wjjaqrAGGKvyihrXTXW00/2/1nTJtxpXz7g==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "^3.973.8", - "@smithy/types": "^4.14.1", - "bowser": "^2.11.0", - "tslib": "^2.6.2" - } - }, - "node_modules/@aws-sdk/util-user-agent-node": { - "version": "3.973.24", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.973.24.tgz", - "integrity": "sha512-ZWwlkjcIp7cEL8ZfTpTAPNkwx25p7xol0xlKoWVVf22+nsjwmLcHYtTPjIV1cSpmB/b6DaK4cb1fSkvCXHgRdw==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/middleware-user-agent": "^3.972.38", - "@aws-sdk/types": "^3.973.8", - "@smithy/node-config-provider": "^4.3.14", - "@smithy/types": "^4.14.1", - "@smithy/util-config-provider": "^4.2.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - }, - "peerDependencies": { - "aws-crt": ">=1.0.0" - }, - "peerDependenciesMeta": { - "aws-crt": { - "optional": true - } - } - }, "node_modules/@aws-sdk/xml-builder": { - "version": "3.972.22", - "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.22.tgz", - "integrity": "sha512-PMYKKtJd70IsSG0yHrdAbxBr+ZWBKLvzFZfD3/urxgf6hXVMzuU5M+3MJ5G67RpOmLBu1fAUN65SbWuKUCOlAA==", + "version": "3.972.24", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.24.tgz", + "integrity": "sha512-V8z5YcDPfsvzrBlj0xR1vhRtocblhYbqdreCJB/voGd4Sr5zjNAeWxexbnqVtskTJe0vFb5KMqbSL++ePl+zRw==", "license": "Apache-2.0", "dependencies": { "@nodable/entities": "2.1.0", "@smithy/types": "^4.14.1", - "fast-xml-parser": "5.7.2", + "fast-xml-parser": "5.7.3", "tslib": "^2.6.2" }, "engines": { @@ -1122,9 +747,9 @@ "link": true }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", - "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.0.tgz", + "integrity": "sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA==", "cpu": [ "ppc64" ], @@ -1139,9 +764,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", - "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.0.tgz", + "integrity": "sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ==", "cpu": [ "arm" ], @@ -1156,9 +781,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", - "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.0.tgz", + "integrity": "sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw==", "cpu": [ "arm64" ], @@ -1173,9 +798,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", - "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.0.tgz", + "integrity": "sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA==", "cpu": [ "x64" ], @@ -1190,9 +815,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", - "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.0.tgz", + "integrity": "sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q==", "cpu": [ "arm64" ], @@ -1207,9 +832,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", - "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.0.tgz", + "integrity": "sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ==", "cpu": [ "x64" ], @@ -1224,9 +849,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", - "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.0.tgz", + "integrity": "sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q==", "cpu": [ "arm64" ], @@ -1241,9 +866,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", - "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.0.tgz", + "integrity": "sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw==", "cpu": [ "x64" ], @@ -1258,9 +883,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", - "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.0.tgz", + "integrity": "sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw==", "cpu": [ "arm" ], @@ -1275,9 +900,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", - "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.0.tgz", + "integrity": "sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A==", "cpu": [ "arm64" ], @@ -1292,9 +917,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", - "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.0.tgz", + "integrity": "sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ==", "cpu": [ "ia32" ], @@ -1309,9 +934,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", - "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.0.tgz", + "integrity": "sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg==", "cpu": [ "loong64" ], @@ -1326,9 +951,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", - "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.0.tgz", + "integrity": "sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w==", "cpu": [ "mips64el" ], @@ -1343,9 +968,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", - "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.0.tgz", + "integrity": "sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg==", "cpu": [ "ppc64" ], @@ -1360,9 +985,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", - "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.0.tgz", + "integrity": "sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ==", "cpu": [ "riscv64" ], @@ -1377,9 +1002,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", - "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.0.tgz", + "integrity": "sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q==", "cpu": [ "s390x" ], @@ -1394,9 +1019,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", - "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.0.tgz", + "integrity": "sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ==", "cpu": [ "x64" ], @@ -1411,9 +1036,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", - "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.0.tgz", + "integrity": "sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw==", "cpu": [ "arm64" ], @@ -1428,9 +1053,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", - "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.0.tgz", + "integrity": "sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw==", "cpu": [ "x64" ], @@ -1445,9 +1070,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", - "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.0.tgz", + "integrity": "sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g==", "cpu": [ "arm64" ], @@ -1462,9 +1087,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", - "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.0.tgz", + "integrity": "sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA==", "cpu": [ "x64" ], @@ -1479,9 +1104,9 @@ } }, "node_modules/@esbuild/openharmony-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", - "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.0.tgz", + "integrity": "sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w==", "cpu": [ "arm64" ], @@ -1496,9 +1121,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", - "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.0.tgz", + "integrity": "sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw==", "cpu": [ "x64" ], @@ -1513,9 +1138,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", - "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.0.tgz", + "integrity": "sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA==", "cpu": [ "arm64" ], @@ -1530,9 +1155,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", - "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.0.tgz", + "integrity": "sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA==", "cpu": [ "ia32" ], @@ -1547,9 +1172,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", - "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.0.tgz", + "integrity": "sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw==", "cpu": [ "x64" ], @@ -1605,6 +1230,19 @@ "node": ">=12" } }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, "node_modules/@isaacs/cliui/node_modules/ansi-styles": { "version": "6.2.3", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", @@ -1643,6 +1281,22 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", @@ -1722,9 +1376,9 @@ } }, "node_modules/@lit-labs/ssr-dom-shim": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/@lit-labs/ssr-dom-shim/-/ssr-dom-shim-1.5.1.tgz", - "integrity": "sha512-Aou5UdlSpr5whQe8AA/bZG0jMj96CoJIWbGfZ91qieWu5AWUMKw8VR/pAkQkJYvBNhmCcWnZlyyk5oze8JIqYA==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@lit-labs/ssr-dom-shim/-/ssr-dom-shim-1.6.0.tgz", + "integrity": "sha512-VHb0ALPMTlgKjM6yIxxoQNnpKyUKLD04VzeQdsiXkMqkvYlAHxq9glGLmgbb889/1GsohSOAjvQYoiBppXFqrQ==", "license": "BSD-3-Clause" }, "node_modules/@lit/reactive-element": { @@ -1786,15 +1440,6 @@ "node": ">=8" } }, - "node_modules/@lmstudio/sdk/node_modules/zod": { - "version": "3.25.76", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", - "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/colinhacks" - } - }, "node_modules/@mariozechner/clipboard": { "version": "0.3.6", "resolved": "https://registry.npmjs.org/@mariozechner/clipboard/-/clipboard-0.3.6.tgz", @@ -2664,13 +2309,12 @@ "version": "1.0.10", "resolved": "https://registry.npmjs.org/@pondwader/socks5-server/-/socks5-server-1.0.10.tgz", "integrity": "sha512-bQY06wzzR8D2+vVCUoBsr5QS2U6UgPUQRmErNwtsuI6vLcyRKkafjkr3KxbtGFf9aBBIV2mcvlsKD1UYaIV+sg==", - "dev": true, "license": "MIT" }, "node_modules/@preact/signals-core": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@preact/signals-core/-/signals-core-1.14.1.tgz", - "integrity": "sha512-vxPpfXqrwUe9lpjqfYNjAF/0RF/eFGeLgdJzdmIIZjpOnTmGmAB4BjWone562mJGMRP4frU6iZ6ei3PDsu52Ng==", + "version": "1.14.2", + "resolved": "https://registry.npmjs.org/@preact/signals-core/-/signals-core-1.14.2.tgz", + "integrity": "sha512-RZHdBj9ZF4n40Rp4jS052EHHjBWf96P9oNdXPfhQTovCuWY9iQn3Gq+gOTJSgBO9A/JBuPfMOWsSX/lIU9Pc/A==", "license": "MIT", "funding": { "type": "opencollective", @@ -2742,9 +2386,9 @@ "license": "BSD-3-Clause" }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.60.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.3.tgz", - "integrity": "sha512-x35CNW/ANXG3hE/EZpRU8MXX1JDN86hBb2wMGAtltkz7pc6cxgjpy1OMMfDosOQ+2hWqIkag/fGok1Yady9nGw==", + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.4.tgz", + "integrity": "sha512-F5QXMSiFebS9hKZj02XhWLLnRpJ3B3AROP0tWbFBSj+6kCbg5m9j5JoHKd4mmSVy5mS/IMQloYgYxCuJC0fxEQ==", "cpu": [ "arm" ], @@ -2756,9 +2400,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.60.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.3.tgz", - "integrity": "sha512-xw3xtkDApIOGayehp2+Rz4zimfkaX65r4t47iy+ymQB2G4iJCBBfj0ogVg5jpvjpn8UWn/+q9tprxleYeNp3Hw==", + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.4.tgz", + "integrity": "sha512-GxxTKApUpzRhof7poWvCJHRF51C67u1R7D6DiluBE8wKU1u5GWE8t+v81JvJYtbawoBFX1hLv5Ei4eVjkWokaw==", "cpu": [ "arm64" ], @@ -2770,9 +2414,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.60.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.3.tgz", - "integrity": "sha512-vo6Y5Qfpx7/5EaamIwi0WqW2+zfiusVihKatLvtN1VFVy3D13uERk/6gZLU1UiHRL6fDXqj/ELIeVRGnvcTE1g==", + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.4.tgz", + "integrity": "sha512-tua0TaJxMOB1R0V0RS1jFZ/RpURFDJIOR2A6jWwQeawuFyS4gBW+rntLRaQd0EQ4bd6Vp44Z2rXW+YYDBsj6IA==", "cpu": [ "arm64" ], @@ -2784,9 +2428,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.60.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.3.tgz", - "integrity": "sha512-D+0QGcZhBzTN82weOnsSlY7V7+RMmPuF1CkbxyMAGE8+ZHeUjyb76ZiWmBlCu//AQQONvxcqRbwZTajZKqjuOw==", + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.4.tgz", + "integrity": "sha512-CSKq7MsP+5PFIcydhAiR1K0UhEI1A2jWXVKHPCBZ151yOutENwvnPocgVHkivu2kviURtCEB6zUQw0vs8RrhMg==", "cpu": [ "x64" ], @@ -2798,9 +2442,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.60.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.3.tgz", - "integrity": "sha512-6HnvHCT7fDyj6R0Ph7A6x8dQS/S38MClRWeDLqc0MdfWkxjiu1HSDYrdPhqSILzjTIC/pnXbbJbo+ft+gy/9hQ==", + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.4.tgz", + "integrity": "sha512-+O8OkVdyvXMtJEciu2wS/pzm1IxntEEQx3z5TAVy4l32G0etZn+RsA48ARRrFm6Ri8fvqPQfgrvNxSjKAbnd3g==", "cpu": [ "arm64" ], @@ -2812,9 +2456,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.60.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.3.tgz", - "integrity": "sha512-KHLgC3WKlUYW3ShFKnnosZDOJ0xjg9zp7au3sIm2bs/tGBeC2ipmvRh/N7JKi0t9Ue20C0dpEshi8WUubg+cnA==", + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.4.tgz", + "integrity": "sha512-Iw3oMskH3AfNuhU0MSN7vNbdi4me/NiYo2azqPz/Le16zHSa+3RRmliCMWWQmh4lcndccU40xcJuTYJZxNo/lw==", "cpu": [ "x64" ], @@ -2826,9 +2470,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.60.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.3.tgz", - "integrity": "sha512-DV6fJoxEYWJOvaZIsok7KrYl0tPvga5OZ2yvKHNNYyk/2roMLqQAbGhr78EQ5YhHpnhLKJD3S1WFusAkmUuV5g==", + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.4.tgz", + "integrity": "sha512-EIPRXTVQpHyF8WOo219AD2yEltPehLTcTMz2fn6JsatLYSzQf00hj3rulF+yauOlF9/FtM2WpkT/hJh/KJFGhA==", "cpu": [ "arm" ], @@ -2840,9 +2484,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.60.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.3.tgz", - "integrity": "sha512-mQKoJAzvuOs6F+TZybQO4GOTSMUu7v0WdxEk24krQ/uUxXoPTtHjuaUuPmFhtBcM4K0ons8nrE3JyhTuCFtT/w==", + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.4.tgz", + "integrity": "sha512-J3Yh9PzzF1Ovah2At+lHiGQdsYgArxBbXv/zHfSyaiFQEqvNv7DcW98pCrmdjCZBrqBiKrKKe2V+aaSGWuBe/w==", "cpu": [ "arm" ], @@ -2854,9 +2498,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.60.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.3.tgz", - "integrity": "sha512-Whjj2qoiJ6+OOJMGptTYazaJvjOJm+iKHpXQM1P3LzGjt7Ff++Tp7nH4N8J/BUA7R9IHfDyx4DJIflifwnbmIA==", + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.4.tgz", + "integrity": "sha512-BFDEZMYfUvLn37ONE1yMBojPxnMlTFsdyNoqncT0qFq1mAfllL+ATMMJd8TeuVMiX84s1KbcxcZbXInmcO2mRg==", "cpu": [ "arm64" ], @@ -2868,9 +2512,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.60.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.3.tgz", - "integrity": "sha512-4YTNHKqGng5+yiZt3mg77nmyuCfmNfX4fPmyUapBcIk+BdwSwmCWGXOUxhXbBEkFHtoN5boLj/5NON+u5QC9tg==", + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.4.tgz", + "integrity": "sha512-pc9EYOSlOgdQ2uPl1o9PF6/kLSgaUosia7gOuS8mB69IxJvlclko1MECXysjs5ryez1/5zjYqx3+xYU0TU6R1A==", "cpu": [ "arm64" ], @@ -2882,9 +2526,9 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.60.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.3.tgz", - "integrity": "sha512-SU3kNlhkpI4UqlUc2VXPGK9o886ZsSeGfMAX2ba2b8DKmMXq4AL7KUrkSWVbb7koVqx41Yczx6dx5PNargIrEA==", + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.4.tgz", + "integrity": "sha512-NxnomyxYerDh5n4iLrNa+sH+Z+U4BMEE46V2PgQ/hoB909i8gV1M5wPojWg9fk1jWpO3IQnOs20K4wyZuFLEFQ==", "cpu": [ "loong64" ], @@ -2896,9 +2540,9 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-musl": { - "version": "4.60.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.3.tgz", - "integrity": "sha512-6lDLl5h4TXpB1mTf2rQWnAk/LcXrx9vBfu/DT5TIPhvMhRWaZ5MxkIc8u4lJAmBo6klTe1ywXIUHFjylW505sg==", + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.4.tgz", + "integrity": "sha512-nbJnQ8a3z1mtmrwImCYhc6BGpThAyYVRQxw9uKSKG4wR6aAYno9sVjJ0zaZcW9BPJX1GbrDPf+SvdWjgTuDmnw==", "cpu": [ "loong64" ], @@ -2910,9 +2554,9 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.60.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.3.tgz", - "integrity": "sha512-BMo8bOw8evlup/8G+cj5xWtPyp93xPdyoSN16Zy90Q2QZ0ZYRhCt6ZJSwbrRzG9HApFabjwj2p25TUPDWrhzqQ==", + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.4.tgz", + "integrity": "sha512-2EU6acNrQLd8tYvo/LXW535wupT3m6fo7HKo6lr7ktQoItxTyOL1ZCR/GfGCuXl2vR+zmfI6eRXkSemafv+iVg==", "cpu": [ "ppc64" ], @@ -2924,9 +2568,9 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-musl": { - "version": "4.60.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.3.tgz", - "integrity": "sha512-E0L8X1dZN1/Rph+5VPF6Xj2G7JJvMACVXtamTJIDrVI44Y3K+G8gQaMEAavbqCGTa16InptiVrX6eM6pmJ+7qA==", + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.4.tgz", + "integrity": "sha512-WeBtoMuaMxiiIrO2IYP3xs6GMWkJP2C0EoT8beTLkUPmzV1i/UcOSVw1d5r9KBODtHKilG5yFxsGRnBbK3wJ4A==", "cpu": [ "ppc64" ], @@ -2938,9 +2582,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.60.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.3.tgz", - "integrity": "sha512-oZJ/WHaVfHUiRAtmTAeo3DcevNsVvH8mbvodjZy7D5QKvCefO371SiKRpxoDcCxB3PTRTLayWBkvmDQKTcX/sw==", + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.4.tgz", + "integrity": "sha512-FJHFfqpKUI3A10WrWKiFbBZ7yVbGT4q4B5o1qKFFojqpaYoh9LrQgqWCmmcxQzVSXYtyB5bzkXrYzlHTs21MYA==", "cpu": [ "riscv64" ], @@ -2952,9 +2596,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.60.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.3.tgz", - "integrity": "sha512-Dhbyh7j9FybM3YaTgaHmVALwA8AkUwTPccyCQ79TG9AJUsMQqgN1DDEZNr4+QUfwiWvLDumW5vdwzoeUF+TNxQ==", + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.4.tgz", + "integrity": "sha512-mcEl6CUT5IAUmQf1m9FYSmVqCJlpQ8r8eyftFUHG8i9OhY7BkBXSUdnLH5DOf0wCOjcP9v/QO93zpmF1SptCCw==", "cpu": [ "riscv64" ], @@ -2966,9 +2610,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.60.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.3.tgz", - "integrity": "sha512-cJd1X5XhHHlltkaypz1UcWLA8AcoIi1aWhsvaWDskD1oz2eKCypnqvTQ8ykMNI0RSmm7NkTdSqSSD7zM0xa6Ig==", + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.4.tgz", + "integrity": "sha512-ynt3JxVd2w2buzoKDWIyiV1pJW93xlQic1THVLXilz429oijRpSHivZAgp65KBu+cMcgf1eVVjdnTLvPxgCuoQ==", "cpu": [ "s390x" ], @@ -2980,9 +2624,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.60.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.3.tgz", - "integrity": "sha512-DAZDBHQfG2oQuhY7mc6I3/qB4LU2fQCjRvxbDwd/Jdvb9fypP4IJ4qmtu6lNjes6B531AI8cg1aKC2di97bUxA==", + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.4.tgz", + "integrity": "sha512-Boiz5+MsaROEWDf+GGEwF8VMHGhlUoQMtIPjOgA5fv4osupqTVnJteQNKJwUcnUog2G55jYXH7KZFFiJe0TEzQ==", "cpu": [ "x64" ], @@ -2994,9 +2638,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.60.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.3.tgz", - "integrity": "sha512-cRxsE8c13mZOh3vP+wLDxpQBRrOHDIGOWyDL93Sy0Ga8y515fBcC2pjUfFwUe5T7tqvTvWbCpg1URM/AXdWIXA==", + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.4.tgz", + "integrity": "sha512-+qfSY27qIrFfI/Hom04KYFw3GKZSGU4lXus51wsb5EuySfFlWRwjkKWoE9emgRw/ukoT4Udsj4W/+xxG8VbPKg==", "cpu": [ "x64" ], @@ -3008,9 +2652,9 @@ ] }, "node_modules/@rollup/rollup-openbsd-x64": { - "version": "4.60.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.3.tgz", - "integrity": "sha512-QaWcIgRxqEdQdhJqW4DJctsH6HCmo5vHxY0krHSX4jMtOqfzC+dqDGuHM87bu4H8JBeibWx7jFz+h6/4C8wA5Q==", + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.4.tgz", + "integrity": "sha512-VpTfOPHgVXEBeeR8hZ2O0F3aSso+JDWqTWmTmzcQKted54IAdUVbxE+j/MVxUsKa8L20HJhv3vUezVPoquqWjA==", "cpu": [ "x64" ], @@ -3022,9 +2666,9 @@ ] }, "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.60.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.3.tgz", - "integrity": "sha512-AaXwSvUi3QIPtroAUw1t5yHGIyqKEXwH54WUocFolZhpGDruJcs8c+xPNDRn4XiQsS7MEwnYsHW2l0MBLDMkWg==", + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.4.tgz", + "integrity": "sha512-IPOsh5aRYuLv/nkU51X10Bf75Bsf6+gZdx1X+QP5QM6lIJFHHqbHLG0uJn/hWthzo13UAc2umiUorqZy3axoZg==", "cpu": [ "arm64" ], @@ -3036,9 +2680,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.60.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.3.tgz", - "integrity": "sha512-65LAKM/bAWDqKNEelHlcHvm2V+Vfb8C6INFxQXRHCvaVN1rJfwr4NvdP4FyzUaLqWfaCGaadf6UbTm8xJeYfEg==", + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.4.tgz", + "integrity": "sha512-4QzE9E81OohJ/HKzHhsqU+zcYYojVOXlFMs1DdyMT6qXl/niOH7AVElmmEdUNHHS/oRkc++d5k6Vy85zFs0DEw==", "cpu": [ "arm64" ], @@ -3050,9 +2694,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.60.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.3.tgz", - "integrity": "sha512-EEM2gyhBF5MFnI6vMKdX1LAosE627RGBzIoGMdLloPZkXrUN0Ckqgr2Qi8+J3zip/8NVVro3/FjB+tjhZUgUHA==", + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.4.tgz", + "integrity": "sha512-zTPgT1YuHHcd+Tmx7h8aml0FWFVelV5N54oHow9SLj+GfoDy/huQ+UV396N/C7KpMDMiPspRktzM1/0r1usYEA==", "cpu": [ "ia32" ], @@ -3064,9 +2708,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.60.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.3.tgz", - "integrity": "sha512-E5Eb5H/DpxaoXH++Qkv28RcUJboMopmdDUALBczvHMf7hNIxaDZqwY5lK12UK1BHacSmvupoEWGu+n993Z0y1A==", + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.4.tgz", + "integrity": "sha512-DRS4G7mi9lJxqEDezIkKCaUIKCrLUUDCUaCsTPCi/rtqaC6D/jjwslMQyiDU50Ka0JKpeXeRBFBAXwArY52vBw==", "cpu": [ "x64" ], @@ -3078,9 +2722,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.60.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.3.tgz", - "integrity": "sha512-hPt/bgL5cE+Qp+/TPHBqptcAgPzgj46mPcg/16zNUmbQk0j+mOEQV/+Lqu8QRtDV3Ek95Q6FeFITpuhl6OTsAA==", + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.4.tgz", + "integrity": "sha512-QVTUovf40zgTqlFVrKA1uXMVvU2QWEFWfAH8Wdc48IxLvrJMQVMBRjuQyUpzZCDkakImib9eVazbWlC6ksWtJw==", "cpu": [ "x64" ], @@ -3097,38 +2741,14 @@ "integrity": "sha512-bnly4BKB3KDTFxrUIcgCLbaeVVS8lrAkri1pEzskpmxu9MdfGQTy8b8EgcD83ywD3RPMsIulY8xJH5Awa+t9fA==", "license": "Apache-2.0" }, - "node_modules/@smithy/config-resolver": { - "version": "4.4.17", - "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-4.4.17.tgz", - "integrity": "sha512-TzDZcAnhTyAHbXVxWZo7/tEcrIeFq20IBk8So3OLOetWpR8EwY/yEqBMBFaJMeyEiREDq4NfEl+qO3OAUD+vbQ==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/node-config-provider": "^4.3.14", - "@smithy/types": "^4.14.1", - "@smithy/util-config-provider": "^4.2.2", - "@smithy/util-endpoints": "^3.4.2", - "@smithy/util-middleware": "^4.2.14", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, "node_modules/@smithy/core": { - "version": "3.23.17", - "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.23.17.tgz", - "integrity": "sha512-x7BlLbUFL8NWCGjMF9C+1N5cVCxcPa7g6Tv9B4A2luWx3be3oU8hQ96wIwxe/s7OhIzvoJH73HAUSg5JXVlEtQ==", + "version": "3.24.3", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.24.3.tgz", + "integrity": "sha512-Ep/7tPamGY8mgESE3LyLKtxJyy6U52WWAqr/3wial47Sj4u3PiIF73AOGI27UyLy9duTkhZbgzodOfLV4TduZg==", "license": "Apache-2.0", "dependencies": { - "@smithy/protocol-http": "^5.3.14", - "@smithy/types": "^4.14.1", - "@smithy/url-parser": "^4.2.14", - "@smithy/util-base64": "^4.3.2", - "@smithy/util-body-length-browser": "^4.2.2", - "@smithy/util-middleware": "^4.2.14", - "@smithy/util-stream": "^4.5.25", - "@smithy/util-utf8": "^4.2.2", - "@smithy/uuid": "^1.1.2", + "@aws-crypto/crc32": "5.2.0", + "@smithy/types": "^4.14.2", "tslib": "^2.6.2" }, "engines": { @@ -3136,85 +2756,13 @@ } }, "node_modules/@smithy/credential-provider-imds": { - "version": "4.2.14", - "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.2.14.tgz", - "integrity": "sha512-Au28zBN48ZAoXdooGUHemuVBrkE+Ie6RPmGNIAJsFqj33Vhb6xAgRifUydZ2aY+M+KaMAETAlKk5NC5h1G7wpg==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.3.3.tgz", + "integrity": "sha512-I2Bti0DKFo2IJyN28ijCsx51BAumEYR4/1yZ1FXyBygy9MqbnMqCev4JPth/MbpRfBSRAX35hITSnAdJRo1u5w==", "license": "Apache-2.0", "dependencies": { - "@smithy/node-config-provider": "^4.3.14", - "@smithy/property-provider": "^4.2.14", - "@smithy/types": "^4.14.1", - "@smithy/url-parser": "^4.2.14", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/eventstream-codec": { - "version": "4.2.14", - "resolved": "https://registry.npmjs.org/@smithy/eventstream-codec/-/eventstream-codec-4.2.14.tgz", - "integrity": "sha512-erZq0nOIpzfeZdCyzZjdJb4nVSKLUmSkaQUVkRGQTXs30gyUGeKnrYEg+Xe1W5gE3aReS7IgsvANwVPxSzY6Pw==", - "license": "Apache-2.0", - "dependencies": { - "@aws-crypto/crc32": "5.2.0", - "@smithy/types": "^4.14.1", - "@smithy/util-hex-encoding": "^4.2.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/eventstream-serde-browser": { - "version": "4.2.14", - "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-browser/-/eventstream-serde-browser-4.2.14.tgz", - "integrity": "sha512-8IelTCtTctWRbb+0Dcy+C0aICh1qa0qWXqgjcXDmMuCvPJRnv26hiDZoAau2ILOniki65mCPKqOQs/BaWvO4CQ==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/eventstream-serde-universal": "^4.2.14", - "@smithy/types": "^4.14.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/eventstream-serde-config-resolver": { - "version": "4.3.14", - "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-config-resolver/-/eventstream-serde-config-resolver-4.3.14.tgz", - "integrity": "sha512-sqHiHpYRYo3FJlaIxD1J8PhbcmJAm7IuM16mVnwSkCToD7g00IBZzKuiLNMGmftULmEUX6/UAz8/NN5uMP8bVA==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^4.14.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/eventstream-serde-node": { - "version": "4.2.14", - "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-node/-/eventstream-serde-node-4.2.14.tgz", - "integrity": "sha512-Ht/8BuGlKfFTy0H3+8eEu0vdpwGztCnaLLXtpXNdQqiR7Hj4vFScU3T436vRAjATglOIPjJXronY+1WxxNLSiw==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/eventstream-serde-universal": "^4.2.14", - "@smithy/types": "^4.14.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/eventstream-serde-universal": { - "version": "4.2.14", - "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-universal/-/eventstream-serde-universal-4.2.14.tgz", - "integrity": "sha512-lWyt4T2XQZUZgK3tQ3Wn0w3XBvZsK/vjTuJl6bXbnGZBHH0ZUSONTYiK9TgjTTzU54xQr3DRFwpjmhp0oLm3gg==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/eventstream-codec": "^4.2.14", - "@smithy/types": "^4.14.1", + "@smithy/core": "^3.24.3", + "@smithy/types": "^4.14.2", "tslib": "^2.6.2" }, "engines": { @@ -3222,43 +2770,13 @@ } }, "node_modules/@smithy/fetch-http-handler": { - "version": "5.3.17", - "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.3.17.tgz", - "integrity": "sha512-bXOvQzaSm6MnmLaWA1elgfQcAtN4UP3vXqV97bHuoOrHQOJiLT3ds6o9eo5bqd0TJfRFpzdGnDQdW3FACiAVdw==", + "version": "5.4.3", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.4.3.tgz", + "integrity": "sha512-F+DRf8IJazRJgYog2A/yJK7eYVc0rqTlRzO+5ZxjJd4WkZoKz0IJRncf7G6t1pdVT3kryJcwuTFhN1c5m6N47A==", "license": "Apache-2.0", "dependencies": { - "@smithy/protocol-http": "^5.3.14", - "@smithy/querystring-builder": "^4.2.14", - "@smithy/types": "^4.14.1", - "@smithy/util-base64": "^4.3.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/hash-node": { - "version": "4.2.14", - "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-4.2.14.tgz", - "integrity": "sha512-8ZBDY2DD4wr+GGjTpPtiglEsqr0lUP+KHqgZcWczFf6qeZ/YRjMIOoQWVQlmwu7EtxKTd8YXD8lblmYcpBIA1g==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^4.14.1", - "@smithy/util-buffer-from": "^4.2.2", - "@smithy/util-utf8": "^4.2.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/invalid-dependency": { - "version": "4.2.14", - "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-4.2.14.tgz", - "integrity": "sha512-c21qJiTSb25xvvOp+H2TNZzPCngrvl5vIPqPB8zQ/DmJF4QWXO19x1dWfMJZ6wZuuWUPPm0gV8C0cU3+ifcWuw==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^4.14.1", + "@smithy/core": "^3.24.3", + "@smithy/types": "^4.14.2", "tslib": "^2.6.2" }, "engines": { @@ -3266,201 +2784,25 @@ } }, "node_modules/@smithy/is-array-buffer": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-4.2.2.tgz", - "integrity": "sha512-n6rQ4N8Jj4YTQO3YFrlgZuwKodf4zUFs7EJIWH86pSCWBaAtAGBFfCM7Wx6D2bBJ2xqFNxGBSrUWswT3M0VJow==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/middleware-content-length": { - "version": "4.2.14", - "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-4.2.14.tgz", - "integrity": "sha512-xhHq7fX4/3lv5NHxLUk3OeEvl0xZ+Ek3qIbWaCL4f9JwgDZEclPBElljaZCAItdGPQl/kSM4LPMOpy1MYgprpw==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/protocol-http": "^5.3.14", - "@smithy/types": "^4.14.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/middleware-endpoint": { - "version": "4.4.32", - "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.4.32.tgz", - "integrity": "sha512-ZZkgyjnJppiZbIm6Qbx92pbXYi1uzenIvGhBSCDlc7NwuAkiqSgS75j1czAD25ZLs2FjMjYy1q7gyRVWG6JA0Q==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/core": "^3.23.17", - "@smithy/middleware-serde": "^4.2.20", - "@smithy/node-config-provider": "^4.3.14", - "@smithy/shared-ini-file-loader": "^4.4.9", - "@smithy/types": "^4.14.1", - "@smithy/url-parser": "^4.2.14", - "@smithy/util-middleware": "^4.2.14", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/middleware-retry": { - "version": "4.5.7", - "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.5.7.tgz", - "integrity": "sha512-bRt6ZImqVSeTk39Nm81K20ObIiAZ3WefY7G6+iz/0tZjs4dgRRjvRX2sgsH+zi6iDCRR/aQvQofLKxxz4rPBZg==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/core": "^3.23.17", - "@smithy/node-config-provider": "^4.3.14", - "@smithy/protocol-http": "^5.3.14", - "@smithy/service-error-classification": "^4.3.1", - "@smithy/smithy-client": "^4.12.13", - "@smithy/types": "^4.14.1", - "@smithy/util-middleware": "^4.2.14", - "@smithy/util-retry": "^4.3.6", - "@smithy/uuid": "^1.1.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/middleware-serde": { - "version": "4.2.20", - "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-4.2.20.tgz", - "integrity": "sha512-Lx9JMO9vArPtiChE3wbEZ5akMIDQpWQtlu90lhACQmNOXcGXRbaDywMHDzuDZ2OkZzP+9wQfZi3YJT9F67zTQQ==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/core": "^3.23.17", - "@smithy/protocol-http": "^5.3.14", - "@smithy/types": "^4.14.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/middleware-stack": { - "version": "4.2.14", - "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-4.2.14.tgz", - "integrity": "sha512-2dvkUKLuFdKsCRmOE4Mn63co0Djtsm+JMh0bYZQupN1pJwMeE8FmQmRLLzzEMN0dnNi7CDCYYH8F0EVwWiPBeA==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^4.14.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/node-config-provider": { - "version": "4.3.14", - "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-4.3.14.tgz", - "integrity": "sha512-S+gFjyo/weSVL0P1b9Ts8C/CwIfNCgUPikk3sl6QVsfE/uUuO+QsF+NsE/JkpvWqqyz1wg7HFdiaZuj5CoBMRg==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/property-provider": "^4.2.14", - "@smithy/shared-ini-file-loader": "^4.4.9", - "@smithy/types": "^4.14.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" + "node": ">=14.0.0" } }, "node_modules/@smithy/node-http-handler": { - "version": "4.6.1", - "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.6.1.tgz", - "integrity": "sha512-iB+orM4x3xrr57X3YaXazfKnntl0LHlZB1kcXSGzMV1Tt0+YwEjGlbjk/44qEGtBzXAz6yFDzkYTKSV6Pj2HUg==", + "version": "4.7.3", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.7.3.tgz", + "integrity": "sha512-/jPhevcTFPMVl6KNjbaI47iOg1zxC7IsnX4PQDGVZKMFceOXtB8IEYaB7a9VvkP/3oC60WzTeKocvSI7vLT0vA==", "license": "Apache-2.0", "dependencies": { - "@smithy/protocol-http": "^5.3.14", - "@smithy/querystring-builder": "^4.2.14", - "@smithy/types": "^4.14.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/property-provider": { - "version": "4.2.14", - "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-4.2.14.tgz", - "integrity": "sha512-WuM31CgfsnQ/10i7NYr0PyxqknD72Y5uMfUMVSniPjbEPceiTErb4eIqJQ+pdxNEAUEWrewrGjIRjVbVHsxZiQ==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^4.14.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/protocol-http": { - "version": "5.3.14", - "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.3.14.tgz", - "integrity": "sha512-dN5F8kHx8RNU0r+pCwNmFZyz6ChjMkzShy/zup6MtkRmmix4vZzJdW+di7x//b1LiynIev88FM18ie+wwPcQtQ==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^4.14.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/querystring-builder": { - "version": "4.2.14", - "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-4.2.14.tgz", - "integrity": "sha512-XYA5Z0IqTeF+5XDdh4BBmSA0HvbgVZIyv4cmOoUheDNR57K1HgBp9ukUMx3Cr3XpDHHpLBnexPE3LAtDsZkj2A==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^4.14.1", - "@smithy/util-uri-escape": "^4.2.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/querystring-parser": { - "version": "4.2.14", - "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-4.2.14.tgz", - "integrity": "sha512-hr+YyqBD23GVvRxGGrcc/oOeNlK3PzT5Fu4dzrDXxzS1LpFiuL2PQQqKPs87M79aW7ziMs+nvB3qdw77SqE7Lw==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^4.14.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/service-error-classification": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-4.3.1.tgz", - "integrity": "sha512-aUQuDGh760ts/8MU+APjIZhlLPKhIIfqyzZaJikLEIMrdxFvxuLYD0WxWzaYWpmLbQlXDe9p7EWM3HsBe0K6Gw==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^4.14.1" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/shared-ini-file-loader": { - "version": "4.4.9", - "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-4.4.9.tgz", - "integrity": "sha512-495/V2I15SHgedSJoDPD23JuSfKAp726ZI1V0wtjB07Wh7q/0tri/0e0DLefZCHgxZonrGKt/OCTpAtP1wE1kQ==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^4.14.1", + "@smithy/core": "^3.24.3", + "@smithy/types": "^4.14.2", "tslib": "^2.6.2" }, "engines": { @@ -3468,36 +2810,13 @@ } }, "node_modules/@smithy/signature-v4": { - "version": "5.3.14", - "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.3.14.tgz", - "integrity": "sha512-1D9Y/nmlVjCeSivCbhZ7hgEpmHyY1h0GvpSZt3l0xcD9JjmjVC1CHOozS6+Gh+/ldMH8JuJ6cujObQqfayAVFA==", + "version": "5.4.3", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.4.3.tgz", + "integrity": "sha512-53+75QuPl6DL+ct6vVEB51FDO5oulXr20TPV46VvJZg76lIlXNWfxi8j+G2V/t0I2qxCBOa3vX/8bmjrpFVo9g==", "license": "Apache-2.0", "dependencies": { - "@smithy/is-array-buffer": "^4.2.2", - "@smithy/protocol-http": "^5.3.14", - "@smithy/types": "^4.14.1", - "@smithy/util-hex-encoding": "^4.2.2", - "@smithy/util-middleware": "^4.2.14", - "@smithy/util-uri-escape": "^4.2.2", - "@smithy/util-utf8": "^4.2.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/smithy-client": { - "version": "4.12.13", - "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.12.13.tgz", - "integrity": "sha512-y/Pcj1V9+qG98gyu1gvftHB7rDpdh+7kIBIggs55yGm3JdtBV8GT8IFF3a1qxZ79QnaJHX9GXzvBG6tAd+czJA==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/core": "^3.23.17", - "@smithy/middleware-endpoint": "^4.4.32", - "@smithy/middleware-stack": "^4.2.14", - "@smithy/protocol-http": "^5.3.14", - "@smithy/types": "^4.14.1", - "@smithy/util-stream": "^4.5.25", + "@smithy/core": "^3.24.3", + "@smithy/types": "^4.14.2", "tslib": "^2.6.2" }, "engines": { @@ -3505,61 +2824,9 @@ } }, "node_modules/@smithy/types": { - "version": "4.14.1", - "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.14.1.tgz", - "integrity": "sha512-59b5HtSVrVR/eYNei3BUj3DCPKD/G7EtDDe7OEJE7i7FtQFugYo6MxbotS8mVJkLNVf8gYaAlEBwwtJ9HzhWSg==", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/url-parser": { - "version": "4.2.14", - "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-4.2.14.tgz", - "integrity": "sha512-p06BiBigJ8bTA3MgnOfCtDUWnAMY0YfedO/GRpmc7p+wg3KW8vbXy1xwSu5ASy0wV7rRYtlfZOIKH4XqfhjSQQ==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/querystring-parser": "^4.2.14", - "@smithy/types": "^4.14.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/util-base64": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/@smithy/util-base64/-/util-base64-4.3.2.tgz", - "integrity": "sha512-XRH6b0H/5A3SgblmMa5ErXQ2XKhfbQB+Fm/oyLZ2O2kCUrwgg55bU0RekmzAhuwOjA9qdN5VU2BprOvGGUkOOQ==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/util-buffer-from": "^4.2.2", - "@smithy/util-utf8": "^4.2.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/util-body-length-browser": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@smithy/util-body-length-browser/-/util-body-length-browser-4.2.2.tgz", - "integrity": "sha512-JKCrLNOup3OOgmzeaKQwi4ZCTWlYR5H4Gm1r2uTMVBXoemo1UEghk5vtMi1xSu2ymgKVGW631e2fp9/R610ZjQ==", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/util-body-length-node": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/@smithy/util-body-length-node/-/util-body-length-node-4.2.3.tgz", - "integrity": "sha512-ZkJGvqBzMHVHE7r/hcuCxlTY8pQr1kMtdsVPs7ex4mMU+EAbcXppfo5NmyxMYi2XU49eqaz56j2gsk4dHHPG/g==", + "version": "4.14.2", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.14.2.tgz", + "integrity": "sha512-P+otAxbV4CqBybp7EkcJCrig63yE2E7PuNVOmilVMRcx/O+QDzGULTrKsq4DV13gSfak9ObPrWaHl/9bL5YcWw==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -3569,235 +2836,94 @@ } }, "node_modules/@smithy/util-buffer-from": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-4.2.2.tgz", - "integrity": "sha512-FDXD7cvUoFWwN6vtQfEta540Y/YBe5JneK3SoZg9bThSoOAC/eGeYEua6RkBgKjGa/sz6Y+DuBZj3+YEY21y4Q==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", "license": "Apache-2.0", "dependencies": { - "@smithy/is-array-buffer": "^4.2.2", + "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/util-config-provider": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@smithy/util-config-provider/-/util-config-provider-4.2.2.tgz", - "integrity": "sha512-dWU03V3XUprJwaUIFVv4iOnS1FC9HnMHDfUrlNDSh4315v0cWyaIErP8KiqGVbf5z+JupoVpNM7ZB3jFiTejvQ==", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/util-defaults-mode-browser": { - "version": "4.3.49", - "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.3.49.tgz", - "integrity": "sha512-a5bNrdiONYB/qE2BuKegvUMd/+ZDwdg4vsNuuSzYE8qs2EYAdK9CynL+Rzn29PbPiUqoz/cbpRbcLzD5lEevHw==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/property-provider": "^4.2.14", - "@smithy/smithy-client": "^4.12.13", - "@smithy/types": "^4.14.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/util-defaults-mode-node": { - "version": "4.2.54", - "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.2.54.tgz", - "integrity": "sha512-g1cvrJvOnzeJgEdf7AE4luI7gp6L8weE0y9a9wQUSGtjb8QRHDbCJYuE4Sy0SD9N8RrnNPFsPltAz/OSoBR9Zw==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/config-resolver": "^4.4.17", - "@smithy/credential-provider-imds": "^4.2.14", - "@smithy/node-config-provider": "^4.3.14", - "@smithy/property-provider": "^4.2.14", - "@smithy/smithy-client": "^4.12.13", - "@smithy/types": "^4.14.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/util-endpoints": { - "version": "3.4.2", - "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-3.4.2.tgz", - "integrity": "sha512-a55Tr+3OKld4TTtnT+RhKOQHyPxm3j/xL4OR83WBUhLJaKDS9dnJ7arRMOp3t31dcLhApwG9bgvrRXBHlLdIkg==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/node-config-provider": "^4.3.14", - "@smithy/types": "^4.14.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/util-hex-encoding": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@smithy/util-hex-encoding/-/util-hex-encoding-4.2.2.tgz", - "integrity": "sha512-Qcz3W5vuHK4sLQdyT93k/rfrUwdJ8/HZ+nMUOyGdpeGA1Wxt65zYwi3oEl9kOM+RswvYq90fzkNDahPS8K0OIg==", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/util-middleware": { - "version": "4.2.14", - "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-4.2.14.tgz", - "integrity": "sha512-1Su2vj9RYNDEv/V+2E+jXkkwGsgR7dc4sfHn9Z7ruzQHJIEni9zzw5CauvRXlFJfmgcqYP8fWa0dkh2Q2YaQyw==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^4.14.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/util-retry": { - "version": "4.3.8", - "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-4.3.8.tgz", - "integrity": "sha512-LUIxbTBi+OpvXpg91poGA6BdyoleMDLnfXjVDqyi2RvZmTveY5loE/FgYUBCR5LU2BThW2SoZRh8dTIIy38IPw==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/service-error-classification": "^4.3.1", - "@smithy/types": "^4.14.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/util-stream": { - "version": "4.5.25", - "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.5.25.tgz", - "integrity": "sha512-/PFpG4k8Ze8Ei+mMKj3oiPICYekthuzePZMgZbCqMiXIHHf4n2aZ4Ps0aSRShycFTGuj/J6XldmC0x0DwednIA==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/fetch-http-handler": "^5.3.17", - "@smithy/node-http-handler": "^4.6.1", - "@smithy/types": "^4.14.1", - "@smithy/util-base64": "^4.3.2", - "@smithy/util-buffer-from": "^4.2.2", - "@smithy/util-hex-encoding": "^4.2.2", - "@smithy/util-utf8": "^4.2.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/util-uri-escape": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@smithy/util-uri-escape/-/util-uri-escape-4.2.2.tgz", - "integrity": "sha512-2kAStBlvq+lTXHyAZYfJRb/DfS3rsinLiwb+69SstC9Vb0s9vNWkRwpnj918Pfi85mzi42sOqdV72OLxWAISnw==", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" + "node": ">=14.0.0" } }, "node_modules/@smithy/util-utf8": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.2.2.tgz", - "integrity": "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", "license": "Apache-2.0", "dependencies": { - "@smithy/util-buffer-from": "^4.2.2", + "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/uuid": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@smithy/uuid/-/uuid-1.1.2.tgz", - "integrity": "sha512-O/IEdcCUKkubz60tFbGA7ceITTAJsty+lBjNoorP4Z6XRqaFb/OjQjZODophEcuq68nKm6/0r+6/lLQ+XVpk8g==", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" + "node": ">=14.0.0" } }, "node_modules/@tailwindcss/cli": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/@tailwindcss/cli/-/cli-4.2.4.tgz", - "integrity": "sha512-e87GGhuXxnyQPyA0TS8an/3wNpj+OUmx8u0F4BicYr48TF72032AIu5917rRYaWm7HorXi3GSZ/uG+ohqP6AKA==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/cli/-/cli-4.3.0.tgz", + "integrity": "sha512-X9kdlqyMopO9fewbgHsEeuy31YzMHbdZ9VsKt004tB+mxSg1CNbyhZYCzvhciN0AM4R4b5lvIprPjtNq7iQxpQ==", "dev": true, "license": "MIT", "dependencies": { "@parcel/watcher": "^2.5.1", - "@tailwindcss/node": "4.2.4", - "@tailwindcss/oxide": "4.2.4", - "enhanced-resolve": "^5.19.0", + "@tailwindcss/node": "4.3.0", + "@tailwindcss/oxide": "4.3.0", + "enhanced-resolve": "^5.21.0", "mri": "^1.2.0", "picocolors": "^1.1.1", - "tailwindcss": "4.2.4" + "tailwindcss": "4.3.0" }, "bin": { "tailwindcss": "dist/index.mjs" } }, "node_modules/@tailwindcss/node": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.2.4.tgz", - "integrity": "sha512-Ai7+yQPxz3ddrDQzFfBKdHEVBg0w3Zl83jnjuwxnZOsnH9pGn93QHQtpU0p/8rYWxvbFZHneni6p1BSLK4DkGA==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.0.tgz", + "integrity": "sha512-aFb4gUhFOgdh9AXo4IzBEOzBkkAxm9VigwDJnMIYv3lcfXCJVesNfbEaBl4BNgVRyid92AmdviqwBUBRKSeY3g==", "dev": true, "license": "MIT", "dependencies": { "@jridgewell/remapping": "^2.3.5", - "enhanced-resolve": "^5.19.0", + "enhanced-resolve": "^5.21.0", "jiti": "^2.6.1", "lightningcss": "1.32.0", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", - "tailwindcss": "4.2.4" + "tailwindcss": "4.3.0" } }, "node_modules/@tailwindcss/oxide": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.2.4.tgz", - "integrity": "sha512-9El/iI069DKDSXwTvB9J4BwdO5JhRrOweGaK25taBAvBXyXqJAX+Jqdvs8r8gKpsI/1m0LeJLyQYTf/WLrBT1Q==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.3.0.tgz", + "integrity": "sha512-F7HZGBeN9I0/AuuJS5PwcD8xayx5ri5GhjYUDBEVYUkexyA/giwbDNjRVrxSezE3T250OU2K/wp/ltWx3UOefg==", "dev": true, "license": "MIT", "engines": { "node": ">= 20" }, "optionalDependencies": { - "@tailwindcss/oxide-android-arm64": "4.2.4", - "@tailwindcss/oxide-darwin-arm64": "4.2.4", - "@tailwindcss/oxide-darwin-x64": "4.2.4", - "@tailwindcss/oxide-freebsd-x64": "4.2.4", - "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.4", - "@tailwindcss/oxide-linux-arm64-gnu": "4.2.4", - "@tailwindcss/oxide-linux-arm64-musl": "4.2.4", - "@tailwindcss/oxide-linux-x64-gnu": "4.2.4", - "@tailwindcss/oxide-linux-x64-musl": "4.2.4", - "@tailwindcss/oxide-wasm32-wasi": "4.2.4", - "@tailwindcss/oxide-win32-arm64-msvc": "4.2.4", - "@tailwindcss/oxide-win32-x64-msvc": "4.2.4" + "@tailwindcss/oxide-android-arm64": "4.3.0", + "@tailwindcss/oxide-darwin-arm64": "4.3.0", + "@tailwindcss/oxide-darwin-x64": "4.3.0", + "@tailwindcss/oxide-freebsd-x64": "4.3.0", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.0", + "@tailwindcss/oxide-linux-arm64-gnu": "4.3.0", + "@tailwindcss/oxide-linux-arm64-musl": "4.3.0", + "@tailwindcss/oxide-linux-x64-gnu": "4.3.0", + "@tailwindcss/oxide-linux-x64-musl": "4.3.0", + "@tailwindcss/oxide-wasm32-wasi": "4.3.0", + "@tailwindcss/oxide-win32-arm64-msvc": "4.3.0", + "@tailwindcss/oxide-win32-x64-msvc": "4.3.0" } }, "node_modules/@tailwindcss/oxide-android-arm64": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.2.4.tgz", - "integrity": "sha512-e7MOr1SAn9U8KlZzPi1ZXGZHeC5anY36qjNwmZv9pOJ8E4Q6jmD1vyEHkQFmNOIN7twGPEMXRHmitN4zCMN03g==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.0.tgz", + "integrity": "sha512-TJPiq67tKlLuObP6RkwvVGDoxCMBVtDgKkLfa/uyj7/FyxvQwHS+UOnVrXXgbEsfUaMgiVvC4KbJnRr26ho4Ng==", "cpu": [ "arm64" ], @@ -3812,9 +2938,9 @@ } }, "node_modules/@tailwindcss/oxide-darwin-arm64": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.2.4.tgz", - "integrity": "sha512-tSC/Kbqpz/5/o/C2sG7QvOxAKqyd10bq+ypZNf+9Fi2TvbVbv1zNpcEptcsU7DPROaSbVgUXmrzKhurFvo5eDg==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.0.tgz", + "integrity": "sha512-oMN/WZRb+SO37BmUElEgeEWuU8E/HXRkiODxJxLe1UTHVXLrdVSgfaJV7pSlhRGMSOiXLuxTIjfsF3wYvz8cgQ==", "cpu": [ "arm64" ], @@ -3829,9 +2955,9 @@ } }, "node_modules/@tailwindcss/oxide-darwin-x64": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.2.4.tgz", - "integrity": "sha512-yPyUXn3yO/ufR6+Kzv0t4fCg2qNr90jxXc5QqBpjlPNd0NqyDXcmQb/6weunH/MEDXW5dhyEi+agTDiqa3WsGg==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.0.tgz", + "integrity": "sha512-N6CUmu4a6bKVADfw77p+iw6Yd9Q3OBhe0veaDX+QazfuVYlQsHfDgxBrsjQ/IW+zywL8mTrNd0SdJT/zgtvMdA==", "cpu": [ "x64" ], @@ -3846,9 +2972,9 @@ } }, "node_modules/@tailwindcss/oxide-freebsd-x64": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.2.4.tgz", - "integrity": "sha512-BoMIB4vMQtZsXdGLVc2z+P9DbETkiopogfWZKbWwM8b/1Vinbs4YcUwo+kM/KeLkX3Ygrf4/PsRndKaYhS8Eiw==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.0.tgz", + "integrity": "sha512-zDL5hBkQdH5C6MpqbK3gQAgP80tsMwSI26vjOzjJtNCMUo0lFgOItzHKBIupOZNQxt3ouPH7RPhvNhiTfCe5CQ==", "cpu": [ "x64" ], @@ -3863,9 +2989,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.2.4.tgz", - "integrity": "sha512-7pIHBLTHYRAlS7V22JNuTh33yLH4VElwKtB3bwchK/UaKUPpQ0lPQiOWcbm4V3WP2I6fNIJ23vABIvoy2izdwA==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.0.tgz", + "integrity": "sha512-R06HdNi7A7OEoMsf6d4tjZ71RCWnZQPHj2mnotSFURjNLdBC+cIgXQ7l81CqeoiQftjf6OOblxXMInMgN2VzMA==", "cpu": [ "arm" ], @@ -3880,9 +3006,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.2.4.tgz", - "integrity": "sha512-+E4wxJ0ZGOzSH325reXTWB48l42i93kQqMvDyz5gqfRzRZ7faNhnmvlV4EPGJU3QJM/3Ab5jhJ5pCRUsKn6OQw==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.0.tgz", + "integrity": "sha512-qTJHELX8jetjhRQHCLilkVLmybpzNQAtaI/gaoVoidn/ufbNDbAo8KlK2J+yPoc8wQxvDxCmh/5lr8nC1+lTbg==", "cpu": [ "arm64" ], @@ -3897,9 +3023,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-arm64-musl": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.2.4.tgz", - "integrity": "sha512-bBADEGAbo4ASnppIziaQJelekCxdMaxisrk+fB7Thit72IBnALp9K6ffA2G4ruj90G9XRS2VQ6q2bCKbfFV82g==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.3.0.tgz", + "integrity": "sha512-Z6sukiQsngnWO+l39X4pPbiWT81IC+PLKF+PHxIlyZbGNb9MODfYlXEVlFvej5BOZInWX01kVyzeLvHsXhfczQ==", "cpu": [ "arm64" ], @@ -3914,9 +3040,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-x64-gnu": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.2.4.tgz", - "integrity": "sha512-7Mx25E4WTfnht0TVRTyC00j3i0M+EeFe7wguMDTlX4mRxafznw0CA8WJkFjWYH5BlgELd1kSjuU2JiPnNZbJDA==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.0.tgz", + "integrity": "sha512-DRNdQRpSGzRGfARVuVkxvM8Q12nh19l4BF/G7zGA1oe+9wcC6saFBHTISrpIcKzhiXtSrlSrluCfvMuledoCTQ==", "cpu": [ "x64" ], @@ -3931,9 +3057,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-x64-musl": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.2.4.tgz", - "integrity": "sha512-2wwJRF7nyhOR0hhHoChc04xngV3iS+akccHTGtz965FwF0up4b2lOdo6kI1EbDaEXKgvcrFBYcYQQ/rrnWFVfA==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.3.0.tgz", + "integrity": "sha512-Z0IADbDo8bh6I7h2IQMx601AdXBLfFpEdUotft86evd/8ZPflZe9COPO8Q1vw+pfLWIUo9zN/JGZvwuAJqduqg==", "cpu": [ "x64" ], @@ -3948,9 +3074,9 @@ } }, "node_modules/@tailwindcss/oxide-wasm32-wasi": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.2.4.tgz", - "integrity": "sha512-FQsqApeor8Fo6gUEklzmaa9994orJZZDBAlQpK2Mq+DslRKFJeD6AjHpBQ0kZFQohVr8o85PPh8eOy86VlSCmw==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.0.tgz", + "integrity": "sha512-HNZGOUxEmElksYR7S6sC5jTeNGpobAsy9u7Gu0AskJ8/20FR9GqebUyB+HBcU/ax6BHuiuJi+Oda4B+YX6H1yA==", "bundleDependencies": [ "@napi-rs/wasm-runtime", "@emnapi/core", @@ -3966,10 +3092,10 @@ "license": "MIT", "optional": true, "dependencies": { - "@emnapi/core": "^1.8.1", - "@emnapi/runtime": "^1.8.1", - "@emnapi/wasi-threads": "^1.1.0", - "@napi-rs/wasm-runtime": "^1.1.1", + "@emnapi/core": "^1.10.0", + "@emnapi/runtime": "^1.10.0", + "@emnapi/wasi-threads": "^1.2.1", + "@napi-rs/wasm-runtime": "^1.1.4", "@tybys/wasm-util": "^0.10.1", "tslib": "^2.8.1" }, @@ -3977,74 +3103,10 @@ "node": ">=14.0.0" } }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/core": { - "version": "1.8.1", - "dev": true, - "inBundle": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/wasi-threads": "1.1.0", - "tslib": "^2.4.0" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/runtime": { - "version": "1.8.1", - "dev": true, - "inBundle": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/wasi-threads": { - "version": "1.1.0", - "dev": true, - "inBundle": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": { - "version": "1.1.1", - "dev": true, - "inBundle": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/core": "^1.7.1", - "@emnapi/runtime": "^1.7.1", - "@tybys/wasm-util": "^0.10.1" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Brooooooklyn" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@tybys/wasm-util": { - "version": "0.10.1", - "dev": true, - "inBundle": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/tslib": { - "version": "2.8.1", - "dev": true, - "inBundle": true, - "license": "0BSD", - "optional": true - }, "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.2.4.tgz", - "integrity": "sha512-L9BXqxC4ToVgwMFqj3pmZRqyHEztulpUJzCxUtLjobMCzTPsGt1Fa9enKbOpY2iIyVtaHNeNvAK8ERP/64sqGQ==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.0.tgz", + "integrity": "sha512-Pe+RPVTi1T+qymuuRpcdvwSVZjnll/f7n8gBxMMh3xLTctMDKqpdfGimbMyioqtLhUYZxdJ9wGNhV7MKHvgZsQ==", "cpu": [ "arm64" ], @@ -4059,9 +3121,9 @@ } }, "node_modules/@tailwindcss/oxide-win32-x64-msvc": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.2.4.tgz", - "integrity": "sha512-ESlKG0EpVJQwRjXDDa9rLvhEAh0mhP1sF7sap9dNZT0yyl9SAG6T7gdP09EH0vIv0UNTlo6jPWyujD6559fZvw==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.3.0.tgz", + "integrity": "sha512-Mvrf2kXW/yeW/OTezZlCGOirXRcUuLIBx/5Y12BaPM7wJoryG6dfS/NJL8aBPqtTEx/Vm4T4vKzFUcKDT+TKUA==", "cpu": [ "x64" ], @@ -4076,15 +3138,15 @@ } }, "node_modules/@tailwindcss/vite": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.2.4.tgz", - "integrity": "sha512-pCvohwOCspk3ZFn6eJzrrX3g4n2JY73H6MmYC87XfGPyTty4YsCjYTMArRZm/zOI8dIt3+EcrLHAFPe5A4bgtw==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.3.0.tgz", + "integrity": "sha512-t6J3OrB5Fc0ExuhohouH0fWUGMYL6PTLhW+E7zIk/pdbnJARZDCwjBznFnkh5ynRnIRSI4YjtTH0t6USjJISrw==", "dev": true, "license": "MIT", "dependencies": { - "@tailwindcss/node": "4.2.4", - "@tailwindcss/oxide": "4.2.4", - "tailwindcss": "4.2.4" + "@tailwindcss/node": "4.3.0", + "@tailwindcss/oxide": "4.3.0", + "tailwindcss": "4.3.0" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7 || ^8" @@ -4133,14 +3195,12 @@ "version": "4.17.24", "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.24.tgz", "integrity": "sha512-gIW7lQLZbue7lRSWEFql49QJJWThrTFFeIMJdp3eH4tKoxm1OvEPg02rm4wCCSHS0cL3/Fizimb35b7k8atwsQ==", - "dev": true, "license": "MIT" }, "node_modules/@types/lodash-es": { "version": "4.17.12", "resolved": "https://registry.npmjs.org/@types/lodash-es/-/lodash-es-4.17.12.tgz", "integrity": "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==", - "dev": true, "license": "MIT", "dependencies": { "@types/lodash": "*" @@ -4154,9 +3214,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "22.19.17", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.17.tgz", - "integrity": "sha512-wGdMcf+vPYM6jikpS/qhg6WiqSV/OhG+jeeHT/KlVqxYfD40iYJf9/AE1uQxVWFvU7MipKRkRv8NSHiCGgPr8Q==", + "version": "22.19.19", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.19.tgz", + "integrity": "sha512-dyh/xO2Fh5bYrfWaaqGrRQQGkNdmYw6AmaAUvYeUMNTWQtvb796ikLdmTchRmOlOiIJ1TDXfWgVx1QkUlQ6Hew==", "license": "MIT", "dependencies": { "undici-types": "~6.21.0" @@ -4490,16 +3550,13 @@ "license": "MIT" }, "node_modules/ansi-regex": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true, "license": "MIT", "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" + "node": ">=8" } }, "node_modules/ansi-styles": { @@ -4539,13 +3596,6 @@ "js-tokens": "^10.0.0" } }, - "node_modules/ast-v8-to-istanbul/node_modules/js-tokens": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz", - "integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==", - "dev": true, - "license": "MIT" - }, "node_modules/balanced-match": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", @@ -4603,9 +3653,9 @@ "license": "MIT" }, "node_modules/brace-expansion": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", - "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", "license": "MIT", "dependencies": { "balanced-match": "^4.0.2" @@ -4741,6 +3791,21 @@ "url": "https://polar.sh/cva" } }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/clsx": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", @@ -4772,7 +3837,6 @@ "version": "12.1.0", "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", - "dev": true, "license": "MIT", "engines": { "node": ">=18" @@ -4803,16 +3867,6 @@ "url": "https://github.com/open-cli-tools/concurrently?sponsor=1" } }, - "node_modules/concurrently/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/concurrently/node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -4843,63 +3897,6 @@ "node": ">=8" } }, - "node_modules/concurrently/node_modules/cliui": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", - "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/concurrently/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/concurrently/node_modules/yargs": { - "version": "17.7.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", - "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", - "dev": true, - "license": "MIT", - "dependencies": { - "cliui": "^8.0.1", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.3", - "y18n": "^5.0.5", - "yargs-parser": "^21.1.1" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/concurrently/node_modules/yargs-parser": { - "version": "21.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", - "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=12" - } - }, "node_modules/core-util-is": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", @@ -5057,9 +4054,9 @@ } }, "node_modules/enhanced-resolve": { - "version": "5.21.0", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.21.0.tgz", - "integrity": "sha512-otxSQPw4lkOZWkHpB3zaEQs6gWYEsmX4xQF68ElXC/TWvGxGMSGOvoNbaLXm6/cS/fSfHtsEdw90y20PCd+sCA==", + "version": "5.21.3", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.21.3.tgz", + "integrity": "sha512-QyL119InA+XXEkNLNTPCXPugSvOfhwv0JOlGNzvxs0hZaiHLNvXSpudUWsOlsXGWJh8G6ckCScEkVHfX3kw/2Q==", "dev": true, "license": "MIT", "dependencies": { @@ -5088,9 +4085,9 @@ "license": "MIT" }, "node_modules/esbuild": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", - "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.0.tgz", + "integrity": "sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -5101,32 +4098,32 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.27.7", - "@esbuild/android-arm": "0.27.7", - "@esbuild/android-arm64": "0.27.7", - "@esbuild/android-x64": "0.27.7", - "@esbuild/darwin-arm64": "0.27.7", - "@esbuild/darwin-x64": "0.27.7", - "@esbuild/freebsd-arm64": "0.27.7", - "@esbuild/freebsd-x64": "0.27.7", - "@esbuild/linux-arm": "0.27.7", - "@esbuild/linux-arm64": "0.27.7", - "@esbuild/linux-ia32": "0.27.7", - "@esbuild/linux-loong64": "0.27.7", - "@esbuild/linux-mips64el": "0.27.7", - "@esbuild/linux-ppc64": "0.27.7", - "@esbuild/linux-riscv64": "0.27.7", - "@esbuild/linux-s390x": "0.27.7", - "@esbuild/linux-x64": "0.27.7", - "@esbuild/netbsd-arm64": "0.27.7", - "@esbuild/netbsd-x64": "0.27.7", - "@esbuild/openbsd-arm64": "0.27.7", - "@esbuild/openbsd-x64": "0.27.7", - "@esbuild/openharmony-arm64": "0.27.7", - "@esbuild/sunos-x64": "0.27.7", - "@esbuild/win32-arm64": "0.27.7", - "@esbuild/win32-ia32": "0.27.7", - "@esbuild/win32-x64": "0.27.7" + "@esbuild/aix-ppc64": "0.28.0", + "@esbuild/android-arm": "0.28.0", + "@esbuild/android-arm64": "0.28.0", + "@esbuild/android-x64": "0.28.0", + "@esbuild/darwin-arm64": "0.28.0", + "@esbuild/darwin-x64": "0.28.0", + "@esbuild/freebsd-arm64": "0.28.0", + "@esbuild/freebsd-x64": "0.28.0", + "@esbuild/linux-arm": "0.28.0", + "@esbuild/linux-arm64": "0.28.0", + "@esbuild/linux-ia32": "0.28.0", + "@esbuild/linux-loong64": "0.28.0", + "@esbuild/linux-mips64el": "0.28.0", + "@esbuild/linux-ppc64": "0.28.0", + "@esbuild/linux-riscv64": "0.28.0", + "@esbuild/linux-s390x": "0.28.0", + "@esbuild/linux-x64": "0.28.0", + "@esbuild/netbsd-arm64": "0.28.0", + "@esbuild/netbsd-x64": "0.28.0", + "@esbuild/openbsd-arm64": "0.28.0", + "@esbuild/openbsd-x64": "0.28.0", + "@esbuild/openharmony-arm64": "0.28.0", + "@esbuild/sunos-x64": "0.28.0", + "@esbuild/win32-arm64": "0.28.0", + "@esbuild/win32-ia32": "0.28.0", + "@esbuild/win32-x64": "0.28.0" } }, "node_modules/escalade": { @@ -5168,19 +4165,6 @@ "node": ">=6" } }, - "node_modules/execa/node_modules/get-stream": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", - "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", - "dev": true, - "license": "MIT", - "dependencies": { - "pump": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/expand-template": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", @@ -5225,9 +4209,9 @@ } }, "node_modules/fast-xml-builder": { - "version": "1.1.9", - "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.1.9.tgz", - "integrity": "sha512-jcyKVSEX13iseJqg7n/KWw+xnu/7fdrZ333Fac54KjHDIELVCfDDJXYIm6DTJ0Su4gSzrhqiK0DzY/wZbF40mw==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.2.0.tgz", + "integrity": "sha512-00aAWieqff+ZJhsXA4g1g7M8k+7AYoMUUHF+/zFb5U6Uv/P0Vl4QZo84/IcufzYalLuEj9928bXN9PbbFzMF0Q==", "funding": [ { "type": "github", @@ -5236,13 +4220,14 @@ ], "license": "MIT", "dependencies": { - "path-expression-matcher": "^1.1.3" + "path-expression-matcher": "^1.5.0", + "xml-naming": "^0.1.0" } }, "node_modules/fast-xml-parser": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.7.2.tgz", - "integrity": "sha512-P7oW7tLbYnhOLQk/Gv7cZgzgMPP/XN03K02/Jy6Y/NHzyIAIpxuZIM/YqAkfiXFPxA2CTm7NtCijK9EDu09u2w==", + "version": "5.7.3", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.7.3.tgz", + "integrity": "sha512-C0AaNuC+mscy6vrAQKAc/rMq+zAPHodfHGZu4sGVehvAQt/JLG1O5zEcYcXSY5zSqr4YVgxsB+pHXTq0i7eDlg==", "funding": [ { "type": "github", @@ -5252,7 +4237,7 @@ "license": "MIT", "dependencies": { "@nodable/entities": "^2.1.0", - "fast-xml-builder": "^1.1.5", + "fast-xml-builder": "^1.1.7", "path-expression-matcher": "^1.5.0", "strnum": "^2.2.3" }, @@ -5483,9 +4468,9 @@ } }, "node_modules/get-east-asian-width": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.5.0.tgz", - "integrity": "sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.6.0.tgz", + "integrity": "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==", "license": "MIT", "engines": { "node": ">=18" @@ -5494,17 +4479,17 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/get-tsconfig": { - "version": "4.14.0", - "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.14.0.tgz", - "integrity": "sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==", + "node_modules/get-stream": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", + "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", "dev": true, "license": "MIT", "dependencies": { - "resolve-pkg-maps": "^1.0.0" + "pump": "^3.0.0" }, - "funding": { - "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + "engines": { + "node": ">=6" } }, "node_modules/github-from-package": { @@ -5908,9 +4893,9 @@ } }, "node_modules/js-tokens": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", - "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz", + "integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==", "dev": true, "license": "MIT" }, @@ -6009,9 +4994,9 @@ } }, "node_modules/katex": { - "version": "0.16.45", - "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.45.tgz", - "integrity": "sha512-pQpZbdBu7wCTmQUh7ufPmLr0pFoObnGUoL/yhtwJDgmmQpbkg/0HSVti25Fu4rmd1oCR6NGWe9vqTWuWv3GcNA==", + "version": "0.16.47", + "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.47.tgz", + "integrity": "sha512-Eeo8Ys1doU1z+x8AZsPpQu+p/QcZBI5PeOo7QGQdy2x2m0MU/hYagBbGOmXwr5KVbEfVuWv9LpnQWeehogurjg==", "funding": [ "https://opencollective.com/katex", "https://github.com/sponsors/katex" @@ -6315,9 +5300,9 @@ } }, "node_modules/lit": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/lit/-/lit-3.3.2.tgz", - "integrity": "sha512-NF9zbsP79l4ao2SNrH3NkfmFgN/hBYSQo90saIVI1o5GpjAdCPVstVzO1MrLOakHoEhYkrtRjPK6Ob521aoYWQ==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/lit/-/lit-3.3.3.tgz", + "integrity": "sha512-fycuvZg/hkpozL00lm1pEJH5nN/lr9ZXd6mJI2HSN4+Bzc+LDNdEApJ6HFbPkdFNHLvOplIIuJvxkS4XUxqirw==", "license": "BSD-3-Clause", "dependencies": { "@lit/reactive-element": "^2.1.0", @@ -6337,9 +5322,9 @@ } }, "node_modules/lit-html": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/lit-html/-/lit-html-3.3.2.tgz", - "integrity": "sha512-Qy9hU88zcmaxBXcc10ZpdK7cOLXvXpRoBxERdtqV9QOrfpMZZ6pSYP91LhpPtap3sFMUiL7Tw2RImbe0Al2/kw==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/lit-html/-/lit-html-3.3.3.tgz", + "integrity": "sha512-el8M6jK2o3RXBnrSHX3ZKrsN8zEV63pSExTO1wYJz7QndGYZ8353e2a5PPX+qHe2aGayfnchQmkAojaWAREOIA==", "license": "BSD-3-Clause", "dependencies": { "@types/trusted-types": "^2.0.2" @@ -6349,7 +5334,6 @@ "version": "4.18.1", "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.18.1.tgz", "integrity": "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==", - "dev": true, "license": "MIT" }, "node_modules/long": { @@ -6581,6 +5565,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "deprecated": "Use your platform's native DOMException instead", "funding": [ { "type": "github", @@ -6865,6 +5850,7 @@ "version": "7.1.3", "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.", "dev": true, "license": "MIT", "dependencies": { @@ -6915,9 +5901,9 @@ } }, "node_modules/protobufjs": { - "version": "7.5.6", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.6.tgz", - "integrity": "sha512-M71sTMB146U3u0di3yup8iM+zv8yPRNQVr1KK4tyBitl3qFvEGucq/rGDRShD2rsJhtN02RJaJ7j5X5hmy8SJg==", + "version": "7.5.8", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.8.tgz", + "integrity": "sha512-dvpCIeLPbXZS/Ete7yLaO7RenOdken2NHKykBXbsaGxZT0UTltcarBciw+A78SRQs9iMAAVpsYA+l8b1hTePIA==", "hasInstallScript": true, "license": "BSD-3-Clause", "dependencies": { @@ -7045,16 +6031,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/resolve-pkg-maps": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", - "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" - } - }, "node_modules/retry": { "version": "0.13.1", "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", @@ -7076,9 +6052,9 @@ } }, "node_modules/rollup": { - "version": "4.60.3", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.3.tgz", - "integrity": "sha512-pAQK9HalE84QSm4Po3EmWIZPd3FnjkShVkiMlz1iligWYkWQ7wHYd1PF/T7QZ5TVSD6uSTon5gBVMSM4JfBV+A==", + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.4.tgz", + "integrity": "sha512-WHeFSbZYsPu3+bLoNRUuAO+wavNlocOPf3wSHTP7hcFKVnJeWsYlCDbr3mTS14FCizf9ccIxXA8sGL8zKeQN3g==", "dev": true, "license": "MIT", "dependencies": { @@ -7092,31 +6068,31 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.60.3", - "@rollup/rollup-android-arm64": "4.60.3", - "@rollup/rollup-darwin-arm64": "4.60.3", - "@rollup/rollup-darwin-x64": "4.60.3", - "@rollup/rollup-freebsd-arm64": "4.60.3", - "@rollup/rollup-freebsd-x64": "4.60.3", - "@rollup/rollup-linux-arm-gnueabihf": "4.60.3", - "@rollup/rollup-linux-arm-musleabihf": "4.60.3", - "@rollup/rollup-linux-arm64-gnu": "4.60.3", - "@rollup/rollup-linux-arm64-musl": "4.60.3", - "@rollup/rollup-linux-loong64-gnu": "4.60.3", - "@rollup/rollup-linux-loong64-musl": "4.60.3", - "@rollup/rollup-linux-ppc64-gnu": "4.60.3", - "@rollup/rollup-linux-ppc64-musl": "4.60.3", - "@rollup/rollup-linux-riscv64-gnu": "4.60.3", - "@rollup/rollup-linux-riscv64-musl": "4.60.3", - "@rollup/rollup-linux-s390x-gnu": "4.60.3", - "@rollup/rollup-linux-x64-gnu": "4.60.3", - "@rollup/rollup-linux-x64-musl": "4.60.3", - "@rollup/rollup-openbsd-x64": "4.60.3", - "@rollup/rollup-openharmony-arm64": "4.60.3", - "@rollup/rollup-win32-arm64-msvc": "4.60.3", - "@rollup/rollup-win32-ia32-msvc": "4.60.3", - "@rollup/rollup-win32-x64-gnu": "4.60.3", - "@rollup/rollup-win32-x64-msvc": "4.60.3", + "@rollup/rollup-android-arm-eabi": "4.60.4", + "@rollup/rollup-android-arm64": "4.60.4", + "@rollup/rollup-darwin-arm64": "4.60.4", + "@rollup/rollup-darwin-x64": "4.60.4", + "@rollup/rollup-freebsd-arm64": "4.60.4", + "@rollup/rollup-freebsd-x64": "4.60.4", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.4", + "@rollup/rollup-linux-arm-musleabihf": "4.60.4", + "@rollup/rollup-linux-arm64-gnu": "4.60.4", + "@rollup/rollup-linux-arm64-musl": "4.60.4", + "@rollup/rollup-linux-loong64-gnu": "4.60.4", + "@rollup/rollup-linux-loong64-musl": "4.60.4", + "@rollup/rollup-linux-ppc64-gnu": "4.60.4", + "@rollup/rollup-linux-ppc64-musl": "4.60.4", + "@rollup/rollup-linux-riscv64-gnu": "4.60.4", + "@rollup/rollup-linux-riscv64-musl": "4.60.4", + "@rollup/rollup-linux-s390x-gnu": "4.60.4", + "@rollup/rollup-linux-x64-gnu": "4.60.4", + "@rollup/rollup-linux-x64-musl": "4.60.4", + "@rollup/rollup-openbsd-x64": "4.60.4", + "@rollup/rollup-openharmony-arm64": "4.60.4", + "@rollup/rollup-win32-arm64-msvc": "4.60.4", + "@rollup/rollup-win32-ia32-msvc": "4.60.4", + "@rollup/rollup-win32-x64-gnu": "4.60.4", + "@rollup/rollup-win32-x64-msvc": "4.60.4", "fsevents": "~2.3.2" } }, @@ -7182,9 +6158,9 @@ "license": "MIT" }, "node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz", + "integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==", "dev": true, "license": "ISC", "bin": { @@ -7227,7 +6203,6 @@ "version": "1.8.3", "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz", "integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -7397,66 +6372,17 @@ "node": ">=8" } }, - "node_modules/string-width-cjs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width-cjs/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/strip-ansi": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", - "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, "license": "MIT", "dependencies": { - "ansi-regex": "^6.2.2" + "ansi-regex": "^5.0.1" }, "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" + "node": ">=8" } }, "node_modules/strip-ansi-cjs": { @@ -7473,16 +6399,6 @@ "node": ">=8" } }, - "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/strip-eof": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", @@ -7516,6 +6432,13 @@ "url": "https://github.com/sponsors/antfu" } }, + "node_modules/strip-literal/node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true, + "license": "MIT" + }, "node_modules/strnum": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.3.0.tgz", @@ -7558,9 +6481,9 @@ } }, "node_modules/tailwind-merge": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.5.0.tgz", - "integrity": "sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A==", + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.6.0.tgz", + "integrity": "sha512-uxL7qAVQriqRQPAyK3pj66VqskWqoZ37PW94jwOTwNfq/z9oyu1V+eqrZqtR2+fCiXdYOZe/Modt8GtvqNzu+w==", "license": "MIT", "funding": { "type": "github", @@ -7587,9 +6510,9 @@ } }, "node_modules/tailwindcss": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.4.tgz", - "integrity": "sha512-HhKppgO81FQof5m6TEnuBWCZGgfRAWbaeOaGT00KOy/Pf/j6oUihdvBpA7ltCeAvZpFhW3j0PTclkxsd4IXYDA==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.0.tgz", + "integrity": "sha512-y6nxMGB1nMW9R6k96e5gdIFzcfL/gTJRNaqGes1YvkLnPVXzWgbqFF2yLC0T8G774n24cx3Pe8XrKoniCOAH+Q==", "license": "MIT" }, "node_modules/tapable": { @@ -7858,14 +6781,13 @@ "license": "0BSD" }, "node_modules/tsx": { - "version": "4.21.0", - "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", - "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", + "version": "4.22.0", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.22.0.tgz", + "integrity": "sha512-8ccZMPD69s1AbKXx0C5ddTNZfNjwV04iIKgjZmKfKxMynEtSYcK0Lh7iQFh53fI5Yu4pb9usgAiqyPmEONaALg==", "dev": true, "license": "MIT", "dependencies": { - "esbuild": "~0.27.0", - "get-tsconfig": "^4.7.5" + "esbuild": "~0.28.0" }, "bin": { "tsx": "dist/cli.mjs" @@ -8038,6 +6960,490 @@ "url": "https://opencollective.com/vitest" } }, + "node_modules/vite/node_modules/@esbuild/aix-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", + "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", + "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", + "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/android-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", + "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", + "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", + "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", + "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", + "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", + "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", + "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", + "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-loong64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", + "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-mips64el": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", + "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", + "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-riscv64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", + "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-s390x": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", + "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", + "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", + "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/netbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", + "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", + "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/openbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", + "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", + "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/sunos-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", + "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", + "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", + "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", + "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/esbuild": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", + "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.7", + "@esbuild/android-arm": "0.27.7", + "@esbuild/android-arm64": "0.27.7", + "@esbuild/android-x64": "0.27.7", + "@esbuild/darwin-arm64": "0.27.7", + "@esbuild/darwin-x64": "0.27.7", + "@esbuild/freebsd-arm64": "0.27.7", + "@esbuild/freebsd-x64": "0.27.7", + "@esbuild/linux-arm": "0.27.7", + "@esbuild/linux-arm64": "0.27.7", + "@esbuild/linux-ia32": "0.27.7", + "@esbuild/linux-loong64": "0.27.7", + "@esbuild/linux-mips64el": "0.27.7", + "@esbuild/linux-ppc64": "0.27.7", + "@esbuild/linux-riscv64": "0.27.7", + "@esbuild/linux-s390x": "0.27.7", + "@esbuild/linux-x64": "0.27.7", + "@esbuild/netbsd-arm64": "0.27.7", + "@esbuild/netbsd-x64": "0.27.7", + "@esbuild/openbsd-arm64": "0.27.7", + "@esbuild/openbsd-x64": "0.27.7", + "@esbuild/openharmony-arm64": "0.27.7", + "@esbuild/sunos-x64": "0.27.7", + "@esbuild/win32-arm64": "0.27.7", + "@esbuild/win32-ia32": "0.27.7", + "@esbuild/win32-x64": "0.27.7" + } + }, "node_modules/vite/node_modules/fdir": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", @@ -8237,52 +7643,6 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -8291,9 +7651,9 @@ "license": "ISC" }, "node_modules/ws": { - "version": "8.20.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz", - "integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==", + "version": "8.20.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.1.tgz", + "integrity": "sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w==", "license": "MIT", "engines": { "node": ">=10.0.0" @@ -8323,6 +7683,21 @@ "node": ">=0.8" } }, + "node_modules/xml-naming": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/xml-naming/-/xml-naming-0.1.0.tgz", + "integrity": "sha512-k8KO9hrMyNk6tUWqUfkTEZbezRRpONVOzUTnc97VnCvyj6Tf9lyUR9EDAIeiVLv56jsMcoXEwjW8Kv5yPY52lw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "engines": { + "node": ">=16.0.0" + } + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", @@ -8334,9 +7709,9 @@ } }, "node_modules/yaml": { - "version": "2.8.4", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.4.tgz", - "integrity": "sha512-ml/JPOj9fOQK8RNnWojA67GbZ0ApXAUlN2UQclwv2eVgTgn7O9gg9o7paZWKMp4g0H3nTLtS9LVzhkpOFIKzog==", + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", + "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", "license": "ISC", "bin": { "yaml": "bin.mjs" @@ -8348,10 +7723,39 @@ "url": "https://github.com/sponsors/eemeli" } }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/zod": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", - "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" @@ -8368,10 +7772,10 @@ }, "packages/agent": { "name": "@earendil-works/pi-agent-core", - "version": "0.74.0", + "version": "0.74.1", "license": "MIT", "dependencies": { - "@earendil-works/pi-ai": "^0.74.0", + "@earendil-works/pi-ai": "^0.74.1", "ignore": "^7.0.5", "typebox": "^1.1.24", "yaml": "^2.8.2" @@ -8387,7 +7791,9 @@ } }, "packages/agent/node_modules/@types/node": { - "version": "24.12.2", + "version": "24.12.4", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.4.tgz", + "integrity": "sha512-GUUEShf+PBCGW2KaXwcIt3Yk+e3pkKwWKb9GSyM9WQVE+ep2jzmHdGsHzu4wgcZy5fN9FBdVzjpBQsYlpfpgLA==", "dev": true, "license": "MIT", "dependencies": { @@ -8396,12 +7802,14 @@ }, "packages/agent/node_modules/undici-types": { "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", "dev": true, "license": "MIT" }, "packages/ai": { "name": "@earendil-works/pi-ai", - "version": "0.74.0", + "version": "0.74.1", "license": "MIT", "dependencies": { "@anthropic-ai/sdk": "^0.91.1", @@ -8427,7 +7835,9 @@ } }, "packages/ai/node_modules/@types/node": { - "version": "24.12.2", + "version": "24.12.4", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.4.tgz", + "integrity": "sha512-GUUEShf+PBCGW2KaXwcIt3Yk+e3pkKwWKb9GSyM9WQVE+ep2jzmHdGsHzu4wgcZy5fN9FBdVzjpBQsYlpfpgLA==", "dev": true, "license": "MIT", "dependencies": { @@ -8436,17 +7846,19 @@ }, "packages/ai/node_modules/undici-types": { "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", "dev": true, "license": "MIT" }, "packages/coding-agent": { "name": "@earendil-works/pi-coding-agent", - "version": "0.74.0", + "version": "0.74.1", "license": "MIT", "dependencies": { - "@earendil-works/pi-agent-core": "^0.74.0", - "@earendil-works/pi-ai": "^0.74.0", - "@earendil-works/pi-tui": "^0.74.0", + "@earendil-works/pi-agent-core": "^0.74.1", + "@earendil-works/pi-ai": "^0.74.1", + "@earendil-works/pi-tui": "^0.74.1", "@silvia-odwyer/photon-node": "^0.3.4", "chalk": "^5.5.0", "diff": "^8.0.2", @@ -8483,86 +7895,25 @@ }, "packages/coding-agent/examples/extensions/custom-provider-anthropic": { "name": "pi-extension-custom-provider-anthropic", - "version": "0.74.0", + "version": "0.74.1", "dependencies": { "@anthropic-ai/sdk": "^0.52.0" } }, "packages/coding-agent/examples/extensions/custom-provider-gitlab-duo": { "name": "pi-extension-custom-provider-gitlab-duo", - "version": "0.74.0" + "version": "0.74.1" }, "packages/coding-agent/examples/extensions/sandbox": { "name": "pi-extension-sandbox", - "version": "1.4.0", + "version": "1.4.1", "dependencies": { "@anthropic-ai/sandbox-runtime": "^0.0.26" } }, - "packages/coding-agent/examples/extensions/sandbox/node_modules/@anthropic-ai/sandbox-runtime": { - "version": "0.0.26", - "license": "Apache-2.0", - "dependencies": { - "@pondwader/socks5-server": "^1.0.10", - "@types/lodash-es": "^4.17.12", - "commander": "^12.1.0", - "lodash-es": "^4.17.21", - "shell-quote": "^1.8.3", - "zod": "^3.24.1" - }, - "bin": { - "srt": "dist/cli.js" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "packages/coding-agent/examples/extensions/sandbox/node_modules/@pondwader/socks5-server": { - "version": "1.0.10", - "license": "MIT" - }, - "packages/coding-agent/examples/extensions/sandbox/node_modules/@types/lodash": { - "version": "4.17.23", - "license": "MIT" - }, - "packages/coding-agent/examples/extensions/sandbox/node_modules/@types/lodash-es": { - "version": "4.17.12", - "license": "MIT", - "dependencies": { - "@types/lodash": "*" - } - }, - "packages/coding-agent/examples/extensions/sandbox/node_modules/commander": { - "version": "12.1.0", - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "packages/coding-agent/examples/extensions/sandbox/node_modules/lodash-es": { - "version": "4.18.1", - "license": "MIT" - }, - "packages/coding-agent/examples/extensions/sandbox/node_modules/shell-quote": { - "version": "1.8.3", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "packages/coding-agent/examples/extensions/sandbox/node_modules/zod": { - "version": "3.25.76", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/colinhacks" - } - }, "packages/coding-agent/examples/extensions/with-deps": { "name": "pi-extension-with-deps", - "version": "0.74.0", + "version": "0.74.1", "dependencies": { "ms": "^2.1.3" }, @@ -8572,13 +7923,17 @@ }, "packages/coding-agent/node_modules/@anthropic-ai/sdk": { "version": "0.52.0", + "resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.52.0.tgz", + "integrity": "sha512-d4c+fg+xy9e46c8+YnrrgIQR45CZlAi7PwdzIfDXDM6ACxEZli1/fxhURsq30ZpMZy6LvSkr41jGq5aF5TD7rQ==", "license": "MIT", "bin": { "anthropic-ai-sdk": "bin/cli" } }, "packages/coding-agent/node_modules/@types/node": { - "version": "24.12.2", + "version": "24.12.4", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.4.tgz", + "integrity": "sha512-GUUEShf+PBCGW2KaXwcIt3Yk+e3pkKwWKb9GSyM9WQVE+ep2jzmHdGsHzu4wgcZy5fN9FBdVzjpBQsYlpfpgLA==", "dev": true, "license": "MIT", "dependencies": { @@ -8587,12 +7942,14 @@ }, "packages/coding-agent/node_modules/undici-types": { "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", "dev": true, "license": "MIT" }, "packages/tui": { "name": "@earendil-works/pi-tui", - "version": "0.74.0", + "version": "0.74.1", "license": "MIT", "dependencies": { "get-east-asian-width": "^1.3.0", @@ -8612,11 +7969,11 @@ }, "packages/web-ui": { "name": "@earendil-works/pi-web-ui", - "version": "0.74.0", + "version": "0.74.1", "license": "MIT", "dependencies": { - "@earendil-works/pi-ai": "^0.74.0", - "@earendil-works/pi-tui": "^0.74.0", + "@earendil-works/pi-ai": "^0.74.1", + "@earendil-works/pi-tui": "^0.74.1", "@lmstudio/sdk": "^1.5.0", "docx-preview": "^0.3.7", "jszip": "^3.10.1", @@ -8639,7 +7996,7 @@ }, "packages/web-ui/example": { "name": "pi-web-ui-example", - "version": "0.74.0", + "version": "0.74.1", "dependencies": { "@earendil-works/pi-ai": "file:../../ai", "@earendil-works/pi-web-ui": "file:../", diff --git a/packages/agent/CHANGELOG.md b/packages/agent/CHANGELOG.md index bcd8f292..923f7298 100644 --- a/packages/agent/CHANGELOG.md +++ b/packages/agent/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## [Unreleased] +## [0.74.1] - 2026-05-16 ## [0.74.0] - 2026-05-07 diff --git a/packages/agent/package.json b/packages/agent/package.json index 4fe6d2fc..b307e762 100644 --- a/packages/agent/package.json +++ b/packages/agent/package.json @@ -1,6 +1,6 @@ { "name": "@earendil-works/pi-agent-core", - "version": "0.74.0", + "version": "0.74.1", "description": "General-purpose agent with transport abstraction, state management, and attachment support", "type": "module", "main": "./dist/index.js", @@ -30,7 +30,7 @@ "prepublishOnly": "npm run clean && npm run build" }, "dependencies": { - "@earendil-works/pi-ai": "^0.74.0", + "@earendil-works/pi-ai": "^0.74.1", "ignore": "^7.0.5", "typebox": "^1.1.24", "yaml": "^2.8.2" diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index 0e7176f5..ffa59a3c 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## [Unreleased] +## [0.74.1] - 2026-05-16 ### Added diff --git a/packages/ai/package.json b/packages/ai/package.json index 4f4d5bf7..6d3f3fef 100644 --- a/packages/ai/package.json +++ b/packages/ai/package.json @@ -1,6 +1,6 @@ { "name": "@earendil-works/pi-ai", - "version": "0.74.0", + "version": "0.74.1", "description": "Unified LLM API with automatic model discovery and provider configuration", "type": "module", "main": "./dist/index.js", diff --git a/packages/ai/scripts/generate-models.ts b/packages/ai/scripts/generate-models.ts index 72858c4e..56dcaa6d 100644 --- a/packages/ai/scripts/generate-models.ts +++ b/packages/ai/scripts/generate-models.ts @@ -1621,8 +1621,32 @@ async function generateModels() { allModels.push(...codexModels); // Add missing Grok models - if (!allModels.some(m => m.provider === "xai" && m.id === "grok-code-fast-1")) { - allModels.push({ + const missingGrokModels: Model[] = [ + { + id: "grok-3", + name: "Grok 3", + api: "openai-completions", + baseUrl: "https://api.x.ai/v1", + provider: "xai", + reasoning: false, + input: ["text"], + cost: { input: 3, output: 15, cacheRead: 0.75, cacheWrite: 0 }, + contextWindow: 131072, + maxTokens: 8192, + }, + { + id: "grok-3-fast", + name: "Grok 3 Fast", + api: "openai-completions", + baseUrl: "https://api.x.ai/v1", + provider: "xai", + reasoning: false, + input: ["text"], + cost: { input: 5, output: 25, cacheRead: 1.25, cacheWrite: 0 }, + contextWindow: 131072, + maxTokens: 8192, + }, + { id: "grok-code-fast-1", name: "Grok Code Fast 1", api: "openai-completions", @@ -1638,7 +1662,12 @@ async function generateModels() { }, contextWindow: 32768, maxTokens: 8192, - }); + }, + ]; + for (const model of missingGrokModels) { + if (!allModels.some(m => m.provider === model.provider && m.id === model.id)) { + allModels.push(model); + } } // Add missing Mistral Medium 3.5 model until models.dev includes it diff --git a/packages/ai/src/image-models.generated.ts b/packages/ai/src/image-models.generated.ts index 9bbee4c8..64836aa0 100644 --- a/packages/ai/src/image-models.generated.ts +++ b/packages/ai/src/image-models.generated.ts @@ -185,6 +185,171 @@ export const IMAGE_MODELS = { cacheWrite: 0, }, } satisfies ImagesModel<"openrouter-images">, + "recraft/recraft-v3": { + id: "recraft/recraft-v3", + name: "Recraft: Recraft V3", + api: "openrouter-images", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + input: ["text", "image"], + output: ["image"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + } satisfies ImagesModel<"openrouter-images">, + "recraft/recraft-v4": { + id: "recraft/recraft-v4", + name: "Recraft: Recraft V4", + api: "openrouter-images", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + input: ["text", "image"], + output: ["image"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + } satisfies ImagesModel<"openrouter-images">, + "recraft/recraft-v4-pro": { + id: "recraft/recraft-v4-pro", + name: "Recraft: Recraft V4 Pro", + api: "openrouter-images", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + input: ["text", "image"], + output: ["image"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + } satisfies ImagesModel<"openrouter-images">, + "recraft/recraft-v4-pro-vector": { + id: "recraft/recraft-v4-pro-vector", + name: "Recraft: Recraft V4 Pro Vector", + api: "openrouter-images", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + input: ["text", "image"], + output: ["image"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + } satisfies ImagesModel<"openrouter-images">, + "recraft/recraft-v4-vector": { + id: "recraft/recraft-v4-vector", + name: "Recraft: Recraft V4 Vector", + api: "openrouter-images", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + input: ["text", "image"], + output: ["image"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + } satisfies ImagesModel<"openrouter-images">, + "recraft/recraft-v4.1": { + id: "recraft/recraft-v4.1", + name: "Recraft: Recraft V4.1", + api: "openrouter-images", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + input: ["text", "image"], + output: ["image"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + } satisfies ImagesModel<"openrouter-images">, + "recraft/recraft-v4.1-pro": { + id: "recraft/recraft-v4.1-pro", + name: "Recraft: Recraft V4.1 Pro", + api: "openrouter-images", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + input: ["text", "image"], + output: ["image"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + } satisfies ImagesModel<"openrouter-images">, + "recraft/recraft-v4.1-pro-vector": { + id: "recraft/recraft-v4.1-pro-vector", + name: "Recraft: Recraft V4.1 Pro Vector", + api: "openrouter-images", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + input: ["text", "image"], + output: ["image"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + } satisfies ImagesModel<"openrouter-images">, + "recraft/recraft-v4.1-utility": { + id: "recraft/recraft-v4.1-utility", + name: "Recraft: Recraft V4.1 Utility", + api: "openrouter-images", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + input: ["text", "image"], + output: ["image"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + } satisfies ImagesModel<"openrouter-images">, + "recraft/recraft-v4.1-utility-pro": { + id: "recraft/recraft-v4.1-utility-pro", + name: "Recraft: Recraft V4.1 Utility Pro", + api: "openrouter-images", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + input: ["text", "image"], + output: ["image"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + } satisfies ImagesModel<"openrouter-images">, + "recraft/recraft-v4.1-vector": { + id: "recraft/recraft-v4.1-vector", + name: "Recraft: Recraft V4.1 Vector", + api: "openrouter-images", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + input: ["text", "image"], + output: ["image"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + } satisfies ImagesModel<"openrouter-images">, "sourceful/riverflow-v2-fast": { id: "sourceful/riverflow-v2-fast", name: "Sourceful: Riverflow V2 Fast", diff --git a/packages/ai/src/models.generated.ts b/packages/ai/src/models.generated.ts index 5c3b6071..63bd4e5f 100644 --- a/packages/ai/src/models.generated.ts +++ b/packages/ai/src/models.generated.ts @@ -56,23 +56,6 @@ export const MODELS = { contextWindow: 128000, maxTokens: 8192, } satisfies Model<"bedrock-converse-stream">, - "amazon.nova-premier-v1:0": { - id: "amazon.nova-premier-v1:0", - name: "Nova Premier", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: true, - input: ["text", "image"], - cost: { - input: 2.5, - output: 12.5, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 1000000, - maxTokens: 16384, - } satisfies Model<"bedrock-converse-stream">, "amazon.nova-pro-v1:0": { id: "amazon.nova-pro-v1:0", name: "Nova Pro", @@ -90,91 +73,6 @@ export const MODELS = { contextWindow: 300000, maxTokens: 8192, } satisfies Model<"bedrock-converse-stream">, - "anthropic.claude-3-5-haiku-20241022-v1:0": { - id: "anthropic.claude-3-5-haiku-20241022-v1:0", - name: "Claude Haiku 3.5", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.8, - output: 4, - cacheRead: 0.08, - cacheWrite: 1, - }, - contextWindow: 200000, - maxTokens: 8192, - } satisfies Model<"bedrock-converse-stream">, - "anthropic.claude-3-5-sonnet-20240620-v1:0": { - id: "anthropic.claude-3-5-sonnet-20240620-v1:0", - name: "Claude Sonnet 3.5", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: false, - input: ["text", "image"], - cost: { - input: 3, - output: 15, - cacheRead: 0.3, - cacheWrite: 3.75, - }, - contextWindow: 200000, - maxTokens: 8192, - } satisfies Model<"bedrock-converse-stream">, - "anthropic.claude-3-5-sonnet-20241022-v2:0": { - id: "anthropic.claude-3-5-sonnet-20241022-v2:0", - name: "Claude Sonnet 3.5 v2", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: false, - input: ["text", "image"], - cost: { - input: 3, - output: 15, - cacheRead: 0.3, - cacheWrite: 3.75, - }, - contextWindow: 200000, - maxTokens: 8192, - } satisfies Model<"bedrock-converse-stream">, - "anthropic.claude-3-7-sonnet-20250219-v1:0": { - id: "anthropic.claude-3-7-sonnet-20250219-v1:0", - name: "Claude Sonnet 3.7", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: false, - input: ["text", "image"], - cost: { - input: 3, - output: 15, - cacheRead: 0.3, - cacheWrite: 3.75, - }, - contextWindow: 200000, - maxTokens: 8192, - } satisfies Model<"bedrock-converse-stream">, - "anthropic.claude-3-haiku-20240307-v1:0": { - id: "anthropic.claude-3-haiku-20240307-v1:0", - name: "Claude Haiku 3", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.25, - output: 1.25, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 200000, - maxTokens: 4096, - } satisfies Model<"bedrock-converse-stream">, "anthropic.claude-haiku-4-5-20251001-v1:0": { id: "anthropic.claude-haiku-4-5-20251001-v1:0", name: "Claude Haiku 4.5", @@ -209,23 +107,6 @@ export const MODELS = { contextWindow: 200000, maxTokens: 32000, } satisfies Model<"bedrock-converse-stream">, - "anthropic.claude-opus-4-20250514-v1:0": { - id: "anthropic.claude-opus-4-20250514-v1:0", - name: "Claude Opus 4", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: true, - input: ["text", "image"], - cost: { - input: 15, - output: 75, - cacheRead: 1.5, - cacheWrite: 18.75, - }, - contextWindow: 200000, - maxTokens: 32000, - } satisfies Model<"bedrock-converse-stream">, "anthropic.claude-opus-4-5-20251101-v1:0": { id: "anthropic.claude-opus-4-5-20251101-v1:0", name: "Claude Opus 4.5", @@ -279,23 +160,6 @@ export const MODELS = { contextWindow: 1000000, maxTokens: 128000, } satisfies Model<"bedrock-converse-stream">, - "anthropic.claude-sonnet-4-20250514-v1:0": { - id: "anthropic.claude-sonnet-4-20250514-v1:0", - name: "Claude Sonnet 4", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: true, - input: ["text", "image"], - cost: { - input: 3, - output: 15, - cacheRead: 0.3, - cacheWrite: 3.75, - }, - contextWindow: 200000, - maxTokens: 64000, - } satisfies Model<"bedrock-converse-stream">, "anthropic.claude-sonnet-4-5-20250929-v1:0": { id: "anthropic.claude-sonnet-4-5-20250929-v1:0", name: "Claude Sonnet 4.5", @@ -330,6 +194,23 @@ export const MODELS = { contextWindow: 1000000, maxTokens: 64000, } satisfies Model<"bedrock-converse-stream">, + "au.anthropic.claude-haiku-4-5-20251001-v1:0": { + id: "au.anthropic.claude-haiku-4-5-20251001-v1:0", + name: "Claude Haiku 4.5 (AU)", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: true, + input: ["text", "image"], + cost: { + input: 1, + output: 5, + cacheRead: 0.1, + cacheWrite: 1.25, + }, + contextWindow: 200000, + maxTokens: 64000, + } satisfies Model<"bedrock-converse-stream">, "au.anthropic.claude-opus-4-6-v1": { id: "au.anthropic.claude-opus-4-6-v1", name: "AU Anthropic Claude Opus 4.6", @@ -348,6 +229,23 @@ export const MODELS = { contextWindow: 1000000, maxTokens: 128000, } satisfies Model<"bedrock-converse-stream">, + "au.anthropic.claude-sonnet-4-5-20250929-v1:0": { + id: "au.anthropic.claude-sonnet-4-5-20250929-v1:0", + name: "Claude Sonnet 4.5 (AU)", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: true, + input: ["text", "image"], + cost: { + input: 3, + output: 15, + cacheRead: 0.3, + cacheWrite: 3.75, + }, + contextWindow: 200000, + maxTokens: 64000, + } satisfies Model<"bedrock-converse-stream">, "au.anthropic.claude-sonnet-4-6": { id: "au.anthropic.claude-sonnet-4-6", name: "AU Anthropic Claude Sonnet 4.6", @@ -486,23 +384,6 @@ export const MODELS = { contextWindow: 1000000, maxTokens: 128000, } satisfies Model<"bedrock-converse-stream">, - "eu.anthropic.claude-sonnet-4-20250514-v1:0": { - id: "eu.anthropic.claude-sonnet-4-20250514-v1:0", - name: "Claude Sonnet 4 (EU)", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.eu-central-1.amazonaws.com", - reasoning: true, - input: ["text", "image"], - cost: { - input: 3, - output: 15, - cacheRead: 0.3, - cacheWrite: 3.75, - }, - contextWindow: 200000, - maxTokens: 64000, - } satisfies Model<"bedrock-converse-stream">, "eu.anthropic.claude-sonnet-4-5-20250929-v1:0": { id: "eu.anthropic.claude-sonnet-4-5-20250929-v1:0", name: "Claude Sonnet 4.5 (EU)", @@ -607,23 +488,6 @@ export const MODELS = { contextWindow: 1000000, maxTokens: 128000, } satisfies Model<"bedrock-converse-stream">, - "global.anthropic.claude-sonnet-4-20250514-v1:0": { - id: "global.anthropic.claude-sonnet-4-20250514-v1:0", - name: "Claude Sonnet 4 (Global)", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: true, - input: ["text", "image"], - cost: { - input: 3, - output: 15, - cacheRead: 0.3, - cacheWrite: 3.75, - }, - contextWindow: 200000, - maxTokens: 64000, - } satisfies Model<"bedrock-converse-stream">, "global.anthropic.claude-sonnet-4-5-20250929-v1:0": { id: "global.anthropic.claude-sonnet-4-5-20250929-v1:0", name: "Claude Sonnet 4.5 (Global)", @@ -692,22 +556,57 @@ export const MODELS = { contextWindow: 128000, maxTokens: 4096, } satisfies Model<"bedrock-converse-stream">, - "meta.llama3-1-405b-instruct-v1:0": { - id: "meta.llama3-1-405b-instruct-v1:0", - name: "Llama 3.1 405B Instruct", + "jp.anthropic.claude-opus-4-7": { + id: "jp.anthropic.claude-opus-4-7", + name: "Claude Opus 4.7 (JP)", api: "bedrock-converse-stream", provider: "amazon-bedrock", baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: false, - input: ["text"], + reasoning: true, + thinkingLevelMap: {"xhigh":"xhigh"}, + input: ["text", "image"], cost: { - input: 2.4, - output: 2.4, - cacheRead: 0, - cacheWrite: 0, + input: 5, + output: 25, + cacheRead: 0.5, + cacheWrite: 6.25, }, - contextWindow: 128000, - maxTokens: 4096, + contextWindow: 1000000, + maxTokens: 128000, + } satisfies Model<"bedrock-converse-stream">, + "jp.anthropic.claude-sonnet-4-5-20250929-v1:0": { + id: "jp.anthropic.claude-sonnet-4-5-20250929-v1:0", + name: "Claude Sonnet 4.5 (JP)", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: true, + input: ["text", "image"], + cost: { + input: 3, + output: 15, + cacheRead: 0.3, + cacheWrite: 3.75, + }, + contextWindow: 200000, + maxTokens: 64000, + } satisfies Model<"bedrock-converse-stream">, + "jp.anthropic.claude-sonnet-4-6": { + id: "jp.anthropic.claude-sonnet-4-6", + name: "Claude Sonnet 4.6 (JP)", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: true, + input: ["text", "image"], + cost: { + input: 3, + output: 15, + cacheRead: 0.3, + cacheWrite: 3.75, + }, + contextWindow: 1000000, + maxTokens: 64000, } satisfies Model<"bedrock-converse-stream">, "meta.llama3-1-70b-instruct-v1:0": { id: "meta.llama3-1-70b-instruct-v1:0", @@ -743,74 +642,6 @@ export const MODELS = { contextWindow: 128000, maxTokens: 4096, } satisfies Model<"bedrock-converse-stream">, - "meta.llama3-2-11b-instruct-v1:0": { - id: "meta.llama3-2-11b-instruct-v1:0", - name: "Llama 3.2 11B Instruct", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.16, - output: 0.16, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 4096, - } satisfies Model<"bedrock-converse-stream">, - "meta.llama3-2-1b-instruct-v1:0": { - id: "meta.llama3-2-1b-instruct-v1:0", - name: "Llama 3.2 1B Instruct", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: false, - input: ["text"], - cost: { - input: 0.1, - output: 0.1, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131000, - maxTokens: 4096, - } satisfies Model<"bedrock-converse-stream">, - "meta.llama3-2-3b-instruct-v1:0": { - id: "meta.llama3-2-3b-instruct-v1:0", - name: "Llama 3.2 3B Instruct", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: false, - input: ["text"], - cost: { - input: 0.15, - output: 0.15, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131000, - maxTokens: 4096, - } satisfies Model<"bedrock-converse-stream">, - "meta.llama3-2-90b-instruct-v1:0": { - id: "meta.llama3-2-90b-instruct-v1:0", - name: "Llama 3.2 90B Instruct", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.72, - output: 0.72, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 4096, - } satisfies Model<"bedrock-converse-stream">, "meta.llama3-3-70b-instruct-v1:0": { id: "meta.llama3-3-70b-instruct-v1:0", name: "Llama 3.3 70B Instruct", @@ -1183,7 +1014,7 @@ export const MODELS = { cacheWrite: 0, }, contextWindow: 128000, - maxTokens: 4096, + maxTokens: 16384, } satisfies Model<"bedrock-converse-stream">, "openai.gpt-oss-20b-1:0": { id: "openai.gpt-oss-20b-1:0", @@ -1200,7 +1031,7 @@ export const MODELS = { cacheWrite: 0, }, contextWindow: 128000, - maxTokens: 4096, + maxTokens: 16384, } satisfies Model<"bedrock-converse-stream">, "openai.gpt-oss-safeguard-120b": { id: "openai.gpt-oss-safeguard-120b", @@ -1217,7 +1048,7 @@ export const MODELS = { cacheWrite: 0, }, contextWindow: 128000, - maxTokens: 4096, + maxTokens: 16384, } satisfies Model<"bedrock-converse-stream">, "openai.gpt-oss-safeguard-20b": { id: "openai.gpt-oss-safeguard-20b", @@ -1234,7 +1065,7 @@ export const MODELS = { cacheWrite: 0, }, contextWindow: 128000, - maxTokens: 4096, + maxTokens: 16384, } satisfies Model<"bedrock-converse-stream">, "qwen.qwen3-235b-a22b-2507-v1:0": { id: "qwen.qwen3-235b-a22b-2507-v1:0", @@ -1389,23 +1220,6 @@ export const MODELS = { contextWindow: 200000, maxTokens: 32000, } satisfies Model<"bedrock-converse-stream">, - "us.anthropic.claude-opus-4-20250514-v1:0": { - id: "us.anthropic.claude-opus-4-20250514-v1:0", - name: "Claude Opus 4 (US)", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: true, - input: ["text", "image"], - cost: { - input: 15, - output: 75, - cacheRead: 1.5, - cacheWrite: 18.75, - }, - contextWindow: 200000, - maxTokens: 32000, - } satisfies Model<"bedrock-converse-stream">, "us.anthropic.claude-opus-4-5-20251101-v1:0": { id: "us.anthropic.claude-opus-4-5-20251101-v1:0", name: "Claude Opus 4.5 (US)", @@ -1459,23 +1273,6 @@ export const MODELS = { contextWindow: 1000000, maxTokens: 128000, } satisfies Model<"bedrock-converse-stream">, - "us.anthropic.claude-sonnet-4-20250514-v1:0": { - id: "us.anthropic.claude-sonnet-4-20250514-v1:0", - name: "Claude Sonnet 4 (US)", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: true, - input: ["text", "image"], - cost: { - input: 3, - output: 15, - cacheRead: 0.3, - cacheWrite: 3.75, - }, - contextWindow: 200000, - maxTokens: 64000, - } satisfies Model<"bedrock-converse-stream">, "us.anthropic.claude-sonnet-4-5-20250929-v1:0": { id: "us.anthropic.claude-sonnet-4-5-20250929-v1:0", name: "Claude Sonnet 4.5 (US)", @@ -1510,6 +1307,57 @@ export const MODELS = { contextWindow: 1000000, maxTokens: 64000, } satisfies Model<"bedrock-converse-stream">, + "us.deepseek.r1-v1:0": { + id: "us.deepseek.r1-v1:0", + name: "DeepSeek-R1 (US)", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: true, + input: ["text"], + cost: { + input: 1.35, + output: 5.4, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 32768, + } satisfies Model<"bedrock-converse-stream">, + "us.meta.llama4-maverick-17b-instruct-v1:0": { + id: "us.meta.llama4-maverick-17b-instruct-v1:0", + name: "Llama 4 Maverick 17B Instruct (US)", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: false, + input: ["text", "image"], + cost: { + input: 0.24, + output: 0.97, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 1000000, + maxTokens: 16384, + } satisfies Model<"bedrock-converse-stream">, + "us.meta.llama4-scout-17b-instruct-v1:0": { + id: "us.meta.llama4-scout-17b-instruct-v1:0", + name: "Llama 4 Scout 17B Instruct (US)", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: false, + input: ["text", "image"], + cost: { + input: 0.17, + output: 0.66, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 3500000, + maxTokens: 16384, + } satisfies Model<"bedrock-converse-stream">, "writer.palmyra-x4-v1:0": { id: "writer.palmyra-x4-v1:0", name: "Palmyra X4", @@ -3604,6 +3452,7 @@ export const MODELS = { api: "anthropic-messages", provider: "fireworks", baseUrl: "https://api.fireworks.ai/inference", + compat: {"sendSessionAffinityHeaders":true,"supportsEagerToolInputStreaming":false,"supportsCacheControlOnTools":false,"supportsLongCacheRetention":false}, reasoning: true, input: ["text"], cost: { @@ -3614,12 +3463,6 @@ export const MODELS = { }, contextWindow: 163840, maxTokens: 163840, - compat: { - sendSessionAffinityHeaders: true, - supportsEagerToolInputStreaming: false, - supportsCacheControlOnTools: false, - supportsLongCacheRetention: false, - }, } satisfies Model<"anthropic-messages">, "accounts/fireworks/models/deepseek-v3p2": { id: "accounts/fireworks/models/deepseek-v3p2", @@ -3627,6 +3470,7 @@ export const MODELS = { api: "anthropic-messages", provider: "fireworks", baseUrl: "https://api.fireworks.ai/inference", + compat: {"sendSessionAffinityHeaders":true,"supportsEagerToolInputStreaming":false,"supportsCacheControlOnTools":false,"supportsLongCacheRetention":false}, reasoning: true, input: ["text"], cost: { @@ -3637,12 +3481,6 @@ export const MODELS = { }, contextWindow: 160000, maxTokens: 160000, - compat: { - sendSessionAffinityHeaders: true, - supportsEagerToolInputStreaming: false, - supportsCacheControlOnTools: false, - supportsLongCacheRetention: false, - }, } satisfies Model<"anthropic-messages">, "accounts/fireworks/models/deepseek-v4-pro": { id: "accounts/fireworks/models/deepseek-v4-pro", @@ -3650,6 +3488,7 @@ export const MODELS = { api: "anthropic-messages", provider: "fireworks", baseUrl: "https://api.fireworks.ai/inference", + compat: {"sendSessionAffinityHeaders":true,"supportsEagerToolInputStreaming":false,"supportsCacheControlOnTools":false,"supportsLongCacheRetention":false}, reasoning: true, input: ["text"], cost: { @@ -3660,12 +3499,6 @@ export const MODELS = { }, contextWindow: 1000000, maxTokens: 384000, - compat: { - sendSessionAffinityHeaders: true, - supportsEagerToolInputStreaming: false, - supportsCacheControlOnTools: false, - supportsLongCacheRetention: false, - }, } satisfies Model<"anthropic-messages">, "accounts/fireworks/models/glm-4p5": { id: "accounts/fireworks/models/glm-4p5", @@ -3673,6 +3506,7 @@ export const MODELS = { api: "anthropic-messages", provider: "fireworks", baseUrl: "https://api.fireworks.ai/inference", + compat: {"sendSessionAffinityHeaders":true,"supportsEagerToolInputStreaming":false,"supportsCacheControlOnTools":false,"supportsLongCacheRetention":false}, reasoning: true, input: ["text"], cost: { @@ -3683,12 +3517,6 @@ export const MODELS = { }, contextWindow: 131072, maxTokens: 131072, - compat: { - sendSessionAffinityHeaders: true, - supportsEagerToolInputStreaming: false, - supportsCacheControlOnTools: false, - supportsLongCacheRetention: false, - }, } satisfies Model<"anthropic-messages">, "accounts/fireworks/models/glm-4p5-air": { id: "accounts/fireworks/models/glm-4p5-air", @@ -3696,6 +3524,7 @@ export const MODELS = { api: "anthropic-messages", provider: "fireworks", baseUrl: "https://api.fireworks.ai/inference", + compat: {"sendSessionAffinityHeaders":true,"supportsEagerToolInputStreaming":false,"supportsCacheControlOnTools":false,"supportsLongCacheRetention":false}, reasoning: true, input: ["text"], cost: { @@ -3706,12 +3535,6 @@ export const MODELS = { }, contextWindow: 131072, maxTokens: 131072, - compat: { - sendSessionAffinityHeaders: true, - supportsEagerToolInputStreaming: false, - supportsCacheControlOnTools: false, - supportsLongCacheRetention: false, - }, } satisfies Model<"anthropic-messages">, "accounts/fireworks/models/glm-4p7": { id: "accounts/fireworks/models/glm-4p7", @@ -3719,6 +3542,7 @@ export const MODELS = { api: "anthropic-messages", provider: "fireworks", baseUrl: "https://api.fireworks.ai/inference", + compat: {"sendSessionAffinityHeaders":true,"supportsEagerToolInputStreaming":false,"supportsCacheControlOnTools":false,"supportsLongCacheRetention":false}, reasoning: true, input: ["text"], cost: { @@ -3729,12 +3553,6 @@ export const MODELS = { }, contextWindow: 198000, maxTokens: 198000, - compat: { - sendSessionAffinityHeaders: true, - supportsEagerToolInputStreaming: false, - supportsCacheControlOnTools: false, - supportsLongCacheRetention: false, - }, } satisfies Model<"anthropic-messages">, "accounts/fireworks/models/glm-5": { id: "accounts/fireworks/models/glm-5", @@ -3742,6 +3560,7 @@ export const MODELS = { api: "anthropic-messages", provider: "fireworks", baseUrl: "https://api.fireworks.ai/inference", + compat: {"sendSessionAffinityHeaders":true,"supportsEagerToolInputStreaming":false,"supportsCacheControlOnTools":false,"supportsLongCacheRetention":false}, reasoning: true, input: ["text"], cost: { @@ -3752,12 +3571,6 @@ export const MODELS = { }, contextWindow: 202752, maxTokens: 131072, - compat: { - sendSessionAffinityHeaders: true, - supportsEagerToolInputStreaming: false, - supportsCacheControlOnTools: false, - supportsLongCacheRetention: false, - }, } satisfies Model<"anthropic-messages">, "accounts/fireworks/models/glm-5p1": { id: "accounts/fireworks/models/glm-5p1", @@ -3765,6 +3578,7 @@ export const MODELS = { api: "anthropic-messages", provider: "fireworks", baseUrl: "https://api.fireworks.ai/inference", + compat: {"sendSessionAffinityHeaders":true,"supportsEagerToolInputStreaming":false,"supportsCacheControlOnTools":false,"supportsLongCacheRetention":false}, reasoning: true, input: ["text"], cost: { @@ -3775,12 +3589,6 @@ export const MODELS = { }, contextWindow: 202800, maxTokens: 131072, - compat: { - sendSessionAffinityHeaders: true, - supportsEagerToolInputStreaming: false, - supportsCacheControlOnTools: false, - supportsLongCacheRetention: false, - }, } satisfies Model<"anthropic-messages">, "accounts/fireworks/models/gpt-oss-120b": { id: "accounts/fireworks/models/gpt-oss-120b", @@ -3788,6 +3596,7 @@ export const MODELS = { api: "anthropic-messages", provider: "fireworks", baseUrl: "https://api.fireworks.ai/inference", + compat: {"sendSessionAffinityHeaders":true,"supportsEagerToolInputStreaming":false,"supportsCacheControlOnTools":false,"supportsLongCacheRetention":false}, reasoning: true, input: ["text"], cost: { @@ -3798,12 +3607,6 @@ export const MODELS = { }, contextWindow: 131072, maxTokens: 32768, - compat: { - sendSessionAffinityHeaders: true, - supportsEagerToolInputStreaming: false, - supportsCacheControlOnTools: false, - supportsLongCacheRetention: false, - }, } satisfies Model<"anthropic-messages">, "accounts/fireworks/models/gpt-oss-20b": { id: "accounts/fireworks/models/gpt-oss-20b", @@ -3811,6 +3614,7 @@ export const MODELS = { api: "anthropic-messages", provider: "fireworks", baseUrl: "https://api.fireworks.ai/inference", + compat: {"sendSessionAffinityHeaders":true,"supportsEagerToolInputStreaming":false,"supportsCacheControlOnTools":false,"supportsLongCacheRetention":false}, reasoning: true, input: ["text"], cost: { @@ -3821,12 +3625,6 @@ export const MODELS = { }, contextWindow: 131072, maxTokens: 32768, - compat: { - sendSessionAffinityHeaders: true, - supportsEagerToolInputStreaming: false, - supportsCacheControlOnTools: false, - supportsLongCacheRetention: false, - }, } satisfies Model<"anthropic-messages">, "accounts/fireworks/models/kimi-k2-instruct": { id: "accounts/fireworks/models/kimi-k2-instruct", @@ -3834,6 +3632,7 @@ export const MODELS = { api: "anthropic-messages", provider: "fireworks", baseUrl: "https://api.fireworks.ai/inference", + compat: {"sendSessionAffinityHeaders":true,"supportsEagerToolInputStreaming":false,"supportsCacheControlOnTools":false,"supportsLongCacheRetention":false}, reasoning: false, input: ["text"], cost: { @@ -3844,12 +3643,6 @@ export const MODELS = { }, contextWindow: 128000, maxTokens: 16384, - compat: { - sendSessionAffinityHeaders: true, - supportsEagerToolInputStreaming: false, - supportsCacheControlOnTools: false, - supportsLongCacheRetention: false, - }, } satisfies Model<"anthropic-messages">, "accounts/fireworks/models/kimi-k2-thinking": { id: "accounts/fireworks/models/kimi-k2-thinking", @@ -3857,6 +3650,7 @@ export const MODELS = { api: "anthropic-messages", provider: "fireworks", baseUrl: "https://api.fireworks.ai/inference", + compat: {"sendSessionAffinityHeaders":true,"supportsEagerToolInputStreaming":false,"supportsCacheControlOnTools":false,"supportsLongCacheRetention":false}, reasoning: true, input: ["text"], cost: { @@ -3867,12 +3661,6 @@ export const MODELS = { }, contextWindow: 256000, maxTokens: 256000, - compat: { - sendSessionAffinityHeaders: true, - supportsEagerToolInputStreaming: false, - supportsCacheControlOnTools: false, - supportsLongCacheRetention: false, - }, } satisfies Model<"anthropic-messages">, "accounts/fireworks/models/kimi-k2p5": { id: "accounts/fireworks/models/kimi-k2p5", @@ -3880,6 +3668,7 @@ export const MODELS = { api: "anthropic-messages", provider: "fireworks", baseUrl: "https://api.fireworks.ai/inference", + compat: {"sendSessionAffinityHeaders":true,"supportsEagerToolInputStreaming":false,"supportsCacheControlOnTools":false,"supportsLongCacheRetention":false}, reasoning: true, input: ["text", "image"], cost: { @@ -3890,12 +3679,6 @@ export const MODELS = { }, contextWindow: 256000, maxTokens: 256000, - compat: { - sendSessionAffinityHeaders: true, - supportsEagerToolInputStreaming: false, - supportsCacheControlOnTools: false, - supportsLongCacheRetention: false, - }, } satisfies Model<"anthropic-messages">, "accounts/fireworks/models/kimi-k2p6": { id: "accounts/fireworks/models/kimi-k2p6", @@ -3903,6 +3686,7 @@ export const MODELS = { api: "anthropic-messages", provider: "fireworks", baseUrl: "https://api.fireworks.ai/inference", + compat: {"sendSessionAffinityHeaders":true,"supportsEagerToolInputStreaming":false,"supportsCacheControlOnTools":false,"supportsLongCacheRetention":false}, reasoning: true, input: ["text", "image"], cost: { @@ -3913,12 +3697,6 @@ export const MODELS = { }, contextWindow: 262000, maxTokens: 262000, - compat: { - sendSessionAffinityHeaders: true, - supportsEagerToolInputStreaming: false, - supportsCacheControlOnTools: false, - supportsLongCacheRetention: false, - }, } satisfies Model<"anthropic-messages">, "accounts/fireworks/models/minimax-m2p1": { id: "accounts/fireworks/models/minimax-m2p1", @@ -3926,6 +3704,7 @@ export const MODELS = { api: "anthropic-messages", provider: "fireworks", baseUrl: "https://api.fireworks.ai/inference", + compat: {"sendSessionAffinityHeaders":true,"supportsEagerToolInputStreaming":false,"supportsCacheControlOnTools":false,"supportsLongCacheRetention":false}, reasoning: true, input: ["text"], cost: { @@ -3936,12 +3715,6 @@ export const MODELS = { }, contextWindow: 200000, maxTokens: 200000, - compat: { - sendSessionAffinityHeaders: true, - supportsEagerToolInputStreaming: false, - supportsCacheControlOnTools: false, - supportsLongCacheRetention: false, - }, } satisfies Model<"anthropic-messages">, "accounts/fireworks/models/minimax-m2p5": { id: "accounts/fireworks/models/minimax-m2p5", @@ -3949,6 +3722,7 @@ export const MODELS = { api: "anthropic-messages", provider: "fireworks", baseUrl: "https://api.fireworks.ai/inference", + compat: {"sendSessionAffinityHeaders":true,"supportsEagerToolInputStreaming":false,"supportsCacheControlOnTools":false,"supportsLongCacheRetention":false}, reasoning: true, input: ["text"], cost: { @@ -3959,12 +3733,6 @@ export const MODELS = { }, contextWindow: 196608, maxTokens: 196608, - compat: { - sendSessionAffinityHeaders: true, - supportsEagerToolInputStreaming: false, - supportsCacheControlOnTools: false, - supportsLongCacheRetention: false, - }, } satisfies Model<"anthropic-messages">, "accounts/fireworks/models/minimax-m2p7": { id: "accounts/fireworks/models/minimax-m2p7", @@ -3972,6 +3740,7 @@ export const MODELS = { api: "anthropic-messages", provider: "fireworks", baseUrl: "https://api.fireworks.ai/inference", + compat: {"sendSessionAffinityHeaders":true,"supportsEagerToolInputStreaming":false,"supportsCacheControlOnTools":false,"supportsLongCacheRetention":false}, reasoning: true, input: ["text"], cost: { @@ -3982,12 +3751,6 @@ export const MODELS = { }, contextWindow: 196608, maxTokens: 196608, - compat: { - sendSessionAffinityHeaders: true, - supportsEagerToolInputStreaming: false, - supportsCacheControlOnTools: false, - supportsLongCacheRetention: false, - }, } satisfies Model<"anthropic-messages">, "accounts/fireworks/models/qwen3p6-plus": { id: "accounts/fireworks/models/qwen3p6-plus", @@ -3995,6 +3758,7 @@ export const MODELS = { api: "anthropic-messages", provider: "fireworks", baseUrl: "https://api.fireworks.ai/inference", + compat: {"sendSessionAffinityHeaders":true,"supportsEagerToolInputStreaming":false,"supportsCacheControlOnTools":false,"supportsLongCacheRetention":false}, reasoning: true, input: ["text", "image"], cost: { @@ -4005,12 +3769,6 @@ export const MODELS = { }, contextWindow: 128000, maxTokens: 8192, - compat: { - sendSessionAffinityHeaders: true, - supportsEagerToolInputStreaming: false, - supportsCacheControlOnTools: false, - supportsLongCacheRetention: false, - }, } satisfies Model<"anthropic-messages">, "accounts/fireworks/routers/kimi-k2p5-turbo": { id: "accounts/fireworks/routers/kimi-k2p5-turbo", @@ -4018,6 +3776,7 @@ export const MODELS = { api: "anthropic-messages", provider: "fireworks", baseUrl: "https://api.fireworks.ai/inference", + compat: {"sendSessionAffinityHeaders":true,"supportsEagerToolInputStreaming":false,"supportsCacheControlOnTools":false,"supportsLongCacheRetention":false}, reasoning: true, input: ["text", "image"], cost: { @@ -4028,12 +3787,6 @@ export const MODELS = { }, contextWindow: 256000, maxTokens: 256000, - compat: { - sendSessionAffinityHeaders: true, - supportsEagerToolInputStreaming: false, - supportsCacheControlOnTools: false, - supportsLongCacheRetention: false, - }, } satisfies Model<"anthropic-messages">, }, "github-copilot": { @@ -4112,25 +3865,6 @@ export const MODELS = { contextWindow: 144000, maxTokens: 64000, } satisfies Model<"anthropic-messages">, - "claude-sonnet-4": { - id: "claude-sonnet-4", - name: "Claude Sonnet 4", - api: "anthropic-messages", - provider: "github-copilot", - baseUrl: "https://api.individual.githubcopilot.com", - headers: {"User-Agent":"GitHubCopilotChat/0.35.0","Editor-Version":"vscode/1.107.0","Editor-Plugin-Version":"copilot-chat/0.35.0","Copilot-Integration-Id":"vscode-chat"}, - compat: {"supportsEagerToolInputStreaming":false}, - reasoning: true, - input: ["text", "image"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 216000, - maxTokens: 16000, - } satisfies Model<"anthropic-messages">, "claude-sonnet-4.5": { id: "claude-sonnet-4.5", name: "Claude Sonnet 4.5", @@ -4206,25 +3940,6 @@ export const MODELS = { contextWindow: 128000, maxTokens: 64000, } satisfies Model<"openai-completions">, - "gemini-3-pro-preview": { - id: "gemini-3-pro-preview", - name: "Gemini 3 Pro Preview", - api: "openai-completions", - provider: "github-copilot", - baseUrl: "https://api.individual.githubcopilot.com", - headers: {"User-Agent":"GitHubCopilotChat/0.35.0","Editor-Version":"vscode/1.107.0","Editor-Plugin-Version":"copilot-chat/0.35.0","Copilot-Integration-Id":"vscode-chat"}, - compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false}, - reasoning: true, - input: ["text", "image"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 64000, - } satisfies Model<"openai-completions">, "gemini-3.1-pro-preview": { id: "gemini-3.1-pro-preview", name: "Gemini 3.1 Pro Preview", @@ -4282,25 +3997,6 @@ export const MODELS = { contextWindow: 128000, maxTokens: 4096, } satisfies Model<"openai-completions">, - "gpt-5": { - id: "gpt-5", - name: "GPT-5", - api: "openai-responses", - provider: "github-copilot", - baseUrl: "https://api.individual.githubcopilot.com", - headers: {"User-Agent":"GitHubCopilotChat/0.35.0","Editor-Version":"vscode/1.107.0","Editor-Plugin-Version":"copilot-chat/0.35.0","Copilot-Integration-Id":"vscode-chat"}, - reasoning: true, - thinkingLevelMap: {"off":null}, - input: ["text", "image"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 128000, - } satisfies Model<"openai-responses">, "gpt-5-mini": { id: "gpt-5-mini", name: "GPT-5-mini", @@ -4320,82 +4016,6 @@ export const MODELS = { contextWindow: 264000, maxTokens: 64000, } satisfies Model<"openai-responses">, - "gpt-5.1": { - id: "gpt-5.1", - name: "GPT-5.1", - api: "openai-responses", - provider: "github-copilot", - baseUrl: "https://api.individual.githubcopilot.com", - headers: {"User-Agent":"GitHubCopilotChat/0.35.0","Editor-Version":"vscode/1.107.0","Editor-Plugin-Version":"copilot-chat/0.35.0","Copilot-Integration-Id":"vscode-chat"}, - reasoning: true, - thinkingLevelMap: {"off":null}, - input: ["text", "image"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 264000, - maxTokens: 64000, - } satisfies Model<"openai-responses">, - "gpt-5.1-codex": { - id: "gpt-5.1-codex", - name: "GPT-5.1-Codex", - api: "openai-responses", - provider: "github-copilot", - baseUrl: "https://api.individual.githubcopilot.com", - headers: {"User-Agent":"GitHubCopilotChat/0.35.0","Editor-Version":"vscode/1.107.0","Editor-Plugin-Version":"copilot-chat/0.35.0","Copilot-Integration-Id":"vscode-chat"}, - reasoning: true, - thinkingLevelMap: {"off":null}, - input: ["text", "image"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"openai-responses">, - "gpt-5.1-codex-max": { - id: "gpt-5.1-codex-max", - name: "GPT-5.1-Codex-max", - api: "openai-responses", - provider: "github-copilot", - baseUrl: "https://api.individual.githubcopilot.com", - headers: {"User-Agent":"GitHubCopilotChat/0.35.0","Editor-Version":"vscode/1.107.0","Editor-Plugin-Version":"copilot-chat/0.35.0","Copilot-Integration-Id":"vscode-chat"}, - reasoning: true, - thinkingLevelMap: {"off":null}, - input: ["text", "image"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"openai-responses">, - "gpt-5.1-codex-mini": { - id: "gpt-5.1-codex-mini", - name: "GPT-5.1-Codex-mini", - api: "openai-responses", - provider: "github-copilot", - baseUrl: "https://api.individual.githubcopilot.com", - headers: {"User-Agent":"GitHubCopilotChat/0.35.0","Editor-Version":"vscode/1.107.0","Editor-Plugin-Version":"copilot-chat/0.35.0","Copilot-Integration-Id":"vscode-chat"}, - reasoning: true, - thinkingLevelMap: {"off":null}, - input: ["text", "image"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"openai-responses">, "gpt-5.2": { id: "gpt-5.2", name: "GPT-5.2", @@ -4644,7 +4264,7 @@ export const MODELS = { cost: { input: 0.1, output: 0.4, - cacheRead: 0.025, + cacheRead: 0.01, cacheWrite: 0, }, contextWindow: 1048576, @@ -4835,7 +4455,7 @@ export const MODELS = { input: 0.25, output: 1.5, cacheRead: 0.025, - cacheWrite: 1, + cacheWrite: 0, }, contextWindow: 1048576, maxTokens: 65536, @@ -4853,7 +4473,7 @@ export const MODELS = { input: 0.25, output: 1.5, cacheRead: 0.025, - cacheWrite: 1, + cacheWrite: 0, }, contextWindow: 1048576, maxTokens: 65536, @@ -7870,6 +7490,25 @@ export const MODELS = { contextWindow: 1000000, maxTokens: 64000, } satisfies Model<"anthropic-messages">, + "deepseek-v4-flash-free": { + id: "deepseek-v4-flash-free", + name: "DeepSeek V4 Flash Free", + api: "openai-completions", + provider: "opencode", + baseUrl: "https://opencode.ai/zen/v1", + compat: {"requiresReasoningContentOnAssistantMessages":true,"thinkingFormat":"deepseek"}, + reasoning: true, + thinkingLevelMap: {"minimal":null,"low":null,"medium":null,"high":"high","xhigh":"max"}, + input: ["text"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 200000, + maxTokens: 128000, + } satisfies Model<"openai-completions">, "gemini-3-flash": { id: "gemini-3-flash", name: "Gemini 3 Flash", @@ -8228,23 +7867,6 @@ export const MODELS = { contextWindow: 1050000, maxTokens: 128000, } satisfies Model<"openai-responses">, - "hy3-preview-free": { - id: "hy3-preview-free", - name: "Hy3 preview Free", - api: "openai-completions", - provider: "opencode", - baseUrl: "https://opencode.ai/zen/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 256000, - maxTokens: 64000, - } satisfies Model<"openai-completions">, "kimi-k2.5": { id: "kimi-k2.5", name: "Kimi K2.5", @@ -8381,6 +8003,23 @@ export const MODELS = { contextWindow: 262144, maxTokens: 65536, } satisfies Model<"anthropic-messages">, + "qwen3.6-plus-free": { + id: "qwen3.6-plus-free", + name: "Qwen3.6 Plus Free", + api: "anthropic-messages", + provider: "opencode", + baseUrl: "https://opencode.ai/zen", + reasoning: true, + input: ["text", "image"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 65536, + } satisfies Model<"anthropic-messages">, }, "opencode-go": { "deepseek-v4-flash": { @@ -8526,9 +8165,9 @@ export const MODELS = { "minimax-m2.5": { id: "minimax-m2.5", name: "MiniMax M2.5", - api: "openai-completions", + api: "anthropic-messages", provider: "opencode-go", - baseUrl: "https://opencode.ai/zen/go/v1", + baseUrl: "https://opencode.ai/zen/go", reasoning: true, input: ["text"], cost: { @@ -8539,7 +8178,7 @@ export const MODELS = { }, contextWindow: 204800, maxTokens: 65536, - } satisfies Model<"openai-completions">, + } satisfies Model<"anthropic-messages">, "minimax-m2.7": { id: "minimax-m2.7", name: "MiniMax M2.7", @@ -8748,40 +8387,6 @@ export const MODELS = { contextWindow: 200000, maxTokens: 8192, } satisfies Model<"openai-completions">, - "anthropic/claude-3.7-sonnet": { - id: "anthropic/claude-3.7-sonnet", - name: "Anthropic: Claude 3.7 Sonnet", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 3, - output: 15, - cacheRead: 0.3, - cacheWrite: 3.75, - }, - contextWindow: 200000, - maxTokens: 64000, - } satisfies Model<"openai-completions">, - "anthropic/claude-3.7-sonnet:thinking": { - id: "anthropic/claude-3.7-sonnet:thinking", - name: "Anthropic: Claude 3.7 Sonnet (thinking)", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 3, - output: 15, - cacheRead: 0.3, - cacheWrite: 3.75, - }, - contextWindow: 200000, - maxTokens: 64000, - } satisfies Model<"openai-completions">, "anthropic/claude-haiku-4.5": { id: "anthropic/claude-haiku-4.5", name: "Anthropic: Claude Haiku 4.5", @@ -8904,6 +8509,24 @@ export const MODELS = { contextWindow: 1000000, maxTokens: 128000, } satisfies Model<"openai-completions">, + "anthropic/claude-opus-4.7-fast": { + id: "anthropic/claude-opus-4.7-fast", + name: "Anthropic: Claude Opus 4.7 (Fast)", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + thinkingLevelMap: {"xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 30, + output: 150, + cacheRead: 3, + cacheWrite: 37.5, + }, + contextWindow: 1000000, + maxTokens: 128000, + } satisfies Model<"openai-completions">, "anthropic/claude-sonnet-4": { id: "anthropic/claude-sonnet-4", name: "Anthropic: Claude Sonnet 4", @@ -8989,6 +8612,23 @@ export const MODELS = { contextWindow: 262144, maxTokens: 262144, } satisfies Model<"openai-completions">, + "arcee-ai/trinity-large-thinking:free": { + id: "arcee-ai/trinity-large-thinking:free", + name: "Arcee AI: Trinity Large Thinking (free)", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + input: ["text"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 80000, + } satisfies Model<"openai-completions">, "arcee-ai/trinity-mini": { id: "arcee-ai/trinity-mini", name: "Arcee AI: Trinity Mini", @@ -9071,7 +8711,7 @@ export const MODELS = { cacheRead: 0, cacheWrite: 0, }, - contextWindow: 120000, + contextWindow: 131072, maxTokens: 8000, } satisfies Model<"openai-completions">, "baidu/ernie-4.5-vl-28b-a3b": { @@ -9088,7 +8728,7 @@ export const MODELS = { cacheRead: 0, cacheWrite: 0, }, - contextWindow: 30000, + contextWindow: 131072, maxTokens: 8000, } satisfies Model<"openai-completions">, "bytedance-seed/seed-1.6": { @@ -9236,13 +8876,13 @@ export const MODELS = { reasoning: true, input: ["text"], cost: { - input: 0.15, - output: 0.75, - cacheRead: 0, + input: 0.21, + output: 0.7899999999999999, + cacheRead: 0.13, cacheWrite: 0, }, - contextWindow: 32768, - maxTokens: 7168, + contextWindow: 163840, + maxTokens: 32768, } satisfies Model<"openai-completions">, "deepseek/deepseek-r1": { id: "deepseek/deepseek-r1", @@ -9258,7 +8898,7 @@ export const MODELS = { cacheRead: 0, cacheWrite: 0, }, - contextWindow: 64000, + contextWindow: 163840, maxTokens: 16000, } satisfies Model<"openai-completions">, "deepseek/deepseek-r1-0528": { @@ -9340,9 +8980,28 @@ export const MODELS = { thinkingLevelMap: {"minimal":null,"low":null,"medium":null,"high":"high","xhigh":"max"}, input: ["text"], cost: { - input: 0.14, - output: 0.28, - cacheRead: 0.0028, + input: 0.112, + output: 0.224, + cacheRead: 0.022, + cacheWrite: 0, + }, + contextWindow: 1048576, + maxTokens: 4096, + } satisfies Model<"openai-completions">, + "deepseek/deepseek-v4-flash:free": { + id: "deepseek/deepseek-v4-flash:free", + name: "DeepSeek: DeepSeek V4 Flash (free)", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"requiresReasoningContentOnAssistantMessages":true,"thinkingFormat":"deepseek"}, + reasoning: true, + thinkingLevelMap: {"minimal":null,"low":null,"medium":null,"high":"high","xhigh":"max"}, + input: ["text"], + cost: { + input: 0, + output: 0, + cacheRead: 0, cacheWrite: 0, }, contextWindow: 1048576, @@ -9602,7 +9261,7 @@ export const MODELS = { cacheRead: 0.19999999999999998, cacheWrite: 0.375, }, - contextWindow: 1048576, + contextWindow: 1048756, maxTokens: 65536, } satisfies Model<"openai-completions">, "google/gemma-3-12b-it": { @@ -9648,13 +9307,13 @@ export const MODELS = { reasoning: true, input: ["text", "image"], cost: { - input: 0.06, - output: 0.33, + input: 0.07, + output: 0.33999999999999997, cacheRead: 0, cacheWrite: 0, }, contextWindow: 262144, - maxTokens: 4096, + maxTokens: 16384, } satisfies Model<"openai-completions">, "google/gemma-4-26b-a4b-it:free": { id: "google/gemma-4-26b-a4b-it:free", @@ -9682,8 +9341,8 @@ export const MODELS = { reasoning: true, input: ["text", "image"], cost: { - input: 0.13, - output: 0.38, + input: 0.12, + output: 0.37, cacheRead: 0, cacheWrite: 0, }, @@ -9768,14 +9427,31 @@ export const MODELS = { reasoning: false, input: ["text"], cost: { - input: 0.08, - output: 0.24, - cacheRead: 0.016, + input: 0.01, + output: 0.03, + cacheRead: 0.002, cacheWrite: 0, }, contextWindow: 262144, maxTokens: 32768, } satisfies Model<"openai-completions">, + "inclusionai/ring-2.6-1t": { + id: "inclusionai/ring-2.6-1t", + name: "inclusionAI: Ring-2.6-1T", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + input: ["text"], + cost: { + input: 0.075, + output: 0.625, + cacheRead: 0.015, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 65536, + } satisfies Model<"openai-completions">, "kwaipilot/kat-coder-pro-v2": { id: "kwaipilot/kat-coder-pro-v2", name: "Kwaipilot: KAT-Coder-Pro V2", @@ -9824,7 +9500,7 @@ export const MODELS = { cacheRead: 0, cacheWrite: 0, }, - contextWindow: 16384, + contextWindow: 131072, maxTokens: 16384, } satisfies Model<"openai-completions">, "meta-llama/llama-3.3-70b-instruct": { @@ -9858,7 +9534,7 @@ export const MODELS = { cacheRead: 0, cacheWrite: 0, }, - contextWindow: 65536, + contextWindow: 131072, maxTokens: 4096, } satisfies Model<"openai-completions">, "meta-llama/llama-4-scout": { @@ -9875,7 +9551,7 @@ export const MODELS = { cacheRead: 0, cacheWrite: 0, }, - contextWindow: 327680, + contextWindow: 10000000, maxTokens: 16384, } satisfies Model<"openai-completions">, "minimax/minimax-m1": { @@ -9909,7 +9585,7 @@ export const MODELS = { cacheRead: 0.03, cacheWrite: 0, }, - contextWindow: 196608, + contextWindow: 204800, maxTokens: 196608, } satisfies Model<"openai-completions">, "minimax/minimax-m2.1": { @@ -9926,7 +9602,7 @@ export const MODELS = { cacheRead: 0.03, cacheWrite: 0, }, - contextWindow: 196608, + contextWindow: 204800, maxTokens: 196608, } satisfies Model<"openai-completions">, "minimax/minimax-m2.5": { @@ -9940,11 +9616,11 @@ export const MODELS = { cost: { input: 0.15, output: 1.15, - cacheRead: 0.03, + cacheRead: 0, cacheWrite: 0, }, - contextWindow: 196608, - maxTokens: 131072, + contextWindow: 204800, + maxTokens: 196608, } satisfies Model<"openai-completions">, "minimax/minimax-m2.5:free": { id: "minimax/minimax-m2.5:free", @@ -9960,7 +9636,7 @@ export const MODELS = { cacheRead: 0, cacheWrite: 0, }, - contextWindow: 196608, + contextWindow: 204800, maxTokens: 8192, } satisfies Model<"openai-completions">, "minimax/minimax-m2.7": { @@ -9972,13 +9648,13 @@ export const MODELS = { reasoning: true, input: ["text"], cost: { - input: 0.3, + input: 0.27899999999999997, output: 1.2, - cacheRead: 0.059, + cacheRead: 0, cacheWrite: 0, }, - contextWindow: 196608, - maxTokens: 4096, + contextWindow: 204800, + maxTokens: 131072, } satisfies Model<"openai-completions">, "mistralai/codestral-2508": { id: "mistralai/codestral-2508", @@ -10228,12 +9904,12 @@ export const MODELS = { input: ["text"], cost: { input: 0.02, - output: 0.03, + output: 0.04, cacheRead: 0, cacheWrite: 0, }, contextWindow: 131072, - maxTokens: 4096, + maxTokens: 16384, } satisfies Model<"openai-completions">, "mistralai/mistral-saba": { id: "mistralai/mistral-saba", @@ -10363,8 +10039,8 @@ export const MODELS = { reasoning: false, input: ["text"], cost: { - input: 0.39999999999999997, - output: 2, + input: 0.6, + output: 2.5, cacheRead: 0, cacheWrite: 0, }, @@ -10382,7 +10058,7 @@ export const MODELS = { cost: { input: 0.6, output: 2.5, - cacheRead: 0.15, + cacheRead: 0, cacheWrite: 0, }, contextWindow: 262144, @@ -10414,13 +10090,13 @@ export const MODELS = { reasoning: true, input: ["text", "image"], cost: { - input: 0.75, - output: 3.5, - cacheRead: 0.15, + input: 0.73, + output: 3.49, + cacheRead: 0.25, cacheWrite: 0, }, contextWindow: 262144, - maxTokens: 16384, + maxTokens: 262142, } satisfies Model<"openai-completions">, "nex-agi/deepseek-v3.1-nex-n1": { id: "nex-agi/deepseek-v3.1-nex-n1", @@ -10516,13 +10192,13 @@ export const MODELS = { reasoning: true, input: ["text"], cost: { - input: 0.09, - output: 0.44999999999999996, + input: 0.09999999999999999, + output: 0.5, cacheRead: 0, cacheWrite: 0, }, - contextWindow: 262144, - maxTokens: 4096, + contextWindow: 1000000, + maxTokens: 16384, } satisfies Model<"openai-completions">, "nvidia/nemotron-3-super-120b-a12b:free": { id: "nvidia/nemotron-3-super-120b-a12b:free", @@ -10538,7 +10214,7 @@ export const MODELS = { cacheRead: 0, cacheWrite: 0, }, - contextWindow: 262144, + contextWindow: 1000000, maxTokens: 262144, } satisfies Model<"openai-completions">, "nvidia/nemotron-nano-12b-v2-vl:free": { @@ -11345,12 +11021,12 @@ export const MODELS = { input: ["text"], cost: { input: 0.039, - output: 0.18, + output: 0.19, cacheRead: 0, cacheWrite: 0, }, contextWindow: 131072, - maxTokens: 4096, + maxTokens: 131072, } satisfies Model<"openai-completions">, "openai/gpt-oss-120b:free": { id: "openai/gpt-oss-120b:free", @@ -11689,7 +11365,7 @@ export const MODELS = { cacheRead: 0, cacheWrite: 0, }, - contextWindow: 32768, + contextWindow: 131072, maxTokens: 16384, } satisfies Model<"openai-completions">, "qwen/qwen-2.5-7b-instruct": { @@ -11706,26 +11382,9 @@ export const MODELS = { cacheRead: 0, cacheWrite: 0, }, - contextWindow: 32768, + contextWindow: 131072, maxTokens: 32768, } satisfies Model<"openai-completions">, - "qwen/qwen-max": { - id: "qwen/qwen-max", - name: "Qwen: Qwen-Max ", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 1.04, - output: 4.16, - cacheRead: 0.20800000000000002, - cacheWrite: 0, - }, - contextWindow: 32768, - maxTokens: 8192, - } satisfies Model<"openai-completions">, "qwen/qwen-plus": { id: "qwen/qwen-plus", name: "Qwen: Qwen-Plus", @@ -11777,40 +11436,6 @@ export const MODELS = { contextWindow: 1000000, maxTokens: 32768, } satisfies Model<"openai-completions">, - "qwen/qwen-turbo": { - id: "qwen/qwen-turbo", - name: "Qwen: Qwen-Turbo", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 0.0325, - output: 0.13, - cacheRead: 0.006500000000000001, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 8192, - } satisfies Model<"openai-completions">, - "qwen/qwen-vl-max": { - id: "qwen/qwen-vl-max", - name: "Qwen: Qwen VL Max", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.52, - output: 2.08, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 32768, - } satisfies Model<"openai-completions">, "qwen/qwen3-14b": { id: "qwen/qwen3-14b", name: "Qwen: Qwen3 14B", @@ -11820,12 +11445,12 @@ export const MODELS = { reasoning: true, input: ["text"], cost: { - input: 0.06, + input: 0.09999999999999999, output: 0.24, cacheRead: 0, cacheWrite: 0, }, - contextWindow: 40960, + contextWindow: 131702, maxTokens: 40960, } satisfies Model<"openai-completions">, "qwen/qwen3-235b-a22b": { @@ -11876,7 +11501,7 @@ export const MODELS = { cacheRead: 0, cacheWrite: 0, }, - contextWindow: 131072, + contextWindow: 262144, maxTokens: 4096, } satisfies Model<"openai-completions">, "qwen/qwen3-30b-a3b": { @@ -11893,7 +11518,7 @@ export const MODELS = { cacheRead: 0, cacheWrite: 0, }, - contextWindow: 40960, + contextWindow: 131072, maxTokens: 20000, } satisfies Model<"openai-completions">, "qwen/qwen3-30b-a3b-instruct-2507": { @@ -11940,12 +11565,12 @@ export const MODELS = { input: ["text"], cost: { input: 0.08, - output: 0.24, - cacheRead: 0.04, + output: 0.28, + cacheRead: 0, cacheWrite: 0, }, - contextWindow: 40960, - maxTokens: 40960, + contextWindow: 131072, + maxTokens: 16384, } satisfies Model<"openai-completions">, "qwen/qwen3-8b": { id: "qwen/qwen3-8b", @@ -11961,7 +11586,7 @@ export const MODELS = { cacheRead: 0.049999999999999996, cacheWrite: 0, }, - contextWindow: 40960, + contextWindow: 131072, maxTokens: 8192, } satisfies Model<"openai-completions">, "qwen/qwen3-coder": { @@ -11978,7 +11603,7 @@ export const MODELS = { cacheRead: 0, cacheWrite: 0, }, - contextWindow: 262144, + contextWindow: 1048576, maxTokens: 65536, } satisfies Model<"openai-completions">, "qwen/qwen3-coder-30b-a3b-instruct": { @@ -12063,7 +11688,7 @@ export const MODELS = { cacheRead: 0, cacheWrite: 0, }, - contextWindow: 262000, + contextWindow: 1048576, maxTokens: 262000, } satisfies Model<"openai-completions">, "qwen/qwen3-max": { @@ -12148,7 +11773,7 @@ export const MODELS = { cacheRead: 0, cacheWrite: 0, }, - contextWindow: 131072, + contextWindow: 262144, maxTokens: 32768, } satisfies Model<"openai-completions">, "qwen/qwen3-vl-235b-a22b-instruct": { @@ -12199,7 +11824,7 @@ export const MODELS = { cacheRead: 0, cacheWrite: 0, }, - contextWindow: 131072, + contextWindow: 262144, maxTokens: 32768, } satisfies Model<"openai-completions">, "qwen/qwen3-vl-30b-a3b-thinking": { @@ -12233,7 +11858,7 @@ export const MODELS = { cacheRead: 0, cacheWrite: 0, }, - contextWindow: 131072, + contextWindow: 262144, maxTokens: 32768, } satisfies Model<"openai-completions">, "qwen/qwen3-vl-8b-instruct": { @@ -12250,7 +11875,7 @@ export const MODELS = { cacheRead: 0, cacheWrite: 0, }, - contextWindow: 131072, + contextWindow: 256000, maxTokens: 32768, } satisfies Model<"openai-completions">, "qwen/qwen3-vl-8b-thinking": { @@ -12267,7 +11892,7 @@ export const MODELS = { cacheRead: 0, cacheWrite: 0, }, - contextWindow: 131072, + contextWindow: 256000, maxTokens: 32768, } satisfies Model<"openai-completions">, "qwen/qwen3.5-122b-a10b": { @@ -12398,8 +12023,8 @@ export const MODELS = { reasoning: true, input: ["text", "image"], cost: { - input: 0.39999999999999997, - output: 2.4, + input: 0.3, + output: 1.7999999999999998, cacheRead: 0, cacheWrite: 0, }, @@ -12449,10 +12074,10 @@ export const MODELS = { reasoning: true, input: ["text", "image"], cost: { - input: 0.25, - output: 1.5, + input: 0.1875, + output: 1.125, cacheRead: 0, - cacheWrite: 0.3125, + cacheWrite: 0.234375, }, contextWindow: 1000000, maxTokens: 65536, @@ -12576,18 +12201,18 @@ export const MODELS = { contextWindow: 262144, maxTokens: 65536, } satisfies Model<"openai-completions">, - "tencent/hy3-preview:free": { - id: "tencent/hy3-preview:free", - name: "Tencent: Hy3 preview (free)", + "tencent/hy3-preview": { + id: "tencent/hy3-preview", + name: "Tencent: Hy3 preview", api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", reasoning: true, input: ["text"], cost: { - input: 0, - output: 0, - cacheRead: 0, + input: 0.06599999999999999, + output: 0.26, + cacheRead: 0.029, cacheWrite: 0, }, contextWindow: 262144, @@ -12627,23 +12252,6 @@ export const MODELS = { contextWindow: 32768, maxTokens: 32768, } satisfies Model<"openai-completions">, - "tngtech/deepseek-r1t2-chimera": { - id: "tngtech/deepseek-r1t2-chimera", - name: "TNG: DeepSeek R1T2 Chimera", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0.3, - output: 1.1, - cacheRead: 0.15, - cacheWrite: 0, - }, - contextWindow: 163840, - maxTokens: 163840, - } satisfies Model<"openai-completions">, "upstage/solar-pro-3": { id: "upstage/solar-pro-3", name: "Upstage: Solar Pro 3", @@ -12661,125 +12269,6 @@ export const MODELS = { contextWindow: 128000, maxTokens: 4096, } satisfies Model<"openai-completions">, - "x-ai/grok-3": { - id: "x-ai/grok-3", - name: "xAI: Grok 3", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 3, - output: 15, - cacheRead: 0.75, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 4096, - } satisfies Model<"openai-completions">, - "x-ai/grok-3-beta": { - id: "x-ai/grok-3-beta", - name: "xAI: Grok 3 Beta", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 3, - output: 15, - cacheRead: 0.75, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 4096, - } satisfies Model<"openai-completions">, - "x-ai/grok-3-mini": { - id: "x-ai/grok-3-mini", - name: "xAI: Grok 3 Mini", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0.3, - output: 0.5, - cacheRead: 0.075, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 4096, - } satisfies Model<"openai-completions">, - "x-ai/grok-3-mini-beta": { - id: "x-ai/grok-3-mini-beta", - name: "xAI: Grok 3 Mini Beta", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0.3, - output: 0.5, - cacheRead: 0.075, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 4096, - } satisfies Model<"openai-completions">, - "x-ai/grok-4": { - id: "x-ai/grok-4", - name: "xAI: Grok 4", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 3, - output: 15, - cacheRead: 0.75, - cacheWrite: 0, - }, - contextWindow: 256000, - maxTokens: 4096, - } satisfies Model<"openai-completions">, - "x-ai/grok-4-fast": { - id: "x-ai/grok-4-fast", - name: "xAI: Grok 4 Fast", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.19999999999999998, - output: 0.5, - cacheRead: 0.049999999999999996, - cacheWrite: 0, - }, - contextWindow: 2000000, - maxTokens: 30000, - } satisfies Model<"openai-completions">, - "x-ai/grok-4.1-fast": { - id: "x-ai/grok-4.1-fast", - name: "xAI: Grok 4.1 Fast", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.19999999999999998, - output: 0.5, - cacheRead: 0.049999999999999996, - cacheWrite: 0, - }, - contextWindow: 2000000, - maxTokens: 30000, - } satisfies Model<"openai-completions">, "x-ai/grok-4.20": { id: "x-ai/grok-4.20", name: "xAI: Grok 4.20", @@ -12814,23 +12303,6 @@ export const MODELS = { contextWindow: 1000000, maxTokens: 4096, } satisfies Model<"openai-completions">, - "x-ai/grok-code-fast-1": { - id: "x-ai/grok-code-fast-1", - name: "xAI: Grok Code Fast 1", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0.19999999999999998, - output: 1.5, - cacheRead: 0.02, - cacheWrite: 0, - }, - contextWindow: 256000, - maxTokens: 10000, - } satisfies Model<"openai-completions">, "xiaomi/mimo-v2-flash": { id: "xiaomi/mimo-v2-flash", name: "Xiaomi: MiMo-V2-Flash", @@ -12840,9 +12312,9 @@ export const MODELS = { reasoning: true, input: ["text"], cost: { - input: 0.09, - output: 0.29, - cacheRead: 0.045, + input: 0.09999999999999999, + output: 0.3, + cacheRead: 0.01, cacheWrite: 0, }, contextWindow: 262144, @@ -12914,7 +12386,7 @@ export const MODELS = { cacheWrite: 0, }, contextWindow: 1048576, - maxTokens: 131072, + maxTokens: 16384, } satisfies Model<"openai-completions">, "z-ai/glm-4-32b": { id: "z-ai/glm-4-32b", @@ -13010,13 +12482,13 @@ export const MODELS = { reasoning: true, input: ["text"], cost: { - input: 0.39, - output: 1.9, - cacheRead: 0, + input: 0.43, + output: 1.74, + cacheRead: 0.08, cacheWrite: 0, }, - contextWindow: 204800, - maxTokens: 204800, + contextWindow: 202752, + maxTokens: 131072, } satisfies Model<"openai-completions">, "z-ai/glm-4.6v": { id: "z-ai/glm-4.6v", @@ -13112,13 +12584,13 @@ export const MODELS = { reasoning: true, input: ["text"], cost: { - input: 1.0499999999999998, - output: 3.5, - cacheRead: 0.5249999999999999, + input: 0.98, + output: 3.08, + cacheRead: 0.182, cacheWrite: 0, }, contextWindow: 202752, - maxTokens: 65535, + maxTokens: 4096, } satisfies Model<"openai-completions">, "z-ai/glm-5v-turbo": { id: "z-ai/glm-5v-turbo", @@ -13231,13 +12703,13 @@ export const MODELS = { reasoning: true, input: ["text", "image"], cost: { - input: 0.75, - output: 3.5, - cacheRead: 0.15, + input: 0.73, + output: 3.49, + cacheRead: 0.25, cacheWrite: 0, }, contextWindow: 262144, - maxTokens: 16384, + maxTokens: 262142, } satisfies Model<"openai-completions">, "~openai/gpt-latest": { id: "~openai/gpt-latest", @@ -13937,23 +13409,6 @@ export const MODELS = { contextWindow: 200000, maxTokens: 8192, } satisfies Model<"anthropic-messages">, - "anthropic/claude-3.7-sonnet": { - id: "anthropic/claude-3.7-sonnet", - name: "Claude 3.7 Sonnet", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text", "image"], - cost: { - input: 3, - output: 15, - cacheRead: 0.3, - cacheWrite: 3.75, - }, - contextWindow: 200000, - maxTokens: 8192, - } satisfies Model<"anthropic-messages">, "anthropic/claude-haiku-4.5": { id: "anthropic/claude-haiku-4.5", name: "Claude Haiku 4.5", @@ -15636,7 +15091,7 @@ export const MODELS = { } satisfies Model<"anthropic-messages">, "openai/gpt-oss-20b": { id: "openai/gpt-oss-20b", - name: "GPT OSS 120B", + name: "GPT OSS 20B", api: "anthropic-messages", provider: "vercel-ai-gateway", baseUrl: "https://ai-gateway.vercel.sh", @@ -15804,125 +15259,6 @@ export const MODELS = { contextWindow: 200000, maxTokens: 8000, } satisfies Model<"anthropic-messages">, - "xai/grok-3": { - id: "xai/grok-3", - name: "Grok 3 Beta", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: false, - input: ["text"], - cost: { - input: 3, - output: 15, - cacheRead: 0.75, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 131072, - } satisfies Model<"anthropic-messages">, - "xai/grok-3-fast": { - id: "xai/grok-3-fast", - name: "Grok 3 Fast Beta", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: false, - input: ["text"], - cost: { - input: 5, - output: 25, - cacheRead: 1.25, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 131072, - } satisfies Model<"anthropic-messages">, - "xai/grok-3-mini": { - id: "xai/grok-3-mini", - name: "Grok 3 Mini Beta", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: false, - input: ["text"], - cost: { - input: 0.3, - output: 0.5, - cacheRead: 0.075, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 131072, - } satisfies Model<"anthropic-messages">, - "xai/grok-3-mini-fast": { - id: "xai/grok-3-mini-fast", - name: "Grok 3 Mini Fast Beta", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: false, - input: ["text"], - cost: { - input: 0.6, - output: 4, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 131072, - } satisfies Model<"anthropic-messages">, - "xai/grok-4": { - id: "xai/grok-4", - name: "Grok 4", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text", "image"], - cost: { - input: 3, - output: 15, - cacheRead: 0.75, - cacheWrite: 0, - }, - contextWindow: 256000, - maxTokens: 256000, - } satisfies Model<"anthropic-messages">, - "xai/grok-4-fast-non-reasoning": { - id: "xai/grok-4-fast-non-reasoning", - name: "Grok 4 Fast Non-Reasoning", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.19999999999999998, - output: 0.5, - cacheRead: 0.049999999999999996, - cacheWrite: 0, - }, - contextWindow: 2000000, - maxTokens: 256000, - } satisfies Model<"anthropic-messages">, - "xai/grok-4-fast-reasoning": { - id: "xai/grok-4-fast-reasoning", - name: "Grok 4 Fast Reasoning", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.19999999999999998, - output: 0.5, - cacheRead: 0.049999999999999996, - cacheWrite: 0, - }, - contextWindow: 2000000, - maxTokens: 256000, - } satisfies Model<"anthropic-messages">, "xai/grok-4.1-fast-non-reasoning": { id: "xai/grok-4.1-fast-non-reasoning", name: "Grok 4.1 Fast Non-Reasoning", @@ -15937,8 +15273,8 @@ export const MODELS = { cacheRead: 0.049999999999999996, cacheWrite: 0, }, - contextWindow: 2000000, - maxTokens: 30000, + contextWindow: 1000000, + maxTokens: 1000000, } satisfies Model<"anthropic-messages">, "xai/grok-4.1-fast-reasoning": { id: "xai/grok-4.1-fast-reasoning", @@ -15954,8 +15290,8 @@ export const MODELS = { cacheRead: 0.049999999999999996, cacheWrite: 0, }, - contextWindow: 2000000, - maxTokens: 30000, + contextWindow: 1000000, + maxTokens: 1000000, } satisfies Model<"anthropic-messages">, "xai/grok-4.20-multi-agent": { id: "xai/grok-4.20-multi-agent", @@ -16076,23 +15412,6 @@ export const MODELS = { contextWindow: 1000000, maxTokens: 1000000, } satisfies Model<"anthropic-messages">, - "xai/grok-code-fast-1": { - id: "xai/grok-code-fast-1", - name: "Grok Code Fast 1", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text"], - cost: { - input: 0.19999999999999998, - output: 1.5, - cacheRead: 0.02, - cacheWrite: 0, - }, - contextWindow: 256000, - maxTokens: 256000, - } satisfies Model<"anthropic-messages">, "xiaomi/mimo-v2-flash": { id: "xiaomi/mimo-v2-flash", name: "MiMo V2 Flash", @@ -16520,193 +15839,6 @@ export const MODELS = { contextWindow: 131072, maxTokens: 8192, } satisfies Model<"openai-completions">, - "grok-3-fast-latest": { - id: "grok-3-fast-latest", - name: "Grok 3 Fast Latest", - api: "openai-completions", - provider: "xai", - baseUrl: "https://api.x.ai/v1", - reasoning: false, - input: ["text"], - cost: { - input: 5, - output: 25, - cacheRead: 1.25, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 8192, - } satisfies Model<"openai-completions">, - "grok-3-latest": { - id: "grok-3-latest", - name: "Grok 3 Latest", - api: "openai-completions", - provider: "xai", - baseUrl: "https://api.x.ai/v1", - reasoning: false, - input: ["text"], - cost: { - input: 3, - output: 15, - cacheRead: 0.75, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 8192, - } satisfies Model<"openai-completions">, - "grok-3-mini": { - id: "grok-3-mini", - name: "Grok 3 Mini", - api: "openai-completions", - provider: "xai", - baseUrl: "https://api.x.ai/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0.3, - output: 0.5, - cacheRead: 0.075, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 8192, - } satisfies Model<"openai-completions">, - "grok-3-mini-fast": { - id: "grok-3-mini-fast", - name: "Grok 3 Mini Fast", - api: "openai-completions", - provider: "xai", - baseUrl: "https://api.x.ai/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0.6, - output: 4, - cacheRead: 0.15, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 8192, - } satisfies Model<"openai-completions">, - "grok-3-mini-fast-latest": { - id: "grok-3-mini-fast-latest", - name: "Grok 3 Mini Fast Latest", - api: "openai-completions", - provider: "xai", - baseUrl: "https://api.x.ai/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0.6, - output: 4, - cacheRead: 0.15, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 8192, - } satisfies Model<"openai-completions">, - "grok-3-mini-latest": { - id: "grok-3-mini-latest", - name: "Grok 3 Mini Latest", - api: "openai-completions", - provider: "xai", - baseUrl: "https://api.x.ai/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0.3, - output: 0.5, - cacheRead: 0.075, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 8192, - } satisfies Model<"openai-completions">, - "grok-4": { - id: "grok-4", - name: "Grok 4", - api: "openai-completions", - provider: "xai", - baseUrl: "https://api.x.ai/v1", - reasoning: true, - input: ["text"], - cost: { - input: 3, - output: 15, - cacheRead: 0.75, - cacheWrite: 0, - }, - contextWindow: 256000, - maxTokens: 64000, - } satisfies Model<"openai-completions">, - "grok-4-1-fast": { - id: "grok-4-1-fast", - name: "Grok 4.1 Fast", - api: "openai-completions", - provider: "xai", - baseUrl: "https://api.x.ai/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.2, - output: 0.5, - cacheRead: 0.05, - cacheWrite: 0, - }, - contextWindow: 2000000, - maxTokens: 30000, - } satisfies Model<"openai-completions">, - "grok-4-1-fast-non-reasoning": { - id: "grok-4-1-fast-non-reasoning", - name: "Grok 4.1 Fast (Non-Reasoning)", - api: "openai-completions", - provider: "xai", - baseUrl: "https://api.x.ai/v1", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.2, - output: 0.5, - cacheRead: 0.05, - cacheWrite: 0, - }, - contextWindow: 2000000, - maxTokens: 30000, - } satisfies Model<"openai-completions">, - "grok-4-fast": { - id: "grok-4-fast", - name: "Grok 4 Fast", - api: "openai-completions", - provider: "xai", - baseUrl: "https://api.x.ai/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.2, - output: 0.5, - cacheRead: 0.05, - cacheWrite: 0, - }, - contextWindow: 2000000, - maxTokens: 30000, - } satisfies Model<"openai-completions">, - "grok-4-fast-non-reasoning": { - id: "grok-4-fast-non-reasoning", - name: "Grok 4 Fast (Non-Reasoning)", - api: "openai-completions", - provider: "xai", - baseUrl: "https://api.x.ai/v1", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.2, - output: 0.5, - cacheRead: 0.05, - cacheWrite: 0, - }, - contextWindow: 2000000, - maxTokens: 30000, - } satisfies Model<"openai-completions">, "grok-4.20-0309-non-reasoning": { id: "grok-4.20-0309-non-reasoning", name: "Grok 4.20 (Non-Reasoning)", @@ -16781,7 +15913,7 @@ export const MODELS = { api: "openai-completions", provider: "xai", baseUrl: "https://api.x.ai/v1", - reasoning: true, + reasoning: false, input: ["text"], cost: { input: 0.2, @@ -16789,8 +15921,8 @@ export const MODELS = { cacheRead: 0.02, cacheWrite: 0, }, - contextWindow: 256000, - maxTokens: 10000, + contextWindow: 32768, + maxTokens: 8192, } satisfies Model<"openai-completions">, "grok-vision-beta": { id: "grok-vision-beta", diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index bed43bf3..97de3a6e 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## [Unreleased] +## [0.74.1] - 2026-05-16 ### New Features diff --git a/packages/coding-agent/examples/extensions/custom-provider-anthropic/package-lock.json b/packages/coding-agent/examples/extensions/custom-provider-anthropic/package-lock.json index 0b479264..e3698b2a 100644 --- a/packages/coding-agent/examples/extensions/custom-provider-anthropic/package-lock.json +++ b/packages/coding-agent/examples/extensions/custom-provider-anthropic/package-lock.json @@ -1,12 +1,12 @@ { "name": "pi-extension-custom-provider", - "version": "0.74.0", + "version": "0.74.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pi-extension-custom-provider", - "version": "0.74.0", + "version": "0.74.1", "dependencies": { "@anthropic-ai/sdk": "^0.52.0" } diff --git a/packages/coding-agent/examples/extensions/custom-provider-anthropic/package.json b/packages/coding-agent/examples/extensions/custom-provider-anthropic/package.json index 1c96be83..cb5dc9b5 100644 --- a/packages/coding-agent/examples/extensions/custom-provider-anthropic/package.json +++ b/packages/coding-agent/examples/extensions/custom-provider-anthropic/package.json @@ -1,7 +1,7 @@ { "name": "pi-extension-custom-provider-anthropic", "private": true, - "version": "0.74.0", + "version": "0.74.1", "type": "module", "scripts": { "clean": "echo 'nothing to clean'", diff --git a/packages/coding-agent/examples/extensions/custom-provider-gitlab-duo/package.json b/packages/coding-agent/examples/extensions/custom-provider-gitlab-duo/package.json index 3f5084d9..3da12995 100644 --- a/packages/coding-agent/examples/extensions/custom-provider-gitlab-duo/package.json +++ b/packages/coding-agent/examples/extensions/custom-provider-gitlab-duo/package.json @@ -1,7 +1,7 @@ { "name": "pi-extension-custom-provider-gitlab-duo", "private": true, - "version": "0.74.0", + "version": "0.74.1", "type": "module", "scripts": { "clean": "echo 'nothing to clean'", diff --git a/packages/coding-agent/examples/extensions/sandbox/package-lock.json b/packages/coding-agent/examples/extensions/sandbox/package-lock.json index 90ef1c2f..ebe657e7 100644 --- a/packages/coding-agent/examples/extensions/sandbox/package-lock.json +++ b/packages/coding-agent/examples/extensions/sandbox/package-lock.json @@ -1,12 +1,12 @@ { "name": "pi-extension-sandbox", - "version": "1.4.0", + "version": "1.4.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pi-extension-sandbox", - "version": "1.4.0", + "version": "1.4.1", "dependencies": { "@anthropic-ai/sandbox-runtime": "^0.0.26" } diff --git a/packages/coding-agent/examples/extensions/sandbox/package.json b/packages/coding-agent/examples/extensions/sandbox/package.json index 688fe224..98ee8b0e 100644 --- a/packages/coding-agent/examples/extensions/sandbox/package.json +++ b/packages/coding-agent/examples/extensions/sandbox/package.json @@ -1,7 +1,7 @@ { "name": "pi-extension-sandbox", "private": true, - "version": "1.4.0", + "version": "1.4.1", "type": "module", "scripts": { "clean": "echo 'nothing to clean'", diff --git a/packages/coding-agent/examples/extensions/with-deps/package-lock.json b/packages/coding-agent/examples/extensions/with-deps/package-lock.json index 89eab8ac..2eed67d8 100644 --- a/packages/coding-agent/examples/extensions/with-deps/package-lock.json +++ b/packages/coding-agent/examples/extensions/with-deps/package-lock.json @@ -1,12 +1,12 @@ { "name": "pi-extension-with-deps", - "version": "0.74.0", + "version": "0.74.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pi-extension-with-deps", - "version": "0.74.0", + "version": "0.74.1", "dependencies": { "ms": "^2.1.3" }, diff --git a/packages/coding-agent/examples/extensions/with-deps/package.json b/packages/coding-agent/examples/extensions/with-deps/package.json index 1a8fa142..022c0414 100644 --- a/packages/coding-agent/examples/extensions/with-deps/package.json +++ b/packages/coding-agent/examples/extensions/with-deps/package.json @@ -1,7 +1,7 @@ { "name": "pi-extension-with-deps", "private": true, - "version": "0.74.0", + "version": "0.74.1", "type": "module", "scripts": { "clean": "echo 'nothing to clean'", diff --git a/packages/coding-agent/package.json b/packages/coding-agent/package.json index 460c365b..98e50c6e 100644 --- a/packages/coding-agent/package.json +++ b/packages/coding-agent/package.json @@ -1,6 +1,6 @@ { "name": "@earendil-works/pi-coding-agent", - "version": "0.74.0", + "version": "0.74.1", "description": "Coding agent CLI with read, bash, edit, write tools and session management", "type": "module", "piConfig": { @@ -38,9 +38,9 @@ "prepublishOnly": "npm run clean && npm run build" }, "dependencies": { - "@earendil-works/pi-agent-core": "^0.74.0", - "@earendil-works/pi-ai": "^0.74.0", - "@earendil-works/pi-tui": "^0.74.0", + "@earendil-works/pi-agent-core": "^0.74.1", + "@earendil-works/pi-ai": "^0.74.1", + "@earendil-works/pi-tui": "^0.74.1", "@silvia-odwyer/photon-node": "^0.3.4", "chalk": "^5.5.0", "diff": "^8.0.2", diff --git a/packages/tui/CHANGELOG.md b/packages/tui/CHANGELOG.md index 90d57382..44b716f9 100644 --- a/packages/tui/CHANGELOG.md +++ b/packages/tui/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## [Unreleased] +## [0.74.1] - 2026-05-16 ### Added diff --git a/packages/tui/package.json b/packages/tui/package.json index c23363e9..3896eb88 100644 --- a/packages/tui/package.json +++ b/packages/tui/package.json @@ -1,6 +1,6 @@ { "name": "@earendil-works/pi-tui", - "version": "0.74.0", + "version": "0.74.1", "description": "Terminal User Interface library with differential rendering for efficient text-based applications", "type": "module", "main": "dist/index.js", diff --git a/packages/web-ui/CHANGELOG.md b/packages/web-ui/CHANGELOG.md index 932ed7b7..0359ca55 100644 --- a/packages/web-ui/CHANGELOG.md +++ b/packages/web-ui/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## [Unreleased] +## [0.74.1] - 2026-05-16 ## [0.74.0] - 2026-05-07 diff --git a/packages/web-ui/example/package.json b/packages/web-ui/example/package.json index b3efac1a..7225e321 100644 --- a/packages/web-ui/example/package.json +++ b/packages/web-ui/example/package.json @@ -1,6 +1,6 @@ { "name": "pi-web-ui-example", - "version": "0.74.0", + "version": "0.74.1", "private": true, "type": "module", "scripts": { diff --git a/packages/web-ui/package.json b/packages/web-ui/package.json index e22b6a72..156866e5 100644 --- a/packages/web-ui/package.json +++ b/packages/web-ui/package.json @@ -1,6 +1,6 @@ { "name": "@earendil-works/pi-web-ui", - "version": "0.74.0", + "version": "0.74.1", "description": "Reusable web UI components for AI chat interfaces powered by @earendil-works/pi-ai", "type": "module", "main": "dist/index.js", @@ -18,8 +18,8 @@ }, "dependencies": { "@lmstudio/sdk": "^1.5.0", - "@earendil-works/pi-ai": "^0.74.0", - "@earendil-works/pi-tui": "^0.74.0", + "@earendil-works/pi-ai": "^0.74.1", + "@earendil-works/pi-tui": "^0.74.1", "typebox": "^1.1.24", "docx-preview": "^0.3.7", "jszip": "^3.10.1", From 35f807cfafbaa63a914e3d9bdbd19dadd5abd1f1 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 17 May 2026 01:57:03 +0200 Subject: [PATCH 067/166] fix(coding-agent): route compaction through streamFn closes #4484 --- packages/coding-agent/CHANGELOG.md | 6 ++ .../coding-agent/src/core/agent-session.ts | 46 ++++++++---- .../src/core/compaction/compaction.ts | 59 ++++++++++++---- .../suite/agent-session-compaction.test.ts | 70 ++++++++++++++++++- 4 files changed, 154 insertions(+), 27 deletions(-) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 97de3a6e..0f86cf10 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## [Unreleased] + +### Fixed + +- Fixed compaction summary calls to use custom agent stream functions, preserving proxy-backed LLM routing ([#4484](https://github.com/earendil-works/pi/issues/4484)). + ## [0.74.1] - 2026-05-16 ### New Features diff --git a/packages/coding-agent/src/core/agent-session.ts b/packages/coding-agent/src/core/agent-session.ts index 9868e8a6..1d51163b 100644 --- a/packages/coding-agent/src/core/agent-session.ts +++ b/packages/coding-agent/src/core/agent-session.ts @@ -31,6 +31,7 @@ import { isContextOverflow, modelsAreEqual, resetApiProviders, + streamSimple, } from "@earendil-works/pi-ai"; import { theme } from "../modes/interactive/theme/theme.js"; import { stripFrontmatter } from "../utils/frontmatter.js"; @@ -367,6 +368,18 @@ export class AgentSession { throw new Error(formatNoApiKeyFoundMessage(model.provider)); } + private async _getCompactionRequestAuth(model: Model): Promise<{ + apiKey?: string; + headers?: Record; + }> { + if (this.agent.streamFn === streamSimple) { + return this._getRequiredRequestAuth(model); + } + + const result = await this._modelRegistry.getApiKeyAndHeaders(model); + return result.ok ? { apiKey: result.apiKey, headers: result.headers } : {}; + } + /** * Install tool hooks once on the Agent instance. * @@ -1618,7 +1631,7 @@ export class AgentSession { throw new Error(formatNoModelSelectedMessage()); } - const { apiKey, headers } = await this._getRequiredRequestAuth(this.model); + const { apiKey, headers } = await this._getCompactionRequestAuth(this.model); const pathEntries = this.sessionManager.getBranch(); const settings = this.settingsManager.getCompactionSettings(); @@ -1676,6 +1689,7 @@ export class AgentSession { customInstructions, this._compactionAbortController.signal, this.thinkingLevel, + this.agent.streamFn, ); summary = result.summary; firstKeptEntryId = result.firstKeptEntryId; @@ -1864,18 +1878,25 @@ export class AgentSession { return; } - const authResult = await this._modelRegistry.getApiKeyAndHeaders(this.model); - if (!authResult.ok || !authResult.apiKey) { - this._emit({ - type: "compaction_end", - reason, - result: undefined, - aborted: false, - willRetry: false, - }); - return; + let apiKey: string | undefined; + let headers: Record | undefined; + if (this.agent.streamFn === streamSimple) { + const authResult = await this._modelRegistry.getApiKeyAndHeaders(this.model); + if (!authResult.ok || !authResult.apiKey) { + this._emit({ + type: "compaction_end", + reason, + result: undefined, + aborted: false, + willRetry: false, + }); + return; + } + apiKey = authResult.apiKey; + headers = authResult.headers; + } else { + ({ apiKey, headers } = await this._getCompactionRequestAuth(this.model)); } - const { apiKey, headers } = authResult; const pathEntries = this.sessionManager.getBranch(); @@ -1941,6 +1962,7 @@ export class AgentSession { undefined, this._autoCompactionAbortController.signal, this.thinkingLevel, + this.agent.streamFn, ); summary = compactResult.summary; firstKeptEntryId = compactResult.firstKeptEntryId; diff --git a/packages/coding-agent/src/core/compaction/compaction.ts b/packages/coding-agent/src/core/compaction/compaction.ts index 5075e124..c12be0ee 100644 --- a/packages/coding-agent/src/core/compaction/compaction.ts +++ b/packages/coding-agent/src/core/compaction/compaction.ts @@ -5,8 +5,8 @@ * and after compaction the session is reloaded. */ -import type { AgentMessage, ThinkingLevel } from "@earendil-works/pi-agent-core"; -import type { AssistantMessage, Model, Usage } from "@earendil-works/pi-ai"; +import type { AgentMessage, StreamFn, ThinkingLevel } from "@earendil-works/pi-agent-core"; +import type { AssistantMessage, Context, Model, SimpleStreamOptions, Usage } from "@earendil-works/pi-ai"; import { completeSimple } from "@earendil-works/pi-ai"; import { convertToLlm, @@ -523,6 +523,34 @@ Use this EXACT format: Keep each section concise. Preserve exact file paths, function names, and error messages.`; +function createSummarizationOptions( + model: Model, + maxTokens: number, + apiKey: string | undefined, + headers: Record | undefined, + signal: AbortSignal | undefined, + thinkingLevel: ThinkingLevel | undefined, +): SimpleStreamOptions { + const options: SimpleStreamOptions = { maxTokens, signal, apiKey, headers }; + if (model.reasoning && thinkingLevel && thinkingLevel !== "off") { + options.reasoning = thinkingLevel; + } + return options; +} + +async function completeSummarization( + model: Model, + context: Context, + options: SimpleStreamOptions, + streamFn?: StreamFn, +): Promise { + if (!streamFn) { + return completeSimple(model, context, options); + } + const stream = await streamFn(model, context, options); + return stream.result(); +} + /** * Generate a summary of the conversation using the LLM. * If previousSummary is provided, uses the update prompt to merge. @@ -531,12 +559,13 @@ export async function generateSummary( currentMessages: AgentMessage[], model: Model, reserveTokens: number, - apiKey: string, + apiKey: string | undefined, headers?: Record, signal?: AbortSignal, customInstructions?: string, previousSummary?: string, thinkingLevel?: ThinkingLevel, + streamFn?: StreamFn, ): Promise { const maxTokens = Math.min( Math.floor(0.8 * reserveTokens), @@ -569,15 +598,13 @@ export async function generateSummary( }, ]; - const completionOptions = - model.reasoning && thinkingLevel && thinkingLevel !== "off" - ? { maxTokens, signal, apiKey, headers, reasoning: thinkingLevel } - : { maxTokens, signal, apiKey, headers }; + const completionOptions = createSummarizationOptions(model, maxTokens, apiKey, headers, signal, thinkingLevel); - const response = await completeSimple( + const response = await completeSummarization( model, { systemPrompt: SUMMARIZATION_SYSTEM_PROMPT, messages: summarizationMessages }, completionOptions, + streamFn, ); if (response.stopReason === "error") { @@ -720,11 +747,12 @@ Be concise. Focus on what's needed to understand the kept suffix.`; export async function compact( preparation: CompactionPreparation, model: Model, - apiKey: string, + apiKey: string | undefined, headers?: Record, customInstructions?: string, signal?: AbortSignal, thinkingLevel?: ThinkingLevel, + streamFn?: StreamFn, ): Promise { const { firstKeptEntryId, @@ -754,6 +782,7 @@ export async function compact( customInstructions, previousSummary, thinkingLevel, + streamFn, ) : Promise.resolve("No prior history."), generateTurnPrefixSummary( @@ -764,6 +793,7 @@ export async function compact( headers, signal, thinkingLevel, + streamFn, ), ]); // Merge into single summary @@ -780,6 +810,7 @@ export async function compact( customInstructions, previousSummary, thinkingLevel, + streamFn, ); } @@ -806,10 +837,11 @@ async function generateTurnPrefixSummary( messages: AgentMessage[], model: Model, reserveTokens: number, - apiKey: string, + apiKey: string | undefined, headers?: Record, signal?: AbortSignal, thinkingLevel?: ThinkingLevel, + streamFn?: StreamFn, ): Promise { const maxTokens = Math.min( Math.floor(0.5 * reserveTokens), @@ -826,12 +858,11 @@ async function generateTurnPrefixSummary( }, ]; - const response = await completeSimple( + const response = await completeSummarization( model, { systemPrompt: SUMMARIZATION_SYSTEM_PROMPT, messages: summarizationMessages }, - model.reasoning && thinkingLevel && thinkingLevel !== "off" - ? { maxTokens, signal, apiKey, headers, reasoning: thinkingLevel } - : { maxTokens, signal, apiKey, headers }, + createSummarizationOptions(model, maxTokens, apiKey, headers, signal, thinkingLevel), + streamFn, ); if (response.stopReason === "error") { diff --git a/packages/coding-agent/test/suite/agent-session-compaction.test.ts b/packages/coding-agent/test/suite/agent-session-compaction.test.ts index cb9f1808..e2d28911 100644 --- a/packages/coding-agent/test/suite/agent-session-compaction.test.ts +++ b/packages/coding-agent/test/suite/agent-session-compaction.test.ts @@ -1,4 +1,9 @@ -import { type AssistantMessage, fauxAssistantMessage, type Model } from "@earendil-works/pi-ai"; +import { + type AssistantMessage, + createAssistantMessageEventStream, + fauxAssistantMessage, + type Model, +} from "@earendil-works/pi-ai"; import { afterEach, describe, expect, it, vi } from "vitest"; import { createHarness, type Harness } from "./harness.js"; @@ -41,6 +46,43 @@ function createAssistant( }; } +function useSummaryStreamFn(harness: Harness, summary: string): () => number { + let callCount = 0; + harness.session.agent.streamFn = (model) => { + callCount++; + const stream = createAssistantMessageEventStream(); + queueMicrotask(() => { + const message: AssistantMessage = { + ...fauxAssistantMessage(summary), + api: model.api, + provider: model.provider, + model: model.id, + usage: createUsage(10), + }; + stream.push({ type: "done", reason: "stop", message }); + }); + return stream; + }; + return () => callCount; +} + +function seedCompactableSession(harness: Harness): void { + const now = Date.now(); + harness.sessionManager.appendMessage({ + role: "user", + content: [{ type: "text", text: "message to compact" }], + timestamp: now - 1000, + }); + harness.sessionManager.appendMessage( + createAssistant(harness, { + stopReason: "stop", + totalTokens: 100, + timestamp: now - 500, + }), + ); + harness.session.agent.state.messages = harness.sessionManager.buildSessionContext().messages; +} + describe("AgentSession compaction characterization", () => { const harnesses: Harness[] = []; @@ -95,6 +137,32 @@ describe("AgentSession compaction characterization", () => { await expect(harness.session.compact()).rejects.toThrow(`No API key found for ${harness.getModel().provider}.`); }); + it("manually compacts with a custom streamFn when registry auth is absent", async () => { + const harness = await createHarness({ withConfiguredAuth: false }); + harnesses.push(harness); + seedCompactableSession(harness); + const getStreamCallCount = useSummaryStreamFn(harness, "summary from custom stream"); + + const result = await harness.session.compact(); + + expect(result.summary).toBe("summary from custom stream"); + expect(getStreamCallCount()).toBe(1); + }); + + it("auto-compacts with a custom streamFn when registry auth is absent", async () => { + const harness = await createHarness({ withConfiguredAuth: false }); + harnesses.push(harness); + seedCompactableSession(harness); + const getStreamCallCount = useSummaryStreamFn(harness, "auto summary from custom stream"); + const sessionInternals = harness.session as unknown as SessionWithCompactionInternals; + + await sessionInternals._runAutoCompaction("threshold", false); + + const compactionEntries = harness.sessionManager.getEntries().filter((entry) => entry.type === "compaction"); + expect(compactionEntries).toHaveLength(1); + expect(getStreamCallCount()).toBe(1); + }); + it("cancels in-progress manual compaction when abortCompaction is called", async () => { const harness = await createHarness({ extensionFactories: [ From 734e08edf82ff315bc3d96472a6ebfa69a1d8016 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 17 May 2026 00:31:05 +0000 Subject: [PATCH 068/166] chore: approve contributor mattiacerutti --- .github/APPROVED_CONTRIBUTORS | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/APPROVED_CONTRIBUTORS b/.github/APPROVED_CONTRIBUTORS index 2845d9d8..253403d9 100644 --- a/.github/APPROVED_CONTRIBUTORS +++ b/.github/APPROVED_CONTRIBUTORS @@ -209,3 +209,5 @@ maximilianzuern pr brianmichel pr abhinavmathur-atlan pr + +mattiacerutti pr From 1af823be9d38efc2004cf7106b7826fd245b17f8 Mon Sep 17 00:00:00 2001 From: Mattia Cerutti Date: Sun, 17 May 2026 03:12:19 +0200 Subject: [PATCH 069/166] fix(ai): update OpenAI Codex model list --- packages/ai/scripts/generate-models.ts | 117 +++++++-------- packages/ai/src/models.generated.ts | 134 ++++++++---------- packages/ai/test/abort.test.ts | 4 +- packages/ai/test/context-overflow.test.ts | 4 +- .../ai/test/cross-provider-handoff.test.ts | 2 +- packages/ai/test/empty.test.ts | 16 +-- packages/ai/test/image-tool-result.test.ts | 8 +- .../openai-codex-cache-affinity-e2e.test.ts | 2 +- ...enai-responses-foreign-toolcall-id.test.ts | 4 +- ...penai-responses-tool-result-images.test.ts | 4 +- packages/ai/test/responseid.test.ts | 2 +- packages/ai/test/tokens.test.ts | 4 +- .../test/tool-call-id-normalization.test.ts | 6 +- .../ai/test/tool-call-without-result.test.ts | 4 +- packages/ai/test/total-tokens.test.ts | 4 +- packages/ai/test/unicode-surrogate.test.ts | 12 +- 16 files changed, 148 insertions(+), 179 deletions(-) diff --git a/packages/ai/scripts/generate-models.ts b/packages/ai/scripts/generate-models.ts index 56dcaa6d..f160a4e9 100644 --- a/packages/ai/scripts/generate-models.ts +++ b/packages/ai/scripts/generate-models.ts @@ -237,9 +237,6 @@ function applyThinkingLevelMetadata(model: Model): void { if (model.provider === "openai-codex" && supportsOpenAiXhigh(model.id)) { mergeThinkingLevelMap(model, { minimal: "low" }); } - if (model.provider === "openai-codex" && model.id === "gpt-5.1-codex-mini") { - mergeThinkingLevelMap(model, { minimal: "medium", low: "medium", medium: "medium", high: "high" }); - } if (model.provider === "openrouter" && model.id.startsWith("inception/mercury-2")) { // Mercury 2 in instant mode (reasoning_effort: "none") disables tool calling. // Mark "off" unsupported so the openai-completions provider omits the reasoning param @@ -1497,42 +1494,6 @@ async function generateModels() { const CODEX_CONTEXT = 272000; const CODEX_MAX_TOKENS = 128000; const codexModels: Model<"openai-codex-responses">[] = [ - { - id: "gpt-5.1", - name: "GPT-5.1", - api: "openai-codex-responses", - provider: "openai-codex", - baseUrl: CODEX_BASE_URL, - reasoning: true, - input: ["text", "image"], - cost: { input: 1.25, output: 10, cacheRead: 0.125, cacheWrite: 0 }, - contextWindow: CODEX_CONTEXT, - maxTokens: CODEX_MAX_TOKENS, - }, - { - id: "gpt-5.1-codex-max", - name: "GPT-5.1 Codex Max", - api: "openai-codex-responses", - provider: "openai-codex", - baseUrl: CODEX_BASE_URL, - reasoning: true, - input: ["text", "image"], - cost: { input: 1.25, output: 10, cacheRead: 0.125, cacheWrite: 0 }, - contextWindow: CODEX_CONTEXT, - maxTokens: CODEX_MAX_TOKENS, - }, - { - id: "gpt-5.1-codex-mini", - name: "GPT-5.1 Codex Mini", - api: "openai-codex-responses", - provider: "openai-codex", - baseUrl: CODEX_BASE_URL, - reasoning: true, - input: ["text", "image"], - cost: { input: 0.25, output: 2, cacheRead: 0.025, cacheWrite: 0 }, - contextWindow: CODEX_CONTEXT, - maxTokens: CODEX_MAX_TOKENS, - }, { id: "gpt-5.2", name: "GPT-5.2", @@ -1545,18 +1506,6 @@ async function generateModels() { contextWindow: CODEX_CONTEXT, maxTokens: CODEX_MAX_TOKENS, }, - { - id: "gpt-5.2-codex", - name: "GPT-5.2 Codex", - api: "openai-codex-responses", - provider: "openai-codex", - baseUrl: CODEX_BASE_URL, - reasoning: true, - input: ["text", "image"], - cost: { input: 1.75, output: 14, cacheRead: 0.175, cacheWrite: 0 }, - contextWindow: CODEX_CONTEXT, - maxTokens: CODEX_MAX_TOKENS, - }, { id: "gpt-5.3-codex", name: "GPT-5.3 Codex", @@ -1569,6 +1518,18 @@ async function generateModels() { contextWindow: CODEX_CONTEXT, maxTokens: CODEX_MAX_TOKENS, }, + { + id: "gpt-5.3-codex-spark", + name: "GPT-5.3 Codex Spark", + api: "openai-codex-responses", + provider: "openai-codex", + baseUrl: CODEX_BASE_URL, + reasoning: true, + input: ["text"], + cost: { input: 1.75, output: 14, cacheRead: 0.175, cacheWrite: 0 }, + contextWindow: CODEX_CONTEXT, + maxTokens: CODEX_MAX_TOKENS, + }, { id: "gpt-5.4", name: "GPT-5.4", @@ -1581,6 +1542,42 @@ async function generateModels() { contextWindow: CODEX_CONTEXT, maxTokens: CODEX_MAX_TOKENS, }, + { + id: "gpt-5.4-fast", + name: "GPT-5.4 Fast", + api: "openai-codex-responses", + provider: "openai-codex", + baseUrl: CODEX_BASE_URL, + reasoning: true, + input: ["text", "image"], + cost: { input: 5, output: 30, cacheRead: 0.5, cacheWrite: 0 }, + contextWindow: CODEX_CONTEXT, + maxTokens: CODEX_MAX_TOKENS, + }, + { + id: "gpt-5.4-mini", + name: "GPT-5.4 mini", + api: "openai-codex-responses", + provider: "openai-codex", + baseUrl: CODEX_BASE_URL, + reasoning: true, + input: ["text", "image"], + cost: { input: 0.75, output: 4.5, cacheRead: 0.075, cacheWrite: 0 }, + contextWindow: CODEX_CONTEXT, + maxTokens: CODEX_MAX_TOKENS, + }, + { + id: "gpt-5.4-mini-fast", + name: "GPT-5.4 mini Fast", + api: "openai-codex-responses", + provider: "openai-codex", + baseUrl: CODEX_BASE_URL, + reasoning: true, + input: ["text", "image"], + cost: { input: 1.5, output: 9, cacheRead: 0.15, cacheWrite: 0 }, + contextWindow: CODEX_CONTEXT, + maxTokens: CODEX_MAX_TOKENS, + }, { id: "gpt-5.5", name: "GPT-5.5", @@ -1594,29 +1591,17 @@ async function generateModels() { maxTokens: CODEX_MAX_TOKENS, }, { - id: "gpt-5.4-mini", - name: "GPT-5.4 Mini", + id: "gpt-5.5-fast", + name: "GPT-5.5 Fast", api: "openai-codex-responses", provider: "openai-codex", baseUrl: CODEX_BASE_URL, reasoning: true, input: ["text", "image"], - cost: { input: 0.75, output: 4.5, cacheRead: 0.075, cacheWrite: 0 }, + cost: { input: 12.5, output: 75, cacheRead: 1.25, cacheWrite: 0 }, contextWindow: CODEX_CONTEXT, maxTokens: CODEX_MAX_TOKENS, }, - { - id: "gpt-5.3-codex-spark", - name: "GPT-5.3 Codex Spark", - api: "openai-codex-responses", - provider: "openai-codex", - baseUrl: CODEX_BASE_URL, - reasoning: true, - input: ["text"], - cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, - contextWindow: 128000, - maxTokens: CODEX_MAX_TOKENS, - }, ]; allModels.push(...codexModels); diff --git a/packages/ai/src/models.generated.ts b/packages/ai/src/models.generated.ts index 63bd4e5f..a743b428 100644 --- a/packages/ai/src/models.generated.ts +++ b/packages/ai/src/models.generated.ts @@ -7155,58 +7155,6 @@ export const MODELS = { } satisfies Model<"openai-responses">, }, "openai-codex": { - "gpt-5.1": { - id: "gpt-5.1", - name: "GPT-5.1", - api: "openai-codex-responses", - provider: "openai-codex", - baseUrl: "https://chatgpt.com/backend-api", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1.25, - output: 10, - cacheRead: 0.125, - cacheWrite: 0, - }, - contextWindow: 272000, - maxTokens: 128000, - } satisfies Model<"openai-codex-responses">, - "gpt-5.1-codex-max": { - id: "gpt-5.1-codex-max", - name: "GPT-5.1 Codex Max", - api: "openai-codex-responses", - provider: "openai-codex", - baseUrl: "https://chatgpt.com/backend-api", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1.25, - output: 10, - cacheRead: 0.125, - cacheWrite: 0, - }, - contextWindow: 272000, - maxTokens: 128000, - } satisfies Model<"openai-codex-responses">, - "gpt-5.1-codex-mini": { - id: "gpt-5.1-codex-mini", - name: "GPT-5.1 Codex Mini", - api: "openai-codex-responses", - provider: "openai-codex", - baseUrl: "https://chatgpt.com/backend-api", - reasoning: true, - thinkingLevelMap: {"minimal":"medium","low":"medium","medium":"medium","high":"high"}, - input: ["text", "image"], - cost: { - input: 0.25, - output: 2, - cacheRead: 0.025, - cacheWrite: 0, - }, - contextWindow: 272000, - maxTokens: 128000, - } satisfies Model<"openai-codex-responses">, "gpt-5.2": { id: "gpt-5.2", name: "GPT-5.2", @@ -7225,24 +7173,6 @@ export const MODELS = { contextWindow: 272000, maxTokens: 128000, } satisfies Model<"openai-codex-responses">, - "gpt-5.2-codex": { - id: "gpt-5.2-codex", - name: "GPT-5.2 Codex", - api: "openai-codex-responses", - provider: "openai-codex", - baseUrl: "https://chatgpt.com/backend-api", - reasoning: true, - thinkingLevelMap: {"xhigh":"xhigh","minimal":"low"}, - input: ["text", "image"], - cost: { - input: 1.75, - output: 14, - cacheRead: 0.175, - cacheWrite: 0, - }, - contextWindow: 272000, - maxTokens: 128000, - } satisfies Model<"openai-codex-responses">, "gpt-5.3-codex": { id: "gpt-5.3-codex", name: "GPT-5.3 Codex", @@ -7271,12 +7201,12 @@ export const MODELS = { thinkingLevelMap: {"xhigh":"xhigh","minimal":"low"}, input: ["text"], cost: { - input: 0, - output: 0, - cacheRead: 0, + input: 1.75, + output: 14, + cacheRead: 0.175, cacheWrite: 0, }, - contextWindow: 128000, + contextWindow: 272000, maxTokens: 128000, } satisfies Model<"openai-codex-responses">, "gpt-5.4": { @@ -7297,9 +7227,27 @@ export const MODELS = { contextWindow: 272000, maxTokens: 128000, } satisfies Model<"openai-codex-responses">, + "gpt-5.4-fast": { + id: "gpt-5.4-fast", + name: "GPT-5.4 Fast", + api: "openai-codex-responses", + provider: "openai-codex", + baseUrl: "https://chatgpt.com/backend-api", + reasoning: true, + thinkingLevelMap: {"xhigh":"xhigh","minimal":"low"}, + input: ["text", "image"], + cost: { + input: 5, + output: 30, + cacheRead: 0.5, + cacheWrite: 0, + }, + contextWindow: 272000, + maxTokens: 128000, + } satisfies Model<"openai-codex-responses">, "gpt-5.4-mini": { id: "gpt-5.4-mini", - name: "GPT-5.4 Mini", + name: "GPT-5.4 mini", api: "openai-codex-responses", provider: "openai-codex", baseUrl: "https://chatgpt.com/backend-api", @@ -7315,6 +7263,24 @@ export const MODELS = { contextWindow: 272000, maxTokens: 128000, } satisfies Model<"openai-codex-responses">, + "gpt-5.4-mini-fast": { + id: "gpt-5.4-mini-fast", + name: "GPT-5.4 mini Fast", + api: "openai-codex-responses", + provider: "openai-codex", + baseUrl: "https://chatgpt.com/backend-api", + reasoning: true, + thinkingLevelMap: {"xhigh":"xhigh","minimal":"low"}, + input: ["text", "image"], + cost: { + input: 1.5, + output: 9, + cacheRead: 0.15, + cacheWrite: 0, + }, + contextWindow: 272000, + maxTokens: 128000, + } satisfies Model<"openai-codex-responses">, "gpt-5.5": { id: "gpt-5.5", name: "GPT-5.5", @@ -7333,6 +7299,24 @@ export const MODELS = { contextWindow: 272000, maxTokens: 128000, } satisfies Model<"openai-codex-responses">, + "gpt-5.5-fast": { + id: "gpt-5.5-fast", + name: "GPT-5.5 Fast", + api: "openai-codex-responses", + provider: "openai-codex", + baseUrl: "https://chatgpt.com/backend-api", + reasoning: true, + thinkingLevelMap: {"xhigh":"xhigh","minimal":"low"}, + input: ["text", "image"], + cost: { + input: 12.5, + output: 75, + cacheRead: 1.25, + cacheWrite: 0, + }, + contextWindow: 272000, + maxTokens: 128000, + } satisfies Model<"openai-codex-responses">, }, "opencode": { "big-pickle": { diff --git a/packages/ai/test/abort.test.ts b/packages/ai/test/abort.test.ts index 57bf39d1..cd46a629 100644 --- a/packages/ai/test/abort.test.ts +++ b/packages/ai/test/abort.test.ts @@ -276,12 +276,12 @@ describe("AI Providers Abort Tests", () => { describe("OpenAI Codex Provider Abort", () => { it.skipIf(!openaiCodexToken)("should abort mid-stream", { retry: 3 }, async () => { - const llm = getModel("openai-codex", "gpt-5.2-codex"); + const llm = getModel("openai-codex", "gpt-5.5"); await testAbortSignal(llm, { apiKey: openaiCodexToken }); }); it.skipIf(!openaiCodexToken)("should handle immediate abort", { retry: 3 }, async () => { - const llm = getModel("openai-codex", "gpt-5.2-codex"); + const llm = getModel("openai-codex", "gpt-5.5"); await testImmediateAbort(llm, { apiKey: openaiCodexToken }); }); }); diff --git a/packages/ai/test/context-overflow.test.ts b/packages/ai/test/context-overflow.test.ts index f84c3f47..718800bf 100644 --- a/packages/ai/test/context-overflow.test.ts +++ b/packages/ai/test/context-overflow.test.ts @@ -228,9 +228,9 @@ describe("Context overflow error handling", () => { describe("OpenAI Codex (OAuth)", () => { it.skipIf(!openaiCodexToken)( - "gpt-5.2-codex - should detect overflow via isContextOverflow", + "gpt-5.5 - should detect overflow via isContextOverflow", async () => { - const model = getModel("openai-codex", "gpt-5.2-codex"); + const model = getModel("openai-codex", "gpt-5.5"); const result = await testContextOverflow(model, openaiCodexToken!); logResult(result); diff --git a/packages/ai/test/cross-provider-handoff.test.ts b/packages/ai/test/cross-provider-handoff.test.ts index 34325f89..a133241a 100644 --- a/packages/ai/test/cross-provider-handoff.test.ts +++ b/packages/ai/test/cross-provider-handoff.test.ts @@ -67,7 +67,7 @@ const PROVIDER_MODEL_PAIRS: ProviderModelPair[] = [ { provider: "openai", model: "gpt-5-mini", label: "openai-responses-gpt-5-mini" }, { provider: "azure-openai-responses", model: "gpt-4o-mini", label: "azure-openai-responses-gpt-4o-mini" }, // OpenAI Codex - { provider: "openai-codex", model: "gpt-5.2-codex", label: "openai-codex-gpt-5.2-codex" }, + { provider: "openai-codex", model: "gpt-5.5", label: "openai-codex-gpt-5.5" }, // GitHub Copilot { provider: "github-copilot", model: "claude-sonnet-4.5", label: "copilot-claude-sonnet-4.5" }, { provider: "github-copilot", model: "gpt-5.1-codex", label: "copilot-gpt-5.1-codex" }, diff --git a/packages/ai/test/empty.test.ts b/packages/ai/test/empty.test.ts index eaf0e666..121c07b9 100644 --- a/packages/ai/test/empty.test.ts +++ b/packages/ai/test/empty.test.ts @@ -704,37 +704,37 @@ describe("AI Providers Empty Message Tests", () => { describe("OpenAI Codex Provider Empty Messages", () => { it.skipIf(!openaiCodexToken)( - "gpt-5.2-codex - should handle empty content array", + "gpt-5.5 - should handle empty content array", { retry: 3, timeout: 30000 }, async () => { - const llm = getModel("openai-codex", "gpt-5.2-codex"); + const llm = getModel("openai-codex", "gpt-5.5"); await testEmptyMessage(llm, { apiKey: openaiCodexToken }); }, ); it.skipIf(!openaiCodexToken)( - "gpt-5.2-codex - should handle empty string content", + "gpt-5.5 - should handle empty string content", { retry: 3, timeout: 30000 }, async () => { - const llm = getModel("openai-codex", "gpt-5.2-codex"); + const llm = getModel("openai-codex", "gpt-5.5"); await testEmptyStringMessage(llm, { apiKey: openaiCodexToken }); }, ); it.skipIf(!openaiCodexToken)( - "gpt-5.2-codex - should handle whitespace-only content", + "gpt-5.5 - should handle whitespace-only content", { retry: 3, timeout: 30000 }, async () => { - const llm = getModel("openai-codex", "gpt-5.2-codex"); + const llm = getModel("openai-codex", "gpt-5.5"); await testWhitespaceOnlyMessage(llm, { apiKey: openaiCodexToken }); }, ); it.skipIf(!openaiCodexToken)( - "gpt-5.2-codex - should handle empty assistant message in conversation", + "gpt-5.5 - should handle empty assistant message in conversation", { retry: 3, timeout: 30000 }, async () => { - const llm = getModel("openai-codex", "gpt-5.2-codex"); + const llm = getModel("openai-codex", "gpt-5.5"); await testEmptyAssistantMessage(llm, { apiKey: openaiCodexToken }); }, ); diff --git a/packages/ai/test/image-tool-result.test.ts b/packages/ai/test/image-tool-result.test.ts index 72bd0414..9d1276d5 100644 --- a/packages/ai/test/image-tool-result.test.ts +++ b/packages/ai/test/image-tool-result.test.ts @@ -481,19 +481,19 @@ describe("Tool Results with Images", () => { describe("OpenAI Codex Provider", () => { it.skipIf(!openaiCodexToken)( - "gpt-5.2-codex - should handle tool result with only image", + "gpt-5.5 - should handle tool result with only image", { retry: 3, timeout: 30000 }, async () => { - const llm = getModel("openai-codex", "gpt-5.2-codex"); + const llm = getModel("openai-codex", "gpt-5.5"); await handleToolWithImageResult(llm, { apiKey: openaiCodexToken }); }, ); it.skipIf(!openaiCodexToken)( - "gpt-5.2-codex - should handle tool result with text and image", + "gpt-5.5 - should handle tool result with text and image", { retry: 3, timeout: 30000 }, async () => { - const llm = getModel("openai-codex", "gpt-5.2-codex"); + const llm = getModel("openai-codex", "gpt-5.5"); await handleToolWithTextAndImageResult(llm, { apiKey: openaiCodexToken }); }, ); diff --git a/packages/ai/test/openai-codex-cache-affinity-e2e.test.ts b/packages/ai/test/openai-codex-cache-affinity-e2e.test.ts index cf0ddf39..0d929724 100644 --- a/packages/ai/test/openai-codex-cache-affinity-e2e.test.ts +++ b/packages/ai/test/openai-codex-cache-affinity-e2e.test.ts @@ -8,7 +8,7 @@ const codexToken = await resolveApiKey("openai-codex"); describe("openai-codex cache affinity e2e", () => { it.skipIf(!codexToken)("handles SSE requests with aligned cache-affinity identifiers", async () => { - const model = getModel("openai-codex", "gpt-5.3-codex"); + const model = getModel("openai-codex", "gpt-5.5"); const sessionId = "0195d6e4-4cf9-7f44-a2d8-f8f7f49ee9d3"; const context: Context = { systemPrompt: "You are a helpful assistant. Reply exactly as requested.", diff --git a/packages/ai/test/openai-responses-foreign-toolcall-id.test.ts b/packages/ai/test/openai-responses-foreign-toolcall-id.test.ts index c50fcc88..52de262b 100644 --- a/packages/ai/test/openai-responses-foreign-toolcall-id.test.ts +++ b/packages/ai/test/openai-responses-foreign-toolcall-id.test.ts @@ -18,7 +18,7 @@ const usage: Usage = { describe("OpenAI Responses foreign tool call ID normalization", () => { it("hashes foreign Copilot tool item IDs into a bounded Codex-safe fc_ shape", () => { - const model = getModel("openai-codex", "gpt-5.3-codex"); + const model = getModel("openai-codex", "gpt-5.5"); const assistant: AssistantMessage = { role: "assistant", content: [ @@ -31,7 +31,7 @@ describe("OpenAI Responses foreign tool call ID normalization", () => { ], api: "openai-responses", provider: "github-copilot", - model: "gpt-5.3-codex", + model: "gpt-5.5", usage, stopReason: "toolUse", timestamp: Date.now() - 2000, diff --git a/packages/ai/test/openai-responses-tool-result-images.test.ts b/packages/ai/test/openai-responses-tool-result-images.test.ts index 05d9c16f..48ab8d08 100644 --- a/packages/ai/test/openai-responses-tool-result-images.test.ts +++ b/packages/ai/test/openai-responses-tool-result-images.test.ts @@ -183,8 +183,8 @@ describe("Responses API tool result images", () => { ); }); - describe("OpenAI Codex Responses Provider (gpt-5.2-codex)", () => { - const model = getModel("openai-codex", "gpt-5.2-codex"); + describe("OpenAI Codex Responses Provider (gpt-5.5)", () => { + const model = getModel("openai-codex", "gpt-5.5"); it.skipIf(!openaiCodexToken)( "should send tool result images in function_call_output", diff --git a/packages/ai/test/responseid.test.ts b/packages/ai/test/responseid.test.ts index 00e6cc5d..61df764b 100644 --- a/packages/ai/test/responseid.test.ts +++ b/packages/ai/test/responseid.test.ts @@ -114,7 +114,7 @@ describe("responseId E2E Tests", () => { describe("OpenAI Codex Provider", () => { it.skipIf(!openaiCodexToken)("should expose responseId", { retry: 3, timeout: 30000 }, async () => { - const llm = getModel("openai-codex", "gpt-5.2-codex"); + const llm = getModel("openai-codex", "gpt-5.5"); await expectResponseId(llm, { apiKey: openaiCodexToken }); }); }); diff --git a/packages/ai/test/tokens.test.ts b/packages/ai/test/tokens.test.ts index 00c2b567..86a70cd2 100644 --- a/packages/ai/test/tokens.test.ts +++ b/packages/ai/test/tokens.test.ts @@ -309,10 +309,10 @@ describe("Token Statistics on Abort", () => { describe("OpenAI Codex Provider", () => { it.skipIf(!openaiCodexToken)( - "gpt-5.2-codex - should include token stats when aborted mid-stream", + "gpt-5.5 - should include token stats when aborted mid-stream", { retry: 3, timeout: 30000 }, async () => { - const llm = getModel("openai-codex", "gpt-5.2-codex"); + const llm = getModel("openai-codex", "gpt-5.5"); await testTokensOnAbort(llm, { apiKey: openaiCodexToken }); }, ); diff --git a/packages/ai/test/tool-call-id-normalization.test.ts b/packages/ai/test/tool-call-id-normalization.test.ts index 0655f754..33ea327f 100644 --- a/packages/ai/test/tool-call-id-normalization.test.ts +++ b/packages/ai/test/tool-call-id-normalization.test.ts @@ -38,7 +38,7 @@ const echoTool: Tool = { * * 1. Use github-copilot gpt-5.2-codex to generate a tool call * 2. Switch to openrouter openai/gpt-5.2-codex and complete - * 3. Switch to openai-codex gpt-5.2-codex and complete + * 3. Switch to openai-codex gpt-5.5 and complete * * Both should succeed without "call_id too long" errors. */ @@ -117,7 +117,7 @@ describe("Tool Call ID Normalization - Live Handoff", () => { "github-copilot -> openai-codex should normalize pipe-separated IDs", async () => { const copilotModel = getModel("github-copilot", "gpt-5.2-codex"); - const codexModel = getModel("openai-codex", "gpt-5.2-codex"); + const codexModel = getModel("openai-codex", "gpt-5.5"); // Step 1: Generate tool call with github-copilot const userMessage: Message = { @@ -266,7 +266,7 @@ describe("Tool Call ID Normalization - Prefilled Context", () => { it.skipIf(!codexToken)( "openai-codex should handle prefilled context with long pipe-separated IDs", async () => { - const model = getModel("openai-codex", "gpt-5.2-codex"); + const model = getModel("openai-codex", "gpt-5.5"); const messages = buildPrefilledMessages(); const response = await completeSimple( diff --git a/packages/ai/test/tool-call-without-result.test.ts b/packages/ai/test/tool-call-without-result.test.ts index 18a21f99..fdab9394 100644 --- a/packages/ai/test/tool-call-without-result.test.ts +++ b/packages/ai/test/tool-call-without-result.test.ts @@ -317,10 +317,10 @@ describe("Tool Call Without Result Tests", () => { describe("OpenAI Codex Provider", () => { it.skipIf(!openaiCodexToken)( - "gpt-5.2-codex - should filter out tool calls without corresponding tool results", + "gpt-5.5 - should filter out tool calls without corresponding tool results", { retry: 3, timeout: 30000 }, async () => { - const model = getModel("openai-codex", "gpt-5.2-codex"); + const model = getModel("openai-codex", "gpt-5.5"); await testToolCallWithoutResult(model, { apiKey: openaiCodexToken }); }, ); diff --git a/packages/ai/test/total-tokens.test.ts b/packages/ai/test/total-tokens.test.ts index f634a5c3..47303da4 100644 --- a/packages/ai/test/total-tokens.test.ts +++ b/packages/ai/test/total-tokens.test.ts @@ -771,10 +771,10 @@ describe("totalTokens field", () => { describe("OpenAI Codex (OAuth)", () => { it.skipIf(!openaiCodexToken)( - "gpt-5.2-codex - should return totalTokens equal to sum of components", + "gpt-5.5 - should return totalTokens equal to sum of components", { retry: 3, timeout: 60000 }, async () => { - const llm = getModel("openai-codex", "gpt-5.2-codex"); + const llm = getModel("openai-codex", "gpt-5.5"); console.log(`\nOpenAI Codex / ${llm.id}:`); const { first, second } = await testTotalTokensWithCache(llm, { apiKey: openaiCodexToken }); diff --git a/packages/ai/test/unicode-surrogate.test.ts b/packages/ai/test/unicode-surrogate.test.ts index 5b348ca5..568de174 100644 --- a/packages/ai/test/unicode-surrogate.test.ts +++ b/packages/ai/test/unicode-surrogate.test.ts @@ -746,28 +746,28 @@ describe("AI Providers Unicode Surrogate Pair Tests", () => { describe("OpenAI Codex Provider Unicode Handling", () => { it.skipIf(!openaiCodexToken)( - "gpt-5.2-codex - should handle emoji in tool results", + "gpt-5.5 - should handle emoji in tool results", { retry: 3, timeout: 30000 }, async () => { - const llm = getModel("openai-codex", "gpt-5.2-codex"); + const llm = getModel("openai-codex", "gpt-5.5"); await testEmojiInToolResults(llm, { apiKey: openaiCodexToken }); }, ); it.skipIf(!openaiCodexToken)( - "gpt-5.2-codex - should handle real-world LinkedIn comment data with emoji", + "gpt-5.5 - should handle real-world LinkedIn comment data with emoji", { retry: 3, timeout: 30000 }, async () => { - const llm = getModel("openai-codex", "gpt-5.2-codex"); + const llm = getModel("openai-codex", "gpt-5.5"); await testRealWorldLinkedInData(llm, { apiKey: openaiCodexToken }); }, ); it.skipIf(!openaiCodexToken)( - "gpt-5.2-codex - should handle unpaired high surrogate (0xD83D) in tool results", + "gpt-5.5 - should handle unpaired high surrogate (0xD83D) in tool results", { retry: 3, timeout: 30000 }, async () => { - const llm = getModel("openai-codex", "gpt-5.2-codex"); + const llm = getModel("openai-codex", "gpt-5.5"); await testUnpairedHighSurrogate(llm, { apiKey: openaiCodexToken }); }, ); From e2fd651eb92a22fcb5e390fb41f78239e2eba4cd Mon Sep 17 00:00:00 2001 From: Nelson Herrera Date: Sat, 16 May 2026 19:10:09 -0700 Subject: [PATCH 070/166] Updated system-prompt.ts to use xml boundaries during system and context file merging rather than using `##` so that agents are less likely to ingest a prompt with inconsistent boundaries. --- packages/coding-agent/src/core/system-prompt.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/coding-agent/src/core/system-prompt.ts b/packages/coding-agent/src/core/system-prompt.ts index 6e474992..3675e413 100644 --- a/packages/coding-agent/src/core/system-prompt.ts +++ b/packages/coding-agent/src/core/system-prompt.ts @@ -59,11 +59,12 @@ export function buildSystemPrompt(options: BuildSystemPromptOptions): string { // Append project context files if (contextFiles.length > 0) { - prompt += "\n\n# Project Context\n\n"; + prompt += "\n\n\n\n"; prompt += "Project-specific instructions and guidelines:\n\n"; for (const { path: filePath, content } of contextFiles) { - prompt += `## ${filePath}\n\n${content}\n\n`; + prompt += `\n${content}\n\n\n`; } + prompt += "\n"; } // Append skills section (only if read tool is available) From 485afc9c3fcf223b908f0bc84233e115911fd125 Mon Sep 17 00:00:00 2001 From: Mattia Cerutti Date: Sun, 17 May 2026 12:18:50 +0200 Subject: [PATCH 071/166] fix(ai): map copilot gpt minimal thinking to low --- packages/ai/scripts/generate-models.ts | 3 +++ packages/ai/src/models.generated.ts | 14 +++++++------- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/packages/ai/scripts/generate-models.ts b/packages/ai/scripts/generate-models.ts index 56dcaa6d..6cd42ca6 100644 --- a/packages/ai/scripts/generate-models.ts +++ b/packages/ai/scripts/generate-models.ts @@ -203,6 +203,9 @@ function applyThinkingLevelMetadata(model: Model): void { ) { mergeThinkingLevelMap(model, { off: null }); } + if (model.provider === "github-copilot" && model.id.startsWith("gpt-5")) { + mergeThinkingLevelMap(model, { minimal: "low" }); + } if ( model.api === "openai-responses" && model.provider === "openai" && diff --git a/packages/ai/src/models.generated.ts b/packages/ai/src/models.generated.ts index 63bd4e5f..6632bc4a 100644 --- a/packages/ai/src/models.generated.ts +++ b/packages/ai/src/models.generated.ts @@ -4005,7 +4005,7 @@ export const MODELS = { baseUrl: "https://api.individual.githubcopilot.com", headers: {"User-Agent":"GitHubCopilotChat/0.35.0","Editor-Version":"vscode/1.107.0","Editor-Plugin-Version":"copilot-chat/0.35.0","Copilot-Integration-Id":"vscode-chat"}, reasoning: true, - thinkingLevelMap: {"off":null}, + thinkingLevelMap: {"off":null,"minimal":"low"}, input: ["text", "image"], cost: { input: 0, @@ -4024,7 +4024,7 @@ export const MODELS = { baseUrl: "https://api.individual.githubcopilot.com", headers: {"User-Agent":"GitHubCopilotChat/0.35.0","Editor-Version":"vscode/1.107.0","Editor-Plugin-Version":"copilot-chat/0.35.0","Copilot-Integration-Id":"vscode-chat"}, reasoning: true, - thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, + thinkingLevelMap: {"off":null,"minimal":"low","xhigh":"xhigh"}, input: ["text", "image"], cost: { input: 0, @@ -4043,7 +4043,7 @@ export const MODELS = { baseUrl: "https://api.individual.githubcopilot.com", headers: {"User-Agent":"GitHubCopilotChat/0.35.0","Editor-Version":"vscode/1.107.0","Editor-Plugin-Version":"copilot-chat/0.35.0","Copilot-Integration-Id":"vscode-chat"}, reasoning: true, - thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, + thinkingLevelMap: {"off":null,"minimal":"low","xhigh":"xhigh"}, input: ["text", "image"], cost: { input: 0, @@ -4062,7 +4062,7 @@ export const MODELS = { baseUrl: "https://api.individual.githubcopilot.com", headers: {"User-Agent":"GitHubCopilotChat/0.35.0","Editor-Version":"vscode/1.107.0","Editor-Plugin-Version":"copilot-chat/0.35.0","Copilot-Integration-Id":"vscode-chat"}, reasoning: true, - thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, + thinkingLevelMap: {"off":null,"minimal":"low","xhigh":"xhigh"}, input: ["text", "image"], cost: { input: 0, @@ -4081,7 +4081,7 @@ export const MODELS = { baseUrl: "https://api.individual.githubcopilot.com", headers: {"User-Agent":"GitHubCopilotChat/0.35.0","Editor-Version":"vscode/1.107.0","Editor-Plugin-Version":"copilot-chat/0.35.0","Copilot-Integration-Id":"vscode-chat"}, reasoning: true, - thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, + thinkingLevelMap: {"off":null,"minimal":"low","xhigh":"xhigh"}, input: ["text", "image"], cost: { input: 0, @@ -4100,7 +4100,7 @@ export const MODELS = { baseUrl: "https://api.individual.githubcopilot.com", headers: {"User-Agent":"GitHubCopilotChat/0.35.0","Editor-Version":"vscode/1.107.0","Editor-Plugin-Version":"copilot-chat/0.35.0","Copilot-Integration-Id":"vscode-chat"}, reasoning: true, - thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, + thinkingLevelMap: {"off":null,"minimal":"low","xhigh":"xhigh"}, input: ["text", "image"], cost: { input: 0, @@ -4119,7 +4119,7 @@ export const MODELS = { baseUrl: "https://api.individual.githubcopilot.com", headers: {"User-Agent":"GitHubCopilotChat/0.35.0","Editor-Version":"vscode/1.107.0","Editor-Plugin-Version":"copilot-chat/0.35.0","Copilot-Integration-Id":"vscode-chat"}, reasoning: true, - thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, + thinkingLevelMap: {"off":null,"minimal":"low","xhigh":"xhigh"}, input: ["text", "image"], cost: { input: 0, From 6236ad50776dc18b723ddc612b9e73fe8015d815 Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Sun, 17 May 2026 18:32:39 +0200 Subject: [PATCH 072/166] chore: restore normal issue gate message --- .github/workflows/issue-gate.yml | 69 +++++--------------------------- 1 file changed, 9 insertions(+), 60 deletions(-) diff --git a/.github/workflows/issue-gate.yml b/.github/workflows/issue-gate.yml index 62018399..62663940 100644 --- a/.github/workflows/issue-gate.yml +++ b/.github/workflows/issue-gate.yml @@ -17,11 +17,8 @@ jobs: script: | const APPROVED_FILE = '.github/APPROVED_CONTRIBUTORS'; const VALID_CAPABILITIES = new Set(['issue', 'pr']); - const ISSUE_GATE_MESSAGE_MODE = 'refactor'; // Switch to 'normal' to restore the standard auto-close message. const issueAuthor = context.payload.issue.user.login; const defaultBranch = context.payload.repository.default_branch; - const issueCreatedDay = new Date(context.payload.issue.created_at).getUTCDay(); - const isFridayThroughSunday = issueCreatedDay === 5 || issueCreatedDay === 6 || issueCreatedDay === 0; if (issueAuthor.endsWith('[bot]') || issueAuthor === 'dependabot[bot]') { console.log(`Skipping bot: ${issueAuthor}`); @@ -97,36 +94,15 @@ jobs: return; } - function buildNormalGateMessage() { - return [ - 'This issue was auto-closed. All issues from new contributors are auto-closed by default.', - ...(isFridayThroughSunday - ? [ - 'Issues submitted Friday through Sunday are not reviewed. If this is urgent, ask on Discord: https://discord.com/invite/3cU7Bz4UPx', - ] - : []), - '', - `Maintainers review auto-closed issues daily and reopen worthwhile ones. Issues that do not meet the quality bar in [CONTRIBUTING.md](https://github.com/${context.repo.owner}/${context.repo.repo}/blob/${defaultBranch}/CONTRIBUTING.md) will not be reopened or receive a reply.`, - '', - 'If a maintainer replies `lgtmi` on one of your issues, your future issues will stay open. If a maintainer replies `lgtm`, your future issues and PRs will stay open.', - '', - `See [CONTRIBUTING.md](https://github.com/${context.repo.owner}/${context.repo.repo}/blob/${defaultBranch}/CONTRIBUTING.md).`, - ].join('\n'); - } - - function buildRefactorGateMessage() { - return [ - 'This issue was auto-closed. All issues will be closed until 2026-05-17 while the project refactor is being completed.', - '', - `The refactor is happening on \`${defaultBranch}\`: https://github.com/${context.repo.owner}/${context.repo.repo}/tree/${defaultBranch}`, - '', - 'Issues closed during this period will not be reviewed. The reason is that the refactor will not get done otherwise, because issue triage has been taking about 8 hours per day.', - '', - 'In case of emergency, ask on Discord: https://discord.com/invite/3cU7Bz4UPx', - ].join('\n'); - } - - const message = ISSUE_GATE_MESSAGE_MODE === 'refactor' ? buildRefactorGateMessage() : buildNormalGateMessage(); + const message = [ + 'This issue was auto-closed. All issues from new contributors are auto-closed by default.', + '', + `Maintainers review auto-closed issues daily and reopen worthwhile ones. Issues that do not meet the quality bar in [CONTRIBUTING.md](https://github.com/${context.repo.owner}/${context.repo.repo}/blob/${defaultBranch}/CONTRIBUTING.md) will not be reopened or receive a reply.`, + '', + 'If a maintainer replies `lgtmi` on one of your issues, your future issues will stay open. If a maintainer replies `lgtm`, your future issues and PRs will stay open.', + '', + `See [CONTRIBUTING.md](https://github.com/${context.repo.owner}/${context.repo.repo}/blob/${defaultBranch}/CONTRIBUTING.md).`, + ].join('\n'); await github.rest.issues.createComment({ owner: context.repo.owner, @@ -135,33 +111,6 @@ jobs: body: message, }); - const labelsToAdd = []; - if (isFridayThroughSunday) labelsToAdd.push('closed-because-weekend'); - if (ISSUE_GATE_MESSAGE_MODE === 'refactor') labelsToAdd.push('closed-because-refactor'); - - if (labelsToAdd.includes('closed-because-refactor')) { - try { - await github.rest.issues.createLabel({ - owner: context.repo.owner, - repo: context.repo.repo, - name: 'closed-because-refactor', - color: '5319e7', - description: 'Closed while the project refactor is in progress', - }); - } catch (error) { - if (error.status !== 422) throw error; - } - } - - if (labelsToAdd.length > 0) { - await github.rest.issues.addLabels({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.issue.number, - labels: labelsToAdd, - }); - } - await github.rest.issues.update({ owner: context.repo.owner, repo: context.repo.repo, From 6d474f8c1ac09f44f07b22991942c2ab0da6d63e Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Sun, 17 May 2026 20:06:59 +0200 Subject: [PATCH 073/166] fix(ai): cap context-sized default output budgets closes #4614 --- packages/ai/CHANGELOG.md | 6 ++++++ packages/ai/src/providers/simple-options.ts | 12 +++++++++++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index ffa59a3c..18759b97 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## [Unreleased] + +### Fixed + +- Fixed `streamSimple()` defaults for models whose advertised output limit is effectively their full context window to avoid impossible default requests ([#4614](https://github.com/earendil-works/pi/issues/4614)). + ## [0.74.1] - 2026-05-16 ### Added diff --git a/packages/ai/src/providers/simple-options.ts b/packages/ai/src/providers/simple-options.ts index 288f2c52..d9dcc348 100644 --- a/packages/ai/src/providers/simple-options.ts +++ b/packages/ai/src/providers/simple-options.ts @@ -1,9 +1,19 @@ import type { Api, Model, SimpleStreamOptions, StreamOptions, ThinkingBudgets, ThinkingLevel } from "../types.js"; +const DEFAULT_MAX_OUTPUT_TOKENS = 32000; +const CONTEXT_WINDOW_OUTPUT_TOLERANCE = 1024; + export function buildBaseOptions(model: Model, options?: SimpleStreamOptions, apiKey?: string): StreamOptions { + const defaultMaxTokens = + model.maxTokens > 0 + ? model.maxTokens >= model.contextWindow - CONTEXT_WINDOW_OUTPUT_TOLERANCE + ? Math.min(model.maxTokens, DEFAULT_MAX_OUTPUT_TOKENS) + : model.maxTokens + : undefined; + return { temperature: options?.temperature, - maxTokens: options?.maxTokens ?? (model.maxTokens > 0 ? model.maxTokens : undefined), + maxTokens: options?.maxTokens ?? defaultMaxTokens, signal: options?.signal, apiKey: apiKey || options?.apiKey, transport: options?.transport, From 6f931797cae24ad9ea392d220c1f7c7f4585b50e Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 17 May 2026 20:47:39 +0200 Subject: [PATCH 074/166] fix(coding-agent): install npm packages in managed root closes #4587 --- packages/coding-agent/CHANGELOG.md | 6 + packages/coding-agent/README.md | 2 +- packages/coding-agent/docs/packages.md | 4 +- packages/coding-agent/docs/settings.md | 4 +- .../coding-agent/src/core/package-manager.ts | 76 +++++---- .../coding-agent/test/package-manager.test.ts | 147 ++++++++++++++---- 6 files changed, 172 insertions(+), 67 deletions(-) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 97de3a6e..b1acdb37 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## [Unreleased] + +### Fixed + +- Fixed user-scoped npm pi packages to install under `~/.pi/agent/npm/` instead of npm's global package root, avoiding permission errors with system-managed Node installs ([#4587](https://github.com/earendil-works/pi/issues/4587)). + ## [0.74.1] - 2026-05-16 ### New Features diff --git a/packages/coding-agent/README.md b/packages/coding-agent/README.md index 44e1bce1..fedc53d2 100644 --- a/packages/coding-agent/README.md +++ b/packages/coding-agent/README.md @@ -406,7 +406,7 @@ pi update npm:@foo/pi-tools # update one package pi config # enable/disable extensions, skills, prompts, themes ``` -Packages install to `~/.pi/agent/git/` (git) or global npm. Use `-l` for project-local installs (`.pi/git/`, `.pi/npm/`). Git packages install dependencies with `npm install --omit=dev` by default, so runtime deps must be listed under `dependencies`; when `npmCommand` is configured, git packages use plain `install` for compatibility with wrappers. If you use a Node version manager and want package installs to reuse a stable npm context, set `npmCommand` in `settings.json`, for example `["mise", "exec", "node@20", "--", "npm"]`. +Packages install to `~/.pi/agent/git/` (git) or `~/.pi/agent/npm/` (npm). Use `-l` for project-local installs (`.pi/git/`, `.pi/npm/`). Git packages install dependencies with `npm install --omit=dev` by default, so runtime deps must be listed under `dependencies`; when `npmCommand` is configured, git packages use plain `install` for compatibility with wrappers. If you use a Node version manager and want package installs to reuse a stable npm context, set `npmCommand` in `settings.json`, for example `["mise", "exec", "node@20", "--", "npm"]`. Create a package by adding a `pi` key to `package.json`: diff --git a/packages/coding-agent/docs/packages.md b/packages/coding-agent/docs/packages.md index e0a2f2c2..575f0acc 100644 --- a/packages/coding-agent/docs/packages.md +++ b/packages/coding-agent/docs/packages.md @@ -36,7 +36,7 @@ pi update npm:@foo/bar # update one package pi update --extension npm:@foo/bar ``` -By default, `install` and `remove` write to global settings (`~/.pi/agent/settings.json`). Use `-l` to write to project settings (`.pi/settings.json`) instead. Project settings can be shared with your team, and pi installs any missing packages automatically on startup. +By default, `install` and `remove` write to user settings (`~/.pi/agent/settings.json`). Use `-l` to write to project settings (`.pi/settings.json`) instead. Project settings can be shared with your team, and pi installs any missing packages automatically on startup. To try a package without installing it, use `--extension` or `-e`. This installs to a temporary directory for the current run only: @@ -57,7 +57,7 @@ npm:pkg ``` - Versioned specs are pinned and skipped by package updates (`pi update`, `pi update --extensions`). -- Global installs use `npm install -g`. +- User installs go under `~/.pi/agent/npm/`. - Project installs go under `.pi/npm/`. - Set `npmCommand` in `settings.json` to pin npm package lookup and install operations to a specific wrapper command such as `mise` or `asdf`. diff --git a/packages/coding-agent/docs/settings.md b/packages/coding-agent/docs/settings.md index 05b38fa1..befd4da3 100644 --- a/packages/coding-agent/docs/settings.md +++ b/packages/coding-agent/docs/settings.md @@ -153,9 +153,7 @@ When a provider requests a retry delay longer than `retry.provider.maxRetryDelay } ``` -`npmCommand` is used for all npm package-manager operations, including installs, uninstalls, and dependency installs inside git packages. Use argv-style entries exactly as the process should be launched. When `npmCommand` is configured, git package dependency installs use plain `install` to avoid npm-specific flags in wrappers or alternate package managers. - -Normally the package manager's global modules location is queried using `root -g`. As a special case, if the first element of `npmCommand` is `"bun"`, the modules location will instead be queried with `pm bin -g`. +`npmCommand` is used for all npm package-manager operations, including installs, uninstalls, and dependency installs inside git packages. User-scoped npm packages install under `~/.pi/agent/npm/`; project-scoped npm packages install under `.pi/npm/`. Use argv-style entries exactly as the process should be launched. When `npmCommand` is configured, git package dependency installs use plain `install` to avoid npm-specific flags in wrappers or alternate package managers. ### Sessions diff --git a/packages/coding-agent/src/core/package-manager.ts b/packages/coding-agent/src/core/package-manager.ts index 27c18fd4..92f9bede 100644 --- a/packages/coding-agent/src/core/package-manager.ts +++ b/packages/coding-agent/src/core/package-manager.ts @@ -1084,7 +1084,7 @@ export class DefaultPackageManager implements PackageManager { } private async shouldUpdateNpmSource(source: NpmSource, scope: InstalledSourceScope): Promise { - const installedPath = this.getNpmInstallPath(source, scope); + const installedPath = this.getManagedNpmInstallPath(source, scope); const installedVersion = existsSync(installedPath) ? this.getInstalledNpmVersion(installedPath) : undefined; if (!installedVersion) { return true; @@ -1114,13 +1114,9 @@ export class DefaultPackageManager implements PackageManager { } private async installNpmBatch(specs: string[], scope: InstalledSourceScope): Promise { - if (scope === "user") { - await this.runNpmCommand(["install", "-g", ...specs]); - return; - } const installRoot = this.getNpmInstallRoot(scope, false); this.ensureNpmProject(installRoot); - await this.runNpmCommand(["install", ...specs, "--prefix", installRoot]); + await this.runNpmCommand(this.getNpmInstallArgs(specs, installRoot)); } async checkForAvailableUpdates(): Promise { @@ -1669,6 +1665,14 @@ export class DefaultPackageManager implements PackageManager { return { command, args }; } + private getPackageManagerName(): string { + const npmCommand = this.getNpmCommand(); + const commandParts = [npmCommand.command, ...npmCommand.args]; + const separatorIndex = commandParts.lastIndexOf("--"); + const packageManagerCommand = separatorIndex >= 0 ? commandParts[separatorIndex + 1] : npmCommand.command; + return packageManagerCommand ? basename(packageManagerCommand).replace(/\.(cmd|exe)$/i, "") : ""; + } + private async runNpmCommand(args: string[], options?: { cwd?: string }): Promise { const npmCommand = this.getNpmCommand(); await this.runCommand(npmCommand.command, [...npmCommand.args, ...args], options); @@ -1687,25 +1691,32 @@ export class DefaultPackageManager implements PackageManager { return this.runCommandSync(npmCommand.command, [...npmCommand.args, ...args]); } - private async installNpm(source: NpmSource, scope: SourceScope, temporary: boolean): Promise { - if (scope === "user" && !temporary) { - await this.runNpmCommand(["install", "-g", source.spec]); - return; + private getNpmInstallArgs(specs: string[], installRoot: string): string[] { + const packageManagerName = this.getPackageManagerName(); + if (packageManagerName === "bun") { + return ["install", ...specs, "--cwd", installRoot]; } + if (packageManagerName === "pnpm") { + return ["install", ...specs, "--prefix", installRoot, "--config.strict-dep-builds=false"]; + } + return ["install", ...specs, "--prefix", installRoot]; + } + + private async installNpm(source: NpmSource, scope: SourceScope, temporary: boolean): Promise { const installRoot = this.getNpmInstallRoot(scope, temporary); this.ensureNpmProject(installRoot); - await this.runNpmCommand(["install", source.spec, "--prefix", installRoot]); + await this.runNpmCommand(this.getNpmInstallArgs([source.spec], installRoot)); } private async uninstallNpm(source: NpmSource, scope: SourceScope): Promise { - if (scope === "user") { - await this.runNpmCommand(["uninstall", "-g", source.name]); - return; - } const installRoot = this.getNpmInstallRoot(scope, false); if (!existsSync(installRoot)) { return; } + if (this.getPackageManagerName() === "bun") { + await this.runNpmCommand(["uninstall", source.name, "--cwd", installRoot]); + return; + } await this.runNpmCommand(["uninstall", source.name, "--prefix", installRoot]); } @@ -1836,7 +1847,7 @@ export class DefaultPackageManager implements PackageManager { if (scope === "project") { return join(this.cwd, CONFIG_DIR_NAME, "npm"); } - return join(this.getGlobalNpmRoot(), ".."); + return join(this.agentDir, "npm"); } private getGlobalNpmRoot(): string { @@ -1845,8 +1856,7 @@ export class DefaultPackageManager implements PackageManager { if (this.globalNpmRoot && this.globalNpmRootCommandKey === commandKey) { return this.globalNpmRoot; } - const isBunPackageManager = npmCommand.command === "bun"; - if (isBunPackageManager) { + if (this.getPackageManagerName() === "bun") { const binDir = this.runNpmCommandSync(["pm", "bin", "-g"]).trim(); this.globalNpmRoot = join(dirname(binDir), "install", "global", "node_modules"); } else { @@ -1857,14 +1867,7 @@ export class DefaultPackageManager implements PackageManager { } private getPnpmGlobalPackagePath(packageName: string): string | undefined { - const npmCommand = this.getNpmCommand(); - const commandParts = [npmCommand.command, ...npmCommand.args]; - const separatorIndex = commandParts.lastIndexOf("--"); - const packageManagerCommand = separatorIndex >= 0 ? commandParts[separatorIndex + 1] : npmCommand.command; - const packageManagerName = packageManagerCommand - ? basename(packageManagerCommand).replace(/\.(cmd|exe)$/i, "") - : ""; - if (packageManagerName !== "pnpm") { + if (this.getPackageManagerName() !== "pnpm") { return undefined; } @@ -1877,14 +1880,31 @@ export class DefaultPackageManager implements PackageManager { return undefined; } - private getNpmInstallPath(source: NpmSource, scope: SourceScope): string { + private getManagedNpmInstallPath(source: NpmSource, scope: SourceScope): string { if (scope === "temporary") { return join(this.getTemporaryDir("npm"), "node_modules", source.name); } if (scope === "project") { return join(this.cwd, CONFIG_DIR_NAME, "npm", "node_modules", source.name); } - return this.getPnpmGlobalPackagePath(source.name) ?? join(this.getGlobalNpmRoot(), source.name); + return join(this.agentDir, "npm", "node_modules", source.name); + } + + private getLegacyGlobalNpmInstallPath(source: NpmSource): string | undefined { + try { + return this.getPnpmGlobalPackagePath(source.name) ?? join(this.getGlobalNpmRoot(), source.name); + } catch { + return undefined; + } + } + + private getNpmInstallPath(source: NpmSource, scope: SourceScope): string { + const managedPath = this.getManagedNpmInstallPath(source, scope); + if (scope !== "user" || existsSync(managedPath)) { + return managedPath; + } + const legacyPath = this.getLegacyGlobalNpmInstallPath(source); + return legacyPath && existsSync(legacyPath) ? legacyPath : managedPath; } private getGitInstallPath(source: GitSource, scope: SourceScope): string { diff --git a/packages/coding-agent/test/package-manager.test.ts b/packages/coding-agent/test/package-manager.test.ts index 7c0fef67..edd2ce95 100644 --- a/packages/coding-agent/test/package-manager.test.ts +++ b/packages/coding-agent/test/package-manager.test.ts @@ -645,7 +645,28 @@ Content`, expect(runCommandSpy).toHaveBeenCalledWith( "mise", - ["exec", "node@20", "--", "npm", "install", "-g", "@scope/pkg"], + ["exec", "node@20", "--", "npm", "install", "@scope/pkg", "--prefix", join(agentDir, "npm")], + undefined, + ); + }); + + it("should use bun --cwd for npm package installs", async () => { + settingsManager = SettingsManager.inMemory({ + npmCommand: ["mise", "exec", "bun@1", "--", "bun"], + }); + packageManager = new DefaultPackageManager({ + cwd: tempDir, + agentDir, + settingsManager, + }); + + const runCommandSpy = vi.spyOn(packageManager as any, "runCommand").mockResolvedValue(undefined); + + await packageManager.install("npm:@scope/pkg"); + + expect(runCommandSpy).toHaveBeenCalledWith( + "mise", + ["exec", "bun@1", "--", "bun", "install", "@scope/pkg", "--cwd", join(agentDir, "npm")], undefined, ); }); @@ -799,7 +820,7 @@ Content`, expect(runCommandSyncSpy).toHaveBeenNthCalledWith(2, "mise", ["exec", "node@22", "--", "npm", "root", "-g"]); }); - it("should resolve pnpm global package paths from pnpm list output", async () => { + it("should install user npm packages into the pi-managed npm root", async () => { settingsManager = SettingsManager.inMemory({ npmCommand: ["pnpm"], packages: ["npm:pnpm-pkg"], @@ -810,38 +831,25 @@ Content`, settingsManager, }); - const pnpmRoot = join(tempDir, "pnpm", "global", "v11"); - const packagePath = join(pnpmRoot, "20-hash", "node_modules", "pnpm-pkg"); - let installed = false; - - vi.spyOn(packageManager as any, "runCommandSync").mockImplementation((...callArgs: unknown[]) => { - const [command, args] = callArgs as [string, string[]]; - if (command !== "pnpm") { - throw new Error(`unexpected command ${command}`); - } - if (args.join(" ") === "list -g --depth 0 --json") { - return JSON.stringify([ - { - path: pnpmRoot, - dependencies: installed ? { "pnpm-pkg": { version: "1.0.0", path: packagePath } } : {}, - }, - ]); - } - if (args.join(" ") === "root -g") { - return pnpmRoot; - } - throw new Error(`unexpected args ${args.join(" ")}`); + const packagePath = join(agentDir, "npm", "node_modules", "pnpm-pkg"); + vi.spyOn(packageManager as any, "runCommandSync").mockImplementation(() => { + throw new Error("legacy lookup unavailable"); }); const runCommandSpy = vi .spyOn(packageManager as any, "runCommand") .mockImplementation(async (...callArgs: unknown[]) => { const [command, args] = callArgs as [string, string[]]; expect(command).toBe("pnpm"); - expect(args).toEqual(["install", "-g", "pnpm-pkg"]); + expect(args).toEqual([ + "install", + "pnpm-pkg", + "--prefix", + join(agentDir, "npm"), + "--config.strict-dep-builds=false", + ]); mkdirSync(join(packagePath, "extensions"), { recursive: true }); writeFileSync(join(packagePath, "package.json"), JSON.stringify({ name: "pnpm-pkg", version: "1.0.0" })); writeFileSync(join(packagePath, "extensions", "index.ts"), "export default function() {};"); - installed = true; }); const first = await packageManager.resolve(); @@ -857,6 +865,49 @@ Content`, expect(packageManager.getInstalledPath("npm:pnpm-pkg", "user")).toBe(packagePath); }); + it("should load legacy pnpm global package paths from pnpm list output", async () => { + settingsManager = SettingsManager.inMemory({ + npmCommand: ["pnpm"], + packages: ["npm:pnpm-pkg"], + }); + packageManager = new DefaultPackageManager({ + cwd: tempDir, + agentDir, + settingsManager, + }); + + const pnpmRoot = join(tempDir, "pnpm", "global", "v11"); + const packagePath = join(pnpmRoot, "20-hash", "node_modules", "pnpm-pkg"); + mkdirSync(join(packagePath, "extensions"), { recursive: true }); + writeFileSync(join(packagePath, "package.json"), JSON.stringify({ name: "pnpm-pkg", version: "1.0.0" })); + writeFileSync(join(packagePath, "extensions", "index.ts"), "export default function() {};"); + + vi.spyOn(packageManager as any, "runCommandSync").mockImplementation((...callArgs: unknown[]) => { + const [command, args] = callArgs as [string, string[]]; + if (command !== "pnpm") { + throw new Error(`unexpected command ${command}`); + } + if (args.join(" ") === "list -g --depth 0 --json") { + return JSON.stringify([ + { + path: pnpmRoot, + dependencies: { "pnpm-pkg": { version: "1.0.0", path: packagePath } }, + }, + ]); + } + throw new Error(`unexpected args ${args.join(" ")}`); + }); + const runCommandSpy = vi.spyOn(packageManager as any, "runCommand").mockResolvedValue(undefined); + + const result = await packageManager.resolve(); + + expect( + result.extensions.some((r) => r.path === join(packagePath, "extensions", "index.ts") && r.enabled), + ).toBe(true); + expect(runCommandSpy).not.toHaveBeenCalled(); + expect(packageManager.getInstalledPath("npm:pnpm-pkg", "user")).toBe(packagePath); + }); + it("should resolve wrapped pnpm global package paths from pnpm list output", () => { settingsManager = SettingsManager.inMemory({ npmCommand: ["mise", "exec", "node@20", "--", "pnpm"], @@ -883,7 +934,7 @@ Content`, expect(packageManager.getInstalledPath("npm:pnpm-pkg", "user")).toBe(packagePath); }); - it("should fail when pnpm global package list is malformed", () => { + it("should ignore malformed legacy pnpm global package lists", () => { settingsManager = SettingsManager.inMemory({ npmCommand: ["pnpm"], }); @@ -895,7 +946,7 @@ Content`, vi.spyOn(packageManager as any, "runCommandSync").mockReturnValue("not json"); - expect(() => packageManager.getInstalledPath("npm:pnpm-pkg", "user")).toThrow(); + expect(packageManager.getInstalledPath("npm:pnpm-pkg", "user")).toBeUndefined(); }); }); @@ -1829,12 +1880,42 @@ export default function(api) { api.registerTool({ name: "test", description: "te expect(runCommandSpy).not.toHaveBeenCalled(); }); - it("should batch npm updates per scope and run git updates in parallel while skipping pinned and current packages", async () => { - vi.spyOn(packageManager as any, "getGlobalNpmRoot").mockReturnValue(join(agentDir, "node_modules")); + it("should migrate legacy user npm installs into the managed npm root during update", async () => { + const legacyRoot = join(tempDir, "legacy-global", "node_modules"); + const legacyPath = join(legacyRoot, "legacy-pkg"); + const managedPath = join(agentDir, "npm", "node_modules", "legacy-pkg"); + mkdirSync(legacyPath, { recursive: true }); + writeFileSync(join(legacyPath, "package.json"), JSON.stringify({ name: "legacy-pkg", version: "1.0.0" })); + settingsManager.setPackages(["npm:legacy-pkg"]); - const userOldPath = join(agentDir, "node_modules", "user-old"); - const userCurrentPath = join(agentDir, "node_modules", "user-current"); - const userUnknownPath = join(agentDir, "node_modules", "user-unknown"); + vi.spyOn(packageManager as any, "getGlobalNpmRoot").mockReturnValue(legacyRoot); + const runCommandCaptureSpy = vi.spyOn(packageManager as any, "runCommandCapture").mockResolvedValue('"1.0.0"'); + const runCommandSpy = vi + .spyOn(packageManager as any, "runCommand") + .mockImplementation(async (...callArgs: unknown[]) => { + const [command, args] = callArgs as [string, string[]]; + expect(command).toBe("npm"); + expect(args).toEqual(["install", "legacy-pkg@latest", "--prefix", join(agentDir, "npm")]); + mkdirSync(managedPath, { recursive: true }); + writeFileSync( + join(managedPath, "package.json"), + JSON.stringify({ name: "legacy-pkg", version: "1.0.0" }), + ); + }); + + expect(packageManager.getInstalledPath("npm:legacy-pkg", "user")).toBe(legacyPath); + + await packageManager.update("npm:legacy-pkg"); + + expect(runCommandCaptureSpy).not.toHaveBeenCalled(); + expect(runCommandSpy).toHaveBeenCalledTimes(1); + expect(packageManager.getInstalledPath("npm:legacy-pkg", "user")).toBe(managedPath); + }); + + it("should batch npm updates per scope and run git updates in parallel while skipping pinned and current packages", async () => { + const userOldPath = join(agentDir, "npm", "node_modules", "user-old"); + const userCurrentPath = join(agentDir, "npm", "node_modules", "user-current"); + const userUnknownPath = join(agentDir, "npm", "node_modules", "user-unknown"); const projectOldPath = join(tempDir, ".pi", "npm", "node_modules", "project-old"); const projectCurrentPath = join(tempDir, ".pi", "npm", "node_modules", "project-current"); const installPaths = [userOldPath, userCurrentPath, userUnknownPath, projectOldPath, projectCurrentPath]; @@ -1924,7 +2005,7 @@ export default function(api) { api.registerTool({ name: "test", description: "te expect(runCommandSpy).toHaveBeenNthCalledWith( 1, "npm", - ["install", "-g", "user-old@latest", "user-unknown@latest"], + ["install", "user-old@latest", "user-unknown@latest", "--prefix", join(agentDir, "npm")], undefined, ); expect(runCommandSpy).toHaveBeenNthCalledWith( From c9e70492125806053e0ea31ad00eef6822d4a268 Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Sun, 17 May 2026 20:51:26 +0200 Subject: [PATCH 075/166] fix(coding-agent): remove global fetch override closes #4619 --- package-lock.json | 20 ++++++++++---------- package.json | 2 +- packages/agent/CHANGELOG.md | 6 ++++++ packages/agent/package.json | 2 +- packages/ai/CHANGELOG.md | 4 ++++ packages/ai/package.json | 2 +- packages/coding-agent/CHANGELOG.md | 6 ++++++ packages/coding-agent/package.json | 4 ++-- packages/coding-agent/src/cli.ts | 13 ++----------- packages/tui/CHANGELOG.md | 6 ++++++ packages/tui/package.json | 2 +- 11 files changed, 40 insertions(+), 27 deletions(-) diff --git a/package-lock.json b/package-lock.json index 08d0d5ae..ba9678e7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -31,7 +31,7 @@ "typescript": "^5.9.2" }, "engines": { - "node": ">=20.0.0" + "node": ">=22.19.0" } }, "node_modules/@ampproject/remapping": { @@ -6842,12 +6842,12 @@ } }, "node_modules/undici": { - "version": "7.25.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-7.25.0.tgz", - "integrity": "sha512-xXnp4kTyor2Zq+J1FfPI6Eq3ew5h6Vl0F/8d9XU5zZQf1tX9s2Su1/3PiMmUANFULpmksxkClamIZcaUqryHsQ==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-8.3.0.tgz", + "integrity": "sha512-TkUDgb6tl7KOGZ+7e8E3d2FYgUQgF6z5YypqjWmixVQSQERFcVrVg0ySADm2LVLRh5ljAaHTCR5Fmz3Q34rB7Q==", "license": "MIT", "engines": { - "node": ">=20.18.1" + "node": ">=22.19.0" } }, "node_modules/undici-types": { @@ -7787,7 +7787,7 @@ "vitest": "^3.2.4" }, "engines": { - "node": ">=20.0.0" + "node": ">=22.19.0" } }, "packages/agent/node_modules/@types/node": { @@ -7831,7 +7831,7 @@ "vitest": "^3.2.4" }, "engines": { - "node": ">=20.0.0" + "node": ">=22.19.0" } }, "packages/ai/node_modules/@types/node": { @@ -7870,7 +7870,7 @@ "minimatch": "^10.2.3", "proper-lockfile": "^4.1.2", "typebox": "^1.1.24", - "undici": "^7.19.1", + "undici": "^8.3.0", "yaml": "^2.8.2" }, "bin": { @@ -7887,7 +7887,7 @@ "vitest": "^3.2.4" }, "engines": { - "node": ">=20.6.0" + "node": ">=22.19.0" }, "optionalDependencies": { "@mariozechner/clipboard": "^0.3.6" @@ -7961,7 +7961,7 @@ "chalk": "^5.5.0" }, "engines": { - "node": ">=20.0.0" + "node": ">=22.19.0" }, "optionalDependencies": { "koffi": "^2.9.0" diff --git a/package.json b/package.json index 288ed3a2..af486099 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,7 @@ "shx": "^0.4.0" }, "engines": { - "node": ">=20.0.0" + "node": ">=22.19.0" }, "version": "0.0.3", "dependencies": { diff --git a/packages/agent/CHANGELOG.md b/packages/agent/CHANGELOG.md index 923f7298..21535883 100644 --- a/packages/agent/CHANGELOG.md +++ b/packages/agent/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## [Unreleased] + +### Breaking Changes + +- Raised the minimum supported Node.js version to 22.19.0. + ## [0.74.1] - 2026-05-16 ## [0.74.0] - 2026-05-07 diff --git a/packages/agent/package.json b/packages/agent/package.json index b307e762..41b92276 100644 --- a/packages/agent/package.json +++ b/packages/agent/package.json @@ -50,7 +50,7 @@ "directory": "packages/agent" }, "engines": { - "node": ">=20.0.0" + "node": ">=22.19.0" }, "devDependencies": { "@types/node": "^24.3.0", diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index 18759b97..b8a94e21 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Breaking Changes + +- Raised the minimum supported Node.js version to 22.19.0. + ### Fixed - Fixed `streamSimple()` defaults for models whose advertised output limit is effectively their full context window to avoid impossible default requests ([#4614](https://github.com/earendil-works/pi/issues/4614)). diff --git a/packages/ai/package.json b/packages/ai/package.json index 6d3f3fef..b72af4d5 100644 --- a/packages/ai/package.json +++ b/packages/ai/package.json @@ -97,7 +97,7 @@ "directory": "packages/ai" }, "engines": { - "node": ">=20.0.0" + "node": ">=22.19.0" }, "devDependencies": { "@types/node": "^24.3.0", diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index b1acdb37..53052c1b 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -2,9 +2,15 @@ ## [Unreleased] +### Breaking Changes + +- Raised the minimum supported Node.js version to 22.19.0. + ### Fixed - Fixed user-scoped npm pi packages to install under `~/.pi/agent/npm/` instead of npm's global package root, avoiding permission errors with system-managed Node installs ([#4587](https://github.com/earendil-works/pi/issues/4587)). +- Fixed Mistral requests failing after the global fetch proxy/timeout workaround by removing the custom fetch override and using undici 8 dispatcher support instead ([#4619](https://github.com/earendil-works/pi/issues/4619)). +- Fixed default output token requests for models whose advertised output limit is effectively their full context window, avoiding impossible provider requests inherited from `@earendil-works/pi-ai` ([#4614](https://github.com/earendil-works/pi/issues/4614)). ## [0.74.1] - 2026-05-16 diff --git a/packages/coding-agent/package.json b/packages/coding-agent/package.json index 98e50c6e..356b42c1 100644 --- a/packages/coding-agent/package.json +++ b/packages/coding-agent/package.json @@ -52,7 +52,7 @@ "minimatch": "^10.2.3", "proper-lockfile": "^4.1.2", "typebox": "^1.1.24", - "undici": "^7.19.1", + "undici": "^8.3.0", "yaml": "^2.8.2" }, "overrides": { @@ -90,6 +90,6 @@ "directory": "packages/coding-agent" }, "engines": { - "node": ">=20.6.0" + "node": ">=22.19.0" } } diff --git a/packages/coding-agent/src/cli.ts b/packages/coding-agent/src/cli.ts index dcfb4bba..6e0e8403 100644 --- a/packages/coding-agent/src/cli.ts +++ b/packages/coding-agent/src/cli.ts @@ -5,7 +5,7 @@ * * Test with: npx tsx src/cli-new.ts [args...] */ -import { EnvHttpProxyAgent, setGlobalDispatcher, fetch as undiciFetch } from "undici"; +import { EnvHttpProxyAgent, setGlobalDispatcher } from "undici"; import { APP_NAME } from "./config.js"; import { main } from "./main.js"; @@ -17,15 +17,6 @@ process.emitWarning = (() => {}) as typeof process.emitWarning; // (e.g. vLLM buffering a large tool call) exceed that and abort the SSE stream // with UND_ERR_BODY_TIMEOUT. Disable both — provider SDKs enforce their own // AbortController-based deadlines via retry.provider.timeoutMs. -// Node 26 uses an internal undici for globalThis.fetch that does not honor npm -// undici's global dispatcher, so route global fetch through npm undici as well. -const dispatcher = new EnvHttpProxyAgent({ bodyTimeout: 0, headersTimeout: 0 }); -setGlobalDispatcher(dispatcher); -const fetchWithDispatcher = undiciFetch as unknown as typeof fetch; -globalThis.fetch = (input, init) => - fetchWithDispatcher(input, { - ...init, - dispatcher, - } as unknown as RequestInit); +setGlobalDispatcher(new EnvHttpProxyAgent({ bodyTimeout: 0, headersTimeout: 0 })); main(process.argv.slice(2)); diff --git a/packages/tui/CHANGELOG.md b/packages/tui/CHANGELOG.md index 44b716f9..08fe3e5e 100644 --- a/packages/tui/CHANGELOG.md +++ b/packages/tui/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## [Unreleased] + +### Breaking Changes + +- Raised the minimum supported Node.js version to 22.19.0. + ## [0.74.1] - 2026-05-16 ### Added diff --git a/packages/tui/package.json b/packages/tui/package.json index 3896eb88..c52be27f 100644 --- a/packages/tui/package.json +++ b/packages/tui/package.json @@ -32,7 +32,7 @@ "directory": "packages/tui" }, "engines": { - "node": ">=20.0.0" + "node": ">=22.19.0" }, "types": "./dist/index.d.ts", "dependencies": { From d5ebc973d0937f01ea2268c5daa764b07a9ff1ea Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Sun, 17 May 2026 21:00:25 +0200 Subject: [PATCH 076/166] chore: audit unreleased changelog entries --- packages/ai/CHANGELOG.md | 2 ++ packages/coding-agent/CHANGELOG.md | 3 +++ 2 files changed, 5 insertions(+) diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index b8a94e21..264fcb11 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -8,6 +8,8 @@ ### Fixed +- Fixed OpenAI Codex generated model metadata to use the current upstream model list ([#4603](https://github.com/earendil-works/pi-mono/pull/4603) by [@mattiacerutti](https://github.com/mattiacerutti)). +- Fixed GitHub Copilot GPT model thinking metadata to map unsupported minimal thinking to low ([#4622](https://github.com/earendil-works/pi-mono/pull/4622) by [@mattiacerutti](https://github.com/mattiacerutti)). - Fixed `streamSimple()` defaults for models whose advertised output limit is effectively their full context window to avoid impossible default requests ([#4614](https://github.com/earendil-works/pi/issues/4614)). ## [0.74.1] - 2026-05-16 diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 4d2115c0..7b9a9181 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -9,6 +9,9 @@ ### Fixed - Fixed compaction summary calls to use custom agent stream functions, preserving proxy-backed LLM routing ([#4484](https://github.com/earendil-works/pi/issues/4484)). +- Fixed system prompt and context file boundaries to use explicit XML tags instead of Markdown headings, reducing inconsistent boundary ingestion by models ([#4541](https://github.com/earendil-works/pi-mono/pull/4541) by [@herrnel](https://github.com/herrnel)). +- Fixed OpenAI Codex generated model metadata to use the current upstream model list inherited from `@earendil-works/pi-ai` ([#4603](https://github.com/earendil-works/pi-mono/pull/4603) by [@mattiacerutti](https://github.com/mattiacerutti)). +- Fixed GitHub Copilot GPT model thinking metadata inherited from `@earendil-works/pi-ai` to map unsupported minimal thinking to low ([#4622](https://github.com/earendil-works/pi-mono/pull/4622) by [@mattiacerutti](https://github.com/mattiacerutti)). - Fixed user-scoped npm pi packages to install under `~/.pi/agent/npm/` instead of npm's global package root, avoiding permission errors with system-managed Node installs ([#4587](https://github.com/earendil-works/pi/issues/4587)). - Fixed Mistral requests failing after the global fetch proxy/timeout workaround by removing the custom fetch override and using undici 8 dispatcher support instead ([#4619](https://github.com/earendil-works/pi/issues/4619)). - Fixed default output token requests for models whose advertised output limit is effectively their full context window, avoiding impossible provider requests inherited from `@earendil-works/pi-ai` ([#4614](https://github.com/earendil-works/pi/issues/4614)). From 12f5c00cc1332e04d18e2120b71731627174bcc7 Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Sun, 17 May 2026 21:02:49 +0200 Subject: [PATCH 077/166] Release v0.75.0 --- package-lock.json | 63 +++++++++---------- packages/agent/CHANGELOG.md | 2 +- packages/agent/package.json | 4 +- packages/ai/CHANGELOG.md | 2 +- packages/ai/package.json | 2 +- packages/coding-agent/CHANGELOG.md | 2 +- .../package-lock.json | 4 +- .../custom-provider-anthropic/package.json | 2 +- .../custom-provider-gitlab-duo/package.json | 2 +- .../extensions/sandbox/package-lock.json | 4 +- .../examples/extensions/sandbox/package.json | 2 +- .../extensions/with-deps/package-lock.json | 4 +- .../extensions/with-deps/package.json | 2 +- packages/coding-agent/package.json | 8 +-- packages/tui/CHANGELOG.md | 2 +- packages/tui/package.json | 2 +- packages/web-ui/example/package.json | 2 +- packages/web-ui/package.json | 6 +- 18 files changed, 57 insertions(+), 58 deletions(-) diff --git a/package-lock.json b/package-lock.json index ba9678e7..776469fa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2346,13 +2346,12 @@ "license": "BSD-3-Clause" }, "node_modules/@protobufjs/fetch": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", - "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.1.tgz", + "integrity": "sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw==", "license": "BSD-3-Clause", "dependencies": { - "@protobufjs/aspromise": "^1.1.1", - "@protobufjs/inquire": "^1.1.0" + "@protobufjs/aspromise": "^1.1.1" } }, "node_modules/@protobufjs/float": { @@ -2362,9 +2361,9 @@ "license": "BSD-3-Clause" }, "node_modules/@protobufjs/inquire": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.1.tgz", - "integrity": "sha512-mnzgDV26ueAvk7rsbt9L7bE0SuAoqyuys/sMMrmVcN5x9VsxpcG3rqAUSgDyLp0UZlmNfIbQ4fHfCtreVBk8Ew==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.2.tgz", + "integrity": "sha512-pa0vFRuws4wkvaXKK1uXZMAwAX4/t8ANaJo45iw/oQHNQ9q5xUzwgFmVJGXiga2BeN+zpX7Vf9vmsiIa2J+MUw==", "license": "BSD-3-Clause" }, "node_modules/@protobufjs/path": { @@ -5901,9 +5900,9 @@ } }, "node_modules/protobufjs": { - "version": "7.5.8", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.8.tgz", - "integrity": "sha512-dvpCIeLPbXZS/Ete7yLaO7RenOdken2NHKykBXbsaGxZT0UTltcarBciw+A78SRQs9iMAAVpsYA+l8b1hTePIA==", + "version": "7.5.9", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.9.tgz", + "integrity": "sha512-Od4muIm3HW1AouyHF5lONOf1FWo3hY1NbFDoy191X9GzhpgW1clCoaFjfVs2rKJNFYpTNJbje4cbAIDBZJ63ZA==", "hasInstallScript": true, "license": "BSD-3-Clause", "dependencies": { @@ -5911,9 +5910,9 @@ "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.5", "@protobufjs/eventemitter": "^1.1.0", - "@protobufjs/fetch": "^1.1.0", + "@protobufjs/fetch": "^1.1.1", "@protobufjs/float": "^1.0.2", - "@protobufjs/inquire": "^1.1.1", + "@protobufjs/inquire": "^1.1.2", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.1", @@ -6781,9 +6780,9 @@ "license": "0BSD" }, "node_modules/tsx": { - "version": "4.22.0", - "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.22.0.tgz", - "integrity": "sha512-8ccZMPD69s1AbKXx0C5ddTNZfNjwV04iIKgjZmKfKxMynEtSYcK0Lh7iQFh53fI5Yu4pb9usgAiqyPmEONaALg==", + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.22.1.tgz", + "integrity": "sha512-TvncJykhxAzFCk0VQZKBTClall4Pm7qXDSodb6uxi8QFa8X8mT6ABjxxsQ2opDRYxG7AzcRWXaFtruz5HJKuWg==", "dev": true, "license": "MIT", "dependencies": { @@ -7772,10 +7771,10 @@ }, "packages/agent": { "name": "@earendil-works/pi-agent-core", - "version": "0.74.1", + "version": "0.75.0", "license": "MIT", "dependencies": { - "@earendil-works/pi-ai": "^0.74.1", + "@earendil-works/pi-ai": "^0.75.0", "ignore": "^7.0.5", "typebox": "^1.1.24", "yaml": "^2.8.2" @@ -7809,7 +7808,7 @@ }, "packages/ai": { "name": "@earendil-works/pi-ai", - "version": "0.74.1", + "version": "0.75.0", "license": "MIT", "dependencies": { "@anthropic-ai/sdk": "^0.91.1", @@ -7853,12 +7852,12 @@ }, "packages/coding-agent": { "name": "@earendil-works/pi-coding-agent", - "version": "0.74.1", + "version": "0.75.0", "license": "MIT", "dependencies": { - "@earendil-works/pi-agent-core": "^0.74.1", - "@earendil-works/pi-ai": "^0.74.1", - "@earendil-works/pi-tui": "^0.74.1", + "@earendil-works/pi-agent-core": "^0.75.0", + "@earendil-works/pi-ai": "^0.75.0", + "@earendil-works/pi-tui": "^0.75.0", "@silvia-odwyer/photon-node": "^0.3.4", "chalk": "^5.5.0", "diff": "^8.0.2", @@ -7895,25 +7894,25 @@ }, "packages/coding-agent/examples/extensions/custom-provider-anthropic": { "name": "pi-extension-custom-provider-anthropic", - "version": "0.74.1", + "version": "0.75.0", "dependencies": { "@anthropic-ai/sdk": "^0.52.0" } }, "packages/coding-agent/examples/extensions/custom-provider-gitlab-duo": { "name": "pi-extension-custom-provider-gitlab-duo", - "version": "0.74.1" + "version": "0.75.0" }, "packages/coding-agent/examples/extensions/sandbox": { "name": "pi-extension-sandbox", - "version": "1.4.1", + "version": "1.5.0", "dependencies": { "@anthropic-ai/sandbox-runtime": "^0.0.26" } }, "packages/coding-agent/examples/extensions/with-deps": { "name": "pi-extension-with-deps", - "version": "0.74.1", + "version": "0.75.0", "dependencies": { "ms": "^2.1.3" }, @@ -7949,7 +7948,7 @@ }, "packages/tui": { "name": "@earendil-works/pi-tui", - "version": "0.74.1", + "version": "0.75.0", "license": "MIT", "dependencies": { "get-east-asian-width": "^1.3.0", @@ -7969,11 +7968,11 @@ }, "packages/web-ui": { "name": "@earendil-works/pi-web-ui", - "version": "0.74.1", + "version": "0.75.0", "license": "MIT", "dependencies": { - "@earendil-works/pi-ai": "^0.74.1", - "@earendil-works/pi-tui": "^0.74.1", + "@earendil-works/pi-ai": "^0.75.0", + "@earendil-works/pi-tui": "^0.75.0", "@lmstudio/sdk": "^1.5.0", "docx-preview": "^0.3.7", "jszip": "^3.10.1", @@ -7996,7 +7995,7 @@ }, "packages/web-ui/example": { "name": "pi-web-ui-example", - "version": "0.74.1", + "version": "0.75.0", "dependencies": { "@earendil-works/pi-ai": "file:../../ai", "@earendil-works/pi-web-ui": "file:../", diff --git a/packages/agent/CHANGELOG.md b/packages/agent/CHANGELOG.md index 21535883..bfd1393f 100644 --- a/packages/agent/CHANGELOG.md +++ b/packages/agent/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## [Unreleased] +## [0.75.0] - 2026-05-17 ### Breaking Changes diff --git a/packages/agent/package.json b/packages/agent/package.json index 41b92276..6e6685f6 100644 --- a/packages/agent/package.json +++ b/packages/agent/package.json @@ -1,6 +1,6 @@ { "name": "@earendil-works/pi-agent-core", - "version": "0.74.1", + "version": "0.75.0", "description": "General-purpose agent with transport abstraction, state management, and attachment support", "type": "module", "main": "./dist/index.js", @@ -30,7 +30,7 @@ "prepublishOnly": "npm run clean && npm run build" }, "dependencies": { - "@earendil-works/pi-ai": "^0.74.1", + "@earendil-works/pi-ai": "^0.75.0", "ignore": "^7.0.5", "typebox": "^1.1.24", "yaml": "^2.8.2" diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index 264fcb11..031df819 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## [Unreleased] +## [0.75.0] - 2026-05-17 ### Breaking Changes diff --git a/packages/ai/package.json b/packages/ai/package.json index b72af4d5..eedc1de4 100644 --- a/packages/ai/package.json +++ b/packages/ai/package.json @@ -1,6 +1,6 @@ { "name": "@earendil-works/pi-ai", - "version": "0.74.1", + "version": "0.75.0", "description": "Unified LLM API with automatic model discovery and provider configuration", "type": "module", "main": "./dist/index.js", diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 7b9a9181..7cfa31d7 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## [Unreleased] +## [0.75.0] - 2026-05-17 ### Breaking Changes diff --git a/packages/coding-agent/examples/extensions/custom-provider-anthropic/package-lock.json b/packages/coding-agent/examples/extensions/custom-provider-anthropic/package-lock.json index e3698b2a..2321b707 100644 --- a/packages/coding-agent/examples/extensions/custom-provider-anthropic/package-lock.json +++ b/packages/coding-agent/examples/extensions/custom-provider-anthropic/package-lock.json @@ -1,12 +1,12 @@ { "name": "pi-extension-custom-provider", - "version": "0.74.1", + "version": "0.75.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pi-extension-custom-provider", - "version": "0.74.1", + "version": "0.75.0", "dependencies": { "@anthropic-ai/sdk": "^0.52.0" } diff --git a/packages/coding-agent/examples/extensions/custom-provider-anthropic/package.json b/packages/coding-agent/examples/extensions/custom-provider-anthropic/package.json index cb5dc9b5..a7c10d75 100644 --- a/packages/coding-agent/examples/extensions/custom-provider-anthropic/package.json +++ b/packages/coding-agent/examples/extensions/custom-provider-anthropic/package.json @@ -1,7 +1,7 @@ { "name": "pi-extension-custom-provider-anthropic", "private": true, - "version": "0.74.1", + "version": "0.75.0", "type": "module", "scripts": { "clean": "echo 'nothing to clean'", diff --git a/packages/coding-agent/examples/extensions/custom-provider-gitlab-duo/package.json b/packages/coding-agent/examples/extensions/custom-provider-gitlab-duo/package.json index 3da12995..4287ccc7 100644 --- a/packages/coding-agent/examples/extensions/custom-provider-gitlab-duo/package.json +++ b/packages/coding-agent/examples/extensions/custom-provider-gitlab-duo/package.json @@ -1,7 +1,7 @@ { "name": "pi-extension-custom-provider-gitlab-duo", "private": true, - "version": "0.74.1", + "version": "0.75.0", "type": "module", "scripts": { "clean": "echo 'nothing to clean'", diff --git a/packages/coding-agent/examples/extensions/sandbox/package-lock.json b/packages/coding-agent/examples/extensions/sandbox/package-lock.json index ebe657e7..62baeba0 100644 --- a/packages/coding-agent/examples/extensions/sandbox/package-lock.json +++ b/packages/coding-agent/examples/extensions/sandbox/package-lock.json @@ -1,12 +1,12 @@ { "name": "pi-extension-sandbox", - "version": "1.4.1", + "version": "1.5.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pi-extension-sandbox", - "version": "1.4.1", + "version": "1.5.0", "dependencies": { "@anthropic-ai/sandbox-runtime": "^0.0.26" } diff --git a/packages/coding-agent/examples/extensions/sandbox/package.json b/packages/coding-agent/examples/extensions/sandbox/package.json index 98ee8b0e..6c548f4e 100644 --- a/packages/coding-agent/examples/extensions/sandbox/package.json +++ b/packages/coding-agent/examples/extensions/sandbox/package.json @@ -1,7 +1,7 @@ { "name": "pi-extension-sandbox", "private": true, - "version": "1.4.1", + "version": "1.5.0", "type": "module", "scripts": { "clean": "echo 'nothing to clean'", diff --git a/packages/coding-agent/examples/extensions/with-deps/package-lock.json b/packages/coding-agent/examples/extensions/with-deps/package-lock.json index 2eed67d8..bed97766 100644 --- a/packages/coding-agent/examples/extensions/with-deps/package-lock.json +++ b/packages/coding-agent/examples/extensions/with-deps/package-lock.json @@ -1,12 +1,12 @@ { "name": "pi-extension-with-deps", - "version": "0.74.1", + "version": "0.75.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pi-extension-with-deps", - "version": "0.74.1", + "version": "0.75.0", "dependencies": { "ms": "^2.1.3" }, diff --git a/packages/coding-agent/examples/extensions/with-deps/package.json b/packages/coding-agent/examples/extensions/with-deps/package.json index 022c0414..77548470 100644 --- a/packages/coding-agent/examples/extensions/with-deps/package.json +++ b/packages/coding-agent/examples/extensions/with-deps/package.json @@ -1,7 +1,7 @@ { "name": "pi-extension-with-deps", "private": true, - "version": "0.74.1", + "version": "0.75.0", "type": "module", "scripts": { "clean": "echo 'nothing to clean'", diff --git a/packages/coding-agent/package.json b/packages/coding-agent/package.json index 356b42c1..028b47b7 100644 --- a/packages/coding-agent/package.json +++ b/packages/coding-agent/package.json @@ -1,6 +1,6 @@ { "name": "@earendil-works/pi-coding-agent", - "version": "0.74.1", + "version": "0.75.0", "description": "Coding agent CLI with read, bash, edit, write tools and session management", "type": "module", "piConfig": { @@ -38,9 +38,9 @@ "prepublishOnly": "npm run clean && npm run build" }, "dependencies": { - "@earendil-works/pi-agent-core": "^0.74.1", - "@earendil-works/pi-ai": "^0.74.1", - "@earendil-works/pi-tui": "^0.74.1", + "@earendil-works/pi-agent-core": "^0.75.0", + "@earendil-works/pi-ai": "^0.75.0", + "@earendil-works/pi-tui": "^0.75.0", "@silvia-odwyer/photon-node": "^0.3.4", "chalk": "^5.5.0", "diff": "^8.0.2", diff --git a/packages/tui/CHANGELOG.md b/packages/tui/CHANGELOG.md index 08fe3e5e..f317954b 100644 --- a/packages/tui/CHANGELOG.md +++ b/packages/tui/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## [Unreleased] +## [0.75.0] - 2026-05-17 ### Breaking Changes diff --git a/packages/tui/package.json b/packages/tui/package.json index c52be27f..a16e36a5 100644 --- a/packages/tui/package.json +++ b/packages/tui/package.json @@ -1,6 +1,6 @@ { "name": "@earendil-works/pi-tui", - "version": "0.74.1", + "version": "0.75.0", "description": "Terminal User Interface library with differential rendering for efficient text-based applications", "type": "module", "main": "dist/index.js", diff --git a/packages/web-ui/example/package.json b/packages/web-ui/example/package.json index 7225e321..604061da 100644 --- a/packages/web-ui/example/package.json +++ b/packages/web-ui/example/package.json @@ -1,6 +1,6 @@ { "name": "pi-web-ui-example", - "version": "0.74.1", + "version": "0.75.0", "private": true, "type": "module", "scripts": { diff --git a/packages/web-ui/package.json b/packages/web-ui/package.json index 156866e5..bff9345e 100644 --- a/packages/web-ui/package.json +++ b/packages/web-ui/package.json @@ -1,6 +1,6 @@ { "name": "@earendil-works/pi-web-ui", - "version": "0.74.1", + "version": "0.75.0", "description": "Reusable web UI components for AI chat interfaces powered by @earendil-works/pi-ai", "type": "module", "main": "dist/index.js", @@ -18,8 +18,8 @@ }, "dependencies": { "@lmstudio/sdk": "^1.5.0", - "@earendil-works/pi-ai": "^0.74.1", - "@earendil-works/pi-tui": "^0.74.1", + "@earendil-works/pi-ai": "^0.75.0", + "@earendil-works/pi-tui": "^0.75.0", "typebox": "^1.1.24", "docx-preview": "^0.3.7", "jszip": "^3.10.1", From 7f3c340dc618892775c494e0bd4d03ed08bbe54b Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Sun, 17 May 2026 21:03:41 +0200 Subject: [PATCH 078/166] Add [Unreleased] section for next cycle --- packages/agent/CHANGELOG.md | 2 ++ packages/ai/CHANGELOG.md | 2 ++ packages/ai/src/models.generated.ts | 20 ++++++++++---------- packages/coding-agent/CHANGELOG.md | 2 ++ packages/tui/CHANGELOG.md | 2 ++ packages/web-ui/CHANGELOG.md | 2 ++ 6 files changed, 20 insertions(+), 10 deletions(-) diff --git a/packages/agent/CHANGELOG.md b/packages/agent/CHANGELOG.md index bfd1393f..5bc519da 100644 --- a/packages/agent/CHANGELOG.md +++ b/packages/agent/CHANGELOG.md @@ -1,5 +1,7 @@ # Changelog +## [Unreleased] + ## [0.75.0] - 2026-05-17 ### Breaking Changes diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index 031df819..19ae4c71 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -1,5 +1,7 @@ # Changelog +## [Unreleased] + ## [0.75.0] - 2026-05-17 ### Breaking Changes diff --git a/packages/ai/src/models.generated.ts b/packages/ai/src/models.generated.ts index 2d4de7b4..9ad1d624 100644 --- a/packages/ai/src/models.generated.ts +++ b/packages/ai/src/models.generated.ts @@ -9291,13 +9291,13 @@ export const MODELS = { reasoning: true, input: ["text", "image"], cost: { - input: 0.07, - output: 0.33999999999999997, + input: 0.06, + output: 0.33, cacheRead: 0, cacheWrite: 0, }, contextWindow: 262144, - maxTokens: 16384, + maxTokens: 4096, } satisfies Model<"openai-completions">, "google/gemma-4-26b-a4b-it:free": { id: "google/gemma-4-26b-a4b-it:free", @@ -9888,12 +9888,12 @@ export const MODELS = { input: ["text"], cost: { input: 0.02, - output: 0.04, + output: 0.03, cacheRead: 0, cacheWrite: 0, }, contextWindow: 131072, - maxTokens: 16384, + maxTokens: 4096, } satisfies Model<"openai-completions">, "mistralai/mistral-saba": { id: "mistralai/mistral-saba", @@ -10176,13 +10176,13 @@ export const MODELS = { reasoning: true, input: ["text"], cost: { - input: 0.09999999999999999, - output: 0.5, + input: 0.09, + output: 0.44999999999999996, cacheRead: 0, cacheWrite: 0, }, contextWindow: 1000000, - maxTokens: 16384, + maxTokens: 4096, } satisfies Model<"openai-completions">, "nvidia/nemotron-3-super-120b-a12b:free": { id: "nvidia/nemotron-3-super-120b-a12b:free", @@ -11005,12 +11005,12 @@ export const MODELS = { input: ["text"], cost: { input: 0.039, - output: 0.19, + output: 0.18, cacheRead: 0, cacheWrite: 0, }, contextWindow: 131072, - maxTokens: 131072, + maxTokens: 4096, } satisfies Model<"openai-completions">, "openai/gpt-oss-120b:free": { id: "openai/gpt-oss-120b:free", diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 7cfa31d7..f004f107 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -1,5 +1,7 @@ # Changelog +## [Unreleased] + ## [0.75.0] - 2026-05-17 ### Breaking Changes diff --git a/packages/tui/CHANGELOG.md b/packages/tui/CHANGELOG.md index f317954b..4457e0f6 100644 --- a/packages/tui/CHANGELOG.md +++ b/packages/tui/CHANGELOG.md @@ -1,5 +1,7 @@ # Changelog +## [Unreleased] + ## [0.75.0] - 2026-05-17 ### Breaking Changes diff --git a/packages/web-ui/CHANGELOG.md b/packages/web-ui/CHANGELOG.md index 0359ca55..26a984b8 100644 --- a/packages/web-ui/CHANGELOG.md +++ b/packages/web-ui/CHANGELOG.md @@ -1,5 +1,7 @@ # Changelog +## [Unreleased] + ## [0.74.1] - 2026-05-16 ## [0.74.0] - 2026-05-07 From b256ac7d7733b56e11d4c691378705c929978f15 Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Sun, 17 May 2026 23:55:25 +0200 Subject: [PATCH 079/166] Closes #4342 --- packages/ai/src/providers/anthropic.ts | 1 + packages/ai/test/oauth.ts | 7 ++++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/ai/src/providers/anthropic.ts b/packages/ai/src/providers/anthropic.ts index 0897e8bc..4079b149 100644 --- a/packages/ai/src/providers/anthropic.ts +++ b/packages/ai/src/providers/anthropic.ts @@ -863,6 +863,7 @@ function createClient( sessionId && getAnthropicCompat(model).sendSessionAffinityHeaders ? { "x-session-affinity": sessionId } : {}; const client = new Anthropic({ apiKey, + authToken: null, baseURL: model.baseUrl, dangerouslyAllowBrowser: true, defaultHeaders: mergeHeaders( diff --git a/packages/ai/test/oauth.ts b/packages/ai/test/oauth.ts index bd32f3d6..8dd302a7 100644 --- a/packages/ai/test/oauth.ts +++ b/packages/ai/test/oauth.ts @@ -74,7 +74,12 @@ export async function resolveApiKey(provider: string): Promise Date: Mon, 18 May 2026 00:02:52 +0200 Subject: [PATCH 080/166] Remove openai-codex fast model variants, they do not work --- packages/ai/scripts/generate-models.ts | 38 +----------------- packages/ai/src/models.generated.ts | 54 -------------------------- 2 files changed, 1 insertion(+), 91 deletions(-) diff --git a/packages/ai/scripts/generate-models.ts b/packages/ai/scripts/generate-models.ts index b86d117b..5a694a20 100644 --- a/packages/ai/scripts/generate-models.ts +++ b/packages/ai/scripts/generate-models.ts @@ -1545,18 +1545,6 @@ async function generateModels() { contextWindow: CODEX_CONTEXT, maxTokens: CODEX_MAX_TOKENS, }, - { - id: "gpt-5.4-fast", - name: "GPT-5.4 Fast", - api: "openai-codex-responses", - provider: "openai-codex", - baseUrl: CODEX_BASE_URL, - reasoning: true, - input: ["text", "image"], - cost: { input: 5, output: 30, cacheRead: 0.5, cacheWrite: 0 }, - contextWindow: CODEX_CONTEXT, - maxTokens: CODEX_MAX_TOKENS, - }, { id: "gpt-5.4-mini", name: "GPT-5.4 mini", @@ -1569,18 +1557,6 @@ async function generateModels() { contextWindow: CODEX_CONTEXT, maxTokens: CODEX_MAX_TOKENS, }, - { - id: "gpt-5.4-mini-fast", - name: "GPT-5.4 mini Fast", - api: "openai-codex-responses", - provider: "openai-codex", - baseUrl: CODEX_BASE_URL, - reasoning: true, - input: ["text", "image"], - cost: { input: 1.5, output: 9, cacheRead: 0.15, cacheWrite: 0 }, - contextWindow: CODEX_CONTEXT, - maxTokens: CODEX_MAX_TOKENS, - }, { id: "gpt-5.5", name: "GPT-5.5", @@ -1593,23 +1569,11 @@ async function generateModels() { contextWindow: CODEX_CONTEXT, maxTokens: CODEX_MAX_TOKENS, }, - { - id: "gpt-5.5-fast", - name: "GPT-5.5 Fast", - api: "openai-codex-responses", - provider: "openai-codex", - baseUrl: CODEX_BASE_URL, - reasoning: true, - input: ["text", "image"], - cost: { input: 12.5, output: 75, cacheRead: 1.25, cacheWrite: 0 }, - contextWindow: CODEX_CONTEXT, - maxTokens: CODEX_MAX_TOKENS, - }, ]; allModels.push(...codexModels); // Add missing Grok models - const missingGrokModels: Model[] = [ + const missingGrokModels: Model<"openai-completions">[] = [ { id: "grok-3", name: "Grok 3", diff --git a/packages/ai/src/models.generated.ts b/packages/ai/src/models.generated.ts index 9ad1d624..791857bf 100644 --- a/packages/ai/src/models.generated.ts +++ b/packages/ai/src/models.generated.ts @@ -7227,24 +7227,6 @@ export const MODELS = { contextWindow: 272000, maxTokens: 128000, } satisfies Model<"openai-codex-responses">, - "gpt-5.4-fast": { - id: "gpt-5.4-fast", - name: "GPT-5.4 Fast", - api: "openai-codex-responses", - provider: "openai-codex", - baseUrl: "https://chatgpt.com/backend-api", - reasoning: true, - thinkingLevelMap: {"xhigh":"xhigh","minimal":"low"}, - input: ["text", "image"], - cost: { - input: 5, - output: 30, - cacheRead: 0.5, - cacheWrite: 0, - }, - contextWindow: 272000, - maxTokens: 128000, - } satisfies Model<"openai-codex-responses">, "gpt-5.4-mini": { id: "gpt-5.4-mini", name: "GPT-5.4 mini", @@ -7263,24 +7245,6 @@ export const MODELS = { contextWindow: 272000, maxTokens: 128000, } satisfies Model<"openai-codex-responses">, - "gpt-5.4-mini-fast": { - id: "gpt-5.4-mini-fast", - name: "GPT-5.4 mini Fast", - api: "openai-codex-responses", - provider: "openai-codex", - baseUrl: "https://chatgpt.com/backend-api", - reasoning: true, - thinkingLevelMap: {"xhigh":"xhigh","minimal":"low"}, - input: ["text", "image"], - cost: { - input: 1.5, - output: 9, - cacheRead: 0.15, - cacheWrite: 0, - }, - contextWindow: 272000, - maxTokens: 128000, - } satisfies Model<"openai-codex-responses">, "gpt-5.5": { id: "gpt-5.5", name: "GPT-5.5", @@ -7299,24 +7263,6 @@ export const MODELS = { contextWindow: 272000, maxTokens: 128000, } satisfies Model<"openai-codex-responses">, - "gpt-5.5-fast": { - id: "gpt-5.5-fast", - name: "GPT-5.5 Fast", - api: "openai-codex-responses", - provider: "openai-codex", - baseUrl: "https://chatgpt.com/backend-api", - reasoning: true, - thinkingLevelMap: {"xhigh":"xhigh","minimal":"low"}, - input: ["text", "image"], - cost: { - input: 12.5, - output: 75, - cacheRead: 1.25, - cacheWrite: 0, - }, - contextWindow: 272000, - maxTokens: 128000, - } satisfies Model<"openai-codex-responses">, }, "opencode": { "big-pickle": { From ed3904ddd3c4a8d4afa27ff364df314392137998 Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Mon, 18 May 2026 00:14:31 +0200 Subject: [PATCH 081/166] fix(ai): switch xiaomi models to openai completions closes #4505 --- packages/ai/CHANGELOG.md | 4 + packages/ai/scripts/generate-models.ts | 10 +-- packages/ai/src/models.generated.ts | 120 ++++++++++++------------- 3 files changed, 69 insertions(+), 65 deletions(-) diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index 19ae4c71..23ff630a 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Fixed + +- Fixed Xiaomi MiMo model metadata to use the OpenAI-compatible endpoints and `openai-completions` API, restoring multi-turn thinking/tool-call sessions ([#4505](https://github.com/earendil-works/pi/issues/4505)). + ## [0.75.0] - 2026-05-17 ### Breaking Changes diff --git a/packages/ai/scripts/generate-models.ts b/packages/ai/scripts/generate-models.ts index 5a694a20..a836e873 100644 --- a/packages/ai/scripts/generate-models.ts +++ b/packages/ai/scripts/generate-models.ts @@ -1074,10 +1074,10 @@ async function loadModelsDevData(): Promise[]> { // keys from platform.xiaomimimo.com). The three `xiaomi-token-plan-*` // providers cover prepaid Token Plan endpoints in cn / ams / sgp. const xiaomiVariants = [ - { provider: "xiaomi", baseUrl: "https://api.xiaomimimo.com/anthropic" }, - { provider: "xiaomi-token-plan-cn", baseUrl: "https://token-plan-cn.xiaomimimo.com/anthropic" }, - { provider: "xiaomi-token-plan-ams", baseUrl: "https://token-plan-ams.xiaomimimo.com/anthropic" }, - { provider: "xiaomi-token-plan-sgp", baseUrl: "https://token-plan-sgp.xiaomimimo.com/anthropic" }, + { provider: "xiaomi", baseUrl: "https://api.xiaomimimo.com/v1" }, + { provider: "xiaomi-token-plan-cn", baseUrl: "https://token-plan-cn.xiaomimimo.com/v1" }, + { provider: "xiaomi-token-plan-ams", baseUrl: "https://token-plan-ams.xiaomimimo.com/v1" }, + { provider: "xiaomi-token-plan-sgp", baseUrl: "https://token-plan-sgp.xiaomimimo.com/v1" }, ] as const; if (data.xiaomi?.models) { @@ -1089,7 +1089,7 @@ async function loadModelsDevData(): Promise[]> { models.push({ id: modelId, name: m.name || modelId, - api: "anthropic-messages", + api: "openai-completions", provider, baseUrl, reasoning: m.reasoning === true, diff --git a/packages/ai/src/models.generated.ts b/packages/ai/src/models.generated.ts index 791857bf..5b980251 100644 --- a/packages/ai/src/models.generated.ts +++ b/packages/ai/src/models.generated.ts @@ -15876,9 +15876,9 @@ export const MODELS = { "mimo-v2-flash": { id: "mimo-v2-flash", name: "MiMo-V2-Flash", - api: "anthropic-messages", + api: "openai-completions", provider: "xiaomi", - baseUrl: "https://api.xiaomimimo.com/anthropic", + baseUrl: "https://api.xiaomimimo.com/v1", reasoning: true, input: ["text"], cost: { @@ -15889,13 +15889,13 @@ export const MODELS = { }, contextWindow: 262144, maxTokens: 65536, - } satisfies Model<"anthropic-messages">, + } satisfies Model<"openai-completions">, "mimo-v2-omni": { id: "mimo-v2-omni", name: "MiMo-V2-Omni", - api: "anthropic-messages", + api: "openai-completions", provider: "xiaomi", - baseUrl: "https://api.xiaomimimo.com/anthropic", + baseUrl: "https://api.xiaomimimo.com/v1", reasoning: true, input: ["text", "image"], cost: { @@ -15906,13 +15906,13 @@ export const MODELS = { }, contextWindow: 262144, maxTokens: 131072, - } satisfies Model<"anthropic-messages">, + } satisfies Model<"openai-completions">, "mimo-v2-pro": { id: "mimo-v2-pro", name: "MiMo-V2-Pro", - api: "anthropic-messages", + api: "openai-completions", provider: "xiaomi", - baseUrl: "https://api.xiaomimimo.com/anthropic", + baseUrl: "https://api.xiaomimimo.com/v1", reasoning: true, input: ["text"], cost: { @@ -15923,13 +15923,13 @@ export const MODELS = { }, contextWindow: 1048576, maxTokens: 131072, - } satisfies Model<"anthropic-messages">, + } satisfies Model<"openai-completions">, "mimo-v2.5": { id: "mimo-v2.5", name: "MiMo-V2.5", - api: "anthropic-messages", + api: "openai-completions", provider: "xiaomi", - baseUrl: "https://api.xiaomimimo.com/anthropic", + baseUrl: "https://api.xiaomimimo.com/v1", reasoning: true, input: ["text", "image"], cost: { @@ -15940,13 +15940,13 @@ export const MODELS = { }, contextWindow: 1048576, maxTokens: 131072, - } satisfies Model<"anthropic-messages">, + } satisfies Model<"openai-completions">, "mimo-v2.5-pro": { id: "mimo-v2.5-pro", name: "MiMo-V2.5-Pro", - api: "anthropic-messages", + api: "openai-completions", provider: "xiaomi", - baseUrl: "https://api.xiaomimimo.com/anthropic", + baseUrl: "https://api.xiaomimimo.com/v1", reasoning: true, input: ["text"], cost: { @@ -15957,15 +15957,15 @@ export const MODELS = { }, contextWindow: 1048576, maxTokens: 131072, - } satisfies Model<"anthropic-messages">, + } satisfies Model<"openai-completions">, }, "xiaomi-token-plan-ams": { "mimo-v2-flash": { id: "mimo-v2-flash", name: "MiMo-V2-Flash", - api: "anthropic-messages", + api: "openai-completions", provider: "xiaomi-token-plan-ams", - baseUrl: "https://token-plan-ams.xiaomimimo.com/anthropic", + baseUrl: "https://token-plan-ams.xiaomimimo.com/v1", reasoning: true, input: ["text"], cost: { @@ -15976,13 +15976,13 @@ export const MODELS = { }, contextWindow: 262144, maxTokens: 65536, - } satisfies Model<"anthropic-messages">, + } satisfies Model<"openai-completions">, "mimo-v2-omni": { id: "mimo-v2-omni", name: "MiMo-V2-Omni", - api: "anthropic-messages", + api: "openai-completions", provider: "xiaomi-token-plan-ams", - baseUrl: "https://token-plan-ams.xiaomimimo.com/anthropic", + baseUrl: "https://token-plan-ams.xiaomimimo.com/v1", reasoning: true, input: ["text", "image"], cost: { @@ -15993,13 +15993,13 @@ export const MODELS = { }, contextWindow: 262144, maxTokens: 131072, - } satisfies Model<"anthropic-messages">, + } satisfies Model<"openai-completions">, "mimo-v2-pro": { id: "mimo-v2-pro", name: "MiMo-V2-Pro", - api: "anthropic-messages", + api: "openai-completions", provider: "xiaomi-token-plan-ams", - baseUrl: "https://token-plan-ams.xiaomimimo.com/anthropic", + baseUrl: "https://token-plan-ams.xiaomimimo.com/v1", reasoning: true, input: ["text"], cost: { @@ -16010,13 +16010,13 @@ export const MODELS = { }, contextWindow: 1048576, maxTokens: 131072, - } satisfies Model<"anthropic-messages">, + } satisfies Model<"openai-completions">, "mimo-v2.5": { id: "mimo-v2.5", name: "MiMo-V2.5", - api: "anthropic-messages", + api: "openai-completions", provider: "xiaomi-token-plan-ams", - baseUrl: "https://token-plan-ams.xiaomimimo.com/anthropic", + baseUrl: "https://token-plan-ams.xiaomimimo.com/v1", reasoning: true, input: ["text", "image"], cost: { @@ -16027,13 +16027,13 @@ export const MODELS = { }, contextWindow: 1048576, maxTokens: 131072, - } satisfies Model<"anthropic-messages">, + } satisfies Model<"openai-completions">, "mimo-v2.5-pro": { id: "mimo-v2.5-pro", name: "MiMo-V2.5-Pro", - api: "anthropic-messages", + api: "openai-completions", provider: "xiaomi-token-plan-ams", - baseUrl: "https://token-plan-ams.xiaomimimo.com/anthropic", + baseUrl: "https://token-plan-ams.xiaomimimo.com/v1", reasoning: true, input: ["text"], cost: { @@ -16044,15 +16044,15 @@ export const MODELS = { }, contextWindow: 1048576, maxTokens: 131072, - } satisfies Model<"anthropic-messages">, + } satisfies Model<"openai-completions">, }, "xiaomi-token-plan-cn": { "mimo-v2-flash": { id: "mimo-v2-flash", name: "MiMo-V2-Flash", - api: "anthropic-messages", + api: "openai-completions", provider: "xiaomi-token-plan-cn", - baseUrl: "https://token-plan-cn.xiaomimimo.com/anthropic", + baseUrl: "https://token-plan-cn.xiaomimimo.com/v1", reasoning: true, input: ["text"], cost: { @@ -16063,13 +16063,13 @@ export const MODELS = { }, contextWindow: 262144, maxTokens: 65536, - } satisfies Model<"anthropic-messages">, + } satisfies Model<"openai-completions">, "mimo-v2-omni": { id: "mimo-v2-omni", name: "MiMo-V2-Omni", - api: "anthropic-messages", + api: "openai-completions", provider: "xiaomi-token-plan-cn", - baseUrl: "https://token-plan-cn.xiaomimimo.com/anthropic", + baseUrl: "https://token-plan-cn.xiaomimimo.com/v1", reasoning: true, input: ["text", "image"], cost: { @@ -16080,13 +16080,13 @@ export const MODELS = { }, contextWindow: 262144, maxTokens: 131072, - } satisfies Model<"anthropic-messages">, + } satisfies Model<"openai-completions">, "mimo-v2-pro": { id: "mimo-v2-pro", name: "MiMo-V2-Pro", - api: "anthropic-messages", + api: "openai-completions", provider: "xiaomi-token-plan-cn", - baseUrl: "https://token-plan-cn.xiaomimimo.com/anthropic", + baseUrl: "https://token-plan-cn.xiaomimimo.com/v1", reasoning: true, input: ["text"], cost: { @@ -16097,13 +16097,13 @@ export const MODELS = { }, contextWindow: 1048576, maxTokens: 131072, - } satisfies Model<"anthropic-messages">, + } satisfies Model<"openai-completions">, "mimo-v2.5": { id: "mimo-v2.5", name: "MiMo-V2.5", - api: "anthropic-messages", + api: "openai-completions", provider: "xiaomi-token-plan-cn", - baseUrl: "https://token-plan-cn.xiaomimimo.com/anthropic", + baseUrl: "https://token-plan-cn.xiaomimimo.com/v1", reasoning: true, input: ["text", "image"], cost: { @@ -16114,13 +16114,13 @@ export const MODELS = { }, contextWindow: 1048576, maxTokens: 131072, - } satisfies Model<"anthropic-messages">, + } satisfies Model<"openai-completions">, "mimo-v2.5-pro": { id: "mimo-v2.5-pro", name: "MiMo-V2.5-Pro", - api: "anthropic-messages", + api: "openai-completions", provider: "xiaomi-token-plan-cn", - baseUrl: "https://token-plan-cn.xiaomimimo.com/anthropic", + baseUrl: "https://token-plan-cn.xiaomimimo.com/v1", reasoning: true, input: ["text"], cost: { @@ -16131,15 +16131,15 @@ export const MODELS = { }, contextWindow: 1048576, maxTokens: 131072, - } satisfies Model<"anthropic-messages">, + } satisfies Model<"openai-completions">, }, "xiaomi-token-plan-sgp": { "mimo-v2-flash": { id: "mimo-v2-flash", name: "MiMo-V2-Flash", - api: "anthropic-messages", + api: "openai-completions", provider: "xiaomi-token-plan-sgp", - baseUrl: "https://token-plan-sgp.xiaomimimo.com/anthropic", + baseUrl: "https://token-plan-sgp.xiaomimimo.com/v1", reasoning: true, input: ["text"], cost: { @@ -16150,13 +16150,13 @@ export const MODELS = { }, contextWindow: 262144, maxTokens: 65536, - } satisfies Model<"anthropic-messages">, + } satisfies Model<"openai-completions">, "mimo-v2-omni": { id: "mimo-v2-omni", name: "MiMo-V2-Omni", - api: "anthropic-messages", + api: "openai-completions", provider: "xiaomi-token-plan-sgp", - baseUrl: "https://token-plan-sgp.xiaomimimo.com/anthropic", + baseUrl: "https://token-plan-sgp.xiaomimimo.com/v1", reasoning: true, input: ["text", "image"], cost: { @@ -16167,13 +16167,13 @@ export const MODELS = { }, contextWindow: 262144, maxTokens: 131072, - } satisfies Model<"anthropic-messages">, + } satisfies Model<"openai-completions">, "mimo-v2-pro": { id: "mimo-v2-pro", name: "MiMo-V2-Pro", - api: "anthropic-messages", + api: "openai-completions", provider: "xiaomi-token-plan-sgp", - baseUrl: "https://token-plan-sgp.xiaomimimo.com/anthropic", + baseUrl: "https://token-plan-sgp.xiaomimimo.com/v1", reasoning: true, input: ["text"], cost: { @@ -16184,13 +16184,13 @@ export const MODELS = { }, contextWindow: 1048576, maxTokens: 131072, - } satisfies Model<"anthropic-messages">, + } satisfies Model<"openai-completions">, "mimo-v2.5": { id: "mimo-v2.5", name: "MiMo-V2.5", - api: "anthropic-messages", + api: "openai-completions", provider: "xiaomi-token-plan-sgp", - baseUrl: "https://token-plan-sgp.xiaomimimo.com/anthropic", + baseUrl: "https://token-plan-sgp.xiaomimimo.com/v1", reasoning: true, input: ["text", "image"], cost: { @@ -16201,13 +16201,13 @@ export const MODELS = { }, contextWindow: 1048576, maxTokens: 131072, - } satisfies Model<"anthropic-messages">, + } satisfies Model<"openai-completions">, "mimo-v2.5-pro": { id: "mimo-v2.5-pro", name: "MiMo-V2.5-Pro", - api: "anthropic-messages", + api: "openai-completions", provider: "xiaomi-token-plan-sgp", - baseUrl: "https://token-plan-sgp.xiaomimimo.com/anthropic", + baseUrl: "https://token-plan-sgp.xiaomimimo.com/v1", reasoning: true, input: ["text"], cost: { @@ -16218,7 +16218,7 @@ export const MODELS = { }, contextWindow: 1048576, maxTokens: 131072, - } satisfies Model<"anthropic-messages">, + } satisfies Model<"openai-completions">, }, "zai": { "glm-4.5-air": { From 6b872be2b9294dbde57634477bdda96582a5eed3 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 18 May 2026 00:21:09 +0200 Subject: [PATCH 082/166] fix(coding-agent): spawn Windows npm shims directly closes #4623 --- packages/coding-agent/CHANGELOG.md | 4 + packages/coding-agent/src/config.ts | 14 +++- .../coding-agent/src/core/package-manager.ts | 23 +++--- .../coding-agent/src/package-manager-cli.ts | 7 +- .../coding-agent/src/utils/child-process.ts | 80 +++++++++++++++++-- .../coding-agent/test/package-manager.test.ts | 57 +++++++++++-- 6 files changed, 156 insertions(+), 29 deletions(-) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index f004f107..95ead56e 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Fixed + +- Fixed npm-family package commands on Windows to avoid shell argument splitting when install prefixes contain spaces ([#4623](https://github.com/earendil-works/pi/issues/4623)). + ## [0.75.0] - 2026-05-17 ### Breaking Changes diff --git a/packages/coding-agent/src/config.ts b/packages/coding-agent/src/config.ts index bf268a77..901bd296 100644 --- a/packages/coding-agent/src/config.ts +++ b/packages/coding-agent/src/config.ts @@ -3,7 +3,7 @@ import { accessSync, constants, existsSync, readFileSync, realpathSync } from "f import { homedir } from "os"; import { basename, dirname, join, resolve, sep, win32 } from "path"; import { fileURLToPath } from "url"; -import { shouldUseWindowsShell } from "./utils/child-process.js"; +import { resolveSpawnCommand } from "./utils/child-process.js"; // ============================================================================= // Package Detection @@ -151,10 +151,18 @@ function readCommandOutput( args: string[], options: { requireSuccess?: boolean } = {}, ): string | undefined { - const result = spawnSync(command, args, { + let resolved: ReturnType; + try { + resolved = resolveSpawnCommand(command, args); + } catch (error) { + if (options.requireSuccess) { + throw error; + } + return undefined; + } + const result = spawnSync(resolved.command, resolved.args, { encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"], - shell: shouldUseWindowsShell(command), }); if (result.status === 0) return result.stdout.trim() || undefined; if (options.requireSuccess) { diff --git a/packages/coding-agent/src/core/package-manager.ts b/packages/coding-agent/src/core/package-manager.ts index 92f9bede..53ff6fea 100644 --- a/packages/coding-agent/src/core/package-manager.ts +++ b/packages/coding-agent/src/core/package-manager.ts @@ -28,7 +28,7 @@ import { globSync } from "glob"; import ignore from "ignore"; import { minimatch } from "minimatch"; import { CONFIG_DIR_NAME } from "../config.js"; -import { shouldUseWindowsShell } from "../utils/child-process.js"; +import { resolveSpawnCommand } from "../utils/child-process.js"; import { type GitSource, parseGitUrl } from "../utils/git.js"; import { canonicalizePath, isLocalPath } from "../utils/paths.js"; import { isStdoutTakenOver } from "./output-guard.js"; @@ -2408,11 +2408,12 @@ export class DefaultPackageManager implements PackageManager { } private spawnCommand(command: string, args: string[], options?: { cwd?: string }): ChildProcess { - return spawn(command, args, { + const env = getEnv(); + const resolved = resolveSpawnCommand(command, args, { env }); + return spawn(resolved.command, resolved.args, { cwd: options?.cwd, stdio: isStdoutTakenOver() ? ["ignore", 2, 2] : "inherit", - shell: shouldUseWindowsShell(command), - env: getEnv(), + env, }); } @@ -2422,11 +2423,12 @@ export class DefaultPackageManager implements PackageManager { options?: { cwd?: string; env?: Record }, ): ChildProcessByStdio { const baseEnv = getEnv(); - return spawn(command, args, { + const env = options?.env ? { ...baseEnv, ...options.env } : baseEnv; + const resolved = resolveSpawnCommand(command, args, { env }); + return spawn(resolved.command, resolved.args, { cwd: options?.cwd, stdio: ["ignore", "pipe", "pipe"], - shell: shouldUseWindowsShell(command), - env: options?.env ? { ...baseEnv, ...options.env } : baseEnv, + env, }); } @@ -2489,11 +2491,12 @@ export class DefaultPackageManager implements PackageManager { } private runCommandSync(command: string, args: string[]): string { - const result = spawnSync(command, args, { + const env = getEnv(); + const resolved = resolveSpawnCommand(command, args, { env }); + const result = spawnSync(resolved.command, resolved.args, { stdio: ["ignore", "pipe", "pipe"], encoding: "utf-8", - shell: shouldUseWindowsShell(command), - env: getEnv(), + env, }); if (result.error || result.status !== 0) { throw new Error( diff --git a/packages/coding-agent/src/package-manager-cli.ts b/packages/coding-agent/src/package-manager-cli.ts index a41fb7a6..f421af32 100644 --- a/packages/coding-agent/src/package-manager-cli.ts +++ b/packages/coding-agent/src/package-manager-cli.ts @@ -12,7 +12,7 @@ import { } from "./config.js"; import { DefaultPackageManager } from "./core/package-manager.js"; import { SettingsManager } from "./core/settings-manager.js"; -import { shouldUseWindowsShell } from "./utils/child-process.js"; +import { resolveSpawnCommand } from "./utils/child-process.js"; import { getLatestPiRelease, isNewerPackageVersion } from "./utils/version-check.js"; export type PackageCommand = "install" | "remove" | "update" | "list"; @@ -316,10 +316,9 @@ async function runSelfUpdate(command: SelfUpdateCommand): Promise { console.log(chalk.dim(`Updating ${APP_NAME} with ${command.display}...`)); for (const step of command.steps ?? [command]) { await new Promise((resolve, reject) => { - // Windows package managers are commonly .cmd shims. Use the shell so Node can execute them. - const child = spawn(step.command, step.args, { + const resolved = resolveSpawnCommand(step.command, step.args); + const child = spawn(resolved.command, resolved.args, { stdio: "inherit", - shell: shouldUseWindowsShell(step.command), }); child.on("error", (error) => { reject(error); diff --git a/packages/coding-agent/src/utils/child-process.ts b/packages/coding-agent/src/utils/child-process.ts index 9f75a285..73fdf5af 100644 --- a/packages/coding-agent/src/utils/child-process.ts +++ b/packages/coding-agent/src/utils/child-process.ts @@ -1,14 +1,82 @@ import type { ChildProcess } from "node:child_process"; -import { basename } from "node:path"; +import { existsSync, readFileSync } from "node:fs"; +import { dirname, extname, join, resolve, sep } from "node:path"; const EXIT_STDIO_GRACE_MS = 100; +const WINDOWS_COMMAND_EXTENSIONS = ["", ".exe", ".cmd", ".bat"]; +const WINDOWS_COMMAND_SHIM_RE = /\.(?:cmd|bat)$/i; +const NODE_SHIM_SCRIPT_RE = /(?:%~dp0|%dp0%|%basedir%)[^"'\r\n<>|&]*?\.(?:cjs|mjs|js)/i; -const WINDOWS_SHELL_COMMANDS = new Set(["npm", "npx", "pnpm", "yarn", "yarnpkg", "corepack"]); +export interface ResolvedSpawnCommand { + command: string; + args: string[]; +} -export function shouldUseWindowsShell(command: string): boolean { - if (process.platform !== "win32") return false; - const commandName = basename(command).toLowerCase(); - return commandName.endsWith(".cmd") || commandName.endsWith(".bat") || WINDOWS_SHELL_COMMANDS.has(commandName); +function findWindowsCommand(command: string, env: NodeJS.ProcessEnv): string | undefined { + const candidates = extname(command) + ? [command] + : WINDOWS_COMMAND_EXTENSIONS.map((extension) => `${command}${extension}`); + const hasPath = command.includes("/") || command.includes("\\") || /^[a-zA-Z]:/.test(command); + if (hasPath) { + const match = candidates.find((candidate) => existsSync(candidate)); + return match ? resolve(match) : undefined; + } + + const pathValue = env.PATH ?? env.Path ?? env.path; + if (!pathValue) return undefined; + for (const dir of pathValue.split(";")) { + for (const candidate of candidates) { + const path = join(dir, candidate); + if (existsSync(path)) return resolve(path); + } + } + return undefined; +} + +function expandShimPath(path: string, shimPath: string): string { + const shimDir = dirname(shimPath); + return resolve( + path + .replace(/%~dp0[\\/]?/gi, `${shimDir}${sep}`) + .replace(/%dp0%[\\/]?/gi, `${shimDir}${sep}`) + .replace(/%basedir%[\\/]?/gi, `${shimDir}${sep}`) + .replace(/\\/g, sep), + ); +} + +function findNodeShimScript(shimPath: string): string | undefined { + const match = readFileSync(shimPath, "utf-8").match(NODE_SHIM_SCRIPT_RE); + if (!match) return undefined; + const scriptPath = expandShimPath(match[0], shimPath); + return existsSync(scriptPath) ? scriptPath : undefined; +} + +export function resolveSpawnCommand( + command: string, + args: string[], + options: { env?: NodeJS.ProcessEnv } = {}, +): ResolvedSpawnCommand { + if (process.platform !== "win32") { + return { command, args }; + } + + const env = options.env ?? process.env; + const resolvedCommand = findWindowsCommand(command, env); + if (!resolvedCommand) { + return { command, args }; + } + + if (!WINDOWS_COMMAND_SHIM_RE.test(resolvedCommand)) { + return { command: resolvedCommand, args }; + } + + const script = findNodeShimScript(resolvedCommand); + if (!script) { + throw new Error(`Refusing to run Windows command shim without a shell: ${resolvedCommand}`); + } + const localNode = join(dirname(resolvedCommand), "node.exe"); + const nodeCommand = existsSync(localNode) ? localNode : (findWindowsCommand("node.exe", env) ?? "node"); + return { command: nodeCommand, args: [script, ...args] }; } /** diff --git a/packages/coding-agent/test/package-manager.test.ts b/packages/coding-agent/test/package-manager.test.ts index edd2ce95..3cdf8cb0 100644 --- a/packages/coding-agent/test/package-manager.test.ts +++ b/packages/coding-agent/test/package-manager.test.ts @@ -6,7 +6,7 @@ import { PassThrough } from "node:stream"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { DefaultPackageManager, type ProgressEvent, type ResolvedResource } from "../src/core/package-manager.js"; import { SettingsManager } from "../src/core/settings-manager.js"; -import { shouldUseWindowsShell } from "../src/utils/child-process.js"; +import { resolveSpawnCommand } from "../src/utils/child-process.js"; function normalizeForMatch(value: string): string { return value.replace(/\\/g, "/"); @@ -618,13 +618,58 @@ Content`, }); describe("windows command spawning", () => { - it("should avoid the shell for git so Windows paths with spaces stay single arguments", () => { + it("should keep unresolved executables as argv commands", () => { vi.spyOn(process, "platform", "get").mockReturnValue("win32"); - expect(shouldUseWindowsShell("git")).toBe(false); - expect(shouldUseWindowsShell("npm")).toBe(true); - expect(shouldUseWindowsShell("pnpm")).toBe(true); - expect(shouldUseWindowsShell("C:/Program Files/nodejs/npm.cmd")).toBe(true); + const resolved = resolveSpawnCommand("git", ["clone", "repo", "C:\\Users\\A B\\repo"], { + env: { PATH: tempDir }, + }); + + expect(resolved).toEqual({ command: "git", args: ["clone", "repo", "C:\\Users\\A B\\repo"] }); + }); + + it("should prefer Windows executables over command scripts", () => { + vi.spyOn(process, "platform", "get").mockReturnValue("win32"); + const binDir = join(tempDir, "bin"); + mkdirSync(binDir, { recursive: true }); + writeFileSync(join(binDir, "npm.exe"), ""); + writeFileSync(join(binDir, "npm.cmd"), "@echo off\r\n"); + + const args = ["install", "pkg", "--prefix", "C:\\Users\\A B\\.pi\\npm"]; + const resolved = resolveSpawnCommand("npm", args, { env: { PATH: binDir } }); + + expect(resolved).toEqual({ command: join(binDir, "npm.exe"), args }); + }); + + it("should resolve npm command shims to their Node entrypoint without a shell", () => { + vi.spyOn(process, "platform", "get").mockReturnValue("win32"); + const binDir = join(tempDir, "node-bin"); + const npmCli = join(binDir, "node_modules", "npm", "bin", "npm-cli.js"); + mkdirSync(join(binDir, "node_modules", "npm", "bin"), { recursive: true }); + writeFileSync(join(binDir, "node.exe"), ""); + writeFileSync(npmCli, ""); + writeFileSync( + join(binDir, "npm.cmd"), + '@ECHO off\r\nSET "dp0=%~dp0"\r\nSET "_prog=%dp0%\\node.exe"\r\nendLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\\node_modules\\npm\\bin\\npm-cli.js" %*\r\n', + ); + + const args = ["install", "pkg", "--prefix", "C:\\Users\\A B\\.pi\\npm"]; + const resolved = resolveSpawnCommand("npm", args, { env: { PATH: binDir } }); + + expect(resolved).toEqual({ command: join(binDir, "node.exe"), args: [npmCli, ...args] }); + }); + + it("should reject unrecognized Windows command scripts instead of using cmd.exe", () => { + vi.spyOn(process, "platform", "get").mockReturnValue("win32"); + const binDir = join(tempDir, "bin"); + mkdirSync(binDir, { recursive: true }); + writeFileSync(join(binDir, "npm.cmd"), "@echo off\r\necho %*\r\n"); + + expect(() => + resolveSpawnCommand("npm", ["install", "pkg"], { + env: { PATH: binDir }, + }), + ).toThrow("Refusing to run Windows command shim without a shell"); }); }); From ceacacaa45f1b9c0fe54faa330ab1160939466d5 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 18 May 2026 00:55:01 +0200 Subject: [PATCH 083/166] chore: add pi-test batch wrapper --- pi-test.bat | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 pi-test.bat diff --git a/pi-test.bat b/pi-test.bat new file mode 100644 index 00000000..048a99db --- /dev/null +++ b/pi-test.bat @@ -0,0 +1,14 @@ +@echo off +setlocal + +set "SCRIPT_DIR=%~dp0" +set "POWERSHELL_EXE=powershell.exe" + +where %POWERSHELL_EXE% >nul 2>nul +if errorlevel 1 ( + >&2 echo powershell.exe not found. Install PowerShell or run pi-test.ps1 directly. + exit /b 1 +) + +%POWERSHELL_EXE% -NoProfile -ExecutionPolicy Bypass -File "%SCRIPT_DIR%pi-test.ps1" %* +exit /b %ERRORLEVEL% From 21d80deda2b0e97c63adacad4dce683514292229 Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Mon, 18 May 2026 01:08:08 +0200 Subject: [PATCH 084/166] fix(ai): normalize opencode go reasoning replay closes #4251 --- packages/ai/CHANGELOG.md | 1 + .../ai/src/providers/openai-completions.ts | 11 +- .../openai-completions-tool-choice.test.ts | 111 +++++++++++++++++- 3 files changed, 120 insertions(+), 3 deletions(-) diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index 23ff630a..787a2898 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -5,6 +5,7 @@ ### Fixed - Fixed Xiaomi MiMo model metadata to use the OpenAI-compatible endpoints and `openai-completions` API, restoring multi-turn thinking/tool-call sessions ([#4505](https://github.com/earendil-works/pi/issues/4505)). +- Fixed OpenCode Go Kimi reasoning replay by normalizing streamed `reasoning` fields back to `reasoning_content` for OpenCode Go only ([#4251](https://github.com/earendil-works/pi/issues/4251)). ## [0.75.0] - 2026-05-17 diff --git a/packages/ai/src/providers/openai-completions.ts b/packages/ai/src/providers/openai-completions.ts index e60b43e8..a89b6dd9 100644 --- a/packages/ai/src/providers/openai-completions.ts +++ b/packages/ai/src/providers/openai-completions.ts @@ -326,7 +326,11 @@ export const streamOpenAICompletions: StreamFunction<"openai-completions", OpenA if (foundReasoningField) { const delta = deltaFields[foundReasoningField]; if (typeof delta === "string" && delta.length > 0) { - const block = ensureThinkingBlock(foundReasoningField); + const thinkingSignature = + model.provider === "opencode-go" && foundReasoningField === "reasoning" + ? "reasoning_content" + : foundReasoningField; + const block = ensureThinkingBlock(thinkingSignature); block.thinking += delta; stream.push({ type: "thinking_delta", @@ -844,7 +848,10 @@ export function convertMessages( } // Use the signature from the first thinking block if available (for llama.cpp server + gpt-oss) - const signature = nonEmptyThinkingBlocks[0].thinkingSignature; + let signature = nonEmptyThinkingBlocks[0].thinkingSignature; + if (model.provider === "opencode-go" && signature === "reasoning") { + signature = "reasoning_content"; + } if (signature && signature.length > 0) { (assistantMsg as any)[signature] = nonEmptyThinkingBlocks.map((block) => block.thinking).join("\n"); } diff --git a/packages/ai/test/openai-completions-tool-choice.test.ts b/packages/ai/test/openai-completions-tool-choice.test.ts index 22b7242a..21b5c0d5 100644 --- a/packages/ai/test/openai-completions-tool-choice.test.ts +++ b/packages/ai/test/openai-completions-tool-choice.test.ts @@ -1,8 +1,9 @@ import { Type } from "typebox"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { getModel } from "../src/models.js"; +import { convertMessages } from "../src/providers/openai-completions.js"; import { streamSimple } from "../src/stream.js"; -import type { Tool } from "../src/types.js"; +import type { Model, Tool } from "../src/types.js"; const mockState = vi.hoisted(() => ({ lastParams: undefined as unknown, @@ -816,6 +817,114 @@ describe("openai-completions tool_choice", () => { expect(writeCall).not.toHaveProperty("partialArgs"); }); + it("normalizes OpenCode Go reasoning deltas to reasoning_content for replay", async () => { + mockState.chunks = [ + { + id: "chatcmpl-opencode-go-reasoning", + choices: [{ delta: { reasoning: "think" }, finish_reason: "stop" }], + }, + ]; + + const { compat: _compat, ...baseModel } = getModel("opencode-go", "kimi-k2.6")!; + const model = { ...baseModel, api: "openai-completions" } as const; + const response = await streamSimple( + model, + { + messages: [{ role: "user", content: "Use reasoning.", timestamp: Date.now() }], + }, + { apiKey: "test" }, + ).result(); + + expect(response.content).toEqual([ + { + type: "thinking", + thinking: "think", + thinkingSignature: "reasoning_content", + }, + ]); + }); + + it("keeps non-OpenCode Go reasoning deltas on the original reasoning field", async () => { + mockState.chunks = [ + { + id: "chatcmpl-reasoning", + choices: [{ delta: { reasoning: "think" }, finish_reason: "stop" }], + }, + ]; + + const { compat: _compat, ...baseModel } = getModel("openai", "gpt-4o-mini")!; + const model = { ...baseModel, api: "openai-completions" } as const; + const response = await streamSimple( + model, + { + messages: [{ role: "user", content: "Use reasoning.", timestamp: Date.now() }], + }, + { apiKey: "test" }, + ).result(); + + expect(response.content).toEqual([ + { + type: "thinking", + thinking: "think", + thinkingSignature: "reasoning", + }, + ]); + }); + + it("replays OpenCode Go reasoning thinking blocks as reasoning_content", () => { + const { compat: _compat, ...baseModel } = getModel("opencode-go", "kimi-k2.6")!; + const model = { ...baseModel, api: "openai-completions" } as Model<"openai-completions">; + const messages = convertMessages( + model, + { + messages: [ + { + role: "assistant", + api: "openai-completions", + provider: "opencode-go", + model: "kimi-k2.6", + content: [ + { type: "thinking", thinking: "think", thinkingSignature: "reasoning" }, + { type: "toolCall", id: "call_1", name: "read", arguments: { path: "README.md" } }, + ], + usage: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + totalTokens: 0, + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 }, + }, + stopReason: "stop", + timestamp: Date.now(), + }, + ], + }, + { + ...model.compat, + supportsStore: false, + supportsDeveloperRole: false, + supportsReasoningEffort: true, + supportsUsageInStreaming: true, + maxTokensField: "max_completion_tokens", + requiresToolResultName: false, + requiresAssistantAfterToolResult: false, + requiresThinkingAsText: false, + requiresReasoningContentOnAssistantMessages: false, + thinkingFormat: "openai", + openRouterRouting: {}, + vercelGatewayRouting: {}, + zaiToolStream: false, + supportsStrictMode: true, + sendSessionAffinityHeaders: false, + supportsLongCacheRetention: true, + }, + ); + + expect(messages[0]).toMatchObject({ role: "assistant", reasoning_content: "think" }); + expect(messages[0]).not.toHaveProperty("reasoning"); + }); + it("does not double-count reasoning tokens in completion usage", async () => { mockState.chunks = [ { From 52e13870a1a5c585603735a590124146462b249f Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Mon, 18 May 2026 01:10:54 +0200 Subject: [PATCH 085/166] fix(ai): prefix HTTP status codes onto Azure OpenAI and OpenAI Responses error messages so auto-retry fires on 5xx/429 closes #4232 --- packages/ai/CHANGELOG.md | 1 + .../ai/src/providers/azure-openai-responses.ts | 18 +++++++++++++++++- packages/ai/src/providers/openai-responses.ts | 18 +++++++++++++++++- 3 files changed, 35 insertions(+), 2 deletions(-) diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index 787a2898..cf957ad0 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -4,6 +4,7 @@ ### Fixed +- Fixed Azure OpenAI Responses and OpenAI Responses error formatting to prefix HTTP status codes onto `errorMessage`, so transient 5xx and 429 errors are correctly matched by the agent-level auto-retry classifier ([#4232](https://github.com/earendil-works/pi/issues/4232)). - Fixed Xiaomi MiMo model metadata to use the OpenAI-compatible endpoints and `openai-completions` API, restoring multi-turn thinking/tool-call sessions ([#4505](https://github.com/earendil-works/pi/issues/4505)). - Fixed OpenCode Go Kimi reasoning replay by normalizing streamed `reasoning` fields back to `reasoning_content` for OpenCode Go only ([#4251](https://github.com/earendil-works/pi/issues/4251)). diff --git a/packages/ai/src/providers/azure-openai-responses.ts b/packages/ai/src/providers/azure-openai-responses.ts index 5d4bc9c6..918ff9f5 100644 --- a/packages/ai/src/providers/azure-openai-responses.ts +++ b/packages/ai/src/providers/azure-openai-responses.ts @@ -40,6 +40,22 @@ function resolveDeploymentName(model: Model<"azure-openai-responses">, options?: return mappedDeployment || model.id; } +function formatAzureOpenAIError(error: unknown): string { + if (error instanceof Error) { + const status = (error as Error & { status?: unknown }).status; + const statusCode = typeof status === "number" ? status : undefined; + if (statusCode !== undefined) { + return `Azure OpenAI API error (${statusCode}): ${error.message}`; + } + return error.message; + } + try { + return JSON.stringify(error); + } catch { + return String(error); + } +} + // Azure OpenAI Responses-specific options export interface AzureOpenAIResponsesOptions extends StreamOptions { reasoningEffort?: "minimal" | "low" | "medium" | "high" | "xhigh"; @@ -119,7 +135,7 @@ export const streamAzureOpenAIResponses: StreamFunction<"azure-openai-responses" delete (block as { partialJson?: string }).partialJson; } output.stopReason = options?.signal?.aborted ? "aborted" : "error"; - output.errorMessage = error instanceof Error ? error.message : JSON.stringify(error); + output.errorMessage = formatAzureOpenAIError(error); stream.push({ type: "error", reason: output.stopReason, error: output }); stream.end(); } diff --git a/packages/ai/src/providers/openai-responses.ts b/packages/ai/src/providers/openai-responses.ts index 8cc446d3..c3881a0f 100644 --- a/packages/ai/src/providers/openai-responses.ts +++ b/packages/ai/src/providers/openai-responses.ts @@ -51,6 +51,22 @@ function getPromptCacheRetention( return cacheRetention === "long" && compat.supportsLongCacheRetention ? "24h" : undefined; } +function formatOpenAIResponsesError(error: unknown): string { + if (error instanceof Error) { + const status = (error as Error & { status?: unknown }).status; + const statusCode = typeof status === "number" ? status : undefined; + if (statusCode !== undefined) { + return `OpenAI API error (${statusCode}): ${error.message}`; + } + return error.message; + } + try { + return JSON.stringify(error); + } catch { + return String(error); + } +} + // OpenAI Responses-specific options export interface OpenAIResponsesOptions extends StreamOptions { reasoningEffort?: "minimal" | "low" | "medium" | "high" | "xhigh"; @@ -130,7 +146,7 @@ export const streamOpenAIResponses: StreamFunction<"openai-responses", OpenAIRes delete (block as { partialJson?: string }).partialJson; } output.stopReason = options?.signal?.aborted ? "aborted" : "error"; - output.errorMessage = error instanceof Error ? error.message : JSON.stringify(error); + output.errorMessage = formatOpenAIResponsesError(error); stream.push({ type: "error", reason: output.stopReason, error: output }); stream.end(); } From 9e2bfc7c40072544e3d364fd76ab2535641398b7 Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Mon, 18 May 2026 01:41:58 +0200 Subject: [PATCH 086/166] fix(ai): skip unknown bedrock content blocks closes #4223 --- packages/ai/CHANGELOG.md | 1 + packages/ai/src/providers/amazon-bedrock.ts | 39 +++-- .../ai/test/bedrock-convert-messages.test.ts | 159 ++++++++++++++++++ 3 files changed, 183 insertions(+), 16 deletions(-) create mode 100644 packages/ai/test/bedrock-convert-messages.test.ts diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index cf957ad0..803bca78 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -4,6 +4,7 @@ ### Fixed +- Fixed Amazon Bedrock message conversion to skip unknown content blocks instead of failing the stream ([#4223](https://github.com/earendil-works/pi/issues/4223)). - Fixed Azure OpenAI Responses and OpenAI Responses error formatting to prefix HTTP status codes onto `errorMessage`, so transient 5xx and 429 errors are correctly matched by the agent-level auto-retry classifier ([#4232](https://github.com/earendil-works/pi/issues/4232)). - Fixed Xiaomi MiMo model metadata to use the OpenAI-compatible endpoints and `openai-completions` API, restoring multi-turn thinking/tool-call sessions ([#4505](https://github.com/earendil-works/pi/issues/4505)). - Fixed OpenCode Go Kimi reasoning replay by normalizing streamed `reasoning` fields back to `reasoning_content` for OpenCode Go only ([#4251](https://github.com/earendil-works/pi/issues/4251)). diff --git a/packages/ai/src/providers/amazon-bedrock.ts b/packages/ai/src/providers/amazon-bedrock.ts index 7c5ac164..a48b5897 100644 --- a/packages/ai/src/providers/amazon-bedrock.ts +++ b/packages/ai/src/providers/amazon-bedrock.ts @@ -618,24 +618,31 @@ function convertMessages( const m = transformedMessages[i]; switch (m.role) { - case "user": + case "user": { + const content: ContentBlock[] = []; + if (typeof m.content === "string") { + content.push({ text: sanitizeSurrogates(m.content) }); + } else { + for (const c of m.content) { + switch (c.type) { + case "text": + content.push({ text: sanitizeSurrogates(c.text) }); + break; + case "image": + content.push({ image: createImageBlock(c.mimeType, c.data) }); + break; + default: + continue; + } + } + } + if (content.length === 0) continue; result.push({ role: ConversationRole.USER, - content: - typeof m.content === "string" - ? [{ text: sanitizeSurrogates(m.content) }] - : m.content.map((c) => { - switch (c.type) { - case "text": - return { text: sanitizeSurrogates(c.text) }; - case "image": - return { image: createImageBlock(c.mimeType, c.data) }; - default: - throw new Error("Unknown user content type"); - } - }), + content, }); break; + } case "assistant": { // Skip assistant messages with empty content (e.g., from aborted requests) // Bedrock rejects messages with empty content arrays @@ -686,7 +693,7 @@ function convertMessages( } break; default: - throw new Error("Unknown assistant content type"); + continue; } } // Skip if all content blocks were filtered out @@ -745,7 +752,7 @@ function convertMessages( break; } default: - throw new Error("Unknown message role"); + continue; } } diff --git a/packages/ai/test/bedrock-convert-messages.test.ts b/packages/ai/test/bedrock-convert-messages.test.ts new file mode 100644 index 00000000..d9608763 --- /dev/null +++ b/packages/ai/test/bedrock-convert-messages.test.ts @@ -0,0 +1,159 @@ +import { describe, expect, it, vi } from "vitest"; + +const bedrockMock = vi.hoisted(() => ({ + constructorCalls: [] as Array>, +})); + +vi.mock("@aws-sdk/client-bedrock-runtime", () => { + class BedrockRuntimeServiceException extends Error {} + + class BedrockRuntimeClient { + constructor(config: Record) { + bedrockMock.constructorCalls.push(config); + } + + send(): Promise { + return Promise.reject(new Error("mock send")); + } + } + + class ConverseStreamCommand { + readonly input: unknown; + + constructor(input: unknown) { + this.input = input; + } + } + + return { + BedrockRuntimeClient, + BedrockRuntimeServiceException, + ConverseStreamCommand, + StopReason: { + END_TURN: "end_turn", + STOP_SEQUENCE: "stop_sequence", + MAX_TOKENS: "max_tokens", + MODEL_CONTEXT_WINDOW_EXCEEDED: "model_context_window_exceeded", + TOOL_USE: "tool_use", + }, + CachePointType: { DEFAULT: "default" }, + CacheTTL: { ONE_HOUR: "ONE_HOUR" }, + ConversationRole: { ASSISTANT: "assistant", USER: "user" }, + ImageFormat: { JPEG: "jpeg", PNG: "png", GIF: "gif", WEBP: "webp" }, + ToolResultStatus: { ERROR: "error", SUCCESS: "success" }, + }; +}); + +import { getModel } from "../src/models.js"; +import { streamBedrock } from "../src/providers/amazon-bedrock.js"; +import type { Context, Message } from "../src/types.js"; + +const baseModel = getModel("amazon-bedrock", "us.anthropic.claude-sonnet-4-5-20250929-v1:0"); + +async function capturePayload(context: Context): Promise { + let capturedPayload: unknown; + const s = streamBedrock(baseModel, context, { + cacheRetention: "none", + signal: AbortSignal.abort(), + onPayload: (payload) => { + capturedPayload = payload; + return payload; + }, + }); + for await (const event of s) { + if (event.type === "error") break; + } + return capturedPayload; +} + +describe("bedrock convertMessages skips unknown content types", () => { + it("skips unknown user content blocks instead of throwing", async () => { + const messages: Message[] = [ + { + role: "user", + content: [ + { type: "text", text: "hello" }, + { type: "unknown", data: "foo" }, + ] as any, + timestamp: Date.now(), + }, + ]; + const payload = await capturePayload({ messages }); + expect(payload).toBeDefined(); + const p = payload as { messages: Array<{ role: string; content: unknown[] }> }; + expect(p.messages).toHaveLength(1); + expect(p.messages[0].content).toHaveLength(1); + expect(p.messages[0].content[0]).toEqual({ text: "hello" }); + }); + + it("skips unknown assistant content blocks instead of throwing", async () => { + const messages: Message[] = [ + { + role: "assistant", + content: [ + { type: "text", text: "hello" }, + { type: "unknown", data: "foo" }, + ] as any, + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + model: baseModel.id, + usage: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + totalTokens: 0, + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 }, + }, + stopReason: "stop", + timestamp: Date.now(), + }, + ]; + const payload = await capturePayload({ messages }); + expect(payload).toBeDefined(); + const p = payload as { messages: Array<{ role: string; content: unknown[] }> }; + expect(p.messages).toHaveLength(1); + expect(p.messages[0].content).toHaveLength(1); + expect(p.messages[0].content[0]).toEqual({ text: "hello" }); + }); + + it("skips user messages with only unknown content blocks", async () => { + const messages: Message[] = [ + { + role: "user", + content: [{ type: "unknown", data: "foo" }] as any, + timestamp: Date.now(), + }, + ]; + const payload = await capturePayload({ messages }); + expect(payload).toBeDefined(); + const p = payload as { messages: Array<{ role: string; content: unknown[] }> }; + expect(p.messages).toHaveLength(0); + }); + + it("skips assistant messages with only unknown content blocks", async () => { + const messages: Message[] = [ + { + role: "assistant", + content: [{ type: "unknown", data: "foo" }] as any, + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + model: baseModel.id, + usage: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + totalTokens: 0, + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 }, + }, + stopReason: "stop", + timestamp: Date.now(), + }, + ]; + const payload = await capturePayload({ messages }); + expect(payload).toBeDefined(); + const p = payload as { messages: Array<{ role: string; content: unknown[] }> }; + expect(p.messages).toHaveLength(0); + }); +}); From 085230373f585c08d32457f615d29b356348b586 Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Mon, 18 May 2026 01:56:14 +0200 Subject: [PATCH 087/166] fix(coding-agent): align undici fetch globals closes #4650 closes #4652 closes #4653 --- packages/coding-agent/CHANGELOG.md | 1 + packages/coding-agent/src/cli.ts | 7 ++++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 95ead56e..8dd9647c 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -4,6 +4,7 @@ ### Fixed +- Fixed JSON parse failures for compressed fetch responses under Node 26.0 by installing undici fetch globals alongside pi's global dispatcher ([#4650](https://github.com/earendil-works/pi/issues/4650), [#4652](https://github.com/earendil-works/pi/issues/4652), [#4653](https://github.com/earendil-works/pi/issues/4653)). - Fixed npm-family package commands on Windows to avoid shell argument splitting when install prefixes contain spaces ([#4623](https://github.com/earendil-works/pi/issues/4623)). ## [0.75.0] - 2026-05-17 diff --git a/packages/coding-agent/src/cli.ts b/packages/coding-agent/src/cli.ts index 6e0e8403..68f960fe 100644 --- a/packages/coding-agent/src/cli.ts +++ b/packages/coding-agent/src/cli.ts @@ -5,7 +5,7 @@ * * Test with: npx tsx src/cli-new.ts [args...] */ -import { EnvHttpProxyAgent, setGlobalDispatcher } from "undici"; +import { EnvHttpProxyAgent, install as installUndiciGlobals, setGlobalDispatcher } from "undici"; import { APP_NAME } from "./config.js"; import { main } from "./main.js"; @@ -19,4 +19,9 @@ process.emitWarning = (() => {}) as typeof process.emitWarning; // AbortController-based deadlines via retry.provider.timeoutMs. setGlobalDispatcher(new EnvHttpProxyAgent({ bodyTimeout: 0, headersTimeout: 0 })); +// Keep fetch and the dispatcher on the same undici implementation. Node 26.0's +// bundled fetch can otherwise consume compressed responses through npm undici's +// dispatcher without decompressing them, causing response.json() failures. +installUndiciGlobals(); + main(process.argv.slice(2)); From f1218fa8aa8c92ef7d8eef9a1c18830192f58f63 Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Mon, 18 May 2026 02:00:07 +0200 Subject: [PATCH 088/166] chore: audit unreleased changelogs --- packages/ai/CHANGELOG.md | 5 +++++ packages/coding-agent/CHANGELOG.md | 10 ++++++++++ 2 files changed, 15 insertions(+) diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index 803bca78..8213427b 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -4,11 +4,16 @@ ### Fixed +- Fixed Anthropic-compatible API-key requests to ignore unrelated `ANTHROPIC_AUTH_TOKEN` environment values, avoiding invalid bearer credentials for providers such as Xiaomi MiMo ([#4342](https://github.com/earendil-works/pi/issues/4342)). - Fixed Amazon Bedrock message conversion to skip unknown content blocks instead of failing the stream ([#4223](https://github.com/earendil-works/pi/issues/4223)). - Fixed Azure OpenAI Responses and OpenAI Responses error formatting to prefix HTTP status codes onto `errorMessage`, so transient 5xx and 429 errors are correctly matched by the agent-level auto-retry classifier ([#4232](https://github.com/earendil-works/pi/issues/4232)). - Fixed Xiaomi MiMo model metadata to use the OpenAI-compatible endpoints and `openai-completions` API, restoring multi-turn thinking/tool-call sessions ([#4505](https://github.com/earendil-works/pi/issues/4505)). - Fixed OpenCode Go Kimi reasoning replay by normalizing streamed `reasoning` fields back to `reasoning_content` for OpenCode Go only ([#4251](https://github.com/earendil-works/pi/issues/4251)). +### Removed + +- Removed non-working OpenAI Codex fast model variants. + ## [0.75.0] - 2026-05-17 ### Breaking Changes diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 8dd9647c..63983f62 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -4,9 +4,19 @@ ### Fixed +- Fixed config selectors to scale their visible row count to terminal height ([#4243](https://github.com/earendil-works/pi-mono/pull/4243) by [@samjonester](https://github.com/samjonester)). +- Fixed Anthropic-compatible API-key requests to ignore unrelated `ANTHROPIC_AUTH_TOKEN` environment values, avoiding invalid bearer credentials for providers such as Xiaomi MiMo inherited from `@earendil-works/pi-ai` ([#4342](https://github.com/earendil-works/pi/issues/4342)). +- Fixed Amazon Bedrock message conversion to skip unknown content blocks instead of failing the stream, inherited from `@earendil-works/pi-ai` ([#4223](https://github.com/earendil-works/pi/issues/4223)). +- Fixed Azure OpenAI Responses and OpenAI Responses error formatting to prefix HTTP status codes onto `errorMessage`, so transient 5xx and 429 errors are correctly matched by the agent-level auto-retry classifier inherited from `@earendil-works/pi-ai` ([#4232](https://github.com/earendil-works/pi/issues/4232)). +- Fixed OpenCode Go Kimi reasoning replay by normalizing streamed `reasoning` fields back to `reasoning_content` for OpenCode Go only, inherited from `@earendil-works/pi-ai` ([#4251](https://github.com/earendil-works/pi/issues/4251)). +- Fixed Xiaomi MiMo model metadata to use the OpenAI-compatible endpoints and `openai-completions` API, restoring multi-turn thinking/tool-call sessions inherited from `@earendil-works/pi-ai` ([#4505](https://github.com/earendil-works/pi/issues/4505)). - Fixed JSON parse failures for compressed fetch responses under Node 26.0 by installing undici fetch globals alongside pi's global dispatcher ([#4650](https://github.com/earendil-works/pi/issues/4650), [#4652](https://github.com/earendil-works/pi/issues/4652), [#4653](https://github.com/earendil-works/pi/issues/4653)). - Fixed npm-family package commands on Windows to avoid shell argument splitting when install prefixes contain spaces ([#4623](https://github.com/earendil-works/pi/issues/4623)). +### Removed + +- Removed non-working OpenAI Codex fast model variants inherited from `@earendil-works/pi-ai`. + ## [0.75.0] - 2026-05-17 ### Breaking Changes From 73a61654af52f8b02439d697a3b74126fa6b6782 Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Mon, 18 May 2026 02:01:03 +0200 Subject: [PATCH 089/166] Release v0.75.1 --- package-lock.json | 32 +++++++++---------- packages/agent/CHANGELOG.md | 2 +- packages/agent/package.json | 4 +-- packages/ai/CHANGELOG.md | 2 +- packages/ai/package.json | 2 +- packages/coding-agent/CHANGELOG.md | 2 +- .../package-lock.json | 4 +-- .../custom-provider-anthropic/package.json | 2 +- .../custom-provider-gitlab-duo/package.json | 2 +- .../extensions/sandbox/package-lock.json | 4 +-- .../examples/extensions/sandbox/package.json | 2 +- .../extensions/with-deps/package-lock.json | 4 +-- .../extensions/with-deps/package.json | 2 +- packages/coding-agent/package.json | 8 ++--- packages/tui/CHANGELOG.md | 2 +- packages/tui/package.json | 2 +- packages/web-ui/CHANGELOG.md | 2 +- packages/web-ui/example/package.json | 2 +- packages/web-ui/package.json | 6 ++-- 19 files changed, 43 insertions(+), 43 deletions(-) diff --git a/package-lock.json b/package-lock.json index 776469fa..47505733 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7771,10 +7771,10 @@ }, "packages/agent": { "name": "@earendil-works/pi-agent-core", - "version": "0.75.0", + "version": "0.75.1", "license": "MIT", "dependencies": { - "@earendil-works/pi-ai": "^0.75.0", + "@earendil-works/pi-ai": "^0.75.1", "ignore": "^7.0.5", "typebox": "^1.1.24", "yaml": "^2.8.2" @@ -7808,7 +7808,7 @@ }, "packages/ai": { "name": "@earendil-works/pi-ai", - "version": "0.75.0", + "version": "0.75.1", "license": "MIT", "dependencies": { "@anthropic-ai/sdk": "^0.91.1", @@ -7852,12 +7852,12 @@ }, "packages/coding-agent": { "name": "@earendil-works/pi-coding-agent", - "version": "0.75.0", + "version": "0.75.1", "license": "MIT", "dependencies": { - "@earendil-works/pi-agent-core": "^0.75.0", - "@earendil-works/pi-ai": "^0.75.0", - "@earendil-works/pi-tui": "^0.75.0", + "@earendil-works/pi-agent-core": "^0.75.1", + "@earendil-works/pi-ai": "^0.75.1", + "@earendil-works/pi-tui": "^0.75.1", "@silvia-odwyer/photon-node": "^0.3.4", "chalk": "^5.5.0", "diff": "^8.0.2", @@ -7894,25 +7894,25 @@ }, "packages/coding-agent/examples/extensions/custom-provider-anthropic": { "name": "pi-extension-custom-provider-anthropic", - "version": "0.75.0", + "version": "0.75.1", "dependencies": { "@anthropic-ai/sdk": "^0.52.0" } }, "packages/coding-agent/examples/extensions/custom-provider-gitlab-duo": { "name": "pi-extension-custom-provider-gitlab-duo", - "version": "0.75.0" + "version": "0.75.1" }, "packages/coding-agent/examples/extensions/sandbox": { "name": "pi-extension-sandbox", - "version": "1.5.0", + "version": "1.5.1", "dependencies": { "@anthropic-ai/sandbox-runtime": "^0.0.26" } }, "packages/coding-agent/examples/extensions/with-deps": { "name": "pi-extension-with-deps", - "version": "0.75.0", + "version": "0.75.1", "dependencies": { "ms": "^2.1.3" }, @@ -7948,7 +7948,7 @@ }, "packages/tui": { "name": "@earendil-works/pi-tui", - "version": "0.75.0", + "version": "0.75.1", "license": "MIT", "dependencies": { "get-east-asian-width": "^1.3.0", @@ -7968,11 +7968,11 @@ }, "packages/web-ui": { "name": "@earendil-works/pi-web-ui", - "version": "0.75.0", + "version": "0.75.1", "license": "MIT", "dependencies": { - "@earendil-works/pi-ai": "^0.75.0", - "@earendil-works/pi-tui": "^0.75.0", + "@earendil-works/pi-ai": "^0.75.1", + "@earendil-works/pi-tui": "^0.75.1", "@lmstudio/sdk": "^1.5.0", "docx-preview": "^0.3.7", "jszip": "^3.10.1", @@ -7995,7 +7995,7 @@ }, "packages/web-ui/example": { "name": "pi-web-ui-example", - "version": "0.75.0", + "version": "0.75.1", "dependencies": { "@earendil-works/pi-ai": "file:../../ai", "@earendil-works/pi-web-ui": "file:../", diff --git a/packages/agent/CHANGELOG.md b/packages/agent/CHANGELOG.md index 5bc519da..2ed8e0dc 100644 --- a/packages/agent/CHANGELOG.md +++ b/packages/agent/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## [Unreleased] +## [0.75.1] - 2026-05-18 ## [0.75.0] - 2026-05-17 diff --git a/packages/agent/package.json b/packages/agent/package.json index 6e6685f6..0ef97503 100644 --- a/packages/agent/package.json +++ b/packages/agent/package.json @@ -1,6 +1,6 @@ { "name": "@earendil-works/pi-agent-core", - "version": "0.75.0", + "version": "0.75.1", "description": "General-purpose agent with transport abstraction, state management, and attachment support", "type": "module", "main": "./dist/index.js", @@ -30,7 +30,7 @@ "prepublishOnly": "npm run clean && npm run build" }, "dependencies": { - "@earendil-works/pi-ai": "^0.75.0", + "@earendil-works/pi-ai": "^0.75.1", "ignore": "^7.0.5", "typebox": "^1.1.24", "yaml": "^2.8.2" diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index 8213427b..5ad0a329 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## [Unreleased] +## [0.75.1] - 2026-05-18 ### Fixed diff --git a/packages/ai/package.json b/packages/ai/package.json index eedc1de4..f21ec37f 100644 --- a/packages/ai/package.json +++ b/packages/ai/package.json @@ -1,6 +1,6 @@ { "name": "@earendil-works/pi-ai", - "version": "0.75.0", + "version": "0.75.1", "description": "Unified LLM API with automatic model discovery and provider configuration", "type": "module", "main": "./dist/index.js", diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 63983f62..fb9887ed 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## [Unreleased] +## [0.75.1] - 2026-05-18 ### Fixed diff --git a/packages/coding-agent/examples/extensions/custom-provider-anthropic/package-lock.json b/packages/coding-agent/examples/extensions/custom-provider-anthropic/package-lock.json index 2321b707..76e3eb34 100644 --- a/packages/coding-agent/examples/extensions/custom-provider-anthropic/package-lock.json +++ b/packages/coding-agent/examples/extensions/custom-provider-anthropic/package-lock.json @@ -1,12 +1,12 @@ { "name": "pi-extension-custom-provider", - "version": "0.75.0", + "version": "0.75.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pi-extension-custom-provider", - "version": "0.75.0", + "version": "0.75.1", "dependencies": { "@anthropic-ai/sdk": "^0.52.0" } diff --git a/packages/coding-agent/examples/extensions/custom-provider-anthropic/package.json b/packages/coding-agent/examples/extensions/custom-provider-anthropic/package.json index a7c10d75..e981ee5d 100644 --- a/packages/coding-agent/examples/extensions/custom-provider-anthropic/package.json +++ b/packages/coding-agent/examples/extensions/custom-provider-anthropic/package.json @@ -1,7 +1,7 @@ { "name": "pi-extension-custom-provider-anthropic", "private": true, - "version": "0.75.0", + "version": "0.75.1", "type": "module", "scripts": { "clean": "echo 'nothing to clean'", diff --git a/packages/coding-agent/examples/extensions/custom-provider-gitlab-duo/package.json b/packages/coding-agent/examples/extensions/custom-provider-gitlab-duo/package.json index 4287ccc7..eb894a0f 100644 --- a/packages/coding-agent/examples/extensions/custom-provider-gitlab-duo/package.json +++ b/packages/coding-agent/examples/extensions/custom-provider-gitlab-duo/package.json @@ -1,7 +1,7 @@ { "name": "pi-extension-custom-provider-gitlab-duo", "private": true, - "version": "0.75.0", + "version": "0.75.1", "type": "module", "scripts": { "clean": "echo 'nothing to clean'", diff --git a/packages/coding-agent/examples/extensions/sandbox/package-lock.json b/packages/coding-agent/examples/extensions/sandbox/package-lock.json index 62baeba0..7daafbad 100644 --- a/packages/coding-agent/examples/extensions/sandbox/package-lock.json +++ b/packages/coding-agent/examples/extensions/sandbox/package-lock.json @@ -1,12 +1,12 @@ { "name": "pi-extension-sandbox", - "version": "1.5.0", + "version": "1.5.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pi-extension-sandbox", - "version": "1.5.0", + "version": "1.5.1", "dependencies": { "@anthropic-ai/sandbox-runtime": "^0.0.26" } diff --git a/packages/coding-agent/examples/extensions/sandbox/package.json b/packages/coding-agent/examples/extensions/sandbox/package.json index 6c548f4e..009e0e93 100644 --- a/packages/coding-agent/examples/extensions/sandbox/package.json +++ b/packages/coding-agent/examples/extensions/sandbox/package.json @@ -1,7 +1,7 @@ { "name": "pi-extension-sandbox", "private": true, - "version": "1.5.0", + "version": "1.5.1", "type": "module", "scripts": { "clean": "echo 'nothing to clean'", diff --git a/packages/coding-agent/examples/extensions/with-deps/package-lock.json b/packages/coding-agent/examples/extensions/with-deps/package-lock.json index bed97766..5d74195a 100644 --- a/packages/coding-agent/examples/extensions/with-deps/package-lock.json +++ b/packages/coding-agent/examples/extensions/with-deps/package-lock.json @@ -1,12 +1,12 @@ { "name": "pi-extension-with-deps", - "version": "0.75.0", + "version": "0.75.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pi-extension-with-deps", - "version": "0.75.0", + "version": "0.75.1", "dependencies": { "ms": "^2.1.3" }, diff --git a/packages/coding-agent/examples/extensions/with-deps/package.json b/packages/coding-agent/examples/extensions/with-deps/package.json index 77548470..da1d2ef1 100644 --- a/packages/coding-agent/examples/extensions/with-deps/package.json +++ b/packages/coding-agent/examples/extensions/with-deps/package.json @@ -1,7 +1,7 @@ { "name": "pi-extension-with-deps", "private": true, - "version": "0.75.0", + "version": "0.75.1", "type": "module", "scripts": { "clean": "echo 'nothing to clean'", diff --git a/packages/coding-agent/package.json b/packages/coding-agent/package.json index 028b47b7..31d1c95d 100644 --- a/packages/coding-agent/package.json +++ b/packages/coding-agent/package.json @@ -1,6 +1,6 @@ { "name": "@earendil-works/pi-coding-agent", - "version": "0.75.0", + "version": "0.75.1", "description": "Coding agent CLI with read, bash, edit, write tools and session management", "type": "module", "piConfig": { @@ -38,9 +38,9 @@ "prepublishOnly": "npm run clean && npm run build" }, "dependencies": { - "@earendil-works/pi-agent-core": "^0.75.0", - "@earendil-works/pi-ai": "^0.75.0", - "@earendil-works/pi-tui": "^0.75.0", + "@earendil-works/pi-agent-core": "^0.75.1", + "@earendil-works/pi-ai": "^0.75.1", + "@earendil-works/pi-tui": "^0.75.1", "@silvia-odwyer/photon-node": "^0.3.4", "chalk": "^5.5.0", "diff": "^8.0.2", diff --git a/packages/tui/CHANGELOG.md b/packages/tui/CHANGELOG.md index 4457e0f6..ca8356db 100644 --- a/packages/tui/CHANGELOG.md +++ b/packages/tui/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## [Unreleased] +## [0.75.1] - 2026-05-18 ## [0.75.0] - 2026-05-17 diff --git a/packages/tui/package.json b/packages/tui/package.json index a16e36a5..4056e28d 100644 --- a/packages/tui/package.json +++ b/packages/tui/package.json @@ -1,6 +1,6 @@ { "name": "@earendil-works/pi-tui", - "version": "0.75.0", + "version": "0.75.1", "description": "Terminal User Interface library with differential rendering for efficient text-based applications", "type": "module", "main": "dist/index.js", diff --git a/packages/web-ui/CHANGELOG.md b/packages/web-ui/CHANGELOG.md index 26a984b8..b10faee3 100644 --- a/packages/web-ui/CHANGELOG.md +++ b/packages/web-ui/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## [Unreleased] +## [0.75.1] - 2026-05-18 ## [0.74.1] - 2026-05-16 diff --git a/packages/web-ui/example/package.json b/packages/web-ui/example/package.json index 604061da..851a103c 100644 --- a/packages/web-ui/example/package.json +++ b/packages/web-ui/example/package.json @@ -1,6 +1,6 @@ { "name": "pi-web-ui-example", - "version": "0.75.0", + "version": "0.75.1", "private": true, "type": "module", "scripts": { diff --git a/packages/web-ui/package.json b/packages/web-ui/package.json index bff9345e..c3f81dfb 100644 --- a/packages/web-ui/package.json +++ b/packages/web-ui/package.json @@ -1,6 +1,6 @@ { "name": "@earendil-works/pi-web-ui", - "version": "0.75.0", + "version": "0.75.1", "description": "Reusable web UI components for AI chat interfaces powered by @earendil-works/pi-ai", "type": "module", "main": "dist/index.js", @@ -18,8 +18,8 @@ }, "dependencies": { "@lmstudio/sdk": "^1.5.0", - "@earendil-works/pi-ai": "^0.75.0", - "@earendil-works/pi-tui": "^0.75.0", + "@earendil-works/pi-ai": "^0.75.1", + "@earendil-works/pi-tui": "^0.75.1", "typebox": "^1.1.24", "docx-preview": "^0.3.7", "jszip": "^3.10.1", From 93b2e7fae75c1136e1c169d2f70d1c13d4b689d7 Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Mon, 18 May 2026 02:01:57 +0200 Subject: [PATCH 090/166] Add [Unreleased] section for next cycle --- packages/agent/CHANGELOG.md | 2 ++ packages/ai/CHANGELOG.md | 2 ++ packages/coding-agent/CHANGELOG.md | 2 ++ packages/tui/CHANGELOG.md | 2 ++ packages/web-ui/CHANGELOG.md | 2 ++ 5 files changed, 10 insertions(+) diff --git a/packages/agent/CHANGELOG.md b/packages/agent/CHANGELOG.md index 2ed8e0dc..0dcff063 100644 --- a/packages/agent/CHANGELOG.md +++ b/packages/agent/CHANGELOG.md @@ -1,5 +1,7 @@ # Changelog +## [Unreleased] + ## [0.75.1] - 2026-05-18 ## [0.75.0] - 2026-05-17 diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index 5ad0a329..6271bcac 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -1,5 +1,7 @@ # Changelog +## [Unreleased] + ## [0.75.1] - 2026-05-18 ### Fixed diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index fb9887ed..969d9d32 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -1,5 +1,7 @@ # Changelog +## [Unreleased] + ## [0.75.1] - 2026-05-18 ### Fixed diff --git a/packages/tui/CHANGELOG.md b/packages/tui/CHANGELOG.md index ca8356db..db2a3050 100644 --- a/packages/tui/CHANGELOG.md +++ b/packages/tui/CHANGELOG.md @@ -1,5 +1,7 @@ # Changelog +## [Unreleased] + ## [0.75.1] - 2026-05-18 ## [0.75.0] - 2026-05-17 diff --git a/packages/web-ui/CHANGELOG.md b/packages/web-ui/CHANGELOG.md index b10faee3..f7ea1c30 100644 --- a/packages/web-ui/CHANGELOG.md +++ b/packages/web-ui/CHANGELOG.md @@ -1,5 +1,7 @@ # Changelog +## [Unreleased] + ## [0.75.1] - 2026-05-18 ## [0.74.1] - 2026-05-16 From 363214bd320ff4c7f52e74cd972300d4973e378f Mon Sep 17 00:00:00 2001 From: Doug Masiero Date: Sun, 17 May 2026 21:58:27 -0400 Subject: [PATCH 091/166] fix(coding-agent): guard undici install under Bun fixes #4657 --- packages/coding-agent/CHANGELOG.md | 4 ++++ packages/coding-agent/src/cli.ts | 6 +++--- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 969d9d32..d1ee7ea9 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Fixed + +- Fixed Bun-compiled release binaries failing to start when Bun's built-in undici shim lacks npm undici's `install` export ([#4657](https://github.com/earendil-works/pi/issues/4657)). + ## [0.75.1] - 2026-05-18 ### Fixed diff --git a/packages/coding-agent/src/cli.ts b/packages/coding-agent/src/cli.ts index 68f960fe..f75d6bd1 100644 --- a/packages/coding-agent/src/cli.ts +++ b/packages/coding-agent/src/cli.ts @@ -5,7 +5,7 @@ * * Test with: npx tsx src/cli-new.ts [args...] */ -import { EnvHttpProxyAgent, install as installUndiciGlobals, setGlobalDispatcher } from "undici"; +import * as undici from "undici"; import { APP_NAME } from "./config.js"; import { main } from "./main.js"; @@ -17,11 +17,11 @@ process.emitWarning = (() => {}) as typeof process.emitWarning; // (e.g. vLLM buffering a large tool call) exceed that and abort the SSE stream // with UND_ERR_BODY_TIMEOUT. Disable both — provider SDKs enforce their own // AbortController-based deadlines via retry.provider.timeoutMs. -setGlobalDispatcher(new EnvHttpProxyAgent({ bodyTimeout: 0, headersTimeout: 0 })); +undici.setGlobalDispatcher(new undici.EnvHttpProxyAgent({ bodyTimeout: 0, headersTimeout: 0 })); // Keep fetch and the dispatcher on the same undici implementation. Node 26.0's // bundled fetch can otherwise consume compressed responses through npm undici's // dispatcher without decompressing them, causing response.json() failures. -installUndiciGlobals(); +undici.install?.(); main(process.argv.slice(2)); From 64248840f87662c1a1aedf3568209ffde575ab94 Mon Sep 17 00:00:00 2001 From: Michael Yu Date: Mon, 18 May 2026 10:43:21 +0800 Subject: [PATCH 092/166] fix(coding-agent): scroll shared tool entries to rendered tool calls --- .../src/core/export-html/template.js | 20 ++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/packages/coding-agent/src/core/export-html/template.js b/packages/coding-agent/src/core/export-html/template.js index cf28b935..07ab3e25 100644 --- a/packages/coding-agent/src/core/export-html/template.js +++ b/packages/coding-agent/src/core/export-html/template.js @@ -910,7 +910,8 @@ ''; }; - let html = `
`; + const toolDomId = `tool-call-${escapeHtml(call.id)}`; + let html = `
`; const args = call.arguments || {}; const name = call.name; @@ -1445,6 +1446,16 @@ // Cache for rendered entry DOM nodes const entryCache = new Map(); + function getScrollTargetElementId(entryId) { + const entry = byId.get(entryId); + if (entry?.type === 'message' && entry.message.role === 'toolResult' && entry.message.toolCallId) { + // getElementById() matches the parsed DOM id attribute, whose HTML entities + // were already resolved from the escaped id rendered by renderToolCall(). + return `tool-call-${entry.message.toolCallId}`; + } + return `entry-${entryId}`; + } + function renderEntryToNode(entry) { // Check cache first if (entryCache.has(entry.id)) { @@ -1506,9 +1517,12 @@ if (scrollMode === 'bottom') { content.scrollTop = content.scrollHeight; } else if (scrollMode === 'target') { - // If scrollToEntryId is provided, scroll to that specific entry + // If scrollToEntryId is provided, scroll to that specific entry. + // Tool result entries are rendered inside their assistant tool-call block, + // so route them to the visible tool-call element instead. const scrollTargetId = scrollToEntryId || targetId; - const targetEl = document.getElementById(`entry-${scrollTargetId}`); + const targetEl = document.getElementById(getScrollTargetElementId(scrollTargetId)) || + document.getElementById(`entry-${scrollTargetId}`); if (targetEl) { targetEl.scrollIntoView({ block: 'center' }); // Briefly highlight the target message From 89b64d9dd5411e40092d4417b8d77ce911092828 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 18 May 2026 08:37:43 +0200 Subject: [PATCH 093/166] fix(coding-agent): avoid Windows self-update native locks closes #4157 --- packages/coding-agent/CHANGELOG.md | 4 + packages/coding-agent/src/main.ts | 7 +- .../coding-agent/src/package-manager-cli.ts | 24 ++++++ .../src/utils/windows-self-update.ts | 84 +++++++++++++++++++ 4 files changed, 118 insertions(+), 1 deletion(-) create mode 100644 packages/coding-agent/src/utils/windows-self-update.ts diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 969d9d32..822b83ec 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Fixed + +- Fixed Windows npm self-updates to move loaded native dependency packages out of the active install before reinstalling pi ([#4157](https://github.com/earendil-works/pi/issues/4157)). + ## [0.75.1] - 2026-05-18 ### Fixed diff --git a/packages/coding-agent/src/main.ts b/packages/coding-agent/src/main.ts index 72e502be..9f1af697 100644 --- a/packages/coding-agent/src/main.ts +++ b/packages/coding-agent/src/main.ts @@ -15,7 +15,7 @@ import { processFileArguments } from "./cli/file-processor.js"; import { buildInitialMessage } from "./cli/initial-message.js"; import { listModels } from "./cli/list-models.js"; import { selectSession } from "./cli/session-picker.js"; -import { ENV_SESSION_DIR, expandTildePath, getAgentDir, VERSION } from "./config.js"; +import { ENV_SESSION_DIR, expandTildePath, getAgentDir, getPackageDir, VERSION } from "./config.js"; import { type CreateAgentSessionRuntimeFactory, createAgentSessionRuntime } from "./core/agent-session-runtime.js"; import { type AgentSessionRuntimeDiagnostic, @@ -46,6 +46,7 @@ import { ExtensionSelectorComponent } from "./modes/interactive/components/exten import { initTheme, stopThemeWatcher } from "./modes/interactive/theme/theme.js"; import { handleConfigCommand, handlePackageCommand } from "./package-manager-cli.js"; import { isLocalPath } from "./utils/paths.js"; +import { cleanupWindowsSelfUpdateQuarantine } from "./utils/windows-self-update.js"; /** * Read all content from piped stdin. @@ -428,6 +429,10 @@ export async function main(args: string[], options?: MainOptions) { process.env.PI_SKIP_VERSION_CHECK = "1"; } + if (process.platform === "win32") { + cleanupWindowsSelfUpdateQuarantine(getPackageDir()); + } + if (await handlePackageCommand(args)) { return; } diff --git a/packages/coding-agent/src/package-manager-cli.ts b/packages/coding-agent/src/package-manager-cli.ts index f421af32..a3ef1e21 100644 --- a/packages/coding-agent/src/package-manager-cli.ts +++ b/packages/coding-agent/src/package-manager-cli.ts @@ -3,7 +3,9 @@ import { spawn } from "child_process"; import { selectConfig } from "./cli/config-selector.js"; import { APP_NAME, + detectInstallMethod, getAgentDir, + getPackageDir, getSelfUpdateCommand, getSelfUpdateUnavailableInstruction, PACKAGE_NAME, @@ -14,6 +16,10 @@ import { DefaultPackageManager } from "./core/package-manager.js"; import { SettingsManager } from "./core/settings-manager.js"; import { resolveSpawnCommand } from "./utils/child-process.js"; import { getLatestPiRelease, isNewerPackageVersion } from "./utils/version-check.js"; +import { + cleanupWindowsSelfUpdateQuarantine, + quarantineWindowsNativeDependencies, +} from "./utils/windows-self-update.js"; export type PackageCommand = "install" | "remove" | "update" | "list"; @@ -336,6 +342,16 @@ async function runSelfUpdate(command: SelfUpdateCommand): Promise { } } +function prepareWindowsNpmSelfUpdate(): void { + if (process.platform !== "win32") { + return; + } + + const packageDir = getPackageDir(); + cleanupWindowsSelfUpdateQuarantine(packageDir); + quarantineWindowsNativeDependencies(packageDir); +} + export async function handleConfigCommand(args: string[]): Promise { if (args[0] !== "config") { return false; @@ -489,6 +505,13 @@ export async function handlePackageCommand(args: string[]): Promise { if (!selfUpdatePlan.shouldRun) { return true; } + const installMethod = detectInstallMethod(); + if (process.platform === "win32" && installMethod !== "npm") { + console.error(chalk.red(`${APP_NAME} self-update on Windows is only supported for npm installs.`)); + console.error(chalk.dim(`Detected install method: ${installMethod}. Update ${APP_NAME} manually.`)); + process.exitCode = 1; + return true; + } const selfUpdateCommand = getSelfUpdateCommand( PACKAGE_NAME, selfUpdateNpmCommand, @@ -500,6 +523,7 @@ export async function handlePackageCommand(args: string[]): Promise { return true; } try { + prepareWindowsNpmSelfUpdate(); await runSelfUpdate(selfUpdateCommand); } catch (error: unknown) { const message = error instanceof Error ? error.message : "Unknown package command error"; diff --git a/packages/coding-agent/src/utils/windows-self-update.ts b/packages/coding-agent/src/utils/windows-self-update.ts new file mode 100644 index 00000000..a988529e --- /dev/null +++ b/packages/coding-agent/src/utils/windows-self-update.ts @@ -0,0 +1,84 @@ +import { randomUUID } from "node:crypto"; +import { copyFileSync, existsSync, mkdirSync, renameSync, rmSync } from "node:fs"; +import { basename, dirname, join, relative, resolve, toNamespacedPath } from "node:path"; +import { getCwdRelativePath } from "./paths.js"; + +const QUARANTINE_DIR_NAME = ".pi-native-quarantine"; + +function normalizePath(path: string): string { + return toNamespacedPath(resolve(path)); +} + +function getQuarantineRoot(packageDir: string): string | undefined { + let current = resolve(packageDir); + while (true) { + if (basename(current).toLowerCase() === "node_modules") { + return join(current, QUARANTINE_DIR_NAME); + } + const parent = dirname(current); + if (parent === current) { + return undefined; + } + current = parent; + } +} + +function getLoadedSharedObjectsInPackageDir(packageDir: string): string[] { + const sharedObjects = (process.report.getReport() as { sharedObjects?: unknown }).sharedObjects; + if (!Array.isArray(sharedObjects)) { + return []; + } + + const root = normalizePath(packageDir).toLowerCase(); + const seen = new Set(); + const loadedFiles: string[] = []; + for (const value of sharedObjects) { + if (typeof value !== "string") { + continue; + } + const filePath = normalizePath(value); + const comparisonPath = filePath.toLowerCase(); + if (getCwdRelativePath(comparisonPath, root) === undefined || seen.has(comparisonPath)) { + continue; + } + seen.add(comparisonPath); + loadedFiles.push(filePath); + } + return loadedFiles; +} + +export function cleanupWindowsSelfUpdateQuarantine(packageDir: string): void { + const quarantineRoot = getQuarantineRoot(packageDir); + if (!quarantineRoot) { + return; + } + try { + rmSync(quarantineRoot, { recursive: true, force: true }); + } catch { + // A previous pi process may still be exiting and holding a native addon. + } +} + +export function quarantineWindowsNativeDependencies(packageDir: string): void { + const resolvedPackageDir = normalizePath(packageDir); + const quarantineRoot = getQuarantineRoot(resolvedPackageDir); + if (!quarantineRoot) { + return; + } + + const loadedFiles = getLoadedSharedObjectsInPackageDir(resolvedPackageDir); + if (loadedFiles.length === 0) { + return; + } + + const quarantineRunDir = join(quarantineRoot, `${Date.now()}-${process.pid}-${randomUUID()}`); + for (const loadedFile of loadedFiles) { + if (!existsSync(loadedFile)) { + continue; + } + const quarantinePath = join(quarantineRunDir, relative(resolvedPackageDir, loadedFile)); + mkdirSync(dirname(quarantinePath), { recursive: true }); + renameSync(loadedFile, quarantinePath); + copyFileSync(quarantinePath, loadedFile); + } +} From 96cad24a07ba41d51935b2400ed0303f1edec288 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 18 May 2026 08:56:24 +0200 Subject: [PATCH 094/166] fix(coding-agent): detect pnpm v11 self-update installs closes #4647 --- packages/coding-agent/CHANGELOG.md | 1 + packages/coding-agent/src/config.ts | 56 ++++++++++++++++------- packages/coding-agent/test/config.test.ts | 49 ++++++++++++++++++++ 3 files changed, 89 insertions(+), 17 deletions(-) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 822b83ec..e772b3b0 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -5,6 +5,7 @@ ### Fixed - Fixed Windows npm self-updates to move loaded native dependency packages out of the active install before reinstalling pi ([#4157](https://github.com/earendil-works/pi/issues/4157)). +- Fixed `pi update --self` detection for pnpm v11 global installs whose package path resolves through the pnpm store ([#4647](https://github.com/earendil-works/pi/issues/4647)). ## [0.75.1] - 2026-05-18 diff --git a/packages/coding-agent/src/config.ts b/packages/coding-agent/src/config.ts index 901bd296..ad306b4c 100644 --- a/packages/coding-agent/src/config.ts +++ b/packages/coding-agent/src/config.ts @@ -215,16 +215,18 @@ function getGlobalPackageRoots(method: InstallMethod, _packageName: string, npmC } } -function normalizeExistingPathForComparison(path: string): string | undefined { +function normalizeExistingPathForComparison(path: string, resolveSymlinks: boolean): string | undefined { const resolvedPath = resolve(path); if (!existsSync(resolvedPath)) { return undefined; } - let normalizedPath: string; - try { - normalizedPath = realpathSync(resolvedPath); - } catch { - return undefined; + let normalizedPath = resolvedPath; + if (resolveSymlinks) { + try { + normalizedPath = realpathSync(resolvedPath); + } catch { + return undefined; + } } if (process.platform === "win32") { normalizedPath = normalizedPath.toLowerCase(); @@ -232,6 +234,29 @@ function normalizeExistingPathForComparison(path: string): string | undefined { return normalizedPath; } +function getPathComparisonCandidates(path: string): string[] { + return Array.from( + new Set( + [normalizeExistingPathForComparison(path, false), normalizeExistingPathForComparison(path, true)].filter( + (candidate): candidate is string => !!candidate, + ), + ), + ); +} + +function getEntrypointPackageDir(): string | undefined { + const entrypoint = process.argv[1]; + if (!entrypoint) return undefined; + let dir = dirname(entrypoint); + while (dir !== dirname(dir)) { + if (existsSync(join(dir, "package.json"))) { + return dir; + } + dir = dirname(dir); + } + return undefined; +} + function isSelfUpdatePathWritable(): boolean { const packageDir = getPackageDir(); try { @@ -244,17 +269,14 @@ function isSelfUpdatePathWritable(): boolean { } function isManagedByGlobalPackageManager(method: InstallMethod, packageName: string, npmCommand?: string[]): boolean { - const packageDir = normalizeExistingPathForComparison(getPackageDir()); - return ( - !!packageDir && - getGlobalPackageRoots(method, packageName, npmCommand).some((root) => { - const normalizedRoot = normalizeExistingPathForComparison(root); - return ( - !!normalizedRoot && - packageDir.startsWith(normalizedRoot.endsWith(sep) ? normalizedRoot : `${normalizedRoot}${sep}`) - ); - }) - ); + const packageDirs = [getPackageDir(), getEntrypointPackageDir()].filter((dir): dir is string => !!dir); + const packageDirCandidates = packageDirs.flatMap((dir) => getPathComparisonCandidates(dir)); + return getGlobalPackageRoots(method, packageName, npmCommand).some((root) => { + return getPathComparisonCandidates(root).some((normalizedRoot) => { + const rootPrefix = normalizedRoot.endsWith(sep) ? normalizedRoot : `${normalizedRoot}${sep}`; + return packageDirCandidates.some((packageDir) => packageDir.startsWith(rootPrefix)); + }); + }); } export function getSelfUpdateCommand( diff --git a/packages/coding-agent/test/config.test.ts b/packages/coding-agent/test/config.test.ts index 6516beb5..71c4b48b 100644 --- a/packages/coding-agent/test/config.test.ts +++ b/packages/coding-agent/test/config.test.ts @@ -12,6 +12,7 @@ import { const execPathDescriptor = Object.getOwnPropertyDescriptor(process, "execPath"); const originalPath = process.env.PATH; const originalPiPackageDir = process.env.PI_PACKAGE_DIR; +const originalArgv1 = process.argv[1]; let tempDir: string | undefined; function setExecPath(value: string): void { @@ -35,6 +36,11 @@ afterEach(() => { } else { process.env.PI_PACKAGE_DIR = originalPiPackageDir; } + if (originalArgv1 === undefined) { + process.argv.splice(1, 1); + } else { + process.argv[1] = originalArgv1; + } if (tempDir) { chmodSync(tempDir, 0o700); rmSync(tempDir, { recursive: true, force: true }); @@ -275,6 +281,49 @@ describe("detectInstallMethod", () => { }); }); + test("self-updates pnpm v11 global installs resolved through the store", () => { + const temp = mkdtempSync(join(tmpdir(), "pi-pnpm11-")); + const binDir = join(temp, "bin"); + const root = join(temp, "Library", "pnpm", "global", "v11"); + const packageName = "@earendil-works/pi-coding-agent"; + const globalPackageDir = join(root, "11e9a", "node_modules", "@earendil-works", "pi-coding-agent"); + const storePackageDir = join( + temp, + "Library", + "pnpm", + "store", + "v11", + "links", + "@earendil-works", + "pi-coding-agent", + "0.75.0", + "hash", + "node_modules", + "@earendil-works", + "pi-coding-agent", + ); + mkdirSync(globalPackageDir, { recursive: true }); + mkdirSync(storePackageDir, { recursive: true }); + mkdirSync(binDir, { recursive: true }); + writeFileSync(join(globalPackageDir, "package.json"), "{}"); + writeFileSync(join(binDir, process.platform === "win32" ? "pnpm.cmd" : "pnpm"), createFakePnpmScript(root)); + chmodSync(join(binDir, process.platform === "win32" ? "pnpm.cmd" : "pnpm"), 0o755); + tempDir = temp; + process.env.PATH = `${binDir}${delimiter}${originalPath ?? ""}`; + process.env.PI_PACKAGE_DIR = storePackageDir; + process.argv[1] = join(globalPackageDir, "dist", "cli.js"); + setExecPath(join(storePackageDir, "dist", "cli.js")); + + const command = getSelfUpdateCommand(packageName); + + expect(detectInstallMethod()).toBe("pnpm"); + expect(command).toEqual({ + command: "pnpm", + args: ["install", "-g", packageName], + display: `pnpm install -g ${packageName}`, + }); + }); + test("self-updates renamed yarn global installs by removing the old package first", () => { createYarnGlobalInstall(); From c65177370b6b97669844797c7ddc80dd042d702b Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 18 May 2026 09:13:12 +0200 Subject: [PATCH 095/166] fix(coding-agent): allow Windows pnpm self-update closes #4157 --- packages/coding-agent/CHANGELOG.md | 1 + packages/coding-agent/src/package-manager-cli.ts | 10 +++++++--- packages/coding-agent/src/utils/child-process.ts | 14 ++++++++------ 3 files changed, 16 insertions(+), 9 deletions(-) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index e772b3b0..0440adcf 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -6,6 +6,7 @@ - Fixed Windows npm self-updates to move loaded native dependency packages out of the active install before reinstalling pi ([#4157](https://github.com/earendil-works/pi/issues/4157)). - Fixed `pi update --self` detection for pnpm v11 global installs whose package path resolves through the pnpm store ([#4647](https://github.com/earendil-works/pi/issues/4647)). +- Fixed Windows pnpm self-updates to resolve pnpm command shims and run through pnpm instead of requiring manual updates ([#4157](https://github.com/earendil-works/pi/issues/4157)). ## [0.75.1] - 2026-05-18 diff --git a/packages/coding-agent/src/package-manager-cli.ts b/packages/coding-agent/src/package-manager-cli.ts index a3ef1e21..d1a51e3b 100644 --- a/packages/coding-agent/src/package-manager-cli.ts +++ b/packages/coding-agent/src/package-manager-cli.ts @@ -506,8 +506,10 @@ export async function handlePackageCommand(args: string[]): Promise { return true; } const installMethod = detectInstallMethod(); - if (process.platform === "win32" && installMethod !== "npm") { - console.error(chalk.red(`${APP_NAME} self-update on Windows is only supported for npm installs.`)); + if (process.platform === "win32" && installMethod !== "npm" && installMethod !== "pnpm") { + console.error( + chalk.red(`${APP_NAME} self-update on Windows is only supported for npm and pnpm installs.`), + ); console.error(chalk.dim(`Detected install method: ${installMethod}. Update ${APP_NAME} manually.`)); process.exitCode = 1; return true; @@ -523,7 +525,9 @@ export async function handlePackageCommand(args: string[]): Promise { return true; } try { - prepareWindowsNpmSelfUpdate(); + if (installMethod === "npm") { + prepareWindowsNpmSelfUpdate(); + } await runSelfUpdate(selfUpdateCommand); } catch (error: unknown) { const message = error instanceof Error ? error.message : "Unknown package command error"; diff --git a/packages/coding-agent/src/utils/child-process.ts b/packages/coding-agent/src/utils/child-process.ts index 73fdf5af..57ebd3ae 100644 --- a/packages/coding-agent/src/utils/child-process.ts +++ b/packages/coding-agent/src/utils/child-process.ts @@ -3,9 +3,9 @@ import { existsSync, readFileSync } from "node:fs"; import { dirname, extname, join, resolve, sep } from "node:path"; const EXIT_STDIO_GRACE_MS = 100; -const WINDOWS_COMMAND_EXTENSIONS = ["", ".exe", ".cmd", ".bat"]; +const WINDOWS_COMMAND_EXTENSIONS = [".exe", ".cmd", ".bat", ""]; const WINDOWS_COMMAND_SHIM_RE = /\.(?:cmd|bat)$/i; -const NODE_SHIM_SCRIPT_RE = /(?:%~dp0|%dp0%|%basedir%)[^"'\r\n<>|&]*?\.(?:cjs|mjs|js)/i; +const NODE_SHIM_SCRIPT_RE = /(?:%~dp0|%dp0%|%basedir%)[^"'\r\n<>|&]*?\.(?:cjs|mjs|js)/gi; export interface ResolvedSpawnCommand { command: string; @@ -45,10 +45,12 @@ function expandShimPath(path: string, shimPath: string): string { } function findNodeShimScript(shimPath: string): string | undefined { - const match = readFileSync(shimPath, "utf-8").match(NODE_SHIM_SCRIPT_RE); - if (!match) return undefined; - const scriptPath = expandShimPath(match[0], shimPath); - return existsSync(scriptPath) ? scriptPath : undefined; + const matches = [...readFileSync(shimPath, "utf-8").matchAll(NODE_SHIM_SCRIPT_RE)]; + for (const match of matches.reverse()) { + const scriptPath = expandShimPath(match[0], shimPath); + if (existsSync(scriptPath)) return scriptPath; + } + return undefined; } export function resolveSpawnCommand( From a294cdafd9731de96b08b30ba42f3cefea9bb650 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 18 May 2026 09:49:24 +0200 Subject: [PATCH 096/166] fix(coding-agent): unblock Windows external editor closes #4612 --- packages/coding-agent/CHANGELOG.md | 1 + .../components/extension-editor.ts | 21 +++++++++++++------ .../src/modes/interactive/interactive-mode.ts | 20 ++++++++++++------ 3 files changed, 30 insertions(+), 12 deletions(-) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 0440adcf..57932dc7 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -4,6 +4,7 @@ ### Fixed +- Fixed Windows external editor handoff so vim/nvim can receive input after opening from the TUI ([#4612](https://github.com/earendil-works/pi/issues/4612)). - Fixed Windows npm self-updates to move loaded native dependency packages out of the active install before reinstalling pi ([#4157](https://github.com/earendil-works/pi/issues/4157)). - Fixed `pi update --self` detection for pnpm v11 global installs whose package path resolves through the pnpm store ([#4647](https://github.com/earendil-works/pi/issues/4647)). - Fixed Windows pnpm self-updates to resolve pnpm command shims and run through pnpm instead of requiring manual updates ([#4157](https://github.com/earendil-works/pi/issues/4157)). diff --git a/packages/coding-agent/src/modes/interactive/components/extension-editor.ts b/packages/coding-agent/src/modes/interactive/components/extension-editor.ts index e1134ce9..b33ad9f5 100644 --- a/packages/coding-agent/src/modes/interactive/components/extension-editor.ts +++ b/packages/coding-agent/src/modes/interactive/components/extension-editor.ts @@ -3,7 +3,7 @@ * Supports Ctrl+G for external editor. */ -import { spawnSync } from "node:child_process"; +import { spawn } from "node:child_process"; import * as fs from "node:fs"; import * as os from "node:os"; import * as path from "node:path"; @@ -110,7 +110,7 @@ export class ExtensionEditorComponent extends Container implements Focusable { this.editor.handleInput(keyData); } - private openExternalEditor(): void { + private async openExternalEditor(): Promise { const editorCmd = process.env.VISUAL || process.env.EDITOR; if (!editorCmd) { return; @@ -124,12 +124,21 @@ export class ExtensionEditorComponent extends Container implements Focusable { this.tui.stop(); const [editor, ...editorArgs] = editorCmd.split(" "); - const result = spawnSync(editor, [...editorArgs, tmpFile], { - stdio: "inherit", - shell: process.platform === "win32", + process.stdout.write(`Launching external editor: ${editorCmd}\nPi will resume when the editor exits.\n`); + + // Do not use spawnSync here. On Windows, synchronous child_process calls can keep + // Node/libuv's console input read active after tui.stop() pauses stdin, racing + // vim/nvim for the console input buffer until Ctrl+C cancels the pending read. + const status = await new Promise((resolve) => { + const child = spawn(editor, [...editorArgs, tmpFile], { + stdio: "inherit", + shell: process.platform === "win32", + }); + child.on("error", () => resolve(null)); + child.on("close", (code) => resolve(code)); }); - if (result.status === 0) { + if (status === 0) { const newContent = fs.readFileSync(tmpFile, "utf-8").replace(/\n$/, ""); this.editor.setText(newContent); } diff --git a/packages/coding-agent/src/modes/interactive/interactive-mode.ts b/packages/coding-agent/src/modes/interactive/interactive-mode.ts index a493bf6f..b3df9eb4 100644 --- a/packages/coding-agent/src/modes/interactive/interactive-mode.ts +++ b/packages/coding-agent/src/modes/interactive/interactive-mode.ts @@ -3497,7 +3497,7 @@ export class InteractiveMode { this.showStatus(`Thinking blocks: ${this.hideThinkingBlock ? "hidden" : "visible"}`); } - private openExternalEditor(): void { + private async openExternalEditor(): Promise { // Determine editor (respect $VISUAL, then $EDITOR) const editorCmd = process.env.VISUAL || process.env.EDITOR; if (!editorCmd) { @@ -3518,14 +3518,22 @@ export class InteractiveMode { // Split by space to support editor arguments (e.g., "code --wait") const [editor, ...editorArgs] = editorCmd.split(" "); - // Spawn editor synchronously with inherited stdio for interactive editing - const result = spawnSync(editor, [...editorArgs, tmpFile], { - stdio: "inherit", - shell: process.platform === "win32", + process.stdout.write(`Launching external editor: ${editorCmd}\nPi will resume when the editor exits.\n`); + + // Do not use spawnSync here. On Windows, synchronous child_process calls can keep + // Node/libuv's console input read active after ui.stop() pauses stdin, racing + // vim/nvim for the console input buffer until Ctrl+C cancels the pending read. + const status = await new Promise((resolve) => { + const child = spawn(editor, [...editorArgs, tmpFile], { + stdio: "inherit", + shell: process.platform === "win32", + }); + child.on("error", () => resolve(null)); + child.on("close", (code) => resolve(code)); }); // On successful exit (status 0), replace editor content - if (result.status === 0) { + if (status === 0) { const newContent = fs.readFileSync(tmpFile, "utf-8").replace(/\n$/, ""); this.editor.setText(newContent); } From 0f066367bf0ccae1f0762856be351829e03760b3 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 18 May 2026 08:52:47 +0000 Subject: [PATCH 097/166] chore: approve contributor josephyoung --- .github/APPROVED_CONTRIBUTORS | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/APPROVED_CONTRIBUTORS b/.github/APPROVED_CONTRIBUTORS index 253403d9..8ddcd120 100644 --- a/.github/APPROVED_CONTRIBUTORS +++ b/.github/APPROVED_CONTRIBUTORS @@ -211,3 +211,5 @@ brianmichel pr abhinavmathur-atlan pr mattiacerutti pr + +josephyoung pr From dbadffdfaefee3ab6f37de3841a0342ca7901d80 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 18 May 2026 11:04:03 +0200 Subject: [PATCH 098/166] chore(prompts): disclose wr-generated comments --- .pi/prompts/wr.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.pi/prompts/wr.md b/.pi/prompts/wr.md index 65108e9c..a2e42b2c 100644 --- a/.pi/prompts/wr.md +++ b/.pi/prompts/wr.md @@ -16,7 +16,11 @@ Rules for context detection: Unless I explicitly override something in this request, do the following in order: 1. Add or update the relevant package changelog entry under `## [Unreleased]` using the repo changelog rules. -2. If this task is tied to a GitHub issue or PR and a final issue or PR comment has not already been posted in this session, draft it in my tone, preview it, and post exactly one final comment. +2. If this task is tied to a GitHub issue or PR and a final issue or PR comment has not already been posted in this session, draft it in my tone, preview it, and post exactly one final comment. The comment must end with this exact standalone disclaimer line, with no variations: + + ```text + This comment is AI-generated by `/wr` + ``` 3. Commit only files you changed in this session. 4. If this task is tied to exactly one GitHub issue, include `closes #` in the commit message. If it is tied to multiple issues, stop and ask which one to use. If it is not tied to any issue, do not include `closes #` or `fixes #` in the commit message. 5. Check the current git branch. If it is not `main`, stop and ask what to do. Do not push from another branch unless I explicitly say so. From b8f51957a045a4716a98f3e760e92ca2c42ae523 Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Mon, 18 May 2026 11:14:54 +0200 Subject: [PATCH 099/166] fix(ai): add Xiaomi reasoning replay compat closes #4678 --- packages/ai/CHANGELOG.md | 4 ++ packages/ai/scripts/generate-models.ts | 5 ++ packages/ai/src/models.generated.ts | 26 ++++++- .../openai-completions-tool-choice.test.ts | 72 ++++++++++++++++++- 4 files changed, 103 insertions(+), 4 deletions(-) diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index 6271bcac..1bb47cb7 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Fixed + +- Fixed Xiaomi MiMo generated model metadata to replay assistant tool-call messages with `reasoning_content` for thinking-mode multi-turn requests ([#4678](https://github.com/earendil-works/pi/issues/4678)). + ## [0.75.1] - 2026-05-18 ### Fixed diff --git a/packages/ai/scripts/generate-models.ts b/packages/ai/scripts/generate-models.ts index a836e873..f752f23b 100644 --- a/packages/ai/scripts/generate-models.ts +++ b/packages/ai/scripts/generate-models.ts @@ -1073,6 +1073,10 @@ async function loadModelsDevData(): Promise[]> { // Built-in `xiaomi` targets the API billing endpoint (single stable URL, // keys from platform.xiaomimimo.com). The three `xiaomi-token-plan-*` // providers cover prepaid Token Plan endpoints in cn / ams / sgp. + const xiaomiCompat: OpenAICompletionsCompat = { + requiresReasoningContentOnAssistantMessages: true, + thinkingFormat: "deepseek", + }; const xiaomiVariants = [ { provider: "xiaomi", baseUrl: "https://api.xiaomimimo.com/v1" }, { provider: "xiaomi-token-plan-cn", baseUrl: "https://token-plan-cn.xiaomimimo.com/v1" }, @@ -1092,6 +1096,7 @@ async function loadModelsDevData(): Promise[]> { api: "openai-completions", provider, baseUrl, + compat: xiaomiCompat, reasoning: m.reasoning === true, input: m.modalities?.input?.includes("image") ? ["text", "image"] : ["text"], cost: { diff --git a/packages/ai/src/models.generated.ts b/packages/ai/src/models.generated.ts index 5b980251..283ecfd2 100644 --- a/packages/ai/src/models.generated.ts +++ b/packages/ai/src/models.generated.ts @@ -7268,9 +7268,9 @@ export const MODELS = { "big-pickle": { id: "big-pickle", name: "Big Pickle", - api: "openai-completions", + api: "anthropic-messages", provider: "opencode", - baseUrl: "https://opencode.ai/zen/v1", + baseUrl: "https://opencode.ai/zen", reasoning: true, input: ["text"], cost: { @@ -7281,7 +7281,7 @@ export const MODELS = { }, contextWindow: 200000, maxTokens: 128000, - } satisfies Model<"openai-completions">, + } satisfies Model<"anthropic-messages">, "claude-haiku-4-5": { id: "claude-haiku-4-5", name: "Claude Haiku 4.5", @@ -15879,6 +15879,7 @@ export const MODELS = { api: "openai-completions", provider: "xiaomi", baseUrl: "https://api.xiaomimimo.com/v1", + compat: {"requiresReasoningContentOnAssistantMessages":true,"thinkingFormat":"deepseek"}, reasoning: true, input: ["text"], cost: { @@ -15896,6 +15897,7 @@ export const MODELS = { api: "openai-completions", provider: "xiaomi", baseUrl: "https://api.xiaomimimo.com/v1", + compat: {"requiresReasoningContentOnAssistantMessages":true,"thinkingFormat":"deepseek"}, reasoning: true, input: ["text", "image"], cost: { @@ -15913,6 +15915,7 @@ export const MODELS = { api: "openai-completions", provider: "xiaomi", baseUrl: "https://api.xiaomimimo.com/v1", + compat: {"requiresReasoningContentOnAssistantMessages":true,"thinkingFormat":"deepseek"}, reasoning: true, input: ["text"], cost: { @@ -15930,6 +15933,7 @@ export const MODELS = { api: "openai-completions", provider: "xiaomi", baseUrl: "https://api.xiaomimimo.com/v1", + compat: {"requiresReasoningContentOnAssistantMessages":true,"thinkingFormat":"deepseek"}, reasoning: true, input: ["text", "image"], cost: { @@ -15947,6 +15951,7 @@ export const MODELS = { api: "openai-completions", provider: "xiaomi", baseUrl: "https://api.xiaomimimo.com/v1", + compat: {"requiresReasoningContentOnAssistantMessages":true,"thinkingFormat":"deepseek"}, reasoning: true, input: ["text"], cost: { @@ -15966,6 +15971,7 @@ export const MODELS = { api: "openai-completions", provider: "xiaomi-token-plan-ams", baseUrl: "https://token-plan-ams.xiaomimimo.com/v1", + compat: {"requiresReasoningContentOnAssistantMessages":true,"thinkingFormat":"deepseek"}, reasoning: true, input: ["text"], cost: { @@ -15983,6 +15989,7 @@ export const MODELS = { api: "openai-completions", provider: "xiaomi-token-plan-ams", baseUrl: "https://token-plan-ams.xiaomimimo.com/v1", + compat: {"requiresReasoningContentOnAssistantMessages":true,"thinkingFormat":"deepseek"}, reasoning: true, input: ["text", "image"], cost: { @@ -16000,6 +16007,7 @@ export const MODELS = { api: "openai-completions", provider: "xiaomi-token-plan-ams", baseUrl: "https://token-plan-ams.xiaomimimo.com/v1", + compat: {"requiresReasoningContentOnAssistantMessages":true,"thinkingFormat":"deepseek"}, reasoning: true, input: ["text"], cost: { @@ -16017,6 +16025,7 @@ export const MODELS = { api: "openai-completions", provider: "xiaomi-token-plan-ams", baseUrl: "https://token-plan-ams.xiaomimimo.com/v1", + compat: {"requiresReasoningContentOnAssistantMessages":true,"thinkingFormat":"deepseek"}, reasoning: true, input: ["text", "image"], cost: { @@ -16034,6 +16043,7 @@ export const MODELS = { api: "openai-completions", provider: "xiaomi-token-plan-ams", baseUrl: "https://token-plan-ams.xiaomimimo.com/v1", + compat: {"requiresReasoningContentOnAssistantMessages":true,"thinkingFormat":"deepseek"}, reasoning: true, input: ["text"], cost: { @@ -16053,6 +16063,7 @@ export const MODELS = { api: "openai-completions", provider: "xiaomi-token-plan-cn", baseUrl: "https://token-plan-cn.xiaomimimo.com/v1", + compat: {"requiresReasoningContentOnAssistantMessages":true,"thinkingFormat":"deepseek"}, reasoning: true, input: ["text"], cost: { @@ -16070,6 +16081,7 @@ export const MODELS = { api: "openai-completions", provider: "xiaomi-token-plan-cn", baseUrl: "https://token-plan-cn.xiaomimimo.com/v1", + compat: {"requiresReasoningContentOnAssistantMessages":true,"thinkingFormat":"deepseek"}, reasoning: true, input: ["text", "image"], cost: { @@ -16087,6 +16099,7 @@ export const MODELS = { api: "openai-completions", provider: "xiaomi-token-plan-cn", baseUrl: "https://token-plan-cn.xiaomimimo.com/v1", + compat: {"requiresReasoningContentOnAssistantMessages":true,"thinkingFormat":"deepseek"}, reasoning: true, input: ["text"], cost: { @@ -16104,6 +16117,7 @@ export const MODELS = { api: "openai-completions", provider: "xiaomi-token-plan-cn", baseUrl: "https://token-plan-cn.xiaomimimo.com/v1", + compat: {"requiresReasoningContentOnAssistantMessages":true,"thinkingFormat":"deepseek"}, reasoning: true, input: ["text", "image"], cost: { @@ -16121,6 +16135,7 @@ export const MODELS = { api: "openai-completions", provider: "xiaomi-token-plan-cn", baseUrl: "https://token-plan-cn.xiaomimimo.com/v1", + compat: {"requiresReasoningContentOnAssistantMessages":true,"thinkingFormat":"deepseek"}, reasoning: true, input: ["text"], cost: { @@ -16140,6 +16155,7 @@ export const MODELS = { api: "openai-completions", provider: "xiaomi-token-plan-sgp", baseUrl: "https://token-plan-sgp.xiaomimimo.com/v1", + compat: {"requiresReasoningContentOnAssistantMessages":true,"thinkingFormat":"deepseek"}, reasoning: true, input: ["text"], cost: { @@ -16157,6 +16173,7 @@ export const MODELS = { api: "openai-completions", provider: "xiaomi-token-plan-sgp", baseUrl: "https://token-plan-sgp.xiaomimimo.com/v1", + compat: {"requiresReasoningContentOnAssistantMessages":true,"thinkingFormat":"deepseek"}, reasoning: true, input: ["text", "image"], cost: { @@ -16174,6 +16191,7 @@ export const MODELS = { api: "openai-completions", provider: "xiaomi-token-plan-sgp", baseUrl: "https://token-plan-sgp.xiaomimimo.com/v1", + compat: {"requiresReasoningContentOnAssistantMessages":true,"thinkingFormat":"deepseek"}, reasoning: true, input: ["text"], cost: { @@ -16191,6 +16209,7 @@ export const MODELS = { api: "openai-completions", provider: "xiaomi-token-plan-sgp", baseUrl: "https://token-plan-sgp.xiaomimimo.com/v1", + compat: {"requiresReasoningContentOnAssistantMessages":true,"thinkingFormat":"deepseek"}, reasoning: true, input: ["text", "image"], cost: { @@ -16208,6 +16227,7 @@ export const MODELS = { api: "openai-completions", provider: "xiaomi-token-plan-sgp", baseUrl: "https://token-plan-sgp.xiaomimimo.com/v1", + compat: {"requiresReasoningContentOnAssistantMessages":true,"thinkingFormat":"deepseek"}, reasoning: true, input: ["text"], cost: { diff --git a/packages/ai/test/openai-completions-tool-choice.test.ts b/packages/ai/test/openai-completions-tool-choice.test.ts index 21b5c0d5..5401ac83 100644 --- a/packages/ai/test/openai-completions-tool-choice.test.ts +++ b/packages/ai/test/openai-completions-tool-choice.test.ts @@ -3,7 +3,7 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; import { getModel } from "../src/models.js"; import { convertMessages } from "../src/providers/openai-completions.js"; import { streamSimple } from "../src/stream.js"; -import type { Model, Tool } from "../src/types.js"; +import type { AssistantMessage, Model, Tool, ToolResultMessage } from "../src/types.js"; const mockState = vi.hoisted(() => ({ lastParams: undefined as unknown, @@ -817,6 +817,76 @@ describe("openai-completions tool_choice", () => { expect(writeCall).not.toHaveProperty("partialArgs"); }); + it("stores Xiaomi MiMo reasoning replay compat in built-in metadata", () => { + const providers = ["xiaomi", "xiaomi-token-plan-cn", "xiaomi-token-plan-ams", "xiaomi-token-plan-sgp"] as const; + + for (const provider of providers) { + const model = getModel(provider, "mimo-v2.5-pro")!; + expect(model.compat?.requiresReasoningContentOnAssistantMessages).toBe(true); + expect(model.compat?.thinkingFormat).toBe("deepseek"); + expect(model.compat?.maxTokensField).toBeUndefined(); + expect(model.compat?.supportsDeveloperRole).toBeUndefined(); + } + }); + + it("replays Xiaomi MiMo assistant tool calls with empty reasoning_content when thinking is missing", async () => { + const model = getModel("xiaomi", "mimo-v2.5-pro")!; + const assistantMessage: AssistantMessage = { + role: "assistant", + api: "openai-completions", + provider: "xiaomi", + model: "mimo-v2.5-pro", + content: [{ type: "toolCall", id: "call_1", name: "read", arguments: { path: "README.md" } }], + usage: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + totalTokens: 0, + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 }, + }, + stopReason: "toolUse", + timestamp: Date.now(), + }; + const toolResult: ToolResultMessage = { + role: "toolResult", + toolCallId: "call_1", + toolName: "read", + content: [{ type: "text", text: "contents" }], + isError: false, + timestamp: Date.now(), + }; + let payload: unknown; + + await streamSimple( + model, + { + messages: [ + { role: "user", content: "Read README.md", timestamp: Date.now() }, + assistantMessage, + toolResult, + ], + }, + { + apiKey: "test", + reasoning: "high", + onPayload: (params: unknown) => { + payload = params; + }, + }, + ).result(); + + const params = (payload ?? mockState.lastParams) as { + messages?: Array>; + thinking?: { type?: string }; + reasoning_effort?: string; + }; + const replayedAssistant = params.messages?.find((message) => message.role === "assistant"); + expect(replayedAssistant).toMatchObject({ role: "assistant", reasoning_content: "" }); + expect(params.thinking).toEqual({ type: "enabled" }); + expect(params.reasoning_effort).toBe("high"); + }); + it("normalizes OpenCode Go reasoning deltas to reasoning_content for replay", async () => { mockState.chunks = [ { From 64150668b882f50bf2dc73557e154847f1f9d11f Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Mon, 18 May 2026 11:30:41 +0200 Subject: [PATCH 100/166] fix(coding-agent): use cross-spawn for Windows shims closes #4665 --- package-lock.json | 72 +++++++++++- packages/coding-agent/CHANGELOG.md | 1 + packages/coding-agent/package.json | 2 + packages/coding-agent/src/config.ts | 14 +-- .../coding-agent/src/core/package-manager.ts | 13 +-- .../coding-agent/src/package-manager-cli.ts | 6 +- .../coding-agent/src/utils/child-process.ts | 104 +++++------------- .../coding-agent/test/package-manager.test.ts | 65 ++--------- 8 files changed, 124 insertions(+), 153 deletions(-) diff --git a/package-lock.json b/package-lock.json index 47505733..b0c47a95 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3162,6 +3162,16 @@ "assertion-error": "^2.0.1" } }, + "node_modules/@types/cross-spawn": { + "version": "6.0.6", + "resolved": "https://registry.npmjs.org/@types/cross-spawn/-/cross-spawn-6.0.6.tgz", + "integrity": "sha512-fXRhhUkG4H3TQk5dBhQ7m/JDdSNHKwR2BBia62lhwEIq9xGiQKLxd6LymNhn47SjXhsUEPmxi+PKw2OkW4LLjA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/deep-eql": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", @@ -4796,7 +4806,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true, "license": "ISC" }, "node_modules/istanbul-lib-coverage": { @@ -7860,6 +7869,7 @@ "@earendil-works/pi-tui": "^0.75.1", "@silvia-odwyer/photon-node": "^0.3.4", "chalk": "^5.5.0", + "cross-spawn": "^7.0.6", "diff": "^8.0.2", "glob": "^13.0.1", "highlight.js": "^10.7.3", @@ -7876,6 +7886,7 @@ "pi": "dist/cli.js" }, "devDependencies": { + "@types/cross-spawn": "^6.0.6", "@types/diff": "^7.0.2", "@types/hosted-git-info": "^3.0.5", "@types/ms": "^2.1.0", @@ -7939,6 +7950,50 @@ "undici-types": "~7.16.0" } }, + "packages/coding-agent/node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "packages/coding-agent/node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "packages/coding-agent/node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "packages/coding-agent/node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "packages/coding-agent/node_modules/undici-types": { "version": "7.16.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", @@ -7946,6 +8001,21 @@ "dev": true, "license": "MIT" }, + "packages/coding-agent/node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, "packages/tui": { "name": "@earendil-works/pi-tui", "version": "0.75.1", diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index a9e4a552..543af174 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -9,6 +9,7 @@ - Fixed Windows npm self-updates to move loaded native dependency packages out of the active install before reinstalling pi ([#4157](https://github.com/earendil-works/pi/issues/4157)). - Fixed `pi update --self` detection for pnpm v11 global installs whose package path resolves through the pnpm store ([#4647](https://github.com/earendil-works/pi/issues/4647)). - Fixed Windows pnpm self-updates to resolve pnpm command shims and run through pnpm instead of requiring manual updates ([#4157](https://github.com/earendil-works/pi/issues/4157)). +- Fixed Windows npm-family command execution to use cross-spawn instead of parsing `.cmd` shim internals ([#4665](https://github.com/earendil-works/pi/issues/4665)). ## [0.75.1] - 2026-05-18 diff --git a/packages/coding-agent/package.json b/packages/coding-agent/package.json index 31d1c95d..5171b159 100644 --- a/packages/coding-agent/package.json +++ b/packages/coding-agent/package.json @@ -43,6 +43,7 @@ "@earendil-works/pi-tui": "^0.75.1", "@silvia-odwyer/photon-node": "^0.3.4", "chalk": "^5.5.0", + "cross-spawn": "^7.0.6", "diff": "^8.0.2", "glob": "^13.0.1", "highlight.js": "^10.7.3", @@ -65,6 +66,7 @@ "@mariozechner/clipboard": "^0.3.6" }, "devDependencies": { + "@types/cross-spawn": "^6.0.6", "@types/diff": "^7.0.2", "@types/hosted-git-info": "^3.0.5", "@types/ms": "^2.1.0", diff --git a/packages/coding-agent/src/config.ts b/packages/coding-agent/src/config.ts index ad306b4c..e2ac87e1 100644 --- a/packages/coding-agent/src/config.ts +++ b/packages/coding-agent/src/config.ts @@ -1,9 +1,8 @@ -import { spawnSync } from "child_process"; import { accessSync, constants, existsSync, readFileSync, realpathSync } from "fs"; import { homedir } from "os"; import { basename, dirname, join, resolve, sep, win32 } from "path"; import { fileURLToPath } from "url"; -import { resolveSpawnCommand } from "./utils/child-process.js"; +import { spawnProcessSync } from "./utils/child-process.js"; // ============================================================================= // Package Detection @@ -151,16 +150,7 @@ function readCommandOutput( args: string[], options: { requireSuccess?: boolean } = {}, ): string | undefined { - let resolved: ReturnType; - try { - resolved = resolveSpawnCommand(command, args); - } catch (error) { - if (options.requireSuccess) { - throw error; - } - return undefined; - } - const result = spawnSync(resolved.command, resolved.args, { + const result = spawnProcessSync(command, args, { encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"], }); diff --git a/packages/coding-agent/src/core/package-manager.ts b/packages/coding-agent/src/core/package-manager.ts index 53ff6fea..04135cbf 100644 --- a/packages/coding-agent/src/core/package-manager.ts +++ b/packages/coding-agent/src/core/package-manager.ts @@ -1,4 +1,4 @@ -import { type ChildProcess, type ChildProcessByStdio, spawn, spawnSync } from "node:child_process"; +import type { ChildProcess, ChildProcessByStdio } from "node:child_process"; import { createHash } from "node:crypto"; import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs"; import { homedir, tmpdir } from "node:os"; @@ -28,7 +28,7 @@ import { globSync } from "glob"; import ignore from "ignore"; import { minimatch } from "minimatch"; import { CONFIG_DIR_NAME } from "../config.js"; -import { resolveSpawnCommand } from "../utils/child-process.js"; +import { spawnProcess, spawnProcessSync } from "../utils/child-process.js"; import { type GitSource, parseGitUrl } from "../utils/git.js"; import { canonicalizePath, isLocalPath } from "../utils/paths.js"; import { isStdoutTakenOver } from "./output-guard.js"; @@ -2409,8 +2409,7 @@ export class DefaultPackageManager implements PackageManager { private spawnCommand(command: string, args: string[], options?: { cwd?: string }): ChildProcess { const env = getEnv(); - const resolved = resolveSpawnCommand(command, args, { env }); - return spawn(resolved.command, resolved.args, { + return spawnProcess(command, args, { cwd: options?.cwd, stdio: isStdoutTakenOver() ? ["ignore", 2, 2] : "inherit", env, @@ -2424,8 +2423,7 @@ export class DefaultPackageManager implements PackageManager { ): ChildProcessByStdio { const baseEnv = getEnv(); const env = options?.env ? { ...baseEnv, ...options.env } : baseEnv; - const resolved = resolveSpawnCommand(command, args, { env }); - return spawn(resolved.command, resolved.args, { + return spawnProcess(command, args, { cwd: options?.cwd, stdio: ["ignore", "pipe", "pipe"], env, @@ -2492,8 +2490,7 @@ export class DefaultPackageManager implements PackageManager { private runCommandSync(command: string, args: string[]): string { const env = getEnv(); - const resolved = resolveSpawnCommand(command, args, { env }); - const result = spawnSync(resolved.command, resolved.args, { + const result = spawnProcessSync(command, args, { stdio: ["ignore", "pipe", "pipe"], encoding: "utf-8", env, diff --git a/packages/coding-agent/src/package-manager-cli.ts b/packages/coding-agent/src/package-manager-cli.ts index d1a51e3b..a55a3750 100644 --- a/packages/coding-agent/src/package-manager-cli.ts +++ b/packages/coding-agent/src/package-manager-cli.ts @@ -1,5 +1,4 @@ import chalk from "chalk"; -import { spawn } from "child_process"; import { selectConfig } from "./cli/config-selector.js"; import { APP_NAME, @@ -14,7 +13,7 @@ import { } from "./config.js"; import { DefaultPackageManager } from "./core/package-manager.js"; import { SettingsManager } from "./core/settings-manager.js"; -import { resolveSpawnCommand } from "./utils/child-process.js"; +import { spawnProcess } from "./utils/child-process.js"; import { getLatestPiRelease, isNewerPackageVersion } from "./utils/version-check.js"; import { cleanupWindowsSelfUpdateQuarantine, @@ -322,8 +321,7 @@ async function runSelfUpdate(command: SelfUpdateCommand): Promise { console.log(chalk.dim(`Updating ${APP_NAME} with ${command.display}...`)); for (const step of command.steps ?? [command]) { await new Promise((resolve, reject) => { - const resolved = resolveSpawnCommand(step.command, step.args); - const child = spawn(resolved.command, resolved.args, { + const child = spawnProcess(step.command, step.args, { stdio: "inherit", }); child.on("error", (error) => { diff --git a/packages/coding-agent/src/utils/child-process.ts b/packages/coding-agent/src/utils/child-process.ts index 57ebd3ae..3d97d3da 100644 --- a/packages/coding-agent/src/utils/child-process.ts +++ b/packages/coding-agent/src/utils/child-process.ts @@ -1,84 +1,38 @@ -import type { ChildProcess } from "node:child_process"; -import { existsSync, readFileSync } from "node:fs"; -import { dirname, extname, join, resolve, sep } from "node:path"; +import { + type ChildProcess, + type ChildProcessByStdio, + spawn as nodeSpawn, + spawnSync as nodeSpawnSync, + type SpawnOptions, + type SpawnOptionsWithStdioTuple, + type SpawnSyncOptionsWithStringEncoding, + type SpawnSyncReturns, + type StdioNull, + type StdioPipe, +} from "node:child_process"; +import type { Readable } from "node:stream"; +import crossSpawn from "cross-spawn"; const EXIT_STDIO_GRACE_MS = 100; -const WINDOWS_COMMAND_EXTENSIONS = [".exe", ".cmd", ".bat", ""]; -const WINDOWS_COMMAND_SHIM_RE = /\.(?:cmd|bat)$/i; -const NODE_SHIM_SCRIPT_RE = /(?:%~dp0|%dp0%|%basedir%)[^"'\r\n<>|&]*?\.(?:cjs|mjs|js)/gi; -export interface ResolvedSpawnCommand { - command: string; - args: string[]; -} - -function findWindowsCommand(command: string, env: NodeJS.ProcessEnv): string | undefined { - const candidates = extname(command) - ? [command] - : WINDOWS_COMMAND_EXTENSIONS.map((extension) => `${command}${extension}`); - const hasPath = command.includes("/") || command.includes("\\") || /^[a-zA-Z]:/.test(command); - if (hasPath) { - const match = candidates.find((candidate) => existsSync(candidate)); - return match ? resolve(match) : undefined; - } - - const pathValue = env.PATH ?? env.Path ?? env.path; - if (!pathValue) return undefined; - for (const dir of pathValue.split(";")) { - for (const candidate of candidates) { - const path = join(dir, candidate); - if (existsSync(path)) return resolve(path); - } - } - return undefined; -} - -function expandShimPath(path: string, shimPath: string): string { - const shimDir = dirname(shimPath); - return resolve( - path - .replace(/%~dp0[\\/]?/gi, `${shimDir}${sep}`) - .replace(/%dp0%[\\/]?/gi, `${shimDir}${sep}`) - .replace(/%basedir%[\\/]?/gi, `${shimDir}${sep}`) - .replace(/\\/g, sep), - ); -} - -function findNodeShimScript(shimPath: string): string | undefined { - const matches = [...readFileSync(shimPath, "utf-8").matchAll(NODE_SHIM_SCRIPT_RE)]; - for (const match of matches.reverse()) { - const scriptPath = expandShimPath(match[0], shimPath); - if (existsSync(scriptPath)) return scriptPath; - } - return undefined; -} - -export function resolveSpawnCommand( +export function spawnProcess( command: string, args: string[], - options: { env?: NodeJS.ProcessEnv } = {}, -): ResolvedSpawnCommand { - if (process.platform !== "win32") { - return { command, args }; - } + options: SpawnOptionsWithStdioTuple, +): ChildProcessByStdio; +export function spawnProcess(command: string, args: string[], options: SpawnOptions): ChildProcess; +export function spawnProcess(command: string, args: string[], options: SpawnOptions): ChildProcess { + return process.platform === "win32" ? crossSpawn(command, args, options) : nodeSpawn(command, args, options); +} - const env = options.env ?? process.env; - const resolvedCommand = findWindowsCommand(command, env); - if (!resolvedCommand) { - return { command, args }; - } - - if (!WINDOWS_COMMAND_SHIM_RE.test(resolvedCommand)) { - return { command: resolvedCommand, args }; - } - - const script = findNodeShimScript(resolvedCommand); - if (!script) { - throw new Error(`Refusing to run Windows command shim without a shell: ${resolvedCommand}`); - } - const localNode = join(dirname(resolvedCommand), "node.exe"); - const nodeCommand = existsSync(localNode) ? localNode : (findWindowsCommand("node.exe", env) ?? "node"); - return { command: nodeCommand, args: [script, ...args] }; +export function spawnProcessSync( + command: string, + args: string[], + options: SpawnSyncOptionsWithStringEncoding, +): SpawnSyncReturns { + return process.platform === "win32" + ? crossSpawn.sync(command, args, options) + : nodeSpawnSync(command, args, options); } /** diff --git a/packages/coding-agent/test/package-manager.test.ts b/packages/coding-agent/test/package-manager.test.ts index 3cdf8cb0..cc6afbc4 100644 --- a/packages/coding-agent/test/package-manager.test.ts +++ b/packages/coding-agent/test/package-manager.test.ts @@ -6,7 +6,6 @@ import { PassThrough } from "node:stream"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { DefaultPackageManager, type ProgressEvent, type ResolvedResource } from "../src/core/package-manager.js"; import { SettingsManager } from "../src/core/settings-manager.js"; -import { resolveSpawnCommand } from "../src/utils/child-process.js"; function normalizeForMatch(value: string): string { return value.replace(/\\/g, "/"); @@ -617,59 +616,19 @@ Content`, }); }); - describe("windows command spawning", () => { - it("should keep unresolved executables as argv commands", () => { - vi.spyOn(process, "platform", "get").mockReturnValue("win32"); + describe("command spawning", () => { + it("should preserve argv entries containing spaces", () => { + const managerWithInternals = packageManager as unknown as { + runCommandSync(command: string, args: string[]): string; + }; + const valueWithSpace = "C:\\Users\\A B\\.pi\\npm"; + const output = managerWithInternals.runCommandSync(process.execPath, [ + "-e", + "console.log(process.argv[1])", + valueWithSpace, + ]); - const resolved = resolveSpawnCommand("git", ["clone", "repo", "C:\\Users\\A B\\repo"], { - env: { PATH: tempDir }, - }); - - expect(resolved).toEqual({ command: "git", args: ["clone", "repo", "C:\\Users\\A B\\repo"] }); - }); - - it("should prefer Windows executables over command scripts", () => { - vi.spyOn(process, "platform", "get").mockReturnValue("win32"); - const binDir = join(tempDir, "bin"); - mkdirSync(binDir, { recursive: true }); - writeFileSync(join(binDir, "npm.exe"), ""); - writeFileSync(join(binDir, "npm.cmd"), "@echo off\r\n"); - - const args = ["install", "pkg", "--prefix", "C:\\Users\\A B\\.pi\\npm"]; - const resolved = resolveSpawnCommand("npm", args, { env: { PATH: binDir } }); - - expect(resolved).toEqual({ command: join(binDir, "npm.exe"), args }); - }); - - it("should resolve npm command shims to their Node entrypoint without a shell", () => { - vi.spyOn(process, "platform", "get").mockReturnValue("win32"); - const binDir = join(tempDir, "node-bin"); - const npmCli = join(binDir, "node_modules", "npm", "bin", "npm-cli.js"); - mkdirSync(join(binDir, "node_modules", "npm", "bin"), { recursive: true }); - writeFileSync(join(binDir, "node.exe"), ""); - writeFileSync(npmCli, ""); - writeFileSync( - join(binDir, "npm.cmd"), - '@ECHO off\r\nSET "dp0=%~dp0"\r\nSET "_prog=%dp0%\\node.exe"\r\nendLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\\node_modules\\npm\\bin\\npm-cli.js" %*\r\n', - ); - - const args = ["install", "pkg", "--prefix", "C:\\Users\\A B\\.pi\\npm"]; - const resolved = resolveSpawnCommand("npm", args, { env: { PATH: binDir } }); - - expect(resolved).toEqual({ command: join(binDir, "node.exe"), args: [npmCli, ...args] }); - }); - - it("should reject unrecognized Windows command scripts instead of using cmd.exe", () => { - vi.spyOn(process, "platform", "get").mockReturnValue("win32"); - const binDir = join(tempDir, "bin"); - mkdirSync(binDir, { recursive: true }); - writeFileSync(join(binDir, "npm.cmd"), "@echo off\r\necho %*\r\n"); - - expect(() => - resolveSpawnCommand("npm", ["install", "pkg"], { - env: { PATH: binDir }, - }), - ).toThrow("Refusing to run Windows command shim without a shell"); + expect(output).toBe(valueWithSpace); }); }); From 92e7f13dc0867a341da871af88a028f6aa548775 Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Mon, 18 May 2026 11:45:39 +0200 Subject: [PATCH 101/166] chore(coding-agent): audit unreleased changelog --- packages/coding-agent/CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 543af174..b19de92c 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -4,7 +4,8 @@ ### Fixed -- Fixed Bun-compiled release binaries failing to start when Bun's built-in undici shim lacks npm undici's `install` export ([#4657](https://github.com/earendil-works/pi/issues/4657)). +- Fixed Bun-compiled release binaries failing to start when Bun's built-in undici shim lacks npm undici's `install` export ([#4661](https://github.com/earendil-works/pi-mono/pull/4661) by [@dmasiero](https://github.com/dmasiero)). +- Fixed Xiaomi MiMo generated model metadata to replay assistant tool-call messages with `reasoning_content` for thinking-mode multi-turn requests, inherited from `@earendil-works/pi-ai` ([#4678](https://github.com/earendil-works/pi/issues/4678)). - Fixed Windows external editor handoff so vim/nvim can receive input after opening from the TUI ([#4612](https://github.com/earendil-works/pi/issues/4612)). - Fixed Windows npm self-updates to move loaded native dependency packages out of the active install before reinstalling pi ([#4157](https://github.com/earendil-works/pi/issues/4157)). - Fixed `pi update --self` detection for pnpm v11 global installs whose package path resolves through the pnpm store ([#4647](https://github.com/earendil-works/pi/issues/4647)). From ea713ba1744e5cf1c5a1914a8d7e7328583042e1 Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Mon, 18 May 2026 11:46:23 +0200 Subject: [PATCH 102/166] Release v0.75.2 --- package-lock.json | 312 ++++++++---------- packages/agent/CHANGELOG.md | 2 +- packages/agent/package.json | 4 +- packages/ai/CHANGELOG.md | 2 +- packages/ai/package.json | 2 +- packages/coding-agent/CHANGELOG.md | 2 +- .../package-lock.json | 4 +- .../custom-provider-anthropic/package.json | 2 +- .../custom-provider-gitlab-duo/package.json | 2 +- .../extensions/sandbox/package-lock.json | 4 +- .../examples/extensions/sandbox/package.json | 2 +- .../extensions/with-deps/package-lock.json | 4 +- .../extensions/with-deps/package.json | 2 +- packages/coding-agent/package.json | 8 +- packages/tui/CHANGELOG.md | 2 +- packages/tui/package.json | 2 +- packages/web-ui/CHANGELOG.md | 2 +- packages/web-ui/example/package.json | 2 +- packages/web-ui/package.json | 6 +- 19 files changed, 156 insertions(+), 210 deletions(-) diff --git a/package-lock.json b/package-lock.json index b0c47a95..c4177026 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3913,30 +3913,17 @@ "license": "MIT" }, "node_modules/cross-spawn": { - "version": "6.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.6.tgz", - "integrity": "sha512-VqCUuhcd1iB+dsv8gxPttb5iZh/D0iubSP21g36KXdEuf6I5JiioesUVjpCdHV9MZRUfVFlvwtIUyPfxo5trtw==", - "dev": true, + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "license": "MIT", "dependencies": { - "nice-try": "^1.0.4", - "path-key": "^2.0.1", - "semver": "^5.5.0", - "shebang-command": "^1.2.0", - "which": "^1.2.9" + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" }, "engines": { - "node": ">=4.8" - } - }, - "node_modules/cross-spawn/node_modules/semver": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", - "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver" + "node": ">= 8" } }, "node_modules/data-uri-to-buffer": { @@ -4174,6 +4161,79 @@ "node": ">=6" } }, + "node_modules/execa/node_modules/cross-spawn": { + "version": "6.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.6.tgz", + "integrity": "sha512-VqCUuhcd1iB+dsv8gxPttb5iZh/D0iubSP21g36KXdEuf6I5JiioesUVjpCdHV9MZRUfVFlvwtIUyPfxo5trtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + }, + "engines": { + "node": ">=4.8" + } + }, + "node_modules/execa/node_modules/path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/execa/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/execa/node_modules/shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/execa/node_modules/shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/execa/node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, "node_modules/expand-template": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", @@ -4317,54 +4377,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/foreground-child/node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/foreground-child/node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/foreground-child/node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, - "license": "MIT", - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/foreground-child/node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/foreground-child/node_modules/signal-exit": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", @@ -4378,22 +4390,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/foreground-child/node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, "node_modules/formdata-polyfill": { "version": "4.0.10", "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", @@ -5358,9 +5354,9 @@ "license": "MIT" }, "node_modules/lru-cache": { - "version": "11.3.6", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.3.6.tgz", - "integrity": "sha512-Gf/KoL3C/MlI7Bt0PGI9I+TeTC/I6r/csU58N4BSNc4lppLBeKsOdFYkK+dX0ABDUMJNfCHTyPpzwwO21Awd3A==", + "version": "11.4.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.4.0.tgz", + "integrity": "sha512-W+R+kFL4HgVxONq2bhXPi3bGpzGe/yEhVOp233qw9wCRtgncJ15P3bC+e4zZMu4Cq7d+WAJjXGW0uUkifhcatA==", "license": "BlueOak-1.0.0", "engines": { "node": "20 || >=22" @@ -5620,6 +5616,16 @@ "node": ">=4" } }, + "node_modules/npm-run-path/node_modules/path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/ollama": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/ollama/-/ollama-0.6.3.tgz", @@ -5724,13 +5730,12 @@ } }, "node_modules/path-key": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", - "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==", - "dev": true, + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", "license": "MIT", "engines": { - "node": ">=4" + "node": ">=8" } }, "node_modules/path-parse": { @@ -6185,26 +6190,24 @@ "license": "MIT" }, "node_modules/shebang-command": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", - "integrity": "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==", - "dev": true, + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", "license": "MIT", "dependencies": { - "shebang-regex": "^1.0.0" + "shebang-regex": "^3.0.0" }, "engines": { - "node": ">=0.10.0" + "node": ">=8" } }, "node_modules/shebang-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", - "integrity": "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==", - "dev": true, + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", "license": "MIT", "engines": { - "node": ">=0.10.0" + "node": ">=8" } }, "node_modules/shell-quote": { @@ -7585,16 +7588,18 @@ "license": "MIT" }, "node_modules/which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", - "dev": true, + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", "license": "ISC", "dependencies": { "isexe": "^2.0.0" }, "bin": { - "which": "bin/which" + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" } }, "node_modules/why-is-node-running": { @@ -7780,10 +7785,10 @@ }, "packages/agent": { "name": "@earendil-works/pi-agent-core", - "version": "0.75.1", + "version": "0.75.2", "license": "MIT", "dependencies": { - "@earendil-works/pi-ai": "^0.75.1", + "@earendil-works/pi-ai": "^0.75.2", "ignore": "^7.0.5", "typebox": "^1.1.24", "yaml": "^2.8.2" @@ -7817,7 +7822,7 @@ }, "packages/ai": { "name": "@earendil-works/pi-ai", - "version": "0.75.1", + "version": "0.75.2", "license": "MIT", "dependencies": { "@anthropic-ai/sdk": "^0.91.1", @@ -7861,12 +7866,12 @@ }, "packages/coding-agent": { "name": "@earendil-works/pi-coding-agent", - "version": "0.75.1", + "version": "0.75.2", "license": "MIT", "dependencies": { - "@earendil-works/pi-agent-core": "^0.75.1", - "@earendil-works/pi-ai": "^0.75.1", - "@earendil-works/pi-tui": "^0.75.1", + "@earendil-works/pi-agent-core": "^0.75.2", + "@earendil-works/pi-ai": "^0.75.2", + "@earendil-works/pi-tui": "^0.75.2", "@silvia-odwyer/photon-node": "^0.3.4", "chalk": "^5.5.0", "cross-spawn": "^7.0.6", @@ -7905,25 +7910,25 @@ }, "packages/coding-agent/examples/extensions/custom-provider-anthropic": { "name": "pi-extension-custom-provider-anthropic", - "version": "0.75.1", + "version": "0.75.2", "dependencies": { "@anthropic-ai/sdk": "^0.52.0" } }, "packages/coding-agent/examples/extensions/custom-provider-gitlab-duo": { "name": "pi-extension-custom-provider-gitlab-duo", - "version": "0.75.1" + "version": "0.75.2" }, "packages/coding-agent/examples/extensions/sandbox": { "name": "pi-extension-sandbox", - "version": "1.5.1", + "version": "1.5.2", "dependencies": { "@anthropic-ai/sandbox-runtime": "^0.0.26" } }, "packages/coding-agent/examples/extensions/with-deps": { "name": "pi-extension-with-deps", - "version": "0.75.1", + "version": "0.75.2", "dependencies": { "ms": "^2.1.3" }, @@ -7950,50 +7955,6 @@ "undici-types": "~7.16.0" } }, - "packages/coding-agent/node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "packages/coding-agent/node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "packages/coding-agent/node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "license": "MIT", - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "packages/coding-agent/node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "packages/coding-agent/node_modules/undici-types": { "version": "7.16.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", @@ -8001,24 +7962,9 @@ "dev": true, "license": "MIT" }, - "packages/coding-agent/node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, "packages/tui": { "name": "@earendil-works/pi-tui", - "version": "0.75.1", + "version": "0.75.2", "license": "MIT", "dependencies": { "get-east-asian-width": "^1.3.0", @@ -8038,11 +7984,11 @@ }, "packages/web-ui": { "name": "@earendil-works/pi-web-ui", - "version": "0.75.1", + "version": "0.75.2", "license": "MIT", "dependencies": { - "@earendil-works/pi-ai": "^0.75.1", - "@earendil-works/pi-tui": "^0.75.1", + "@earendil-works/pi-ai": "^0.75.2", + "@earendil-works/pi-tui": "^0.75.2", "@lmstudio/sdk": "^1.5.0", "docx-preview": "^0.3.7", "jszip": "^3.10.1", @@ -8065,7 +8011,7 @@ }, "packages/web-ui/example": { "name": "pi-web-ui-example", - "version": "0.75.1", + "version": "0.75.2", "dependencies": { "@earendil-works/pi-ai": "file:../../ai", "@earendil-works/pi-web-ui": "file:../", diff --git a/packages/agent/CHANGELOG.md b/packages/agent/CHANGELOG.md index 0dcff063..62dbfb6a 100644 --- a/packages/agent/CHANGELOG.md +++ b/packages/agent/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## [Unreleased] +## [0.75.2] - 2026-05-18 ## [0.75.1] - 2026-05-18 diff --git a/packages/agent/package.json b/packages/agent/package.json index 0ef97503..e8a66eb5 100644 --- a/packages/agent/package.json +++ b/packages/agent/package.json @@ -1,6 +1,6 @@ { "name": "@earendil-works/pi-agent-core", - "version": "0.75.1", + "version": "0.75.2", "description": "General-purpose agent with transport abstraction, state management, and attachment support", "type": "module", "main": "./dist/index.js", @@ -30,7 +30,7 @@ "prepublishOnly": "npm run clean && npm run build" }, "dependencies": { - "@earendil-works/pi-ai": "^0.75.1", + "@earendil-works/pi-ai": "^0.75.2", "ignore": "^7.0.5", "typebox": "^1.1.24", "yaml": "^2.8.2" diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index 1bb47cb7..b529ea1e 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## [Unreleased] +## [0.75.2] - 2026-05-18 ### Fixed diff --git a/packages/ai/package.json b/packages/ai/package.json index f21ec37f..dc6bc432 100644 --- a/packages/ai/package.json +++ b/packages/ai/package.json @@ -1,6 +1,6 @@ { "name": "@earendil-works/pi-ai", - "version": "0.75.1", + "version": "0.75.2", "description": "Unified LLM API with automatic model discovery and provider configuration", "type": "module", "main": "./dist/index.js", diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index b19de92c..20620fe9 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## [Unreleased] +## [0.75.2] - 2026-05-18 ### Fixed diff --git a/packages/coding-agent/examples/extensions/custom-provider-anthropic/package-lock.json b/packages/coding-agent/examples/extensions/custom-provider-anthropic/package-lock.json index 76e3eb34..f74307e2 100644 --- a/packages/coding-agent/examples/extensions/custom-provider-anthropic/package-lock.json +++ b/packages/coding-agent/examples/extensions/custom-provider-anthropic/package-lock.json @@ -1,12 +1,12 @@ { "name": "pi-extension-custom-provider", - "version": "0.75.1", + "version": "0.75.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pi-extension-custom-provider", - "version": "0.75.1", + "version": "0.75.2", "dependencies": { "@anthropic-ai/sdk": "^0.52.0" } diff --git a/packages/coding-agent/examples/extensions/custom-provider-anthropic/package.json b/packages/coding-agent/examples/extensions/custom-provider-anthropic/package.json index e981ee5d..6e5b2eb1 100644 --- a/packages/coding-agent/examples/extensions/custom-provider-anthropic/package.json +++ b/packages/coding-agent/examples/extensions/custom-provider-anthropic/package.json @@ -1,7 +1,7 @@ { "name": "pi-extension-custom-provider-anthropic", "private": true, - "version": "0.75.1", + "version": "0.75.2", "type": "module", "scripts": { "clean": "echo 'nothing to clean'", diff --git a/packages/coding-agent/examples/extensions/custom-provider-gitlab-duo/package.json b/packages/coding-agent/examples/extensions/custom-provider-gitlab-duo/package.json index eb894a0f..daa083be 100644 --- a/packages/coding-agent/examples/extensions/custom-provider-gitlab-duo/package.json +++ b/packages/coding-agent/examples/extensions/custom-provider-gitlab-duo/package.json @@ -1,7 +1,7 @@ { "name": "pi-extension-custom-provider-gitlab-duo", "private": true, - "version": "0.75.1", + "version": "0.75.2", "type": "module", "scripts": { "clean": "echo 'nothing to clean'", diff --git a/packages/coding-agent/examples/extensions/sandbox/package-lock.json b/packages/coding-agent/examples/extensions/sandbox/package-lock.json index 7daafbad..447b095c 100644 --- a/packages/coding-agent/examples/extensions/sandbox/package-lock.json +++ b/packages/coding-agent/examples/extensions/sandbox/package-lock.json @@ -1,12 +1,12 @@ { "name": "pi-extension-sandbox", - "version": "1.5.1", + "version": "1.5.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pi-extension-sandbox", - "version": "1.5.1", + "version": "1.5.2", "dependencies": { "@anthropic-ai/sandbox-runtime": "^0.0.26" } diff --git a/packages/coding-agent/examples/extensions/sandbox/package.json b/packages/coding-agent/examples/extensions/sandbox/package.json index 009e0e93..1b555195 100644 --- a/packages/coding-agent/examples/extensions/sandbox/package.json +++ b/packages/coding-agent/examples/extensions/sandbox/package.json @@ -1,7 +1,7 @@ { "name": "pi-extension-sandbox", "private": true, - "version": "1.5.1", + "version": "1.5.2", "type": "module", "scripts": { "clean": "echo 'nothing to clean'", diff --git a/packages/coding-agent/examples/extensions/with-deps/package-lock.json b/packages/coding-agent/examples/extensions/with-deps/package-lock.json index 5d74195a..95eb9804 100644 --- a/packages/coding-agent/examples/extensions/with-deps/package-lock.json +++ b/packages/coding-agent/examples/extensions/with-deps/package-lock.json @@ -1,12 +1,12 @@ { "name": "pi-extension-with-deps", - "version": "0.75.1", + "version": "0.75.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pi-extension-with-deps", - "version": "0.75.1", + "version": "0.75.2", "dependencies": { "ms": "^2.1.3" }, diff --git a/packages/coding-agent/examples/extensions/with-deps/package.json b/packages/coding-agent/examples/extensions/with-deps/package.json index da1d2ef1..d755f650 100644 --- a/packages/coding-agent/examples/extensions/with-deps/package.json +++ b/packages/coding-agent/examples/extensions/with-deps/package.json @@ -1,7 +1,7 @@ { "name": "pi-extension-with-deps", "private": true, - "version": "0.75.1", + "version": "0.75.2", "type": "module", "scripts": { "clean": "echo 'nothing to clean'", diff --git a/packages/coding-agent/package.json b/packages/coding-agent/package.json index 5171b159..ab6b5ad1 100644 --- a/packages/coding-agent/package.json +++ b/packages/coding-agent/package.json @@ -1,6 +1,6 @@ { "name": "@earendil-works/pi-coding-agent", - "version": "0.75.1", + "version": "0.75.2", "description": "Coding agent CLI with read, bash, edit, write tools and session management", "type": "module", "piConfig": { @@ -38,9 +38,9 @@ "prepublishOnly": "npm run clean && npm run build" }, "dependencies": { - "@earendil-works/pi-agent-core": "^0.75.1", - "@earendil-works/pi-ai": "^0.75.1", - "@earendil-works/pi-tui": "^0.75.1", + "@earendil-works/pi-agent-core": "^0.75.2", + "@earendil-works/pi-ai": "^0.75.2", + "@earendil-works/pi-tui": "^0.75.2", "@silvia-odwyer/photon-node": "^0.3.4", "chalk": "^5.5.0", "cross-spawn": "^7.0.6", diff --git a/packages/tui/CHANGELOG.md b/packages/tui/CHANGELOG.md index db2a3050..d4d9d0a2 100644 --- a/packages/tui/CHANGELOG.md +++ b/packages/tui/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## [Unreleased] +## [0.75.2] - 2026-05-18 ## [0.75.1] - 2026-05-18 diff --git a/packages/tui/package.json b/packages/tui/package.json index 4056e28d..374ee152 100644 --- a/packages/tui/package.json +++ b/packages/tui/package.json @@ -1,6 +1,6 @@ { "name": "@earendil-works/pi-tui", - "version": "0.75.1", + "version": "0.75.2", "description": "Terminal User Interface library with differential rendering for efficient text-based applications", "type": "module", "main": "dist/index.js", diff --git a/packages/web-ui/CHANGELOG.md b/packages/web-ui/CHANGELOG.md index f7ea1c30..33946106 100644 --- a/packages/web-ui/CHANGELOG.md +++ b/packages/web-ui/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## [Unreleased] +## [0.75.2] - 2026-05-18 ## [0.75.1] - 2026-05-18 diff --git a/packages/web-ui/example/package.json b/packages/web-ui/example/package.json index 851a103c..bbc9312d 100644 --- a/packages/web-ui/example/package.json +++ b/packages/web-ui/example/package.json @@ -1,6 +1,6 @@ { "name": "pi-web-ui-example", - "version": "0.75.1", + "version": "0.75.2", "private": true, "type": "module", "scripts": { diff --git a/packages/web-ui/package.json b/packages/web-ui/package.json index c3f81dfb..e285102b 100644 --- a/packages/web-ui/package.json +++ b/packages/web-ui/package.json @@ -1,6 +1,6 @@ { "name": "@earendil-works/pi-web-ui", - "version": "0.75.1", + "version": "0.75.2", "description": "Reusable web UI components for AI chat interfaces powered by @earendil-works/pi-ai", "type": "module", "main": "dist/index.js", @@ -18,8 +18,8 @@ }, "dependencies": { "@lmstudio/sdk": "^1.5.0", - "@earendil-works/pi-ai": "^0.75.1", - "@earendil-works/pi-tui": "^0.75.1", + "@earendil-works/pi-ai": "^0.75.2", + "@earendil-works/pi-tui": "^0.75.2", "typebox": "^1.1.24", "docx-preview": "^0.3.7", "jszip": "^3.10.1", From 0c4d704a7bbe27b8f52cf8ed13302200444caea3 Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Mon, 18 May 2026 11:47:16 +0200 Subject: [PATCH 103/166] Add [Unreleased] section for next cycle --- packages/agent/CHANGELOG.md | 2 ++ packages/ai/CHANGELOG.md | 2 ++ packages/ai/src/models.generated.ts | 6 +++--- packages/coding-agent/CHANGELOG.md | 2 ++ packages/tui/CHANGELOG.md | 2 ++ packages/web-ui/CHANGELOG.md | 2 ++ 6 files changed, 13 insertions(+), 3 deletions(-) diff --git a/packages/agent/CHANGELOG.md b/packages/agent/CHANGELOG.md index 62dbfb6a..007755de 100644 --- a/packages/agent/CHANGELOG.md +++ b/packages/agent/CHANGELOG.md @@ -1,5 +1,7 @@ # Changelog +## [Unreleased] + ## [0.75.2] - 2026-05-18 ## [0.75.1] - 2026-05-18 diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index b529ea1e..1e70d0c3 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -1,5 +1,7 @@ # Changelog +## [Unreleased] + ## [0.75.2] - 2026-05-18 ### Fixed diff --git a/packages/ai/src/models.generated.ts b/packages/ai/src/models.generated.ts index 283ecfd2..33df1db2 100644 --- a/packages/ai/src/models.generated.ts +++ b/packages/ai/src/models.generated.ts @@ -7268,9 +7268,9 @@ export const MODELS = { "big-pickle": { id: "big-pickle", name: "Big Pickle", - api: "anthropic-messages", + api: "openai-completions", provider: "opencode", - baseUrl: "https://opencode.ai/zen", + baseUrl: "https://opencode.ai/zen/v1", reasoning: true, input: ["text"], cost: { @@ -7281,7 +7281,7 @@ export const MODELS = { }, contextWindow: 200000, maxTokens: 128000, - } satisfies Model<"anthropic-messages">, + } satisfies Model<"openai-completions">, "claude-haiku-4-5": { id: "claude-haiku-4-5", name: "Claude Haiku 4.5", diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 20620fe9..b60da8e4 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -1,5 +1,7 @@ # Changelog +## [Unreleased] + ## [0.75.2] - 2026-05-18 ### Fixed diff --git a/packages/tui/CHANGELOG.md b/packages/tui/CHANGELOG.md index d4d9d0a2..00780441 100644 --- a/packages/tui/CHANGELOG.md +++ b/packages/tui/CHANGELOG.md @@ -1,5 +1,7 @@ # Changelog +## [Unreleased] + ## [0.75.2] - 2026-05-18 ## [0.75.1] - 2026-05-18 diff --git a/packages/web-ui/CHANGELOG.md b/packages/web-ui/CHANGELOG.md index 33946106..1eb785ad 100644 --- a/packages/web-ui/CHANGELOG.md +++ b/packages/web-ui/CHANGELOG.md @@ -1,5 +1,7 @@ # Changelog +## [Unreleased] + ## [0.75.2] - 2026-05-18 ## [0.75.1] - 2026-05-18 From 967fb4d8b99cdb7cdd05fc597e88f16f67f64483 Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Mon, 18 May 2026 11:57:51 +0200 Subject: [PATCH 104/166] fix(coding-agent): disable undici http2 Fixes #4681. Fixes #4682. --- packages/coding-agent/CHANGELOG.md | 4 ++++ packages/coding-agent/src/cli.ts | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index b60da8e4..4ab8d314 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Fixed + +- Fixed undici 8 HTTP/2 destroyed-session races crashing the Node CLI by preserving the previous HTTP/1.1-only fetch dispatcher behavior ([#4681](https://github.com/earendil-works/pi/issues/4681)). + ## [0.75.2] - 2026-05-18 ### Fixed diff --git a/packages/coding-agent/src/cli.ts b/packages/coding-agent/src/cli.ts index f75d6bd1..1fdadb99 100644 --- a/packages/coding-agent/src/cli.ts +++ b/packages/coding-agent/src/cli.ts @@ -17,7 +17,7 @@ process.emitWarning = (() => {}) as typeof process.emitWarning; // (e.g. vLLM buffering a large tool call) exceed that and abort the SSE stream // with UND_ERR_BODY_TIMEOUT. Disable both — provider SDKs enforce their own // AbortController-based deadlines via retry.provider.timeoutMs. -undici.setGlobalDispatcher(new undici.EnvHttpProxyAgent({ bodyTimeout: 0, headersTimeout: 0 })); +undici.setGlobalDispatcher(new undici.EnvHttpProxyAgent({ allowH2: false, bodyTimeout: 0, headersTimeout: 0 })); // Keep fetch and the dispatcher on the same undici implementation. Node 26.0's // bundled fetch can otherwise consume compressed responses through npm undici's From a7d8dd3d5db7d66aa5cc6886e32768b1196ce91f Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Mon, 18 May 2026 11:58:34 +0200 Subject: [PATCH 105/166] Release v0.75.3 --- package-lock.json | 32 +++++++++---------- packages/agent/CHANGELOG.md | 2 +- packages/agent/package.json | 4 +-- packages/ai/CHANGELOG.md | 2 +- packages/ai/package.json | 2 +- packages/coding-agent/CHANGELOG.md | 2 +- .../package-lock.json | 4 +-- .../custom-provider-anthropic/package.json | 2 +- .../custom-provider-gitlab-duo/package.json | 2 +- .../extensions/sandbox/package-lock.json | 4 +-- .../examples/extensions/sandbox/package.json | 2 +- .../extensions/with-deps/package-lock.json | 4 +-- .../extensions/with-deps/package.json | 2 +- packages/coding-agent/package.json | 8 ++--- packages/tui/CHANGELOG.md | 2 +- packages/tui/package.json | 2 +- packages/web-ui/CHANGELOG.md | 2 +- packages/web-ui/example/package.json | 2 +- packages/web-ui/package.json | 6 ++-- 19 files changed, 43 insertions(+), 43 deletions(-) diff --git a/package-lock.json b/package-lock.json index c4177026..292b1ebb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7785,10 +7785,10 @@ }, "packages/agent": { "name": "@earendil-works/pi-agent-core", - "version": "0.75.2", + "version": "0.75.3", "license": "MIT", "dependencies": { - "@earendil-works/pi-ai": "^0.75.2", + "@earendil-works/pi-ai": "^0.75.3", "ignore": "^7.0.5", "typebox": "^1.1.24", "yaml": "^2.8.2" @@ -7822,7 +7822,7 @@ }, "packages/ai": { "name": "@earendil-works/pi-ai", - "version": "0.75.2", + "version": "0.75.3", "license": "MIT", "dependencies": { "@anthropic-ai/sdk": "^0.91.1", @@ -7866,12 +7866,12 @@ }, "packages/coding-agent": { "name": "@earendil-works/pi-coding-agent", - "version": "0.75.2", + "version": "0.75.3", "license": "MIT", "dependencies": { - "@earendil-works/pi-agent-core": "^0.75.2", - "@earendil-works/pi-ai": "^0.75.2", - "@earendil-works/pi-tui": "^0.75.2", + "@earendil-works/pi-agent-core": "^0.75.3", + "@earendil-works/pi-ai": "^0.75.3", + "@earendil-works/pi-tui": "^0.75.3", "@silvia-odwyer/photon-node": "^0.3.4", "chalk": "^5.5.0", "cross-spawn": "^7.0.6", @@ -7910,25 +7910,25 @@ }, "packages/coding-agent/examples/extensions/custom-provider-anthropic": { "name": "pi-extension-custom-provider-anthropic", - "version": "0.75.2", + "version": "0.75.3", "dependencies": { "@anthropic-ai/sdk": "^0.52.0" } }, "packages/coding-agent/examples/extensions/custom-provider-gitlab-duo": { "name": "pi-extension-custom-provider-gitlab-duo", - "version": "0.75.2" + "version": "0.75.3" }, "packages/coding-agent/examples/extensions/sandbox": { "name": "pi-extension-sandbox", - "version": "1.5.2", + "version": "1.5.3", "dependencies": { "@anthropic-ai/sandbox-runtime": "^0.0.26" } }, "packages/coding-agent/examples/extensions/with-deps": { "name": "pi-extension-with-deps", - "version": "0.75.2", + "version": "0.75.3", "dependencies": { "ms": "^2.1.3" }, @@ -7964,7 +7964,7 @@ }, "packages/tui": { "name": "@earendil-works/pi-tui", - "version": "0.75.2", + "version": "0.75.3", "license": "MIT", "dependencies": { "get-east-asian-width": "^1.3.0", @@ -7984,11 +7984,11 @@ }, "packages/web-ui": { "name": "@earendil-works/pi-web-ui", - "version": "0.75.2", + "version": "0.75.3", "license": "MIT", "dependencies": { - "@earendil-works/pi-ai": "^0.75.2", - "@earendil-works/pi-tui": "^0.75.2", + "@earendil-works/pi-ai": "^0.75.3", + "@earendil-works/pi-tui": "^0.75.3", "@lmstudio/sdk": "^1.5.0", "docx-preview": "^0.3.7", "jszip": "^3.10.1", @@ -8011,7 +8011,7 @@ }, "packages/web-ui/example": { "name": "pi-web-ui-example", - "version": "0.75.2", + "version": "0.75.3", "dependencies": { "@earendil-works/pi-ai": "file:../../ai", "@earendil-works/pi-web-ui": "file:../", diff --git a/packages/agent/CHANGELOG.md b/packages/agent/CHANGELOG.md index 007755de..d6cafcc8 100644 --- a/packages/agent/CHANGELOG.md +++ b/packages/agent/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## [Unreleased] +## [0.75.3] - 2026-05-18 ## [0.75.2] - 2026-05-18 diff --git a/packages/agent/package.json b/packages/agent/package.json index e8a66eb5..ef39353f 100644 --- a/packages/agent/package.json +++ b/packages/agent/package.json @@ -1,6 +1,6 @@ { "name": "@earendil-works/pi-agent-core", - "version": "0.75.2", + "version": "0.75.3", "description": "General-purpose agent with transport abstraction, state management, and attachment support", "type": "module", "main": "./dist/index.js", @@ -30,7 +30,7 @@ "prepublishOnly": "npm run clean && npm run build" }, "dependencies": { - "@earendil-works/pi-ai": "^0.75.2", + "@earendil-works/pi-ai": "^0.75.3", "ignore": "^7.0.5", "typebox": "^1.1.24", "yaml": "^2.8.2" diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index 1e70d0c3..90849343 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## [Unreleased] +## [0.75.3] - 2026-05-18 ## [0.75.2] - 2026-05-18 diff --git a/packages/ai/package.json b/packages/ai/package.json index dc6bc432..daeb856b 100644 --- a/packages/ai/package.json +++ b/packages/ai/package.json @@ -1,6 +1,6 @@ { "name": "@earendil-works/pi-ai", - "version": "0.75.2", + "version": "0.75.3", "description": "Unified LLM API with automatic model discovery and provider configuration", "type": "module", "main": "./dist/index.js", diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 4ab8d314..0efd6152 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## [Unreleased] +## [0.75.3] - 2026-05-18 ### Fixed diff --git a/packages/coding-agent/examples/extensions/custom-provider-anthropic/package-lock.json b/packages/coding-agent/examples/extensions/custom-provider-anthropic/package-lock.json index f74307e2..8ca5ae51 100644 --- a/packages/coding-agent/examples/extensions/custom-provider-anthropic/package-lock.json +++ b/packages/coding-agent/examples/extensions/custom-provider-anthropic/package-lock.json @@ -1,12 +1,12 @@ { "name": "pi-extension-custom-provider", - "version": "0.75.2", + "version": "0.75.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pi-extension-custom-provider", - "version": "0.75.2", + "version": "0.75.3", "dependencies": { "@anthropic-ai/sdk": "^0.52.0" } diff --git a/packages/coding-agent/examples/extensions/custom-provider-anthropic/package.json b/packages/coding-agent/examples/extensions/custom-provider-anthropic/package.json index 6e5b2eb1..d8e358c5 100644 --- a/packages/coding-agent/examples/extensions/custom-provider-anthropic/package.json +++ b/packages/coding-agent/examples/extensions/custom-provider-anthropic/package.json @@ -1,7 +1,7 @@ { "name": "pi-extension-custom-provider-anthropic", "private": true, - "version": "0.75.2", + "version": "0.75.3", "type": "module", "scripts": { "clean": "echo 'nothing to clean'", diff --git a/packages/coding-agent/examples/extensions/custom-provider-gitlab-duo/package.json b/packages/coding-agent/examples/extensions/custom-provider-gitlab-duo/package.json index daa083be..cda287b8 100644 --- a/packages/coding-agent/examples/extensions/custom-provider-gitlab-duo/package.json +++ b/packages/coding-agent/examples/extensions/custom-provider-gitlab-duo/package.json @@ -1,7 +1,7 @@ { "name": "pi-extension-custom-provider-gitlab-duo", "private": true, - "version": "0.75.2", + "version": "0.75.3", "type": "module", "scripts": { "clean": "echo 'nothing to clean'", diff --git a/packages/coding-agent/examples/extensions/sandbox/package-lock.json b/packages/coding-agent/examples/extensions/sandbox/package-lock.json index 447b095c..80c45099 100644 --- a/packages/coding-agent/examples/extensions/sandbox/package-lock.json +++ b/packages/coding-agent/examples/extensions/sandbox/package-lock.json @@ -1,12 +1,12 @@ { "name": "pi-extension-sandbox", - "version": "1.5.2", + "version": "1.5.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pi-extension-sandbox", - "version": "1.5.2", + "version": "1.5.3", "dependencies": { "@anthropic-ai/sandbox-runtime": "^0.0.26" } diff --git a/packages/coding-agent/examples/extensions/sandbox/package.json b/packages/coding-agent/examples/extensions/sandbox/package.json index 1b555195..785ca407 100644 --- a/packages/coding-agent/examples/extensions/sandbox/package.json +++ b/packages/coding-agent/examples/extensions/sandbox/package.json @@ -1,7 +1,7 @@ { "name": "pi-extension-sandbox", "private": true, - "version": "1.5.2", + "version": "1.5.3", "type": "module", "scripts": { "clean": "echo 'nothing to clean'", diff --git a/packages/coding-agent/examples/extensions/with-deps/package-lock.json b/packages/coding-agent/examples/extensions/with-deps/package-lock.json index 95eb9804..a1cdd835 100644 --- a/packages/coding-agent/examples/extensions/with-deps/package-lock.json +++ b/packages/coding-agent/examples/extensions/with-deps/package-lock.json @@ -1,12 +1,12 @@ { "name": "pi-extension-with-deps", - "version": "0.75.2", + "version": "0.75.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pi-extension-with-deps", - "version": "0.75.2", + "version": "0.75.3", "dependencies": { "ms": "^2.1.3" }, diff --git a/packages/coding-agent/examples/extensions/with-deps/package.json b/packages/coding-agent/examples/extensions/with-deps/package.json index d755f650..eb66a8a9 100644 --- a/packages/coding-agent/examples/extensions/with-deps/package.json +++ b/packages/coding-agent/examples/extensions/with-deps/package.json @@ -1,7 +1,7 @@ { "name": "pi-extension-with-deps", "private": true, - "version": "0.75.2", + "version": "0.75.3", "type": "module", "scripts": { "clean": "echo 'nothing to clean'", diff --git a/packages/coding-agent/package.json b/packages/coding-agent/package.json index ab6b5ad1..30982c27 100644 --- a/packages/coding-agent/package.json +++ b/packages/coding-agent/package.json @@ -1,6 +1,6 @@ { "name": "@earendil-works/pi-coding-agent", - "version": "0.75.2", + "version": "0.75.3", "description": "Coding agent CLI with read, bash, edit, write tools and session management", "type": "module", "piConfig": { @@ -38,9 +38,9 @@ "prepublishOnly": "npm run clean && npm run build" }, "dependencies": { - "@earendil-works/pi-agent-core": "^0.75.2", - "@earendil-works/pi-ai": "^0.75.2", - "@earendil-works/pi-tui": "^0.75.2", + "@earendil-works/pi-agent-core": "^0.75.3", + "@earendil-works/pi-ai": "^0.75.3", + "@earendil-works/pi-tui": "^0.75.3", "@silvia-odwyer/photon-node": "^0.3.4", "chalk": "^5.5.0", "cross-spawn": "^7.0.6", diff --git a/packages/tui/CHANGELOG.md b/packages/tui/CHANGELOG.md index 00780441..f4f089f8 100644 --- a/packages/tui/CHANGELOG.md +++ b/packages/tui/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## [Unreleased] +## [0.75.3] - 2026-05-18 ## [0.75.2] - 2026-05-18 diff --git a/packages/tui/package.json b/packages/tui/package.json index 374ee152..b8695403 100644 --- a/packages/tui/package.json +++ b/packages/tui/package.json @@ -1,6 +1,6 @@ { "name": "@earendil-works/pi-tui", - "version": "0.75.2", + "version": "0.75.3", "description": "Terminal User Interface library with differential rendering for efficient text-based applications", "type": "module", "main": "dist/index.js", diff --git a/packages/web-ui/CHANGELOG.md b/packages/web-ui/CHANGELOG.md index 1eb785ad..9329ffb6 100644 --- a/packages/web-ui/CHANGELOG.md +++ b/packages/web-ui/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## [Unreleased] +## [0.75.3] - 2026-05-18 ## [0.75.2] - 2026-05-18 diff --git a/packages/web-ui/example/package.json b/packages/web-ui/example/package.json index bbc9312d..193dacfb 100644 --- a/packages/web-ui/example/package.json +++ b/packages/web-ui/example/package.json @@ -1,6 +1,6 @@ { "name": "pi-web-ui-example", - "version": "0.75.2", + "version": "0.75.3", "private": true, "type": "module", "scripts": { diff --git a/packages/web-ui/package.json b/packages/web-ui/package.json index e285102b..e9168f06 100644 --- a/packages/web-ui/package.json +++ b/packages/web-ui/package.json @@ -1,6 +1,6 @@ { "name": "@earendil-works/pi-web-ui", - "version": "0.75.2", + "version": "0.75.3", "description": "Reusable web UI components for AI chat interfaces powered by @earendil-works/pi-ai", "type": "module", "main": "dist/index.js", @@ -18,8 +18,8 @@ }, "dependencies": { "@lmstudio/sdk": "^1.5.0", - "@earendil-works/pi-ai": "^0.75.2", - "@earendil-works/pi-tui": "^0.75.2", + "@earendil-works/pi-ai": "^0.75.3", + "@earendil-works/pi-tui": "^0.75.3", "typebox": "^1.1.24", "docx-preview": "^0.3.7", "jszip": "^3.10.1", From 4943c1d62c8e6f3437446f480bb3e8422869535d Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Mon, 18 May 2026 11:59:24 +0200 Subject: [PATCH 106/166] Add [Unreleased] section for next cycle --- packages/agent/CHANGELOG.md | 2 ++ packages/ai/CHANGELOG.md | 2 ++ packages/coding-agent/CHANGELOG.md | 2 ++ packages/tui/CHANGELOG.md | 2 ++ packages/web-ui/CHANGELOG.md | 2 ++ 5 files changed, 10 insertions(+) diff --git a/packages/agent/CHANGELOG.md b/packages/agent/CHANGELOG.md index d6cafcc8..7c451eb0 100644 --- a/packages/agent/CHANGELOG.md +++ b/packages/agent/CHANGELOG.md @@ -1,5 +1,7 @@ # Changelog +## [Unreleased] + ## [0.75.3] - 2026-05-18 ## [0.75.2] - 2026-05-18 diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index 90849343..2f5bd8ad 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -1,5 +1,7 @@ # Changelog +## [Unreleased] + ## [0.75.3] - 2026-05-18 ## [0.75.2] - 2026-05-18 diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 0efd6152..280658eb 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -1,5 +1,7 @@ # Changelog +## [Unreleased] + ## [0.75.3] - 2026-05-18 ### Fixed diff --git a/packages/tui/CHANGELOG.md b/packages/tui/CHANGELOG.md index f4f089f8..47656ed1 100644 --- a/packages/tui/CHANGELOG.md +++ b/packages/tui/CHANGELOG.md @@ -1,5 +1,7 @@ # Changelog +## [Unreleased] + ## [0.75.3] - 2026-05-18 ## [0.75.2] - 2026-05-18 diff --git a/packages/web-ui/CHANGELOG.md b/packages/web-ui/CHANGELOG.md index 9329ffb6..fa32c7ae 100644 --- a/packages/web-ui/CHANGELOG.md +++ b/packages/web-ui/CHANGELOG.md @@ -1,5 +1,7 @@ # Changelog +## [Unreleased] + ## [0.75.3] - 2026-05-18 ## [0.75.2] - 2026-05-18 From 137547a47012119310fe13a8a542cac5ba63c521 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 18 May 2026 12:47:33 +0200 Subject: [PATCH 107/166] docs(coding-agent): add uninstall instructions --- packages/coding-agent/docs/index.md | 8 ++++++++ packages/coding-agent/docs/packages.md | 2 ++ packages/coding-agent/docs/quickstart.md | 20 ++++++++++++++++++++ packages/coding-agent/docs/usage.md | 2 ++ 4 files changed, 32 insertions(+) diff --git a/packages/coding-agent/docs/index.md b/packages/coding-agent/docs/index.md index a4294d3a..ffe97865 100644 --- a/packages/coding-agent/docs/index.md +++ b/packages/coding-agent/docs/index.md @@ -16,6 +16,14 @@ Or alternatively with npm: npm install -g @earendil-works/pi-coding-agent ``` +To uninstall pi itself, use npm for curl and npm installs: + +```bash +npm uninstall -g @earendil-works/pi-coding-agent +``` + +For pnpm, Yarn, or Bun installs, use the matching global remove command: `pnpm remove -g @earendil-works/pi-coding-agent`, `yarn global remove @earendil-works/pi-coding-agent`, or `bun uninstall -g @earendil-works/pi-coding-agent`. + Then run it in a project directory: ```bash diff --git a/packages/coding-agent/docs/packages.md b/packages/coding-agent/docs/packages.md index 575f0acc..db5ab6cc 100644 --- a/packages/coding-agent/docs/packages.md +++ b/packages/coding-agent/docs/packages.md @@ -36,6 +36,8 @@ pi update npm:@foo/bar # update one package pi update --extension npm:@foo/bar ``` +These commands manage pi packages, not the pi CLI installation. To uninstall pi itself, see [Quickstart](quickstart.md#uninstall). + By default, `install` and `remove` write to user settings (`~/.pi/agent/settings.json`). Use `-l` to write to project settings (`.pi/settings.json`) instead. Project settings can be shared with your team, and pi installs any missing packages automatically on startup. To try a package without installing it, use `--extension` or `-e`. This installs to a temporary directory for the current run only: diff --git a/packages/coding-agent/docs/quickstart.md b/packages/coding-agent/docs/quickstart.md index 55c6e346..29f9896a 100644 --- a/packages/coding-agent/docs/quickstart.md +++ b/packages/coding-agent/docs/quickstart.md @@ -10,6 +10,26 @@ Pi is distributed as an npm package: npm install -g @earendil-works/pi-coding-agent ``` +### Uninstall + +Use the package manager that installed pi. The curl installer uses npm globally, so curl and npm installs are removed with npm: + +```bash +# curl installer or npm install -g +npm uninstall -g @earendil-works/pi-coding-agent + +# pnpm +pnpm remove -g @earendil-works/pi-coding-agent + +# Yarn +yarn global remove @earendil-works/pi-coding-agent + +# Bun +bun uninstall -g @earendil-works/pi-coding-agent +``` + +Uninstalling pi leaves settings, credentials, sessions, and installed pi packages in `~/.pi/agent/`. + Then start pi in the project directory you want it to work on: ```bash diff --git a/packages/coding-agent/docs/usage.md b/packages/coding-agent/docs/usage.md index 487e72ac..65081090 100644 --- a/packages/coding-agent/docs/usage.md +++ b/packages/coding-agent/docs/usage.md @@ -137,6 +137,8 @@ pi list # List installed packages pi config # Enable/disable package resources ``` +These commands manage pi packages, not the pi CLI installation. To uninstall pi itself, see [Quickstart](quickstart.md#uninstall). + See [Pi Packages](packages.md) for package sources and security notes. ### Modes From 7577d3b8d52b24879b6dc95d93c9e6876fc3c1fa Mon Sep 17 00:00:00 2001 From: Nelson Herrera Date: Mon, 18 May 2026 08:54:16 -0700 Subject: [PATCH 108/166] Updated the default system prompt to also use xml boundaries instead of using ## so that agents are less likely to ingest a prompt with unclear boundaries. --- packages/coding-agent/src/core/system-prompt.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/coding-agent/src/core/system-prompt.ts b/packages/coding-agent/src/core/system-prompt.ts index 3675e413..a88b41b3 100644 --- a/packages/coding-agent/src/core/system-prompt.ts +++ b/packages/coding-agent/src/core/system-prompt.ts @@ -153,11 +153,12 @@ Pi documentation (read only when the user asks about pi itself, its SDK, extensi // Append project context files if (contextFiles.length > 0) { - prompt += "\n\n# Project Context\n\n"; + prompt += "\n\n\n\n"; prompt += "Project-specific instructions and guidelines:\n\n"; for (const { path: filePath, content } of contextFiles) { - prompt += `## ${filePath}\n\n${content}\n\n`; + prompt += `\n${content}\n\n\n`; } + prompt += "\n"; } // Append skills section (only if read tool is available) From 479024603eea9b8e0384fcf718c5b294ae89c8bf Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 18 May 2026 18:04:55 +0200 Subject: [PATCH 109/166] fix(coding-agent): set explicit theme text colors --- .../coding-agent/src/modes/interactive/theme/dark.json | 9 +++++---- .../coding-agent/src/modes/interactive/theme/light.json | 9 +++++---- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/packages/coding-agent/src/modes/interactive/theme/dark.json b/packages/coding-agent/src/modes/interactive/theme/dark.json index fc52fe68..c133e232 100644 --- a/packages/coding-agent/src/modes/interactive/theme/dark.json +++ b/packages/coding-agent/src/modes/interactive/theme/dark.json @@ -7,6 +7,7 @@ "green": "#b5bd68", "red": "#cc6666", "yellow": "#ffff00", + "text": "#d4d4d4", "gray": "#808080", "dimGray": "#666666", "darkGray": "#505050", @@ -28,19 +29,19 @@ "warning": "yellow", "muted": "gray", "dim": "dimGray", - "text": "", + "text": "text", "thinkingText": "gray", "selectedBg": "selectedBg", "userMessageBg": "userMsgBg", - "userMessageText": "", + "userMessageText": "text", "customMessageBg": "customMsgBg", - "customMessageText": "", + "customMessageText": "text", "customMessageLabel": "#9575cd", "toolPendingBg": "toolPendingBg", "toolSuccessBg": "toolSuccessBg", "toolErrorBg": "toolErrorBg", - "toolTitle": "", + "toolTitle": "text", "toolOutput": "gray", "mdHeading": "#f0c674", diff --git a/packages/coding-agent/src/modes/interactive/theme/light.json b/packages/coding-agent/src/modes/interactive/theme/light.json index 65304407..0a417b56 100644 --- a/packages/coding-agent/src/modes/interactive/theme/light.json +++ b/packages/coding-agent/src/modes/interactive/theme/light.json @@ -7,6 +7,7 @@ "green": "#588458", "red": "#aa5555", "yellow": "#9a7326", + "text": "#1f2328", "mediumGray": "#6c6c6c", "dimGray": "#767676", "lightGray": "#b0b0b0", @@ -27,19 +28,19 @@ "warning": "yellow", "muted": "mediumGray", "dim": "dimGray", - "text": "", + "text": "text", "thinkingText": "mediumGray", "selectedBg": "selectedBg", "userMessageBg": "userMsgBg", - "userMessageText": "", + "userMessageText": "text", "customMessageBg": "customMsgBg", - "customMessageText": "", + "customMessageText": "text", "customMessageLabel": "#7e57c2", "toolPendingBg": "toolPendingBg", "toolSuccessBg": "toolSuccessBg", "toolErrorBg": "toolErrorBg", - "toolTitle": "", + "toolTitle": "text", "toolOutput": "mediumGray", "mdHeading": "yellow", From f10cf57e96addf6ba8e2ed3e859e68a911923756 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 18 May 2026 18:28:08 +0200 Subject: [PATCH 110/166] feat(coding-agent): improve terminal theme detection --- .../src/modes/interactive/theme/theme.ts | 125 +++++++++++++++--- .../coding-agent/test/theme-detection.test.ts | 49 +++++++ 2 files changed, 159 insertions(+), 15 deletions(-) create mode 100644 packages/coding-agent/test/theme-detection.test.ts diff --git a/packages/coding-agent/src/modes/interactive/theme/theme.ts b/packages/coding-agent/src/modes/interactive/theme/theme.ts index 1ff111b2..77b34287 100644 --- a/packages/coding-agent/src/modes/interactive/theme/theme.ts +++ b/packages/coding-agent/src/modes/interactive/theme/theme.ts @@ -633,23 +633,118 @@ export function getThemeByName(name: string): Theme | undefined { } } -function detectTerminalBackground(): "dark" | "light" { - const colorfgbg = process.env.COLORFGBG || ""; - if (colorfgbg) { - const parts = colorfgbg.split(";"); - if (parts.length >= 2) { - const bg = parseInt(parts[1], 10); - if (!Number.isNaN(bg)) { - const result = bg < 8 ? "dark" : "light"; - return result; - } - } - } - return "dark"; +export type TerminalTheme = "dark" | "light"; + +export interface RgbColor { + r: number; + g: number; + b: number; } -function getDefaultTheme(): string { - return detectTerminalBackground(); +export interface TerminalThemeDetection { + theme: TerminalTheme; + source: "terminal background" | "COLORFGBG" | "fallback"; + detail: string; + confidence: "high" | "low"; +} + +export interface TerminalThemeDetectionOptions { + env?: NodeJS.ProcessEnv; +} + +function getColorFgBgBackgroundIndex(colorfgbg: string): number | undefined { + const parts = colorfgbg.split(";"); + for (let i = parts.length - 1; i >= 0; i--) { + const bg = parseInt(parts[i].trim(), 10); + if (Number.isInteger(bg) && bg >= 0 && bg <= 255) { + return bg; + } + } + return undefined; +} + +function getRgbColorLuminance({ r, g, b }: RgbColor): number { + const toLinear = (channel: number) => { + const value = channel / 255; + return value <= 0.03928 ? value / 12.92 : ((value + 0.055) / 1.055) ** 2.4; + }; + return 0.2126 * toLinear(r) + 0.7152 * toLinear(g) + 0.0722 * toLinear(b); +} + +function getAnsiColorLuminance(index: number): number { + return getRgbColorLuminance(hexToRgb(ansi256ToHex(index))); +} + +export function getThemeForRgbColor(rgb: RgbColor): TerminalTheme { + return getRgbColorLuminance(rgb) >= 0.5 ? "light" : "dark"; +} + +function parseOscHexChannel(channel: string): number | undefined { + if (!/^[0-9a-f]+$/i.test(channel)) { + return undefined; + } + const max = 16 ** channel.length - 1; + if (max <= 0) { + return undefined; + } + return Math.round((parseInt(channel, 16) / max) * 255); +} + +export function parseOsc11BackgroundColor(data: string): RgbColor | undefined { + const match = data.match(/^\x1b\]11;([^\x07\x1b]*)(?:\x07|\x1b\\)$/i); + if (!match) { + return undefined; + } + + const value = match[1].trim(); + if (value.startsWith("#")) { + const hex = value.slice(1); + if (/^[0-9a-f]{6}$/i.test(hex)) { + return hexToRgb(value); + } + if (/^[0-9a-f]{12}$/i.test(hex)) { + const r = parseOscHexChannel(hex.slice(0, 4)); + const g = parseOscHexChannel(hex.slice(4, 8)); + const b = parseOscHexChannel(hex.slice(8, 12)); + return r !== undefined && g !== undefined && b !== undefined ? { r, g, b } : undefined; + } + return undefined; + } + + const rgbValue = value.replace(/^rgba?:/i, ""); + const [red, green, blue] = rgbValue.split("/"); + if (red === undefined || green === undefined || blue === undefined) { + return undefined; + } + const r = parseOscHexChannel(red); + const g = parseOscHexChannel(green); + const b = parseOscHexChannel(blue); + return r !== undefined && g !== undefined && b !== undefined ? { r, g, b } : undefined; +} + +export function detectTerminalBackground(options: TerminalThemeDetectionOptions = {}): TerminalThemeDetection { + const env = options.env ?? process.env; + const colorfgbg = env.COLORFGBG || ""; + const bg = getColorFgBgBackgroundIndex(colorfgbg); + if (bg !== undefined) { + return { + theme: getAnsiColorLuminance(bg) >= 0.5 ? "light" : "dark", + source: "COLORFGBG", + detail: `background color index ${bg}`, + confidence: "high", + }; + } + + return { + theme: "dark", + source: "fallback", + detail: "no terminal background hint found", + confidence: "low", + }; +} + +export function getDefaultTheme(): string { + return detectTerminalBackground().theme; } // ============================================================================ diff --git a/packages/coding-agent/test/theme-detection.test.ts b/packages/coding-agent/test/theme-detection.test.ts new file mode 100644 index 00000000..119c6e61 --- /dev/null +++ b/packages/coding-agent/test/theme-detection.test.ts @@ -0,0 +1,49 @@ +import { describe, expect, it } from "vitest"; +import { + detectTerminalBackground, + getThemeForRgbColor, + parseOsc11BackgroundColor, +} from "../src/modes/interactive/theme/theme.js"; + +describe("detectTerminalBackground", () => { + it("uses the COLORFGBG background color index", () => { + expect(detectTerminalBackground({ env: { COLORFGBG: "0;15" } })).toMatchObject({ + theme: "light", + source: "COLORFGBG", + confidence: "high", + }); + expect(detectTerminalBackground({ env: { COLORFGBG: "15;0" } })).toMatchObject({ + theme: "dark", + source: "COLORFGBG", + confidence: "high", + }); + }); + + it("uses the last COLORFGBG field as the background", () => { + expect(detectTerminalBackground({ env: { COLORFGBG: "0;7;15" } }).theme).toBe("light"); + }); + + it("defaults to dark without terminal background hints", () => { + expect(detectTerminalBackground({ env: {} })).toMatchObject({ + theme: "dark", + source: "fallback", + confidence: "low", + }); + }); +}); + +describe("parseOsc11BackgroundColor", () => { + it("parses 16-bit OSC 11 rgb responses", () => { + expect(parseOsc11BackgroundColor("\x1b]11;rgb:0000/8000/ffff\x07")).toEqual({ r: 0, g: 128, b: 255 }); + }); + + it("parses OSC 11 hex responses", () => { + expect(parseOsc11BackgroundColor("\x1b]11;#ffffff\x1b\\")).toEqual({ r: 255, g: 255, b: 255 }); + expect(parseOsc11BackgroundColor("\x1b]11;#000000\x07")).toEqual({ r: 0, g: 0, b: 0 }); + }); + + it("classifies RGB colors by luminance", () => { + expect(getThemeForRgbColor({ r: 8, g: 8, b: 8 })).toBe("dark"); + expect(getThemeForRgbColor({ r: 250, g: 250, b: 250 })).toBe("light"); + }); +}); From dafcf61a34f74ba1ce90ed4f610121ffc68c755f Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 18 May 2026 21:56:48 +0200 Subject: [PATCH 111/166] fix: align theme truecolor detection --- .../src/modes/interactive/theme/theme.ts | 39 +++++-------------- .../coding-agent/test/theme-detection.test.ts | 24 +++++++++++- packages/tui/src/terminal-image.ts | 7 ++-- packages/tui/test/terminal-image.test.ts | 26 +++++++++++++ 4 files changed, 61 insertions(+), 35 deletions(-) diff --git a/packages/coding-agent/src/modes/interactive/theme/theme.ts b/packages/coding-agent/src/modes/interactive/theme/theme.ts index 77b34287..9fb8b031 100644 --- a/packages/coding-agent/src/modes/interactive/theme/theme.ts +++ b/packages/coding-agent/src/modes/interactive/theme/theme.ts @@ -1,6 +1,12 @@ import * as fs from "node:fs"; import * as path from "node:path"; -import type { EditorTheme, MarkdownTheme, SelectListTheme } from "@earendil-works/pi-tui"; +import { + type EditorTheme, + getCapabilities, + type MarkdownTheme, + type SelectListTheme, + type SettingsListTheme, +} from "@earendil-works/pi-tui"; import chalk from "chalk"; import { type Static, Type } from "typebox"; import { Compile } from "typebox/compile"; @@ -158,33 +164,6 @@ type ColorMode = "truecolor" | "256color"; // Color Utilities // ============================================================================ -function detectColorMode(): ColorMode { - const colorterm = process.env.COLORTERM; - if (colorterm === "truecolor" || colorterm === "24bit") { - return "truecolor"; - } - // Windows Terminal supports truecolor - if (process.env.WT_SESSION) { - return "truecolor"; - } - const term = process.env.TERM || ""; - // Fall back to 256color for truly limited terminals - if (term === "dumb" || term === "" || term === "linux") { - return "256color"; - } - // Terminal.app also doesn't support truecolor - if (process.env.TERM_PROGRAM === "Apple_Terminal") { - return "256color"; - } - // GNU screen doesn't support truecolor unless explicitly opted in via COLORTERM=truecolor. - // TERM under screen is typically "screen", "screen-256color", or "screen.xterm-256color". - if (term === "screen" || term.startsWith("screen-") || term.startsWith("screen.")) { - return "256color"; - } - // Assume truecolor for everything else - virtually all modern terminals support it - return "truecolor"; -} - function hexToRgb(hex: string): { r: number; g: number; b: number } { const cleaned = hex.replace("#", ""); if (cleaned.length !== 6) { @@ -585,7 +564,7 @@ function loadThemeJson(name: string): ThemeJson { } function createTheme(themeJson: ThemeJson, mode?: ColorMode, sourcePath?: string): Theme { - const colorMode = mode ?? detectColorMode(); + const colorMode = mode ?? (getCapabilities().trueColor ? "truecolor" : "256color"); const resolvedColors = resolveThemeColors(themeJson.colors, themeJson.vars); const fgColors: Record = {} as Record; const bgColors: Record = {} as Record; @@ -1227,7 +1206,7 @@ export function getEditorTheme(): EditorTheme { }; } -export function getSettingsListTheme(): import("@earendil-works/pi-tui").SettingsListTheme { +export function getSettingsListTheme(): SettingsListTheme { return { label: (text: string, selected: boolean) => (selected ? theme.fg("accent", text) : text), value: (text: string, selected: boolean) => (selected ? theme.fg("accent", text) : theme.fg("muted", text)), diff --git a/packages/coding-agent/test/theme-detection.test.ts b/packages/coding-agent/test/theme-detection.test.ts index 119c6e61..582358bb 100644 --- a/packages/coding-agent/test/theme-detection.test.ts +++ b/packages/coding-agent/test/theme-detection.test.ts @@ -1,10 +1,16 @@ -import { describe, expect, it } from "vitest"; +import { resetCapabilitiesCache, setCapabilities } from "@earendil-works/pi-tui"; +import { afterEach, describe, expect, it } from "vitest"; import { detectTerminalBackground, + getThemeByName, getThemeForRgbColor, parseOsc11BackgroundColor, } from "../src/modes/interactive/theme/theme.js"; +afterEach(() => { + resetCapabilitiesCache(); +}); + describe("detectTerminalBackground", () => { it("uses the COLORFGBG background color index", () => { expect(detectTerminalBackground({ env: { COLORFGBG: "0;15" } })).toMatchObject({ @@ -32,6 +38,22 @@ describe("detectTerminalBackground", () => { }); }); +describe("theme color mode", () => { + it("uses terminal capabilities", () => { + setCapabilities({ images: null, trueColor: false, hyperlinks: false }); + const ansi256Theme = getThemeByName("dark"); + if (!ansi256Theme) throw new Error("dark theme not found"); + expect(ansi256Theme.getColorMode()).toBe("256color"); + expect(ansi256Theme.getFgAnsi("accent")).toMatch(/^\x1b\[38;5;\d+m$/); + + setCapabilities({ images: null, trueColor: true, hyperlinks: false }); + const truecolorTheme = getThemeByName("dark"); + if (!truecolorTheme) throw new Error("dark theme not found"); + expect(truecolorTheme.getColorMode()).toBe("truecolor"); + expect(truecolorTheme.getFgAnsi("accent")).toMatch(/^\x1b\[38;2;\d+;\d+;\d+m$/); + }); +}); + describe("parseOsc11BackgroundColor", () => { it("parses 16-bit OSC 11 rgb responses", () => { expect(parseOsc11BackgroundColor("\x1b]11;rgb:0000/8000/ffff\x07")).toEqual({ r: 0, g: 128, b: 255 }); diff --git a/packages/tui/src/terminal-image.ts b/packages/tui/src/terminal-image.ts index b355f0f3..d6fe13bd 100644 --- a/packages/tui/src/terminal-image.ts +++ b/packages/tui/src/terminal-image.ts @@ -43,6 +43,7 @@ export function detectCapabilities(): TerminalCapabilities { const termProgram = process.env.TERM_PROGRAM?.toLowerCase() || ""; const term = process.env.TERM?.toLowerCase() || ""; const colorTerm = process.env.COLORTERM?.toLowerCase() || ""; + const hasTrueColorHint = colorTerm === "truecolor" || colorTerm === "24bit"; // tmux and screen swallow OSC 8 by default (passthrough is opt-in and wraps // sequences differently). Force hyperlinks off whenever we detect them, even @@ -50,8 +51,7 @@ export function detectCapabilities(): TerminalCapabilities { // also unreliable under tmux/screen, so leave `images: null` for safety. const inTmuxOrScreen = !!process.env.TMUX || term.startsWith("tmux") || term.startsWith("screen"); if (inTmuxOrScreen) { - const trueColor = colorTerm === "truecolor" || colorTerm === "24bit"; - return { images: null, trueColor, hyperlinks: false }; + return { images: null, trueColor: hasTrueColorHint, hyperlinks: false }; } if (process.env.KITTY_WINDOW_ID || termProgram === "kitty") { @@ -82,8 +82,7 @@ export function detectCapabilities(): TerminalCapabilities { // text" on terminals that swallow it, which means the URL disappears from // the rendered output. Default to the legacy `text (url)` behavior unless we // have positively identified a hyperlink-capable terminal above. - const trueColor = colorTerm === "truecolor" || colorTerm === "24bit"; - return { images: null, trueColor, hyperlinks: false }; + return { images: null, trueColor: hasTrueColorHint || !!process.env.WT_SESSION, hyperlinks: false }; } export function getCapabilities(): TerminalCapabilities { diff --git a/packages/tui/test/terminal-image.test.ts b/packages/tui/test/terminal-image.test.ts index 68c576bd..4f7c238b 100644 --- a/packages/tui/test/terminal-image.test.ts +++ b/packages/tui/test/terminal-image.test.ts @@ -27,6 +27,7 @@ const ENV_KEYS = [ "GHOSTTY_RESOURCES_DIR", "WEZTERM_PANE", "ITERM_SESSION_ID", + "WT_SESSION", "CMUX_WORKSPACE_ID", ] as const; @@ -271,6 +272,31 @@ describe("detectCapabilities", () => { assert.strictEqual(caps.hyperlinks, true); }); }); + + it("detects truecolor for Windows Terminal outside multiplexers", () => { + withEnv({ WT_SESSION: "session", TERM: "xterm-256color" }, () => { + const caps = detectCapabilities(); + assert.strictEqual(caps.trueColor, true); + }); + }); + + it("does not inherit Windows Terminal truecolor through tmux", () => { + withEnv({ WT_SESSION: "session", TMUX: "/tmp/tmux-1000/default,1234,0", TERM: "tmux-256color" }, () => { + const caps = detectCapabilities(); + assert.strictEqual(caps.trueColor, false); + assert.strictEqual(caps.hyperlinks, false); + assert.strictEqual(caps.images, null); + }); + }); + + it("trusts explicit truecolor hints through tmux", () => { + withEnv({ COLORTERM: "truecolor", TMUX: "/tmp/tmux-1000/default,1234,0", TERM: "tmux-256color" }, () => { + const caps = detectCapabilities(); + assert.strictEqual(caps.trueColor, true); + assert.strictEqual(caps.hyperlinks, false); + assert.strictEqual(caps.images, null); + }); + }); }); describe("Kitty image cursor movement", () => { From 93ecdbea3551d24e6ba375f82a1751eeedadb4a9 Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Mon, 18 May 2026 22:52:43 +0200 Subject: [PATCH 112/166] fix(coding-agent): improve subagent parallel summaries closes #4710 --- packages/coding-agent/CHANGELOG.md | 4 ++ .../examples/extensions/subagent/README.md | 3 + .../examples/extensions/subagent/index.ts | 60 +++++++++++++------ 3 files changed, 48 insertions(+), 19 deletions(-) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 280658eb..87dd1e88 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Fixed + +- Fixed the subagent extension's parallel mode to return useful per-task output and failed-task diagnostics to the parent model instead of 100-character previews ([#4710](https://github.com/earendil-works/pi/issues/4710)). + ## [0.75.3] - 2026-05-18 ### Fixed diff --git a/packages/coding-agent/examples/extensions/subagent/README.md b/packages/coding-agent/examples/extensions/subagent/README.md index 8599679f..f98b6458 100644 --- a/packages/coding-agent/examples/extensions/subagent/README.md +++ b/packages/coding-agent/examples/extensions/subagent/README.md @@ -113,6 +113,8 @@ Use a chain: first have scout find the read tool, then have planner suggest impr - Shows all tasks with live status (⏳ running, ✓ done, ✗ failed) - Updates as each task makes progress - Shows "2/3 done, 1 running" status +- Returns each completed task's final output to the parent model, capped at 50 KB per task +- Returns failure diagnostics from stderr/error messages when a child exits before producing output **Tool call formatting** (mimics built-in tools): - `$ command` for bash @@ -168,5 +170,6 @@ Project agents override user agents with the same name when `agentScope: "both"` ## Limitations - Output truncated to last 10 items in collapsed view (expand to see all) +- Parallel model-visible output is capped at 50 KB per task; full results remain in tool details - Agents discovered fresh on each invocation (allows editing mid-session) - Parallel mode limited to 8 tasks, 4 concurrent diff --git a/packages/coding-agent/examples/extensions/subagent/index.ts b/packages/coding-agent/examples/extensions/subagent/index.ts index a9b6da42..2853ad78 100644 --- a/packages/coding-agent/examples/extensions/subagent/index.ts +++ b/packages/coding-agent/examples/extensions/subagent/index.ts @@ -27,6 +27,7 @@ import { type AgentConfig, type AgentScope, discoverAgents } from "./agents.js"; const MAX_PARALLEL_TASKS = 8; const MAX_CONCURRENCY = 4; const COLLAPSED_ITEM_COUNT = 10; +const PER_TASK_OUTPUT_CAP = 50 * 1024; function formatTokens(count: number): string { if (count < 1000) return count.toString(); @@ -172,6 +173,28 @@ function getFinalOutput(messages: Message[]): string { return ""; } +function isFailedResult(result: SingleResult): boolean { + return result.exitCode !== 0 || result.stopReason === "error" || result.stopReason === "aborted"; +} + +function getResultOutput(result: SingleResult): string { + if (isFailedResult(result)) { + return result.errorMessage || result.stderr || getFinalOutput(result.messages) || "(no output)"; + } + return getFinalOutput(result.messages) || "(no output)"; +} + +function truncateParallelOutput(output: string): string { + const byteLength = Buffer.byteLength(output, "utf8"); + if (byteLength <= PER_TASK_OUTPUT_CAP) return output; + + let truncated = output.slice(0, PER_TASK_OUTPUT_CAP); + while (Buffer.byteLength(truncated, "utf8") > PER_TASK_OUTPUT_CAP) { + truncated = truncated.slice(0, -1); + } + return `${truncated}\n\n[Output truncated: ${byteLength - Buffer.byteLength(truncated, "utf8")} bytes omitted. Full output preserved in tool details.]`; +} + type DisplayItem = { type: "text"; text: string } | { type: "toolCall"; name: string; args: Record }; function getDisplayItems(messages: Message[]): DisplayItem[] { @@ -534,11 +557,9 @@ export default function (pi: ExtensionAPI) { ); results.push(result); - const isError = - result.exitCode !== 0 || result.stopReason === "error" || result.stopReason === "aborted"; + const isError = isFailedResult(result); if (isError) { - const errorMsg = - result.errorMessage || result.stderr || getFinalOutput(result.messages) || "(no output)"; + const errorMsg = getResultOutput(result); return { content: [{ type: "text", text: `Chain stopped at step ${i + 1} (${step.agent}): ${errorMsg}` }], details: makeDetails("chain")(results), @@ -617,17 +638,19 @@ export default function (pi: ExtensionAPI) { return result; }); - const successCount = results.filter((r) => r.exitCode === 0).length; + const successCount = results.filter((r) => !isFailedResult(r)).length; const summaries = results.map((r) => { - const output = getFinalOutput(r.messages); - const preview = output.slice(0, 100) + (output.length > 100 ? "..." : ""); - return `[${r.agent}] ${r.exitCode === 0 ? "completed" : "failed"}: ${preview || "(no output)"}`; + const output = truncateParallelOutput(getResultOutput(r)); + const status = isFailedResult(r) + ? `failed${r.stopReason && r.stopReason !== "end" ? ` (${r.stopReason})` : ""}` + : "completed"; + return `### [${r.agent}] ${status}\n\n${output}`; }); return { content: [ { type: "text", - text: `Parallel: ${successCount}/${results.length} succeeded\n\n${summaries.join("\n\n")}`, + text: `Parallel: ${successCount}/${results.length} succeeded\n\n${summaries.join("\n\n---\n\n")}`, }, ], details: makeDetails("parallel")(results), @@ -646,10 +669,9 @@ export default function (pi: ExtensionAPI) { onUpdate, makeDetails("single"), ); - const isError = result.exitCode !== 0 || result.stopReason === "error" || result.stopReason === "aborted"; + const isError = isFailedResult(result); if (isError) { - const errorMsg = - result.errorMessage || result.stderr || getFinalOutput(result.messages) || "(no output)"; + const errorMsg = getResultOutput(result); return { content: [{ type: "text", text: `Agent ${result.stopReason || "failed"}: ${errorMsg}` }], details: makeDetails("single")([result]), @@ -740,7 +762,7 @@ export default function (pi: ExtensionAPI) { if (details.mode === "single" && details.results.length === 1) { const r = details.results[0]; - const isError = r.exitCode !== 0 || r.stopReason === "error" || r.stopReason === "aborted"; + const isError = isFailedResult(r); const icon = isError ? theme.fg("error", "✗") : theme.fg("success", "✓"); const displayItems = getDisplayItems(r.messages); const finalOutput = getFinalOutput(r.messages); @@ -893,8 +915,8 @@ export default function (pi: ExtensionAPI) { if (details.mode === "parallel") { const running = details.results.filter((r) => r.exitCode === -1).length; - const successCount = details.results.filter((r) => r.exitCode === 0).length; - const failCount = details.results.filter((r) => r.exitCode > 0).length; + const successCount = details.results.filter((r) => r.exitCode !== -1 && !isFailedResult(r)).length; + const failCount = details.results.filter((r) => r.exitCode !== -1 && isFailedResult(r)).length; const isRunning = running > 0; const icon = isRunning ? theme.fg("warning", "⏳") @@ -916,7 +938,7 @@ export default function (pi: ExtensionAPI) { ); for (const r of details.results) { - const rIcon = r.exitCode === 0 ? theme.fg("success", "✓") : theme.fg("error", "✗"); + const rIcon = isFailedResult(r) ? theme.fg("error", "✗") : theme.fg("success", "✓"); const displayItems = getDisplayItems(r.messages); const finalOutput = getFinalOutput(r.messages); @@ -963,9 +985,9 @@ export default function (pi: ExtensionAPI) { const rIcon = r.exitCode === -1 ? theme.fg("warning", "⏳") - : r.exitCode === 0 - ? theme.fg("success", "✓") - : theme.fg("error", "✗"); + : isFailedResult(r) + ? theme.fg("error", "✗") + : theme.fg("success", "✓"); const displayItems = getDisplayItems(r.messages); text += `\n\n${theme.fg("muted", "─── ")}${theme.fg("accent", r.agent)} ${rIcon}`; if (displayItems.length === 0) From b1893b3d7720273c1c2289dde88cf22e6c1a83aa Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Mon, 18 May 2026 23:05:34 +0200 Subject: [PATCH 113/166] fix(agent): preserve oversized tail output with trailing newline closes #4715 --- packages/agent/CHANGELOG.md | 4 ++++ packages/agent/src/harness/utils/truncate.ts | 1 + packages/agent/test/harness/truncate.test.ts | 11 +++++++++++ 3 files changed, 16 insertions(+) diff --git a/packages/agent/CHANGELOG.md b/packages/agent/CHANGELOG.md index 7c451eb0..44fa99ee 100644 --- a/packages/agent/CHANGELOG.md +++ b/packages/agent/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Fixed + +- Fixed tail truncation for oversized single-line output that ends with a trailing newline ([#4715](https://github.com/earendil-works/pi/issues/4715)). + ## [0.75.3] - 2026-05-18 ## [0.75.2] - 2026-05-18 diff --git a/packages/agent/src/harness/utils/truncate.ts b/packages/agent/src/harness/utils/truncate.ts index 1870f423..2372a941 100644 --- a/packages/agent/src/harness/utils/truncate.ts +++ b/packages/agent/src/harness/utils/truncate.ts @@ -218,6 +218,7 @@ export function truncateTail(content: string, options: TruncationOptions = {}): const totalBytes = utf8ByteLength(content); const lines = content.split("\n"); + if (lines.length > 1 && lines[lines.length - 1] === "") lines.pop(); const totalLines = lines.length; // Check if no truncation needed diff --git a/packages/agent/test/harness/truncate.test.ts b/packages/agent/test/harness/truncate.test.ts index a859b321..f712cc2c 100644 --- a/packages/agent/test/harness/truncate.test.ts +++ b/packages/agent/test/harness/truncate.test.ts @@ -102,6 +102,17 @@ describe("truncate utilities", () => { expect(result.outputBytes).toBe(5); }); + it("truncates an oversized single line with a trailing newline", () => { + const input = `${"X".repeat(300_000)}\n`; + const result = truncateTail(input, { maxBytes: 1024, maxLines: 100 }); + + expect(result.content).toBe("X".repeat(1024)); + expect(result.outputBytes).toBe(1024); + expect(result.outputLines).toBe(1); + expect(result.lastLinePartial).toBe(true); + expect(result.truncatedBy).toBe("bytes"); + }); + it("drops an oversized trailing character when it cannot fit in tail byte limit", () => { const result = truncateTail("abc🙂", { maxBytes: 3, maxLines: 10 }); From 29b3417a7a6d87a0db48841ce5f316f9e9711352 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 18 May 2026 23:20:22 +0200 Subject: [PATCH 114/166] fix(windows): hide bash helper consoles closes #4699 --- packages/agent/CHANGELOG.md | 1 + packages/agent/src/harness/env/nodejs.ts | 7 ++++++- packages/coding-agent/CHANGELOG.md | 1 + packages/coding-agent/src/core/tools/bash.ts | 1 + packages/coding-agent/src/utils/shell.ts | 7 ++++++- 5 files changed, 15 insertions(+), 2 deletions(-) diff --git a/packages/agent/CHANGELOG.md b/packages/agent/CHANGELOG.md index 44fa99ee..7c04cc4c 100644 --- a/packages/agent/CHANGELOG.md +++ b/packages/agent/CHANGELOG.md @@ -5,6 +5,7 @@ ### Fixed - Fixed tail truncation for oversized single-line output that ends with a trailing newline ([#4715](https://github.com/earendil-works/pi/issues/4715)). +- Fixed Windows Node execution environment command spawns to hide helper console windows from background processes ([#4699](https://github.com/earendil-works/pi/issues/4699)). ## [0.75.3] - 2026-05-18 diff --git a/packages/agent/src/harness/env/nodejs.ts b/packages/agent/src/harness/env/nodejs.ts index b86f6fe8..931651bc 100644 --- a/packages/agent/src/harness/env/nodejs.ts +++ b/packages/agent/src/harness/env/nodejs.ts @@ -108,7 +108,10 @@ async function runCommand( let stdout = ""; let child: ReturnType; try { - child = spawn(command, args, { stdio: ["ignore", "pipe", "ignore"] }); + child = spawn(command, args, { + stdio: ["ignore", "pipe", "ignore"], + windowsHide: true, + }); } catch { resolve({ stdout: "", status: null }); return; @@ -192,6 +195,7 @@ function killProcessTree(pid: number): void { spawn("taskkill", ["/F", "/T", "/PID", String(pid)], { stdio: "ignore", detached: true, + windowsHide: true, }); } catch { // Ignore errors. @@ -275,6 +279,7 @@ export class NodeExecutionEnv implements ExecutionEnv { detached: process.platform !== "win32", env: getShellEnv(this.shellEnv, options?.env), stdio: ["ignore", "pipe", "pipe"], + windowsHide: true, }); } catch (error) { const cause = toError(error); diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 87dd1e88..7ff38a6a 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -5,6 +5,7 @@ ### Fixed - Fixed the subagent extension's parallel mode to return useful per-task output and failed-task diagnostics to the parent model instead of 100-character previews ([#4710](https://github.com/earendil-works/pi/issues/4710)). +- Fixed Windows local bash execution to hide helper console windows when launched from background SDK processes ([#4699](https://github.com/earendil-works/pi/issues/4699)). ## [0.75.3] - 2026-05-18 diff --git a/packages/coding-agent/src/core/tools/bash.ts b/packages/coding-agent/src/core/tools/bash.ts index 1d6591d4..20728500 100644 --- a/packages/coding-agent/src/core/tools/bash.ts +++ b/packages/coding-agent/src/core/tools/bash.ts @@ -76,6 +76,7 @@ export function createLocalBashOperations(options?: { shellPath?: string }): Bas detached: process.platform !== "win32", env: env ?? getShellEnv(), stdio: ["ignore", "pipe", "pipe"], + windowsHide: true, }); if (child.pid) trackDetachedChildPid(child.pid); let timedOut = false; diff --git a/packages/coding-agent/src/utils/shell.ts b/packages/coding-agent/src/utils/shell.ts index 1c235802..7e3cc8ef 100644 --- a/packages/coding-agent/src/utils/shell.ts +++ b/packages/coding-agent/src/utils/shell.ts @@ -15,7 +15,11 @@ function findBashOnPath(): string | null { if (process.platform === "win32") { // Windows: Use 'where' and verify file exists (where can return non-existent paths) try { - const result = spawnSync("where", ["bash.exe"], { encoding: "utf-8", timeout: 5000 }); + const result = spawnSync("where", ["bash.exe"], { + encoding: "utf-8", + timeout: 5000, + windowsHide: true, + }); if (result.status === 0 && result.stdout) { const firstMatch = result.stdout.trim().split(/\r?\n/)[0]; if (firstMatch && existsSync(firstMatch)) { @@ -190,6 +194,7 @@ export function killProcessTree(pid: number): void { spawn("taskkill", ["/F", "/T", "/PID", String(pid)], { stdio: "ignore", detached: true, + windowsHide: true, }); } catch { // Ignore errors if taskkill fails From 6a21a52d910eae0d109b6db066781c2d9c0bbc0a Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Mon, 18 May 2026 23:50:22 +0200 Subject: [PATCH 115/166] docs: add ad-hoc script guidance --- AGENTS.md | 1 + 1 file changed, 1 insertion(+) diff --git a/AGENTS.md b/AGENTS.md index e3880791..40438dc4 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -32,6 +32,7 @@ - When writing tests, run them, identify issues in either the test or implementation, and iterate until fixed. - For `packages/coding-agent/test/suite/`, use `test/suite/harness.ts` plus the faux provider. Do not use real provider APIs, real API keys, or paid tokens. - Put issue-specific regressions under `packages/coding-agent/test/suite/regressions/` and name them `-.test.ts`. +- For ad-hoc scripts, write the script to a temporary file (for example under `/tmp`) using `write`, run that file, edit it if needed, and remove it when it is no longer needed. Do not embed multi-line scripts directly in `bash` commands. - NEVER commit unless user asks ## Contribution Gate From 32bcdc9739d4b806c209a584c6c81a7a22366482 Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Tue, 19 May 2026 09:52:58 +0200 Subject: [PATCH 116/166] fix(coding-agent): simplify agent session settlement --- packages/coding-agent/CHANGELOG.md | 1 + .../coding-agent/src/core/agent-session.ts | 236 ++++++------------ .../agent-session-bash-persistence.test.ts | 4 +- .../suite/agent-session-compaction.test.ts | 24 +- ...113-agent-session-event-settlement.test.ts | 95 +++++++ 5 files changed, 187 insertions(+), 173 deletions(-) create mode 100644 packages/coding-agent/test/suite/regressions/1717-2113-agent-session-event-settlement.test.ts diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 7ff38a6a..147b3b19 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -4,6 +4,7 @@ ### Fixed +- Fixed AgentSession retry, compaction, and event settlement to use the awaited agent lifecycle instead of a separate event queue. - Fixed the subagent extension's parallel mode to return useful per-task output and failed-task diagnostics to the parent model instead of 100-character previews ([#4710](https://github.com/earendil-works/pi/issues/4710)). - Fixed Windows local bash execution to hide helper console windows when launched from background SDK processes ([#4699](https://github.com/earendil-works/pi/issues/4699)). diff --git a/packages/coding-agent/src/core/agent-session.ts b/packages/coding-agent/src/core/agent-session.ts index 1d51163b..2f5b805d 100644 --- a/packages/coding-agent/src/core/agent-session.ts +++ b/packages/coding-agent/src/core/agent-session.ts @@ -252,7 +252,6 @@ export class AgentSession { // Event subscription state private _unsubscribeAgent?: () => void; private _eventListeners: AgentSessionEventListener[] = []; - private _agentEventQueue: Promise = Promise.resolve(); /** Tracks pending steering messages for UI display. Removed when delivered. */ private _steeringMessages: string[] = []; @@ -272,8 +271,6 @@ export class AgentSession { // Retry state private _retryAbortController: AbortController | undefined = undefined; private _retryAttempt = 0; - private _retryPromise: Promise | undefined = undefined; - private _retryResolve: (() => void) | undefined = undefined; // Bash execution state private _bashAbortController: AbortController | undefined = undefined; @@ -395,8 +392,6 @@ export class AgentSession { return undefined; } - await this._agentEventQueue; - try { return await runner.emitToolCall({ type: "tool_call", @@ -463,54 +458,7 @@ export class AgentSession { private _lastAssistantMessage: AssistantMessage | undefined = undefined; /** Internal handler for agent events - shared by subscribe and reconnect */ - private _handleAgentEvent = (event: AgentEvent): void => { - // Create retry promise synchronously before queueing async processing. - // Agent.emit() calls this handler synchronously, and prompt() calls waitForRetry() - // as soon as agent.prompt() resolves. If _retryPromise is created only inside - // _processAgentEvent, slow earlier queued events can delay agent_end processing - // and waitForRetry() can miss the in-flight retry. - this._createRetryPromiseForAgentEnd(event); - - this._agentEventQueue = this._agentEventQueue.then( - () => this._processAgentEvent(event), - () => this._processAgentEvent(event), - ); - - // Keep queue alive if an event handler fails - this._agentEventQueue.catch(() => {}); - }; - - private _createRetryPromiseForAgentEnd(event: AgentEvent): void { - if (event.type !== "agent_end" || this._retryPromise) { - return; - } - - const settings = this.settingsManager.getRetrySettings(); - if (!settings.enabled) { - return; - } - - const lastAssistant = this._findLastAssistantInMessages(event.messages); - if (!lastAssistant || !this._isRetryableError(lastAssistant)) { - return; - } - - this._retryPromise = new Promise((resolve) => { - this._retryResolve = resolve; - }); - } - - private _findLastAssistantInMessages(messages: AgentMessage[]): AssistantMessage | undefined { - for (let i = messages.length - 1; i >= 0; i--) { - const message = messages[i]; - if (message.role === "assistant") { - return message as AssistantMessage; - } - } - return undefined; - } - - private async _processAgentEvent(event: AgentEvent): Promise { + private _handleAgentEvent = async (event: AgentEvent): Promise => { // When a user message starts, check if it's from either queue and remove it BEFORE emitting // This ensures the UI sees the updated queue state if (event.type === "message_start" && event.message.role === "user") { @@ -581,31 +529,7 @@ export class AgentSession { } } } - - // Check auto-retry and auto-compaction after agent completes - if (event.type === "agent_end" && this._lastAssistantMessage) { - const msg = this._lastAssistantMessage; - this._lastAssistantMessage = undefined; - - // Check for retryable errors first (overloaded, rate limit, server errors) - if (this._isRetryableError(msg)) { - const didRetry = await this._handleRetryableError(msg); - if (didRetry) return; // Retry was initiated, don't proceed to compaction - } - - this._resolveRetry(); - await this._checkCompaction(msg); - } - } - - /** Resolve the pending retry promise */ - private _resolveRetry(): void { - if (this._retryResolve) { - this._retryResolve(); - this._retryResolve = undefined; - this._retryPromise = undefined; - } - } + }; /** Extract text content from a message */ private _getUserMessageText(message: Message): string { @@ -630,7 +554,7 @@ export class AgentSession { private _replaceMessageInPlace(target: AgentMessage, replacement: AgentMessage): void { // Agent-core stores the finalized message object in its state before emitting message_end. - // SessionManager persistence happens later in _processAgentEvent() with event.message. + // SessionManager persistence happens later in _handleAgentEvent() with event.message. // Mutating this object in place keeps agent state, later turn/agent events, listeners, // and the eventual SessionManager.appendMessage(event.message) persistence in sync. if (target === replacement) { @@ -968,6 +892,41 @@ export class AgentSession { // Prompting // ========================================================================= + private async _runAgentPrompt(messages: AgentMessage | AgentMessage[]): Promise { + try { + await this.agent.prompt(messages); + while (await this._handlePostAgentRun()) { + await this.agent.continue(); + } + } finally { + this._flushPendingBashMessages(); + } + } + + private async _handlePostAgentRun(): Promise { + const msg = this._lastAssistantMessage; + this._lastAssistantMessage = undefined; + if (!msg) { + return false; + } + + if (this._isRetryableError(msg) && (await this._prepareRetry(msg))) { + return true; + } + + if (msg.stopReason === "error" && this._retryAttempt > 0) { + this._emit({ + type: "auto_retry_end", + success: false, + attempt: this._retryAttempt, + finalError: msg.errorMessage, + }); + this._retryAttempt = 0; + } + + return await this._checkCompaction(msg); + } + /** * Send a prompt to the agent. * - Handles extension commands (registered via pi.registerCommand) immediately, even during streaming @@ -1058,8 +1017,15 @@ export class AgentSession { // Check if we need to compact before sending (catches aborted responses) const lastAssistant = this._findLastAssistantMessage(); - if (lastAssistant) { - await this._checkCompaction(lastAssistant, false); + if (lastAssistant && (await this._checkCompaction(lastAssistant, false))) { + try { + await this.agent.continue(); + while (await this._handlePostAgentRun()) { + await this.agent.continue(); + } + } finally { + this._flushPendingBashMessages(); + } } // Build messages array (custom message if any, then user message) @@ -1119,8 +1085,7 @@ export class AgentSession { } preflightResult?.(true); - await this.agent.prompt(messages); - await this.waitForRetry(); + await this._runAgentPrompt(messages); } /** @@ -1306,7 +1271,7 @@ export class AgentSession { this.agent.steer(appMessage); } } else if (options?.triggerTurn) { - await this.agent.prompt(appMessage); + await this._runAgentPrompt(appMessage); } else { this.agent.state.messages.push(appMessage); this.sessionManager.appendCustomMessageEntry( @@ -1777,12 +1742,12 @@ export class AgentSession { * @param assistantMessage The assistant message to check * @param skipAbortedCheck If false, include aborted messages (for pre-prompt check). Default: true */ - private async _checkCompaction(assistantMessage: AssistantMessage, skipAbortedCheck = true): Promise { + private async _checkCompaction(assistantMessage: AssistantMessage, skipAbortedCheck = true): Promise { const settings = this.settingsManager.getCompactionSettings(); - if (!settings.enabled) return; + if (!settings.enabled) return false; // Skip if message was aborted (user cancelled) - unless skipAbortedCheck is false - if (skipAbortedCheck && assistantMessage.stopReason === "aborted") return; + if (skipAbortedCheck && assistantMessage.stopReason === "aborted") return false; const contextWindow = this.model?.contextWindow ?? 0; @@ -1800,7 +1765,7 @@ export class AgentSession { const assistantIsFromBeforeCompaction = compactionEntry !== null && assistantMessage.timestamp <= new Date(compactionEntry.timestamp).getTime(); if (assistantIsFromBeforeCompaction) { - return; + return false; } // Case 1: Overflow - LLM returned context overflow error @@ -1815,7 +1780,7 @@ export class AgentSession { errorMessage: "Context overflow recovery failed after one compact-and-retry attempt. Try reducing context or switching to a larger-context model.", }); - return; + return false; } this._overflowRecoveryAttempted = true; @@ -1825,8 +1790,7 @@ export class AgentSession { if (messages.length > 0 && messages[messages.length - 1].role === "assistant") { this.agent.state.messages = messages.slice(0, -1); } - await this._runAutoCompaction("overflow", true); - return; + return await this._runAutoCompaction("overflow", true); } // Case 2: Threshold - context is getting large @@ -1836,7 +1800,7 @@ export class AgentSession { if (assistantMessage.stopReason === "error") { const messages = this.agent.state.messages; const estimate = estimateContextTokens(messages); - if (estimate.lastUsageIndex === null) return; // No usage data at all + if (estimate.lastUsageIndex === null) return false; // No usage data at all // Verify the usage source is post-compaction. Kept pre-compaction messages // have stale usage reflecting the old (larger) context and would falsely // trigger compaction right after one just finished. @@ -1846,21 +1810,22 @@ export class AgentSession { usageMsg.role === "assistant" && (usageMsg as AssistantMessage).timestamp <= new Date(compactionEntry.timestamp).getTime() ) { - return; + return false; } contextTokens = estimate.tokens; } else { contextTokens = calculateContextTokens(assistantMessage.usage); } if (shouldCompact(contextTokens, contextWindow, settings)) { - await this._runAutoCompaction("threshold", false); + return await this._runAutoCompaction("threshold", false); } + return false; } /** * Internal: Run auto-compaction with events. */ - private async _runAutoCompaction(reason: "overflow" | "threshold", willRetry: boolean): Promise { + private async _runAutoCompaction(reason: "overflow" | "threshold", willRetry: boolean): Promise { const settings = this.settingsManager.getCompactionSettings(); this._emit({ type: "compaction_start", reason }); @@ -1875,7 +1840,7 @@ export class AgentSession { aborted: false, willRetry: false, }); - return; + return false; } let apiKey: string | undefined; @@ -1890,7 +1855,7 @@ export class AgentSession { aborted: false, willRetry: false, }); - return; + return false; } apiKey = authResult.apiKey; headers = authResult.headers; @@ -1909,7 +1874,7 @@ export class AgentSession { aborted: false, willRetry: false, }); - return; + return false; } let extensionCompaction: CompactionResult | undefined; @@ -1932,7 +1897,7 @@ export class AgentSession { aborted: true, willRetry: false, }); - return; + return false; } if (extensionResult?.compaction) { @@ -1978,7 +1943,7 @@ export class AgentSession { aborted: true, willRetry: false, }); - return; + return false; } this.sessionManager.appendCompaction(summary, firstKeptEntryId, tokensBefore, details, fromExtension); @@ -2013,17 +1978,12 @@ export class AgentSession { if (lastMsg?.role === "assistant" && (lastMsg as AssistantMessage).stopReason === "error") { this.agent.state.messages = messages.slice(0, -1); } - - setTimeout(() => { - this.agent.continue().catch(() => {}); - }, 100); - } else if (this.agent.hasQueuedMessages()) { - // Auto-compaction can complete while follow-up/steering/custom messages are waiting. - // Kick the loop so queued messages are actually delivered. - setTimeout(() => { - this.agent.continue().catch(() => {}); - }, 100); + return true; } + + // Auto-compaction can complete while follow-up/steering/custom messages are waiting. + // Continue once so queued messages are delivered. + return this.agent.hasQueuedMessages(); } catch (error) { const errorMessage = error instanceof Error ? error.message : "compaction failed"; this._emit({ @@ -2037,6 +1997,7 @@ export class AgentSession { ? `Context overflow recovery failed: ${errorMessage}` : `Auto-compaction failed: ${errorMessage}`, }); + return false; } finally { this._autoCompactionAbortController = undefined; } @@ -2448,36 +2409,20 @@ export class AgentSession { } /** - * Handle retryable errors with exponential backoff. - * @returns true if retry was initiated, false if max retries exceeded or disabled + * Prepare a retryable error for continuation with exponential backoff. + * @returns true if the caller should continue the agent, false otherwise */ - private async _handleRetryableError(message: AssistantMessage): Promise { + private async _prepareRetry(message: AssistantMessage): Promise { const settings = this.settingsManager.getRetrySettings(); if (!settings.enabled) { - this._resolveRetry(); return false; } - // Retry promise is created synchronously in _handleAgentEvent for agent_end. - // Keep a defensive fallback here in case a future refactor bypasses that path. - if (!this._retryPromise) { - this._retryPromise = new Promise((resolve) => { - this._retryResolve = resolve; - }); - } - this._retryAttempt++; if (this._retryAttempt > settings.maxRetries) { - // Max retries exceeded, emit final failure and reset - this._emit({ - type: "auto_retry_end", - success: false, - attempt: this._retryAttempt - 1, - finalError: message.errorMessage, - }); - this._retryAttempt = 0; - this._resolveRetry(); // Resolve so waitForRetry() completes + // Preserve the completed attempt count so post-run handling can emit the final failure. + this._retryAttempt--; return false; } @@ -2505,24 +2450,16 @@ export class AgentSession { // Aborted during sleep - emit end event so UI can clean up const attempt = this._retryAttempt; this._retryAttempt = 0; - this._retryAbortController = undefined; this._emit({ type: "auto_retry_end", success: false, attempt, finalError: "Retry cancelled", }); - this._resolveRetry(); return false; + } finally { + this._retryAbortController = undefined; } - this._retryAbortController = undefined; - - // Retry via continue() - use setTimeout to break out of event handler chain - setTimeout(() => { - this.agent.continue().catch(() => { - // Retry failed - will be caught by next agent_end - }); - }, 0); return true; } @@ -2532,26 +2469,11 @@ export class AgentSession { */ abortRetry(): void { this._retryAbortController?.abort(); - // Note: _retryAttempt is reset in the catch block of _autoRetry - this._resolveRetry(); - } - - /** - * Wait for any in-progress retry to complete. - * Returns immediately if no retry is in progress. - */ - private async waitForRetry(): Promise { - if (!this._retryPromise) { - return; - } - - await this._retryPromise; - await this.agent.waitForIdle(); } /** Whether auto-retry is currently in progress */ get isRetrying(): boolean { - return this._retryPromise !== undefined; + return this._retryAbortController !== undefined; } /** Whether auto-retry is enabled */ diff --git a/packages/coding-agent/test/suite/agent-session-bash-persistence.test.ts b/packages/coding-agent/test/suite/agent-session-bash-persistence.test.ts index a597903c..f59146b4 100644 --- a/packages/coding-agent/test/suite/agent-session-bash-persistence.test.ts +++ b/packages/coding-agent/test/suite/agent-session-bash-persistence.test.ts @@ -85,8 +85,8 @@ describe("AgentSession bash and persistence characterization", () => { releaseToolExecution?.(); await firstPrompt; - expect(harness.session.hasPendingBashMessages).toBe(true); - expect(harness.session.messages.some((message) => message.role === "bashExecution")).toBe(false); + expect(harness.session.hasPendingBashMessages).toBe(false); + expect(harness.session.messages.some((message) => message.role === "bashExecution")).toBe(true); await harness.session.prompt("next turn"); diff --git a/packages/coding-agent/test/suite/agent-session-compaction.test.ts b/packages/coding-agent/test/suite/agent-session-compaction.test.ts index e2d28911..8ca5d0e5 100644 --- a/packages/coding-agent/test/suite/agent-session-compaction.test.ts +++ b/packages/coding-agent/test/suite/agent-session-compaction.test.ts @@ -8,8 +8,8 @@ import { afterEach, describe, expect, it, vi } from "vitest"; import { createHarness, type Harness } from "./harness.js"; type SessionWithCompactionInternals = { - _checkCompaction: (assistantMessage: AssistantMessage, skipAbortedCheck?: boolean) => Promise; - _runAutoCompaction: (reason: "overflow" | "threshold", willRetry: boolean) => Promise; + _checkCompaction: (assistantMessage: AssistantMessage, skipAbortedCheck?: boolean) => Promise; + _runAutoCompaction: (reason: "overflow" | "threshold", willRetry: boolean) => Promise; }; function createUsage(totalTokens: number) { @@ -217,13 +217,9 @@ describe("AgentSession compaction characterization", () => { timestamp: Date.now(), }); - const continueSpy = vi.spyOn(harness.session.agent, "continue").mockResolvedValue(); const sessionInternals = harness.session as unknown as SessionWithCompactionInternals; - await sessionInternals._runAutoCompaction("threshold", false); - await vi.advanceTimersByTimeAsync(100); - - expect(continueSpy).toHaveBeenCalledTimes(1); + await expect(sessionInternals._runAutoCompaction("threshold", false)).resolves.toBe(true); }); it("does not retry overflow recovery more than once", async () => { @@ -235,7 +231,7 @@ describe("AgentSession compaction characterization", () => { errorMessage: "prompt is too long", timestamp: Date.now(), }); - const runAutoCompactionSpy = vi.spyOn(sessionInternals, "_runAutoCompaction").mockResolvedValue(); + const runAutoCompactionSpy = vi.spyOn(sessionInternals, "_runAutoCompaction").mockResolvedValue(false); const compactionErrors: string[] = []; harness.session.subscribe((event) => { if (event.type === "compaction_end" && event.errorMessage) { @@ -283,7 +279,7 @@ describe("AgentSession compaction characterization", () => { timestamp: Date.now(), }); - const runAutoCompactionSpy = vi.spyOn(sessionInternals, "_runAutoCompaction").mockResolvedValue(); + const runAutoCompactionSpy = vi.spyOn(sessionInternals, "_runAutoCompaction").mockResolvedValue(false); await sessionInternals._checkCompaction(staleAssistant, false); @@ -311,7 +307,7 @@ describe("AgentSession compaction characterization", () => { errorAssistant, ]; - const runAutoCompactionSpy = vi.spyOn(sessionInternals, "_runAutoCompaction").mockResolvedValue(); + const runAutoCompactionSpy = vi.spyOn(sessionInternals, "_runAutoCompaction").mockResolvedValue(false); await sessionInternals._checkCompaction(errorAssistant); @@ -332,7 +328,7 @@ describe("AgentSession compaction characterization", () => { errorAssistant, ]; - const runAutoCompactionSpy = vi.spyOn(sessionInternals, "_runAutoCompaction").mockResolvedValue(); + const runAutoCompactionSpy = vi.spyOn(sessionInternals, "_runAutoCompaction").mockResolvedValue(false); await sessionInternals._checkCompaction(errorAssistant); @@ -377,7 +373,7 @@ describe("AgentSession compaction characterization", () => { errorAssistant, ]; - const runAutoCompactionSpy = vi.spyOn(sessionInternals, "_runAutoCompaction").mockResolvedValue(); + const runAutoCompactionSpy = vi.spyOn(sessionInternals, "_runAutoCompaction").mockResolvedValue(false); await sessionInternals._checkCompaction(errorAssistant); @@ -395,8 +391,8 @@ describe("AgentSession compaction characterization", () => { const belowThresholdInternals = belowThresholdHarness.session as unknown as SessionWithCompactionInternals; const disabledInternals = disabledHarness.session as unknown as SessionWithCompactionInternals; - const belowThresholdSpy = vi.spyOn(belowThresholdInternals, "_runAutoCompaction").mockResolvedValue(); - const disabledSpy = vi.spyOn(disabledInternals, "_runAutoCompaction").mockResolvedValue(); + const belowThresholdSpy = vi.spyOn(belowThresholdInternals, "_runAutoCompaction").mockResolvedValue(false); + const disabledSpy = vi.spyOn(disabledInternals, "_runAutoCompaction").mockResolvedValue(false); await belowThresholdInternals._checkCompaction( createAssistant(belowThresholdHarness, { stopReason: "stop", totalTokens: 1_000, timestamp: Date.now() }), diff --git a/packages/coding-agent/test/suite/regressions/1717-2113-agent-session-event-settlement.test.ts b/packages/coding-agent/test/suite/regressions/1717-2113-agent-session-event-settlement.test.ts new file mode 100644 index 00000000..bea38c9e --- /dev/null +++ b/packages/coding-agent/test/suite/regressions/1717-2113-agent-session-event-settlement.test.ts @@ -0,0 +1,95 @@ +import type { AgentTool } from "@earendil-works/pi-agent-core"; +import { fauxAssistantMessage, fauxToolCall } from "@earendil-works/pi-ai"; +import { Type } from "typebox"; +import { afterEach, describe, expect, it } from "vitest"; +import { createHarness, type Harness } from "../harness.js"; + +function createEchoTool(): AgentTool { + return { + name: "echo", + label: "Echo", + description: "Echo text back", + parameters: Type.Object({ text: Type.String() }), + execute: async (_toolCallId, params) => { + const text = typeof params === "object" && params !== null && "text" in params ? String(params.text) : ""; + return { content: [{ type: "text", text }], details: { text } }; + }, + }; +} + +describe("regressions #1717/#2113: agent session event settlement", () => { + const harnesses: Harness[] = []; + + afterEach(() => { + while (harnesses.length > 0) { + harnesses.pop()?.cleanup(); + } + }); + + it("keeps persisted assistant/toolResult message order when extension message_end handlers yield", async () => { + const harness = await createHarness({ + tools: [createEchoTool()], + extensionFactories: [ + (pi) => { + pi.on("message_end", async (event) => { + if (event.message.role === "assistant") { + await new Promise((resolve) => setTimeout(resolve, 20)); + } + }); + }, + ], + }); + harnesses.push(harness); + harness.setResponses([ + fauxAssistantMessage([fauxToolCall("echo", { text: "one" }), fauxToolCall("echo", { text: "two" })], { + stopReason: "toolUse", + }), + fauxAssistantMessage("done"), + ]); + await harness.session.prompt("run tools"); + + const branchMessages = harness.sessionManager + .getBranch() + .filter((entry) => entry.type === "message") + .map((entry) => entry.message); + expect(branchMessages.map((message) => message.role)).toEqual([ + "user", + "assistant", + "toolResult", + "toolResult", + "assistant", + ]); + const firstToolResultIndex = branchMessages.findIndex((message) => message.role === "toolResult"); + expect(firstToolResultIndex).toBeGreaterThan(0); + expect(branchMessages[firstToolResultIndex - 1]?.role).toBe("assistant"); + }); + + it("runs tool_call handlers after the assistant tool-use message is settled in the session", async () => { + let harness: Harness; + const branchRolesAtToolCall: string[][] = []; + harness = await createHarness({ + tools: [createEchoTool()], + extensionFactories: [ + (pi) => { + pi.on("tool_call", () => { + branchRolesAtToolCall.push( + harness.sessionManager + .getBranch() + .filter((entry) => entry.type === "message") + .map((entry) => entry.message.role), + ); + }); + }, + ], + }); + harnesses.push(harness); + harness.setResponses([ + fauxAssistantMessage([fauxToolCall("echo", { text: "hello" })], { stopReason: "toolUse" }), + fauxAssistantMessage("done"), + ]); + + await harness.session.prompt("run tool"); + + expect(branchRolesAtToolCall).toEqual([["user", "assistant"]]); + }); +}); From c685b2736982eb5c21969870813d075bf36a4ebd Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Tue, 19 May 2026 10:33:16 +0200 Subject: [PATCH 117/166] fix(coding-agent): mark retrying agent end events --- packages/coding-agent/CHANGELOG.md | 2 +- .../coding-agent/src/core/agent-session.ts | 24 +++++++++++++++++-- ...gent-session-auto-compaction-queue.test.ts | 7 +++--- .../suite/agent-session-retry-events.test.ts | 2 ++ 4 files changed, 28 insertions(+), 7 deletions(-) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 147b3b19..d4725a75 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -4,7 +4,7 @@ ### Fixed -- Fixed AgentSession retry, compaction, and event settlement to use the awaited agent lifecycle instead of a separate event queue. +- Fixed AgentSession retry, compaction, and event settlement to use the awaited agent lifecycle instead of a separate event queue, and added `willRetry` to `agent_end` session events. - Fixed the subagent extension's parallel mode to return useful per-task output and failed-task diagnostics to the parent model instead of 100-character previews ([#4710](https://github.com/earendil-works/pi/issues/4710)). - Fixed Windows local bash execution to hide helper console windows when launched from background SDK processes ([#4699](https://github.com/earendil-works/pi/issues/4699)). diff --git a/packages/coding-agent/src/core/agent-session.ts b/packages/coding-agent/src/core/agent-session.ts index 2f5b805d..c04791c2 100644 --- a/packages/coding-agent/src/core/agent-session.ts +++ b/packages/coding-agent/src/core/agent-session.ts @@ -120,7 +120,12 @@ export function parseSkillBlock(text: string): ParsedSkillBlock | null { /** Session-specific events that extend the core AgentEvent */ export type AgentSessionEvent = - | AgentEvent + | Exclude + | { + type: "agent_end"; + messages: AgentMessage[]; + willRetry: boolean; + } | { type: "queue_update"; steering: readonly string[]; @@ -485,7 +490,7 @@ export class AgentSession { await this._emitExtensionEvent(event); // Notify all listeners - this._emit(event); + this._emit(event.type === "agent_end" ? { ...event, willRetry: this._willRetryAfterAgentEnd(event) } : event); // Handle session persistence if (event.type === "message_end") { @@ -531,6 +536,21 @@ export class AgentSession { } }; + private _willRetryAfterAgentEnd(event: Extract): boolean { + const settings = this.settingsManager.getRetrySettings(); + if (!settings.enabled || this._retryAttempt >= settings.maxRetries) { + return false; + } + + for (let i = event.messages.length - 1; i >= 0; i--) { + const message = event.messages[i]; + if (message.role === "assistant") { + return this._isRetryableError(message as AssistantMessage); + } + } + return false; + } + /** Extract text content from a message */ private _getUserMessageText(message: Message): string { if (message.role !== "user") return ""; diff --git a/packages/coding-agent/test/agent-session-auto-compaction-queue.test.ts b/packages/coding-agent/test/agent-session-auto-compaction-queue.test.ts index f5343170..1dd01c9b 100644 --- a/packages/coding-agent/test/agent-session-auto-compaction-queue.test.ts +++ b/packages/coding-agent/test/agent-session-auto-compaction-queue.test.ts @@ -113,14 +113,13 @@ describe("AgentSession auto-compaction queue resume", () => { const runAutoCompaction = ( session as unknown as { - _runAutoCompaction: (reason: "overflow" | "threshold", willRetry: boolean) => Promise; + _runAutoCompaction: (reason: "overflow" | "threshold", willRetry: boolean) => Promise; } )._runAutoCompaction.bind(session); - await runAutoCompaction("threshold", false); - await vi.advanceTimersByTimeAsync(100); + await expect(runAutoCompaction("threshold", false)).resolves.toBe(true); - expect(continueSpy).toHaveBeenCalledTimes(1); + expect(continueSpy).not.toHaveBeenCalled(); }); it("should not compact repeatedly after overflow recovery already attempted", async () => { diff --git a/packages/coding-agent/test/suite/agent-session-retry-events.test.ts b/packages/coding-agent/test/suite/agent-session-retry-events.test.ts index d10fe90c..ff3e5e95 100644 --- a/packages/coding-agent/test/suite/agent-session-retry-events.test.ts +++ b/packages/coding-agent/test/suite/agent-session-retry-events.test.ts @@ -47,6 +47,7 @@ describe("AgentSession retry and event characterization", () => { await harness.session.prompt("test"); expect(retryEvents).toEqual(["start:1", "end:true"]); + expect(harness.eventsOfType("agent_end").map((event) => event.willRetry)).toEqual([true, false]); expect(harness.faux.state.callCount).toBe(2); expect(harness.session.isRetrying).toBe(false); }); @@ -90,6 +91,7 @@ describe("AgentSession retry and event characterization", () => { await harness.session.prompt("test"); expect(retryEvents).toEqual(["start:1", "start:2", "end:false"]); + expect(harness.eventsOfType("agent_end").map((event) => event.willRetry)).toEqual([true, true, false]); expect(harness.faux.state.callCount).toBe(3); expect(harness.session.isRetrying).toBe(false); }); From 725e2b7b2adf8e5e695de5643074b2fbb1b3f64d Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Tue, 19 May 2026 10:43:06 +0200 Subject: [PATCH 118/166] docs(agent): clarify harness stream settlement --- packages/agent/docs/agent-harness.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/agent/docs/agent-harness.md b/packages/agent/docs/agent-harness.md index 7ac55457..65828889 100644 --- a/packages/agent/docs/agent-harness.md +++ b/packages/agent/docs/agent-harness.md @@ -17,6 +17,8 @@ The intended rule is: - getters return latest harness config, not in-flight snapshots - listeners/hooks currently receive no facade; if they close over the raw harness and call settlement APIs such as `waitForIdle()` during the active run, they can deadlock. A future facade should expose `runWhenIdle()` instead. +`AssistantMessageStream` already decouples provider transport streaming, such as SSE or websocket reads, from downstream event consumption. The harness can therefore await listeners, extension hooks, persistence, and save-point work without blocking the provider transport reader or reintroducing ad hoc event queues. Lifecycle code should prefer explicit awaited sequencing at harness boundaries over fire-and-forget hook/event settlement. + A final lifecycle hardening pass should prove these guarantees with a broad listener/hook reentrancy test suite. ## Error handling @@ -145,7 +147,7 @@ At a save point the harness: 2. creates a fresh turn snapshot if the low-level loop may continue 3. applies the fresh context/model/thinking-level/stream-options/session-id state before the next provider request -This lets model, thinking level, tool, resource, stream option, and system prompt changes made during a turn affect the next turn in the same run, while never mutating an in-flight provider request. The loop callbacks are not recreated at save points. +This lets model, thinking level, tool, resource, stream option, and system prompt changes made during a turn affect the next turn in the same run, while never mutating an in-flight provider request. Because provider transport reading is already decoupled by `AssistantMessageStream`, save-point work and hook settlement can be awaited directly to keep transcript/session ordering deterministic. The loop callbacks are not recreated at save points. The low-level loop converts harness `ThinkingLevel` to provider `reasoning` at the provider boundary: @@ -169,7 +171,7 @@ Summary: - Hook registration provenance is sidecar metadata on the registration. Resource and tool provenance belongs on app-specific concrete value types. - Hook context should be a plain object of facades, not raw internals or late-bound getter mazes. -Event payloads describe what is happening. Harness getters describe latest config for future snapshots. +Event payloads describe what is happening. Harness getters describe latest config for future snapshots. Hook and listener settlement should be awaited in lifecycle order where possible; transport backpressure is handled below the harness by `AssistantMessageStream`, so the harness does not need a separate async event queue merely to keep SSE or websocket reads flowing. ## Planned session facade From 7be75badede41576e0b8a751ac746f048a39357b Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Tue, 19 May 2026 10:51:11 +0200 Subject: [PATCH 119/166] fix(ai): clamp OpenAI prompt cache keys closes #4720 --- packages/ai/CHANGELOG.md | 4 ++ .../src/providers/azure-openai-responses.ts | 3 +- .../src/providers/openai-codex-responses.ts | 3 +- .../ai/src/providers/openai-completions.ts | 3 +- .../ai/src/providers/openai-prompt-cache.ts | 8 +++ packages/ai/src/providers/openai-responses.ts | 3 +- .../ai/test/azure-openai-base-url.test.ts | 20 +++++++- packages/ai/test/openai-codex-stream.test.ts | 50 +++++++++++++++++++ .../openai-completions-prompt-cache.test.ts | 7 +++ .../openai-responses-copilot-provider.test.ts | 32 ++++++++++++ 10 files changed, 128 insertions(+), 5 deletions(-) create mode 100644 packages/ai/src/providers/openai-prompt-cache.ts diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index 2f5bd8ad..39a71698 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Fixed + +- Fixed OpenAI prompt cache keys to clamp session-derived values to the 64-character API limit across OpenAI Responses, Chat Completions, Codex Responses, and Azure OpenAI Responses ([#4720](https://github.com/earendil-works/pi/issues/4720)). + ## [0.75.3] - 2026-05-18 ## [0.75.2] - 2026-05-18 diff --git a/packages/ai/src/providers/azure-openai-responses.ts b/packages/ai/src/providers/azure-openai-responses.ts index 918ff9f5..5625dbd3 100644 --- a/packages/ai/src/providers/azure-openai-responses.ts +++ b/packages/ai/src/providers/azure-openai-responses.ts @@ -13,6 +13,7 @@ import type { } from "../types.js"; import { AssistantMessageEventStream } from "../utils/event-stream.js"; import { headersToRecord } from "../utils/headers.js"; +import { clampOpenAIPromptCacheKey } from "./openai-prompt-cache.js"; import { convertResponsesMessages, convertResponsesTools, processResponsesStream } from "./openai-responses-shared.js"; import { buildBaseOptions } from "./simple-options.js"; @@ -261,7 +262,7 @@ function buildParams( model: deploymentName, input: messages, stream: true, - prompt_cache_key: options?.sessionId, + prompt_cache_key: clampOpenAIPromptCacheKey(options?.sessionId), }; if (options?.maxTokens) { diff --git a/packages/ai/src/providers/openai-codex-responses.ts b/packages/ai/src/providers/openai-codex-responses.ts index dc8d8ef7..ab2ba789 100644 --- a/packages/ai/src/providers/openai-codex-responses.ts +++ b/packages/ai/src/providers/openai-codex-responses.ts @@ -40,6 +40,7 @@ import { } from "../utils/diagnostics.js"; import { AssistantMessageEventStream } from "../utils/event-stream.js"; import { headersToRecord } from "../utils/headers.js"; +import { clampOpenAIPromptCacheKey } from "./openai-prompt-cache.js"; import { convertResponsesMessages, convertResponsesTools, processResponsesStream } from "./openai-responses-shared.js"; import { buildBaseOptions } from "./simple-options.js"; @@ -378,7 +379,7 @@ function buildRequestBody( input: messages, text: { verbosity: options?.textVerbosity || "low" }, include: ["reasoning.encrypted_content"], - prompt_cache_key: options?.sessionId, + prompt_cache_key: clampOpenAIPromptCacheKey(options?.sessionId), tool_choice: "auto", parallel_tool_calls: true, }; diff --git a/packages/ai/src/providers/openai-completions.ts b/packages/ai/src/providers/openai-completions.ts index a89b6dd9..658d4179 100644 --- a/packages/ai/src/providers/openai-completions.ts +++ b/packages/ai/src/providers/openai-completions.ts @@ -36,6 +36,7 @@ import { parseStreamingJson } from "../utils/json-parse.js"; import { sanitizeSurrogates } from "../utils/sanitize-unicode.js"; import { isCloudflareProvider, resolveCloudflareBaseUrl } from "./cloudflare.js"; import { buildCopilotDynamicHeaders, hasCopilotVisionInput } from "./github-copilot-headers.js"; +import { clampOpenAIPromptCacheKey } from "./openai-prompt-cache.js"; import { buildBaseOptions } from "./simple-options.js"; import { transformMessages } from "./transform-messages.js"; @@ -516,7 +517,7 @@ function buildParams( prompt_cache_key: (model.baseUrl.includes("api.openai.com") && cacheRetention !== "none") || (cacheRetention === "long" && compat.supportsLongCacheRetention) - ? options?.sessionId + ? clampOpenAIPromptCacheKey(options?.sessionId) : undefined, prompt_cache_retention: cacheRetention === "long" && compat.supportsLongCacheRetention ? "24h" : undefined, }; diff --git a/packages/ai/src/providers/openai-prompt-cache.ts b/packages/ai/src/providers/openai-prompt-cache.ts new file mode 100644 index 00000000..1c3355d5 --- /dev/null +++ b/packages/ai/src/providers/openai-prompt-cache.ts @@ -0,0 +1,8 @@ +export const OPENAI_PROMPT_CACHE_KEY_MAX_LENGTH = 64; + +export function clampOpenAIPromptCacheKey(key: string | undefined): string | undefined { + if (key === undefined) return undefined; + const chars = Array.from(key); + if (chars.length <= OPENAI_PROMPT_CACHE_KEY_MAX_LENGTH) return key; + return chars.slice(0, OPENAI_PROMPT_CACHE_KEY_MAX_LENGTH).join(""); +} diff --git a/packages/ai/src/providers/openai-responses.ts b/packages/ai/src/providers/openai-responses.ts index c3881a0f..a23b197c 100644 --- a/packages/ai/src/providers/openai-responses.ts +++ b/packages/ai/src/providers/openai-responses.ts @@ -18,6 +18,7 @@ import { AssistantMessageEventStream } from "../utils/event-stream.js"; import { headersToRecord } from "../utils/headers.js"; import { isCloudflareProvider, resolveCloudflareBaseUrl } from "./cloudflare.js"; import { buildCopilotDynamicHeaders, hasCopilotVisionInput } from "./github-copilot-headers.js"; +import { clampOpenAIPromptCacheKey } from "./openai-prompt-cache.js"; import { convertResponsesMessages, convertResponsesTools, processResponsesStream } from "./openai-responses-shared.js"; import { buildBaseOptions } from "./simple-options.js"; @@ -240,7 +241,7 @@ function buildParams(model: Model<"openai-responses">, context: Context, options model: model.id, input: messages, stream: true, - prompt_cache_key: cacheRetention === "none" ? undefined : options?.sessionId, + prompt_cache_key: cacheRetention === "none" ? undefined : clampOpenAIPromptCacheKey(options?.sessionId), prompt_cache_retention: getPromptCacheRetention(compat, cacheRetention), store: false, }; diff --git a/packages/ai/test/azure-openai-base-url.test.ts b/packages/ai/test/azure-openai-base-url.test.ts index 3a8e9c19..cdaebc4e 100644 --- a/packages/ai/test/azure-openai-base-url.test.ts +++ b/packages/ai/test/azure-openai-base-url.test.ts @@ -11,14 +11,20 @@ interface CapturedAzureClientOptions { baseURL: string; } +interface CapturedAzureResponsesPayload { + prompt_cache_key?: string; +} + const azureMock = vi.hoisted(() => ({ constructorCalls: [] as CapturedAzureClientOptions[], + lastParams: undefined as CapturedAzureResponsesPayload | undefined, })); vi.mock("openai", () => { class AzureOpenAI { responses = { - create: () => { + create: (params: CapturedAzureResponsesPayload) => { + azureMock.lastParams = params; throw new Error("mock create"); }, }; @@ -42,6 +48,7 @@ const originalAzureOpenAIApiKey = process.env.AZURE_OPENAI_API_KEY; beforeEach(() => { azureMock.constructorCalls.length = 0; + azureMock.lastParams = undefined; delete process.env.AZURE_OPENAI_BASE_URL; delete process.env.AZURE_OPENAI_RESOURCE_NAME; delete process.env.AZURE_OPENAI_API_VERSION; @@ -126,6 +133,17 @@ describe("azure-openai-responses base URL normalization", () => { expect(result.errorMessage).toContain("Invalid Azure OpenAI base URL"); }); + it("clamps prompt_cache_key to OpenAI's 64-character limit", async () => { + const model = getModel("azure-openai-responses", "gpt-4o-mini"); + await streamAzureOpenAIResponses(model, context, { + apiKey: "test-api-key", + azureBaseUrl: "https://my-resource.openai.azure.com", + sessionId: "x".repeat(67), + }).result(); + + expect(azureMock.lastParams?.prompt_cache_key).toBe("x".repeat(64)); + }); + it("builds correct default URL from AZURE_OPENAI_RESOURCE_NAME", async () => { process.env.AZURE_OPENAI_RESOURCE_NAME = "my-resource"; const model = getModel("azure-openai-responses", "gpt-4o-mini"); diff --git a/packages/ai/test/openai-codex-stream.test.ts b/packages/ai/test/openai-codex-stream.test.ts index 65b88315..5c54fdb4 100644 --- a/packages/ai/test/openai-codex-stream.test.ts +++ b/packages/ai/test/openai-codex-stream.test.ts @@ -411,6 +411,56 @@ describe("openai-codex streaming", () => { await streamResult.result(); }); + it("clamps prompt_cache_key to OpenAI's 64-character limit", async () => { + const token = mockToken(); + const sessionId = "x".repeat(67); + let capturedPayload: { prompt_cache_key?: string } | undefined; + const encoder = new TextEncoder(); + vi.stubGlobal( + "fetch", + vi.fn( + async () => + new Response( + new ReadableStream({ + start(controller) { + controller.enqueue(encoder.encode(buildSSEPayload({ status: "completed" }))); + controller.close(); + }, + }), + { status: 200, headers: { "content-type": "text/event-stream" } }, + ), + ), + ); + + const model: Model<"openai-codex-responses"> = { + id: "gpt-5.1-codex", + name: "GPT-5.1 Codex", + api: "openai-codex-responses", + provider: "openai-codex", + baseUrl: "https://chatgpt.com/backend-api", + reasoning: true, + input: ["text"], + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, + contextWindow: 400000, + maxTokens: 128000, + }; + const context: Context = { + systemPrompt: "You are a helpful assistant.", + messages: [{ role: "user", content: "Say hello", timestamp: Date.now() }], + }; + + await streamOpenAICodexResponses(model, context, { + apiKey: token, + transport: "sse", + sessionId, + onPayload: (payload) => { + capturedPayload = payload as { prompt_cache_key?: string }; + }, + }).result(); + + expect(capturedPayload?.prompt_cache_key).toBe("x".repeat(64)); + }); + it("preserves gpt-5.5 xhigh reasoning effort from simple options", async () => { const tempDir = mkdtempSync(join(tmpdir(), "pi-codex-stream-")); process.env.PI_CODING_AGENT_DIR = tempDir; diff --git a/packages/ai/test/openai-completions-prompt-cache.test.ts b/packages/ai/test/openai-completions-prompt-cache.test.ts index 147097ef..b1846421 100644 --- a/packages/ai/test/openai-completions-prompt-cache.test.ts +++ b/packages/ai/test/openai-completions-prompt-cache.test.ts @@ -125,6 +125,13 @@ describe("openai-completions prompt caching", () => { expect(payload?.prompt_cache_retention).toBe("24h"); }); + it("clamps prompt_cache_key to OpenAI's 64-character limit", async () => { + const sessionId = "x".repeat(67); + const { payload } = await captureRequest({ sessionId }); + + expect(payload?.prompt_cache_key).toBe("x".repeat(64)); + }); + it("omits prompt cache fields when cacheRetention is none", async () => { const { payload } = await captureRequest({ cacheRetention: "none", sessionId: "session-789" }); diff --git a/packages/ai/test/openai-responses-copilot-provider.test.ts b/packages/ai/test/openai-responses-copilot-provider.test.ts index c4140a5c..c956030d 100644 --- a/packages/ai/test/openai-responses-copilot-provider.test.ts +++ b/packages/ai/test/openai-responses-copilot-provider.test.ts @@ -171,6 +171,38 @@ describe("openai-responses provider defaults", () => { expect(captured).toEqual({ sessionId: "session-123", clientRequestId: "session-123" }); }); + it("clamps prompt_cache_key to OpenAI's 64-character limit", async () => { + const sessionId = "x".repeat(67); + let capturedPayload: { prompt_cache_key?: string } | undefined; + vi.spyOn(globalThis, "fetch").mockResolvedValue( + new Response("data: [DONE]\n\n", { + status: 200, + headers: { "content-type": "text/event-stream" }, + }), + ); + + const stream = streamOpenAIResponses( + getModel("openai", "gpt-5.4"), + { + systemPrompt: "sys", + messages: [{ role: "user", content: "hi", timestamp: Date.now() }], + }, + { + apiKey: "test-key", + sessionId, + onPayload: (payload) => { + capturedPayload = payload as { prompt_cache_key?: string }; + }, + }, + ); + + for await (const event of stream) { + if (event.type === "done" || event.type === "error") break; + } + + expect(capturedPayload?.prompt_cache_key).toBe("x".repeat(64)); + }); + it("sets cache-affinity headers for proxy OpenAI Responses requests with a sessionId", async () => { const proxyModel: Model<"openai-responses"> = { ...getModel("openai", "gpt-5.4"), From 5e8f0cf45e9f7d6d34f576e4d41fdcf80cb09ea7 Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Tue, 19 May 2026 11:27:35 +0200 Subject: [PATCH 120/166] chore: update issue analysis prompt --- .pi/prompts/is.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pi/prompts/is.md b/.pi/prompts/is.md index 3dee06a5..ccdb27e1 100644 --- a/.pi/prompts/is.md +++ b/.pi/prompts/is.md @@ -6,7 +6,7 @@ Analyze GitHub issue(s): $ARGUMENTS For each issue: -1. Add the `inprogress` label to the issue via GitHub CLI before analysis starts. If adding the label fails, report that explicitly and continue. +1. Add the `inprogress` label to the issue via GitHub CLI and assign the issue to the local `gh` user before analysis starts. If either action fails, report that explicitly and continue. 2. Read the issue in full, including all comments and linked issues/PRs. 3. Do not trust analysis written in the issue. Independently verify behavior and derive your own analysis from the code and execution path. From 2787b601d7cec86a82541e3c6b876748deca880d Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Tue, 19 May 2026 11:49:44 +0200 Subject: [PATCH 121/166] fix(ai): stop defaulting max token request caps closes #4675 --- packages/ai/CHANGELOG.md | 1 + packages/ai/src/providers/amazon-bedrock.ts | 4 +- packages/ai/src/providers/anthropic.ts | 6 ++- packages/ai/src/providers/simple-options.ts | 20 +++------- .../ai/test/github-copilot-anthropic.test.ts | 2 +- .../openai-completions-empty-tools.test.ts | 38 ++++++++++++++++++- 6 files changed, 51 insertions(+), 20 deletions(-) diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index 39a71698..001a10a2 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -4,6 +4,7 @@ ### Fixed +- Fixed OpenAI-compatible `streamSimple()` requests to stop sending model-derived default output token caps, avoiding context-window reservation failures on servers such as vLLM while preserving explicit `maxTokens` and required Anthropic `max_tokens` handling ([#4675](https://github.com/earendil-works/pi/issues/4675)). - Fixed OpenAI prompt cache keys to clamp session-derived values to the 64-character API limit across OpenAI Responses, Chat Completions, Codex Responses, and Azure OpenAI Responses ([#4720](https://github.com/earendil-works/pi/issues/4720)). ## [0.75.3] - 2026-05-18 diff --git a/packages/ai/src/providers/amazon-bedrock.ts b/packages/ai/src/providers/amazon-bedrock.ts index a48b5897..6e616759 100644 --- a/packages/ai/src/providers/amazon-bedrock.ts +++ b/packages/ai/src/providers/amazon-bedrock.ts @@ -314,8 +314,10 @@ export const streamSimpleBedrock: StreamFunction<"bedrock-converse-stream", Simp } satisfies BedrockOptions); } + // Undefined means the caller did not request an output cap; let the helper use the model cap. + // Do not coerce to 0 here, or the thinking budget would become the entire maxTokens value. const adjusted = adjustMaxTokensForThinking( - base.maxTokens || 0, + base.maxTokens, model.maxTokens, options.reasoning, options.thinkingBudgets, diff --git a/packages/ai/src/providers/anthropic.ts b/packages/ai/src/providers/anthropic.ts index 4079b149..29f51df9 100644 --- a/packages/ai/src/providers/anthropic.ts +++ b/packages/ai/src/providers/anthropic.ts @@ -751,8 +751,10 @@ export const streamSimpleAnthropic: StreamFunction<"anthropic-messages", SimpleS } satisfies AnthropicOptions); } + // Undefined means the caller did not request an output cap; let the helper use the model cap. + // Do not coerce to 0 here, or the thinking budget would become the entire max_tokens value. const adjusted = adjustMaxTokensForThinking( - base.maxTokens || 0, + base.maxTokens, model.maxTokens, options.reasoning, options.thinkingBudgets, @@ -891,7 +893,7 @@ function buildParams( const params: MessageCreateParamsStreaming = { model: model.id, messages: convertMessages(context.messages, model, isOAuthToken, cacheControl), - max_tokens: options?.maxTokens || (model.maxTokens / 3) | 0, + max_tokens: options?.maxTokens ?? model.maxTokens, stream: true, }; diff --git a/packages/ai/src/providers/simple-options.ts b/packages/ai/src/providers/simple-options.ts index d9dcc348..1822ebb1 100644 --- a/packages/ai/src/providers/simple-options.ts +++ b/packages/ai/src/providers/simple-options.ts @@ -1,19 +1,9 @@ import type { Api, Model, SimpleStreamOptions, StreamOptions, ThinkingBudgets, ThinkingLevel } from "../types.js"; -const DEFAULT_MAX_OUTPUT_TOKENS = 32000; -const CONTEXT_WINDOW_OUTPUT_TOLERANCE = 1024; - -export function buildBaseOptions(model: Model, options?: SimpleStreamOptions, apiKey?: string): StreamOptions { - const defaultMaxTokens = - model.maxTokens > 0 - ? model.maxTokens >= model.contextWindow - CONTEXT_WINDOW_OUTPUT_TOLERANCE - ? Math.min(model.maxTokens, DEFAULT_MAX_OUTPUT_TOKENS) - : model.maxTokens - : undefined; - +export function buildBaseOptions(_model: Model, options?: SimpleStreamOptions, apiKey?: string): StreamOptions { return { temperature: options?.temperature, - maxTokens: options?.maxTokens ?? defaultMaxTokens, + maxTokens: options?.maxTokens, signal: options?.signal, apiKey: apiKey || options?.apiKey, transport: options?.transport, @@ -34,7 +24,8 @@ export function clampReasoning(effort: ThinkingLevel | undefined): Exclude { const params = mockState.createParams!; expect(params.model).toBe("claude-sonnet-4.6"); expect(params.stream).toBe(true); - expect(params.max_tokens).toBeGreaterThan(0); + expect(params.max_tokens).toBe(model.maxTokens); expect(Array.isArray(params.messages)).toBe(true); }); diff --git a/packages/ai/test/openai-completions-empty-tools.test.ts b/packages/ai/test/openai-completions-empty-tools.test.ts index 8197efb0..8e5d49c7 100644 --- a/packages/ai/test/openai-completions-empty-tools.test.ts +++ b/packages/ai/test/openai-completions-empty-tools.test.ts @@ -93,6 +93,40 @@ describe("openai-completions empty tools handling", () => { expect("tools" in (params as object)).toBe(false); }); + it("does not send default max token fields", async () => { + const { compat: _compat, ...baseModel } = getModel("openai", "gpt-4o-mini")!; + const model = { ...baseModel, api: "openai-completions" } as const; + + await streamSimple( + model, + { + messages: [{ role: "user", content: "hi", timestamp: Date.now() }], + }, + { apiKey: "test" }, + ).result(); + + const params = mockState.lastParams as { max_tokens?: number; max_completion_tokens?: number }; + expect(params.max_tokens).toBeUndefined(); + expect(params.max_completion_tokens).toBeUndefined(); + }); + + it("sends explicit maxTokens", async () => { + const { compat: _compat, ...baseModel } = getModel("openai", "gpt-4o-mini")!; + const model = { ...baseModel, api: "openai-completions" } as const; + + await streamSimple( + model, + { + messages: [{ role: "user", content: "hi", timestamp: Date.now() }], + }, + { apiKey: "test", maxTokens: 1234 }, + ).result(); + + const params = mockState.lastParams as { max_tokens?: number; max_completion_tokens?: number }; + expect(params.max_tokens).toBeUndefined(); + expect(params.max_completion_tokens).toBe(1234); + }); + it("uses conservative OpenAI-compatible fields for Cloudflare AI Gateway /compat models", async () => { process.env.CLOUDFLARE_ACCOUNT_ID = "account-id"; process.env.CLOUDFLARE_GATEWAY_ID = "gateway-id"; @@ -104,7 +138,7 @@ describe("openai-completions empty tools handling", () => { systemPrompt: "You are helpful.", messages: [{ role: "user", content: "hi", timestamp: Date.now() }], }, - { apiKey: "test", reasoning: "high" }, + { apiKey: "test", maxTokens: 1234, reasoning: "high" }, ).result(); const params = mockState.lastParams as { @@ -115,7 +149,7 @@ describe("openai-completions empty tools handling", () => { store?: boolean; }; expect(params.messages[0].role).toBe("system"); - expect(params.max_tokens).toBeDefined(); + expect(params.max_tokens).toBe(1234); expect(params.max_completion_tokens).toBeUndefined(); expect(params.reasoning_effort).toBeUndefined(); expect(params.store).toBeUndefined(); From f4f0ac7adaef08f2ec750ccb4c76cd90e93922a7 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 19 May 2026 12:08:13 +0200 Subject: [PATCH 122/166] feat(coding-agent): show update notes (#4724) --- .../src/modes/interactive/interactive-mode.ts | 31 ++++++++----- .../coding-agent/src/package-manager-cli.ts | 44 ++++++++++++++++++- .../coding-agent/src/utils/version-check.ts | 22 +++++++--- .../coding-agent/test/version-check.test.ts | 23 ++++++++-- 4 files changed, 97 insertions(+), 23 deletions(-) diff --git a/packages/coding-agent/src/modes/interactive/interactive-mode.ts b/packages/coding-agent/src/modes/interactive/interactive-mode.ts index b3df9eb4..1cd75a47 100644 --- a/packages/coding-agent/src/modes/interactive/interactive-mode.ts +++ b/packages/coding-agent/src/modes/interactive/interactive-mode.ts @@ -91,7 +91,7 @@ import { getCwdRelativePath } from "../../utils/paths.js"; import { getPiUserAgent } from "../../utils/pi-user-agent.js"; import { killTrackedDetachedChildren } from "../../utils/shell.js"; import { ensureTool } from "../../utils/tools-manager.js"; -import { checkForNewPiVersion } from "../../utils/version-check.js"; +import { checkForNewPiVersion, type LatestPiRelease } from "../../utils/version-check.js"; import { ArminComponent } from "./components/armin.js"; import { AssistantMessageComponent } from "./components/assistant-message.js"; import { BashExecutionComponent } from "./components/bash-execution.js"; @@ -711,9 +711,9 @@ export class InteractiveMode { await this.init(); // Start version check asynchronously - checkForNewPiVersion(this.version).then((newVersion) => { - if (newVersion) { - this.showNewVersionNotification(newVersion); + checkForNewPiVersion(this.version).then((newRelease) => { + if (newRelease) { + this.showNewVersionNotification(newRelease); } }); @@ -3575,24 +3575,31 @@ export class InteractiveMode { this.ui.requestRender(); } - showNewVersionNotification(newVersion: string): void { + showNewVersionNotification(release: LatestPiRelease): void { const action = theme.fg("accent", `${APP_NAME} update`); - const updateInstruction = theme.fg("muted", `New version ${newVersion} is available. Run `) + action; - const changelogUrl = "https://github.com/earendil-works/pi-mono/blob/main/packages/coding-agent/CHANGELOG.md"; + const updateInstruction = theme.fg("muted", `New version ${release.version} is available. Run `) + action; + const changelogUrl = "https://pi.dev/changelog"; const changelogLink = getCapabilities().hyperlinks ? hyperlink(theme.fg("accent", "open changelog"), changelogUrl) : theme.fg("accent", changelogUrl); const changelogLine = theme.fg("muted", "Changelog: ") + changelogLink; + const note = release.note?.trim(); this.chatContainer.addChild(new Spacer(1)); this.chatContainer.addChild(new DynamicBorder((text) => theme.fg("warning", text))); this.chatContainer.addChild( - new Text( - `${theme.bold(theme.fg("warning", "Update Available"))}\n${updateInstruction}\n${changelogLine}`, - 1, - 0, - ), + new Text(`${theme.bold(theme.fg("warning", "Update Available"))}\n${updateInstruction}`, 1, 0), ); + if (note) { + this.chatContainer.addChild(new Spacer(1)); + this.chatContainer.addChild( + new Markdown(note, 1, 0, this.getMarkdownThemeWithSettings(), { + color: (text) => theme.fg("muted", text), + }), + ); + this.chatContainer.addChild(new Spacer(1)); + } + this.chatContainer.addChild(new Text(changelogLine, 1, 0)); this.chatContainer.addChild(new DynamicBorder((text) => theme.fg("warning", text))); this.ui.requestRender(); } diff --git a/packages/coding-agent/src/package-manager-cli.ts b/packages/coding-agent/src/package-manager-cli.ts index a55a3750..8fd5d6d4 100644 --- a/packages/coding-agent/src/package-manager-cli.ts +++ b/packages/coding-agent/src/package-manager-cli.ts @@ -1,3 +1,4 @@ +import { Markdown, type MarkdownTheme } from "@earendil-works/pi-tui"; import chalk from "chalk"; import { selectConfig } from "./cli/config-selector.js"; import { @@ -24,6 +25,23 @@ export type PackageCommand = "install" | "remove" | "update" | "list"; type UpdateTarget = { type: "all" } | { type: "self" } | { type: "extensions"; source?: string }; +const SELF_UPDATE_NOTE_MARKDOWN_THEME: MarkdownTheme = { + heading: (text) => chalk.bold(chalk.yellow(text)), + link: (text) => chalk.cyan(text), + linkUrl: (text) => chalk.dim(text), + code: (text) => chalk.yellow(text), + codeBlock: (text) => chalk.dim(text), + codeBlockBorder: (text) => chalk.dim(text), + quote: (text) => chalk.dim(text), + quoteBorder: (text) => chalk.dim(text), + hr: (text) => chalk.dim(text), + listBullet: (text) => chalk.yellow(text), + bold: (text) => chalk.bold(text), + italic: (text) => chalk.italic(text), + strikethrough: (text) => chalk.strikethrough(text), + underline: (text) => chalk.underline(text), +}; + interface PackageCommandOptions { command: PackageCommand; source?: string; @@ -293,9 +311,30 @@ function printSelfUpdateFallback(command: SelfUpdateCommand): void { console.error(chalk.dim(`If this keeps failing, run this command yourself: ${command.display}`)); } +function printSelfUpdateNote(note: string): void { + const trimmedNote = note.trim(); + if (!trimmedNote) { + return; + } + + console.log(); + console.log(chalk.bold(chalk.yellow("Update note"))); + try { + const width = Math.max(20, process.stdout.columns ?? 80); + const renderedLines = new Markdown(trimmedNote, 0, 0, SELF_UPDATE_NOTE_MARKDOWN_THEME) + .render(width) + .map((line) => line.trimEnd()); + console.log(renderedLines.join("\n")); + } catch { + console.log(trimmedNote); + } + console.log(); +} + interface SelfUpdatePlan { packageName: string; shouldRun: boolean; + note?: string; } async function getSelfUpdatePlan(force: boolean): Promise { @@ -307,7 +346,7 @@ async function getSelfUpdatePlan(force: boolean): Promise { const latestRelease = await getLatestPiRelease(VERSION); const packageName = latestRelease?.packageName ?? PACKAGE_NAME; if (!latestRelease || packageName !== PACKAGE_NAME || isNewerPackageVersion(latestRelease.version, VERSION)) { - return { packageName, shouldRun: true }; + return { packageName, shouldRun: true, ...(latestRelease?.note ? { note: latestRelease.note } : {}) }; } } catch { return { packageName: PACKAGE_NAME, shouldRun: true }; @@ -522,6 +561,9 @@ export async function handlePackageCommand(args: string[]): Promise { process.exitCode = 1; return true; } + if (selfUpdatePlan.note) { + printSelfUpdateNote(selfUpdatePlan.note); + } try { if (installMethod === "npm") { prepareWindowsNpmSelfUpdate(); diff --git a/packages/coding-agent/src/utils/version-check.ts b/packages/coding-agent/src/utils/version-check.ts index ec969469..27f3390c 100644 --- a/packages/coding-agent/src/utils/version-check.ts +++ b/packages/coding-agent/src/utils/version-check.ts @@ -6,6 +6,7 @@ const DEFAULT_VERSION_CHECK_TIMEOUT_MS = 10000; export interface LatestPiRelease { version: string; packageName?: string; + note?: string; } interface ParsedVersion { @@ -67,13 +68,22 @@ export async function getLatestPiRelease( }); if (!response.ok) return undefined; - const data = (await response.json()) as { packageName?: unknown; version?: unknown }; + const data = (await response.json()) as { + packageName?: unknown; + version?: unknown; + note?: unknown; + }; if (typeof data.version !== "string" || !data.version.trim()) { return undefined; } const packageName = typeof data.packageName === "string" && data.packageName.trim() ? data.packageName.trim() : undefined; - return { version: data.version.trim(), packageName }; + const note = typeof data.note === "string" && data.note.trim() ? data.note.trim() : undefined; + return { + version: data.version.trim(), + packageName, + ...(note ? { note } : {}), + }; } export async function getLatestPiVersion( @@ -83,11 +93,11 @@ export async function getLatestPiVersion( return (await getLatestPiRelease(currentVersion, options))?.version; } -export async function checkForNewPiVersion(currentVersion: string): Promise { +export async function checkForNewPiVersion(currentVersion: string): Promise { try { - const latestVersion = await getLatestPiVersion(currentVersion); - if (latestVersion && isNewerPackageVersion(latestVersion, currentVersion)) { - return latestVersion; + const latestRelease = await getLatestPiRelease(currentVersion); + if (latestRelease && isNewerPackageVersion(latestRelease.version, currentVersion)) { + return latestRelease; } return undefined; } catch { diff --git a/packages/coding-agent/test/version-check.test.ts b/packages/coding-agent/test/version-check.test.ts index c871718f..f05bfd2e 100644 --- a/packages/coding-agent/test/version-check.test.ts +++ b/packages/coding-agent/test/version-check.test.ts @@ -38,7 +38,7 @@ describe("version checks", () => { vi.stubGlobal("fetch", fetchMock); await expect(checkForNewPiVersion("1.2.3")).resolves.toBeUndefined(); - await expect(checkForNewPiVersion("1.2.2")).resolves.toBe("1.2.3"); + await expect(checkForNewPiVersion("1.2.2")).resolves.toEqual({ version: "1.2.3" }); }); it("uses the pi.dev version check api with a pi user agent", async () => { @@ -57,11 +57,26 @@ describe("version checks", () => { ); }); - it("returns the active package name from the version check api", async () => { - const fetchMock = vi.fn(async () => Response.json({ packageName: "@new-scope/pi", version: "1.2.4" })); + it("returns the active package metadata from the version check api", async () => { + const fetchMock = vi.fn(async () => + Response.json({ + packageName: "@new-scope/pi", + version: "1.2.4", + }), + ); vi.stubGlobal("fetch", fetchMock); - await expect(getLatestPiRelease("1.2.3")).resolves.toEqual({ packageName: "@new-scope/pi", version: "1.2.4" }); + await expect(getLatestPiRelease("1.2.3")).resolves.toEqual({ + packageName: "@new-scope/pi", + version: "1.2.4", + }); + }); + + it("returns update notes from the version check api", async () => { + const fetchMock = vi.fn(async () => Response.json({ note: " **Read this** ", version: "1.2.4" })); + vi.stubGlobal("fetch", fetchMock); + + await expect(getLatestPiRelease("1.2.3")).resolves.toEqual({ note: "**Read this**", version: "1.2.4" }); }); it("skips api calls when version checks are disabled", async () => { From b94482762321ed0b9f8f245be57c84d786a7105d Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Tue, 19 May 2026 13:09:39 +0200 Subject: [PATCH 123/166] fix(agent): stop tool preflight after extension abort closes #4276 --- packages/agent/CHANGELOG.md | 1 + packages/agent/src/agent-loop.ts | 24 +++++++++++++++++++ packages/coding-agent/CHANGELOG.md | 1 + .../coding-agent/src/core/agent-session.ts | 13 +++++++++- .../src/modes/interactive/interactive-mode.ts | 7 +++++- 5 files changed, 44 insertions(+), 2 deletions(-) diff --git a/packages/agent/CHANGELOG.md b/packages/agent/CHANGELOG.md index 7c04cc4c..62318b07 100644 --- a/packages/agent/CHANGELOG.md +++ b/packages/agent/CHANGELOG.md @@ -4,6 +4,7 @@ ### Fixed +- Fixed tool-call preflight to stop preparing sibling tool calls after the run is aborted ([#4276](https://github.com/earendil-works/pi/issues/4276)). - Fixed tail truncation for oversized single-line output that ends with a trailing newline ([#4715](https://github.com/earendil-works/pi/issues/4715)). - Fixed Windows Node execution environment command spawns to hide helper console windows from background processes ([#4699](https://github.com/earendil-works/pi/issues/4699)). diff --git a/packages/agent/src/agent-loop.ts b/packages/agent/src/agent-loop.ts index 7226082a..eb57039f 100644 --- a/packages/agent/src/agent-loop.ts +++ b/packages/agent/src/agent-loop.ts @@ -436,6 +436,10 @@ async function executeToolCallsSequential( await emitToolResultMessage(toolResultMessage, emit); finalizedCalls.push(finalized); messages.push(toolResultMessage); + + if (signal?.aborted) { + break; + } } return { @@ -471,6 +475,9 @@ async function executeToolCallsParallel( } satisfies FinalizedToolCallOutcome; await emitToolExecutionEnd(finalized, emit); finalizedCalls.push(finalized); + if (signal?.aborted) { + break; + } continue; } @@ -487,6 +494,9 @@ async function executeToolCallsParallel( await emitToolExecutionEnd(finalized, emit); return finalized; }); + if (signal?.aborted) { + break; + } } const orderedFinalizedCalls = await Promise.all( @@ -578,6 +588,13 @@ async function prepareToolCall( }, signal, ); + if (signal?.aborted) { + return { + kind: "immediate", + result: createErrorToolResult("Operation aborted"), + isError: true, + }; + } if (beforeResult?.block) { return { kind: "immediate", @@ -586,6 +603,13 @@ async function prepareToolCall( }; } } + if (signal?.aborted) { + return { + kind: "immediate", + result: createErrorToolResult("Operation aborted"), + isError: true, + }; + } return { kind: "prepared", toolCall, diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index d4725a75..d664d11a 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -4,6 +4,7 @@ ### Fixed +- Fixed extension `ctx.abort()` during tool-call preflight to stop later confirmations and restore queued interactive input like Escape ([#4276](https://github.com/earendil-works/pi/issues/4276)). - Fixed AgentSession retry, compaction, and event settlement to use the awaited agent lifecycle instead of a separate event queue, and added `willRetry` to `agent_end` session events. - Fixed the subagent extension's parallel mode to return useful per-task output and failed-task diagnostics to the parent model instead of 100-character previews ([#4710](https://github.com/earendil-works/pi/issues/4710)). - Fixed Windows local bash execution to hide helper console windows when launched from background SDK processes ([#4699](https://github.com/earendil-works/pi/issues/4699)). diff --git a/packages/coding-agent/src/core/agent-session.ts b/packages/coding-agent/src/core/agent-session.ts index c04791c2..362a9b9f 100644 --- a/packages/coding-agent/src/core/agent-session.ts +++ b/packages/coding-agent/src/core/agent-session.ts @@ -185,6 +185,7 @@ export interface AgentSessionConfig { export interface ExtensionBindings { uiContext?: ExtensionUIContext; commandContextActions?: ExtensionCommandContextActions; + abortHandler?: () => void; shutdownHandler?: ShutdownHandler; onError?: ExtensionErrorListener; } @@ -296,6 +297,7 @@ export class AgentSession { private _sessionStartEvent: SessionStartEvent; private _extensionUIContext?: ExtensionUIContext; private _extensionCommandContextActions?: ExtensionCommandContextActions; + private _extensionAbortHandler?: () => void; private _extensionShutdownHandler?: ShutdownHandler; private _extensionErrorListener?: ExtensionErrorListener; private _extensionErrorUnsubscriber?: () => void; @@ -2042,6 +2044,9 @@ export class AgentSession { if (bindings.commandContextActions !== undefined) { this._extensionCommandContextActions = bindings.commandContextActions; } + if (bindings.abortHandler !== undefined) { + this._extensionAbortHandler = bindings.abortHandler; + } if (bindings.shutdownHandler !== undefined) { this._extensionShutdownHandler = bindings.shutdownHandler; } @@ -2206,7 +2211,13 @@ export class AgentSession { getModel: () => this.model, isIdle: () => !this.isStreaming, getSignal: () => this.agent.signal, - abort: () => this.abort(), + abort: () => { + if (this._extensionAbortHandler) { + this._extensionAbortHandler(); + return; + } + void this.abort(); + }, hasPendingMessages: () => this.pendingMessageCount > 0, shutdown: () => { this._extensionShutdownHandler?.(); diff --git a/packages/coding-agent/src/modes/interactive/interactive-mode.ts b/packages/coding-agent/src/modes/interactive/interactive-mode.ts index 1cd75a47..616e6218 100644 --- a/packages/coding-agent/src/modes/interactive/interactive-mode.ts +++ b/packages/coding-agent/src/modes/interactive/interactive-mode.ts @@ -1478,6 +1478,9 @@ export class InteractiveMode { const uiContext = this.createExtensionUIContext(); await this.session.bindExtensions({ uiContext, + abortHandler: () => { + this.restoreQueuedMessagesToEditor({ abort: true }); + }, commandContextActions: { waitForIdle: () => this.session.agent.waitForIdle(), newSession: async (options) => { @@ -1627,7 +1630,9 @@ export class InteractiveMode { model: this.session.model, isIdle: () => !this.session.isStreaming, signal: this.session.agent.signal, - abort: () => this.session.abort(), + abort: () => { + this.restoreQueuedMessagesToEditor({ abort: true }); + }, hasPendingMessages: () => this.session.pendingMessageCount > 0, shutdown: () => { this.shutdownRequested = true; From 48b6510c18e3499d462ade5c656fc2db7cc8eaa9 Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Tue, 19 May 2026 18:01:10 +0200 Subject: [PATCH 124/166] fix(coding-agent): clarify pi docs path resolution closes #4752 --- packages/coding-agent/CHANGELOG.md | 1 + packages/coding-agent/src/core/system-prompt.ts | 1 + packages/coding-agent/test/system-prompt.test.ts | 12 ++++++++++++ 3 files changed, 14 insertions(+) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index d664d11a..c190751a 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -4,6 +4,7 @@ ### Fixed +- Fixed the system prompt to tell models to resolve pi docs and examples under the absolute package paths before reading topic-specific relative references ([#4752](https://github.com/earendil-works/pi/issues/4752)). - Fixed extension `ctx.abort()` during tool-call preflight to stop later confirmations and restore queued interactive input like Escape ([#4276](https://github.com/earendil-works/pi/issues/4276)). - Fixed AgentSession retry, compaction, and event settlement to use the awaited agent lifecycle instead of a separate event queue, and added `willRetry` to `agent_end` session events. - Fixed the subagent extension's parallel mode to return useful per-task output and failed-task diagnostics to the parent model instead of 100-character previews ([#4710](https://github.com/earendil-works/pi/issues/4710)). diff --git a/packages/coding-agent/src/core/system-prompt.ts b/packages/coding-agent/src/core/system-prompt.ts index a88b41b3..c6b1017d 100644 --- a/packages/coding-agent/src/core/system-prompt.ts +++ b/packages/coding-agent/src/core/system-prompt.ts @@ -143,6 +143,7 @@ Pi documentation (read only when the user asks about pi itself, its SDK, extensi - Main documentation: ${readmePath} - Additional docs: ${docsPath} - Examples: ${examplesPath} (extensions, custom tools, SDK) +- When reading pi docs or examples, resolve docs/... under Additional docs and examples/... under Examples, not the current working directory - When asked about: extensions (docs/extensions.md, examples/extensions/), themes (docs/themes.md), skills (docs/skills.md), prompt templates (docs/prompt-templates.md), TUI components (docs/tui.md), keybindings (docs/keybindings.md), SDK integrations (docs/sdk.md), custom providers (docs/custom-provider.md), adding models (docs/models.md), pi packages (docs/packages.md) - When working on pi topics, read the docs and examples, and follow .md cross-references before implementing - Always read pi .md files completely and follow links to related docs (e.g., tui.md for TUI API details)`; diff --git a/packages/coding-agent/test/system-prompt.test.ts b/packages/coding-agent/test/system-prompt.test.ts index 31a5d6b4..bdc19056 100644 --- a/packages/coding-agent/test/system-prompt.test.ts +++ b/packages/coding-agent/test/system-prompt.test.ts @@ -45,6 +45,18 @@ describe("buildSystemPrompt", () => { expect(prompt).toContain("- edit:"); expect(prompt).toContain("- write:"); }); + + test("instructs models to resolve pi docs and examples under absolute base paths", () => { + const prompt = buildSystemPrompt({ + contextFiles: [], + skills: [], + cwd: process.cwd(), + }); + + expect(prompt).toContain( + "- When reading pi docs or examples, resolve docs/... under Additional docs and examples/... under Examples, not the current working directory", + ); + }); }); describe("custom tool snippets", () => { From 06c6c324d773564decb5429adc744624a9229cfc Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Tue, 19 May 2026 23:15:39 +0200 Subject: [PATCH 125/166] chore: enforce erasable TypeScript syntax --- AGENTS.md | 1 + package.json | 3 +- packages/agent/CHANGELOG.md | 4 ++ packages/agent/src/agent.ts | 5 +- packages/agent/src/harness/types.ts | 69 +++++++++---------- packages/ai/CHANGELOG.md | 4 ++ packages/ai/src/utils/event-stream.ts | 9 +-- packages/coding-agent/CHANGELOG.md | 4 ++ .../src/core/agent-session-runtime.ts | 23 +++++-- .../coding-agent/src/core/auth-storage.ts | 10 ++- .../coding-agent/src/core/model-registry.ts | 9 +-- .../interactive/components/countdown-timer.ts | 13 ++-- .../modes/interactive/components/footer.ts | 10 +-- .../interactive/components/login-dialog.ts | 4 +- .../interactive/components/tree-selector.ts | 6 +- .../src/modes/interactive/interactive-mode.ts | 17 +++-- .../coding-agent/src/modes/rpc/rpc-client.ts | 5 +- 17 files changed, 122 insertions(+), 74 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 40438dc4..03b1f4eb 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -16,6 +16,7 @@ - Check node_modules for external API type definitions instead of guessing - **NEVER use inline imports** - no `await import("./foo.js")`, no `import("pkg").Type` in type positions, no dynamic imports for types. Always use standard top-level imports. - NEVER remove or downgrade code to fix type errors from outdated dependencies; upgrade the dependency instead +- In core source packages (`packages/ai/src`, `packages/agent/src`, `packages/coding-agent/src`), use only erasable TypeScript syntax compatible with Node strip-only mode. Do not use constructor parameter properties, `enum`, `namespace`/`module`, `import =`, `export =`, or other TypeScript constructs that require JavaScript emit. Use explicit fields and constructor assignments instead of parameter properties. - Always ask before removing functionality or code that appears to be intentional - Do not preserve backward compatibility unless the user explicitly asks for it - Never hardcode key checks with, eg. `matchesKey(keyData, "ctrl+x")`. All keybindings must be configurable. Add default to matching object (`DEFAULT_EDITOR_KEYBINDINGS` or `DEFAULT_APP_KEYBINDINGS`) diff --git a/package.json b/package.json index af486099..4e31d158 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,8 @@ "build": "cd packages/tui && npm run build && cd ../ai && npm run build && cd ../agent && npm run build && cd ../coding-agent && npm run build && cd ../web-ui && npm run build", "dev": "concurrently --names \"ai,agent,coding-agent,web-ui,tui\" --prefix-colors \"cyan,yellow,red,green,magenta\" \"cd packages/ai && npm run dev\" \"cd packages/agent && npm run dev\" \"cd packages/coding-agent && npm run dev\" \"cd packages/web-ui && npm run dev\" \"cd packages/tui && npm run dev\"", "dev:tsc": "concurrently --names \"ai,web-ui\" --prefix-colors \"cyan,green\" \"cd packages/ai && npm run dev:tsc\" \"cd packages/web-ui && npm run dev:tsc\"", - "check": "biome check --write --error-on-warnings . && tsgo --noEmit && npm run check:browser-smoke && cd packages/web-ui && npm run check", + "check": "biome check --write --error-on-warnings . && tsgo --noEmit && npm run check:erasable-types && npm run check:browser-smoke && cd packages/web-ui && npm run check", + "check:erasable-types": "tsgo -p packages/ai/tsconfig.build.json --noEmit --erasableSyntaxOnly && tsgo -p packages/agent/tsconfig.build.json --noEmit --erasableSyntaxOnly && tsgo -p packages/coding-agent/tsconfig.build.json --noEmit --erasableSyntaxOnly", "check:browser-smoke": "node scripts/check-browser-smoke.mjs", "profile:tui": "node scripts/profile-coding-agent-node.mjs --mode tui", "profile:rpc": "node scripts/profile-coding-agent-node.mjs --mode rpc", diff --git a/packages/agent/CHANGELOG.md b/packages/agent/CHANGELOG.md index 62318b07..81b71b75 100644 --- a/packages/agent/CHANGELOG.md +++ b/packages/agent/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Changed + +- Changed source syntax to avoid TypeScript constructs that require JavaScript emit, keeping the package compatible with Node.js strip-only TypeScript checks. + ### Fixed - Fixed tool-call preflight to stop preparing sibling tool calls after the run is aborted ([#4276](https://github.com/earendil-works/pi/issues/4276)). diff --git a/packages/agent/src/agent.ts b/packages/agent/src/agent.ts index 59c06415..b9e0484c 100644 --- a/packages/agent/src/agent.ts +++ b/packages/agent/src/agent.ts @@ -117,8 +117,11 @@ export interface AgentOptions { class PendingMessageQueue { private messages: AgentMessage[] = []; + public mode: QueueMode; - constructor(public mode: QueueMode) {} + constructor(mode: QueueMode) { + this.mode = mode; + } enqueue(message: AgentMessage): void { this.messages.push(message); diff --git a/packages/agent/src/harness/types.ts b/packages/agent/src/harness/types.ts index 96b60f63..c9773be4 100644 --- a/packages/agent/src/harness/types.ts +++ b/packages/agent/src/harness/types.ts @@ -120,16 +120,16 @@ export type FileErrorCode = /** Error returned by {@link FileSystem} file operations. */ export class FileError extends Error { - constructor( - /** Backend-independent error code. */ - public code: FileErrorCode, - message: string, - /** Absolute addressed path associated with the failure, when available. */ - public path?: string, - cause?: Error, - ) { + /** Backend-independent error code. */ + public code: FileErrorCode; + /** Absolute addressed path associated with the failure, when available. */ + public path?: string; + + constructor(code: FileErrorCode, message: string, path?: string, cause?: Error) { super(message, cause === undefined ? undefined : { cause }); this.name = "FileError"; + this.code = code; + this.path = path; } } @@ -144,14 +144,13 @@ export type ExecutionErrorCode = /** Error returned by {@link ExecutionEnv.exec}. */ export class ExecutionError extends Error { - constructor( - /** Backend-independent error code. */ - public code: ExecutionErrorCode, - message: string, - cause?: Error, - ) { + /** Backend-independent error code. */ + public code: ExecutionErrorCode; + + constructor(code: ExecutionErrorCode, message: string, cause?: Error) { super(message, cause === undefined ? undefined : { cause }); this.name = "ExecutionError"; + this.code = code; } } @@ -160,14 +159,13 @@ export type CompactionErrorCode = "aborted" | "summarization_failed" | "invalid_ /** Error returned by compaction helpers. */ export class CompactionError extends Error { - constructor( - /** Backend-independent error code. */ - public code: CompactionErrorCode, - message: string, - cause?: Error, - ) { + /** Backend-independent error code. */ + public code: CompactionErrorCode; + + constructor(code: CompactionErrorCode, message: string, cause?: Error) { super(message, cause === undefined ? undefined : { cause }); this.name = "CompactionError"; + this.code = code; } } @@ -176,14 +174,13 @@ export type BranchSummaryErrorCode = "aborted" | "summarization_failed" | "inval /** Error returned by branch summarization helpers. */ export class BranchSummaryError extends Error { - constructor( - /** Backend-independent error code. */ - public code: BranchSummaryErrorCode, - message: string, - cause?: Error, - ) { + /** Backend-independent error code. */ + public code: BranchSummaryErrorCode; + + constructor(code: BranchSummaryErrorCode, message: string, cause?: Error) { super(message, cause === undefined ? undefined : { cause }); this.name = "BranchSummaryError"; + this.code = code; } } @@ -197,14 +194,13 @@ export type SessionErrorCode = /** Error thrown by session storage, repositories, and session tree operations. */ export class SessionError extends Error { - constructor( - /** Session subsystem error code. */ - public code: SessionErrorCode, - message: string, - cause?: Error, - ) { + /** Session subsystem error code. */ + public code: SessionErrorCode; + + constructor(code: SessionErrorCode, message: string, cause?: Error) { super(message, cause === undefined ? undefined : { cause }); this.name = "SessionError"; + this.code = code; } } @@ -221,13 +217,12 @@ export type AgentHarnessErrorCode = /** Public AgentHarness failure with a stable top-level classification. */ export class AgentHarnessError extends Error { - constructor( - public code: AgentHarnessErrorCode, - message: string, - cause?: Error, - ) { + public code: AgentHarnessErrorCode; + + constructor(code: AgentHarnessErrorCode, message: string, cause?: Error) { super(message, cause === undefined ? undefined : { cause }); this.name = "AgentHarnessError"; + this.code = code; } } diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index 001a10a2..1b6d144a 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Changed + +- Changed source syntax to avoid TypeScript constructs that require JavaScript emit, keeping the package compatible with Node.js strip-only TypeScript checks. + ### Fixed - Fixed OpenAI-compatible `streamSimple()` requests to stop sending model-derived default output token caps, avoiding context-window reservation failures on servers such as vLLM while preserving explicit `maxTokens` and required Anthropic `max_tokens` handling ([#4675](https://github.com/earendil-works/pi/issues/4675)). diff --git a/packages/ai/src/utils/event-stream.ts b/packages/ai/src/utils/event-stream.ts index f4a7ceba..43461ea2 100644 --- a/packages/ai/src/utils/event-stream.ts +++ b/packages/ai/src/utils/event-stream.ts @@ -7,11 +7,12 @@ export class EventStream implements AsyncIterable { private done = false; private finalResultPromise: Promise; private resolveFinalResult!: (result: R) => void; + private isComplete: (event: T) => boolean; + private extractResult: (event: T) => R; - constructor( - private isComplete: (event: T) => boolean, - private extractResult: (event: T) => R, - ) { + constructor(isComplete: (event: T) => boolean, extractResult: (event: T) => R) { + this.isComplete = isComplete; + this.extractResult = extractResult; this.finalResultPromise = new Promise((resolve) => { this.resolveFinalResult = resolve; }); diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index c190751a..9625a075 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Changed + +- Changed source syntax to avoid TypeScript constructs that require JavaScript emit, keeping core sources compatible with Node.js strip-only TypeScript checks. + ### Fixed - Fixed the system prompt to tell models to resolve pi docs and examples under the absolute package paths before reading topic-specific relative references ([#4752](https://github.com/earendil-works/pi/issues/4752)). diff --git a/packages/coding-agent/src/core/agent-session-runtime.ts b/packages/coding-agent/src/core/agent-session-runtime.ts index b64def1b..eebd7575 100644 --- a/packages/coding-agent/src/core/agent-session-runtime.ts +++ b/packages/coding-agent/src/core/agent-session-runtime.ts @@ -67,14 +67,25 @@ function extractUserMessageText(content: string | Array<{ type: string; text?: s export class AgentSessionRuntime { private rebindSession?: (session: AgentSession) => Promise; private beforeSessionInvalidate?: () => void; + private _session: AgentSession; + private _services: AgentSessionServices; + private readonly createRuntime: CreateAgentSessionRuntimeFactory; + private _diagnostics: AgentSessionRuntimeDiagnostic[]; + private _modelFallbackMessage?: string; constructor( - private _session: AgentSession, - private _services: AgentSessionServices, - private readonly createRuntime: CreateAgentSessionRuntimeFactory, - private _diagnostics: AgentSessionRuntimeDiagnostic[] = [], - private _modelFallbackMessage?: string, - ) {} + _session: AgentSession, + _services: AgentSessionServices, + createRuntime: CreateAgentSessionRuntimeFactory, + _diagnostics: AgentSessionRuntimeDiagnostic[] = [], + _modelFallbackMessage?: string, + ) { + this._session = _session; + this._services = _services; + this.createRuntime = createRuntime; + this._diagnostics = _diagnostics; + this._modelFallbackMessage = _modelFallbackMessage; + } get services(): AgentSessionServices { return this._services; diff --git a/packages/coding-agent/src/core/auth-storage.ts b/packages/coding-agent/src/core/auth-storage.ts index 5b57424e..1522cc67 100644 --- a/packages/coding-agent/src/core/auth-storage.ts +++ b/packages/coding-agent/src/core/auth-storage.ts @@ -50,7 +50,11 @@ export interface AuthStorageBackend { } export class FileAuthStorageBackend implements AuthStorageBackend { - constructor(private authPath: string = join(getAgentDir(), "auth.json")) {} + private authPath: string; + + constructor(authPath: string = join(getAgentDir(), "auth.json")) { + this.authPath = authPath; + } private ensureParentDir(): void { const dir = dirname(this.authPath); @@ -194,8 +198,10 @@ export class AuthStorage { private fallbackResolver?: (provider: string) => string | undefined; private loadError: Error | null = null; private errors: Error[] = []; + private storage: AuthStorageBackend; - private constructor(private storage: AuthStorageBackend) { + private constructor(storage: AuthStorageBackend) { + this.storage = storage; this.reload(); } diff --git a/packages/coding-agent/src/core/model-registry.ts b/packages/coding-agent/src/core/model-registry.ts index a30ee210..6cf8c40f 100644 --- a/packages/coding-agent/src/core/model-registry.ts +++ b/packages/coding-agent/src/core/model-registry.ts @@ -334,11 +334,12 @@ export class ModelRegistry { private modelRequestHeaders: Map> = new Map(); private registeredProviders: Map = new Map(); private loadError: string | undefined = undefined; + readonly authStorage: AuthStorage; + private modelsJsonPath: string | undefined; - private constructor( - readonly authStorage: AuthStorage, - private modelsJsonPath: string | undefined, - ) { + private constructor(authStorage: AuthStorage, modelsJsonPath: string | undefined) { + this.authStorage = authStorage; + this.modelsJsonPath = modelsJsonPath; this.loadModels(); } diff --git a/packages/coding-agent/src/modes/interactive/components/countdown-timer.ts b/packages/coding-agent/src/modes/interactive/components/countdown-timer.ts index ac730644..73ec2734 100644 --- a/packages/coding-agent/src/modes/interactive/components/countdown-timer.ts +++ b/packages/coding-agent/src/modes/interactive/components/countdown-timer.ts @@ -7,13 +7,14 @@ import type { TUI } from "@earendil-works/pi-tui"; export class CountdownTimer { private intervalId: ReturnType | undefined; private remainingSeconds: number; + private tui: TUI | undefined; + private onTick: (seconds: number) => void; + private onExpire: () => void; - constructor( - timeoutMs: number, - private tui: TUI | undefined, - private onTick: (seconds: number) => void, - private onExpire: () => void, - ) { + constructor(timeoutMs: number, tui: TUI | undefined, onTick: (seconds: number) => void, onExpire: () => void) { + this.tui = tui; + this.onTick = onTick; + this.onExpire = onExpire; this.remainingSeconds = Math.ceil(timeoutMs / 1000); this.onTick(this.remainingSeconds); diff --git a/packages/coding-agent/src/modes/interactive/components/footer.ts b/packages/coding-agent/src/modes/interactive/components/footer.ts index 8f0653e7..7e8109a5 100644 --- a/packages/coding-agent/src/modes/interactive/components/footer.ts +++ b/packages/coding-agent/src/modes/interactive/components/footer.ts @@ -32,11 +32,13 @@ function formatTokens(count: number): string { */ export class FooterComponent implements Component { private autoCompactEnabled = true; + private session: AgentSession; + private footerData: ReadonlyFooterDataProvider; - constructor( - private session: AgentSession, - private footerData: ReadonlyFooterDataProvider, - ) {} + constructor(session: AgentSession, footerData: ReadonlyFooterDataProvider) { + this.session = session; + this.footerData = footerData; + } setSession(session: AgentSession): void { this.session = session; diff --git a/packages/coding-agent/src/modes/interactive/components/login-dialog.ts b/packages/coding-agent/src/modes/interactive/components/login-dialog.ts index 737e8b27..a9a6f2b0 100644 --- a/packages/coding-agent/src/modes/interactive/components/login-dialog.ts +++ b/packages/coding-agent/src/modes/interactive/components/login-dialog.ts @@ -15,6 +15,7 @@ export class LoginDialogComponent extends Container implements Focusable { private abortController = new AbortController(); private inputResolver?: (value: string) => void; private inputRejecter?: (error: Error) => void; + private onComplete: (success: boolean, message?: string) => void; // Focusable implementation - propagate to input for IME cursor positioning private _focused = false; @@ -29,12 +30,13 @@ export class LoginDialogComponent extends Container implements Focusable { constructor( tui: TUI, providerId: string, - private onComplete: (success: boolean, message?: string) => void, + onComplete: (success: boolean, message?: string) => void, providerNameOverride?: string, titleOverride?: string, ) { super(); this.tui = tui; + this.onComplete = onComplete; const providerInfo = getOAuthProviders().find((p) => p.id === providerId); const providerName = providerNameOverride || providerInfo?.name || providerId; diff --git a/packages/coding-agent/src/modes/interactive/components/tree-selector.ts b/packages/coding-agent/src/modes/interactive/components/tree-selector.ts index 29962d7e..d895a892 100644 --- a/packages/coding-agent/src/modes/interactive/components/tree-selector.ts +++ b/packages/coding-agent/src/modes/interactive/components/tree-selector.ts @@ -1056,7 +1056,11 @@ class TreeList implements Component { /** Component that displays the current search query */ class SearchLine implements Component { - constructor(private treeList: TreeList) {} + private treeList: TreeList; + + constructor(treeList: TreeList) { + this.treeList = treeList; + } invalidate(): void {} diff --git a/packages/coding-agent/src/modes/interactive/interactive-mode.ts b/packages/coding-agent/src/modes/interactive/interactive-mode.ts index 616e6218..7fe94ded 100644 --- a/packages/coding-agent/src/modes/interactive/interactive-mode.ts +++ b/packages/coding-agent/src/modes/interactive/interactive-mode.ts @@ -147,14 +147,19 @@ function isExpandable(obj: unknown): obj is Expandable { } class ExpandableText extends Text implements Expandable { + private readonly getCollapsedText: () => string; + private readonly getExpandedText: () => string; + constructor( - private readonly getCollapsedText: () => string, - private readonly getExpandedText: () => string, + getCollapsedText: () => string, + getExpandedText: () => string, expanded = false, paddingX = 0, paddingY = 0, ) { super(expanded ? getExpandedText() : getCollapsedText(), paddingX, paddingY); + this.getCollapsedText = getCollapsedText; + this.getExpandedText = getExpandedText; } setExpanded(expanded: boolean): void { @@ -334,6 +339,8 @@ export class InteractiveMode { // Custom header from extension (undefined = use built-in header) private customHeader: (Component & { dispose?(): void }) | undefined = undefined; + private options: InteractiveModeOptions; + // Convenience accessors private get session(): AgentSession { return this.runtimeHost.session; @@ -348,11 +355,9 @@ export class InteractiveMode { return this.session.settingsManager; } - constructor( - runtimeHost: AgentSessionRuntime, - private options: InteractiveModeOptions = {}, - ) { + constructor(runtimeHost: AgentSessionRuntime, options: InteractiveModeOptions = {}) { this.runtimeHost = runtimeHost; + this.options = options; this.runtimeHost.setBeforeSessionInvalidate(() => { this.resetExtensionUI(); }); diff --git a/packages/coding-agent/src/modes/rpc/rpc-client.ts b/packages/coding-agent/src/modes/rpc/rpc-client.ts index 00fd3cd9..194f479e 100644 --- a/packages/coding-agent/src/modes/rpc/rpc-client.ts +++ b/packages/coding-agent/src/modes/rpc/rpc-client.ts @@ -59,8 +59,11 @@ export class RpcClient { new Map(); private requestId = 0; private stderr = ""; + private options: RpcClientOptions; - constructor(private options: RpcClientOptions = {}) {} + constructor(options: RpcClientOptions = {}) { + this.options = options; + } /** * Start the RPC agent process. From ae9450dc51917afb2797e8e2a532a13073285f0f Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Wed, 20 May 2026 00:04:03 +0200 Subject: [PATCH 126/166] chore(ts): use source import extensions --- packages/agent/src/agent-loop.ts | 2 +- packages/agent/src/agent.ts | 6 +- packages/agent/src/harness/agent-harness.ts | 18 +- .../compaction/branch-summarization.ts | 14 +- .../src/harness/compaction/compaction.ts | 12 +- .../agent/src/harness/compaction/utils.ts | 2 +- packages/agent/src/harness/env/nodejs.ts | 2 +- packages/agent/src/harness/messages.ts | 4 +- .../agent/src/harness/prompt-templates.ts | 2 +- .../agent/src/harness/session/jsonl-repo.ts | 8 +- .../src/harness/session/jsonl-storage.ts | 8 +- .../agent/src/harness/session/memory-repo.ts | 6 +- .../src/harness/session/memory-storage.ts | 4 +- .../agent/src/harness/session/repo-utils.ts | 6 +- packages/agent/src/harness/session/session.ts | 8 +- packages/agent/src/harness/skills.ts | 2 +- packages/agent/src/harness/system-prompt.ts | 2 +- packages/agent/src/harness/types.ts | 8 +- .../agent/src/harness/utils/shell-output.ts | 4 +- packages/agent/src/index.ts | 38 ++-- packages/agent/src/node.ts | 4 +- packages/ai/scripts/generate-image-models.ts | 2 +- packages/ai/scripts/generate-models.ts | 2 +- packages/ai/src/api-registry.ts | 2 +- packages/ai/src/bedrock-provider.ts | 2 +- packages/ai/src/cli.ts | 4 +- packages/ai/src/env-api-keys.ts | 2 +- packages/ai/src/image-models.generated.ts | 17 +- packages/ai/src/image-models.ts | 4 +- packages/ai/src/images-api-registry.ts | 2 +- packages/ai/src/images.ts | 6 +- packages/ai/src/index.ts | 58 +++---- packages/ai/src/models.generated.ts | 102 +++++++++-- packages/ai/src/models.ts | 4 +- packages/ai/src/oauth.ts | 2 +- packages/ai/src/providers/amazon-bedrock.ts | 16 +- packages/ai/src/providers/anthropic.ts | 22 +-- .../src/providers/azure-openai-responses.ts | 16 +- packages/ai/src/providers/cloudflare.ts | 2 +- packages/ai/src/providers/faux.ts | 6 +- .../src/providers/github-copilot-headers.ts | 2 +- packages/ai/src/providers/google-shared.ts | 6 +- packages/ai/src/providers/google-vertex.ts | 14 +- packages/ai/src/providers/google.ts | 16 +- .../ai/src/providers/images/openrouter.ts | 8 +- .../src/providers/images/register-builtins.ts | 8 +- packages/ai/src/providers/mistral.ts | 18 +- .../src/providers/openai-codex-responses.ts | 20 +-- .../ai/src/providers/openai-completions.ts | 24 +-- .../src/providers/openai-responses-shared.ts | 14 +- packages/ai/src/providers/openai-responses.ts | 20 +-- .../ai/src/providers/register-builtins.ts | 47 ++--- packages/ai/src/providers/simple-options.ts | 2 +- .../ai/src/providers/transform-messages.ts | 2 +- packages/ai/src/stream.ts | 8 +- packages/ai/src/types.ts | 6 +- packages/ai/src/utils/event-stream.ts | 2 +- packages/ai/src/utils/oauth/anthropic.ts | 6 +- packages/ai/src/utils/oauth/github-copilot.ts | 6 +- packages/ai/src/utils/oauth/index.ts | 16 +- packages/ai/src/utils/oauth/openai-codex.ts | 6 +- packages/ai/src/utils/oauth/types.ts | 2 +- packages/ai/src/utils/overflow.ts | 2 +- packages/ai/src/utils/validation.ts | 2 +- .../examples/extensions/overlay-qa-tests.ts | 163 +++++++++++------- .../examples/extensions/overlay-test.ts | 11 +- packages/coding-agent/src/bun/cli.ts | 8 +- packages/coding-agent/src/cli.ts | 4 +- packages/coding-agent/src/cli/args.ts | 4 +- .../coding-agent/src/cli/config-selector.ts | 8 +- .../coding-agent/src/cli/file-processor.ts | 6 +- .../coding-agent/src/cli/initial-message.ts | 2 +- packages/coding-agent/src/cli/list-models.ts | 4 +- .../coding-agent/src/cli/session-picker.ts | 6 +- packages/coding-agent/src/config.ts | 2 +- .../src/core/agent-session-runtime.ts | 16 +- .../src/core/agent-session-services.ts | 16 +- .../coding-agent/src/core/agent-session.ts | 48 +++--- .../coding-agent/src/core/auth-guidance.ts | 2 +- .../coding-agent/src/core/auth-storage.ts | 4 +- .../coding-agent/src/core/bash-executor.ts | 8 +- .../core/compaction/branch-summarization.ts | 10 +- .../src/core/compaction/compaction.ts | 6 +- .../coding-agent/src/core/compaction/index.ts | 6 +- packages/coding-agent/src/core/exec.ts | 2 +- .../src/core/export-html/index.ts | 10 +- .../src/core/export-html/tool-renderer.ts | 6 +- .../coding-agent/src/core/extensions/index.ts | 16 +- .../src/core/extensions/loader.ts | 16 +- .../src/core/extensions/runner.ts | 14 +- .../coding-agent/src/core/extensions/types.ts | 38 ++-- .../src/core/extensions/wrapper.ts | 6 +- .../src/core/footer-data-provider.ts | 2 +- packages/coding-agent/src/core/index.ts | 16 +- packages/coding-agent/src/core/keybindings.ts | 2 +- .../coding-agent/src/core/model-registry.ts | 8 +- .../coding-agent/src/core/model-resolver.ts | 6 +- .../coding-agent/src/core/package-manager.ts | 12 +- .../coding-agent/src/core/prompt-templates.ts | 6 +- .../src/core/resolve-config-value.ts | 2 +- .../coding-agent/src/core/resource-loader.ts | 30 ++-- packages/coding-agent/src/core/sdk.ts | 42 ++--- .../coding-agent/src/core/session-manager.ts | 4 +- .../coding-agent/src/core/settings-manager.ts | 2 +- packages/coding-agent/src/core/skills.ts | 10 +- .../coding-agent/src/core/slash-commands.ts | 4 +- packages/coding-agent/src/core/source-info.ts | 2 +- .../coding-agent/src/core/system-prompt.ts | 4 +- packages/coding-agent/src/core/telemetry.ts | 2 +- packages/coding-agent/src/core/tools/bash.ts | 20 +-- .../coding-agent/src/core/tools/edit-diff.ts | 2 +- packages/coding-agent/src/core/tools/edit.ts | 22 +-- packages/coding-agent/src/core/tools/find.ts | 18 +- packages/coding-agent/src/core/tools/grep.ts | 18 +- packages/coding-agent/src/core/tools/index.ts | 34 ++-- packages/coding-agent/src/core/tools/ls.ts | 16 +- .../src/core/tools/output-accumulator.ts | 2 +- packages/coding-agent/src/core/tools/read.ts | 22 +-- .../src/core/tools/render-utils.ts | 4 +- .../src/core/tools/tool-definition-wrapper.ts | 2 +- packages/coding-agent/src/core/tools/write.ts | 18 +- packages/coding-agent/src/index.ts | 54 +++--- packages/coding-agent/src/main.ts | 56 +++--- packages/coding-agent/src/migrations.ts | 4 +- packages/coding-agent/src/modes/index.ts | 10 +- .../src/modes/interactive/components/armin.ts | 2 +- .../components/assistant-message.ts | 2 +- .../interactive/components/bash-execution.ts | 12 +- .../interactive/components/bordered-loader.ts | 6 +- .../components/branch-summary-message.ts | 6 +- .../components/compaction-summary-message.ts | 6 +- .../interactive/components/config-selector.ts | 12 +- .../interactive/components/custom-editor.ts | 2 +- .../interactive/components/custom-message.ts | 6 +- .../modes/interactive/components/daxnuts.ts | 2 +- .../src/modes/interactive/components/diff.ts | 2 +- .../interactive/components/dynamic-border.ts | 2 +- .../components/earendil-announcement.ts | 6 +- .../components/extension-editor.ts | 8 +- .../interactive/components/extension-input.ts | 8 +- .../components/extension-selector.ts | 8 +- .../modes/interactive/components/footer.ts | 6 +- .../src/modes/interactive/components/index.ts | 62 +++---- .../components/keybinding-hints.ts | 2 +- .../interactive/components/login-dialog.ts | 6 +- .../interactive/components/model-selector.ts | 10 +- .../interactive/components/oauth-selector.ts | 6 +- .../components/scoped-models-selector.ts | 6 +- .../components/session-selector-search.ts | 2 +- .../components/session-selector.ts | 14 +- .../components/settings-selector.ts | 8 +- .../components/show-images-selector.ts | 4 +- .../components/skill-invocation-message.ts | 6 +- .../interactive/components/theme-selector.ts | 4 +- .../components/thinking-selector.ts | 4 +- .../interactive/components/tool-execution.ts | 10 +- .../interactive/components/tree-selector.ts | 8 +- .../components/user-message-selector.ts | 4 +- .../interactive/components/user-message.ts | 2 +- .../src/modes/interactive/interactive-mode.ts | 110 ++++++------ .../src/modes/interactive/theme/theme.ts | 8 +- packages/coding-agent/src/modes/print-mode.ts | 6 +- .../coding-agent/src/modes/rpc/rpc-client.ts | 10 +- .../coding-agent/src/modes/rpc/rpc-mode.ts | 16 +- .../coding-agent/src/modes/rpc/rpc-types.ts | 8 +- .../coding-agent/src/package-manager-cli.ts | 14 +- packages/coding-agent/src/utils/changelog.ts | 2 +- .../coding-agent/src/utils/clipboard-image.ts | 4 +- packages/coding-agent/src/utils/clipboard.ts | 4 +- .../src/utils/exif-orientation.ts | 2 +- .../coding-agent/src/utils/image-convert.ts | 4 +- .../coding-agent/src/utils/image-resize.ts | 4 +- packages/coding-agent/src/utils/shell.ts | 2 +- .../src/utils/syntax-highlight.ts | 2 +- .../coding-agent/src/utils/tools-manager.ts | 2 +- .../coding-agent/src/utils/version-check.ts | 2 +- .../src/utils/windows-self-update.ts | 2 +- packages/tui/src/autocomplete.ts | 2 +- packages/tui/src/components/box.ts | 4 +- .../tui/src/components/cancellable-loader.ts | 4 +- packages/tui/src/components/editor.ts | 16 +- packages/tui/src/components/image.ts | 4 +- packages/tui/src/components/input.ts | 12 +- packages/tui/src/components/loader.ts | 16 +- packages/tui/src/components/markdown.ts | 6 +- packages/tui/src/components/select-list.ts | 6 +- packages/tui/src/components/settings-list.ts | 10 +- packages/tui/src/components/spacer.ts | 2 +- packages/tui/src/components/text.ts | 4 +- packages/tui/src/components/truncated-text.ts | 4 +- packages/tui/src/editor-component.ts | 4 +- packages/tui/src/index.ts | 44 ++--- packages/tui/src/keybindings.ts | 2 +- packages/tui/src/terminal.ts | 4 +- packages/tui/src/tui.ts | 8 +- packages/tui/test/markdown.test.ts | 5 +- .../tui/test/overlay-non-capturing.test.ts | 11 +- packages/tui/test/overlay-options.test.ts | 11 +- .../tui/test/overlay-short-content.test.ts | 7 +- .../tui/test/tui-overlay-style-leak.test.ts | 12 +- packages/web-ui/src/ChatPanel.ts | 18 +- .../web-ui/src/components/AgentInterface.ts | 26 +-- .../web-ui/src/components/AttachmentTile.ts | 6 +- .../web-ui/src/components/ConsoleBlock.ts | 2 +- .../src/components/CustomProviderCard.ts | 2 +- packages/web-ui/src/components/Input.ts | 2 +- .../web-ui/src/components/MessageEditor.ts | 6 +- packages/web-ui/src/components/MessageList.ts | 2 +- packages/web-ui/src/components/Messages.ts | 10 +- .../web-ui/src/components/ProviderKeyInput.ts | 6 +- .../web-ui/src/components/SandboxedIframe.ts | 8 +- .../sandbox/ArtifactsRuntimeProvider.ts | 18 +- .../sandbox/AttachmentsRuntimeProvider.ts | 12 +- .../sandbox/ConsoleRuntimeProvider.ts | 2 +- .../sandbox/FileDownloadRuntimeProvider.ts | 2 +- .../sandbox/RuntimeMessageRouter.ts | 2 +- .../web-ui/src/dialogs/ApiKeyPromptDialog.ts | 6 +- .../web-ui/src/dialogs/AttachmentOverlay.ts | 4 +- .../src/dialogs/CustomProviderDialog.ts | 6 +- packages/web-ui/src/dialogs/ModelSelector.ts | 12 +- .../src/dialogs/PersistentStorageDialog.ts | 2 +- .../web-ui/src/dialogs/ProvidersModelsTab.ts | 14 +- .../web-ui/src/dialogs/SessionListDialog.ts | 8 +- packages/web-ui/src/dialogs/SettingsDialog.ts | 4 +- packages/web-ui/src/index.ts | 130 +++++++------- packages/web-ui/src/storage/app-storage.ts | 10 +- .../backends/indexeddb-storage-backend.ts | 7 +- packages/web-ui/src/storage/store.ts | 2 +- .../storage/stores/custom-providers-store.ts | 4 +- .../src/storage/stores/provider-keys-store.ts | 4 +- .../src/storage/stores/sessions-store.ts | 4 +- .../src/storage/stores/settings-store.ts | 4 +- .../src/tools/artifacts/ArtifactPill.ts | 2 +- .../web-ui/src/tools/artifacts/Console.ts | 2 +- .../src/tools/artifacts/DocxArtifact.ts | 4 +- .../src/tools/artifacts/ExcelArtifact.ts | 4 +- .../src/tools/artifacts/GenericArtifact.ts | 4 +- .../src/tools/artifacts/HtmlArtifact.ts | 16 +- .../src/tools/artifacts/ImageArtifact.ts | 4 +- .../src/tools/artifacts/MarkdownArtifact.ts | 4 +- .../web-ui/src/tools/artifacts/PdfArtifact.ts | 4 +- .../web-ui/src/tools/artifacts/SvgArtifact.ts | 4 +- .../src/tools/artifacts/TextArtifact.ts | 4 +- .../artifacts/artifacts-tool-renderer.ts | 18 +- .../web-ui/src/tools/artifacts/artifacts.ts | 34 ++-- packages/web-ui/src/tools/artifacts/index.ts | 14 +- packages/web-ui/src/tools/extract-document.ts | 10 +- packages/web-ui/src/tools/index.ts | 12 +- packages/web-ui/src/tools/javascript-repl.ts | 12 +- .../web-ui/src/tools/renderer-registry.ts | 2 +- .../src/tools/renderers/BashRenderer.ts | 6 +- .../src/tools/renderers/CalculateRenderer.ts | 6 +- .../src/tools/renderers/DefaultRenderer.ts | 6 +- .../tools/renderers/GetCurrentTimeRenderer.ts | 6 +- packages/web-ui/src/utils/attachment-utils.ts | 2 +- packages/web-ui/src/utils/auth-token.ts | 2 +- packages/web-ui/tsconfig.build.json | 2 + scripts/update-source-imports-to-ts.sh | 10 ++ tsconfig.base.json | 4 +- 259 files changed, 1483 insertions(+), 1300 deletions(-) create mode 100644 scripts/update-source-imports-to-ts.sh diff --git a/packages/agent/src/agent-loop.ts b/packages/agent/src/agent-loop.ts index eb57039f..28f037f5 100644 --- a/packages/agent/src/agent-loop.ts +++ b/packages/agent/src/agent-loop.ts @@ -20,7 +20,7 @@ import type { AgentToolCall, AgentToolResult, StreamFn, -} from "./types.js"; +} from "./types.ts"; export type AgentEventSink = (event: AgentEvent) => Promise | void; diff --git a/packages/agent/src/agent.ts b/packages/agent/src/agent.ts index b9e0484c..db6684a8 100644 --- a/packages/agent/src/agent.ts +++ b/packages/agent/src/agent.ts @@ -8,7 +8,7 @@ import { type ThinkingBudgets, type Transport, } from "@earendil-works/pi-ai"; -import { runAgentLoop, runAgentLoopContinue } from "./agent-loop.js"; +import { runAgentLoop, runAgentLoopContinue } from "./agent-loop.ts"; import type { AfterToolCallContext, AfterToolCallResult, @@ -24,9 +24,9 @@ import type { QueueMode, StreamFn, ToolExecutionMode, -} from "./types.js"; +} from "./types.ts"; -export type { QueueMode } from "./types.js"; +export type { QueueMode } from "./types.ts"; function defaultConvertToLlm(messages: AgentMessage[]): Message[] { return messages.filter( diff --git a/packages/agent/src/harness/agent-harness.ts b/packages/agent/src/harness/agent-harness.ts index 6ab5dbef..80535e8e 100644 --- a/packages/agent/src/harness/agent-harness.ts +++ b/packages/agent/src/harness/agent-harness.ts @@ -5,7 +5,7 @@ import { streamSimple, type UserMessage, } from "@earendil-works/pi-ai"; -import { runAgentLoop } from "../agent-loop.js"; +import { runAgentLoop } from "../agent-loop.ts"; import type { AgentContext, AgentEvent, @@ -15,12 +15,12 @@ import type { QueueMode, StreamFn, ThinkingLevel, -} from "../types.js"; -import { collectEntriesForBranchSummary, generateBranchSummary } from "./compaction/branch-summarization.js"; -import { compact, DEFAULT_COMPACTION_SETTINGS, prepareCompaction } from "./compaction/compaction.js"; -import { convertToLlm } from "./messages.js"; -import { formatPromptTemplateInvocation } from "./prompt-templates.js"; -import { formatSkillInvocation } from "./skills.js"; +} from "../types.ts"; +import { collectEntriesForBranchSummary, generateBranchSummary } from "./compaction/branch-summarization.ts"; +import { compact, DEFAULT_COMPACTION_SETTINGS, prepareCompaction } from "./compaction/compaction.ts"; +import { convertToLlm } from "./messages.ts"; +import { formatPromptTemplateInvocation } from "./prompt-templates.ts"; +import { formatSkillInvocation } from "./skills.ts"; import type { AbortResult, AgentHarnessEvent, @@ -37,8 +37,8 @@ import type { PromptTemplate, Session, Skill, -} from "./types.js"; -import { AgentHarnessError, BranchSummaryError, CompactionError, SessionError, toError } from "./types.js"; +} from "./types.ts"; +import { AgentHarnessError, BranchSummaryError, CompactionError, SessionError, toError } from "./types.ts"; function createUserMessage(text: string, images?: ImageContent[]): UserMessage { const content: Array<{ type: "text"; text: string } | ImageContent> = [{ type: "text", text }]; diff --git a/packages/agent/src/harness/compaction/branch-summarization.ts b/packages/agent/src/harness/compaction/branch-summarization.ts index 37af3b15..6ca4a430 100644 --- a/packages/agent/src/harness/compaction/branch-summarization.ts +++ b/packages/agent/src/harness/compaction/branch-summarization.ts @@ -1,15 +1,15 @@ import type { Model } from "@earendil-works/pi-ai"; import { completeSimple } from "@earendil-works/pi-ai"; -import type { AgentMessage } from "../../types.js"; +import type { AgentMessage } from "../../types.ts"; import { convertToLlm, createBranchSummaryMessage, createCompactionSummaryMessage, createCustomMessage, -} from "../messages.js"; -import type { BranchSummaryResult, Session, SessionTreeEntry } from "../types.js"; -import { BranchSummaryError, err, ok, type Result, SessionError } from "../types.js"; -import { estimateTokens, SUMMARIZATION_SYSTEM_PROMPT } from "./compaction.js"; +} from "../messages.ts"; +import type { BranchSummaryResult, Session, SessionTreeEntry } from "../types.ts"; +import { BranchSummaryError, err, ok, type Result, SessionError } from "../types.ts"; +import { estimateTokens, SUMMARIZATION_SYSTEM_PROMPT } from "./compaction.ts"; import { computeFileLists, createFileOps, @@ -17,7 +17,7 @@ import { type FileOperations, formatFileOperations, serializeConversation, -} from "./utils.js"; +} from "./utils.ts"; /** File-operation details stored on generated branch summary entries. */ export interface BranchSummaryDetails { @@ -27,7 +27,7 @@ export interface BranchSummaryDetails { modifiedFiles: string[]; } -export type { FileOperations } from "./utils.js"; +export type { FileOperations } from "./utils.ts"; /** Prepared branch content for summarization. */ export interface BranchPreparation { diff --git a/packages/agent/src/harness/compaction/compaction.ts b/packages/agent/src/harness/compaction/compaction.ts index a04ccd07..aa7b32c6 100644 --- a/packages/agent/src/harness/compaction/compaction.ts +++ b/packages/agent/src/harness/compaction/compaction.ts @@ -1,14 +1,14 @@ import type { AssistantMessage, ImageContent, Model, TextContent, Usage } from "@earendil-works/pi-ai"; import { completeSimple } from "@earendil-works/pi-ai"; -import type { AgentMessage, ThinkingLevel } from "../../types.js"; +import type { AgentMessage, ThinkingLevel } from "../../types.ts"; import { convertToLlm, createBranchSummaryMessage, createCompactionSummaryMessage, createCustomMessage, -} from "../messages.js"; -import { buildSessionContext } from "../session/session.js"; -import { type CompactionEntry, CompactionError, err, ok, type Result, type SessionTreeEntry } from "../types.js"; +} from "../messages.ts"; +import { buildSessionContext } from "../session/session.ts"; +import { type CompactionEntry, CompactionError, err, ok, type Result, type SessionTreeEntry } from "../types.ts"; import { computeFileLists, createFileOps, @@ -16,7 +16,7 @@ import { type FileOperations, formatFileOperations, serializeConversation, -} from "./utils.js"; +} from "./utils.ts"; /** File-operation details stored on generated compaction entries. */ export interface CompactionDetails { @@ -620,7 +620,7 @@ Summarize the prefix to provide context for the retained suffix: Be concise. Focus on what's needed to understand the kept suffix.`; -export { serializeConversation } from "./utils.js"; +export { serializeConversation } from "./utils.ts"; /** Generate compaction summary data from prepared session history. */ export async function compact( diff --git a/packages/agent/src/harness/compaction/utils.ts b/packages/agent/src/harness/compaction/utils.ts index 2b852a46..07535b79 100644 --- a/packages/agent/src/harness/compaction/utils.ts +++ b/packages/agent/src/harness/compaction/utils.ts @@ -1,5 +1,5 @@ import type { Message } from "@earendil-works/pi-ai"; -import type { AgentMessage } from "../../types.js"; +import type { AgentMessage } from "../../types.ts"; /** File paths touched by a session branch or compaction range. */ export interface FileOperations { diff --git a/packages/agent/src/harness/env/nodejs.ts b/packages/agent/src/harness/env/nodejs.ts index 931651bc..e56e7aeb 100644 --- a/packages/agent/src/harness/env/nodejs.ts +++ b/packages/agent/src/harness/env/nodejs.ts @@ -26,7 +26,7 @@ import { ok, type Result, toError, -} from "../types.js"; +} from "../types.ts"; function resolvePath(cwd: string, path: string): string { return isAbsolute(path) ? path : resolve(cwd, path); diff --git a/packages/agent/src/harness/messages.ts b/packages/agent/src/harness/messages.ts index 615bf1e4..36ce96a1 100644 --- a/packages/agent/src/harness/messages.ts +++ b/packages/agent/src/harness/messages.ts @@ -1,5 +1,5 @@ import type { ImageContent, Message, TextContent } from "@earendil-works/pi-ai"; -import type { AgentMessage } from "../types.js"; +import type { AgentMessage } from "../types.ts"; export const COMPACTION_SUMMARY_PREFIX = `The conversation history before this point was compacted into the following summary: @@ -51,7 +51,7 @@ export interface CompactionSummaryMessage { timestamp: number; } -declare module "../types.js" { +declare module "../types.ts" { interface CustomAgentMessages { bashExecution: BashExecutionMessage; custom: CustomMessage; diff --git a/packages/agent/src/harness/prompt-templates.ts b/packages/agent/src/harness/prompt-templates.ts index 3e678cfc..6c1b6be0 100644 --- a/packages/agent/src/harness/prompt-templates.ts +++ b/packages/agent/src/harness/prompt-templates.ts @@ -1,5 +1,5 @@ import { parse } from "yaml"; -import { type ExecutionEnv, type FileInfo, type PromptTemplate, type Result, toError } from "./types.js"; +import { type ExecutionEnv, type FileInfo, type PromptTemplate, type Result, toError } from "./types.ts"; export type PromptTemplateDiagnosticCode = "file_info_failed" | "list_failed" | "read_failed" | "parse_failed"; diff --git a/packages/agent/src/harness/session/jsonl-repo.ts b/packages/agent/src/harness/session/jsonl-repo.ts index cd6ac1d8..1a08d4d3 100644 --- a/packages/agent/src/harness/session/jsonl-repo.ts +++ b/packages/agent/src/harness/session/jsonl-repo.ts @@ -5,16 +5,16 @@ import type { JsonlSessionMetadata, JsonlSessionRepoApi, Session, -} from "../types.js"; -import { SessionError, toError } from "../types.js"; -import { JsonlSessionStorage, loadJsonlSessionMetadata } from "./jsonl-storage.js"; +} from "../types.ts"; +import { SessionError, toError } from "../types.ts"; +import { JsonlSessionStorage, loadJsonlSessionMetadata } from "./jsonl-storage.ts"; import { createSessionId, createTimestamp, getEntriesToFork, getFileSystemResultOrThrow, toSession, -} from "./repo-utils.js"; +} from "./repo-utils.ts"; type JsonlSessionRepoFileSystem = Pick< FileSystem, diff --git a/packages/agent/src/harness/session/jsonl-storage.ts b/packages/agent/src/harness/session/jsonl-storage.ts index eac8334d..54ad9eb0 100644 --- a/packages/agent/src/harness/session/jsonl-storage.ts +++ b/packages/agent/src/harness/session/jsonl-storage.ts @@ -1,7 +1,7 @@ -import type { FileSystem, JsonlSessionMetadata, LeafEntry, SessionStorage, SessionTreeEntry } from "../types.js"; -import { SessionError, toError } from "../types.js"; -import { getFileSystemResultOrThrow } from "./repo-utils.js"; -import { uuidv7 } from "./uuid.js"; +import type { FileSystem, JsonlSessionMetadata, LeafEntry, SessionStorage, SessionTreeEntry } from "../types.ts"; +import { SessionError, toError } from "../types.ts"; +import { getFileSystemResultOrThrow } from "./repo-utils.ts"; +import { uuidv7 } from "./uuid.ts"; type JsonlSessionStorageFileSystem = Pick; diff --git a/packages/agent/src/harness/session/memory-repo.ts b/packages/agent/src/harness/session/memory-repo.ts index 54095514..145bc2b7 100644 --- a/packages/agent/src/harness/session/memory-repo.ts +++ b/packages/agent/src/harness/session/memory-repo.ts @@ -1,6 +1,6 @@ -import { type Session, SessionError, type SessionMetadata, type SessionRepo } from "../types.js"; -import { InMemorySessionStorage } from "./memory-storage.js"; -import { createSessionId, createTimestamp, getEntriesToFork, toSession } from "./repo-utils.js"; +import { type Session, SessionError, type SessionMetadata, type SessionRepo } from "../types.ts"; +import { InMemorySessionStorage } from "./memory-storage.ts"; +import { createSessionId, createTimestamp, getEntriesToFork, toSession } from "./repo-utils.ts"; export class InMemorySessionRepo implements SessionRepo { private sessions = new Map>(); diff --git a/packages/agent/src/harness/session/memory-storage.ts b/packages/agent/src/harness/session/memory-storage.ts index 8c45685b..c6428cac 100644 --- a/packages/agent/src/harness/session/memory-storage.ts +++ b/packages/agent/src/harness/session/memory-storage.ts @@ -4,8 +4,8 @@ import { type SessionMetadata, type SessionStorage, type SessionTreeEntry, -} from "../types.js"; -import { uuidv7 } from "./uuid.js"; +} from "../types.ts"; +import { uuidv7 } from "./uuid.ts"; function updateLabelCache(labelsById: Map, entry: SessionTreeEntry): void { if (entry.type !== "label") return; diff --git a/packages/agent/src/harness/session/repo-utils.ts b/packages/agent/src/harness/session/repo-utils.ts index f98b1256..a25b62de 100644 --- a/packages/agent/src/harness/session/repo-utils.ts +++ b/packages/agent/src/harness/session/repo-utils.ts @@ -5,9 +5,9 @@ import { type SessionMetadata, type SessionStorage, type SessionTreeEntry, -} from "../types.js"; -import { Session } from "./session.js"; -import { uuidv7 } from "./uuid.js"; +} from "../types.ts"; +import { Session } from "./session.ts"; +import { uuidv7 } from "./uuid.ts"; export function createSessionId(): string { return uuidv7(); diff --git a/packages/agent/src/harness/session/session.ts b/packages/agent/src/harness/session/session.ts index 0b0b3e1e..4baf50ec 100644 --- a/packages/agent/src/harness/session/session.ts +++ b/packages/agent/src/harness/session/session.ts @@ -1,6 +1,6 @@ import type { ImageContent, TextContent } from "@earendil-works/pi-ai"; -import type { AgentMessage } from "../../types.js"; -import { createBranchSummaryMessage, createCompactionSummaryMessage, createCustomMessage } from "../messages.js"; +import type { AgentMessage } from "../../types.ts"; +import { createBranchSummaryMessage, createCompactionSummaryMessage, createCustomMessage } from "../messages.ts"; import type { BranchSummaryEntry, CompactionEntry, @@ -15,8 +15,8 @@ import type { SessionStorage, SessionTreeEntry, ThinkingLevelChangeEntry, -} from "../types.js"; -import { SessionError } from "../types.js"; +} from "../types.ts"; +import { SessionError } from "../types.ts"; export function buildSessionContext(pathEntries: SessionTreeEntry[]): SessionContext { let thinkingLevel = "off"; diff --git a/packages/agent/src/harness/skills.ts b/packages/agent/src/harness/skills.ts index 7269857e..a1a4c0c4 100644 --- a/packages/agent/src/harness/skills.ts +++ b/packages/agent/src/harness/skills.ts @@ -1,6 +1,6 @@ import ignore from "ignore"; import { parse } from "yaml"; -import { type ExecutionEnv, type FileInfo, type Result, type Skill, toError } from "./types.js"; +import { type ExecutionEnv, type FileInfo, type Result, type Skill, toError } from "./types.ts"; const MAX_NAME_LENGTH = 64; const MAX_DESCRIPTION_LENGTH = 1024; diff --git a/packages/agent/src/harness/system-prompt.ts b/packages/agent/src/harness/system-prompt.ts index 44b8f623..ec121da2 100644 --- a/packages/agent/src/harness/system-prompt.ts +++ b/packages/agent/src/harness/system-prompt.ts @@ -1,4 +1,4 @@ -import type { Skill } from "./types.js"; +import type { Skill } from "./types.ts"; export function formatSkillsForSystemPrompt(skills: Skill[]): string { const visibleSkills = skills.filter((skill) => !skill.disableModelInvocation); diff --git a/packages/agent/src/harness/types.ts b/packages/agent/src/harness/types.ts index c9773be4..2d9096c4 100644 --- a/packages/agent/src/harness/types.ts +++ b/packages/agent/src/harness/types.ts @@ -1,6 +1,6 @@ import type { ImageContent, Model, SimpleStreamOptions, TextContent, Transport } from "@earendil-works/pi-ai"; -import type { AgentEvent, AgentMessage, AgentTool, QueueMode, ThinkingLevel } from "../index.js"; -import type { Session } from "./session/session.js"; +import type { AgentEvent, AgentMessage, AgentTool, QueueMode, ThinkingLevel } from "../index.ts"; +import type { Session } from "./session/session.ts"; /** Result of a fallible operation. Expected failures are returned as `ok: false` instead of thrown. */ export type Result = { ok: true; value: TValue } | { ok: false; error: TError }; @@ -446,7 +446,7 @@ export interface SessionStorage; } -export type { Session } from "./session/session.js"; +export type { Session } from "./session/session.ts"; export interface SessionCreateOptions { id?: string; @@ -812,4 +812,4 @@ export interface AgentHarnessOptions< followUpMode?: QueueMode; } -export type { AgentHarness } from "./agent-harness.js"; +export type { AgentHarness } from "./agent-harness.ts"; diff --git a/packages/agent/src/harness/utils/shell-output.ts b/packages/agent/src/harness/utils/shell-output.ts index 23f1362c..fea46d95 100644 --- a/packages/agent/src/harness/utils/shell-output.ts +++ b/packages/agent/src/harness/utils/shell-output.ts @@ -6,8 +6,8 @@ import { ok, type Result, toError, -} from "../types.js"; -import { DEFAULT_MAX_BYTES, truncateTail } from "./truncate.js"; +} from "../types.ts"; +import { DEFAULT_MAX_BYTES, truncateTail } from "./truncate.ts"; export interface ShellCaptureOptions extends Omit { onChunk?: (chunk: string) => void; diff --git a/packages/agent/src/index.ts b/packages/agent/src/index.ts index 04275403..fd3c2b9f 100644 --- a/packages/agent/src/index.ts +++ b/packages/agent/src/index.ts @@ -1,8 +1,8 @@ // Core Agent -export * from "./agent.js"; +export * from "./agent.ts"; // Loop functions -export * from "./agent-loop.js"; -export * from "./harness/agent-harness.js"; +export * from "./agent-loop.ts"; +export * from "./harness/agent-harness.ts"; export { type BranchPreparation, type BranchSummaryDetails, @@ -10,7 +10,7 @@ export { collectEntriesForBranchSummary, generateBranchSummary, prepareBranchEntries, -} from "./harness/compaction/branch-summarization.js"; +} from "./harness/compaction/branch-summarization.ts"; export { calculateContextTokens, compact, @@ -24,21 +24,21 @@ export { prepareCompaction, serializeConversation, shouldCompact, -} from "./harness/compaction/compaction.js"; -export * from "./harness/messages.js"; -export * from "./harness/prompt-templates.js"; -export * from "./harness/session/jsonl-repo.js"; -export * from "./harness/session/memory-repo.js"; -export * from "./harness/session/repo-utils.js"; -export * from "./harness/session/session.js"; -export { uuidv7 } from "./harness/session/uuid.js"; -export * from "./harness/skills.js"; -export * from "./harness/system-prompt.js"; +} from "./harness/compaction/compaction.ts"; +export * from "./harness/messages.ts"; +export * from "./harness/prompt-templates.ts"; +export * from "./harness/session/jsonl-repo.ts"; +export * from "./harness/session/memory-repo.ts"; +export * from "./harness/session/repo-utils.ts"; +export * from "./harness/session/session.ts"; +export { uuidv7 } from "./harness/session/uuid.ts"; +export * from "./harness/skills.ts"; +export * from "./harness/system-prompt.ts"; // Harness -export * from "./harness/types.js"; -export * from "./harness/utils/shell-output.js"; -export * from "./harness/utils/truncate.js"; +export * from "./harness/types.ts"; +export * from "./harness/utils/shell-output.ts"; +export * from "./harness/utils/truncate.ts"; // Proxy utilities -export * from "./proxy.js"; +export * from "./proxy.ts"; // Types -export * from "./types.js"; +export * from "./types.ts"; diff --git a/packages/agent/src/node.ts b/packages/agent/src/node.ts index 23ccae43..b6f53a5e 100644 --- a/packages/agent/src/node.ts +++ b/packages/agent/src/node.ts @@ -1,2 +1,2 @@ -export { NodeExecutionEnv } from "./harness/env/nodejs.js"; -export * from "./index.js"; +export { NodeExecutionEnv } from "./harness/env/nodejs.ts"; +export * from "./index.ts"; diff --git a/packages/ai/scripts/generate-image-models.ts b/packages/ai/scripts/generate-image-models.ts index bd0c6727..c312f3e4 100644 --- a/packages/ai/scripts/generate-image-models.ts +++ b/packages/ai/scripts/generate-image-models.ts @@ -109,7 +109,7 @@ function generateImageModelsFile(models: ImagesModel<"openrouter-images">[]): st return `// This file is auto-generated by scripts/generate-image-models.ts // Do not edit manually - run 'npm run generate-image-models' to update -import type { ImagesApi, ImagesModel } from "./types.js"; +import type { ImagesApi, ImagesModel } from "./types.ts"; export const IMAGE_MODELS = { ${providerEntries} diff --git a/packages/ai/scripts/generate-models.ts b/packages/ai/scripts/generate-models.ts index f752f23b..e896726c 100644 --- a/packages/ai/scripts/generate-models.ts +++ b/packages/ai/scripts/generate-models.ts @@ -1863,7 +1863,7 @@ async function generateModels() { let output = `// This file is auto-generated by scripts/generate-models.ts // Do not edit manually - run 'npm run generate-models' to update -import type { Model } from "./types.js"; +import type { Model } from "./types.ts"; export const MODELS = { `; diff --git a/packages/ai/src/api-registry.ts b/packages/ai/src/api-registry.ts index ca0e0610..d86dd3e9 100644 --- a/packages/ai/src/api-registry.ts +++ b/packages/ai/src/api-registry.ts @@ -6,7 +6,7 @@ import type { SimpleStreamOptions, StreamFunction, StreamOptions, -} from "./types.js"; +} from "./types.ts"; export type ApiStreamFunction = ( model: Model, diff --git a/packages/ai/src/bedrock-provider.ts b/packages/ai/src/bedrock-provider.ts index 07305418..cf08b33e 100644 --- a/packages/ai/src/bedrock-provider.ts +++ b/packages/ai/src/bedrock-provider.ts @@ -1,4 +1,4 @@ -import { streamBedrock, streamSimpleBedrock } from "./providers/amazon-bedrock.js"; +import { streamBedrock, streamSimpleBedrock } from "./providers/amazon-bedrock.ts"; export const bedrockProviderModule = { streamBedrock, diff --git a/packages/ai/src/cli.ts b/packages/ai/src/cli.ts index 7a4ff1fd..38ee7e34 100644 --- a/packages/ai/src/cli.ts +++ b/packages/ai/src/cli.ts @@ -2,8 +2,8 @@ import { createInterface } from "node:readline"; import { existsSync, readFileSync, writeFileSync } from "fs"; -import { getOAuthProvider, getOAuthProviders } from "./utils/oauth/index.js"; -import type { OAuthCredentials, OAuthProviderId } from "./utils/oauth/types.js"; +import { getOAuthProvider, getOAuthProviders } from "./utils/oauth/index.ts"; +import type { OAuthCredentials, OAuthProviderId } from "./utils/oauth/types.ts"; const AUTH_FILE = "auth.json"; const PROVIDERS = getOAuthProviders(); diff --git a/packages/ai/src/env-api-keys.ts b/packages/ai/src/env-api-keys.ts index 6ed76d41..4aa0f760 100644 --- a/packages/ai/src/env-api-keys.ts +++ b/packages/ai/src/env-api-keys.ts @@ -23,7 +23,7 @@ if (typeof process !== "undefined" && (process.versions?.node || process.version }); } -import type { KnownProvider } from "./types.js"; +import type { KnownProvider } from "./types.ts"; let _procEnvCache: Map | null = null; diff --git a/packages/ai/src/image-models.generated.ts b/packages/ai/src/image-models.generated.ts index 64836aa0..f5305b11 100644 --- a/packages/ai/src/image-models.generated.ts +++ b/packages/ai/src/image-models.generated.ts @@ -1,7 +1,7 @@ // This file is auto-generated by scripts/generate-image-models.ts // Do not edit manually - run 'npm run generate-image-models' to update -import type { ImagesApi, ImagesModel } from "./types.js"; +import type { ImagesApi, ImagesModel } from "./types.ts"; export const IMAGE_MODELS = { openrouter: { @@ -425,5 +425,20 @@ export const IMAGE_MODELS = { cacheWrite: 0, }, } satisfies ImagesModel<"openrouter-images">, + "x-ai/grok-imagine-image-quality": { + id: "x-ai/grok-imagine-image-quality", + name: "xAI: Grok Imagine Image Quality", + api: "openrouter-images", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + input: ["text", "image"], + output: ["image"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + } satisfies ImagesModel<"openrouter-images">, }, } as const satisfies Record>>; diff --git a/packages/ai/src/image-models.ts b/packages/ai/src/image-models.ts index afa4f415..e8eb59bf 100644 --- a/packages/ai/src/image-models.ts +++ b/packages/ai/src/image-models.ts @@ -1,5 +1,5 @@ -import { IMAGE_MODELS } from "./image-models.generated.js"; -import type { ImagesApi, ImagesModel, KnownImagesProvider } from "./types.js"; +import { IMAGE_MODELS } from "./image-models.generated.ts"; +import type { ImagesApi, ImagesModel, KnownImagesProvider } from "./types.ts"; const imageModelRegistry: Map>> = new Map(); diff --git a/packages/ai/src/images-api-registry.ts b/packages/ai/src/images-api-registry.ts index 2a0c0fd9..4758439b 100644 --- a/packages/ai/src/images-api-registry.ts +++ b/packages/ai/src/images-api-registry.ts @@ -1,4 +1,4 @@ -import type { AssistantImages, ImagesApi, ImagesContext, ImagesFunction, ImagesModel, ImagesOptions } from "./types.js"; +import type { AssistantImages, ImagesApi, ImagesContext, ImagesFunction, ImagesModel, ImagesOptions } from "./types.ts"; export type ImagesApiFunction = ( model: ImagesModel, diff --git a/packages/ai/src/images.ts b/packages/ai/src/images.ts index 88eb09f2..47d0eecc 100644 --- a/packages/ai/src/images.ts +++ b/packages/ai/src/images.ts @@ -1,7 +1,7 @@ -import "./providers/images/register-builtins.js"; +import "./providers/images/register-builtins.ts"; -import { getImagesApiProvider } from "./images-api-registry.js"; -import type { AssistantImages, ImagesApi, ImagesContext, ImagesModel, ProviderImagesOptions } from "./types.js"; +import { getImagesApiProvider } from "./images-api-registry.ts"; +import type { AssistantImages, ImagesApi, ImagesContext, ImagesModel, ProviderImagesOptions } from "./types.ts"; function resolveImagesApiProvider(api: ImagesApi) { const provider = getImagesApiProvider(api); diff --git a/packages/ai/src/index.ts b/packages/ai/src/index.ts index 0a5ce4ed..fd06fe81 100644 --- a/packages/ai/src/index.ts +++ b/packages/ai/src/index.ts @@ -1,34 +1,34 @@ export type { Static, TSchema } from "typebox"; export { Type } from "typebox"; -export * from "./api-registry.js"; -export * from "./env-api-keys.js"; -export * from "./image-models.js"; -export * from "./images.js"; -export * from "./images-api-registry.js"; -export * from "./models.js"; -export type { BedrockOptions, BedrockThinkingDisplay } from "./providers/amazon-bedrock.js"; -export type { AnthropicEffort, AnthropicOptions, AnthropicThinkingDisplay } from "./providers/anthropic.js"; -export type { AzureOpenAIResponsesOptions } from "./providers/azure-openai-responses.js"; -export * from "./providers/faux.js"; -export type { GoogleOptions } from "./providers/google.js"; -export type { GoogleThinkingLevel } from "./providers/google-shared.js"; -export type { GoogleVertexOptions } from "./providers/google-vertex.js"; -export * from "./providers/images/register-builtins.js"; -export type { MistralOptions } from "./providers/mistral.js"; +export * from "./api-registry.ts"; +export * from "./env-api-keys.ts"; +export * from "./image-models.ts"; +export * from "./images.ts"; +export * from "./images-api-registry.ts"; +export * from "./models.ts"; +export type { BedrockOptions, BedrockThinkingDisplay } from "./providers/amazon-bedrock.ts"; +export type { AnthropicEffort, AnthropicOptions, AnthropicThinkingDisplay } from "./providers/anthropic.ts"; +export type { AzureOpenAIResponsesOptions } from "./providers/azure-openai-responses.ts"; +export * from "./providers/faux.ts"; +export type { GoogleOptions } from "./providers/google.ts"; +export type { GoogleThinkingLevel } from "./providers/google-shared.ts"; +export type { GoogleVertexOptions } from "./providers/google-vertex.ts"; +export * from "./providers/images/register-builtins.ts"; +export type { MistralOptions } from "./providers/mistral.ts"; export type { OpenAICodexResponsesOptions, OpenAICodexWebSocketDebugStats, -} from "./providers/openai-codex-responses.js"; -export type { OpenAICompletionsOptions } from "./providers/openai-completions.js"; -export type { OpenAIResponsesOptions } from "./providers/openai-responses.js"; -export * from "./providers/register-builtins.js"; -export * from "./session-resources.js"; -export * from "./stream.js"; -export * from "./types.js"; -export * from "./utils/diagnostics.js"; -export * from "./utils/event-stream.js"; -export * from "./utils/json-parse.js"; +} from "./providers/openai-codex-responses.ts"; +export type { OpenAICompletionsOptions } from "./providers/openai-completions.ts"; +export type { OpenAIResponsesOptions } from "./providers/openai-responses.ts"; +export * from "./providers/register-builtins.ts"; +export * from "./session-resources.ts"; +export * from "./stream.ts"; +export * from "./types.ts"; +export * from "./utils/diagnostics.ts"; +export * from "./utils/event-stream.ts"; +export * from "./utils/json-parse.ts"; export type { OAuthAuthInfo, OAuthCredentials, @@ -40,7 +40,7 @@ export type { OAuthProviderInterface, OAuthSelectOption, OAuthSelectPrompt, -} from "./utils/oauth/types.js"; -export * from "./utils/overflow.js"; -export * from "./utils/typebox-helpers.js"; -export * from "./utils/validation.js"; +} from "./utils/oauth/types.ts"; +export * from "./utils/overflow.ts"; +export * from "./utils/typebox-helpers.ts"; +export * from "./utils/validation.ts"; diff --git a/packages/ai/src/models.generated.ts b/packages/ai/src/models.generated.ts index 33df1db2..dbd7d495 100644 --- a/packages/ai/src/models.generated.ts +++ b/packages/ai/src/models.generated.ts @@ -1,7 +1,7 @@ // This file is auto-generated by scripts/generate-models.ts // Do not edit manually - run 'npm run generate-models' to update -import type { Model } from "./types.js"; +import type { Model } from "./types.ts"; export const MODELS = { "amazon-bedrock": { @@ -911,8 +911,8 @@ export const MODELS = { cacheRead: 0, cacheWrite: 0, }, - contextWindow: 256000, - maxTokens: 256000, + contextWindow: 262143, + maxTokens: 16000, } satisfies Model<"bedrock-converse-stream">, "moonshotai.kimi-k2.5": { id: "moonshotai.kimi-k2.5", @@ -928,8 +928,8 @@ export const MODELS = { cacheRead: 0, cacheWrite: 0, }, - contextWindow: 256000, - maxTokens: 256000, + contextWindow: 262143, + maxTokens: 16000, } satisfies Model<"bedrock-converse-stream">, "nvidia.nemotron-nano-12b-v2": { id: "nvidia.nemotron-nano-12b-v2", @@ -3482,6 +3482,24 @@ export const MODELS = { contextWindow: 160000, maxTokens: 160000, } satisfies Model<"anthropic-messages">, + "accounts/fireworks/models/deepseek-v4-flash": { + id: "accounts/fireworks/models/deepseek-v4-flash", + name: "DeepSeek V4 Flash", + api: "anthropic-messages", + provider: "fireworks", + baseUrl: "https://api.fireworks.ai/inference", + compat: {"sendSessionAffinityHeaders":true,"supportsEagerToolInputStreaming":false,"supportsCacheControlOnTools":false,"supportsLongCacheRetention":false}, + reasoning: true, + input: ["text"], + cost: { + input: 0.14, + output: 0.28, + cacheRead: 0.03, + cacheWrite: 0, + }, + contextWindow: 1000000, + maxTokens: 384000, + } satisfies Model<"anthropic-messages">, "accounts/fireworks/models/deepseek-v4-pro": { id: "accounts/fireworks/models/deepseek-v4-pro", name: "DeepSeek V4 Pro", @@ -4514,6 +4532,24 @@ export const MODELS = { contextWindow: 1048576, maxTokens: 65536, } satisfies Model<"google-generative-ai">, + "gemini-3.5-flash": { + id: "gemini-3.5-flash", + name: "Gemini 3.5 Flash", + api: "google-generative-ai", + provider: "google", + baseUrl: "https://generativelanguage.googleapis.com/v1beta", + reasoning: true, + thinkingLevelMap: {"off":null}, + input: ["text", "image"], + cost: { + input: 1.5, + output: 9, + cacheRead: 0.15, + cacheWrite: 0, + }, + contextWindow: 1048576, + maxTokens: 65536, + } satisfies Model<"google-generative-ai">, "gemini-flash-latest": { id: "gemini-flash-latest", name: "Gemini Flash Latest", @@ -9194,6 +9230,23 @@ export const MODELS = { contextWindow: 1048756, maxTokens: 65536, } satisfies Model<"openai-completions">, + "google/gemini-3.5-flash": { + id: "google/gemini-3.5-flash", + name: "Google: Gemini 3.5 Flash", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + input: ["text", "image"], + cost: { + input: 1.5, + output: 9, + cacheRead: 0.15, + cacheWrite: 0.08333333333333334, + }, + contextWindow: 1048576, + maxTokens: 65536, + } satisfies Model<"openai-completions">, "google/gemma-3-12b-it": { id: "google/gemma-3-12b-it", name: "Google: Gemma 3 12B", @@ -11868,13 +11921,13 @@ export const MODELS = { reasoning: true, input: ["text", "image"], cost: { - input: 0.14, + input: 0.13899999999999998, output: 1, - cacheRead: 0.049999999999999996, + cacheRead: 0, cacheWrite: 0, }, contextWindow: 262144, - maxTokens: 81920, + maxTokens: 4096, } satisfies Model<"openai-completions">, "qwen/qwen3.5-397b-a17b": { id: "qwen/qwen3.5-397b-a17b", @@ -12514,13 +12567,13 @@ export const MODELS = { reasoning: true, input: ["text"], cost: { - input: 0.98, - output: 3.08, - cacheRead: 0.182, + input: 0, + output: 0, + cacheRead: 0, cacheWrite: 0, }, - contextWindow: 202752, - maxTokens: 4096, + contextWindow: 202800, + maxTokens: 202800, } satisfies Model<"openai-completions">, "z-ai/glm-5v-turbo": { id: "z-ai/glm-5v-turbo", @@ -12599,9 +12652,9 @@ export const MODELS = { reasoning: true, input: ["text", "image"], cost: { - input: 0.5, - output: 3, - cacheRead: 0.049999999999999996, + input: 1.5, + output: 9, + cacheRead: 0.15, cacheWrite: 0.08333333333333334, }, contextWindow: 1048576, @@ -13868,6 +13921,23 @@ export const MODELS = { contextWindow: 1000000, maxTokens: 64000, } satisfies Model<"anthropic-messages">, + "google/gemini-3.5-flash": { + id: "google/gemini-3.5-flash", + name: "Gemini 3.5 Flash", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text", "image"], + cost: { + input: 1.5, + output: 9, + cacheRead: 0.15, + cacheWrite: 0, + }, + contextWindow: 1000000, + maxTokens: 64000, + } satisfies Model<"anthropic-messages">, "google/gemma-4-26b-a4b-it": { id: "google/gemma-4-26b-a4b-it", name: "Gemma 4 26B A4B IT", diff --git a/packages/ai/src/models.ts b/packages/ai/src/models.ts index 2125e685..e14a6c2f 100644 --- a/packages/ai/src/models.ts +++ b/packages/ai/src/models.ts @@ -1,5 +1,5 @@ -import { MODELS } from "./models.generated.js"; -import type { Api, KnownProvider, Model, ModelThinkingLevel, Usage } from "./types.js"; +import { MODELS } from "./models.generated.ts"; +import type { Api, KnownProvider, Model, ModelThinkingLevel, Usage } from "./types.ts"; const modelRegistry: Map>> = new Map(); diff --git a/packages/ai/src/oauth.ts b/packages/ai/src/oauth.ts index d768a0fe..487816d8 100644 --- a/packages/ai/src/oauth.ts +++ b/packages/ai/src/oauth.ts @@ -1 +1 @@ -export * from "./utils/oauth/index.js"; +export * from "./utils/oauth/index.ts"; diff --git a/packages/ai/src/providers/amazon-bedrock.ts b/packages/ai/src/providers/amazon-bedrock.ts index 6e616759..5d89ac56 100644 --- a/packages/ai/src/providers/amazon-bedrock.ts +++ b/packages/ai/src/providers/amazon-bedrock.ts @@ -22,7 +22,7 @@ import { } from "@aws-sdk/client-bedrock-runtime"; import { NodeHttpHandler } from "@smithy/node-http-handler"; import type { DocumentType } from "@smithy/types"; -import { calculateCost } from "../models.js"; +import { calculateCost } from "../models.ts"; import type { Api, AssistantMessage, @@ -40,13 +40,13 @@ import type { Tool, ToolCall, ToolResultMessage, -} from "../types.js"; -import { AssistantMessageEventStream } from "../utils/event-stream.js"; -import { parseStreamingJson } from "../utils/json-parse.js"; -import { createHttpProxyAgentsForTarget } from "../utils/node-http-proxy.js"; -import { sanitizeSurrogates } from "../utils/sanitize-unicode.js"; -import { adjustMaxTokensForThinking, buildBaseOptions, clampReasoning } from "./simple-options.js"; -import { transformMessages } from "./transform-messages.js"; +} from "../types.ts"; +import { AssistantMessageEventStream } from "../utils/event-stream.ts"; +import { parseStreamingJson } from "../utils/json-parse.ts"; +import { createHttpProxyAgentsForTarget } from "../utils/node-http-proxy.ts"; +import { sanitizeSurrogates } from "../utils/sanitize-unicode.ts"; +import { adjustMaxTokensForThinking, buildBaseOptions, clampReasoning } from "./simple-options.ts"; +import { transformMessages } from "./transform-messages.ts"; export type BedrockThinkingDisplay = "summarized" | "omitted"; diff --git a/packages/ai/src/providers/anthropic.ts b/packages/ai/src/providers/anthropic.ts index 29f51df9..a594a479 100644 --- a/packages/ai/src/providers/anthropic.ts +++ b/packages/ai/src/providers/anthropic.ts @@ -6,8 +6,8 @@ import type { MessageParam, RawMessageStreamEvent, } from "@anthropic-ai/sdk/resources/messages.js"; -import { getEnvApiKey } from "../env-api-keys.js"; -import { calculateCost } from "../models.js"; +import { getEnvApiKey } from "../env-api-keys.ts"; +import { calculateCost } from "../models.ts"; import type { AnthropicMessagesCompat, Api, @@ -26,16 +26,16 @@ import type { Tool, ToolCall, ToolResultMessage, -} from "../types.js"; -import { AssistantMessageEventStream } from "../utils/event-stream.js"; -import { headersToRecord } from "../utils/headers.js"; -import { parseJsonWithRepair, parseStreamingJson } from "../utils/json-parse.js"; -import { sanitizeSurrogates } from "../utils/sanitize-unicode.js"; +} from "../types.ts"; +import { AssistantMessageEventStream } from "../utils/event-stream.ts"; +import { headersToRecord } from "../utils/headers.ts"; +import { parseJsonWithRepair, parseStreamingJson } from "../utils/json-parse.ts"; +import { sanitizeSurrogates } from "../utils/sanitize-unicode.ts"; -import { resolveCloudflareBaseUrl } from "./cloudflare.js"; -import { buildCopilotDynamicHeaders, hasCopilotVisionInput } from "./github-copilot-headers.js"; -import { adjustMaxTokensForThinking, buildBaseOptions } from "./simple-options.js"; -import { transformMessages } from "./transform-messages.js"; +import { resolveCloudflareBaseUrl } from "./cloudflare.ts"; +import { buildCopilotDynamicHeaders, hasCopilotVisionInput } from "./github-copilot-headers.ts"; +import { adjustMaxTokensForThinking, buildBaseOptions } from "./simple-options.ts"; +import { transformMessages } from "./transform-messages.ts"; /** * Resolve cache retention preference. diff --git a/packages/ai/src/providers/azure-openai-responses.ts b/packages/ai/src/providers/azure-openai-responses.ts index 5625dbd3..60601a38 100644 --- a/packages/ai/src/providers/azure-openai-responses.ts +++ b/packages/ai/src/providers/azure-openai-responses.ts @@ -1,7 +1,7 @@ import { AzureOpenAI } from "openai"; import type { ResponseCreateParamsStreaming } from "openai/resources/responses/responses.js"; -import { getEnvApiKey } from "../env-api-keys.js"; -import { clampThinkingLevel } from "../models.js"; +import { getEnvApiKey } from "../env-api-keys.ts"; +import { clampThinkingLevel } from "../models.ts"; import type { Api, AssistantMessage, @@ -10,12 +10,12 @@ import type { SimpleStreamOptions, StreamFunction, StreamOptions, -} from "../types.js"; -import { AssistantMessageEventStream } from "../utils/event-stream.js"; -import { headersToRecord } from "../utils/headers.js"; -import { clampOpenAIPromptCacheKey } from "./openai-prompt-cache.js"; -import { convertResponsesMessages, convertResponsesTools, processResponsesStream } from "./openai-responses-shared.js"; -import { buildBaseOptions } from "./simple-options.js"; +} from "../types.ts"; +import { AssistantMessageEventStream } from "../utils/event-stream.ts"; +import { headersToRecord } from "../utils/headers.ts"; +import { clampOpenAIPromptCacheKey } from "./openai-prompt-cache.ts"; +import { convertResponsesMessages, convertResponsesTools, processResponsesStream } from "./openai-responses-shared.ts"; +import { buildBaseOptions } from "./simple-options.ts"; const DEFAULT_AZURE_API_VERSION = "v1"; const AZURE_TOOL_CALL_PROVIDERS = new Set(["openai", "openai-codex", "opencode", "azure-openai-responses"]); diff --git a/packages/ai/src/providers/cloudflare.ts b/packages/ai/src/providers/cloudflare.ts index 8e9d44cb..cd0a8159 100644 --- a/packages/ai/src/providers/cloudflare.ts +++ b/packages/ai/src/providers/cloudflare.ts @@ -1,4 +1,4 @@ -import type { Api, Model } from "../types.js"; +import type { Api, Model } from "../types.ts"; /** Workers AI direct endpoint. */ export const CLOUDFLARE_WORKERS_AI_BASE_URL = diff --git a/packages/ai/src/providers/faux.ts b/packages/ai/src/providers/faux.ts index 7bba1b72..7e629847 100644 --- a/packages/ai/src/providers/faux.ts +++ b/packages/ai/src/providers/faux.ts @@ -1,4 +1,4 @@ -import { registerApiProvider, unregisterApiProviders } from "../api-registry.js"; +import { registerApiProvider, unregisterApiProviders } from "../api-registry.ts"; import type { AssistantMessage, AssistantMessageEventStream, @@ -14,8 +14,8 @@ import type { ToolCall, ToolResultMessage, Usage, -} from "../types.js"; -import { createAssistantMessageEventStream } from "../utils/event-stream.js"; +} from "../types.ts"; +import { createAssistantMessageEventStream } from "../utils/event-stream.ts"; const DEFAULT_API = "faux"; const DEFAULT_PROVIDER = "faux"; diff --git a/packages/ai/src/providers/github-copilot-headers.ts b/packages/ai/src/providers/github-copilot-headers.ts index 4f01a9d2..602750c4 100644 --- a/packages/ai/src/providers/github-copilot-headers.ts +++ b/packages/ai/src/providers/github-copilot-headers.ts @@ -1,4 +1,4 @@ -import type { Message } from "../types.js"; +import type { Message } from "../types.ts"; // Copilot expects X-Initiator to indicate whether the request is user-initiated // or agent-initiated (e.g. follow-up after assistant/tool messages). diff --git a/packages/ai/src/providers/google-shared.ts b/packages/ai/src/providers/google-shared.ts index 9f0ab180..1559b20b 100644 --- a/packages/ai/src/providers/google-shared.ts +++ b/packages/ai/src/providers/google-shared.ts @@ -3,9 +3,9 @@ */ import { type Content, FinishReason, FunctionCallingConfigMode, type Part } from "@google/genai"; -import type { Context, ImageContent, Model, StopReason, TextContent, Tool } from "../types.js"; -import { sanitizeSurrogates } from "../utils/sanitize-unicode.js"; -import { transformMessages } from "./transform-messages.js"; +import type { Context, ImageContent, Model, StopReason, TextContent, Tool } from "../types.ts"; +import { sanitizeSurrogates } from "../utils/sanitize-unicode.ts"; +import { transformMessages } from "./transform-messages.ts"; type GoogleApiType = "google-generative-ai" | "google-vertex"; diff --git a/packages/ai/src/providers/google-vertex.ts b/packages/ai/src/providers/google-vertex.ts index e29adcb1..e5f18dab 100644 --- a/packages/ai/src/providers/google-vertex.ts +++ b/packages/ai/src/providers/google-vertex.ts @@ -7,7 +7,7 @@ import { type ThinkingConfig, ThinkingLevel, } from "@google/genai"; -import { calculateCost, clampThinkingLevel } from "../models.js"; +import { calculateCost, clampThinkingLevel } from "../models.ts"; import type { Api, AssistantMessage, @@ -21,10 +21,10 @@ import type { ThinkingBudgets, ThinkingContent, ToolCall, -} from "../types.js"; -import { AssistantMessageEventStream } from "../utils/event-stream.js"; -import { sanitizeSurrogates } from "../utils/sanitize-unicode.js"; -import type { GoogleThinkingLevel } from "./google-shared.js"; +} from "../types.ts"; +import { AssistantMessageEventStream } from "../utils/event-stream.ts"; +import { sanitizeSurrogates } from "../utils/sanitize-unicode.ts"; +import type { GoogleThinkingLevel } from "./google-shared.ts"; import { convertMessages, convertTools, @@ -32,8 +32,8 @@ import { mapStopReason, mapToolChoice, retainThoughtSignature, -} from "./google-shared.js"; -import { buildBaseOptions } from "./simple-options.js"; +} from "./google-shared.ts"; +import { buildBaseOptions } from "./simple-options.ts"; export interface GoogleVertexOptions extends StreamOptions { toolChoice?: "auto" | "none" | "any"; diff --git a/packages/ai/src/providers/google.ts b/packages/ai/src/providers/google.ts index fd1adfdd..d0ddf673 100644 --- a/packages/ai/src/providers/google.ts +++ b/packages/ai/src/providers/google.ts @@ -4,8 +4,8 @@ import { GoogleGenAI, type ThinkingConfig, } from "@google/genai"; -import { getEnvApiKey } from "../env-api-keys.js"; -import { calculateCost, clampThinkingLevel } from "../models.js"; +import { getEnvApiKey } from "../env-api-keys.ts"; +import { calculateCost, clampThinkingLevel } from "../models.ts"; import type { Api, AssistantMessage, @@ -19,10 +19,10 @@ import type { ThinkingContent, ThinkingLevel, ToolCall, -} from "../types.js"; -import { AssistantMessageEventStream } from "../utils/event-stream.js"; -import { sanitizeSurrogates } from "../utils/sanitize-unicode.js"; -import type { GoogleThinkingLevel } from "./google-shared.js"; +} from "../types.ts"; +import { AssistantMessageEventStream } from "../utils/event-stream.ts"; +import { sanitizeSurrogates } from "../utils/sanitize-unicode.ts"; +import type { GoogleThinkingLevel } from "./google-shared.ts"; import { convertMessages, convertTools, @@ -30,8 +30,8 @@ import { mapStopReason, mapToolChoice, retainThoughtSignature, -} from "./google-shared.js"; -import { buildBaseOptions } from "./simple-options.js"; +} from "./google-shared.ts"; +import { buildBaseOptions } from "./simple-options.ts"; export interface GoogleOptions extends StreamOptions { toolChoice?: "auto" | "none" | "any"; diff --git a/packages/ai/src/providers/images/openrouter.ts b/packages/ai/src/providers/images/openrouter.ts index 70c95a8c..01e81a60 100644 --- a/packages/ai/src/providers/images/openrouter.ts +++ b/packages/ai/src/providers/images/openrouter.ts @@ -6,7 +6,7 @@ import type { ChatCompletionContentPartText, ChatCompletionCreateParamsNonStreaming, } from "openai/resources/chat/completions.js"; -import { getEnvApiKey } from "../../env-api-keys.js"; +import { getEnvApiKey } from "../../env-api-keys.ts"; import type { AssistantImages, ImageContent, @@ -15,9 +15,9 @@ import type { ImagesModel, ImagesOptions, TextContent, -} from "../../types.js"; -import { headersToRecord } from "../../utils/headers.js"; -import { sanitizeSurrogates } from "../../utils/sanitize-unicode.js"; +} from "../../types.ts"; +import { headersToRecord } from "../../utils/headers.ts"; +import { sanitizeSurrogates } from "../../utils/sanitize-unicode.ts"; interface OpenRouterGeneratedImage { image_url?: string | { url?: string }; diff --git a/packages/ai/src/providers/images/register-builtins.ts b/packages/ai/src/providers/images/register-builtins.ts index 04338f20..e3decbb9 100644 --- a/packages/ai/src/providers/images/register-builtins.ts +++ b/packages/ai/src/providers/images/register-builtins.ts @@ -1,6 +1,6 @@ -import { registerImagesApiProvider } from "../../images-api-registry.js"; -import type { AssistantImages, ImagesContext, ImagesFunction, ImagesModel, ImagesOptions } from "../../types.js"; -import type { generateImagesOpenRouter as generateImagesOpenRouterFunction } from "./openrouter.js"; +import { registerImagesApiProvider } from "../../images-api-registry.ts"; +import type { AssistantImages, ImagesContext, ImagesFunction, ImagesModel, ImagesOptions } from "../../types.ts"; +import type { generateImagesOpenRouter as generateImagesOpenRouterFunction } from "./openrouter.ts"; interface OpenRouterImagesProviderModule { generateImagesOpenRouter: typeof generateImagesOpenRouterFunction; @@ -21,7 +21,7 @@ function createLazyLoadErrorImages(model: ImagesModel<"openrouter-images">, erro } function loadOpenRouterImagesProviderModule(): Promise { - openRouterImagesProviderModulePromise ||= import("./openrouter.js").then( + openRouterImagesProviderModulePromise ||= import("./openrouter.ts").then( (module) => module as OpenRouterImagesProviderModule, ); return openRouterImagesProviderModulePromise; diff --git a/packages/ai/src/providers/mistral.ts b/packages/ai/src/providers/mistral.ts index c1500dbf..a15f141a 100644 --- a/packages/ai/src/providers/mistral.ts +++ b/packages/ai/src/providers/mistral.ts @@ -6,8 +6,8 @@ import type { ContentChunk, FunctionTool, } from "@mistralai/mistralai/models/components"; -import { getEnvApiKey } from "../env-api-keys.js"; -import { calculateCost, clampThinkingLevel } from "../models.js"; +import { getEnvApiKey } from "../env-api-keys.ts"; +import { calculateCost, clampThinkingLevel } from "../models.ts"; import type { AssistantMessage, Context, @@ -21,13 +21,13 @@ import type { ThinkingContent, Tool, ToolCall, -} from "../types.js"; -import { AssistantMessageEventStream } from "../utils/event-stream.js"; -import { shortHash } from "../utils/hash.js"; -import { parseStreamingJson } from "../utils/json-parse.js"; -import { sanitizeSurrogates } from "../utils/sanitize-unicode.js"; -import { buildBaseOptions } from "./simple-options.js"; -import { transformMessages } from "./transform-messages.js"; +} from "../types.ts"; +import { AssistantMessageEventStream } from "../utils/event-stream.ts"; +import { shortHash } from "../utils/hash.ts"; +import { parseStreamingJson } from "../utils/json-parse.ts"; +import { sanitizeSurrogates } from "../utils/sanitize-unicode.ts"; +import { buildBaseOptions } from "./simple-options.ts"; +import { transformMessages } from "./transform-messages.ts"; const MISTRAL_TOOL_CALL_ID_LENGTH = 9; const MAX_MISTRAL_ERROR_BODY_CHARS = 4000; diff --git a/packages/ai/src/providers/openai-codex-responses.ts b/packages/ai/src/providers/openai-codex-responses.ts index ab2ba789..7bb9ca47 100644 --- a/packages/ai/src/providers/openai-codex-responses.ts +++ b/packages/ai/src/providers/openai-codex-responses.ts @@ -20,9 +20,9 @@ if (typeof process !== "undefined" && (process.versions?.node || process.version }); } -import { getEnvApiKey } from "../env-api-keys.js"; -import { clampThinkingLevel } from "../models.js"; -import { registerSessionResourceCleanup } from "../session-resources.js"; +import { getEnvApiKey } from "../env-api-keys.ts"; +import { clampThinkingLevel } from "../models.ts"; +import { registerSessionResourceCleanup } from "../session-resources.ts"; import type { Api, AssistantMessage, @@ -32,17 +32,17 @@ import type { StreamFunction, StreamOptions, Usage, -} from "../types.js"; +} from "../types.ts"; import { appendAssistantMessageDiagnostic, createAssistantMessageDiagnostic, formatThrownValue, -} from "../utils/diagnostics.js"; -import { AssistantMessageEventStream } from "../utils/event-stream.js"; -import { headersToRecord } from "../utils/headers.js"; -import { clampOpenAIPromptCacheKey } from "./openai-prompt-cache.js"; -import { convertResponsesMessages, convertResponsesTools, processResponsesStream } from "./openai-responses-shared.js"; -import { buildBaseOptions } from "./simple-options.js"; +} from "../utils/diagnostics.ts"; +import { AssistantMessageEventStream } from "../utils/event-stream.ts"; +import { headersToRecord } from "../utils/headers.ts"; +import { clampOpenAIPromptCacheKey } from "./openai-prompt-cache.ts"; +import { convertResponsesMessages, convertResponsesTools, processResponsesStream } from "./openai-responses-shared.ts"; +import { buildBaseOptions } from "./simple-options.ts"; // ============================================================================ // Configuration diff --git a/packages/ai/src/providers/openai-completions.ts b/packages/ai/src/providers/openai-completions.ts index 658d4179..12b6a508 100644 --- a/packages/ai/src/providers/openai-completions.ts +++ b/packages/ai/src/providers/openai-completions.ts @@ -10,8 +10,8 @@ import type { ChatCompletionSystemMessageParam, ChatCompletionToolMessageParam, } from "openai/resources/chat/completions.js"; -import { getEnvApiKey } from "../env-api-keys.js"; -import { calculateCost, clampThinkingLevel } from "../models.js"; +import { getEnvApiKey } from "../env-api-keys.ts"; +import { calculateCost, clampThinkingLevel } from "../models.ts"; import type { AssistantMessage, CacheRetention, @@ -29,16 +29,16 @@ import type { Tool, ToolCall, ToolResultMessage, -} from "../types.js"; -import { AssistantMessageEventStream } from "../utils/event-stream.js"; -import { headersToRecord } from "../utils/headers.js"; -import { parseStreamingJson } from "../utils/json-parse.js"; -import { sanitizeSurrogates } from "../utils/sanitize-unicode.js"; -import { isCloudflareProvider, resolveCloudflareBaseUrl } from "./cloudflare.js"; -import { buildCopilotDynamicHeaders, hasCopilotVisionInput } from "./github-copilot-headers.js"; -import { clampOpenAIPromptCacheKey } from "./openai-prompt-cache.js"; -import { buildBaseOptions } from "./simple-options.js"; -import { transformMessages } from "./transform-messages.js"; +} from "../types.ts"; +import { AssistantMessageEventStream } from "../utils/event-stream.ts"; +import { headersToRecord } from "../utils/headers.ts"; +import { parseStreamingJson } from "../utils/json-parse.ts"; +import { sanitizeSurrogates } from "../utils/sanitize-unicode.ts"; +import { isCloudflareProvider, resolveCloudflareBaseUrl } from "./cloudflare.ts"; +import { buildCopilotDynamicHeaders, hasCopilotVisionInput } from "./github-copilot-headers.ts"; +import { clampOpenAIPromptCacheKey } from "./openai-prompt-cache.ts"; +import { buildBaseOptions } from "./simple-options.ts"; +import { transformMessages } from "./transform-messages.ts"; /** * Check if conversation messages contain tool calls or tool results. diff --git a/packages/ai/src/providers/openai-responses-shared.ts b/packages/ai/src/providers/openai-responses-shared.ts index 0d68cd7f..b7bcd192 100644 --- a/packages/ai/src/providers/openai-responses-shared.ts +++ b/packages/ai/src/providers/openai-responses-shared.ts @@ -12,7 +12,7 @@ import type { ResponseReasoningItem, ResponseStreamEvent, } from "openai/resources/responses/responses.js"; -import { calculateCost } from "../models.js"; +import { calculateCost } from "../models.ts"; import type { Api, AssistantMessage, @@ -26,12 +26,12 @@ import type { Tool, ToolCall, Usage, -} from "../types.js"; -import type { AssistantMessageEventStream } from "../utils/event-stream.js"; -import { shortHash } from "../utils/hash.js"; -import { parseStreamingJson } from "../utils/json-parse.js"; -import { sanitizeSurrogates } from "../utils/sanitize-unicode.js"; -import { transformMessages } from "./transform-messages.js"; +} from "../types.ts"; +import type { AssistantMessageEventStream } from "../utils/event-stream.ts"; +import { shortHash } from "../utils/hash.ts"; +import { parseStreamingJson } from "../utils/json-parse.ts"; +import { sanitizeSurrogates } from "../utils/sanitize-unicode.ts"; +import { transformMessages } from "./transform-messages.ts"; // ============================================================================= // Utilities diff --git a/packages/ai/src/providers/openai-responses.ts b/packages/ai/src/providers/openai-responses.ts index a23b197c..e2d1ca80 100644 --- a/packages/ai/src/providers/openai-responses.ts +++ b/packages/ai/src/providers/openai-responses.ts @@ -1,7 +1,7 @@ import OpenAI from "openai"; import type { ResponseCreateParamsStreaming } from "openai/resources/responses/responses.js"; -import { getEnvApiKey } from "../env-api-keys.js"; -import { clampThinkingLevel } from "../models.js"; +import { getEnvApiKey } from "../env-api-keys.ts"; +import { clampThinkingLevel } from "../models.ts"; import type { Api, AssistantMessage, @@ -13,14 +13,14 @@ import type { StreamFunction, StreamOptions, Usage, -} from "../types.js"; -import { AssistantMessageEventStream } from "../utils/event-stream.js"; -import { headersToRecord } from "../utils/headers.js"; -import { isCloudflareProvider, resolveCloudflareBaseUrl } from "./cloudflare.js"; -import { buildCopilotDynamicHeaders, hasCopilotVisionInput } from "./github-copilot-headers.js"; -import { clampOpenAIPromptCacheKey } from "./openai-prompt-cache.js"; -import { convertResponsesMessages, convertResponsesTools, processResponsesStream } from "./openai-responses-shared.js"; -import { buildBaseOptions } from "./simple-options.js"; +} from "../types.ts"; +import { AssistantMessageEventStream } from "../utils/event-stream.ts"; +import { headersToRecord } from "../utils/headers.ts"; +import { isCloudflareProvider, resolveCloudflareBaseUrl } from "./cloudflare.ts"; +import { buildCopilotDynamicHeaders, hasCopilotVisionInput } from "./github-copilot-headers.ts"; +import { clampOpenAIPromptCacheKey } from "./openai-prompt-cache.ts"; +import { convertResponsesMessages, convertResponsesTools, processResponsesStream } from "./openai-responses-shared.ts"; +import { buildBaseOptions } from "./simple-options.ts"; const OPENAI_TOOL_CALL_PROVIDERS = new Set(["openai", "openai-codex", "opencode"]); diff --git a/packages/ai/src/providers/register-builtins.ts b/packages/ai/src/providers/register-builtins.ts index 64475680..8fdcaaf0 100644 --- a/packages/ai/src/providers/register-builtins.ts +++ b/packages/ai/src/providers/register-builtins.ts @@ -1,4 +1,4 @@ -import { clearApiProviders, registerApiProvider } from "../api-registry.js"; +import { clearApiProviders, registerApiProvider } from "../api-registry.ts"; import type { Api, AssistantMessage, @@ -8,17 +8,17 @@ import type { SimpleStreamOptions, StreamFunction, StreamOptions, -} from "../types.js"; -import { AssistantMessageEventStream } from "../utils/event-stream.js"; -import type { BedrockOptions } from "./amazon-bedrock.js"; -import type { AnthropicOptions } from "./anthropic.js"; -import type { AzureOpenAIResponsesOptions } from "./azure-openai-responses.js"; -import type { GoogleOptions } from "./google.js"; -import type { GoogleVertexOptions } from "./google-vertex.js"; -import type { MistralOptions } from "./mistral.js"; -import type { OpenAICodexResponsesOptions } from "./openai-codex-responses.js"; -import type { OpenAICompletionsOptions } from "./openai-completions.js"; -import type { OpenAIResponsesOptions } from "./openai-responses.js"; +} from "../types.ts"; +import { AssistantMessageEventStream } from "../utils/event-stream.ts"; +import type { BedrockOptions } from "./amazon-bedrock.ts"; +import type { AnthropicOptions } from "./anthropic.ts"; +import type { AzureOpenAIResponsesOptions } from "./azure-openai-responses.ts"; +import type { GoogleOptions } from "./google.ts"; +import type { GoogleVertexOptions } from "./google-vertex.ts"; +import type { MistralOptions } from "./mistral.ts"; +import type { OpenAICodexResponsesOptions } from "./openai-codex-responses.ts"; +import type { OpenAICompletionsOptions } from "./openai-completions.ts"; +import type { OpenAIResponsesOptions } from "./openai-responses.ts"; interface LazyProviderModule< TApi extends Api, @@ -86,7 +86,10 @@ interface BedrockProviderModule { ) => AsyncIterable; } -const importNodeOnlyProvider = (specifier: string): Promise => import(specifier); +const importNodeOnlyProvider = (specifier: string): Promise => { + const runtimeSpecifier = import.meta.url.endsWith(".js") ? specifier.replace(/\.ts$/, ".js") : specifier; + return import(runtimeSpecifier); +}; let anthropicProviderModulePromise: | Promise> @@ -203,7 +206,7 @@ function createLazySimpleStream< function loadAnthropicProviderModule(): Promise< LazyProviderModule<"anthropic-messages", AnthropicOptions, SimpleStreamOptions> > { - anthropicProviderModulePromise ||= import("./anthropic.js").then((module) => { + anthropicProviderModulePromise ||= import("./anthropic.ts").then((module) => { const provider = module as AnthropicProviderModule; return { stream: provider.streamAnthropic, @@ -216,7 +219,7 @@ function loadAnthropicProviderModule(): Promise< function loadAzureOpenAIResponsesProviderModule(): Promise< LazyProviderModule<"azure-openai-responses", AzureOpenAIResponsesOptions, SimpleStreamOptions> > { - azureOpenAIResponsesProviderModulePromise ||= import("./azure-openai-responses.js").then((module) => { + azureOpenAIResponsesProviderModulePromise ||= import("./azure-openai-responses.ts").then((module) => { const provider = module as AzureOpenAIResponsesProviderModule; return { stream: provider.streamAzureOpenAIResponses, @@ -229,7 +232,7 @@ function loadAzureOpenAIResponsesProviderModule(): Promise< function loadGoogleProviderModule(): Promise< LazyProviderModule<"google-generative-ai", GoogleOptions, SimpleStreamOptions> > { - googleProviderModulePromise ||= import("./google.js").then((module) => { + googleProviderModulePromise ||= import("./google.ts").then((module) => { const provider = module as GoogleProviderModule; return { stream: provider.streamGoogle, @@ -242,7 +245,7 @@ function loadGoogleProviderModule(): Promise< function loadGoogleVertexProviderModule(): Promise< LazyProviderModule<"google-vertex", GoogleVertexOptions, SimpleStreamOptions> > { - googleVertexProviderModulePromise ||= import("./google-vertex.js").then((module) => { + googleVertexProviderModulePromise ||= import("./google-vertex.ts").then((module) => { const provider = module as GoogleVertexProviderModule; return { stream: provider.streamGoogleVertex, @@ -255,7 +258,7 @@ function loadGoogleVertexProviderModule(): Promise< function loadMistralProviderModule(): Promise< LazyProviderModule<"mistral-conversations", MistralOptions, SimpleStreamOptions> > { - mistralProviderModulePromise ||= import("./mistral.js").then((module) => { + mistralProviderModulePromise ||= import("./mistral.ts").then((module) => { const provider = module as MistralProviderModule; return { stream: provider.streamMistral, @@ -268,7 +271,7 @@ function loadMistralProviderModule(): Promise< function loadOpenAICodexResponsesProviderModule(): Promise< LazyProviderModule<"openai-codex-responses", OpenAICodexResponsesOptions, SimpleStreamOptions> > { - openAICodexResponsesProviderModulePromise ||= import("./openai-codex-responses.js").then((module) => { + openAICodexResponsesProviderModulePromise ||= import("./openai-codex-responses.ts").then((module) => { const provider = module as OpenAICodexResponsesProviderModule; return { stream: provider.streamOpenAICodexResponses, @@ -281,7 +284,7 @@ function loadOpenAICodexResponsesProviderModule(): Promise< function loadOpenAICompletionsProviderModule(): Promise< LazyProviderModule<"openai-completions", OpenAICompletionsOptions, SimpleStreamOptions> > { - openAICompletionsProviderModulePromise ||= import("./openai-completions.js").then((module) => { + openAICompletionsProviderModulePromise ||= import("./openai-completions.ts").then((module) => { const provider = module as OpenAICompletionsProviderModule; return { stream: provider.streamOpenAICompletions, @@ -294,7 +297,7 @@ function loadOpenAICompletionsProviderModule(): Promise< function loadOpenAIResponsesProviderModule(): Promise< LazyProviderModule<"openai-responses", OpenAIResponsesOptions, SimpleStreamOptions> > { - openAIResponsesProviderModulePromise ||= import("./openai-responses.js").then((module) => { + openAIResponsesProviderModulePromise ||= import("./openai-responses.ts").then((module) => { const provider = module as OpenAIResponsesProviderModule; return { stream: provider.streamOpenAIResponses, @@ -310,7 +313,7 @@ function loadBedrockProviderModule(): Promise< if (bedrockProviderModuleOverride) { return Promise.resolve(bedrockProviderModuleOverride); } - bedrockProviderModulePromise ||= importNodeOnlyProvider("./amazon-bedrock.js").then((module) => { + bedrockProviderModulePromise ||= importNodeOnlyProvider("./amazon-bedrock.ts").then((module) => { const provider = module as BedrockProviderModule; return { stream: provider.streamBedrock, diff --git a/packages/ai/src/providers/simple-options.ts b/packages/ai/src/providers/simple-options.ts index 1822ebb1..7f0aeea7 100644 --- a/packages/ai/src/providers/simple-options.ts +++ b/packages/ai/src/providers/simple-options.ts @@ -1,4 +1,4 @@ -import type { Api, Model, SimpleStreamOptions, StreamOptions, ThinkingBudgets, ThinkingLevel } from "../types.js"; +import type { Api, Model, SimpleStreamOptions, StreamOptions, ThinkingBudgets, ThinkingLevel } from "../types.ts"; export function buildBaseOptions(_model: Model, options?: SimpleStreamOptions, apiKey?: string): StreamOptions { return { diff --git a/packages/ai/src/providers/transform-messages.ts b/packages/ai/src/providers/transform-messages.ts index 8fde3716..4029a064 100644 --- a/packages/ai/src/providers/transform-messages.ts +++ b/packages/ai/src/providers/transform-messages.ts @@ -7,7 +7,7 @@ import type { TextContent, ToolCall, ToolResultMessage, -} from "../types.js"; +} from "../types.ts"; const NON_VISION_USER_IMAGE_PLACEHOLDER = "(image omitted: model does not support images)"; const NON_VISION_TOOL_IMAGE_PLACEHOLDER = "(tool image omitted: model does not support images)"; diff --git a/packages/ai/src/stream.ts b/packages/ai/src/stream.ts index e8a2e50e..a38b01fd 100644 --- a/packages/ai/src/stream.ts +++ b/packages/ai/src/stream.ts @@ -1,6 +1,6 @@ -import "./providers/register-builtins.js"; +import "./providers/register-builtins.ts"; -import { getApiProvider } from "./api-registry.js"; +import { getApiProvider } from "./api-registry.ts"; import type { Api, AssistantMessage, @@ -10,9 +10,9 @@ import type { ProviderStreamOptions, SimpleStreamOptions, StreamOptions, -} from "./types.js"; +} from "./types.ts"; -export { getEnvApiKey } from "./env-api-keys.js"; +export { getEnvApiKey } from "./env-api-keys.ts"; function resolveApiProvider(api: Api) { const provider = getApiProvider(api); diff --git a/packages/ai/src/types.ts b/packages/ai/src/types.ts index 851e4d4b..6fc703f1 100644 --- a/packages/ai/src/types.ts +++ b/packages/ai/src/types.ts @@ -1,7 +1,7 @@ -import type { AssistantMessageDiagnostic } from "./utils/diagnostics.js"; -import type { AssistantMessageEventStream } from "./utils/event-stream.js"; +import type { AssistantMessageDiagnostic } from "./utils/diagnostics.ts"; +import type { AssistantMessageEventStream } from "./utils/event-stream.ts"; -export type { AssistantMessageEventStream } from "./utils/event-stream.js"; +export type { AssistantMessageEventStream } from "./utils/event-stream.ts"; export type KnownApi = | "openai-completions" diff --git a/packages/ai/src/utils/event-stream.ts b/packages/ai/src/utils/event-stream.ts index 43461ea2..e5eef921 100644 --- a/packages/ai/src/utils/event-stream.ts +++ b/packages/ai/src/utils/event-stream.ts @@ -1,4 +1,4 @@ -import type { AssistantMessage, AssistantMessageEvent } from "../types.js"; +import type { AssistantMessage, AssistantMessageEvent } from "../types.ts"; // Generic event stream class for async iteration export class EventStream implements AsyncIterable { diff --git a/packages/ai/src/utils/oauth/anthropic.ts b/packages/ai/src/utils/oauth/anthropic.ts index 1d404266..feeb34fa 100644 --- a/packages/ai/src/utils/oauth/anthropic.ts +++ b/packages/ai/src/utils/oauth/anthropic.ts @@ -6,9 +6,9 @@ */ import type { Server } from "node:http"; -import { oauthErrorHtml, oauthSuccessHtml } from "./oauth-page.js"; -import { generatePKCE } from "./pkce.js"; -import type { OAuthCredentials, OAuthLoginCallbacks, OAuthPrompt, OAuthProviderInterface } from "./types.js"; +import { oauthErrorHtml, oauthSuccessHtml } from "./oauth-page.ts"; +import { generatePKCE } from "./pkce.ts"; +import type { OAuthCredentials, OAuthLoginCallbacks, OAuthPrompt, OAuthProviderInterface } from "./types.ts"; type CallbackServerInfo = { server: Server; diff --git a/packages/ai/src/utils/oauth/github-copilot.ts b/packages/ai/src/utils/oauth/github-copilot.ts index dcacb43d..6182ab91 100644 --- a/packages/ai/src/utils/oauth/github-copilot.ts +++ b/packages/ai/src/utils/oauth/github-copilot.ts @@ -2,9 +2,9 @@ * GitHub Copilot OAuth flow */ -import { getModels } from "../../models.js"; -import type { Api, Model } from "../../types.js"; -import type { OAuthCredentials, OAuthLoginCallbacks, OAuthProviderInterface } from "./types.js"; +import { getModels } from "../../models.ts"; +import type { Api, Model } from "../../types.ts"; +import type { OAuthCredentials, OAuthLoginCallbacks, OAuthProviderInterface } from "./types.ts"; type CopilotCredentials = OAuthCredentials & { enterpriseUrl?: string; diff --git a/packages/ai/src/utils/oauth/index.ts b/packages/ai/src/utils/oauth/index.ts index f5f180bf..3a3a01b1 100644 --- a/packages/ai/src/utils/oauth/index.ts +++ b/packages/ai/src/utils/oauth/index.ts @@ -8,7 +8,7 @@ */ // Anthropic -export { anthropicOAuthProvider, loginAnthropic, refreshAnthropicToken } from "./anthropic.js"; +export { anthropicOAuthProvider, loginAnthropic, refreshAnthropicToken } from "./anthropic.ts"; // GitHub Copilot export { getGitHubCopilotBaseUrl, @@ -16,20 +16,20 @@ export { loginGitHubCopilot, normalizeDomain, refreshGitHubCopilotToken, -} from "./github-copilot.js"; +} from "./github-copilot.ts"; // OpenAI Codex (ChatGPT OAuth) -export { loginOpenAICodex, openaiCodexOAuthProvider, refreshOpenAICodexToken } from "./openai-codex.js"; +export { loginOpenAICodex, openaiCodexOAuthProvider, refreshOpenAICodexToken } from "./openai-codex.ts"; -export * from "./types.js"; +export * from "./types.ts"; // ============================================================================ // Provider Registry // ============================================================================ -import { anthropicOAuthProvider } from "./anthropic.js"; -import { githubCopilotOAuthProvider } from "./github-copilot.js"; -import { openaiCodexOAuthProvider } from "./openai-codex.js"; -import type { OAuthCredentials, OAuthProviderId, OAuthProviderInfo, OAuthProviderInterface } from "./types.js"; +import { anthropicOAuthProvider } from "./anthropic.ts"; +import { githubCopilotOAuthProvider } from "./github-copilot.ts"; +import { openaiCodexOAuthProvider } from "./openai-codex.ts"; +import type { OAuthCredentials, OAuthProviderId, OAuthProviderInfo, OAuthProviderInterface } from "./types.ts"; const BUILT_IN_OAUTH_PROVIDERS: OAuthProviderInterface[] = [ anthropicOAuthProvider, diff --git a/packages/ai/src/utils/oauth/openai-codex.ts b/packages/ai/src/utils/oauth/openai-codex.ts index 62c4c352..28600d23 100644 --- a/packages/ai/src/utils/oauth/openai-codex.ts +++ b/packages/ai/src/utils/oauth/openai-codex.ts @@ -17,9 +17,9 @@ if (typeof process !== "undefined" && (process.versions?.node || process.version }); } -import { oauthErrorHtml, oauthSuccessHtml } from "./oauth-page.js"; -import { generatePKCE } from "./pkce.js"; -import type { OAuthCredentials, OAuthLoginCallbacks, OAuthPrompt, OAuthProviderInterface } from "./types.js"; +import { oauthErrorHtml, oauthSuccessHtml } from "./oauth-page.ts"; +import { generatePKCE } from "./pkce.ts"; +import type { OAuthCredentials, OAuthLoginCallbacks, OAuthPrompt, OAuthProviderInterface } from "./types.ts"; const CALLBACK_HOST = process.env.PI_OAUTH_CALLBACK_HOST || "127.0.0.1"; const CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann"; diff --git a/packages/ai/src/utils/oauth/types.ts b/packages/ai/src/utils/oauth/types.ts index 03cbf101..a1426d81 100644 --- a/packages/ai/src/utils/oauth/types.ts +++ b/packages/ai/src/utils/oauth/types.ts @@ -1,4 +1,4 @@ -import type { Api, Model } from "../../types.js"; +import type { Api, Model } from "../../types.ts"; export type OAuthCredentials = { refresh: string; diff --git a/packages/ai/src/utils/overflow.ts b/packages/ai/src/utils/overflow.ts index 7eef4a75..8b908e85 100644 --- a/packages/ai/src/utils/overflow.ts +++ b/packages/ai/src/utils/overflow.ts @@ -1,4 +1,4 @@ -import type { AssistantMessage } from "../types.js"; +import type { AssistantMessage } from "../types.ts"; /** * Regex patterns to detect context overflow errors from different providers. diff --git a/packages/ai/src/utils/validation.ts b/packages/ai/src/utils/validation.ts index 6fee2868..060898c7 100644 --- a/packages/ai/src/utils/validation.ts +++ b/packages/ai/src/utils/validation.ts @@ -1,7 +1,7 @@ import { Compile } from "typebox/compile"; import type { TLocalizedValidationError } from "typebox/error"; import { Value } from "typebox/value"; -import type { Tool, ToolCall } from "../types.js"; +import type { Tool, ToolCall } from "../types.ts"; const validatorCache = new WeakMap>(); const TYPEBOX_KIND = Symbol.for("TypeBox.Kind"); diff --git a/packages/coding-agent/examples/extensions/overlay-qa-tests.ts b/packages/coding-agent/examples/extensions/overlay-qa-tests.ts index b13f43a9..306c139b 100644 --- a/packages/coding-agent/examples/extensions/overlay-qa-tests.ts +++ b/packages/coding-agent/examples/extensions/overlay-qa-tests.ts @@ -303,7 +303,11 @@ function sleep(ms: number): Promise { // Base overlay component with common rendering abstract class BaseOverlay { - constructor(protected theme: Theme) {} + protected theme: Theme; + + constructor(theme: Theme) { + this.theme = theme; + } protected box(lines: string[], width: number, title?: string): string[] { const th = this.theme; @@ -330,12 +334,13 @@ abstract class BaseOverlay { // Anchor position test class AnchorTestComponent extends BaseOverlay { - constructor( - theme: Theme, - private anchor: OverlayAnchor, - private done: (result: "next" | "confirm" | "cancel") => void, - ) { + private anchor: OverlayAnchor; + private done: (result: "next" | "confirm" | "cancel") => void; + + constructor(theme: Theme, anchor: OverlayAnchor, done: (result: "next" | "confirm" | "cancel") => void) { super(theme); + this.anchor = anchor; + this.done = done; } handleInput(data: string): void { @@ -368,12 +373,17 @@ class AnchorTestComponent extends BaseOverlay { // Margin/offset test class MarginTestComponent extends BaseOverlay { + private config: { name: string; options: OverlayOptions }; + private done: (result: "next" | "close") => void; + constructor( theme: Theme, - private config: { name: string; options: OverlayOptions }, - private done: (result: "next" | "close") => void, + config: { name: string; options: OverlayOptions }, + done: (result: "next" | "close") => void, ) { super(theme); + this.config = config; + this.done = done; } handleInput(data: string): void { @@ -403,13 +413,15 @@ class MarginTestComponent extends BaseOverlay { // Stacked overlay test class StackOverlayComponent extends BaseOverlay { - constructor( - theme: Theme, - private num: number, - private position: string, - private done: (result: string) => void, - ) { + private num: number; + private position: string; + private done: (result: string) => void; + + constructor(theme: Theme, num: number, position: string, done: (result: string) => void) { super(theme); + this.num = num; + this.position = position; + this.done = done; } handleInput(data: string): void { @@ -446,19 +458,19 @@ class StackOverlayComponent extends BaseOverlay { // Streaming overflow test - spawns real process with colored output (original crash scenario) class StreamingOverflowComponent extends BaseOverlay { + private tui: TUI; private lines: string[] = []; private proc: ReturnType | null = null; private scrollOffset = 0; private maxVisibleLines = 15; private finished = false; private disposed = false; + private done: () => void; - constructor( - private tui: TUI, - theme: Theme, - private done: () => void, - ) { + constructor(tui: TUI, theme: Theme, done: () => void) { super(theme); + this.tui = tui; + this.done = done; this.startProcess(); } @@ -579,11 +591,11 @@ class StreamingOverflowComponent extends BaseOverlay { // Edge position test class EdgeTestComponent extends BaseOverlay { - constructor( - theme: Theme, - private done: () => void, - ) { + private done: () => void; + + constructor(theme: Theme, done: () => void) { super(theme); + this.done = done; } handleInput(data: string): void { @@ -614,12 +626,17 @@ class EdgeTestComponent extends BaseOverlay { // Percentage positioning test class PercentTestComponent extends BaseOverlay { + private config: { name: string; row: number; col: number }; + private done: (result: "next" | "close") => void; + constructor( theme: Theme, - private config: { name: string; row: number; col: number }, - private done: (result: "next" | "close") => void, + config: { name: string; row: number; col: number }, + done: (result: "next" | "close") => void, ) { super(theme); + this.config = config; + this.done = done; } handleInput(data: string): void { @@ -649,11 +666,11 @@ class PercentTestComponent extends BaseOverlay { // MaxHeight test - renders 20 lines, truncated to 10 by maxHeight class MaxHeightTestComponent extends BaseOverlay { - constructor( - theme: Theme, - private done: () => void, - ) { + private done: () => void; + + constructor(theme: Theme, done: () => void) { super(theme); + this.done = done; } handleInput(data: string): void { @@ -684,15 +701,15 @@ class MaxHeightTestComponent extends BaseOverlay { // Responsive sidepanel - demonstrates percentage width and visibility callback class SidepanelComponent extends BaseOverlay { + private tui: TUI; private items = ["Dashboard", "Messages", "Settings", "Help", "About"]; private selectedIndex = 0; + private done: () => void; - constructor( - private tui: TUI, - theme: Theme, - private done: () => void, - ) { + constructor(tui: TUI, theme: Theme, done: () => void) { super(theme); + this.tui = tui; + this.done = done; } handleInput(data: string): void { @@ -745,18 +762,18 @@ class SidepanelComponent extends BaseOverlay { // Animation demo - proves overlays can handle real-time updates like pi-doom class AnimationDemoComponent extends BaseOverlay { + private tui: TUI; private frame = 0; private interval: ReturnType | null = null; private fps = 0; private lastFpsUpdate = Date.now(); private framesSinceLastFps = 0; + private done: () => void; - constructor( - private tui: TUI, - theme: Theme, - private done: () => void, - ) { + constructor(tui: TUI, theme: Theme, done: () => void) { super(theme); + this.tui = tui; + this.done = done; this.startAnimation(); } @@ -860,15 +877,15 @@ function hslToRgb(h: number, s: number, l: number): [number, number, number] { // Toggle demo - demonstrates OverlayHandle.setHidden() via onHandle callback class ToggleDemoComponent extends BaseOverlay { + private tui: TUI; private toggleCount = 0; private isToggling = false; + private done: () => void; - constructor( - private tui: TUI, - theme: Theme, - private done: () => void, - ) { + constructor(tui: TUI, theme: Theme, done: () => void) { super(theme); + this.tui = tui; + this.done = done; } handleInput(data: string): void { @@ -923,19 +940,19 @@ class ToggleDemoComponent extends BaseOverlay { class PassiveDemoController extends BaseOverlay { focused = false; + private tui: TUI; private typed = ""; private timerComponent: TimerPanel; private timerHandle: OverlayHandle | null = null; private interval: ReturnType | null = null; private inputCount = 0; private lastInputDebug = ""; + private done: () => void; - constructor( - private tui: TUI, - theme: Theme, - private done: () => void, - ) { + constructor(tui: TUI, theme: Theme, done: () => void) { super(theme); + this.tui = tui; + this.done = done; this.timerComponent = new TimerPanel(theme); this.timerHandle = this.tui.showOverlay(this.timerComponent, { nonCapturing: true, @@ -1015,16 +1032,16 @@ class TimerPanel extends BaseOverlay { // === Focus cycling demo === class FocusDemoController extends BaseOverlay { + private tui: TUI; private panels: FocusPanel[] = []; private handles: OverlayHandle[] = []; private focusIndex = -1; + private done: () => void; - constructor( - private tui: TUI, - theme: Theme, - private done: () => void, - ) { + constructor(tui: TUI, theme: Theme, done: () => void) { super(theme); + this.tui = tui; + this.done = done; const colors = ["error", "success", "accent"] as const; const labels = ["Alpha", "Beta", "Gamma"]; @@ -1107,16 +1124,22 @@ class FocusDemoController extends BaseOverlay { class FocusPanel extends BaseOverlay { handle: OverlayHandle | null = null; readonly label: string; + private color: "error" | "success" | "accent"; + private onTab: () => void; + private onClose: () => void; constructor( theme: Theme, label: string, - private color: "error" | "success" | "accent", - private onTab: () => void, - private onClose: () => void, + color: "error" | "success" | "accent", + onTab: () => void, + onClose: () => void, ) { super(theme); this.label = label; + this.color = color; + this.onTab = onTab; + this.onClose = onClose; } handleInput(data: string): void { @@ -1155,19 +1178,19 @@ class FocusPanel extends BaseOverlay { // === Streaming input panel test (/overlay-streaming) === class StreamingInputController extends BaseOverlay { + private tui: TUI; private panels: StreamingInputPanel[] = []; private handles: OverlayHandle[] = []; private focusIndex = -1; // -1 = controller focused, 0-2 = panel focused private streamLines: string[] = []; private streamInterval: ReturnType | null = null; private lineCount = 0; + private done: () => void; - constructor( - private tui: TUI, - theme: Theme, - private done: () => void, - ) { + constructor(tui: TUI, theme: Theme, done: () => void) { super(theme); + this.tui = tui; + this.done = done; // Create 3 input panels as non-capturing overlays const colors = ["error", "success", "accent"] as const; @@ -1287,17 +1310,25 @@ class StreamingInputController extends BaseOverlay { class StreamingInputPanel implements Component { handle: OverlayHandle | null = null; + private theme: Theme; private typed = ""; readonly label: string; + private color: "error" | "success" | "accent"; + private onTab: () => void; + private onClose: () => void; constructor( - private theme: Theme, + theme: Theme, label: string, - private color: "error" | "success" | "accent", - private onTab: () => void, - private onClose: () => void, + color: "error" | "success" | "accent", + onTab: () => void, + onClose: () => void, ) { + this.theme = theme; this.label = label; + this.color = color; + this.onTab = onTab; + this.onClose = onClose; } handleInput(data: string): void { diff --git a/packages/coding-agent/examples/extensions/overlay-test.ts b/packages/coding-agent/examples/extensions/overlay-test.ts index 02f8204b..c51cde1a 100644 --- a/packages/coding-agent/examples/extensions/overlay-test.ts +++ b/packages/coding-agent/examples/extensions/overlay-test.ts @@ -42,10 +42,13 @@ class OverlayTestComponent implements Focusable { { label: "Cancel", hasInput: false, text: "", cursor: 0 }, ]; - constructor( - private theme: Theme, - private done: (result: { action: string; query?: string } | undefined) => void, - ) {} + private theme: Theme; + private done: (result: { action: string; query?: string } | undefined) => void; + + constructor(theme: Theme, done: (result: { action: string; query?: string } | undefined) => void) { + this.theme = theme; + this.done = done; + } handleInput(data: string): void { if (matchesKey(data, "escape")) { diff --git a/packages/coding-agent/src/bun/cli.ts b/packages/coding-agent/src/bun/cli.ts index 0aacb95b..fa68a0ba 100644 --- a/packages/coding-agent/src/bun/cli.ts +++ b/packages/coding-agent/src/bun/cli.ts @@ -1,12 +1,12 @@ #!/usr/bin/env node -import { APP_NAME } from "../config.js"; +import { APP_NAME } from "../config.ts"; process.title = APP_NAME; process.emitWarning = (() => {}) as typeof process.emitWarning; -import { restoreSandboxEnv } from "./restore-sandbox-env.js"; +import { restoreSandboxEnv } from "./restore-sandbox-env.ts"; restoreSandboxEnv(); -await import("./register-bedrock.js"); -await import("../cli.js"); +await import("./register-bedrock.ts"); +await import("../cli.ts"); diff --git a/packages/coding-agent/src/cli.ts b/packages/coding-agent/src/cli.ts index 1fdadb99..4f09675e 100644 --- a/packages/coding-agent/src/cli.ts +++ b/packages/coding-agent/src/cli.ts @@ -6,8 +6,8 @@ * Test with: npx tsx src/cli-new.ts [args...] */ import * as undici from "undici"; -import { APP_NAME } from "./config.js"; -import { main } from "./main.js"; +import { APP_NAME } from "./config.ts"; +import { main } from "./main.ts"; process.title = APP_NAME; process.env.PI_CODING_AGENT = "true"; diff --git a/packages/coding-agent/src/cli/args.ts b/packages/coding-agent/src/cli/args.ts index 7d91a9be..e4cb27dd 100644 --- a/packages/coding-agent/src/cli/args.ts +++ b/packages/coding-agent/src/cli/args.ts @@ -4,8 +4,8 @@ import type { ThinkingLevel } from "@earendil-works/pi-agent-core"; import chalk from "chalk"; -import { APP_NAME, CONFIG_DIR_NAME, ENV_AGENT_DIR, ENV_SESSION_DIR } from "../config.js"; -import type { ExtensionFlag } from "../core/extensions/types.js"; +import { APP_NAME, CONFIG_DIR_NAME, ENV_AGENT_DIR, ENV_SESSION_DIR } from "../config.ts"; +import type { ExtensionFlag } from "../core/extensions/types.ts"; export type Mode = "text" | "json" | "rpc"; diff --git a/packages/coding-agent/src/cli/config-selector.ts b/packages/coding-agent/src/cli/config-selector.ts index 18ff3b53..74265a10 100644 --- a/packages/coding-agent/src/cli/config-selector.ts +++ b/packages/coding-agent/src/cli/config-selector.ts @@ -3,10 +3,10 @@ */ import { ProcessTerminal, TUI } from "@earendil-works/pi-tui"; -import type { ResolvedPaths } from "../core/package-manager.js"; -import type { SettingsManager } from "../core/settings-manager.js"; -import { ConfigSelectorComponent } from "../modes/interactive/components/config-selector.js"; -import { initTheme, stopThemeWatcher } from "../modes/interactive/theme/theme.js"; +import type { ResolvedPaths } from "../core/package-manager.ts"; +import type { SettingsManager } from "../core/settings-manager.ts"; +import { ConfigSelectorComponent } from "../modes/interactive/components/config-selector.ts"; +import { initTheme, stopThemeWatcher } from "../modes/interactive/theme/theme.ts"; export interface ConfigSelectorOptions { resolvedPaths: ResolvedPaths; diff --git a/packages/coding-agent/src/cli/file-processor.ts b/packages/coding-agent/src/cli/file-processor.ts index c9ab5495..fe0e32eb 100644 --- a/packages/coding-agent/src/cli/file-processor.ts +++ b/packages/coding-agent/src/cli/file-processor.ts @@ -6,9 +6,9 @@ import { access, readFile, stat } from "node:fs/promises"; import type { ImageContent } from "@earendil-works/pi-ai"; import chalk from "chalk"; import { resolve } from "path"; -import { resolveReadPath } from "../core/tools/path-utils.js"; -import { formatDimensionNote, resizeImage } from "../utils/image-resize.js"; -import { detectSupportedImageMimeTypeFromFile } from "../utils/mime.js"; +import { resolveReadPath } from "../core/tools/path-utils.ts"; +import { formatDimensionNote, resizeImage } from "../utils/image-resize.ts"; +import { detectSupportedImageMimeTypeFromFile } from "../utils/mime.ts"; export interface ProcessedFiles { text: string; diff --git a/packages/coding-agent/src/cli/initial-message.ts b/packages/coding-agent/src/cli/initial-message.ts index a3a7f1b7..c73f18a2 100644 --- a/packages/coding-agent/src/cli/initial-message.ts +++ b/packages/coding-agent/src/cli/initial-message.ts @@ -1,5 +1,5 @@ import type { ImageContent } from "@earendil-works/pi-ai"; -import type { Args } from "./args.js"; +import type { Args } from "./args.ts"; export interface InitialMessageInput { parsed: Args; diff --git a/packages/coding-agent/src/cli/list-models.ts b/packages/coding-agent/src/cli/list-models.ts index 7d538573..b648fb96 100644 --- a/packages/coding-agent/src/cli/list-models.ts +++ b/packages/coding-agent/src/cli/list-models.ts @@ -5,8 +5,8 @@ import type { Api, Model } from "@earendil-works/pi-ai"; import { fuzzyFilter } from "@earendil-works/pi-tui"; import chalk from "chalk"; -import { formatNoModelsAvailableMessage } from "../core/auth-guidance.js"; -import type { ModelRegistry } from "../core/model-registry.js"; +import { formatNoModelsAvailableMessage } from "../core/auth-guidance.ts"; +import type { ModelRegistry } from "../core/model-registry.ts"; /** * Format a number as human-readable (e.g., 200000 -> "200K", 1000000 -> "1M") diff --git a/packages/coding-agent/src/cli/session-picker.ts b/packages/coding-agent/src/cli/session-picker.ts index f84b6a08..42dcb808 100644 --- a/packages/coding-agent/src/cli/session-picker.ts +++ b/packages/coding-agent/src/cli/session-picker.ts @@ -3,9 +3,9 @@ */ import { ProcessTerminal, setKeybindings, TUI } from "@earendil-works/pi-tui"; -import { KeybindingsManager } from "../core/keybindings.js"; -import type { SessionInfo, SessionListProgress } from "../core/session-manager.js"; -import { SessionSelectorComponent } from "../modes/interactive/components/session-selector.js"; +import { KeybindingsManager } from "../core/keybindings.ts"; +import type { SessionInfo, SessionListProgress } from "../core/session-manager.ts"; +import { SessionSelectorComponent } from "../modes/interactive/components/session-selector.ts"; type SessionsLoader = (onProgress?: SessionListProgress) => Promise; diff --git a/packages/coding-agent/src/config.ts b/packages/coding-agent/src/config.ts index e2ac87e1..1a94ea02 100644 --- a/packages/coding-agent/src/config.ts +++ b/packages/coding-agent/src/config.ts @@ -2,7 +2,7 @@ import { accessSync, constants, existsSync, readFileSync, realpathSync } from "f import { homedir } from "os"; import { basename, dirname, join, resolve, sep, win32 } from "path"; import { fileURLToPath } from "url"; -import { spawnProcessSync } from "./utils/child-process.js"; +import { spawnProcessSync } from "./utils/child-process.ts"; // ============================================================================= // Package Detection diff --git a/packages/coding-agent/src/core/agent-session-runtime.ts b/packages/coding-agent/src/core/agent-session-runtime.ts index eebd7575..a89fef83 100644 --- a/packages/coding-agent/src/core/agent-session-runtime.ts +++ b/packages/coding-agent/src/core/agent-session-runtime.ts @@ -1,12 +1,12 @@ import { copyFileSync, existsSync, mkdirSync } from "node:fs"; import { basename, join, resolve } from "node:path"; -import type { AgentSession } from "./agent-session.js"; -import type { AgentSessionRuntimeDiagnostic, AgentSessionServices } from "./agent-session-services.js"; -import type { ReplacedSessionContext, SessionShutdownEvent, SessionStartEvent } from "./extensions/index.js"; -import { emitSessionShutdownEvent } from "./extensions/runner.js"; -import type { CreateAgentSessionResult } from "./sdk.js"; -import { assertSessionCwdExists } from "./session-cwd.js"; -import { SessionManager } from "./session-manager.js"; +import type { AgentSession } from "./agent-session.ts"; +import type { AgentSessionRuntimeDiagnostic, AgentSessionServices } from "./agent-session-services.ts"; +import type { ReplacedSessionContext, SessionShutdownEvent, SessionStartEvent } from "./extensions/index.ts"; +import { emitSessionShutdownEvent } from "./extensions/runner.ts"; +import type { CreateAgentSessionResult } from "./sdk.ts"; +import { assertSessionCwdExists } from "./session-cwd.ts"; +import { SessionManager } from "./session-manager.ts"; /** * Result returned by runtime creation. @@ -417,4 +417,4 @@ export { type CreateAgentSessionServicesOptions, createAgentSessionFromServices, createAgentSessionServices, -} from "./agent-session-services.js"; +} from "./agent-session-services.ts"; diff --git a/packages/coding-agent/src/core/agent-session-services.ts b/packages/coding-agent/src/core/agent-session-services.ts index 061399ed..852776a9 100644 --- a/packages/coding-agent/src/core/agent-session-services.ts +++ b/packages/coding-agent/src/core/agent-session-services.ts @@ -1,14 +1,14 @@ import { join } from "node:path"; import type { ThinkingLevel } from "@earendil-works/pi-agent-core"; import type { Model } from "@earendil-works/pi-ai"; -import { getAgentDir } from "../config.js"; -import { AuthStorage } from "./auth-storage.js"; -import type { SessionStartEvent, ToolDefinition } from "./extensions/index.js"; -import { ModelRegistry } from "./model-registry.js"; -import { DefaultResourceLoader, type DefaultResourceLoaderOptions, type ResourceLoader } from "./resource-loader.js"; -import { type CreateAgentSessionOptions, type CreateAgentSessionResult, createAgentSession } from "./sdk.js"; -import type { SessionManager } from "./session-manager.js"; -import { SettingsManager } from "./settings-manager.js"; +import { getAgentDir } from "../config.ts"; +import { AuthStorage } from "./auth-storage.ts"; +import type { SessionStartEvent, ToolDefinition } from "./extensions/index.ts"; +import { ModelRegistry } from "./model-registry.ts"; +import { DefaultResourceLoader, type DefaultResourceLoaderOptions, type ResourceLoader } from "./resource-loader.ts"; +import { type CreateAgentSessionOptions, type CreateAgentSessionResult, createAgentSession } from "./sdk.ts"; +import type { SessionManager } from "./session-manager.ts"; +import { SettingsManager } from "./settings-manager.ts"; /** * Non-fatal issues collected while creating services or sessions. diff --git a/packages/coding-agent/src/core/agent-session.ts b/packages/coding-agent/src/core/agent-session.ts index 362a9b9f..d18591b7 100644 --- a/packages/coding-agent/src/core/agent-session.ts +++ b/packages/coding-agent/src/core/agent-session.ts @@ -33,11 +33,11 @@ import { resetApiProviders, streamSimple, } from "@earendil-works/pi-ai"; -import { theme } from "../modes/interactive/theme/theme.js"; -import { stripFrontmatter } from "../utils/frontmatter.js"; -import { sleep } from "../utils/sleep.js"; -import { formatNoApiKeyFoundMessage, formatNoModelSelectedMessage } from "./auth-guidance.js"; -import { type BashResult, executeBashWithOperations } from "./bash-executor.js"; +import { theme } from "../modes/interactive/theme/theme.ts"; +import { stripFrontmatter } from "../utils/frontmatter.ts"; +import { sleep } from "../utils/sleep.ts"; +import { formatNoApiKeyFoundMessage, formatNoModelSelectedMessage } from "./auth-guidance.ts"; +import { type BashResult, executeBashWithOperations } from "./bash-executor.ts"; import { type CompactionResult, calculateContextTokens, @@ -47,10 +47,10 @@ import { generateBranchSummary, prepareCompaction, shouldCompact, -} from "./compaction/index.js"; -import { DEFAULT_THINKING_LEVEL } from "./defaults.js"; -import { exportSessionToHtml, type ToolHtmlRenderer } from "./export-html/index.js"; -import { createToolHtmlRenderer } from "./export-html/tool-renderer.js"; +} from "./compaction/index.ts"; +import { DEFAULT_THINKING_LEVEL } from "./defaults.ts"; +import { exportSessionToHtml, type ToolHtmlRenderer } from "./export-html/index.ts"; +import { createToolHtmlRenderer } from "./export-html/tool-renderer.ts"; import { type ContextUsage, type ExtensionCommandContextActions, @@ -75,21 +75,21 @@ import { type TurnEndEvent, type TurnStartEvent, wrapRegisteredTools, -} from "./extensions/index.js"; -import { emitSessionShutdownEvent } from "./extensions/runner.js"; -import type { BashExecutionMessage, CustomMessage } from "./messages.js"; -import type { ModelRegistry } from "./model-registry.js"; -import { expandPromptTemplate, type PromptTemplate } from "./prompt-templates.js"; -import type { ResourceExtensionPaths, ResourceLoader } from "./resource-loader.js"; -import type { BranchSummaryEntry, CompactionEntry, SessionManager } from "./session-manager.js"; -import { CURRENT_SESSION_VERSION, getLatestCompactionEntry, type SessionHeader } from "./session-manager.js"; -import type { SettingsManager } from "./settings-manager.js"; -import type { SlashCommandInfo } from "./slash-commands.js"; -import { createSyntheticSourceInfo, type SourceInfo } from "./source-info.js"; -import { type BuildSystemPromptOptions, buildSystemPrompt } from "./system-prompt.js"; -import { type BashOperations, createLocalBashOperations } from "./tools/bash.js"; -import { createAllToolDefinitions } from "./tools/index.js"; -import { createToolDefinitionFromAgentTool } from "./tools/tool-definition-wrapper.js"; +} from "./extensions/index.ts"; +import { emitSessionShutdownEvent } from "./extensions/runner.ts"; +import type { BashExecutionMessage, CustomMessage } from "./messages.ts"; +import type { ModelRegistry } from "./model-registry.ts"; +import { expandPromptTemplate, type PromptTemplate } from "./prompt-templates.ts"; +import type { ResourceExtensionPaths, ResourceLoader } from "./resource-loader.ts"; +import type { BranchSummaryEntry, CompactionEntry, SessionManager } from "./session-manager.ts"; +import { CURRENT_SESSION_VERSION, getLatestCompactionEntry, type SessionHeader } from "./session-manager.ts"; +import type { SettingsManager } from "./settings-manager.ts"; +import type { SlashCommandInfo } from "./slash-commands.ts"; +import { createSyntheticSourceInfo, type SourceInfo } from "./source-info.ts"; +import { type BuildSystemPromptOptions, buildSystemPrompt } from "./system-prompt.ts"; +import { type BashOperations, createLocalBashOperations } from "./tools/bash.ts"; +import { createAllToolDefinitions } from "./tools/index.ts"; +import { createToolDefinitionFromAgentTool } from "./tools/tool-definition-wrapper.ts"; // ============================================================================ // Skill Block Parsing diff --git a/packages/coding-agent/src/core/auth-guidance.ts b/packages/coding-agent/src/core/auth-guidance.ts index 567b603d..9782cdaf 100644 --- a/packages/coding-agent/src/core/auth-guidance.ts +++ b/packages/coding-agent/src/core/auth-guidance.ts @@ -1,5 +1,5 @@ import { join } from "node:path"; -import { getDocsPath } from "../config.js"; +import { getDocsPath } from "../config.ts"; const UNKNOWN_PROVIDER = "unknown"; diff --git a/packages/coding-agent/src/core/auth-storage.ts b/packages/coding-agent/src/core/auth-storage.ts index 1522cc67..ae2996c4 100644 --- a/packages/coding-agent/src/core/auth-storage.ts +++ b/packages/coding-agent/src/core/auth-storage.ts @@ -17,8 +17,8 @@ import { getOAuthApiKey, getOAuthProvider, getOAuthProviders } from "@earendil-w import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "fs"; import { dirname, join } from "path"; import lockfile from "proper-lockfile"; -import { getAgentDir } from "../config.js"; -import { resolveConfigValue } from "./resolve-config-value.js"; +import { getAgentDir } from "../config.ts"; +import { resolveConfigValue } from "./resolve-config-value.ts"; export type ApiKeyCredential = { type: "api_key"; diff --git a/packages/coding-agent/src/core/bash-executor.ts b/packages/coding-agent/src/core/bash-executor.ts index c1818dac..bacb2ecc 100644 --- a/packages/coding-agent/src/core/bash-executor.ts +++ b/packages/coding-agent/src/core/bash-executor.ts @@ -10,10 +10,10 @@ import { randomBytes } from "node:crypto"; import { createWriteStream, type WriteStream } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; -import { stripAnsi } from "../utils/ansi.js"; -import { sanitizeBinaryOutput } from "../utils/shell.js"; -import type { BashOperations } from "./tools/bash.js"; -import { DEFAULT_MAX_BYTES, truncateTail } from "./tools/truncate.js"; +import { stripAnsi } from "../utils/ansi.ts"; +import { sanitizeBinaryOutput } from "../utils/shell.ts"; +import type { BashOperations } from "./tools/bash.ts"; +import { DEFAULT_MAX_BYTES, truncateTail } from "./tools/truncate.ts"; // ============================================================================ // Types diff --git a/packages/coding-agent/src/core/compaction/branch-summarization.ts b/packages/coding-agent/src/core/compaction/branch-summarization.ts index 69925dda..0039bc79 100644 --- a/packages/coding-agent/src/core/compaction/branch-summarization.ts +++ b/packages/coding-agent/src/core/compaction/branch-summarization.ts @@ -13,9 +13,9 @@ import { createBranchSummaryMessage, createCompactionSummaryMessage, createCustomMessage, -} from "../messages.js"; -import type { ReadonlySessionManager, SessionEntry } from "../session-manager.js"; -import { estimateTokens } from "./compaction.js"; +} from "../messages.ts"; +import type { ReadonlySessionManager, SessionEntry } from "../session-manager.ts"; +import { estimateTokens } from "./compaction.ts"; import { computeFileLists, createFileOps, @@ -24,7 +24,7 @@ import { formatFileOperations, SUMMARIZATION_SYSTEM_PROMPT, serializeConversation, -} from "./utils.js"; +} from "./utils.ts"; // ============================================================================ // Types @@ -44,7 +44,7 @@ export interface BranchSummaryDetails { modifiedFiles: string[]; } -export type { FileOperations } from "./utils.js"; +export type { FileOperations } from "./utils.ts"; export interface BranchPreparation { /** Messages extracted for summarization, in chronological order */ diff --git a/packages/coding-agent/src/core/compaction/compaction.ts b/packages/coding-agent/src/core/compaction/compaction.ts index c12be0ee..b8339fba 100644 --- a/packages/coding-agent/src/core/compaction/compaction.ts +++ b/packages/coding-agent/src/core/compaction/compaction.ts @@ -13,8 +13,8 @@ import { createBranchSummaryMessage, createCompactionSummaryMessage, createCustomMessage, -} from "../messages.js"; -import { buildSessionContext, type CompactionEntry, type SessionEntry } from "../session-manager.js"; +} from "../messages.ts"; +import { buildSessionContext, type CompactionEntry, type SessionEntry } from "../session-manager.ts"; import { computeFileLists, createFileOps, @@ -23,7 +23,7 @@ import { formatFileOperations, SUMMARIZATION_SYSTEM_PROMPT, serializeConversation, -} from "./utils.js"; +} from "./utils.ts"; // ============================================================================ // File Operation Tracking diff --git a/packages/coding-agent/src/core/compaction/index.ts b/packages/coding-agent/src/core/compaction/index.ts index d8c92a67..7fae5f2c 100644 --- a/packages/coding-agent/src/core/compaction/index.ts +++ b/packages/coding-agent/src/core/compaction/index.ts @@ -2,6 +2,6 @@ * Compaction and summarization utilities. */ -export * from "./branch-summarization.js"; -export * from "./compaction.js"; -export * from "./utils.js"; +export * from "./branch-summarization.ts"; +export * from "./compaction.ts"; +export * from "./utils.ts"; diff --git a/packages/coding-agent/src/core/exec.ts b/packages/coding-agent/src/core/exec.ts index 40f0453d..5afe9ec3 100644 --- a/packages/coding-agent/src/core/exec.ts +++ b/packages/coding-agent/src/core/exec.ts @@ -3,7 +3,7 @@ */ import { spawn } from "node:child_process"; -import { waitForChildProcess } from "../utils/child-process.js"; +import { waitForChildProcess } from "../utils/child-process.ts"; /** * Options for executing shell commands. diff --git a/packages/coding-agent/src/core/export-html/index.ts b/packages/coding-agent/src/core/export-html/index.ts index 6b5579b6..a831ae42 100644 --- a/packages/coding-agent/src/core/export-html/index.ts +++ b/packages/coding-agent/src/core/export-html/index.ts @@ -1,11 +1,11 @@ import type { AgentState } from "@earendil-works/pi-agent-core"; import { existsSync, readFileSync, writeFileSync } from "fs"; import { basename, join } from "path"; -import { APP_NAME, getExportTemplateDir } from "../../config.js"; -import { getResolvedThemeColors, getThemeExportColors } from "../../modes/interactive/theme/theme.js"; -import type { ToolDefinition } from "../extensions/types.js"; -import type { SessionEntry } from "../session-manager.js"; -import { SessionManager } from "../session-manager.js"; +import { APP_NAME, getExportTemplateDir } from "../../config.ts"; +import { getResolvedThemeColors, getThemeExportColors } from "../../modes/interactive/theme/theme.ts"; +import type { ToolDefinition } from "../extensions/types.ts"; +import type { SessionEntry } from "../session-manager.ts"; +import { SessionManager } from "../session-manager.ts"; /** * Interface for rendering custom tools to HTML. diff --git a/packages/coding-agent/src/core/export-html/tool-renderer.ts b/packages/coding-agent/src/core/export-html/tool-renderer.ts index ee4fcb86..c2030a27 100644 --- a/packages/coding-agent/src/core/export-html/tool-renderer.ts +++ b/packages/coding-agent/src/core/export-html/tool-renderer.ts @@ -7,9 +7,9 @@ import type { ImageContent, TextContent } from "@earendil-works/pi-ai"; import type { Component } from "@earendil-works/pi-tui"; -import type { Theme } from "../../modes/interactive/theme/theme.js"; -import type { ToolDefinition, ToolRenderContext } from "../extensions/types.js"; -import { ansiLinesToHtml } from "./ansi-to-html.js"; +import type { Theme } from "../../modes/interactive/theme/theme.ts"; +import type { ToolDefinition, ToolRenderContext } from "../extensions/types.ts"; +import { ansiLinesToHtml } from "./ansi-to-html.ts"; export interface ToolHtmlRendererDeps { /** Function to look up tool definition by name */ diff --git a/packages/coding-agent/src/core/extensions/index.ts b/packages/coding-agent/src/core/extensions/index.ts index fdc235c6..acbc50b1 100644 --- a/packages/coding-agent/src/core/extensions/index.ts +++ b/packages/coding-agent/src/core/extensions/index.ts @@ -2,14 +2,14 @@ * Extension system for lifecycle events and custom tools. */ -export type { SlashCommandInfo, SlashCommandSource } from "../slash-commands.js"; -export type { SourceInfo } from "../source-info.js"; +export type { SlashCommandInfo, SlashCommandSource } from "../slash-commands.ts"; +export type { SourceInfo } from "../source-info.ts"; export { createExtensionRuntime, discoverAndLoadExtensions, loadExtensionFromFactory, loadExtensions, -} from "./loader.js"; +} from "./loader.ts"; export type { ExtensionErrorListener, ForkHandler, @@ -17,8 +17,8 @@ export type { NewSessionHandler, ShutdownHandler, SwitchSessionHandler, -} from "./runner.js"; -export { ExtensionRunner } from "./runner.js"; +} from "./runner.ts"; +export { ExtensionRunner } from "./runner.ts"; export type { AfterProviderResponseEvent, AgentEndEvent, @@ -156,7 +156,7 @@ export type { WorkingIndicatorOptions, WriteToolCallEvent, WriteToolResultEvent, -} from "./types.js"; +} from "./types.ts"; // Type guards export { defineTool, @@ -168,5 +168,5 @@ export { isReadToolResult, isToolCallEventType, isWriteToolResult, -} from "./types.js"; -export { wrapRegisteredTool, wrapRegisteredTools } from "./wrapper.js"; +} from "./types.ts"; +export { wrapRegisteredTool, wrapRegisteredTools } from "./wrapper.ts"; diff --git a/packages/coding-agent/src/core/extensions/loader.ts b/packages/coding-agent/src/core/extensions/loader.ts index b2d7b275..60ad625d 100644 --- a/packages/coding-agent/src/core/extensions/loader.ts +++ b/packages/coding-agent/src/core/extensions/loader.ts @@ -20,14 +20,14 @@ import { createJiti } from "jiti/static"; import * as _bundledTypebox from "typebox"; import * as _bundledTypeboxCompile from "typebox/compile"; import * as _bundledTypeboxValue from "typebox/value"; -import { CONFIG_DIR_NAME, getAgentDir, isBunBinary } from "../../config.js"; +import { CONFIG_DIR_NAME, getAgentDir, isBunBinary } from "../../config.ts"; // NOTE: This import works because loader.ts exports are NOT re-exported from index.ts, // avoiding a circular dependency. Extensions can import from @earendil-works/pi-coding-agent. -import * as _bundledPiCodingAgent from "../../index.js"; -import { createEventBus, type EventBus } from "../event-bus.js"; -import type { ExecOptions } from "../exec.js"; -import { execCommand } from "../exec.js"; -import { createSyntheticSourceInfo } from "../source-info.js"; +import * as _bundledPiCodingAgent from "../../index.ts"; +import { createEventBus, type EventBus } from "../event-bus.ts"; +import type { ExecOptions } from "../exec.ts"; +import { execCommand } from "../exec.ts"; +import { createSyntheticSourceInfo } from "../source-info.ts"; import type { Extension, ExtensionAPI, @@ -38,7 +38,7 @@ import type { ProviderConfig, RegisteredCommand, ToolDefinition, -} from "./types.js"; +} from "./types.ts"; /** Modules available to extensions via virtualModules (for compiled Bun binary) */ const VIRTUAL_MODULES: Record = { @@ -236,7 +236,7 @@ function createExtensionAPI( shortcut: KeyId, options: { description?: string; - handler: (ctx: import("./types.js").ExtensionContext) => Promise | void; + handler: (ctx: import("./types.ts").ExtensionContext) => Promise | void; }, ): void { runtime.assertActive(); diff --git a/packages/coding-agent/src/core/extensions/runner.ts b/packages/coding-agent/src/core/extensions/runner.ts index c37a4588..167c52cc 100644 --- a/packages/coding-agent/src/core/extensions/runner.ts +++ b/packages/coding-agent/src/core/extensions/runner.ts @@ -5,12 +5,12 @@ import type { AgentMessage } from "@earendil-works/pi-agent-core"; import type { ImageContent, Model } from "@earendil-works/pi-ai"; import type { KeyId } from "@earendil-works/pi-tui"; -import { type Theme, theme } from "../../modes/interactive/theme/theme.js"; -import type { ResourceDiagnostic } from "../diagnostics.js"; -import type { KeybindingsConfig } from "../keybindings.js"; -import type { ModelRegistry } from "../model-registry.js"; -import type { SessionManager } from "../session-manager.js"; -import type { BuildSystemPromptOptions } from "../system-prompt.js"; +import { type Theme, theme } from "../../modes/interactive/theme/theme.ts"; +import type { ResourceDiagnostic } from "../diagnostics.ts"; +import type { KeybindingsConfig } from "../keybindings.ts"; +import type { ModelRegistry } from "../model-registry.ts"; +import type { SessionManager } from "../session-manager.ts"; +import type { BuildSystemPromptOptions } from "../system-prompt.ts"; import type { BeforeAgentStartEvent, BeforeAgentStartEventResult, @@ -55,7 +55,7 @@ import type { ToolResultEventResult, UserBashEvent, UserBashEventResult, -} from "./types.js"; +} from "./types.ts"; // Extension shortcuts compete with canonical keybinding ids from keybindings.json. // Only editor-global shortcuts are reserved here. Picker-specific bindings are not. diff --git a/packages/coding-agent/src/core/extensions/types.ts b/packages/coding-agent/src/core/extensions/types.ts index 4c1b6fca..58b85227 100644 --- a/packages/coding-agent/src/core/extensions/types.ts +++ b/packages/coding-agent/src/core/extensions/types.ts @@ -40,27 +40,27 @@ import type { TUI, } from "@earendil-works/pi-tui"; import type { Static, TSchema } from "typebox"; -import type { Theme } from "../../modes/interactive/theme/theme.js"; -import type { BashResult } from "../bash-executor.js"; -import type { CompactionPreparation, CompactionResult } from "../compaction/index.js"; -import type { EventBus } from "../event-bus.js"; -import type { ExecOptions, ExecResult } from "../exec.js"; -import type { ReadonlyFooterDataProvider } from "../footer-data-provider.js"; -import type { KeybindingsManager } from "../keybindings.js"; -import type { CustomMessage } from "../messages.js"; -import type { ModelRegistry } from "../model-registry.js"; +import type { Theme } from "../../modes/interactive/theme/theme.ts"; +import type { BashResult } from "../bash-executor.ts"; +import type { CompactionPreparation, CompactionResult } from "../compaction/index.ts"; +import type { EventBus } from "../event-bus.ts"; +import type { ExecOptions, ExecResult } from "../exec.ts"; +import type { ReadonlyFooterDataProvider } from "../footer-data-provider.ts"; +import type { KeybindingsManager } from "../keybindings.ts"; +import type { CustomMessage } from "../messages.ts"; +import type { ModelRegistry } from "../model-registry.ts"; import type { BranchSummaryEntry, CompactionEntry, ReadonlySessionManager, SessionEntry, SessionManager, -} from "../session-manager.js"; -import type { SlashCommandInfo } from "../slash-commands.js"; -import type { SourceInfo } from "../source-info.js"; -import type { BuildSystemPromptOptions } from "../system-prompt.js"; -import type { BashOperations } from "../tools/bash.js"; -import type { EditToolDetails } from "../tools/edit.js"; +} from "../session-manager.ts"; +import type { SlashCommandInfo } from "../slash-commands.ts"; +import type { SourceInfo } from "../source-info.ts"; +import type { BuildSystemPromptOptions } from "../system-prompt.ts"; +import type { BashOperations } from "../tools/bash.ts"; +import type { EditToolDetails } from "../tools/edit.ts"; import type { BashToolDetails, BashToolInput, @@ -74,12 +74,12 @@ import type { ReadToolDetails, ReadToolInput, WriteToolInput, -} from "../tools/index.js"; +} from "../tools/index.ts"; -export type { ExecOptions, ExecResult } from "../exec.js"; -export type { BuildSystemPromptOptions } from "../system-prompt.js"; +export type { ExecOptions, ExecResult } from "../exec.ts"; +export type { BuildSystemPromptOptions } from "../system-prompt.ts"; export type { AgentToolResult, AgentToolUpdateCallback, ToolExecutionMode }; -export type { AppKeybinding, KeybindingsManager } from "../keybindings.js"; +export type { AppKeybinding, KeybindingsManager } from "../keybindings.ts"; // ============================================================================ // UI Context diff --git a/packages/coding-agent/src/core/extensions/wrapper.ts b/packages/coding-agent/src/core/extensions/wrapper.ts index 9b9d281c..6a84038c 100644 --- a/packages/coding-agent/src/core/extensions/wrapper.ts +++ b/packages/coding-agent/src/core/extensions/wrapper.ts @@ -6,9 +6,9 @@ */ import type { AgentTool } from "@earendil-works/pi-agent-core"; -import { wrapToolDefinition, wrapToolDefinitions } from "../tools/tool-definition-wrapper.js"; -import type { ExtensionRunner } from "./runner.js"; -import type { RegisteredTool } from "./types.js"; +import { wrapToolDefinition, wrapToolDefinitions } from "../tools/tool-definition-wrapper.ts"; +import type { ExtensionRunner } from "./runner.ts"; +import type { RegisteredTool } from "./types.ts"; /** * Wrap a RegisteredTool into an AgentTool. diff --git a/packages/coding-agent/src/core/footer-data-provider.ts b/packages/coding-agent/src/core/footer-data-provider.ts index 5d729221..b7ea09b8 100644 --- a/packages/coding-agent/src/core/footer-data-provider.ts +++ b/packages/coding-agent/src/core/footer-data-provider.ts @@ -1,7 +1,7 @@ import { type ExecFileException, execFile, spawnSync } from "child_process"; import { existsSync, type FSWatcher, readFileSync, statSync, unwatchFile, watchFile } from "fs"; import { dirname, join, resolve } from "path"; -import { closeWatcher, FS_WATCH_RETRY_DELAY_MS, watchWithErrorHandler } from "../utils/fs-watch.js"; +import { closeWatcher, FS_WATCH_RETRY_DELAY_MS, watchWithErrorHandler } from "../utils/fs-watch.ts"; type GitPaths = { repoDir: string; diff --git a/packages/coding-agent/src/core/index.ts b/packages/coding-agent/src/core/index.ts index 003e1394..71c45e9c 100644 --- a/packages/coding-agent/src/core/index.ts +++ b/packages/coding-agent/src/core/index.ts @@ -10,13 +10,13 @@ export { type ModelCycleResult, type PromptOptions, type SessionStats, -} from "./agent-session.js"; +} from "./agent-session.ts"; export { AgentSessionRuntime, type CreateAgentSessionRuntimeFactory, type CreateAgentSessionRuntimeResult, createAgentSessionRuntime, -} from "./agent-session-runtime.js"; +} from "./agent-session-runtime.ts"; export { type AgentSessionRuntimeDiagnostic, type AgentSessionServices, @@ -24,10 +24,10 @@ export { type CreateAgentSessionServicesOptions, createAgentSessionFromServices, createAgentSessionServices, -} from "./agent-session-services.js"; -export { type BashExecutorOptions, type BashResult, executeBashWithOperations } from "./bash-executor.js"; -export type { CompactionResult } from "./compaction/index.js"; -export { createEventBus, type EventBus, type EventBusController } from "./event-bus.js"; +} from "./agent-session-services.ts"; +export { type BashExecutorOptions, type BashResult, executeBashWithOperations } from "./bash-executor.ts"; +export type { CompactionResult } from "./compaction/index.ts"; +export { createEventBus, type EventBus, type EventBusController } from "./event-bus.ts"; // Extensions system export { type AgentEndEvent, @@ -73,5 +73,5 @@ export { type TurnEndEvent, type TurnStartEvent, type WorkingIndicatorOptions, -} from "./extensions/index.js"; -export { createSyntheticSourceInfo } from "./source-info.js"; +} from "./extensions/index.ts"; +export { createSyntheticSourceInfo } from "./source-info.ts"; diff --git a/packages/coding-agent/src/core/keybindings.ts b/packages/coding-agent/src/core/keybindings.ts index f1663b1a..7c0de643 100644 --- a/packages/coding-agent/src/core/keybindings.ts +++ b/packages/coding-agent/src/core/keybindings.ts @@ -8,7 +8,7 @@ import { } from "@earendil-works/pi-tui"; import { existsSync, readFileSync } from "fs"; import { join } from "path"; -import { getAgentDir } from "../config.js"; +import { getAgentDir } from "../config.ts"; export interface AppKeybindings { "app.interrupt": true; diff --git a/packages/coding-agent/src/core/model-registry.ts b/packages/coding-agent/src/core/model-registry.ts index 6cf8c40f..4d040463 100644 --- a/packages/coding-agent/src/core/model-registry.ts +++ b/packages/coding-agent/src/core/model-registry.ts @@ -24,15 +24,15 @@ import { join } from "path"; import { type Static, Type } from "typebox"; import { Compile } from "typebox/compile"; import type { TLocalizedValidationError } from "typebox/error"; -import { getAgentDir } from "../config.js"; -import type { AuthStatus, AuthStorage } from "./auth-storage.js"; -import { BUILT_IN_PROVIDER_DISPLAY_NAMES } from "./provider-display-names.js"; +import { getAgentDir } from "../config.ts"; +import type { AuthStatus, AuthStorage } from "./auth-storage.ts"; +import { BUILT_IN_PROVIDER_DISPLAY_NAMES } from "./provider-display-names.ts"; import { clearConfigValueCache, resolveConfigValueOrThrow, resolveConfigValueUncached, resolveHeadersOrThrow, -} from "./resolve-config-value.js"; +} from "./resolve-config-value.ts"; // Schema for OpenRouter routing preferences const PercentileCutoffsSchema = Type.Object({ diff --git a/packages/coding-agent/src/core/model-resolver.ts b/packages/coding-agent/src/core/model-resolver.ts index 18a3eed8..764c06f5 100644 --- a/packages/coding-agent/src/core/model-resolver.ts +++ b/packages/coding-agent/src/core/model-resolver.ts @@ -6,9 +6,9 @@ import type { ThinkingLevel } from "@earendil-works/pi-agent-core"; import { type Api, type KnownProvider, type Model, modelsAreEqual } from "@earendil-works/pi-ai"; import chalk from "chalk"; import { minimatch } from "minimatch"; -import { isValidThinkingLevel } from "../cli/args.js"; -import { DEFAULT_THINKING_LEVEL } from "./defaults.js"; -import type { ModelRegistry } from "./model-registry.js"; +import { isValidThinkingLevel } from "../cli/args.ts"; +import { DEFAULT_THINKING_LEVEL } from "./defaults.ts"; +import type { ModelRegistry } from "./model-registry.ts"; /** Default model IDs for each known provider */ export const defaultModelPerProvider: Record = { diff --git a/packages/coding-agent/src/core/package-manager.ts b/packages/coding-agent/src/core/package-manager.ts index 04135cbf..59d53427 100644 --- a/packages/coding-agent/src/core/package-manager.ts +++ b/packages/coding-agent/src/core/package-manager.ts @@ -27,12 +27,12 @@ import type { Readable } from "node:stream"; import { globSync } from "glob"; import ignore from "ignore"; import { minimatch } from "minimatch"; -import { CONFIG_DIR_NAME } from "../config.js"; -import { spawnProcess, spawnProcessSync } from "../utils/child-process.js"; -import { type GitSource, parseGitUrl } from "../utils/git.js"; -import { canonicalizePath, isLocalPath } from "../utils/paths.js"; -import { isStdoutTakenOver } from "./output-guard.js"; -import type { PackageSource, SettingsManager } from "./settings-manager.js"; +import { CONFIG_DIR_NAME } from "../config.ts"; +import { spawnProcess, spawnProcessSync } from "../utils/child-process.ts"; +import { type GitSource, parseGitUrl } from "../utils/git.ts"; +import { canonicalizePath, isLocalPath } from "../utils/paths.ts"; +import { isStdoutTakenOver } from "./output-guard.ts"; +import type { PackageSource, SettingsManager } from "./settings-manager.ts"; const NETWORK_TIMEOUT_MS = 10000; const UPDATE_CHECK_CONCURRENCY = 4; diff --git a/packages/coding-agent/src/core/prompt-templates.ts b/packages/coding-agent/src/core/prompt-templates.ts index 45e779a1..b646896d 100644 --- a/packages/coding-agent/src/core/prompt-templates.ts +++ b/packages/coding-agent/src/core/prompt-templates.ts @@ -1,9 +1,9 @@ import { existsSync, readdirSync, readFileSync, statSync } from "fs"; import { homedir } from "os"; import { basename, dirname, isAbsolute, join, resolve, sep } from "path"; -import { CONFIG_DIR_NAME } from "../config.js"; -import { parseFrontmatter } from "../utils/frontmatter.js"; -import { createSyntheticSourceInfo, type SourceInfo } from "./source-info.js"; +import { CONFIG_DIR_NAME } from "../config.ts"; +import { parseFrontmatter } from "../utils/frontmatter.ts"; +import { createSyntheticSourceInfo, type SourceInfo } from "./source-info.ts"; /** * Represents a prompt template loaded from a markdown file diff --git a/packages/coding-agent/src/core/resolve-config-value.ts b/packages/coding-agent/src/core/resolve-config-value.ts index 646042e1..215914ba 100644 --- a/packages/coding-agent/src/core/resolve-config-value.ts +++ b/packages/coding-agent/src/core/resolve-config-value.ts @@ -4,7 +4,7 @@ */ import { execSync, spawnSync } from "child_process"; -import { getShellConfig } from "../utils/shell.js"; +import { getShellConfig } from "../utils/shell.ts"; // Cache for shell command results (persists for process lifetime) const commandResultCache = new Map(); diff --git a/packages/coding-agent/src/core/resource-loader.ts b/packages/coding-agent/src/core/resource-loader.ts index 338daf7e..57d90f97 100644 --- a/packages/coding-agent/src/core/resource-loader.ts +++ b/packages/coding-agent/src/core/resource-loader.ts @@ -2,23 +2,23 @@ import { existsSync, readdirSync, readFileSync, statSync } from "node:fs"; import { homedir } from "node:os"; import { join, resolve, sep } from "node:path"; import chalk from "chalk"; -import { CONFIG_DIR_NAME } from "../config.js"; -import { loadThemeFromPath, type Theme } from "../modes/interactive/theme/theme.js"; -import type { ResourceDiagnostic } from "./diagnostics.js"; +import { CONFIG_DIR_NAME } from "../config.ts"; +import { loadThemeFromPath, type Theme } from "../modes/interactive/theme/theme.ts"; +import type { ResourceDiagnostic } from "./diagnostics.ts"; -export type { ResourceCollision, ResourceDiagnostic } from "./diagnostics.js"; +export type { ResourceCollision, ResourceDiagnostic } from "./diagnostics.ts"; -import { canonicalizePath, isLocalPath } from "../utils/paths.js"; -import { createEventBus, type EventBus } from "./event-bus.js"; -import { createExtensionRuntime, loadExtensionFromFactory, loadExtensions } from "./extensions/loader.js"; -import type { Extension, ExtensionFactory, ExtensionRuntime, LoadExtensionsResult } from "./extensions/types.js"; -import { DefaultPackageManager, type PathMetadata } from "./package-manager.js"; -import type { PromptTemplate } from "./prompt-templates.js"; -import { loadPromptTemplates } from "./prompt-templates.js"; -import { SettingsManager } from "./settings-manager.js"; -import type { Skill } from "./skills.js"; -import { loadSkills } from "./skills.js"; -import { createSourceInfo, type SourceInfo } from "./source-info.js"; +import { canonicalizePath, isLocalPath } from "../utils/paths.ts"; +import { createEventBus, type EventBus } from "./event-bus.ts"; +import { createExtensionRuntime, loadExtensionFromFactory, loadExtensions } from "./extensions/loader.ts"; +import type { Extension, ExtensionFactory, ExtensionRuntime, LoadExtensionsResult } from "./extensions/types.ts"; +import { DefaultPackageManager, type PathMetadata } from "./package-manager.ts"; +import type { PromptTemplate } from "./prompt-templates.ts"; +import { loadPromptTemplates } from "./prompt-templates.ts"; +import { SettingsManager } from "./settings-manager.ts"; +import type { Skill } from "./skills.ts"; +import { loadSkills } from "./skills.ts"; +import { createSourceInfo, type SourceInfo } from "./source-info.ts"; export interface ResourceExtensionPaths { skillPaths?: Array<{ path: string; metadata: PathMetadata }>; diff --git a/packages/coding-agent/src/core/sdk.ts b/packages/coding-agent/src/core/sdk.ts index 3e0533d0..7caa0fa1 100644 --- a/packages/coding-agent/src/core/sdk.ts +++ b/packages/coding-agent/src/core/sdk.ts @@ -1,21 +1,21 @@ import { join } from "node:path"; import { Agent, type AgentMessage, type ThinkingLevel } from "@earendil-works/pi-agent-core"; import { clampThinkingLevel, type Message, type Model, streamSimple } from "@earendil-works/pi-ai"; -import { getAgentDir } from "../config.js"; -import { AgentSession } from "./agent-session.js"; -import { formatNoModelsAvailableMessage } from "./auth-guidance.js"; -import { AuthStorage } from "./auth-storage.js"; -import { DEFAULT_THINKING_LEVEL } from "./defaults.js"; -import type { ExtensionRunner, LoadExtensionsResult, SessionStartEvent, ToolDefinition } from "./extensions/index.js"; -import { convertToLlm } from "./messages.js"; -import { ModelRegistry } from "./model-registry.js"; -import { findInitialModel } from "./model-resolver.js"; -import type { ResourceLoader } from "./resource-loader.js"; -import { DefaultResourceLoader } from "./resource-loader.js"; -import { getDefaultSessionDir, SessionManager } from "./session-manager.js"; -import { SettingsManager } from "./settings-manager.js"; -import { isInstallTelemetryEnabled } from "./telemetry.js"; -import { time } from "./timings.js"; +import { getAgentDir } from "../config.ts"; +import { AgentSession } from "./agent-session.ts"; +import { formatNoModelsAvailableMessage } from "./auth-guidance.ts"; +import { AuthStorage } from "./auth-storage.ts"; +import { DEFAULT_THINKING_LEVEL } from "./defaults.ts"; +import type { ExtensionRunner, LoadExtensionsResult, SessionStartEvent, ToolDefinition } from "./extensions/index.ts"; +import { convertToLlm } from "./messages.ts"; +import { ModelRegistry } from "./model-registry.ts"; +import { findInitialModel } from "./model-resolver.ts"; +import type { ResourceLoader } from "./resource-loader.ts"; +import { DefaultResourceLoader } from "./resource-loader.ts"; +import { getDefaultSessionDir, SessionManager } from "./session-manager.ts"; +import { SettingsManager } from "./settings-manager.ts"; +import { isInstallTelemetryEnabled } from "./telemetry.ts"; +import { time } from "./timings.ts"; import { createBashTool, createCodingTools, @@ -28,7 +28,7 @@ import { createWriteTool, type ToolName, withFileMutationQueue, -} from "./tools/index.js"; +} from "./tools/index.ts"; export interface CreateAgentSessionOptions { /** Working directory for project-local discovery. Default: process.cwd() */ @@ -91,7 +91,7 @@ export interface CreateAgentSessionResult { // Re-exports -export * from "./agent-session-runtime.js"; +export * from "./agent-session-runtime.ts"; export type { ExtensionAPI, ExtensionCommandContext, @@ -100,10 +100,10 @@ export type { SlashCommandInfo, SlashCommandSource, ToolDefinition, -} from "./extensions/index.js"; -export type { PromptTemplate } from "./prompt-templates.js"; -export type { Skill } from "./skills.js"; -export type { Tool } from "./tools/index.js"; +} from "./extensions/index.ts"; +export type { PromptTemplate } from "./prompt-templates.ts"; +export type { Skill } from "./skills.ts"; +export type { Tool } from "./tools/index.ts"; export { withFileMutationQueue, diff --git a/packages/coding-agent/src/core/session-manager.ts b/packages/coding-agent/src/core/session-manager.ts index 0c6e2334..533916f3 100644 --- a/packages/coding-agent/src/core/session-manager.ts +++ b/packages/coding-agent/src/core/session-manager.ts @@ -15,14 +15,14 @@ import { } from "fs"; import { readdir, readFile, stat } from "fs/promises"; import { join, resolve } from "path"; -import { getAgentDir as getDefaultAgentDir, getSessionsDir } from "../config.js"; +import { getAgentDir as getDefaultAgentDir, getSessionsDir } from "../config.ts"; import { type BashExecutionMessage, type CustomMessage, createBranchSummaryMessage, createCompactionSummaryMessage, createCustomMessage, -} from "./messages.js"; +} from "./messages.ts"; export const CURRENT_SESSION_VERSION = 3; diff --git a/packages/coding-agent/src/core/settings-manager.ts b/packages/coding-agent/src/core/settings-manager.ts index 4db92056..7dc6931e 100644 --- a/packages/coding-agent/src/core/settings-manager.ts +++ b/packages/coding-agent/src/core/settings-manager.ts @@ -3,7 +3,7 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs"; import { homedir } from "os"; import { dirname, join } from "path"; import lockfile from "proper-lockfile"; -import { CONFIG_DIR_NAME, getAgentDir } from "../config.js"; +import { CONFIG_DIR_NAME, getAgentDir } from "../config.ts"; export interface CompactionSettings { enabled?: boolean; // default: true diff --git a/packages/coding-agent/src/core/skills.ts b/packages/coding-agent/src/core/skills.ts index 82dc9a63..675da3a7 100644 --- a/packages/coding-agent/src/core/skills.ts +++ b/packages/coding-agent/src/core/skills.ts @@ -2,11 +2,11 @@ import { existsSync, readdirSync, readFileSync, statSync } from "fs"; import ignore from "ignore"; import { homedir } from "os"; import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "path"; -import { CONFIG_DIR_NAME, getAgentDir } from "../config.js"; -import { parseFrontmatter } from "../utils/frontmatter.js"; -import { canonicalizePath } from "../utils/paths.js"; -import type { ResourceDiagnostic } from "./diagnostics.js"; -import { createSyntheticSourceInfo, type SourceInfo } from "./source-info.js"; +import { CONFIG_DIR_NAME, getAgentDir } from "../config.ts"; +import { parseFrontmatter } from "../utils/frontmatter.ts"; +import { canonicalizePath } from "../utils/paths.ts"; +import type { ResourceDiagnostic } from "./diagnostics.ts"; +import { createSyntheticSourceInfo, type SourceInfo } from "./source-info.ts"; /** Max name length per spec */ const MAX_NAME_LENGTH = 64; diff --git a/packages/coding-agent/src/core/slash-commands.ts b/packages/coding-agent/src/core/slash-commands.ts index ffff92bb..e52892c8 100644 --- a/packages/coding-agent/src/core/slash-commands.ts +++ b/packages/coding-agent/src/core/slash-commands.ts @@ -1,5 +1,5 @@ -import { APP_NAME } from "../config.js"; -import type { SourceInfo } from "./source-info.js"; +import { APP_NAME } from "../config.ts"; +import type { SourceInfo } from "./source-info.ts"; export type SlashCommandSource = "extension" | "prompt" | "skill"; diff --git a/packages/coding-agent/src/core/source-info.ts b/packages/coding-agent/src/core/source-info.ts index c75d3f94..c8c9837d 100644 --- a/packages/coding-agent/src/core/source-info.ts +++ b/packages/coding-agent/src/core/source-info.ts @@ -1,4 +1,4 @@ -import type { PathMetadata } from "./package-manager.js"; +import type { PathMetadata } from "./package-manager.ts"; export type SourceScope = "user" | "project" | "temporary"; export type SourceOrigin = "package" | "top-level"; diff --git a/packages/coding-agent/src/core/system-prompt.ts b/packages/coding-agent/src/core/system-prompt.ts index c6b1017d..8664d871 100644 --- a/packages/coding-agent/src/core/system-prompt.ts +++ b/packages/coding-agent/src/core/system-prompt.ts @@ -2,8 +2,8 @@ * System prompt construction and project context loading */ -import { getDocsPath, getExamplesPath, getReadmePath } from "../config.js"; -import { formatSkillsForPrompt, type Skill } from "./skills.js"; +import { getDocsPath, getExamplesPath, getReadmePath } from "../config.ts"; +import { formatSkillsForPrompt, type Skill } from "./skills.ts"; export interface BuildSystemPromptOptions { /** Custom system prompt (replaces default). */ diff --git a/packages/coding-agent/src/core/telemetry.ts b/packages/coding-agent/src/core/telemetry.ts index b563d34b..25111c2b 100644 --- a/packages/coding-agent/src/core/telemetry.ts +++ b/packages/coding-agent/src/core/telemetry.ts @@ -1,4 +1,4 @@ -import type { SettingsManager } from "./settings-manager.js"; +import type { SettingsManager } from "./settings-manager.ts"; function isTruthyEnvFlag(value: string | undefined): boolean { if (!value) return false; diff --git a/packages/coding-agent/src/core/tools/bash.ts b/packages/coding-agent/src/core/tools/bash.ts index 20728500..f70c9470 100644 --- a/packages/coding-agent/src/core/tools/bash.ts +++ b/packages/coding-agent/src/core/tools/bash.ts @@ -3,22 +3,22 @@ import type { AgentTool } from "@earendil-works/pi-agent-core"; import { Container, Text, truncateToWidth } from "@earendil-works/pi-tui"; import { spawn } from "child_process"; import { type Static, Type } from "typebox"; -import { keyHint } from "../../modes/interactive/components/keybinding-hints.js"; -import { truncateToVisualLines } from "../../modes/interactive/components/visual-truncate.js"; -import { theme } from "../../modes/interactive/theme/theme.js"; -import { waitForChildProcess } from "../../utils/child-process.js"; +import { keyHint } from "../../modes/interactive/components/keybinding-hints.ts"; +import { truncateToVisualLines } from "../../modes/interactive/components/visual-truncate.ts"; +import { theme } from "../../modes/interactive/theme/theme.ts"; +import { waitForChildProcess } from "../../utils/child-process.ts"; import { getShellConfig, getShellEnv, killProcessTree, trackDetachedChildPid, untrackDetachedChildPid, -} from "../../utils/shell.js"; -import type { ToolDefinition, ToolRenderResultOptions } from "../extensions/types.js"; -import { OutputAccumulator } from "./output-accumulator.js"; -import { getTextOutput, invalidArgText, str } from "./render-utils.js"; -import { wrapToolDefinition } from "./tool-definition-wrapper.js"; -import { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, formatSize, type TruncationResult } from "./truncate.js"; +} from "../../utils/shell.ts"; +import type { ToolDefinition, ToolRenderResultOptions } from "../extensions/types.ts"; +import { OutputAccumulator } from "./output-accumulator.ts"; +import { getTextOutput, invalidArgText, str } from "./render-utils.ts"; +import { wrapToolDefinition } from "./tool-definition-wrapper.ts"; +import { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, formatSize, type TruncationResult } from "./truncate.ts"; const bashSchema = Type.Object({ command: Type.String({ description: "Bash command to execute" }), diff --git a/packages/coding-agent/src/core/tools/edit-diff.ts b/packages/coding-agent/src/core/tools/edit-diff.ts index d8c4c003..691d882a 100644 --- a/packages/coding-agent/src/core/tools/edit-diff.ts +++ b/packages/coding-agent/src/core/tools/edit-diff.ts @@ -6,7 +6,7 @@ import * as Diff from "diff"; import { constants } from "fs"; import { access, readFile } from "fs/promises"; -import { resolveToCwd } from "./path-utils.js"; +import { resolveToCwd } from "./path-utils.ts"; export function detectLineEnding(content: string): "\r\n" | "\n" { const crlfIdx = content.indexOf("\r\n"); diff --git a/packages/coding-agent/src/core/tools/edit.ts b/packages/coding-agent/src/core/tools/edit.ts index 6a5f960f..e5656378 100644 --- a/packages/coding-agent/src/core/tools/edit.ts +++ b/packages/coding-agent/src/core/tools/edit.ts @@ -3,8 +3,8 @@ import { Box, Container, Spacer, Text } from "@earendil-works/pi-tui"; import { constants } from "fs"; import { access as fsAccess, readFile as fsReadFile, writeFile as fsWriteFile } from "fs/promises"; import { type Static, Type } from "typebox"; -import { renderDiff } from "../../modes/interactive/components/diff.js"; -import type { ToolDefinition } from "../extensions/types.js"; +import { renderDiff } from "../../modes/interactive/components/diff.ts"; +import type { ToolDefinition } from "../extensions/types.ts"; import { applyEditsToNormalizedContent, computeEditsDiff, @@ -16,11 +16,11 @@ import { normalizeToLF, restoreLineEndings, stripBom, -} from "./edit-diff.js"; -import { withFileMutationQueue } from "./file-mutation-queue.js"; -import { resolveToCwd } from "./path-utils.js"; -import { invalidArgText, shortenPath, str } from "./render-utils.js"; -import { wrapToolDefinition } from "./tool-definition-wrapper.js"; +} from "./edit-diff.ts"; +import { withFileMutationQueue } from "./file-mutation-queue.ts"; +import { resolveToCwd } from "./path-utils.ts"; +import { invalidArgText, shortenPath, str } from "./render-utils.ts"; +import { wrapToolDefinition } from "./tool-definition-wrapper.ts"; type EditPreview = EditDiffResult | EditDiffError; @@ -190,7 +190,7 @@ function getRenderablePreviewInput(args: RenderableEditArgs | undefined): { path function formatEditCall( args: RenderableEditArgs | undefined, - theme: typeof import("../../modes/interactive/theme/theme.js").theme, + theme: typeof import("../../modes/interactive/theme/theme.ts").theme, ): string { const invalidArg = invalidArgText(theme); const rawPath = str(args?.file_path ?? args?.path); @@ -203,7 +203,7 @@ function formatEditResult( args: RenderableEditArgs | undefined, preview: EditPreview | undefined, result: EditToolResultLike, - theme: typeof import("../../modes/interactive/theme/theme.js").theme, + theme: typeof import("../../modes/interactive/theme/theme.ts").theme, isError: boolean, ): string | undefined { const rawPath = str(args?.file_path ?? args?.path); @@ -231,7 +231,7 @@ function formatEditResult( function getEditHeaderBg( preview: EditPreview | undefined, settledError: boolean | undefined, - theme: typeof import("../../modes/interactive/theme/theme.js").theme, + theme: typeof import("../../modes/interactive/theme/theme.ts").theme, ): (text: string) => string { if (preview) { if ("error" in preview) { @@ -248,7 +248,7 @@ function getEditHeaderBg( function buildEditCallComponent( component: EditCallRenderComponent, args: RenderableEditArgs | undefined, - theme: typeof import("../../modes/interactive/theme/theme.js").theme, + theme: typeof import("../../modes/interactive/theme/theme.ts").theme, ): EditCallRenderComponent { component.setBgFn(getEditHeaderBg(component.preview, component.settledError, theme)); component.clear(); diff --git a/packages/coding-agent/src/core/tools/find.ts b/packages/coding-agent/src/core/tools/find.ts index e2d552d4..3fd2f683 100644 --- a/packages/coding-agent/src/core/tools/find.ts +++ b/packages/coding-agent/src/core/tools/find.ts @@ -5,13 +5,13 @@ import { spawn } from "child_process"; import { existsSync } from "fs"; import path from "path"; import { type Static, Type } from "typebox"; -import { keyHint } from "../../modes/interactive/components/keybinding-hints.js"; -import { ensureTool } from "../../utils/tools-manager.js"; -import type { ToolDefinition, ToolRenderResultOptions } from "../extensions/types.js"; -import { resolveToCwd } from "./path-utils.js"; -import { getTextOutput, invalidArgText, shortenPath, str } from "./render-utils.js"; -import { wrapToolDefinition } from "./tool-definition-wrapper.js"; -import { DEFAULT_MAX_BYTES, formatSize, type TruncationResult, truncateHead } from "./truncate.js"; +import { keyHint } from "../../modes/interactive/components/keybinding-hints.ts"; +import { ensureTool } from "../../utils/tools-manager.ts"; +import type { ToolDefinition, ToolRenderResultOptions } from "../extensions/types.ts"; +import { resolveToCwd } from "./path-utils.ts"; +import { getTextOutput, invalidArgText, shortenPath, str } from "./render-utils.ts"; +import { wrapToolDefinition } from "./tool-definition-wrapper.ts"; +import { DEFAULT_MAX_BYTES, formatSize, type TruncationResult, truncateHead } from "./truncate.ts"; function toPosixPath(value: string): string { return value.split(path.sep).join("/"); @@ -58,7 +58,7 @@ export interface FindToolOptions { function formatFindCall( args: { pattern: string; path?: string; limit?: number } | undefined, - theme: typeof import("../../modes/interactive/theme/theme.js").theme, + theme: typeof import("../../modes/interactive/theme/theme.ts").theme, ): string { const pattern = str(args?.pattern); const rawPath = str(args?.path); @@ -82,7 +82,7 @@ function formatFindResult( details?: FindToolDetails; }, options: ToolRenderResultOptions, - theme: typeof import("../../modes/interactive/theme/theme.js").theme, + theme: typeof import("../../modes/interactive/theme/theme.ts").theme, showImages: boolean, ): string { const output = getTextOutput(result, showImages).trim(); diff --git a/packages/coding-agent/src/core/tools/grep.ts b/packages/coding-agent/src/core/tools/grep.ts index 4d441b18..041b3148 100644 --- a/packages/coding-agent/src/core/tools/grep.ts +++ b/packages/coding-agent/src/core/tools/grep.ts @@ -5,12 +5,12 @@ import { spawn } from "child_process"; import { readFileSync, statSync } from "fs"; import path from "path"; import { type Static, Type } from "typebox"; -import { keyHint } from "../../modes/interactive/components/keybinding-hints.js"; -import { ensureTool } from "../../utils/tools-manager.js"; -import type { ToolDefinition, ToolRenderResultOptions } from "../extensions/types.js"; -import { resolveToCwd } from "./path-utils.js"; -import { getTextOutput, invalidArgText, shortenPath, str } from "./render-utils.js"; -import { wrapToolDefinition } from "./tool-definition-wrapper.js"; +import { keyHint } from "../../modes/interactive/components/keybinding-hints.ts"; +import { ensureTool } from "../../utils/tools-manager.ts"; +import type { ToolDefinition, ToolRenderResultOptions } from "../extensions/types.ts"; +import { resolveToCwd } from "./path-utils.ts"; +import { getTextOutput, invalidArgText, shortenPath, str } from "./render-utils.ts"; +import { wrapToolDefinition } from "./tool-definition-wrapper.ts"; import { DEFAULT_MAX_BYTES, formatSize, @@ -18,7 +18,7 @@ import { type TruncationResult, truncateHead, truncateLine, -} from "./truncate.js"; +} from "./truncate.ts"; const grepSchema = Type.Object({ pattern: Type.String({ description: "Search pattern (regex or literal string)" }), @@ -66,7 +66,7 @@ export interface GrepToolOptions { function formatGrepCall( args: { pattern: string; path?: string; glob?: string; limit?: number } | undefined, - theme: typeof import("../../modes/interactive/theme/theme.js").theme, + theme: typeof import("../../modes/interactive/theme/theme.ts").theme, ): string { const pattern = str(args?.pattern); const rawPath = str(args?.path); @@ -90,7 +90,7 @@ function formatGrepResult( details?: GrepToolDetails; }, options: ToolRenderResultOptions, - theme: typeof import("../../modes/interactive/theme/theme.js").theme, + theme: typeof import("../../modes/interactive/theme/theme.ts").theme, showImages: boolean, ): string { const output = getTextOutput(result, showImages).trim(); diff --git a/packages/coding-agent/src/core/tools/index.ts b/packages/coding-agent/src/core/tools/index.ts index 659b5484..e55f9140 100644 --- a/packages/coding-agent/src/core/tools/index.ts +++ b/packages/coding-agent/src/core/tools/index.ts @@ -8,7 +8,7 @@ export { createBashTool, createBashToolDefinition, createLocalBashOperations, -} from "./bash.js"; +} from "./bash.ts"; export { createEditTool, createEditToolDefinition, @@ -16,8 +16,8 @@ export { type EditToolDetails, type EditToolInput, type EditToolOptions, -} from "./edit.js"; -export { withFileMutationQueue } from "./file-mutation-queue.js"; +} from "./edit.ts"; +export { withFileMutationQueue } from "./file-mutation-queue.ts"; export { createFindTool, createFindToolDefinition, @@ -25,7 +25,7 @@ export { type FindToolDetails, type FindToolInput, type FindToolOptions, -} from "./find.js"; +} from "./find.ts"; export { createGrepTool, createGrepToolDefinition, @@ -33,7 +33,7 @@ export { type GrepToolDetails, type GrepToolInput, type GrepToolOptions, -} from "./grep.js"; +} from "./grep.ts"; export { createLsTool, createLsToolDefinition, @@ -41,7 +41,7 @@ export { type LsToolDetails, type LsToolInput, type LsToolOptions, -} from "./ls.js"; +} from "./ls.ts"; export { createReadTool, createReadToolDefinition, @@ -49,7 +49,7 @@ export { type ReadToolDetails, type ReadToolInput, type ReadToolOptions, -} from "./read.js"; +} from "./read.ts"; export { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, @@ -59,24 +59,24 @@ export { truncateHead, truncateLine, truncateTail, -} from "./truncate.js"; +} from "./truncate.ts"; export { createWriteTool, createWriteToolDefinition, type WriteOperations, type WriteToolInput, type WriteToolOptions, -} from "./write.js"; +} from "./write.ts"; import type { AgentTool } from "@earendil-works/pi-agent-core"; -import type { ToolDefinition } from "../extensions/types.js"; -import { type BashToolOptions, createBashTool, createBashToolDefinition } from "./bash.js"; -import { createEditTool, createEditToolDefinition, type EditToolOptions } from "./edit.js"; -import { createFindTool, createFindToolDefinition, type FindToolOptions } from "./find.js"; -import { createGrepTool, createGrepToolDefinition, type GrepToolOptions } from "./grep.js"; -import { createLsTool, createLsToolDefinition, type LsToolOptions } from "./ls.js"; -import { createReadTool, createReadToolDefinition, type ReadToolOptions } from "./read.js"; -import { createWriteTool, createWriteToolDefinition, type WriteToolOptions } from "./write.js"; +import type { ToolDefinition } from "../extensions/types.ts"; +import { type BashToolOptions, createBashTool, createBashToolDefinition } from "./bash.ts"; +import { createEditTool, createEditToolDefinition, type EditToolOptions } from "./edit.ts"; +import { createFindTool, createFindToolDefinition, type FindToolOptions } from "./find.ts"; +import { createGrepTool, createGrepToolDefinition, type GrepToolOptions } from "./grep.ts"; +import { createLsTool, createLsToolDefinition, type LsToolOptions } from "./ls.ts"; +import { createReadTool, createReadToolDefinition, type ReadToolOptions } from "./read.ts"; +import { createWriteTool, createWriteToolDefinition, type WriteToolOptions } from "./write.ts"; export type Tool = AgentTool; export type ToolDef = ToolDefinition; diff --git a/packages/coding-agent/src/core/tools/ls.ts b/packages/coding-agent/src/core/tools/ls.ts index e6ae22c7..f3d22c1d 100644 --- a/packages/coding-agent/src/core/tools/ls.ts +++ b/packages/coding-agent/src/core/tools/ls.ts @@ -3,12 +3,12 @@ import { Text } from "@earendil-works/pi-tui"; import { existsSync, readdirSync, statSync } from "fs"; import nodePath from "path"; import { type Static, Type } from "typebox"; -import { keyHint } from "../../modes/interactive/components/keybinding-hints.js"; -import type { ToolDefinition, ToolRenderResultOptions } from "../extensions/types.js"; -import { resolveToCwd } from "./path-utils.js"; -import { getTextOutput, invalidArgText, shortenPath, str } from "./render-utils.js"; -import { wrapToolDefinition } from "./tool-definition-wrapper.js"; -import { DEFAULT_MAX_BYTES, formatSize, type TruncationResult, truncateHead } from "./truncate.js"; +import { keyHint } from "../../modes/interactive/components/keybinding-hints.ts"; +import type { ToolDefinition, ToolRenderResultOptions } from "../extensions/types.ts"; +import { resolveToCwd } from "./path-utils.ts"; +import { getTextOutput, invalidArgText, shortenPath, str } from "./render-utils.ts"; +import { wrapToolDefinition } from "./tool-definition-wrapper.ts"; +import { DEFAULT_MAX_BYTES, formatSize, type TruncationResult, truncateHead } from "./truncate.ts"; const lsSchema = Type.Object({ path: Type.Optional(Type.String({ description: "Directory to list (default: current directory)" })), @@ -50,7 +50,7 @@ export interface LsToolOptions { function formatLsCall( args: { path?: string; limit?: number } | undefined, - theme: typeof import("../../modes/interactive/theme/theme.js").theme, + theme: typeof import("../../modes/interactive/theme/theme.ts").theme, ): string { const rawPath = str(args?.path); const path = rawPath !== null ? shortenPath(rawPath || ".") : null; @@ -69,7 +69,7 @@ function formatLsResult( details?: LsToolDetails; }, options: ToolRenderResultOptions, - theme: typeof import("../../modes/interactive/theme/theme.js").theme, + theme: typeof import("../../modes/interactive/theme/theme.ts").theme, showImages: boolean, ): string { const output = getTextOutput(result, showImages).trim(); diff --git a/packages/coding-agent/src/core/tools/output-accumulator.ts b/packages/coding-agent/src/core/tools/output-accumulator.ts index 30f4d663..78fcfe9e 100644 --- a/packages/coding-agent/src/core/tools/output-accumulator.ts +++ b/packages/coding-agent/src/core/tools/output-accumulator.ts @@ -2,7 +2,7 @@ import { randomBytes } from "node:crypto"; import { createWriteStream, type WriteStream } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; -import { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, type TruncationResult, truncateTail } from "./truncate.js"; +import { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, type TruncationResult, truncateTail } from "./truncate.ts"; export interface OutputAccumulatorOptions { maxLines?: number; diff --git a/packages/coding-agent/src/core/tools/read.ts b/packages/coding-agent/src/core/tools/read.ts index 867d04dc..cd3b41ed 100644 --- a/packages/coding-agent/src/core/tools/read.ts +++ b/packages/coding-agent/src/core/tools/read.ts @@ -5,17 +5,17 @@ import { Text } from "@earendil-works/pi-tui"; import { constants } from "fs"; import { access as fsAccess, readFile as fsReadFile } from "fs/promises"; import { type Static, Type } from "typebox"; -import { getReadmePath } from "../../config.js"; -import { keyHint, keyText } from "../../modes/interactive/components/keybinding-hints.js"; -import { getLanguageFromPath, highlightCode, type Theme } from "../../modes/interactive/theme/theme.js"; -import { formatDimensionNote, resizeImage } from "../../utils/image-resize.js"; -import { detectSupportedImageMimeTypeFromFile } from "../../utils/mime.js"; -import { formatPathRelativeToCwdOrAbsolute } from "../../utils/paths.js"; -import type { ToolDefinition, ToolRenderResultOptions } from "../extensions/types.js"; -import { resolveReadPath } from "./path-utils.js"; -import { getTextOutput, invalidArgText, replaceTabs, shortenPath, str } from "./render-utils.js"; -import { wrapToolDefinition } from "./tool-definition-wrapper.js"; -import { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, formatSize, type TruncationResult, truncateHead } from "./truncate.js"; +import { getReadmePath } from "../../config.ts"; +import { keyHint, keyText } from "../../modes/interactive/components/keybinding-hints.ts"; +import { getLanguageFromPath, highlightCode, type Theme } from "../../modes/interactive/theme/theme.ts"; +import { formatDimensionNote, resizeImage } from "../../utils/image-resize.ts"; +import { detectSupportedImageMimeTypeFromFile } from "../../utils/mime.ts"; +import { formatPathRelativeToCwdOrAbsolute } from "../../utils/paths.ts"; +import type { ToolDefinition, ToolRenderResultOptions } from "../extensions/types.ts"; +import { resolveReadPath } from "./path-utils.ts"; +import { getTextOutput, invalidArgText, replaceTabs, shortenPath, str } from "./render-utils.ts"; +import { wrapToolDefinition } from "./tool-definition-wrapper.ts"; +import { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, formatSize, type TruncationResult, truncateHead } from "./truncate.ts"; const readSchema = Type.Object({ path: Type.String({ description: "Path to the file to read (relative or absolute)" }), diff --git a/packages/coding-agent/src/core/tools/render-utils.ts b/packages/coding-agent/src/core/tools/render-utils.ts index 8c7a6fcd..1614ed64 100644 --- a/packages/coding-agent/src/core/tools/render-utils.ts +++ b/packages/coding-agent/src/core/tools/render-utils.ts @@ -1,8 +1,8 @@ import * as os from "node:os"; import type { ImageContent, TextContent } from "@earendil-works/pi-ai"; import { getCapabilities, getImageDimensions, imageFallback } from "@earendil-works/pi-tui"; -import { stripAnsi } from "../../utils/ansi.js"; -import { sanitizeBinaryOutput } from "../../utils/shell.js"; +import { stripAnsi } from "../../utils/ansi.ts"; +import { sanitizeBinaryOutput } from "../../utils/shell.ts"; export function shortenPath(path: unknown): string { if (typeof path !== "string") return ""; diff --git a/packages/coding-agent/src/core/tools/tool-definition-wrapper.ts b/packages/coding-agent/src/core/tools/tool-definition-wrapper.ts index 1aca3f30..c9da525a 100644 --- a/packages/coding-agent/src/core/tools/tool-definition-wrapper.ts +++ b/packages/coding-agent/src/core/tools/tool-definition-wrapper.ts @@ -1,5 +1,5 @@ import type { AgentTool } from "@earendil-works/pi-agent-core"; -import type { ExtensionContext, ToolDefinition } from "../extensions/types.js"; +import type { ExtensionContext, ToolDefinition } from "../extensions/types.ts"; /** Wrap a ToolDefinition into an AgentTool for the core runtime. */ export function wrapToolDefinition( diff --git a/packages/coding-agent/src/core/tools/write.ts b/packages/coding-agent/src/core/tools/write.ts index e1aa40aa..5c07848e 100644 --- a/packages/coding-agent/src/core/tools/write.ts +++ b/packages/coding-agent/src/core/tools/write.ts @@ -3,13 +3,13 @@ import { Container, Text } from "@earendil-works/pi-tui"; import { mkdir as fsMkdir, writeFile as fsWriteFile } from "fs/promises"; import { dirname } from "path"; import { type Static, Type } from "typebox"; -import { keyHint } from "../../modes/interactive/components/keybinding-hints.js"; -import { getLanguageFromPath, highlightCode } from "../../modes/interactive/theme/theme.js"; -import type { ToolDefinition, ToolRenderResultOptions } from "../extensions/types.js"; -import { withFileMutationQueue } from "./file-mutation-queue.js"; -import { resolveToCwd } from "./path-utils.js"; -import { invalidArgText, normalizeDisplayText, replaceTabs, shortenPath, str } from "./render-utils.js"; -import { wrapToolDefinition } from "./tool-definition-wrapper.js"; +import { keyHint } from "../../modes/interactive/components/keybinding-hints.ts"; +import { getLanguageFromPath, highlightCode } from "../../modes/interactive/theme/theme.ts"; +import type { ToolDefinition, ToolRenderResultOptions } from "../extensions/types.ts"; +import { withFileMutationQueue } from "./file-mutation-queue.ts"; +import { resolveToCwd } from "./path-utils.ts"; +import { invalidArgText, normalizeDisplayText, replaceTabs, shortenPath, str } from "./render-utils.ts"; +import { wrapToolDefinition } from "./tool-definition-wrapper.ts"; const writeSchema = Type.Object({ path: Type.String({ description: "Path to the file to write (relative or absolute)" }), @@ -131,7 +131,7 @@ function trimTrailingEmptyLines(lines: string[]): string[] { function formatWriteCall( args: { path?: string; file_path?: string; content?: string } | undefined, options: ToolRenderResultOptions, - theme: typeof import("../../modes/interactive/theme/theme.js").theme, + theme: typeof import("../../modes/interactive/theme/theme.ts").theme, cache: WriteHighlightCache | undefined, ): string { const rawPath = str(args?.file_path ?? args?.path); @@ -163,7 +163,7 @@ function formatWriteCall( function formatWriteResult( result: { content: Array<{ type: string; text?: string; data?: string; mimeType?: string }>; isError?: boolean }, - theme: typeof import("../../modes/interactive/theme/theme.js").theme, + theme: typeof import("../../modes/interactive/theme/theme.ts").theme, ): string | undefined { if (!result.isError) { return undefined; diff --git a/packages/coding-agent/src/index.ts b/packages/coding-agent/src/index.ts index 8001509f..4fe81824 100644 --- a/packages/coding-agent/src/index.ts +++ b/packages/coding-agent/src/index.ts @@ -1,7 +1,7 @@ // Core session management // Config paths -export { getAgentDir, VERSION } from "./config.js"; +export { getAgentDir, VERSION } from "./config.ts"; export { AgentSession, type AgentSessionConfig, @@ -12,7 +12,7 @@ export { type PromptOptions, parseSkillBlock, type SessionStats, -} from "./core/agent-session.js"; +} from "./core/agent-session.ts"; // Auth and model registry export { type ApiKeyCredential, @@ -23,7 +23,7 @@ export { FileAuthStorageBackend, InMemoryAuthStorageBackend, type OAuthCredential, -} from "./core/auth-storage.js"; +} from "./core/auth-storage.ts"; // Compaction export { type BranchPreparation, @@ -46,8 +46,8 @@ export { prepareBranchEntries, serializeConversation, shouldCompact, -} from "./core/compaction/index.js"; -export { createEventBus, type EventBus, type EventBusController } from "./core/event-bus.js"; +} from "./core/compaction/index.ts"; +export { createEventBus, type EventBus, type EventBusController } from "./core/event-bus.ts"; // Extension system export type { AgentEndEvent, @@ -128,7 +128,7 @@ export type { WidgetPlacement, WorkingIndicatorOptions, WriteToolCallEvent, -} from "./core/extensions/index.js"; +} from "./core/extensions/index.ts"; export { createExtensionRuntime, defineTool, @@ -144,11 +144,11 @@ export { isWriteToolResult, wrapRegisteredTool, wrapRegisteredTools, -} from "./core/extensions/index.js"; +} from "./core/extensions/index.ts"; // Footer data provider (git branch + extension statuses - data not otherwise available to extensions) -export type { ReadonlyFooterDataProvider } from "./core/footer-data-provider.js"; -export { convertToLlm } from "./core/messages.js"; -export { ModelRegistry } from "./core/model-registry.js"; +export type { ReadonlyFooterDataProvider } from "./core/footer-data-provider.ts"; +export { convertToLlm } from "./core/messages.ts"; +export { ModelRegistry } from "./core/model-registry.ts"; export type { PackageManager, PathMetadata, @@ -156,10 +156,10 @@ export type { ProgressEvent, ResolvedPaths, ResolvedResource, -} from "./core/package-manager.js"; -export { DefaultPackageManager } from "./core/package-manager.js"; -export type { ResourceCollision, ResourceDiagnostic, ResourceLoader } from "./core/resource-loader.js"; -export { DefaultResourceLoader, loadProjectContextFiles } from "./core/resource-loader.js"; +} from "./core/package-manager.ts"; +export { DefaultPackageManager } from "./core/package-manager.ts"; +export type { ResourceCollision, ResourceDiagnostic, ResourceLoader } from "./core/resource-loader.ts"; +export { DefaultResourceLoader, loadProjectContextFiles } from "./core/resource-loader.ts"; // SDK for programmatic usage export { AgentSessionRuntime, @@ -187,7 +187,7 @@ export { createReadTool, createWriteTool, type PromptTemplate, -} from "./core/sdk.js"; +} from "./core/sdk.ts"; export { type BranchSummaryEntry, buildSessionContext, @@ -210,14 +210,14 @@ export { SessionManager, type SessionMessageEntry, type ThinkingLevelChangeEntry, -} from "./core/session-manager.js"; +} from "./core/session-manager.ts"; export { type CompactionSettings, type ImageSettings, type PackageSource, type RetrySettings, SettingsManager, -} from "./core/settings-manager.js"; +} from "./core/settings-manager.ts"; // Skills export { formatSkillsForPrompt, @@ -227,8 +227,8 @@ export { loadSkillsFromDir, type Skill, type SkillFrontmatter, -} from "./core/skills.js"; -export { createSyntheticSourceInfo } from "./core/source-info.js"; +} from "./core/skills.ts"; +export { createSyntheticSourceInfo } from "./core/source-info.ts"; // Tools export { type BashOperations, @@ -278,9 +278,9 @@ export { type WriteToolInput, type WriteToolOptions, withFileMutationQueue, -} from "./core/tools/index.js"; +} from "./core/tools/index.ts"; // Main entry point -export { type MainOptions, main } from "./main.js"; +export { type MainOptions, main } from "./main.ts"; // Run modes for programmatic SDK usage export { InteractiveMode, @@ -295,7 +295,7 @@ export { type RpcSessionState, runPrintMode, runRpcMode, -} from "./modes/index.js"; +} from "./modes/index.ts"; // UI components for extensions export { ArminComponent, @@ -334,7 +334,7 @@ export { UserMessageComponent, UserMessageSelectorComponent, type VisualTruncateResult, -} from "./modes/interactive/components/index.js"; +} from "./modes/interactive/components/index.ts"; // Theme utilities for custom tools and extensions export { getLanguageFromPath, @@ -345,9 +345,9 @@ export { initTheme, Theme, type ThemeColor, -} from "./modes/interactive/theme/theme.js"; +} from "./modes/interactive/theme/theme.ts"; // Clipboard utilities -export { copyToClipboard } from "./utils/clipboard.js"; -export { parseFrontmatter, stripFrontmatter } from "./utils/frontmatter.js"; +export { copyToClipboard } from "./utils/clipboard.ts"; +export { parseFrontmatter, stripFrontmatter } from "./utils/frontmatter.ts"; // Shell utilities -export { getShellConfig } from "./utils/shell.js"; +export { getShellConfig } from "./utils/shell.ts"; diff --git a/packages/coding-agent/src/main.ts b/packages/coding-agent/src/main.ts index 9f1af697..141a34e7 100644 --- a/packages/coding-agent/src/main.ts +++ b/packages/coding-agent/src/main.ts @@ -10,43 +10,43 @@ import { createInterface } from "node:readline"; import { type ImageContent, modelsAreEqual } from "@earendil-works/pi-ai"; import { ProcessTerminal, setKeybindings, TUI } from "@earendil-works/pi-tui"; import chalk from "chalk"; -import { type Args, type Mode, parseArgs, printHelp } from "./cli/args.js"; -import { processFileArguments } from "./cli/file-processor.js"; -import { buildInitialMessage } from "./cli/initial-message.js"; -import { listModels } from "./cli/list-models.js"; -import { selectSession } from "./cli/session-picker.js"; -import { ENV_SESSION_DIR, expandTildePath, getAgentDir, getPackageDir, VERSION } from "./config.js"; -import { type CreateAgentSessionRuntimeFactory, createAgentSessionRuntime } from "./core/agent-session-runtime.js"; +import { type Args, type Mode, parseArgs, printHelp } from "./cli/args.ts"; +import { processFileArguments } from "./cli/file-processor.ts"; +import { buildInitialMessage } from "./cli/initial-message.ts"; +import { listModels } from "./cli/list-models.ts"; +import { selectSession } from "./cli/session-picker.ts"; +import { ENV_SESSION_DIR, expandTildePath, getAgentDir, getPackageDir, VERSION } from "./config.ts"; +import { type CreateAgentSessionRuntimeFactory, createAgentSessionRuntime } from "./core/agent-session-runtime.ts"; import { type AgentSessionRuntimeDiagnostic, createAgentSessionFromServices, createAgentSessionServices, -} from "./core/agent-session-services.js"; -import { formatNoModelsAvailableMessage } from "./core/auth-guidance.js"; -import { AuthStorage } from "./core/auth-storage.js"; -import { exportFromFile } from "./core/export-html/index.js"; -import type { ExtensionFactory } from "./core/extensions/types.js"; -import { KeybindingsManager } from "./core/keybindings.js"; -import type { ModelRegistry } from "./core/model-registry.js"; -import { resolveCliModel, resolveModelScope, type ScopedModel } from "./core/model-resolver.js"; -import { restoreStdout, takeOverStdout } from "./core/output-guard.js"; -import type { CreateAgentSessionOptions } from "./core/sdk.js"; +} from "./core/agent-session-services.ts"; +import { formatNoModelsAvailableMessage } from "./core/auth-guidance.ts"; +import { AuthStorage } from "./core/auth-storage.ts"; +import { exportFromFile } from "./core/export-html/index.ts"; +import type { ExtensionFactory } from "./core/extensions/types.ts"; +import { KeybindingsManager } from "./core/keybindings.ts"; +import type { ModelRegistry } from "./core/model-registry.ts"; +import { resolveCliModel, resolveModelScope, type ScopedModel } from "./core/model-resolver.ts"; +import { restoreStdout, takeOverStdout } from "./core/output-guard.ts"; +import type { CreateAgentSessionOptions } from "./core/sdk.ts"; import { formatMissingSessionCwdPrompt, getMissingSessionCwdIssue, MissingSessionCwdError, type SessionCwdIssue, -} from "./core/session-cwd.js"; -import { SessionManager } from "./core/session-manager.js"; -import { SettingsManager } from "./core/settings-manager.js"; -import { printTimings, resetTimings, time } from "./core/timings.js"; -import { runMigrations, showDeprecationWarnings } from "./migrations.js"; -import { InteractiveMode, runPrintMode, runRpcMode } from "./modes/index.js"; -import { ExtensionSelectorComponent } from "./modes/interactive/components/extension-selector.js"; -import { initTheme, stopThemeWatcher } from "./modes/interactive/theme/theme.js"; -import { handleConfigCommand, handlePackageCommand } from "./package-manager-cli.js"; -import { isLocalPath } from "./utils/paths.js"; -import { cleanupWindowsSelfUpdateQuarantine } from "./utils/windows-self-update.js"; +} from "./core/session-cwd.ts"; +import { SessionManager } from "./core/session-manager.ts"; +import { SettingsManager } from "./core/settings-manager.ts"; +import { printTimings, resetTimings, time } from "./core/timings.ts"; +import { runMigrations, showDeprecationWarnings } from "./migrations.ts"; +import { InteractiveMode, runPrintMode, runRpcMode } from "./modes/index.ts"; +import { ExtensionSelectorComponent } from "./modes/interactive/components/extension-selector.ts"; +import { initTheme, stopThemeWatcher } from "./modes/interactive/theme/theme.ts"; +import { handleConfigCommand, handlePackageCommand } from "./package-manager-cli.ts"; +import { isLocalPath } from "./utils/paths.ts"; +import { cleanupWindowsSelfUpdateQuarantine } from "./utils/windows-self-update.ts"; /** * Read all content from piped stdin. diff --git a/packages/coding-agent/src/migrations.ts b/packages/coding-agent/src/migrations.ts index 42851d08..39aeea04 100644 --- a/packages/coding-agent/src/migrations.ts +++ b/packages/coding-agent/src/migrations.ts @@ -5,8 +5,8 @@ import chalk from "chalk"; import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "fs"; import { dirname, join } from "path"; -import { CONFIG_DIR_NAME, getAgentDir, getBinDir } from "./config.js"; -import { migrateKeybindingsConfig } from "./core/keybindings.js"; +import { CONFIG_DIR_NAME, getAgentDir, getBinDir } from "./config.ts"; +import { migrateKeybindingsConfig } from "./core/keybindings.ts"; const MIGRATION_GUIDE_URL = "https://github.com/earendil-works/pi-mono/blob/main/packages/coding-agent/CHANGELOG.md#extensions-migration"; diff --git a/packages/coding-agent/src/modes/index.ts b/packages/coding-agent/src/modes/index.ts index 205e9f54..02cab782 100644 --- a/packages/coding-agent/src/modes/index.ts +++ b/packages/coding-agent/src/modes/index.ts @@ -2,8 +2,8 @@ * Run modes for the coding agent. */ -export { InteractiveMode, type InteractiveModeOptions } from "./interactive/interactive-mode.js"; -export { type PrintModeOptions, runPrintMode } from "./print-mode.js"; -export { type ModelInfo, RpcClient, type RpcClientOptions, type RpcEventListener } from "./rpc/rpc-client.js"; -export { runRpcMode } from "./rpc/rpc-mode.js"; -export type { RpcCommand, RpcResponse, RpcSessionState } from "./rpc/rpc-types.js"; +export { InteractiveMode, type InteractiveModeOptions } from "./interactive/interactive-mode.ts"; +export { type PrintModeOptions, runPrintMode } from "./print-mode.ts"; +export { type ModelInfo, RpcClient, type RpcClientOptions, type RpcEventListener } from "./rpc/rpc-client.ts"; +export { runRpcMode } from "./rpc/rpc-mode.ts"; +export type { RpcCommand, RpcResponse, RpcSessionState } from "./rpc/rpc-types.ts"; diff --git a/packages/coding-agent/src/modes/interactive/components/armin.ts b/packages/coding-agent/src/modes/interactive/components/armin.ts index 794e5562..209ca167 100644 --- a/packages/coding-agent/src/modes/interactive/components/armin.ts +++ b/packages/coding-agent/src/modes/interactive/components/armin.ts @@ -3,7 +3,7 @@ */ import type { Component, TUI } from "@earendil-works/pi-tui"; -import { theme } from "../theme/theme.js"; +import { theme } from "../theme/theme.ts"; // XBM image: 31x36 pixels, LSB first, 1=background, 0=foreground const WIDTH = 31; diff --git a/packages/coding-agent/src/modes/interactive/components/assistant-message.ts b/packages/coding-agent/src/modes/interactive/components/assistant-message.ts index 7eb7e306..32276467 100644 --- a/packages/coding-agent/src/modes/interactive/components/assistant-message.ts +++ b/packages/coding-agent/src/modes/interactive/components/assistant-message.ts @@ -1,6 +1,6 @@ import type { AssistantMessage } from "@earendil-works/pi-ai"; import { Container, Markdown, type MarkdownTheme, Spacer, Text } from "@earendil-works/pi-tui"; -import { getMarkdownTheme, theme } from "../theme/theme.js"; +import { getMarkdownTheme, theme } from "../theme/theme.ts"; const OSC133_ZONE_START = "\x1b]133;A\x07"; const OSC133_ZONE_END = "\x1b]133;B\x07"; diff --git a/packages/coding-agent/src/modes/interactive/components/bash-execution.ts b/packages/coding-agent/src/modes/interactive/components/bash-execution.ts index 240cd12d..e53f5a5e 100644 --- a/packages/coding-agent/src/modes/interactive/components/bash-execution.ts +++ b/packages/coding-agent/src/modes/interactive/components/bash-execution.ts @@ -8,12 +8,12 @@ import { DEFAULT_MAX_LINES, type TruncationResult, truncateTail, -} from "../../../core/tools/truncate.js"; -import { stripAnsi } from "../../../utils/ansi.js"; -import { theme } from "../theme/theme.js"; -import { DynamicBorder } from "./dynamic-border.js"; -import { keyHint, keyText } from "./keybinding-hints.js"; -import { truncateToVisualLines } from "./visual-truncate.js"; +} from "../../../core/tools/truncate.ts"; +import { stripAnsi } from "../../../utils/ansi.ts"; +import { theme } from "../theme/theme.ts"; +import { DynamicBorder } from "./dynamic-border.ts"; +import { keyHint, keyText } from "./keybinding-hints.ts"; +import { truncateToVisualLines } from "./visual-truncate.ts"; // Preview line limit when not expanded (matches tool execution behavior) const PREVIEW_LINES = 20; diff --git a/packages/coding-agent/src/modes/interactive/components/bordered-loader.ts b/packages/coding-agent/src/modes/interactive/components/bordered-loader.ts index e9b13919..4d692ffc 100644 --- a/packages/coding-agent/src/modes/interactive/components/bordered-loader.ts +++ b/packages/coding-agent/src/modes/interactive/components/bordered-loader.ts @@ -1,7 +1,7 @@ import { CancellableLoader, Container, Loader, Spacer, Text, type TUI } from "@earendil-works/pi-tui"; -import type { Theme } from "../theme/theme.js"; -import { DynamicBorder } from "./dynamic-border.js"; -import { keyHint } from "./keybinding-hints.js"; +import type { Theme } from "../theme/theme.ts"; +import { DynamicBorder } from "./dynamic-border.ts"; +import { keyHint } from "./keybinding-hints.ts"; /** Loader wrapped with borders for extension UI */ export class BorderedLoader extends Container { diff --git a/packages/coding-agent/src/modes/interactive/components/branch-summary-message.ts b/packages/coding-agent/src/modes/interactive/components/branch-summary-message.ts index b140f496..af85115f 100644 --- a/packages/coding-agent/src/modes/interactive/components/branch-summary-message.ts +++ b/packages/coding-agent/src/modes/interactive/components/branch-summary-message.ts @@ -1,7 +1,7 @@ import { Box, Markdown, type MarkdownTheme, Spacer, Text } from "@earendil-works/pi-tui"; -import type { BranchSummaryMessage } from "../../../core/messages.js"; -import { getMarkdownTheme, theme } from "../theme/theme.js"; -import { keyText } from "./keybinding-hints.js"; +import type { BranchSummaryMessage } from "../../../core/messages.ts"; +import { getMarkdownTheme, theme } from "../theme/theme.ts"; +import { keyText } from "./keybinding-hints.ts"; /** * Component that renders a branch summary message with collapsed/expanded state. diff --git a/packages/coding-agent/src/modes/interactive/components/compaction-summary-message.ts b/packages/coding-agent/src/modes/interactive/components/compaction-summary-message.ts index e4b78f88..cad613d7 100644 --- a/packages/coding-agent/src/modes/interactive/components/compaction-summary-message.ts +++ b/packages/coding-agent/src/modes/interactive/components/compaction-summary-message.ts @@ -1,7 +1,7 @@ import { Box, Markdown, type MarkdownTheme, Spacer, Text } from "@earendil-works/pi-tui"; -import type { CompactionSummaryMessage } from "../../../core/messages.js"; -import { getMarkdownTheme, theme } from "../theme/theme.js"; -import { keyText } from "./keybinding-hints.js"; +import type { CompactionSummaryMessage } from "../../../core/messages.ts"; +import { getMarkdownTheme, theme } from "../theme/theme.ts"; +import { keyText } from "./keybinding-hints.ts"; /** * Component that renders a compaction message with collapsed/expanded state. diff --git a/packages/coding-agent/src/modes/interactive/components/config-selector.ts b/packages/coding-agent/src/modes/interactive/components/config-selector.ts index 21628307..d4f16726 100644 --- a/packages/coding-agent/src/modes/interactive/components/config-selector.ts +++ b/packages/coding-agent/src/modes/interactive/components/config-selector.ts @@ -15,12 +15,12 @@ import { truncateToWidth, visibleWidth, } from "@earendil-works/pi-tui"; -import { CONFIG_DIR_NAME } from "../../../config.js"; -import type { PathMetadata, ResolvedPaths, ResolvedResource } from "../../../core/package-manager.js"; -import type { PackageSource, SettingsManager } from "../../../core/settings-manager.js"; -import { theme } from "../theme/theme.js"; -import { DynamicBorder } from "./dynamic-border.js"; -import { rawKeyHint } from "./keybinding-hints.js"; +import { CONFIG_DIR_NAME } from "../../../config.ts"; +import type { PathMetadata, ResolvedPaths, ResolvedResource } from "../../../core/package-manager.ts"; +import type { PackageSource, SettingsManager } from "../../../core/settings-manager.ts"; +import { theme } from "../theme/theme.ts"; +import { DynamicBorder } from "./dynamic-border.ts"; +import { rawKeyHint } from "./keybinding-hints.ts"; type ResourceType = "extensions" | "skills" | "prompts" | "themes"; diff --git a/packages/coding-agent/src/modes/interactive/components/custom-editor.ts b/packages/coding-agent/src/modes/interactive/components/custom-editor.ts index 18f7d27a..a9d25622 100644 --- a/packages/coding-agent/src/modes/interactive/components/custom-editor.ts +++ b/packages/coding-agent/src/modes/interactive/components/custom-editor.ts @@ -1,5 +1,5 @@ import { Editor, type EditorOptions, type EditorTheme, type TUI } from "@earendil-works/pi-tui"; -import type { AppKeybinding, KeybindingsManager } from "../../../core/keybindings.js"; +import type { AppKeybinding, KeybindingsManager } from "../../../core/keybindings.ts"; /** * Custom editor that handles app-level keybindings for coding-agent. diff --git a/packages/coding-agent/src/modes/interactive/components/custom-message.ts b/packages/coding-agent/src/modes/interactive/components/custom-message.ts index 5cf6bbe2..a8fffc6f 100644 --- a/packages/coding-agent/src/modes/interactive/components/custom-message.ts +++ b/packages/coding-agent/src/modes/interactive/components/custom-message.ts @@ -1,9 +1,9 @@ import type { TextContent } from "@earendil-works/pi-ai"; import type { Component } from "@earendil-works/pi-tui"; import { Box, Container, Markdown, type MarkdownTheme, Spacer, Text } from "@earendil-works/pi-tui"; -import type { MessageRenderer } from "../../../core/extensions/types.js"; -import type { CustomMessage } from "../../../core/messages.js"; -import { getMarkdownTheme, theme } from "../theme/theme.js"; +import type { MessageRenderer } from "../../../core/extensions/types.ts"; +import type { CustomMessage } from "../../../core/messages.ts"; +import { getMarkdownTheme, theme } from "../theme/theme.ts"; /** * Component that renders a custom message entry from extensions. diff --git a/packages/coding-agent/src/modes/interactive/components/daxnuts.ts b/packages/coding-agent/src/modes/interactive/components/daxnuts.ts index bfc4dbaa..c855f146 100644 --- a/packages/coding-agent/src/modes/interactive/components/daxnuts.ts +++ b/packages/coding-agent/src/modes/interactive/components/daxnuts.ts @@ -5,7 +5,7 @@ */ import type { Component, TUI } from "@earendil-works/pi-tui"; -import { theme } from "../theme/theme.js"; +import { theme } from "../theme/theme.ts"; // 32x32 RGB image of dax, hex encoded (3 bytes per pixel) const DAX_HEX = diff --git a/packages/coding-agent/src/modes/interactive/components/diff.ts b/packages/coding-agent/src/modes/interactive/components/diff.ts index d575d63e..54e88273 100644 --- a/packages/coding-agent/src/modes/interactive/components/diff.ts +++ b/packages/coding-agent/src/modes/interactive/components/diff.ts @@ -1,5 +1,5 @@ import * as Diff from "diff"; -import { theme } from "../theme/theme.js"; +import { theme } from "../theme/theme.ts"; /** * Parse diff line to extract prefix, line number, and content. diff --git a/packages/coding-agent/src/modes/interactive/components/dynamic-border.ts b/packages/coding-agent/src/modes/interactive/components/dynamic-border.ts index d4fa67c4..77342b25 100644 --- a/packages/coding-agent/src/modes/interactive/components/dynamic-border.ts +++ b/packages/coding-agent/src/modes/interactive/components/dynamic-border.ts @@ -1,5 +1,5 @@ import type { Component } from "@earendil-works/pi-tui"; -import { theme } from "../theme/theme.js"; +import { theme } from "../theme/theme.ts"; /** * Dynamic border component that adjusts to viewport width. diff --git a/packages/coding-agent/src/modes/interactive/components/earendil-announcement.ts b/packages/coding-agent/src/modes/interactive/components/earendil-announcement.ts index 59ec78f7..f2726d98 100644 --- a/packages/coding-agent/src/modes/interactive/components/earendil-announcement.ts +++ b/packages/coding-agent/src/modes/interactive/components/earendil-announcement.ts @@ -1,8 +1,8 @@ import * as fs from "node:fs"; import { Container, Image, Spacer, Text } from "@earendil-works/pi-tui"; -import { getBundledInteractiveAssetPath } from "../../../config.js"; -import { theme } from "../theme/theme.js"; -import { DynamicBorder } from "./dynamic-border.js"; +import { getBundledInteractiveAssetPath } from "../../../config.ts"; +import { theme } from "../theme/theme.ts"; +import { DynamicBorder } from "./dynamic-border.ts"; const BLOG_URL = "https://mariozechner.at/posts/2026-04-08-ive-sold-out/"; const IMAGE_FILENAME = "clankolas.png"; diff --git a/packages/coding-agent/src/modes/interactive/components/extension-editor.ts b/packages/coding-agent/src/modes/interactive/components/extension-editor.ts index b33ad9f5..e000fff4 100644 --- a/packages/coding-agent/src/modes/interactive/components/extension-editor.ts +++ b/packages/coding-agent/src/modes/interactive/components/extension-editor.ts @@ -17,10 +17,10 @@ import { Text, type TUI, } from "@earendil-works/pi-tui"; -import type { KeybindingsManager } from "../../../core/keybindings.js"; -import { getEditorTheme, theme } from "../theme/theme.js"; -import { DynamicBorder } from "./dynamic-border.js"; -import { keyHint } from "./keybinding-hints.js"; +import type { KeybindingsManager } from "../../../core/keybindings.ts"; +import { getEditorTheme, theme } from "../theme/theme.ts"; +import { DynamicBorder } from "./dynamic-border.ts"; +import { keyHint } from "./keybinding-hints.ts"; export class ExtensionEditorComponent extends Container implements Focusable { private editor: Editor; diff --git a/packages/coding-agent/src/modes/interactive/components/extension-input.ts b/packages/coding-agent/src/modes/interactive/components/extension-input.ts index 39f44f0f..581ef4f5 100644 --- a/packages/coding-agent/src/modes/interactive/components/extension-input.ts +++ b/packages/coding-agent/src/modes/interactive/components/extension-input.ts @@ -3,10 +3,10 @@ */ import { Container, type Focusable, getKeybindings, Input, Spacer, Text, type TUI } from "@earendil-works/pi-tui"; -import { theme } from "../theme/theme.js"; -import { CountdownTimer } from "./countdown-timer.js"; -import { DynamicBorder } from "./dynamic-border.js"; -import { keyHint } from "./keybinding-hints.js"; +import { theme } from "../theme/theme.ts"; +import { CountdownTimer } from "./countdown-timer.ts"; +import { DynamicBorder } from "./dynamic-border.ts"; +import { keyHint } from "./keybinding-hints.ts"; export interface ExtensionInputOptions { tui?: TUI; diff --git a/packages/coding-agent/src/modes/interactive/components/extension-selector.ts b/packages/coding-agent/src/modes/interactive/components/extension-selector.ts index 8b59c2f2..d1dbdb4d 100644 --- a/packages/coding-agent/src/modes/interactive/components/extension-selector.ts +++ b/packages/coding-agent/src/modes/interactive/components/extension-selector.ts @@ -4,10 +4,10 @@ */ import { Container, getKeybindings, Spacer, Text, type TUI } from "@earendil-works/pi-tui"; -import { theme } from "../theme/theme.js"; -import { CountdownTimer } from "./countdown-timer.js"; -import { DynamicBorder } from "./dynamic-border.js"; -import { keyHint, rawKeyHint } from "./keybinding-hints.js"; +import { theme } from "../theme/theme.ts"; +import { CountdownTimer } from "./countdown-timer.ts"; +import { DynamicBorder } from "./dynamic-border.ts"; +import { keyHint, rawKeyHint } from "./keybinding-hints.ts"; export interface ExtensionSelectorOptions { tui?: TUI; diff --git a/packages/coding-agent/src/modes/interactive/components/footer.ts b/packages/coding-agent/src/modes/interactive/components/footer.ts index 7e8109a5..283c1c0a 100644 --- a/packages/coding-agent/src/modes/interactive/components/footer.ts +++ b/packages/coding-agent/src/modes/interactive/components/footer.ts @@ -1,7 +1,7 @@ import { type Component, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui"; -import type { AgentSession } from "../../../core/agent-session.js"; -import type { ReadonlyFooterDataProvider } from "../../../core/footer-data-provider.js"; -import { theme } from "../theme/theme.js"; +import type { AgentSession } from "../../../core/agent-session.ts"; +import type { ReadonlyFooterDataProvider } from "../../../core/footer-data-provider.ts"; +import { theme } from "../theme/theme.ts"; /** * Sanitize text for display in a single-line status. diff --git a/packages/coding-agent/src/modes/interactive/components/index.ts b/packages/coding-agent/src/modes/interactive/components/index.ts index 242d9667..d9b5d773 100644 --- a/packages/coding-agent/src/modes/interactive/components/index.ts +++ b/packages/coding-agent/src/modes/interactive/components/index.ts @@ -1,32 +1,32 @@ // UI Components for extensions -export { ArminComponent } from "./armin.js"; -export { AssistantMessageComponent } from "./assistant-message.js"; -export { BashExecutionComponent } from "./bash-execution.js"; -export { BorderedLoader } from "./bordered-loader.js"; -export { BranchSummaryMessageComponent } from "./branch-summary-message.js"; -export { CompactionSummaryMessageComponent } from "./compaction-summary-message.js"; -export { CustomEditor } from "./custom-editor.js"; -export { CustomMessageComponent } from "./custom-message.js"; -export { DaxnutsComponent } from "./daxnuts.js"; -export { type RenderDiffOptions, renderDiff } from "./diff.js"; -export { DynamicBorder } from "./dynamic-border.js"; -export { ExtensionEditorComponent } from "./extension-editor.js"; -export { ExtensionInputComponent } from "./extension-input.js"; -export { ExtensionSelectorComponent } from "./extension-selector.js"; -export { FooterComponent } from "./footer.js"; -export { keyHint, keyText, rawKeyHint } from "./keybinding-hints.js"; -export { LoginDialogComponent } from "./login-dialog.js"; -export { ModelSelectorComponent } from "./model-selector.js"; -export { OAuthSelectorComponent } from "./oauth-selector.js"; -export { type ModelsCallbacks, type ModelsConfig, ScopedModelsSelectorComponent } from "./scoped-models-selector.js"; -export { SessionSelectorComponent } from "./session-selector.js"; -export { type SettingsCallbacks, type SettingsConfig, SettingsSelectorComponent } from "./settings-selector.js"; -export { ShowImagesSelectorComponent } from "./show-images-selector.js"; -export { SkillInvocationMessageComponent } from "./skill-invocation-message.js"; -export { ThemeSelectorComponent } from "./theme-selector.js"; -export { ThinkingSelectorComponent } from "./thinking-selector.js"; -export { ToolExecutionComponent, type ToolExecutionOptions } from "./tool-execution.js"; -export { TreeSelectorComponent } from "./tree-selector.js"; -export { UserMessageComponent } from "./user-message.js"; -export { UserMessageSelectorComponent } from "./user-message-selector.js"; -export { truncateToVisualLines, type VisualTruncateResult } from "./visual-truncate.js"; +export { ArminComponent } from "./armin.ts"; +export { AssistantMessageComponent } from "./assistant-message.ts"; +export { BashExecutionComponent } from "./bash-execution.ts"; +export { BorderedLoader } from "./bordered-loader.ts"; +export { BranchSummaryMessageComponent } from "./branch-summary-message.ts"; +export { CompactionSummaryMessageComponent } from "./compaction-summary-message.ts"; +export { CustomEditor } from "./custom-editor.ts"; +export { CustomMessageComponent } from "./custom-message.ts"; +export { DaxnutsComponent } from "./daxnuts.ts"; +export { type RenderDiffOptions, renderDiff } from "./diff.ts"; +export { DynamicBorder } from "./dynamic-border.ts"; +export { ExtensionEditorComponent } from "./extension-editor.ts"; +export { ExtensionInputComponent } from "./extension-input.ts"; +export { ExtensionSelectorComponent } from "./extension-selector.ts"; +export { FooterComponent } from "./footer.ts"; +export { keyHint, keyText, rawKeyHint } from "./keybinding-hints.ts"; +export { LoginDialogComponent } from "./login-dialog.ts"; +export { ModelSelectorComponent } from "./model-selector.ts"; +export { OAuthSelectorComponent } from "./oauth-selector.ts"; +export { type ModelsCallbacks, type ModelsConfig, ScopedModelsSelectorComponent } from "./scoped-models-selector.ts"; +export { SessionSelectorComponent } from "./session-selector.ts"; +export { type SettingsCallbacks, type SettingsConfig, SettingsSelectorComponent } from "./settings-selector.ts"; +export { ShowImagesSelectorComponent } from "./show-images-selector.ts"; +export { SkillInvocationMessageComponent } from "./skill-invocation-message.ts"; +export { ThemeSelectorComponent } from "./theme-selector.ts"; +export { ThinkingSelectorComponent } from "./thinking-selector.ts"; +export { ToolExecutionComponent, type ToolExecutionOptions } from "./tool-execution.ts"; +export { TreeSelectorComponent } from "./tree-selector.ts"; +export { UserMessageComponent } from "./user-message.ts"; +export { UserMessageSelectorComponent } from "./user-message-selector.ts"; +export { truncateToVisualLines, type VisualTruncateResult } from "./visual-truncate.ts"; diff --git a/packages/coding-agent/src/modes/interactive/components/keybinding-hints.ts b/packages/coding-agent/src/modes/interactive/components/keybinding-hints.ts index bb018c18..22601abd 100644 --- a/packages/coding-agent/src/modes/interactive/components/keybinding-hints.ts +++ b/packages/coding-agent/src/modes/interactive/components/keybinding-hints.ts @@ -3,7 +3,7 @@ */ import { getKeybindings, type Keybinding, type KeyId } from "@earendil-works/pi-tui"; -import { theme } from "../theme/theme.js"; +import { theme } from "../theme/theme.ts"; export interface KeyTextFormatOptions { capitalize?: boolean; diff --git a/packages/coding-agent/src/modes/interactive/components/login-dialog.ts b/packages/coding-agent/src/modes/interactive/components/login-dialog.ts index a9a6f2b0..80560f14 100644 --- a/packages/coding-agent/src/modes/interactive/components/login-dialog.ts +++ b/packages/coding-agent/src/modes/interactive/components/login-dialog.ts @@ -1,9 +1,9 @@ import { getOAuthProviders } from "@earendil-works/pi-ai/oauth"; import { Container, type Focusable, getKeybindings, Input, Spacer, Text, type TUI } from "@earendil-works/pi-tui"; import { exec } from "child_process"; -import { theme } from "../theme/theme.js"; -import { DynamicBorder } from "./dynamic-border.js"; -import { keyHint } from "./keybinding-hints.js"; +import { theme } from "../theme/theme.ts"; +import { DynamicBorder } from "./dynamic-border.ts"; +import { keyHint } from "./keybinding-hints.ts"; /** * Login dialog component - replaces editor during OAuth login flow diff --git a/packages/coding-agent/src/modes/interactive/components/model-selector.ts b/packages/coding-agent/src/modes/interactive/components/model-selector.ts index 613ca9e1..b9f5ec77 100644 --- a/packages/coding-agent/src/modes/interactive/components/model-selector.ts +++ b/packages/coding-agent/src/modes/interactive/components/model-selector.ts @@ -9,11 +9,11 @@ import { Text, type TUI, } from "@earendil-works/pi-tui"; -import type { ModelRegistry } from "../../../core/model-registry.js"; -import type { SettingsManager } from "../../../core/settings-manager.js"; -import { theme } from "../theme/theme.js"; -import { DynamicBorder } from "./dynamic-border.js"; -import { keyHint } from "./keybinding-hints.js"; +import type { ModelRegistry } from "../../../core/model-registry.ts"; +import type { SettingsManager } from "../../../core/settings-manager.ts"; +import { theme } from "../theme/theme.ts"; +import { DynamicBorder } from "./dynamic-border.ts"; +import { keyHint } from "./keybinding-hints.ts"; interface ModelItem { provider: string; diff --git a/packages/coding-agent/src/modes/interactive/components/oauth-selector.ts b/packages/coding-agent/src/modes/interactive/components/oauth-selector.ts index 93ce0e93..5910e700 100644 --- a/packages/coding-agent/src/modes/interactive/components/oauth-selector.ts +++ b/packages/coding-agent/src/modes/interactive/components/oauth-selector.ts @@ -7,9 +7,9 @@ import { Spacer, TruncatedText, } from "@earendil-works/pi-tui"; -import type { AuthStatus, AuthStorage } from "../../../core/auth-storage.js"; -import { theme } from "../theme/theme.js"; -import { DynamicBorder } from "./dynamic-border.js"; +import type { AuthStatus, AuthStorage } from "../../../core/auth-storage.ts"; +import { theme } from "../theme/theme.ts"; +import { DynamicBorder } from "./dynamic-border.ts"; export type AuthSelectorProvider = { id: string; diff --git a/packages/coding-agent/src/modes/interactive/components/scoped-models-selector.ts b/packages/coding-agent/src/modes/interactive/components/scoped-models-selector.ts index c5a82d9a..06ce9169 100644 --- a/packages/coding-agent/src/modes/interactive/components/scoped-models-selector.ts +++ b/packages/coding-agent/src/modes/interactive/components/scoped-models-selector.ts @@ -10,9 +10,9 @@ import { Spacer, Text, } from "@earendil-works/pi-tui"; -import { theme } from "../theme/theme.js"; -import { DynamicBorder } from "./dynamic-border.js"; -import { keyText } from "./keybinding-hints.js"; +import { theme } from "../theme/theme.ts"; +import { DynamicBorder } from "./dynamic-border.ts"; +import { keyText } from "./keybinding-hints.ts"; // EnabledIds: null = all enabled (no filter), string[] = explicit ordered list type EnabledIds = string[] | null; diff --git a/packages/coding-agent/src/modes/interactive/components/session-selector-search.ts b/packages/coding-agent/src/modes/interactive/components/session-selector-search.ts index e82a9c05..9b5bf232 100644 --- a/packages/coding-agent/src/modes/interactive/components/session-selector-search.ts +++ b/packages/coding-agent/src/modes/interactive/components/session-selector-search.ts @@ -1,5 +1,5 @@ import { fuzzyMatch } from "@earendil-works/pi-tui"; -import type { SessionInfo } from "../../../core/session-manager.js"; +import type { SessionInfo } from "../../../core/session-manager.ts"; export type SortMode = "threaded" | "recent" | "relevance"; diff --git a/packages/coding-agent/src/modes/interactive/components/session-selector.ts b/packages/coding-agent/src/modes/interactive/components/session-selector.ts index e7c0b072..74141e5e 100644 --- a/packages/coding-agent/src/modes/interactive/components/session-selector.ts +++ b/packages/coding-agent/src/modes/interactive/components/session-selector.ts @@ -13,13 +13,13 @@ import { truncateToWidth, visibleWidth, } from "@earendil-works/pi-tui"; -import { KeybindingsManager } from "../../../core/keybindings.js"; -import type { SessionInfo, SessionListProgress } from "../../../core/session-manager.js"; -import { canonicalizePath as _canonicalizePath } from "../../../utils/paths.js"; -import { theme } from "../theme/theme.js"; -import { DynamicBorder } from "./dynamic-border.js"; -import { keyHint, keyText } from "./keybinding-hints.js"; -import { filterAndSortSessions, hasSessionName, type NameFilter, type SortMode } from "./session-selector-search.js"; +import { KeybindingsManager } from "../../../core/keybindings.ts"; +import type { SessionInfo, SessionListProgress } from "../../../core/session-manager.ts"; +import { canonicalizePath as _canonicalizePath } from "../../../utils/paths.ts"; +import { theme } from "../theme/theme.ts"; +import { DynamicBorder } from "./dynamic-border.ts"; +import { keyHint, keyText } from "./keybinding-hints.ts"; +import { filterAndSortSessions, hasSessionName, type NameFilter, type SortMode } from "./session-selector-search.ts"; type SessionScope = "current" | "all"; diff --git a/packages/coding-agent/src/modes/interactive/components/settings-selector.ts b/packages/coding-agent/src/modes/interactive/components/settings-selector.ts index 7d9d9cdf..f99d7381 100644 --- a/packages/coding-agent/src/modes/interactive/components/settings-selector.ts +++ b/packages/coding-agent/src/modes/interactive/components/settings-selector.ts @@ -11,10 +11,10 @@ import { Spacer, Text, } from "@earendil-works/pi-tui"; -import type { WarningSettings } from "../../../core/settings-manager.js"; -import { getSelectListTheme, getSettingsListTheme, theme } from "../theme/theme.js"; -import { DynamicBorder } from "./dynamic-border.js"; -import { keyDisplayText } from "./keybinding-hints.js"; +import type { WarningSettings } from "../../../core/settings-manager.ts"; +import { getSelectListTheme, getSettingsListTheme, theme } from "../theme/theme.ts"; +import { DynamicBorder } from "./dynamic-border.ts"; +import { keyDisplayText } from "./keybinding-hints.ts"; const SETTINGS_SUBMENU_SELECT_LIST_LAYOUT: SelectListLayoutOptions = { minPrimaryColumnWidth: 12, diff --git a/packages/coding-agent/src/modes/interactive/components/show-images-selector.ts b/packages/coding-agent/src/modes/interactive/components/show-images-selector.ts index c0c7bc15..29c69f74 100644 --- a/packages/coding-agent/src/modes/interactive/components/show-images-selector.ts +++ b/packages/coding-agent/src/modes/interactive/components/show-images-selector.ts @@ -1,6 +1,6 @@ import { Container, type SelectItem, SelectList, type SelectListLayoutOptions } from "@earendil-works/pi-tui"; -import { getSelectListTheme } from "../theme/theme.js"; -import { DynamicBorder } from "./dynamic-border.js"; +import { getSelectListTheme } from "../theme/theme.ts"; +import { DynamicBorder } from "./dynamic-border.ts"; const SHOW_IMAGES_SELECT_LIST_LAYOUT: SelectListLayoutOptions = { minPrimaryColumnWidth: 12, diff --git a/packages/coding-agent/src/modes/interactive/components/skill-invocation-message.ts b/packages/coding-agent/src/modes/interactive/components/skill-invocation-message.ts index 8714edc2..e09febc7 100644 --- a/packages/coding-agent/src/modes/interactive/components/skill-invocation-message.ts +++ b/packages/coding-agent/src/modes/interactive/components/skill-invocation-message.ts @@ -1,7 +1,7 @@ import { Box, Markdown, type MarkdownTheme, Text } from "@earendil-works/pi-tui"; -import type { ParsedSkillBlock } from "../../../core/agent-session.js"; -import { getMarkdownTheme, theme } from "../theme/theme.js"; -import { keyText } from "./keybinding-hints.js"; +import type { ParsedSkillBlock } from "../../../core/agent-session.ts"; +import { getMarkdownTheme, theme } from "../theme/theme.ts"; +import { keyText } from "./keybinding-hints.ts"; /** * Component that renders a skill invocation message with collapsed/expanded state. diff --git a/packages/coding-agent/src/modes/interactive/components/theme-selector.ts b/packages/coding-agent/src/modes/interactive/components/theme-selector.ts index 29e6e779..07b37742 100644 --- a/packages/coding-agent/src/modes/interactive/components/theme-selector.ts +++ b/packages/coding-agent/src/modes/interactive/components/theme-selector.ts @@ -1,6 +1,6 @@ import { Container, type SelectItem, SelectList, type SelectListLayoutOptions } from "@earendil-works/pi-tui"; -import { getAvailableThemes, getSelectListTheme } from "../theme/theme.js"; -import { DynamicBorder } from "./dynamic-border.js"; +import { getAvailableThemes, getSelectListTheme } from "../theme/theme.ts"; +import { DynamicBorder } from "./dynamic-border.ts"; const THEME_SELECT_LIST_LAYOUT: SelectListLayoutOptions = { minPrimaryColumnWidth: 12, diff --git a/packages/coding-agent/src/modes/interactive/components/thinking-selector.ts b/packages/coding-agent/src/modes/interactive/components/thinking-selector.ts index 9eadb86f..91a3a626 100644 --- a/packages/coding-agent/src/modes/interactive/components/thinking-selector.ts +++ b/packages/coding-agent/src/modes/interactive/components/thinking-selector.ts @@ -1,7 +1,7 @@ import type { ThinkingLevel } from "@earendil-works/pi-agent-core"; import { Container, type SelectItem, SelectList, type SelectListLayoutOptions } from "@earendil-works/pi-tui"; -import { getSelectListTheme } from "../theme/theme.js"; -import { DynamicBorder } from "./dynamic-border.js"; +import { getSelectListTheme } from "../theme/theme.ts"; +import { DynamicBorder } from "./dynamic-border.ts"; const THINKING_SELECT_LIST_LAYOUT: SelectListLayoutOptions = { minPrimaryColumnWidth: 12, diff --git a/packages/coding-agent/src/modes/interactive/components/tool-execution.ts b/packages/coding-agent/src/modes/interactive/components/tool-execution.ts index 867a0bb5..b941dfc8 100644 --- a/packages/coding-agent/src/modes/interactive/components/tool-execution.ts +++ b/packages/coding-agent/src/modes/interactive/components/tool-execution.ts @@ -1,9 +1,9 @@ import { Box, type Component, Container, getCapabilities, Image, Spacer, Text, type TUI } from "@earendil-works/pi-tui"; -import type { ToolDefinition, ToolRenderContext } from "../../../core/extensions/types.js"; -import { createAllToolDefinitions, type ToolName } from "../../../core/tools/index.js"; -import { getTextOutput as getRenderedTextOutput } from "../../../core/tools/render-utils.js"; -import { convertToPng } from "../../../utils/image-convert.js"; -import { theme } from "../theme/theme.js"; +import type { ToolDefinition, ToolRenderContext } from "../../../core/extensions/types.ts"; +import { createAllToolDefinitions, type ToolName } from "../../../core/tools/index.ts"; +import { getTextOutput as getRenderedTextOutput } from "../../../core/tools/render-utils.ts"; +import { convertToPng } from "../../../utils/image-convert.ts"; +import { theme } from "../theme/theme.ts"; export interface ToolExecutionOptions { showImages?: boolean; diff --git a/packages/coding-agent/src/modes/interactive/components/tree-selector.ts b/packages/coding-agent/src/modes/interactive/components/tree-selector.ts index d895a892..990e705d 100644 --- a/packages/coding-agent/src/modes/interactive/components/tree-selector.ts +++ b/packages/coding-agent/src/modes/interactive/components/tree-selector.ts @@ -9,10 +9,10 @@ import { TruncatedText, truncateToWidth, } from "@earendil-works/pi-tui"; -import type { SessionTreeNode } from "../../../core/session-manager.js"; -import { theme } from "../theme/theme.js"; -import { DynamicBorder } from "./dynamic-border.js"; -import { keyHint, keyText } from "./keybinding-hints.js"; +import type { SessionTreeNode } from "../../../core/session-manager.ts"; +import { theme } from "../theme/theme.ts"; +import { DynamicBorder } from "./dynamic-border.ts"; +import { keyHint, keyText } from "./keybinding-hints.ts"; /** Gutter info: position (displayIndent where connector was) and whether to show │ */ interface GutterInfo { diff --git a/packages/coding-agent/src/modes/interactive/components/user-message-selector.ts b/packages/coding-agent/src/modes/interactive/components/user-message-selector.ts index 6cf4223a..69fbbc82 100644 --- a/packages/coding-agent/src/modes/interactive/components/user-message-selector.ts +++ b/packages/coding-agent/src/modes/interactive/components/user-message-selector.ts @@ -1,6 +1,6 @@ import { type Component, Container, getKeybindings, Spacer, Text, truncateToWidth } from "@earendil-works/pi-tui"; -import { theme } from "../theme/theme.js"; -import { DynamicBorder } from "./dynamic-border.js"; +import { theme } from "../theme/theme.ts"; +import { DynamicBorder } from "./dynamic-border.ts"; interface UserMessageItem { id: string; // Entry ID in the session diff --git a/packages/coding-agent/src/modes/interactive/components/user-message.ts b/packages/coding-agent/src/modes/interactive/components/user-message.ts index 969df02f..8fda83e9 100644 --- a/packages/coding-agent/src/modes/interactive/components/user-message.ts +++ b/packages/coding-agent/src/modes/interactive/components/user-message.ts @@ -1,5 +1,5 @@ import { Box, Container, Markdown, type MarkdownTheme } from "@earendil-works/pi-tui"; -import { getMarkdownTheme, theme } from "../theme/theme.js"; +import { getMarkdownTheme, theme } from "../theme/theme.ts"; const OSC133_ZONE_START = "\x1b]133;A\x07"; const OSC133_ZONE_END = "\x1b]133;B\x07"; diff --git a/packages/coding-agent/src/modes/interactive/interactive-mode.ts b/packages/coding-agent/src/modes/interactive/interactive-mode.ts index 7fe94ded..c93beb93 100644 --- a/packages/coding-agent/src/modes/interactive/interactive-mode.ts +++ b/packages/coding-agent/src/modes/interactive/interactive-mode.ts @@ -57,9 +57,9 @@ import { getDocsPath, getShareViewerUrl, VERSION, -} from "../../config.js"; -import { type AgentSession, type AgentSessionEvent, parseSkillBlock } from "../../core/agent-session.js"; -import { type AgentSessionRuntime, SessionImportFileNotFoundError } from "../../core/agent-session-runtime.js"; +} from "../../config.ts"; +import { type AgentSession, type AgentSessionEvent, parseSkillBlock } from "../../core/agent-session.ts"; +import { type AgentSessionRuntime, SessionImportFileNotFoundError } from "../../core/agent-session-runtime.ts"; import type { AutocompleteProviderFactory, EditorFactory, @@ -69,57 +69,57 @@ import type { ExtensionUIContext, ExtensionUIDialogOptions, ExtensionWidgetOptions, -} from "../../core/extensions/index.js"; -import { FooterDataProvider, type ReadonlyFooterDataProvider } from "../../core/footer-data-provider.js"; -import { type AppKeybinding, KeybindingsManager } from "../../core/keybindings.js"; -import { createCompactionSummaryMessage } from "../../core/messages.js"; -import { defaultModelPerProvider, findExactModelReferenceMatch, resolveModelScope } from "../../core/model-resolver.js"; -import { DefaultPackageManager } from "../../core/package-manager.js"; -import { BUILT_IN_PROVIDER_DISPLAY_NAMES } from "../../core/provider-display-names.js"; -import type { ResourceDiagnostic } from "../../core/resource-loader.js"; -import { formatMissingSessionCwdPrompt, MissingSessionCwdError } from "../../core/session-cwd.js"; -import { type SessionContext, SessionManager } from "../../core/session-manager.js"; -import { BUILTIN_SLASH_COMMANDS } from "../../core/slash-commands.js"; -import type { SourceInfo } from "../../core/source-info.js"; -import { isInstallTelemetryEnabled } from "../../core/telemetry.js"; -import type { TruncationResult } from "../../core/tools/truncate.js"; -import { getChangelogPath, getNewEntries, parseChangelog } from "../../utils/changelog.js"; -import { copyToClipboard } from "../../utils/clipboard.js"; -import { extensionForImageMimeType, readClipboardImage } from "../../utils/clipboard-image.js"; -import { parseGitUrl } from "../../utils/git.js"; -import { getCwdRelativePath } from "../../utils/paths.js"; -import { getPiUserAgent } from "../../utils/pi-user-agent.js"; -import { killTrackedDetachedChildren } from "../../utils/shell.js"; -import { ensureTool } from "../../utils/tools-manager.js"; -import { checkForNewPiVersion, type LatestPiRelease } from "../../utils/version-check.js"; -import { ArminComponent } from "./components/armin.js"; -import { AssistantMessageComponent } from "./components/assistant-message.js"; -import { BashExecutionComponent } from "./components/bash-execution.js"; -import { BorderedLoader } from "./components/bordered-loader.js"; -import { BranchSummaryMessageComponent } from "./components/branch-summary-message.js"; -import { CompactionSummaryMessageComponent } from "./components/compaction-summary-message.js"; -import { CountdownTimer } from "./components/countdown-timer.js"; -import { CustomEditor } from "./components/custom-editor.js"; -import { CustomMessageComponent } from "./components/custom-message.js"; -import { DaxnutsComponent } from "./components/daxnuts.js"; -import { DynamicBorder } from "./components/dynamic-border.js"; -import { EarendilAnnouncementComponent } from "./components/earendil-announcement.js"; -import { ExtensionEditorComponent } from "./components/extension-editor.js"; -import { ExtensionInputComponent } from "./components/extension-input.js"; -import { ExtensionSelectorComponent } from "./components/extension-selector.js"; -import { FooterComponent } from "./components/footer.js"; -import { formatKeyText, keyDisplayText, keyHint, keyText, rawKeyHint } from "./components/keybinding-hints.js"; -import { LoginDialogComponent } from "./components/login-dialog.js"; -import { ModelSelectorComponent } from "./components/model-selector.js"; -import { type AuthSelectorProvider, OAuthSelectorComponent } from "./components/oauth-selector.js"; -import { ScopedModelsSelectorComponent } from "./components/scoped-models-selector.js"; -import { SessionSelectorComponent } from "./components/session-selector.js"; -import { SettingsSelectorComponent } from "./components/settings-selector.js"; -import { SkillInvocationMessageComponent } from "./components/skill-invocation-message.js"; -import { ToolExecutionComponent } from "./components/tool-execution.js"; -import { TreeSelectorComponent } from "./components/tree-selector.js"; -import { UserMessageComponent } from "./components/user-message.js"; -import { UserMessageSelectorComponent } from "./components/user-message-selector.js"; +} from "../../core/extensions/index.ts"; +import { FooterDataProvider, type ReadonlyFooterDataProvider } from "../../core/footer-data-provider.ts"; +import { type AppKeybinding, KeybindingsManager } from "../../core/keybindings.ts"; +import { createCompactionSummaryMessage } from "../../core/messages.ts"; +import { defaultModelPerProvider, findExactModelReferenceMatch, resolveModelScope } from "../../core/model-resolver.ts"; +import { DefaultPackageManager } from "../../core/package-manager.ts"; +import { BUILT_IN_PROVIDER_DISPLAY_NAMES } from "../../core/provider-display-names.ts"; +import type { ResourceDiagnostic } from "../../core/resource-loader.ts"; +import { formatMissingSessionCwdPrompt, MissingSessionCwdError } from "../../core/session-cwd.ts"; +import { type SessionContext, SessionManager } from "../../core/session-manager.ts"; +import { BUILTIN_SLASH_COMMANDS } from "../../core/slash-commands.ts"; +import type { SourceInfo } from "../../core/source-info.ts"; +import { isInstallTelemetryEnabled } from "../../core/telemetry.ts"; +import type { TruncationResult } from "../../core/tools/truncate.ts"; +import { getChangelogPath, getNewEntries, parseChangelog } from "../../utils/changelog.ts"; +import { copyToClipboard } from "../../utils/clipboard.ts"; +import { extensionForImageMimeType, readClipboardImage } from "../../utils/clipboard-image.ts"; +import { parseGitUrl } from "../../utils/git.ts"; +import { getCwdRelativePath } from "../../utils/paths.ts"; +import { getPiUserAgent } from "../../utils/pi-user-agent.ts"; +import { killTrackedDetachedChildren } from "../../utils/shell.ts"; +import { ensureTool } from "../../utils/tools-manager.ts"; +import { checkForNewPiVersion, type LatestPiRelease } from "../../utils/version-check.ts"; +import { ArminComponent } from "./components/armin.ts"; +import { AssistantMessageComponent } from "./components/assistant-message.ts"; +import { BashExecutionComponent } from "./components/bash-execution.ts"; +import { BorderedLoader } from "./components/bordered-loader.ts"; +import { BranchSummaryMessageComponent } from "./components/branch-summary-message.ts"; +import { CompactionSummaryMessageComponent } from "./components/compaction-summary-message.ts"; +import { CountdownTimer } from "./components/countdown-timer.ts"; +import { CustomEditor } from "./components/custom-editor.ts"; +import { CustomMessageComponent } from "./components/custom-message.ts"; +import { DaxnutsComponent } from "./components/daxnuts.ts"; +import { DynamicBorder } from "./components/dynamic-border.ts"; +import { EarendilAnnouncementComponent } from "./components/earendil-announcement.ts"; +import { ExtensionEditorComponent } from "./components/extension-editor.ts"; +import { ExtensionInputComponent } from "./components/extension-input.ts"; +import { ExtensionSelectorComponent } from "./components/extension-selector.ts"; +import { FooterComponent } from "./components/footer.ts"; +import { formatKeyText, keyDisplayText, keyHint, keyText, rawKeyHint } from "./components/keybinding-hints.ts"; +import { LoginDialogComponent } from "./components/login-dialog.ts"; +import { ModelSelectorComponent } from "./components/model-selector.ts"; +import { type AuthSelectorProvider, OAuthSelectorComponent } from "./components/oauth-selector.ts"; +import { ScopedModelsSelectorComponent } from "./components/scoped-models-selector.ts"; +import { SessionSelectorComponent } from "./components/session-selector.ts"; +import { SettingsSelectorComponent } from "./components/settings-selector.ts"; +import { SkillInvocationMessageComponent } from "./components/skill-invocation-message.ts"; +import { ToolExecutionComponent } from "./components/tool-execution.ts"; +import { TreeSelectorComponent } from "./components/tree-selector.ts"; +import { UserMessageComponent } from "./components/user-message.ts"; +import { UserMessageSelectorComponent } from "./components/user-message-selector.ts"; import { getAvailableThemes, getAvailableThemesWithPaths, @@ -135,7 +135,7 @@ import { Theme, type ThemeColor, theme, -} from "./theme/theme.js"; +} from "./theme/theme.ts"; /** Interface for components that can be expanded/collapsed */ interface Expandable { diff --git a/packages/coding-agent/src/modes/interactive/theme/theme.ts b/packages/coding-agent/src/modes/interactive/theme/theme.ts index 9fb8b031..0560f014 100644 --- a/packages/coding-agent/src/modes/interactive/theme/theme.ts +++ b/packages/coding-agent/src/modes/interactive/theme/theme.ts @@ -10,10 +10,10 @@ import { import chalk from "chalk"; import { type Static, Type } from "typebox"; import { Compile } from "typebox/compile"; -import { getCustomThemesDir, getThemesDir } from "../../../config.js"; -import type { SourceInfo } from "../../../core/source-info.js"; -import { closeWatcher, watchWithErrorHandler } from "../../../utils/fs-watch.js"; -import { highlight, supportsLanguage } from "../../../utils/syntax-highlight.js"; +import { getCustomThemesDir, getThemesDir } from "../../../config.ts"; +import type { SourceInfo } from "../../../core/source-info.ts"; +import { closeWatcher, watchWithErrorHandler } from "../../../utils/fs-watch.ts"; +import { highlight, supportsLanguage } from "../../../utils/syntax-highlight.ts"; // ============================================================================ // Types & Schema diff --git a/packages/coding-agent/src/modes/print-mode.ts b/packages/coding-agent/src/modes/print-mode.ts index 2f9fb048..c9553c55 100644 --- a/packages/coding-agent/src/modes/print-mode.ts +++ b/packages/coding-agent/src/modes/print-mode.ts @@ -7,9 +7,9 @@ */ import type { AssistantMessage, ImageContent } from "@earendil-works/pi-ai"; -import type { AgentSessionRuntime } from "../core/agent-session-runtime.js"; -import { flushRawStdout, writeRawStdout } from "../core/output-guard.js"; -import { killTrackedDetachedChildren } from "../utils/shell.js"; +import type { AgentSessionRuntime } from "../core/agent-session-runtime.ts"; +import { flushRawStdout, writeRawStdout } from "../core/output-guard.ts"; +import { killTrackedDetachedChildren } from "../utils/shell.ts"; /** * Options for print mode. diff --git a/packages/coding-agent/src/modes/rpc/rpc-client.ts b/packages/coding-agent/src/modes/rpc/rpc-client.ts index 194f479e..53b96e64 100644 --- a/packages/coding-agent/src/modes/rpc/rpc-client.ts +++ b/packages/coding-agent/src/modes/rpc/rpc-client.ts @@ -7,11 +7,11 @@ import { type ChildProcess, spawn } from "node:child_process"; import type { AgentEvent, AgentMessage, ThinkingLevel } from "@earendil-works/pi-agent-core"; import type { ImageContent } from "@earendil-works/pi-ai"; -import type { SessionStats } from "../../core/agent-session.js"; -import type { BashResult } from "../../core/bash-executor.js"; -import type { CompactionResult } from "../../core/compaction/index.js"; -import { attachJsonlLineReader, serializeJsonLine } from "./jsonl.js"; -import type { RpcCommand, RpcResponse, RpcSessionState, RpcSlashCommand } from "./rpc-types.js"; +import type { SessionStats } from "../../core/agent-session.ts"; +import type { BashResult } from "../../core/bash-executor.ts"; +import type { CompactionResult } from "../../core/compaction/index.ts"; +import { attachJsonlLineReader, serializeJsonLine } from "./jsonl.ts"; +import type { RpcCommand, RpcResponse, RpcSessionState, RpcSlashCommand } from "./rpc-types.ts"; // ============================================================================ // Types diff --git a/packages/coding-agent/src/modes/rpc/rpc-mode.ts b/packages/coding-agent/src/modes/rpc/rpc-mode.ts index b10692ed..828c9f67 100644 --- a/packages/coding-agent/src/modes/rpc/rpc-mode.ts +++ b/packages/coding-agent/src/modes/rpc/rpc-mode.ts @@ -12,17 +12,17 @@ */ import * as crypto from "node:crypto"; -import type { AgentSessionRuntime } from "../../core/agent-session-runtime.js"; +import type { AgentSessionRuntime } from "../../core/agent-session-runtime.ts"; import type { ExtensionUIContext, ExtensionUIDialogOptions, ExtensionWidgetOptions, WorkingIndicatorOptions, -} from "../../core/extensions/index.js"; -import { takeOverStdout, writeRawStdout } from "../../core/output-guard.js"; -import { killTrackedDetachedChildren } from "../../utils/shell.js"; -import { type Theme, theme } from "../interactive/theme/theme.js"; -import { attachJsonlLineReader, serializeJsonLine } from "./jsonl.js"; +} from "../../core/extensions/index.ts"; +import { takeOverStdout, writeRawStdout } from "../../core/output-guard.ts"; +import { killTrackedDetachedChildren } from "../../utils/shell.ts"; +import { type Theme, theme } from "../interactive/theme/theme.ts"; +import { attachJsonlLineReader, serializeJsonLine } from "./jsonl.ts"; import type { RpcCommand, RpcExtensionUIRequest, @@ -30,7 +30,7 @@ import type { RpcResponse, RpcSessionState, RpcSlashCommand, -} from "./rpc-types.js"; +} from "./rpc-types.ts"; // Re-export types for consumers export type { @@ -39,7 +39,7 @@ export type { RpcExtensionUIResponse, RpcResponse, RpcSessionState, -} from "./rpc-types.js"; +} from "./rpc-types.ts"; /** * Run in RPC mode. diff --git a/packages/coding-agent/src/modes/rpc/rpc-types.ts b/packages/coding-agent/src/modes/rpc/rpc-types.ts index 43aeeeeb..02c8a6b9 100644 --- a/packages/coding-agent/src/modes/rpc/rpc-types.ts +++ b/packages/coding-agent/src/modes/rpc/rpc-types.ts @@ -7,10 +7,10 @@ import type { AgentMessage, ThinkingLevel } from "@earendil-works/pi-agent-core"; import type { ImageContent, Model } from "@earendil-works/pi-ai"; -import type { SessionStats } from "../../core/agent-session.js"; -import type { BashResult } from "../../core/bash-executor.js"; -import type { CompactionResult } from "../../core/compaction/index.js"; -import type { SourceInfo } from "../../core/source-info.js"; +import type { SessionStats } from "../../core/agent-session.ts"; +import type { BashResult } from "../../core/bash-executor.ts"; +import type { CompactionResult } from "../../core/compaction/index.ts"; +import type { SourceInfo } from "../../core/source-info.ts"; // ============================================================================ // RPC Commands (stdin) diff --git a/packages/coding-agent/src/package-manager-cli.ts b/packages/coding-agent/src/package-manager-cli.ts index 8fd5d6d4..f61d4ff2 100644 --- a/packages/coding-agent/src/package-manager-cli.ts +++ b/packages/coding-agent/src/package-manager-cli.ts @@ -1,6 +1,6 @@ import { Markdown, type MarkdownTheme } from "@earendil-works/pi-tui"; import chalk from "chalk"; -import { selectConfig } from "./cli/config-selector.js"; +import { selectConfig } from "./cli/config-selector.ts"; import { APP_NAME, detectInstallMethod, @@ -11,15 +11,15 @@ import { PACKAGE_NAME, type SelfUpdateCommand, VERSION, -} from "./config.js"; -import { DefaultPackageManager } from "./core/package-manager.js"; -import { SettingsManager } from "./core/settings-manager.js"; -import { spawnProcess } from "./utils/child-process.js"; -import { getLatestPiRelease, isNewerPackageVersion } from "./utils/version-check.js"; +} from "./config.ts"; +import { DefaultPackageManager } from "./core/package-manager.ts"; +import { SettingsManager } from "./core/settings-manager.ts"; +import { spawnProcess } from "./utils/child-process.ts"; +import { getLatestPiRelease, isNewerPackageVersion } from "./utils/version-check.ts"; import { cleanupWindowsSelfUpdateQuarantine, quarantineWindowsNativeDependencies, -} from "./utils/windows-self-update.js"; +} from "./utils/windows-self-update.ts"; export type PackageCommand = "install" | "remove" | "update" | "list"; diff --git a/packages/coding-agent/src/utils/changelog.ts b/packages/coding-agent/src/utils/changelog.ts index 79b17d0e..b9e8e35d 100644 --- a/packages/coding-agent/src/utils/changelog.ts +++ b/packages/coding-agent/src/utils/changelog.ts @@ -96,4 +96,4 @@ export function getNewEntries(entries: ChangelogEntry[], lastVersion: string): C } // Re-export getChangelogPath from paths.ts for convenience -export { getChangelogPath } from "../config.js"; +export { getChangelogPath } from "../config.ts"; diff --git a/packages/coding-agent/src/utils/clipboard-image.ts b/packages/coding-agent/src/utils/clipboard-image.ts index 4cf44908..44f8fb10 100644 --- a/packages/coding-agent/src/utils/clipboard-image.ts +++ b/packages/coding-agent/src/utils/clipboard-image.ts @@ -4,8 +4,8 @@ import { readFileSync, unlinkSync } from "fs"; import { tmpdir } from "os"; import { join } from "path"; -import { clipboard } from "./clipboard-native.js"; -import { loadPhoton } from "./photon.js"; +import { clipboard } from "./clipboard-native.ts"; +import { loadPhoton } from "./photon.ts"; export type ClipboardImage = { bytes: Uint8Array; diff --git a/packages/coding-agent/src/utils/clipboard.ts b/packages/coding-agent/src/utils/clipboard.ts index 84166e95..26e1d018 100644 --- a/packages/coding-agent/src/utils/clipboard.ts +++ b/packages/coding-agent/src/utils/clipboard.ts @@ -1,7 +1,7 @@ import { execSync, spawn } from "child_process"; import { platform } from "os"; -import { isWaylandSession } from "./clipboard-image.js"; -import { clipboard } from "./clipboard-native.js"; +import { isWaylandSession } from "./clipboard-image.ts"; +import { clipboard } from "./clipboard-native.ts"; type NativeClipboardExecOptions = { input: string; diff --git a/packages/coding-agent/src/utils/exif-orientation.ts b/packages/coding-agent/src/utils/exif-orientation.ts index ac34c790..4b454afa 100644 --- a/packages/coding-agent/src/utils/exif-orientation.ts +++ b/packages/coding-agent/src/utils/exif-orientation.ts @@ -1,4 +1,4 @@ -import type { PhotonImageType } from "./photon.js"; +import type { PhotonImageType } from "./photon.ts"; type Photon = typeof import("@silvia-odwyer/photon-node"); diff --git a/packages/coding-agent/src/utils/image-convert.ts b/packages/coding-agent/src/utils/image-convert.ts index 670997c0..4d5f0204 100644 --- a/packages/coding-agent/src/utils/image-convert.ts +++ b/packages/coding-agent/src/utils/image-convert.ts @@ -1,5 +1,5 @@ -import { applyExifOrientation } from "./exif-orientation.js"; -import { loadPhoton } from "./photon.js"; +import { applyExifOrientation } from "./exif-orientation.ts"; +import { loadPhoton } from "./photon.ts"; /** * Convert image to PNG format for terminal display. diff --git a/packages/coding-agent/src/utils/image-resize.ts b/packages/coding-agent/src/utils/image-resize.ts index 84b4ac33..3eb17dae 100644 --- a/packages/coding-agent/src/utils/image-resize.ts +++ b/packages/coding-agent/src/utils/image-resize.ts @@ -1,6 +1,6 @@ import type { ImageContent } from "@earendil-works/pi-ai"; -import { applyExifOrientation } from "./exif-orientation.js"; -import { loadPhoton } from "./photon.js"; +import { applyExifOrientation } from "./exif-orientation.ts"; +import { loadPhoton } from "./photon.ts"; export interface ImageResizeOptions { maxWidth?: number; // Default: 2000 diff --git a/packages/coding-agent/src/utils/shell.ts b/packages/coding-agent/src/utils/shell.ts index 7e3cc8ef..817ba4f9 100644 --- a/packages/coding-agent/src/utils/shell.ts +++ b/packages/coding-agent/src/utils/shell.ts @@ -1,7 +1,7 @@ import { existsSync } from "node:fs"; import { delimiter } from "node:path"; import { spawn, spawnSync } from "child_process"; -import { getBinDir } from "../config.js"; +import { getBinDir } from "../config.ts"; export interface ShellConfig { shell: string; diff --git a/packages/coding-agent/src/utils/syntax-highlight.ts b/packages/coding-agent/src/utils/syntax-highlight.ts index fc8a0281..bcc4add1 100644 --- a/packages/coding-agent/src/utils/syntax-highlight.ts +++ b/packages/coding-agent/src/utils/syntax-highlight.ts @@ -1,5 +1,5 @@ import hljs from "highlight.js/lib/index.js"; -import { decodeHtmlEntityAt } from "./html.js"; +import { decodeHtmlEntityAt } from "./html.ts"; export type HighlightFormatter = (text: string) => string; export type HighlightTheme = Partial>; diff --git a/packages/coding-agent/src/utils/tools-manager.ts b/packages/coding-agent/src/utils/tools-manager.ts index 2ee60a0d..09d9a119 100644 --- a/packages/coding-agent/src/utils/tools-manager.ts +++ b/packages/coding-agent/src/utils/tools-manager.ts @@ -5,7 +5,7 @@ import { arch, platform } from "os"; import { join } from "path"; import { Readable } from "stream"; import { pipeline } from "stream/promises"; -import { APP_NAME, getBinDir } from "../config.js"; +import { APP_NAME, getBinDir } from "../config.ts"; const TOOLS_DIR = getBinDir(); const NETWORK_TIMEOUT_MS = 10_000; diff --git a/packages/coding-agent/src/utils/version-check.ts b/packages/coding-agent/src/utils/version-check.ts index 27f3390c..aec25bdb 100644 --- a/packages/coding-agent/src/utils/version-check.ts +++ b/packages/coding-agent/src/utils/version-check.ts @@ -1,4 +1,4 @@ -import { getPiUserAgent } from "./pi-user-agent.js"; +import { getPiUserAgent } from "./pi-user-agent.ts"; const LATEST_VERSION_URL = "https://pi.dev/api/latest-version"; const DEFAULT_VERSION_CHECK_TIMEOUT_MS = 10000; diff --git a/packages/coding-agent/src/utils/windows-self-update.ts b/packages/coding-agent/src/utils/windows-self-update.ts index a988529e..c837d491 100644 --- a/packages/coding-agent/src/utils/windows-self-update.ts +++ b/packages/coding-agent/src/utils/windows-self-update.ts @@ -1,7 +1,7 @@ import { randomUUID } from "node:crypto"; import { copyFileSync, existsSync, mkdirSync, renameSync, rmSync } from "node:fs"; import { basename, dirname, join, relative, resolve, toNamespacedPath } from "node:path"; -import { getCwdRelativePath } from "./paths.js"; +import { getCwdRelativePath } from "./paths.ts"; const QUARANTINE_DIR_NAME = ".pi-native-quarantine"; diff --git a/packages/tui/src/autocomplete.ts b/packages/tui/src/autocomplete.ts index 965dc303..5408967d 100644 --- a/packages/tui/src/autocomplete.ts +++ b/packages/tui/src/autocomplete.ts @@ -2,7 +2,7 @@ import { spawn } from "child_process"; import { readdirSync, statSync } from "fs"; import { homedir } from "os"; import { basename, dirname, join } from "path"; -import { fuzzyFilter } from "./fuzzy.js"; +import { fuzzyFilter } from "./fuzzy.ts"; const PATH_DELIMITERS = new Set([" ", "\t", '"', "'", "="]); diff --git a/packages/tui/src/components/box.ts b/packages/tui/src/components/box.ts index c99b8600..3573ab09 100644 --- a/packages/tui/src/components/box.ts +++ b/packages/tui/src/components/box.ts @@ -1,5 +1,5 @@ -import type { Component } from "../tui.js"; -import { applyBackgroundToLine, visibleWidth } from "../utils.js"; +import type { Component } from "../tui.ts"; +import { applyBackgroundToLine, visibleWidth } from "../utils.ts"; type RenderCache = { childLines: string[]; diff --git a/packages/tui/src/components/cancellable-loader.ts b/packages/tui/src/components/cancellable-loader.ts index e8bd85b9..7822cb04 100644 --- a/packages/tui/src/components/cancellable-loader.ts +++ b/packages/tui/src/components/cancellable-loader.ts @@ -1,5 +1,5 @@ -import { getKeybindings } from "../keybindings.js"; -import { Loader } from "./loader.js"; +import { getKeybindings } from "../keybindings.ts"; +import { Loader } from "./loader.ts"; /** * Loader that can be cancelled with Escape. diff --git a/packages/tui/src/components/editor.ts b/packages/tui/src/components/editor.ts index 990469e5..0b30ad0a 100644 --- a/packages/tui/src/components/editor.ts +++ b/packages/tui/src/components/editor.ts @@ -1,11 +1,11 @@ -import type { AutocompleteProvider, AutocompleteSuggestions } from "../autocomplete.js"; -import { getKeybindings } from "../keybindings.js"; -import { decodePrintableKey, matchesKey } from "../keys.js"; -import { KillRing } from "../kill-ring.js"; -import { type Component, CURSOR_MARKER, type Focusable, type TUI } from "../tui.js"; -import { UndoStack } from "../undo-stack.js"; -import { getSegmenter, isPunctuationChar, isWhitespaceChar, truncateToWidth, visibleWidth } from "../utils.js"; -import { SelectList, type SelectListLayoutOptions, type SelectListTheme } from "./select-list.js"; +import type { AutocompleteProvider, AutocompleteSuggestions } from "../autocomplete.ts"; +import { getKeybindings } from "../keybindings.ts"; +import { decodePrintableKey, matchesKey } from "../keys.ts"; +import { KillRing } from "../kill-ring.ts"; +import { type Component, CURSOR_MARKER, type Focusable, type TUI } from "../tui.ts"; +import { UndoStack } from "../undo-stack.ts"; +import { getSegmenter, isPunctuationChar, isWhitespaceChar, truncateToWidth, visibleWidth } from "../utils.ts"; +import { SelectList, type SelectListLayoutOptions, type SelectListTheme } from "./select-list.ts"; const baseSegmenter = getSegmenter(); diff --git a/packages/tui/src/components/image.ts b/packages/tui/src/components/image.ts index eb68757d..50c1e0f7 100644 --- a/packages/tui/src/components/image.ts +++ b/packages/tui/src/components/image.ts @@ -6,8 +6,8 @@ import { type ImageDimensions, imageFallback, renderImage, -} from "../terminal-image.js"; -import type { Component } from "../tui.js"; +} from "../terminal-image.ts"; +import type { Component } from "../tui.ts"; export interface ImageTheme { fallbackColor: (str: string) => string; diff --git a/packages/tui/src/components/input.ts b/packages/tui/src/components/input.ts index 9c97af95..71f2363b 100644 --- a/packages/tui/src/components/input.ts +++ b/packages/tui/src/components/input.ts @@ -1,9 +1,9 @@ -import { getKeybindings } from "../keybindings.js"; -import { decodeKittyPrintable } from "../keys.js"; -import { KillRing } from "../kill-ring.js"; -import { type Component, CURSOR_MARKER, type Focusable } from "../tui.js"; -import { UndoStack } from "../undo-stack.js"; -import { getSegmenter, isPunctuationChar, isWhitespaceChar, sliceByColumn, visibleWidth } from "../utils.js"; +import { getKeybindings } from "../keybindings.ts"; +import { decodeKittyPrintable } from "../keys.ts"; +import { KillRing } from "../kill-ring.ts"; +import { type Component, CURSOR_MARKER, type Focusable } from "../tui.ts"; +import { UndoStack } from "../undo-stack.ts"; +import { getSegmenter, isPunctuationChar, isWhitespaceChar, sliceByColumn, visibleWidth } from "../utils.ts"; const segmenter = getSegmenter(); diff --git a/packages/tui/src/components/loader.ts b/packages/tui/src/components/loader.ts index 6bac6ce8..9bce7293 100644 --- a/packages/tui/src/components/loader.ts +++ b/packages/tui/src/components/loader.ts @@ -1,5 +1,5 @@ -import type { TUI } from "../tui.js"; -import { Text } from "./text.js"; +import type { TUI } from "../tui.ts"; +import { Text } from "./text.ts"; export interface LoaderIndicatorOptions { /** Animation frames. Use an empty array to hide the indicator. */ @@ -21,17 +21,23 @@ export class Loader extends Text { private intervalId: NodeJS.Timeout | null = null; private ui: TUI | null = null; private renderIndicatorVerbatim = false; + private spinnerColorFn: (str: string) => string; + private messageColorFn: (str: string) => string; + private message: string = "Loading..."; constructor( ui: TUI, - private spinnerColorFn: (str: string) => string, - private messageColorFn: (str: string) => string, - private message: string = "Loading...", + spinnerColorFn: (str: string) => string, + messageColorFn: (str: string) => string, + message: string = "Loading...", indicator?: LoaderIndicatorOptions, ) { super("", 1, 0); this.ui = ui; this.setIndicator(indicator); + this.spinnerColorFn = spinnerColorFn; + this.messageColorFn = messageColorFn; + this.message = message; } render(width: number): string[] { diff --git a/packages/tui/src/components/markdown.ts b/packages/tui/src/components/markdown.ts index 8e23e289..2f0ab68d 100644 --- a/packages/tui/src/components/markdown.ts +++ b/packages/tui/src/components/markdown.ts @@ -1,7 +1,7 @@ import { Marked, type Token, Tokenizer, type Tokens } from "marked"; -import { getCapabilities, hyperlink, isImageLine } from "../terminal-image.js"; -import type { Component } from "../tui.js"; -import { applyBackgroundToLine, visibleWidth, wrapTextWithAnsi } from "../utils.js"; +import { getCapabilities, hyperlink, isImageLine } from "../terminal-image.ts"; +import type { Component } from "../tui.ts"; +import { applyBackgroundToLine, visibleWidth, wrapTextWithAnsi } from "../utils.ts"; const STRICT_STRIKETHROUGH_REGEX = /^(~~)(?=[^\s~])((?:\\.|[^\\])*?(?:\\.|[^\s~\\]))\1(?=[^~]|$)/; diff --git a/packages/tui/src/components/select-list.ts b/packages/tui/src/components/select-list.ts index 09353e54..26fdb685 100644 --- a/packages/tui/src/components/select-list.ts +++ b/packages/tui/src/components/select-list.ts @@ -1,6 +1,6 @@ -import { getKeybindings } from "../keybindings.js"; -import type { Component } from "../tui.js"; -import { truncateToWidth, visibleWidth } from "../utils.js"; +import { getKeybindings } from "../keybindings.ts"; +import type { Component } from "../tui.ts"; +import { truncateToWidth, visibleWidth } from "../utils.ts"; const DEFAULT_PRIMARY_COLUMN_WIDTH = 32; const PRIMARY_COLUMN_GAP = 2; diff --git a/packages/tui/src/components/settings-list.ts b/packages/tui/src/components/settings-list.ts index 6a6235dd..88e9433f 100644 --- a/packages/tui/src/components/settings-list.ts +++ b/packages/tui/src/components/settings-list.ts @@ -1,8 +1,8 @@ -import { fuzzyFilter } from "../fuzzy.js"; -import { getKeybindings } from "../keybindings.js"; -import type { Component } from "../tui.js"; -import { truncateToWidth, visibleWidth, wrapTextWithAnsi } from "../utils.js"; -import { Input } from "./input.js"; +import { fuzzyFilter } from "../fuzzy.ts"; +import { getKeybindings } from "../keybindings.ts"; +import type { Component } from "../tui.ts"; +import { truncateToWidth, visibleWidth, wrapTextWithAnsi } from "../utils.ts"; +import { Input } from "./input.ts"; export interface SettingItem { /** Unique identifier for this setting */ diff --git a/packages/tui/src/components/spacer.ts b/packages/tui/src/components/spacer.ts index 8c63d3c2..7abe1551 100644 --- a/packages/tui/src/components/spacer.ts +++ b/packages/tui/src/components/spacer.ts @@ -1,4 +1,4 @@ -import type { Component } from "../tui.js"; +import type { Component } from "../tui.ts"; /** * Spacer component that renders empty lines diff --git a/packages/tui/src/components/text.ts b/packages/tui/src/components/text.ts index efcf25b4..3809a48a 100644 --- a/packages/tui/src/components/text.ts +++ b/packages/tui/src/components/text.ts @@ -1,5 +1,5 @@ -import type { Component } from "../tui.js"; -import { applyBackgroundToLine, visibleWidth, wrapTextWithAnsi } from "../utils.js"; +import type { Component } from "../tui.ts"; +import { applyBackgroundToLine, visibleWidth, wrapTextWithAnsi } from "../utils.ts"; /** * Text component - displays multi-line text with word wrapping diff --git a/packages/tui/src/components/truncated-text.ts b/packages/tui/src/components/truncated-text.ts index 12eac558..c26b8892 100644 --- a/packages/tui/src/components/truncated-text.ts +++ b/packages/tui/src/components/truncated-text.ts @@ -1,5 +1,5 @@ -import type { Component } from "../tui.js"; -import { truncateToWidth, visibleWidth } from "../utils.js"; +import type { Component } from "../tui.ts"; +import { truncateToWidth, visibleWidth } from "../utils.ts"; /** * Text component that truncates to fit viewport width diff --git a/packages/tui/src/editor-component.ts b/packages/tui/src/editor-component.ts index c6b6c43d..59564503 100644 --- a/packages/tui/src/editor-component.ts +++ b/packages/tui/src/editor-component.ts @@ -1,5 +1,5 @@ -import type { AutocompleteProvider } from "./autocomplete.js"; -import type { Component } from "./tui.js"; +import type { AutocompleteProvider } from "./autocomplete.ts"; +import type { Component } from "./tui.ts"; /** * Interface for custom editor components. diff --git a/packages/tui/src/index.ts b/packages/tui/src/index.ts index 3c8ae0aa..9404aa65 100644 --- a/packages/tui/src/index.ts +++ b/packages/tui/src/index.ts @@ -7,30 +7,30 @@ export { type AutocompleteSuggestions, CombinedAutocompleteProvider, type SlashCommand, -} from "./autocomplete.js"; +} from "./autocomplete.ts"; // Components -export { Box } from "./components/box.js"; -export { CancellableLoader } from "./components/cancellable-loader.js"; -export { Editor, type EditorOptions, type EditorTheme } from "./components/editor.js"; -export { Image, type ImageOptions, type ImageTheme } from "./components/image.js"; -export { Input } from "./components/input.js"; -export { Loader, type LoaderIndicatorOptions } from "./components/loader.js"; -export { type DefaultTextStyle, Markdown, type MarkdownTheme } from "./components/markdown.js"; +export { Box } from "./components/box.ts"; +export { CancellableLoader } from "./components/cancellable-loader.ts"; +export { Editor, type EditorOptions, type EditorTheme } from "./components/editor.ts"; +export { Image, type ImageOptions, type ImageTheme } from "./components/image.ts"; +export { Input } from "./components/input.ts"; +export { Loader, type LoaderIndicatorOptions } from "./components/loader.ts"; +export { type DefaultTextStyle, Markdown, type MarkdownTheme } from "./components/markdown.ts"; export { type SelectItem, SelectList, type SelectListLayoutOptions, type SelectListTheme, type SelectListTruncatePrimaryContext, -} from "./components/select-list.js"; -export { type SettingItem, SettingsList, type SettingsListTheme } from "./components/settings-list.js"; -export { Spacer } from "./components/spacer.js"; -export { Text } from "./components/text.js"; -export { TruncatedText } from "./components/truncated-text.js"; +} from "./components/select-list.ts"; +export { type SettingItem, SettingsList, type SettingsListTheme } from "./components/settings-list.ts"; +export { Spacer } from "./components/spacer.ts"; +export { Text } from "./components/text.ts"; +export { TruncatedText } from "./components/truncated-text.ts"; // Editor component interface (for custom editors) -export type { EditorComponent } from "./editor-component.js"; +export type { EditorComponent } from "./editor-component.ts"; // Fuzzy matching -export { type FuzzyMatch, fuzzyFilter, fuzzyMatch } from "./fuzzy.js"; +export { type FuzzyMatch, fuzzyFilter, fuzzyMatch } from "./fuzzy.ts"; // Keybindings export { getKeybindings, @@ -43,7 +43,7 @@ export { KeybindingsManager, setKeybindings, TUI_KEYBINDINGS, -} from "./keybindings.js"; +} from "./keybindings.ts"; // Keyboard input handling export { decodeKittyPrintable, @@ -56,11 +56,11 @@ export { matchesKey, parseKey, setKittyProtocolActive, -} from "./keys.js"; +} from "./keys.ts"; // Input buffering for batch splitting -export { StdinBuffer, type StdinBufferEventMap, type StdinBufferOptions } from "./stdin-buffer.js"; +export { StdinBuffer, type StdinBufferEventMap, type StdinBufferOptions } from "./stdin-buffer.ts"; // Terminal interface and implementations -export { ProcessTerminal, type Terminal } from "./terminal.js"; +export { ProcessTerminal, type Terminal } from "./terminal.ts"; // Terminal image support export { allocateImageId, @@ -88,7 +88,7 @@ export { setCapabilities, setCellDimensions, type TerminalCapabilities, -} from "./terminal-image.js"; +} from "./terminal-image.ts"; export { type Component, Container, @@ -101,6 +101,6 @@ export { type OverlayOptions, type SizeValue, TUI, -} from "./tui.js"; +} from "./tui.ts"; // Utilities -export { truncateToWidth, visibleWidth, wrapTextWithAnsi } from "./utils.js"; +export { truncateToWidth, visibleWidth, wrapTextWithAnsi } from "./utils.ts"; diff --git a/packages/tui/src/keybindings.ts b/packages/tui/src/keybindings.ts index 356554cf..789124ad 100644 --- a/packages/tui/src/keybindings.ts +++ b/packages/tui/src/keybindings.ts @@ -1,4 +1,4 @@ -import { type KeyId, matchesKey } from "./keys.js"; +import { type KeyId, matchesKey } from "./keys.ts"; /** * Global keybinding registry. diff --git a/packages/tui/src/terminal.ts b/packages/tui/src/terminal.ts index 60eb0359..9d24dcdd 100644 --- a/packages/tui/src/terminal.ts +++ b/packages/tui/src/terminal.ts @@ -1,8 +1,8 @@ import * as fs from "node:fs"; import { createRequire } from "node:module"; import * as path from "node:path"; -import { setKittyProtocolActive } from "./keys.js"; -import { StdinBuffer } from "./stdin-buffer.js"; +import { setKittyProtocolActive } from "./keys.ts"; +import { StdinBuffer } from "./stdin-buffer.ts"; const cjsRequire = createRequire(import.meta.url); diff --git a/packages/tui/src/tui.ts b/packages/tui/src/tui.ts index 824ac82f..2874ef02 100644 --- a/packages/tui/src/tui.ts +++ b/packages/tui/src/tui.ts @@ -6,10 +6,10 @@ import * as fs from "node:fs"; import * as os from "node:os"; import * as path from "node:path"; import { performance } from "node:perf_hooks"; -import { isKeyRelease, matchesKey } from "./keys.js"; -import type { Terminal } from "./terminal.js"; -import { deleteKittyImage, getCapabilities, isImageLine, setCellDimensions } from "./terminal-image.js"; -import { extractSegments, normalizeTerminalOutput, sliceByColumn, sliceWithWidth, visibleWidth } from "./utils.js"; +import { isKeyRelease, matchesKey } from "./keys.ts"; +import type { Terminal } from "./terminal.ts"; +import { deleteKittyImage, getCapabilities, isImageLine, setCellDimensions } from "./terminal-image.ts"; +import { extractSegments, normalizeTerminalOutput, sliceByColumn, sliceWithWidth, visibleWidth } from "./utils.ts"; const KITTY_SEQUENCE_PREFIX = "\x1b_G"; diff --git a/packages/tui/test/markdown.test.ts b/packages/tui/test/markdown.test.ts index 4ba33dca..2e1ed022 100644 --- a/packages/tui/test/markdown.test.ts +++ b/packages/tui/test/markdown.test.ts @@ -645,8 +645,11 @@ describe("Markdown component", () => { it("should not leak styles into following lines when rendered in TUI", async () => { class MarkdownWithInput implements Component { public markdownLineCount = 0; + private readonly markdown: Markdown; - constructor(private readonly markdown: Markdown) {} + constructor(markdown: Markdown) { + this.markdown = markdown; + } render(width: number): string[] { const lines = this.markdown.render(width); diff --git a/packages/tui/test/overlay-non-capturing.test.ts b/packages/tui/test/overlay-non-capturing.test.ts index c388814c..a0a7a676 100644 --- a/packages/tui/test/overlay-non-capturing.test.ts +++ b/packages/tui/test/overlay-non-capturing.test.ts @@ -5,7 +5,11 @@ import { TUI } from "../src/tui.js"; import { VirtualTerminal } from "./virtual-terminal.js"; class StaticOverlay implements Component { - constructor(private lines: string[]) {} + private lines: string[]; + + constructor(lines: string[]) { + this.lines = lines; + } render(): string[] { return this.lines; @@ -24,8 +28,11 @@ class EmptyContent implements Component { class FocusableOverlay implements Component, Focusable { focused = false; inputs: string[] = []; + private lines: string[]; - constructor(private lines: string[]) {} + constructor(lines: string[]) { + this.lines = lines; + } handleInput(data: string): void { this.inputs.push(data); diff --git a/packages/tui/test/overlay-options.test.ts b/packages/tui/test/overlay-options.test.ts index 45d909b6..942bddd3 100644 --- a/packages/tui/test/overlay-options.test.ts +++ b/packages/tui/test/overlay-options.test.ts @@ -5,10 +5,13 @@ import { TUI } from "../src/tui.js"; import { VirtualTerminal } from "./virtual-terminal.js"; class StaticOverlay implements Component { - constructor( - private lines: string[], - public requestedWidth?: number, - ) {} + private lines: string[]; + requestedWidth?: number; + + constructor(lines: string[], requestedWidth?: number) { + this.lines = lines; + this.requestedWidth = requestedWidth; + } render(width: number): string[] { // Store the width we were asked to render at for verification diff --git a/packages/tui/test/overlay-short-content.test.ts b/packages/tui/test/overlay-short-content.test.ts index 98c37029..7721ea00 100644 --- a/packages/tui/test/overlay-short-content.test.ts +++ b/packages/tui/test/overlay-short-content.test.ts @@ -4,7 +4,12 @@ import { type Component, TUI } from "../src/tui.js"; import { VirtualTerminal } from "./virtual-terminal.js"; class SimpleContent implements Component { - constructor(private lines: string[]) {} + private lines: string[]; + + constructor(lines: string[]) { + this.lines = lines; + } + render(): string[] { return this.lines; } diff --git a/packages/tui/test/tui-overlay-style-leak.test.ts b/packages/tui/test/tui-overlay-style-leak.test.ts index 59713371..df93deef 100644 --- a/packages/tui/test/tui-overlay-style-leak.test.ts +++ b/packages/tui/test/tui-overlay-style-leak.test.ts @@ -5,7 +5,11 @@ import { type Component, TUI } from "../src/tui.js"; import { VirtualTerminal } from "./virtual-terminal.js"; class StaticLines implements Component { - constructor(private readonly lines: string[]) {} + private readonly lines: string[]; + + constructor(lines: string[]) { + this.lines = lines; + } render(): string[] { return this.lines; @@ -15,7 +19,11 @@ class StaticLines implements Component { } class StaticOverlay implements Component { - constructor(private readonly line: string) {} + private readonly line: string; + + constructor(line: string) { + this.line = line; + } render(): string[] { return [this.line]; diff --git a/packages/web-ui/src/ChatPanel.ts b/packages/web-ui/src/ChatPanel.ts index 0a7a4823..bf86234e 100644 --- a/packages/web-ui/src/ChatPanel.ts +++ b/packages/web-ui/src/ChatPanel.ts @@ -1,16 +1,16 @@ import { Badge } from "@mariozechner/mini-lit/dist/Badge.js"; import { html, LitElement } from "lit"; import { customElement, state } from "lit/decorators.js"; -import "./components/AgentInterface.js"; +import "./components/AgentInterface.ts"; import type { Agent, AgentTool } from "@earendil-works/pi-agent-core"; -import type { AgentInterface } from "./components/AgentInterface.js"; -import { ArtifactsRuntimeProvider } from "./components/sandbox/ArtifactsRuntimeProvider.js"; -import { AttachmentsRuntimeProvider } from "./components/sandbox/AttachmentsRuntimeProvider.js"; -import type { SandboxRuntimeProvider } from "./components/sandbox/SandboxRuntimeProvider.js"; -import { ArtifactsPanel, ArtifactsToolRenderer } from "./tools/artifacts/index.js"; -import { registerToolRenderer } from "./tools/renderer-registry.js"; -import type { Attachment } from "./utils/attachment-utils.js"; -import { i18n } from "./utils/i18n.js"; +import type { AgentInterface } from "./components/AgentInterface.ts"; +import { ArtifactsRuntimeProvider } from "./components/sandbox/ArtifactsRuntimeProvider.ts"; +import { AttachmentsRuntimeProvider } from "./components/sandbox/AttachmentsRuntimeProvider.ts"; +import type { SandboxRuntimeProvider } from "./components/sandbox/SandboxRuntimeProvider.ts"; +import { ArtifactsPanel, ArtifactsToolRenderer } from "./tools/artifacts/index.ts"; +import { registerToolRenderer } from "./tools/renderer-registry.ts"; +import type { Attachment } from "./utils/attachment-utils.ts"; +import { i18n } from "./utils/i18n.ts"; const BREAKPOINT = 800; // px - switch between overlay and side-by-side diff --git a/packages/web-ui/src/components/AgentInterface.ts b/packages/web-ui/src/components/AgentInterface.ts index 0a536e96..672b8097 100644 --- a/packages/web-ui/src/components/AgentInterface.ts +++ b/packages/web-ui/src/components/AgentInterface.ts @@ -1,20 +1,20 @@ import { streamSimple, type ToolResultMessage, type Usage } from "@earendil-works/pi-ai"; import { html, LitElement } from "lit"; import { customElement, property, query } from "lit/decorators.js"; -import { ModelSelector } from "../dialogs/ModelSelector.js"; -import type { MessageEditor } from "./MessageEditor.js"; -import "./MessageEditor.js"; -import "./MessageList.js"; -import "./Messages.js"; // Import for side effects to register the custom elements -import { getAppStorage } from "../storage/app-storage.js"; -import "./StreamingMessageContainer.js"; +import { ModelSelector } from "../dialogs/ModelSelector.ts"; +import type { MessageEditor } from "./MessageEditor.ts"; +import "./MessageEditor.ts"; +import "./MessageList.ts"; +import "./Messages.ts"; // Import for side effects to register the custom elements +import { getAppStorage } from "../storage/app-storage.ts"; +import "./StreamingMessageContainer.ts"; import type { Agent, AgentEvent } from "@earendil-works/pi-agent-core"; -import type { Attachment } from "../utils/attachment-utils.js"; -import { formatUsage } from "../utils/format.js"; -import { i18n } from "../utils/i18n.js"; -import { createStreamFn } from "../utils/proxy-utils.js"; -import type { UserMessageWithAttachments } from "./Messages.js"; -import type { StreamingMessageContainer } from "./StreamingMessageContainer.js"; +import type { Attachment } from "../utils/attachment-utils.ts"; +import { formatUsage } from "../utils/format.ts"; +import { i18n } from "../utils/i18n.ts"; +import { createStreamFn } from "../utils/proxy-utils.ts"; +import type { UserMessageWithAttachments } from "./Messages.ts"; +import type { StreamingMessageContainer } from "./StreamingMessageContainer.ts"; @customElement("agent-interface") export class AgentInterface extends LitElement { diff --git a/packages/web-ui/src/components/AttachmentTile.ts b/packages/web-ui/src/components/AttachmentTile.ts index 823001d3..31cacf79 100644 --- a/packages/web-ui/src/components/AttachmentTile.ts +++ b/packages/web-ui/src/components/AttachmentTile.ts @@ -3,9 +3,9 @@ import { LitElement } from "lit"; import { customElement, property } from "lit/decorators.js"; import { html } from "lit/html.js"; import { FileSpreadsheet, FileText, X } from "lucide"; -import { AttachmentOverlay } from "../dialogs/AttachmentOverlay.js"; -import type { Attachment } from "../utils/attachment-utils.js"; -import { i18n } from "../utils/i18n.js"; +import { AttachmentOverlay } from "../dialogs/AttachmentOverlay.ts"; +import type { Attachment } from "../utils/attachment-utils.ts"; +import { i18n } from "../utils/i18n.ts"; @customElement("attachment-tile") export class AttachmentTile extends LitElement { diff --git a/packages/web-ui/src/components/ConsoleBlock.ts b/packages/web-ui/src/components/ConsoleBlock.ts index 66f38b0b..982f0f08 100644 --- a/packages/web-ui/src/components/ConsoleBlock.ts +++ b/packages/web-ui/src/components/ConsoleBlock.ts @@ -3,7 +3,7 @@ import { LitElement } from "lit"; import { property, state } from "lit/decorators.js"; import { html } from "lit/html.js"; import { Check, Copy } from "lucide"; -import { i18n } from "../utils/i18n.js"; +import { i18n } from "../utils/i18n.ts"; export class ConsoleBlock extends LitElement { @property() content: string = ""; diff --git a/packages/web-ui/src/components/CustomProviderCard.ts b/packages/web-ui/src/components/CustomProviderCard.ts index 1050b855..b4f9da57 100644 --- a/packages/web-ui/src/components/CustomProviderCard.ts +++ b/packages/web-ui/src/components/CustomProviderCard.ts @@ -2,7 +2,7 @@ import { i18n } from "@mariozechner/mini-lit"; import { Button } from "@mariozechner/mini-lit/dist/Button.js"; import { html, LitElement, type TemplateResult } from "lit"; import { customElement, property } from "lit/decorators.js"; -import type { CustomProvider } from "../storage/stores/custom-providers-store.js"; +import type { CustomProvider } from "../storage/stores/custom-providers-store.ts"; @customElement("custom-provider-card") export class CustomProviderCard extends LitElement { diff --git a/packages/web-ui/src/components/Input.ts b/packages/web-ui/src/components/Input.ts index 112e0813..dff36891 100644 --- a/packages/web-ui/src/components/Input.ts +++ b/packages/web-ui/src/components/Input.ts @@ -1,7 +1,7 @@ import { type BaseComponentProps, fc } from "@mariozechner/mini-lit/dist/mini.js"; import { html } from "lit"; import { type Ref, ref } from "lit/directives/ref.js"; -import { i18n } from "../utils/i18n.js"; +import { i18n } from "../utils/i18n.ts"; export type InputType = "text" | "email" | "password" | "number" | "url" | "tel" | "search"; export type InputSize = "sm" | "md" | "lg"; diff --git a/packages/web-ui/src/components/MessageEditor.ts b/packages/web-ui/src/components/MessageEditor.ts index e50d9401..d9a5124c 100644 --- a/packages/web-ui/src/components/MessageEditor.ts +++ b/packages/web-ui/src/components/MessageEditor.ts @@ -6,9 +6,9 @@ import { html, LitElement } from "lit"; import { customElement, property, state } from "lit/decorators.js"; import { createRef, ref } from "lit/directives/ref.js"; import { Brain, Loader2, Paperclip, Send, Sparkles, Square } from "lucide"; -import { type Attachment, loadAttachment } from "../utils/attachment-utils.js"; -import { i18n } from "../utils/i18n.js"; -import "./AttachmentTile.js"; +import { type Attachment, loadAttachment } from "../utils/attachment-utils.ts"; +import { i18n } from "../utils/i18n.ts"; +import "./AttachmentTile.ts"; import type { ThinkingLevel } from "@earendil-works/pi-agent-core"; @customElement("message-editor") diff --git a/packages/web-ui/src/components/MessageList.ts b/packages/web-ui/src/components/MessageList.ts index 59cc6955..77723a3b 100644 --- a/packages/web-ui/src/components/MessageList.ts +++ b/packages/web-ui/src/components/MessageList.ts @@ -6,7 +6,7 @@ import type { import { html, LitElement, type TemplateResult } from "lit"; import { property } from "lit/decorators.js"; import { repeat } from "lit/directives/repeat.js"; -import { renderMessage } from "./message-renderer-registry.js"; +import { renderMessage } from "./message-renderer-registry.ts"; export class MessageList extends LitElement { @property({ type: Array }) messages: AgentMessage[] = []; diff --git a/packages/web-ui/src/components/Messages.ts b/packages/web-ui/src/components/Messages.ts index 16237543..2809750f 100644 --- a/packages/web-ui/src/components/Messages.ts +++ b/packages/web-ui/src/components/Messages.ts @@ -8,11 +8,11 @@ import type { } from "@earendil-works/pi-ai"; import { html, LitElement, type TemplateResult } from "lit"; import { customElement, property } from "lit/decorators.js"; -import { renderTool } from "../tools/index.js"; -import type { Attachment } from "../utils/attachment-utils.js"; -import { formatUsage } from "../utils/format.js"; -import { i18n } from "../utils/i18n.js"; -import "./ThinkingBlock.js"; +import { renderTool } from "../tools/index.ts"; +import type { Attachment } from "../utils/attachment-utils.ts"; +import { formatUsage } from "../utils/format.ts"; +import { i18n } from "../utils/i18n.ts"; +import "./ThinkingBlock.ts"; import type { AgentTool } from "@earendil-works/pi-agent-core"; export type UserMessageWithAttachments = { diff --git a/packages/web-ui/src/components/ProviderKeyInput.ts b/packages/web-ui/src/components/ProviderKeyInput.ts index c43d72d0..1b74f598 100644 --- a/packages/web-ui/src/components/ProviderKeyInput.ts +++ b/packages/web-ui/src/components/ProviderKeyInput.ts @@ -4,9 +4,9 @@ import { Badge } from "@mariozechner/mini-lit/dist/Badge.js"; import { Button } from "@mariozechner/mini-lit/dist/Button.js"; import { html, LitElement } from "lit"; import { customElement, property, state } from "lit/decorators.js"; -import { getAppStorage } from "../storage/app-storage.js"; -import { applyProxyIfNeeded } from "../utils/proxy-utils.js"; -import { Input } from "./Input.js"; +import { getAppStorage } from "../storage/app-storage.ts"; +import { applyProxyIfNeeded } from "../utils/proxy-utils.ts"; +import { Input } from "./Input.ts"; // Test models for each provider const TEST_MODELS: Record = { diff --git a/packages/web-ui/src/components/SandboxedIframe.ts b/packages/web-ui/src/components/SandboxedIframe.ts index dd8f15ec..3739c5e1 100644 --- a/packages/web-ui/src/components/SandboxedIframe.ts +++ b/packages/web-ui/src/components/SandboxedIframe.ts @@ -1,9 +1,9 @@ import { LitElement } from "lit"; import { customElement, property } from "lit/decorators.js"; -import { ConsoleRuntimeProvider } from "./sandbox/ConsoleRuntimeProvider.js"; -import { RuntimeMessageBridge } from "./sandbox/RuntimeMessageBridge.js"; -import { type MessageConsumer, RUNTIME_MESSAGE_ROUTER } from "./sandbox/RuntimeMessageRouter.js"; -import type { SandboxRuntimeProvider } from "./sandbox/SandboxRuntimeProvider.js"; +import { ConsoleRuntimeProvider } from "./sandbox/ConsoleRuntimeProvider.ts"; +import { RuntimeMessageBridge } from "./sandbox/RuntimeMessageBridge.ts"; +import { type MessageConsumer, RUNTIME_MESSAGE_ROUTER } from "./sandbox/RuntimeMessageRouter.ts"; +import type { SandboxRuntimeProvider } from "./sandbox/SandboxRuntimeProvider.ts"; export interface SandboxFile { fileName: string; diff --git a/packages/web-ui/src/components/sandbox/ArtifactsRuntimeProvider.ts b/packages/web-ui/src/components/sandbox/ArtifactsRuntimeProvider.ts index 95366a18..626fe0a7 100644 --- a/packages/web-ui/src/components/sandbox/ArtifactsRuntimeProvider.ts +++ b/packages/web-ui/src/components/sandbox/ArtifactsRuntimeProvider.ts @@ -2,8 +2,8 @@ import type { AgentMessage } from "@earendil-works/pi-agent-core"; import { ARTIFACTS_RUNTIME_PROVIDER_DESCRIPTION_RO, ARTIFACTS_RUNTIME_PROVIDER_DESCRIPTION_RW, -} from "../../prompts/prompts.js"; -import type { SandboxRuntimeProvider } from "./SandboxRuntimeProvider.js"; +} from "../../prompts/prompts.ts"; +import type { SandboxRuntimeProvider } from "./SandboxRuntimeProvider.ts"; // Define minimal interface for ArtifactsPanel to avoid circular dependencies interface ArtifactsPanelLike { @@ -25,11 +25,15 @@ interface AgentLike { * Supports both online (extension) and offline (downloaded HTML) modes. */ export class ArtifactsRuntimeProvider implements SandboxRuntimeProvider { - constructor( - private artifactsPanel: ArtifactsPanelLike, - private agent?: AgentLike, - private readWrite: boolean = true, - ) {} + private artifactsPanel: ArtifactsPanelLike; + private agent?: AgentLike; + private readWrite: boolean; + + constructor(artifactsPanel: ArtifactsPanelLike, agent?: AgentLike, readWrite: boolean = true) { + this.artifactsPanel = artifactsPanel; + this.agent = agent; + this.readWrite = readWrite; + } getData(): Record { // Inject artifact snapshot for offline mode diff --git a/packages/web-ui/src/components/sandbox/AttachmentsRuntimeProvider.ts b/packages/web-ui/src/components/sandbox/AttachmentsRuntimeProvider.ts index 9c8b3d7d..b706a528 100644 --- a/packages/web-ui/src/components/sandbox/AttachmentsRuntimeProvider.ts +++ b/packages/web-ui/src/components/sandbox/AttachmentsRuntimeProvider.ts @@ -1,6 +1,6 @@ -import { ATTACHMENTS_RUNTIME_DESCRIPTION } from "../../prompts/prompts.js"; -import type { Attachment } from "../../utils/attachment-utils.js"; -import type { SandboxRuntimeProvider } from "./SandboxRuntimeProvider.js"; +import { ATTACHMENTS_RUNTIME_DESCRIPTION } from "../../prompts/prompts.ts"; +import type { Attachment } from "../../utils/attachment-utils.ts"; +import type { SandboxRuntimeProvider } from "./SandboxRuntimeProvider.ts"; /** * Attachments Runtime Provider @@ -10,7 +10,11 @@ import type { SandboxRuntimeProvider } from "./SandboxRuntimeProvider.js"; * Attachments are read-only snapshot data - no messaging needed. */ export class AttachmentsRuntimeProvider implements SandboxRuntimeProvider { - constructor(private attachments: Attachment[]) {} + private attachments: Attachment[]; + + constructor(attachments: Attachment[]) { + this.attachments = attachments; + } getData(): Record { const attachmentsData = this.attachments.map((a) => ({ diff --git a/packages/web-ui/src/components/sandbox/ConsoleRuntimeProvider.ts b/packages/web-ui/src/components/sandbox/ConsoleRuntimeProvider.ts index 326361c7..18cc3c1a 100644 --- a/packages/web-ui/src/components/sandbox/ConsoleRuntimeProvider.ts +++ b/packages/web-ui/src/components/sandbox/ConsoleRuntimeProvider.ts @@ -1,4 +1,4 @@ -import type { SandboxRuntimeProvider } from "./SandboxRuntimeProvider.js"; +import type { SandboxRuntimeProvider } from "./SandboxRuntimeProvider.ts"; export interface ConsoleLog { type: "log" | "warn" | "error" | "info"; diff --git a/packages/web-ui/src/components/sandbox/FileDownloadRuntimeProvider.ts b/packages/web-ui/src/components/sandbox/FileDownloadRuntimeProvider.ts index 824ae125..c2e1f295 100644 --- a/packages/web-ui/src/components/sandbox/FileDownloadRuntimeProvider.ts +++ b/packages/web-ui/src/components/sandbox/FileDownloadRuntimeProvider.ts @@ -1,4 +1,4 @@ -import type { SandboxRuntimeProvider } from "./SandboxRuntimeProvider.js"; +import type { SandboxRuntimeProvider } from "./SandboxRuntimeProvider.ts"; export interface DownloadableFile { fileName: string; diff --git a/packages/web-ui/src/components/sandbox/RuntimeMessageRouter.ts b/packages/web-ui/src/components/sandbox/RuntimeMessageRouter.ts index 3ab84b04..0b6c6f00 100644 --- a/packages/web-ui/src/components/sandbox/RuntimeMessageRouter.ts +++ b/packages/web-ui/src/components/sandbox/RuntimeMessageRouter.ts @@ -1,4 +1,4 @@ -import type { SandboxRuntimeProvider } from "./SandboxRuntimeProvider.js"; +import type { SandboxRuntimeProvider } from "./SandboxRuntimeProvider.ts"; // Type declaration for chrome extension API (when available) declare const chrome: any; diff --git a/packages/web-ui/src/dialogs/ApiKeyPromptDialog.ts b/packages/web-ui/src/dialogs/ApiKeyPromptDialog.ts index 7a45c561..fe7d52c4 100644 --- a/packages/web-ui/src/dialogs/ApiKeyPromptDialog.ts +++ b/packages/web-ui/src/dialogs/ApiKeyPromptDialog.ts @@ -1,10 +1,10 @@ import { customElement, state } from "lit/decorators.js"; -import "../components/ProviderKeyInput.js"; +import "../components/ProviderKeyInput.ts"; import { DialogContent, DialogHeader } from "@mariozechner/mini-lit/dist/Dialog.js"; import { DialogBase } from "@mariozechner/mini-lit/dist/DialogBase.js"; import { html } from "lit"; -import { getAppStorage } from "../storage/app-storage.js"; -import { i18n } from "../utils/i18n.js"; +import { getAppStorage } from "../storage/app-storage.ts"; +import { i18n } from "../utils/i18n.ts"; @customElement("api-key-prompt-dialog") export class ApiKeyPromptDialog extends DialogBase { diff --git a/packages/web-ui/src/dialogs/AttachmentOverlay.ts b/packages/web-ui/src/dialogs/AttachmentOverlay.ts index 4ec7734c..329a98f9 100644 --- a/packages/web-ui/src/dialogs/AttachmentOverlay.ts +++ b/packages/web-ui/src/dialogs/AttachmentOverlay.ts @@ -7,8 +7,8 @@ import { state } from "lit/decorators.js"; import { Download, X } from "lucide"; import * as pdfjsLib from "pdfjs-dist"; import * as XLSX from "xlsx"; -import type { Attachment } from "../utils/attachment-utils.js"; -import { i18n } from "../utils/i18n.js"; +import type { Attachment } from "../utils/attachment-utils.ts"; +import { i18n } from "../utils/i18n.ts"; type FileType = "image" | "pdf" | "docx" | "pptx" | "excel" | "text"; diff --git a/packages/web-ui/src/dialogs/CustomProviderDialog.ts b/packages/web-ui/src/dialogs/CustomProviderDialog.ts index 2db0ce60..c39777b2 100644 --- a/packages/web-ui/src/dialogs/CustomProviderDialog.ts +++ b/packages/web-ui/src/dialogs/CustomProviderDialog.ts @@ -7,9 +7,9 @@ import { Label } from "@mariozechner/mini-lit/dist/Label.js"; import { Select } from "@mariozechner/mini-lit/dist/Select.js"; import { html, type TemplateResult } from "lit"; import { state } from "lit/decorators.js"; -import { getAppStorage } from "../storage/app-storage.js"; -import type { CustomProvider, CustomProviderType } from "../storage/stores/custom-providers-store.js"; -import { discoverModels } from "../utils/model-discovery.js"; +import { getAppStorage } from "../storage/app-storage.ts"; +import type { CustomProvider, CustomProviderType } from "../storage/stores/custom-providers-store.ts"; +import { discoverModels } from "../utils/model-discovery.ts"; export class CustomProviderDialog extends DialogBase { private provider?: CustomProvider; diff --git a/packages/web-ui/src/dialogs/ModelSelector.ts b/packages/web-ui/src/dialogs/ModelSelector.ts index 1884765f..d23d2484 100644 --- a/packages/web-ui/src/dialogs/ModelSelector.ts +++ b/packages/web-ui/src/dialogs/ModelSelector.ts @@ -8,12 +8,12 @@ import { html, type PropertyValues, type TemplateResult } from "lit"; import { customElement, state } from "lit/decorators.js"; import { createRef, ref } from "lit/directives/ref.js"; import { Brain, Image as ImageIcon } from "lucide"; -import { Input } from "../components/Input.js"; -import { getAppStorage } from "../storage/app-storage.js"; -import type { AutoDiscoveryProviderType } from "../storage/stores/custom-providers-store.js"; -import { formatModelCost } from "../utils/format.js"; -import { i18n } from "../utils/i18n.js"; -import { discoverModels } from "../utils/model-discovery.js"; +import { Input } from "../components/Input.ts"; +import { getAppStorage } from "../storage/app-storage.ts"; +import type { AutoDiscoveryProviderType } from "../storage/stores/custom-providers-store.ts"; +import { formatModelCost } from "../utils/format.ts"; +import { i18n } from "../utils/i18n.ts"; +import { discoverModels } from "../utils/model-discovery.ts"; /** * Score a query against a text using subsequence matching. diff --git a/packages/web-ui/src/dialogs/PersistentStorageDialog.ts b/packages/web-ui/src/dialogs/PersistentStorageDialog.ts index f5c83a13..8db72764 100644 --- a/packages/web-ui/src/dialogs/PersistentStorageDialog.ts +++ b/packages/web-ui/src/dialogs/PersistentStorageDialog.ts @@ -3,7 +3,7 @@ import { DialogContent, DialogHeader } from "@mariozechner/mini-lit/dist/Dialog. import { DialogBase } from "@mariozechner/mini-lit/dist/DialogBase.js"; import { html } from "lit"; import { customElement, state } from "lit/decorators.js"; -import { i18n } from "../utils/i18n.js"; +import { i18n } from "../utils/i18n.ts"; @customElement("persistent-storage-dialog") export class PersistentStorageDialog extends DialogBase { diff --git a/packages/web-ui/src/dialogs/ProvidersModelsTab.ts b/packages/web-ui/src/dialogs/ProvidersModelsTab.ts index 440205c3..b2eee51f 100644 --- a/packages/web-ui/src/dialogs/ProvidersModelsTab.ts +++ b/packages/web-ui/src/dialogs/ProvidersModelsTab.ts @@ -3,17 +3,17 @@ import { i18n } from "@mariozechner/mini-lit"; import { Select } from "@mariozechner/mini-lit/dist/Select.js"; import { html, type TemplateResult } from "lit"; import { customElement, state } from "lit/decorators.js"; -import "../components/CustomProviderCard.js"; -import "../components/ProviderKeyInput.js"; -import { getAppStorage } from "../storage/app-storage.js"; +import "../components/CustomProviderCard.ts"; +import "../components/ProviderKeyInput.ts"; +import { getAppStorage } from "../storage/app-storage.ts"; import type { AutoDiscoveryProviderType, CustomProvider, CustomProviderType, -} from "../storage/stores/custom-providers-store.js"; -import { discoverModels } from "../utils/model-discovery.js"; -import { CustomProviderDialog } from "./CustomProviderDialog.js"; -import { SettingsTab } from "./SettingsDialog.js"; +} from "../storage/stores/custom-providers-store.ts"; +import { discoverModels } from "../utils/model-discovery.ts"; +import { CustomProviderDialog } from "./CustomProviderDialog.ts"; +import { SettingsTab } from "./SettingsDialog.ts"; @customElement("providers-models-tab") export class ProvidersModelsTab extends SettingsTab { diff --git a/packages/web-ui/src/dialogs/SessionListDialog.ts b/packages/web-ui/src/dialogs/SessionListDialog.ts index 66908d59..b3ad3108 100644 --- a/packages/web-ui/src/dialogs/SessionListDialog.ts +++ b/packages/web-ui/src/dialogs/SessionListDialog.ts @@ -2,10 +2,10 @@ import { DialogContent, DialogHeader } from "@mariozechner/mini-lit/dist/Dialog. import { DialogBase } from "@mariozechner/mini-lit/dist/DialogBase.js"; import { html } from "lit"; import { customElement, state } from "lit/decorators.js"; -import { getAppStorage } from "../storage/app-storage.js"; -import type { SessionMetadata } from "../storage/types.js"; -import { formatUsage } from "../utils/format.js"; -import { i18n } from "../utils/i18n.js"; +import { getAppStorage } from "../storage/app-storage.ts"; +import type { SessionMetadata } from "../storage/types.ts"; +import { formatUsage } from "../utils/format.ts"; +import { i18n } from "../utils/i18n.ts"; @customElement("session-list-dialog") export class SessionListDialog extends DialogBase { diff --git a/packages/web-ui/src/dialogs/SettingsDialog.ts b/packages/web-ui/src/dialogs/SettingsDialog.ts index 7f671b0a..6d914827 100644 --- a/packages/web-ui/src/dialogs/SettingsDialog.ts +++ b/packages/web-ui/src/dialogs/SettingsDialog.ts @@ -6,8 +6,8 @@ import { Label } from "@mariozechner/mini-lit/dist/Label.js"; import { Switch } from "@mariozechner/mini-lit/dist/Switch.js"; import { html, LitElement, type TemplateResult } from "lit"; import { customElement, property, state } from "lit/decorators.js"; -import "../components/ProviderKeyInput.js"; -import { getAppStorage } from "../storage/app-storage.js"; +import "../components/ProviderKeyInput.ts"; +import { getAppStorage } from "../storage/app-storage.ts"; // Base class for settings tabs export abstract class SettingsTab extends LitElement { diff --git a/packages/web-ui/src/index.ts b/packages/web-ui/src/index.ts index c808cde2..22ba992e 100644 --- a/packages/web-ui/src/index.ts +++ b/packages/web-ui/src/index.ts @@ -2,18 +2,18 @@ export type { Agent, AgentMessage, AgentState, ThinkingLevel } from "@earendil-works/pi-agent-core"; export type { Model } from "@earendil-works/pi-ai"; -export { ChatPanel } from "./ChatPanel.js"; +export { ChatPanel } from "./ChatPanel.ts"; // Components -export { AgentInterface } from "./components/AgentInterface.js"; -export { AttachmentTile } from "./components/AttachmentTile.js"; -export { ConsoleBlock } from "./components/ConsoleBlock.js"; -export { CustomProviderCard } from "./components/CustomProviderCard.js"; -export { ExpandableSection } from "./components/ExpandableSection.js"; -export { Input } from "./components/Input.js"; -export { MessageEditor } from "./components/MessageEditor.js"; -export { MessageList } from "./components/MessageList.js"; +export { AgentInterface } from "./components/AgentInterface.ts"; +export { AttachmentTile } from "./components/AttachmentTile.ts"; +export { ConsoleBlock } from "./components/ConsoleBlock.ts"; +export { CustomProviderCard } from "./components/CustomProviderCard.ts"; +export { ExpandableSection } from "./components/ExpandableSection.ts"; +export { Input } from "./components/Input.ts"; +export { MessageEditor } from "./components/MessageEditor.ts"; +export { MessageList } from "./components/MessageList.ts"; // Message components -export type { ArtifactMessage, UserMessageWithAttachments } from "./components/Messages.js"; +export type { ArtifactMessage, UserMessageWithAttachments } from "./components/Messages.ts"; export { AbortedMessage, AssistantMessage, @@ -24,7 +24,7 @@ export { ToolMessage, ToolMessageDebugView, UserMessage, -} from "./components/Messages.js"; +} from "./components/Messages.ts"; // Message renderer registry export { getMessageRenderer, @@ -32,55 +32,55 @@ export { type MessageRole, registerMessageRenderer, renderMessage, -} from "./components/message-renderer-registry.js"; -export { ProviderKeyInput } from "./components/ProviderKeyInput.js"; +} from "./components/message-renderer-registry.ts"; +export { ProviderKeyInput } from "./components/ProviderKeyInput.ts"; export { type SandboxFile, SandboxIframe, type SandboxResult, type SandboxUrlProvider, -} from "./components/SandboxedIframe.js"; -export { StreamingMessageContainer } from "./components/StreamingMessageContainer.js"; +} from "./components/SandboxedIframe.ts"; +export { StreamingMessageContainer } from "./components/StreamingMessageContainer.ts"; // Sandbox Runtime Providers -export { ArtifactsRuntimeProvider } from "./components/sandbox/ArtifactsRuntimeProvider.js"; -export { AttachmentsRuntimeProvider } from "./components/sandbox/AttachmentsRuntimeProvider.js"; -export { type ConsoleLog, ConsoleRuntimeProvider } from "./components/sandbox/ConsoleRuntimeProvider.js"; +export { ArtifactsRuntimeProvider } from "./components/sandbox/ArtifactsRuntimeProvider.ts"; +export { AttachmentsRuntimeProvider } from "./components/sandbox/AttachmentsRuntimeProvider.ts"; +export { type ConsoleLog, ConsoleRuntimeProvider } from "./components/sandbox/ConsoleRuntimeProvider.ts"; export { type DownloadableFile, FileDownloadRuntimeProvider, -} from "./components/sandbox/FileDownloadRuntimeProvider.js"; -export { RuntimeMessageBridge } from "./components/sandbox/RuntimeMessageBridge.js"; -export { RUNTIME_MESSAGE_ROUTER } from "./components/sandbox/RuntimeMessageRouter.js"; -export type { SandboxRuntimeProvider } from "./components/sandbox/SandboxRuntimeProvider.js"; -export { ThinkingBlock } from "./components/ThinkingBlock.js"; -export { ApiKeyPromptDialog } from "./dialogs/ApiKeyPromptDialog.js"; -export { AttachmentOverlay } from "./dialogs/AttachmentOverlay.js"; -export { CustomProviderDialog } from "./dialogs/CustomProviderDialog.js"; +} from "./components/sandbox/FileDownloadRuntimeProvider.ts"; +export { RuntimeMessageBridge } from "./components/sandbox/RuntimeMessageBridge.ts"; +export { RUNTIME_MESSAGE_ROUTER } from "./components/sandbox/RuntimeMessageRouter.ts"; +export type { SandboxRuntimeProvider } from "./components/sandbox/SandboxRuntimeProvider.ts"; +export { ThinkingBlock } from "./components/ThinkingBlock.ts"; +export { ApiKeyPromptDialog } from "./dialogs/ApiKeyPromptDialog.ts"; +export { AttachmentOverlay } from "./dialogs/AttachmentOverlay.ts"; +export { CustomProviderDialog } from "./dialogs/CustomProviderDialog.ts"; // Dialogs -export { ModelSelector } from "./dialogs/ModelSelector.js"; -export { PersistentStorageDialog } from "./dialogs/PersistentStorageDialog.js"; -export { ProvidersModelsTab } from "./dialogs/ProvidersModelsTab.js"; -export { SessionListDialog } from "./dialogs/SessionListDialog.js"; -export { ApiKeysTab, ProxyTab, SettingsDialog, SettingsTab } from "./dialogs/SettingsDialog.js"; +export { ModelSelector } from "./dialogs/ModelSelector.ts"; +export { PersistentStorageDialog } from "./dialogs/PersistentStorageDialog.ts"; +export { ProvidersModelsTab } from "./dialogs/ProvidersModelsTab.ts"; +export { SessionListDialog } from "./dialogs/SessionListDialog.ts"; +export { ApiKeysTab, ProxyTab, SettingsDialog, SettingsTab } from "./dialogs/SettingsDialog.ts"; // Prompts export { ARTIFACTS_RUNTIME_PROVIDER_DESCRIPTION_RO, ARTIFACTS_RUNTIME_PROVIDER_DESCRIPTION_RW, ATTACHMENTS_RUNTIME_DESCRIPTION, -} from "./prompts/prompts.js"; +} from "./prompts/prompts.ts"; // Storage -export { AppStorage, getAppStorage, setAppStorage } from "./storage/app-storage.js"; -export { IndexedDBStorageBackend } from "./storage/backends/indexeddb-storage-backend.js"; -export { Store } from "./storage/store.js"; +export { AppStorage, getAppStorage, setAppStorage } from "./storage/app-storage.ts"; +export { IndexedDBStorageBackend } from "./storage/backends/indexeddb-storage-backend.ts"; +export { Store } from "./storage/store.ts"; export type { AutoDiscoveryProviderType, CustomProvider, CustomProviderType, -} from "./storage/stores/custom-providers-store.js"; -export { CustomProvidersStore } from "./storage/stores/custom-providers-store.js"; -export { ProviderKeysStore } from "./storage/stores/provider-keys-store.js"; -export { SessionsStore } from "./storage/stores/sessions-store.js"; -export { SettingsStore } from "./storage/stores/settings-store.js"; +} from "./storage/stores/custom-providers-store.ts"; +export { CustomProvidersStore } from "./storage/stores/custom-providers-store.ts"; +export { ProviderKeysStore } from "./storage/stores/provider-keys-store.ts"; +export { SessionsStore } from "./storage/stores/sessions-store.ts"; +export { SettingsStore } from "./storage/stores/settings-store.ts"; export type { IndexConfig, IndexedDBConfig, @@ -89,32 +89,32 @@ export type { StorageBackend, StorageTransaction, StoreConfig, -} from "./storage/types.js"; +} from "./storage/types.ts"; // Artifacts -export { ArtifactElement } from "./tools/artifacts/ArtifactElement.js"; -export { ArtifactPill } from "./tools/artifacts/ArtifactPill.js"; -export { type Artifact, ArtifactsPanel, type ArtifactsParams } from "./tools/artifacts/artifacts.js"; -export { ArtifactsToolRenderer } from "./tools/artifacts/artifacts-tool-renderer.js"; -export { HtmlArtifact } from "./tools/artifacts/HtmlArtifact.js"; -export { ImageArtifact } from "./tools/artifacts/ImageArtifact.js"; -export { MarkdownArtifact } from "./tools/artifacts/MarkdownArtifact.js"; -export { SvgArtifact } from "./tools/artifacts/SvgArtifact.js"; -export { TextArtifact } from "./tools/artifacts/TextArtifact.js"; -export { createExtractDocumentTool, extractDocumentTool } from "./tools/extract-document.js"; +export { ArtifactElement } from "./tools/artifacts/ArtifactElement.ts"; +export { ArtifactPill } from "./tools/artifacts/ArtifactPill.ts"; +export { type Artifact, ArtifactsPanel, type ArtifactsParams } from "./tools/artifacts/artifacts.ts"; +export { ArtifactsToolRenderer } from "./tools/artifacts/artifacts-tool-renderer.ts"; +export { HtmlArtifact } from "./tools/artifacts/HtmlArtifact.ts"; +export { ImageArtifact } from "./tools/artifacts/ImageArtifact.ts"; +export { MarkdownArtifact } from "./tools/artifacts/MarkdownArtifact.ts"; +export { SvgArtifact } from "./tools/artifacts/SvgArtifact.ts"; +export { TextArtifact } from "./tools/artifacts/TextArtifact.ts"; +export { createExtractDocumentTool, extractDocumentTool } from "./tools/extract-document.ts"; // Tools -export { getToolRenderer, registerToolRenderer, renderTool, setShowJsonMode } from "./tools/index.js"; -export { createJavaScriptReplTool, javascriptReplTool } from "./tools/javascript-repl.js"; -export { renderCollapsibleHeader, renderHeader } from "./tools/renderer-registry.js"; -export { BashRenderer } from "./tools/renderers/BashRenderer.js"; -export { CalculateRenderer } from "./tools/renderers/CalculateRenderer.js"; +export { getToolRenderer, registerToolRenderer, renderTool, setShowJsonMode } from "./tools/index.ts"; +export { createJavaScriptReplTool, javascriptReplTool } from "./tools/javascript-repl.ts"; +export { renderCollapsibleHeader, renderHeader } from "./tools/renderer-registry.ts"; +export { BashRenderer } from "./tools/renderers/BashRenderer.ts"; +export { CalculateRenderer } from "./tools/renderers/CalculateRenderer.ts"; // Tool renderers -export { DefaultRenderer } from "./tools/renderers/DefaultRenderer.js"; -export { GetCurrentTimeRenderer } from "./tools/renderers/GetCurrentTimeRenderer.js"; -export type { ToolRenderer, ToolRenderResult } from "./tools/types.js"; -export type { Attachment } from "./utils/attachment-utils.js"; +export { DefaultRenderer } from "./tools/renderers/DefaultRenderer.ts"; +export { GetCurrentTimeRenderer } from "./tools/renderers/GetCurrentTimeRenderer.ts"; +export type { ToolRenderer, ToolRenderResult } from "./tools/types.ts"; +export type { Attachment } from "./utils/attachment-utils.ts"; // Utils -export { loadAttachment } from "./utils/attachment-utils.js"; -export { clearAuthToken, getAuthToken } from "./utils/auth-token.js"; -export { formatCost, formatModelCost, formatTokenCount, formatUsage } from "./utils/format.js"; -export { i18n, setLanguage, translations } from "./utils/i18n.js"; -export { applyProxyIfNeeded, createStreamFn, isCorsError, shouldUseProxyForProvider } from "./utils/proxy-utils.js"; +export { loadAttachment } from "./utils/attachment-utils.ts"; +export { clearAuthToken, getAuthToken } from "./utils/auth-token.ts"; +export { formatCost, formatModelCost, formatTokenCount, formatUsage } from "./utils/format.ts"; +export { i18n, setLanguage, translations } from "./utils/i18n.ts"; +export { applyProxyIfNeeded, createStreamFn, isCorsError, shouldUseProxyForProvider } from "./utils/proxy-utils.ts"; diff --git a/packages/web-ui/src/storage/app-storage.ts b/packages/web-ui/src/storage/app-storage.ts index 420104d1..cda5d3ef 100644 --- a/packages/web-ui/src/storage/app-storage.ts +++ b/packages/web-ui/src/storage/app-storage.ts @@ -1,8 +1,8 @@ -import type { CustomProvidersStore } from "./stores/custom-providers-store.js"; -import type { ProviderKeysStore } from "./stores/provider-keys-store.js"; -import type { SessionsStore } from "./stores/sessions-store.js"; -import type { SettingsStore } from "./stores/settings-store.js"; -import type { StorageBackend } from "./types.js"; +import type { CustomProvidersStore } from "./stores/custom-providers-store.ts"; +import type { ProviderKeysStore } from "./stores/provider-keys-store.ts"; +import type { SessionsStore } from "./stores/sessions-store.ts"; +import type { SettingsStore } from "./stores/settings-store.ts"; +import type { StorageBackend } from "./types.ts"; /** * High-level storage API providing access to all storage operations. diff --git a/packages/web-ui/src/storage/backends/indexeddb-storage-backend.ts b/packages/web-ui/src/storage/backends/indexeddb-storage-backend.ts index 619c41d9..0950db11 100644 --- a/packages/web-ui/src/storage/backends/indexeddb-storage-backend.ts +++ b/packages/web-ui/src/storage/backends/indexeddb-storage-backend.ts @@ -1,4 +1,4 @@ -import type { IndexedDBConfig, StorageBackend, StorageTransaction } from "../types.js"; +import type { IndexedDBConfig, StorageBackend, StorageTransaction } from "../types.ts"; /** * IndexedDB implementation of StorageBackend. @@ -6,8 +6,11 @@ import type { IndexedDBConfig, StorageBackend, StorageTransaction } from "../typ */ export class IndexedDBStorageBackend implements StorageBackend { private dbPromise: Promise | null = null; + private config: IndexedDBConfig; - constructor(private config: IndexedDBConfig) {} + constructor(config: IndexedDBConfig) { + this.config = config; + } private async getDB(): Promise { if (!this.dbPromise) { diff --git a/packages/web-ui/src/storage/store.ts b/packages/web-ui/src/storage/store.ts index b5ea8b08..60bb4563 100644 --- a/packages/web-ui/src/storage/store.ts +++ b/packages/web-ui/src/storage/store.ts @@ -1,4 +1,4 @@ -import type { StorageBackend, StoreConfig } from "./types.js"; +import type { StorageBackend, StoreConfig } from "./types.ts"; /** * Base class for all storage stores. diff --git a/packages/web-ui/src/storage/stores/custom-providers-store.ts b/packages/web-ui/src/storage/stores/custom-providers-store.ts index 5a686b62..de02f23e 100644 --- a/packages/web-ui/src/storage/stores/custom-providers-store.ts +++ b/packages/web-ui/src/storage/stores/custom-providers-store.ts @@ -1,6 +1,6 @@ import type { Model } from "@earendil-works/pi-ai"; -import { Store } from "../store.js"; -import type { StoreConfig } from "../types.js"; +import { Store } from "../store.ts"; +import type { StoreConfig } from "../types.ts"; export type AutoDiscoveryProviderType = "ollama" | "llama.cpp" | "vllm" | "lmstudio"; diff --git a/packages/web-ui/src/storage/stores/provider-keys-store.ts b/packages/web-ui/src/storage/stores/provider-keys-store.ts index 41cf8885..4ac1769c 100644 --- a/packages/web-ui/src/storage/stores/provider-keys-store.ts +++ b/packages/web-ui/src/storage/stores/provider-keys-store.ts @@ -1,5 +1,5 @@ -import { Store } from "../store.js"; -import type { StoreConfig } from "../types.js"; +import { Store } from "../store.ts"; +import type { StoreConfig } from "../types.ts"; /** * Store for LLM provider API keys (Anthropic, OpenAI, etc.). diff --git a/packages/web-ui/src/storage/stores/sessions-store.ts b/packages/web-ui/src/storage/stores/sessions-store.ts index d244f955..3fc40066 100644 --- a/packages/web-ui/src/storage/stores/sessions-store.ts +++ b/packages/web-ui/src/storage/stores/sessions-store.ts @@ -1,6 +1,6 @@ import type { AgentState } from "@earendil-works/pi-agent-core"; -import { Store } from "../store.js"; -import type { SessionData, SessionMetadata, StoreConfig } from "../types.js"; +import { Store } from "../store.ts"; +import type { SessionData, SessionMetadata, StoreConfig } from "../types.ts"; /** * Store for chat sessions (data and metadata). diff --git a/packages/web-ui/src/storage/stores/settings-store.ts b/packages/web-ui/src/storage/stores/settings-store.ts index f2f3181e..38d4f432 100644 --- a/packages/web-ui/src/storage/stores/settings-store.ts +++ b/packages/web-ui/src/storage/stores/settings-store.ts @@ -1,5 +1,5 @@ -import { Store } from "../store.js"; -import type { StoreConfig } from "../types.js"; +import { Store } from "../store.ts"; +import type { StoreConfig } from "../types.ts"; /** * Store for application settings (theme, proxy config, etc.). diff --git a/packages/web-ui/src/tools/artifacts/ArtifactPill.ts b/packages/web-ui/src/tools/artifacts/ArtifactPill.ts index 13078fcd..28292771 100644 --- a/packages/web-ui/src/tools/artifacts/ArtifactPill.ts +++ b/packages/web-ui/src/tools/artifacts/ArtifactPill.ts @@ -1,7 +1,7 @@ import { icon } from "@mariozechner/mini-lit"; import { html, type TemplateResult } from "lit"; import { FileCode2 } from "lucide"; -import type { ArtifactsPanel } from "./artifacts.js"; +import type { ArtifactsPanel } from "./artifacts.ts"; export function ArtifactPill(filename: string, artifactsPanel?: ArtifactsPanel): TemplateResult { const handleClick = (e: Event) => { diff --git a/packages/web-ui/src/tools/artifacts/Console.ts b/packages/web-ui/src/tools/artifacts/Console.ts index 287ed043..809c7728 100644 --- a/packages/web-ui/src/tools/artifacts/Console.ts +++ b/packages/web-ui/src/tools/artifacts/Console.ts @@ -5,7 +5,7 @@ import { customElement, property, state } from "lit/decorators.js"; import { createRef, type Ref, ref } from "lit/directives/ref.js"; import { repeat } from "lit/directives/repeat.js"; import { ChevronDown, ChevronRight, ChevronsDown, Lock } from "lucide"; -import { i18n } from "../../utils/i18n.js"; +import { i18n } from "../../utils/i18n.ts"; interface LogEntry { type: "log" | "error"; diff --git a/packages/web-ui/src/tools/artifacts/DocxArtifact.ts b/packages/web-ui/src/tools/artifacts/DocxArtifact.ts index 0d9e22c4..97f52c7d 100644 --- a/packages/web-ui/src/tools/artifacts/DocxArtifact.ts +++ b/packages/web-ui/src/tools/artifacts/DocxArtifact.ts @@ -2,8 +2,8 @@ import { DownloadButton } from "@mariozechner/mini-lit/dist/DownloadButton.js"; import { renderAsync } from "docx-preview"; import { html, type TemplateResult } from "lit"; import { customElement, property, state } from "lit/decorators.js"; -import { i18n } from "../../utils/i18n.js"; -import { ArtifactElement } from "./ArtifactElement.js"; +import { i18n } from "../../utils/i18n.ts"; +import { ArtifactElement } from "./ArtifactElement.ts"; @customElement("docx-artifact") export class DocxArtifact extends ArtifactElement { diff --git a/packages/web-ui/src/tools/artifacts/ExcelArtifact.ts b/packages/web-ui/src/tools/artifacts/ExcelArtifact.ts index dd6d41d7..cd488d7f 100644 --- a/packages/web-ui/src/tools/artifacts/ExcelArtifact.ts +++ b/packages/web-ui/src/tools/artifacts/ExcelArtifact.ts @@ -2,8 +2,8 @@ import { DownloadButton } from "@mariozechner/mini-lit/dist/DownloadButton.js"; import { html, type TemplateResult } from "lit"; import { customElement, property, state } from "lit/decorators.js"; import * as XLSX from "xlsx"; -import { i18n } from "../../utils/i18n.js"; -import { ArtifactElement } from "./ArtifactElement.js"; +import { i18n } from "../../utils/i18n.ts"; +import { ArtifactElement } from "./ArtifactElement.ts"; @customElement("excel-artifact") export class ExcelArtifact extends ArtifactElement { diff --git a/packages/web-ui/src/tools/artifacts/GenericArtifact.ts b/packages/web-ui/src/tools/artifacts/GenericArtifact.ts index 597615e5..43c8939c 100644 --- a/packages/web-ui/src/tools/artifacts/GenericArtifact.ts +++ b/packages/web-ui/src/tools/artifacts/GenericArtifact.ts @@ -1,8 +1,8 @@ import { DownloadButton } from "@mariozechner/mini-lit/dist/DownloadButton.js"; import { html, type TemplateResult } from "lit"; import { customElement, property } from "lit/decorators.js"; -import { i18n } from "../../utils/i18n.js"; -import { ArtifactElement } from "./ArtifactElement.js"; +import { i18n } from "../../utils/i18n.ts"; +import { ArtifactElement } from "./ArtifactElement.ts"; @customElement("generic-artifact") export class GenericArtifact extends ArtifactElement { diff --git a/packages/web-ui/src/tools/artifacts/HtmlArtifact.ts b/packages/web-ui/src/tools/artifacts/HtmlArtifact.ts index f4bf36d7..ca13cb2e 100644 --- a/packages/web-ui/src/tools/artifacts/HtmlArtifact.ts +++ b/packages/web-ui/src/tools/artifacts/HtmlArtifact.ts @@ -4,14 +4,14 @@ import { customElement, property, state } from "lit/decorators.js"; import { createRef, type Ref, ref } from "lit/directives/ref.js"; import { unsafeHTML } from "lit/directives/unsafe-html.js"; import { RefreshCw } from "lucide"; -import type { SandboxIframe } from "../../components/SandboxedIframe.js"; -import { type MessageConsumer, RUNTIME_MESSAGE_ROUTER } from "../../components/sandbox/RuntimeMessageRouter.js"; -import type { SandboxRuntimeProvider } from "../../components/sandbox/SandboxRuntimeProvider.js"; -import { i18n } from "../../utils/i18n.js"; -import "../../components/SandboxedIframe.js"; -import { ArtifactElement } from "./ArtifactElement.js"; -import type { Console } from "./Console.js"; -import "./Console.js"; +import type { SandboxIframe } from "../../components/SandboxedIframe.ts"; +import { type MessageConsumer, RUNTIME_MESSAGE_ROUTER } from "../../components/sandbox/RuntimeMessageRouter.ts"; +import type { SandboxRuntimeProvider } from "../../components/sandbox/SandboxRuntimeProvider.ts"; +import { i18n } from "../../utils/i18n.ts"; +import "../../components/SandboxedIframe.ts"; +import { ArtifactElement } from "./ArtifactElement.ts"; +import type { Console } from "./Console.ts"; +import "./Console.ts"; import { icon } from "@mariozechner/mini-lit"; import { Button } from "@mariozechner/mini-lit/dist/Button.js"; import { CopyButton } from "@mariozechner/mini-lit/dist/CopyButton.js"; diff --git a/packages/web-ui/src/tools/artifacts/ImageArtifact.ts b/packages/web-ui/src/tools/artifacts/ImageArtifact.ts index cc7b5f52..a7a1cdac 100644 --- a/packages/web-ui/src/tools/artifacts/ImageArtifact.ts +++ b/packages/web-ui/src/tools/artifacts/ImageArtifact.ts @@ -1,8 +1,8 @@ import { DownloadButton } from "@mariozechner/mini-lit/dist/DownloadButton.js"; import { html, type TemplateResult } from "lit"; import { customElement, property } from "lit/decorators.js"; -import { i18n } from "../../utils/i18n.js"; -import { ArtifactElement } from "./ArtifactElement.js"; +import { i18n } from "../../utils/i18n.ts"; +import { ArtifactElement } from "./ArtifactElement.ts"; @customElement("image-artifact") export class ImageArtifact extends ArtifactElement { diff --git a/packages/web-ui/src/tools/artifacts/MarkdownArtifact.ts b/packages/web-ui/src/tools/artifacts/MarkdownArtifact.ts index 7f763b31..5a68168c 100644 --- a/packages/web-ui/src/tools/artifacts/MarkdownArtifact.ts +++ b/packages/web-ui/src/tools/artifacts/MarkdownArtifact.ts @@ -2,12 +2,12 @@ import hljs from "highlight.js"; import { html } from "lit"; import { customElement, property, state } from "lit/decorators.js"; import { unsafeHTML } from "lit/directives/unsafe-html.js"; -import { i18n } from "../../utils/i18n.js"; +import { i18n } from "../../utils/i18n.ts"; import "@mariozechner/mini-lit/dist/MarkdownBlock.js"; import { CopyButton } from "@mariozechner/mini-lit/dist/CopyButton.js"; import { DownloadButton } from "@mariozechner/mini-lit/dist/DownloadButton.js"; import { PreviewCodeToggle } from "@mariozechner/mini-lit/dist/PreviewCodeToggle.js"; -import { ArtifactElement } from "./ArtifactElement.js"; +import { ArtifactElement } from "./ArtifactElement.ts"; @customElement("markdown-artifact") export class MarkdownArtifact extends ArtifactElement { diff --git a/packages/web-ui/src/tools/artifacts/PdfArtifact.ts b/packages/web-ui/src/tools/artifacts/PdfArtifact.ts index e139c2a4..ffa5efe2 100644 --- a/packages/web-ui/src/tools/artifacts/PdfArtifact.ts +++ b/packages/web-ui/src/tools/artifacts/PdfArtifact.ts @@ -2,8 +2,8 @@ import { DownloadButton } from "@mariozechner/mini-lit/dist/DownloadButton.js"; import { html, type TemplateResult } from "lit"; import { customElement, property, state } from "lit/decorators.js"; import * as pdfjsLib from "pdfjs-dist"; -import { i18n } from "../../utils/i18n.js"; -import { ArtifactElement } from "./ArtifactElement.js"; +import { i18n } from "../../utils/i18n.ts"; +import { ArtifactElement } from "./ArtifactElement.ts"; // Configure PDF.js worker pdfjsLib.GlobalWorkerOptions.workerSrc = new URL("pdfjs-dist/build/pdf.worker.min.mjs", import.meta.url).toString(); diff --git a/packages/web-ui/src/tools/artifacts/SvgArtifact.ts b/packages/web-ui/src/tools/artifacts/SvgArtifact.ts index fe9f398f..2a736597 100644 --- a/packages/web-ui/src/tools/artifacts/SvgArtifact.ts +++ b/packages/web-ui/src/tools/artifacts/SvgArtifact.ts @@ -5,8 +5,8 @@ import hljs from "highlight.js"; import { html } from "lit"; import { customElement, property, state } from "lit/decorators.js"; import { unsafeHTML } from "lit/directives/unsafe-html.js"; -import { i18n } from "../../utils/i18n.js"; -import { ArtifactElement } from "./ArtifactElement.js"; +import { i18n } from "../../utils/i18n.ts"; +import { ArtifactElement } from "./ArtifactElement.ts"; @customElement("svg-artifact") export class SvgArtifact extends ArtifactElement { diff --git a/packages/web-ui/src/tools/artifacts/TextArtifact.ts b/packages/web-ui/src/tools/artifacts/TextArtifact.ts index 853267ee..9110e396 100644 --- a/packages/web-ui/src/tools/artifacts/TextArtifact.ts +++ b/packages/web-ui/src/tools/artifacts/TextArtifact.ts @@ -4,8 +4,8 @@ import hljs from "highlight.js"; import { html } from "lit"; import { customElement, property } from "lit/decorators.js"; import { unsafeHTML } from "lit/directives/unsafe-html.js"; -import { i18n } from "../../utils/i18n.js"; -import { ArtifactElement } from "./ArtifactElement.js"; +import { i18n } from "../../utils/i18n.ts"; +import { ArtifactElement } from "./ArtifactElement.ts"; // Known code file extensions for highlighting const CODE_EXTENSIONS = [ diff --git a/packages/web-ui/src/tools/artifacts/artifacts-tool-renderer.ts b/packages/web-ui/src/tools/artifacts/artifacts-tool-renderer.ts index 6767dcce..c992a79e 100644 --- a/packages/web-ui/src/tools/artifacts/artifacts-tool-renderer.ts +++ b/packages/web-ui/src/tools/artifacts/artifacts-tool-renderer.ts @@ -2,14 +2,14 @@ import "@mariozechner/mini-lit/dist/CodeBlock.js"; import type { ToolResultMessage } from "@earendil-works/pi-ai"; import { createRef, ref } from "lit/directives/ref.js"; import { FileCode2 } from "lucide"; -import "../../components/ConsoleBlock.js"; +import "../../components/ConsoleBlock.ts"; import { Diff } from "@mariozechner/mini-lit/dist/Diff.js"; import { html, type TemplateResult } from "lit"; -import { i18n } from "../../utils/i18n.js"; -import { renderCollapsibleHeader, renderHeader } from "../renderer-registry.js"; -import type { ToolRenderer, ToolRenderResult } from "../types.js"; -import { ArtifactPill } from "./ArtifactPill.js"; -import type { ArtifactsPanel, ArtifactsParams } from "./artifacts.js"; +import { i18n } from "../../utils/i18n.ts"; +import { renderCollapsibleHeader, renderHeader } from "../renderer-registry.ts"; +import type { ToolRenderer, ToolRenderResult } from "../types.ts"; +import { ArtifactPill } from "./ArtifactPill.ts"; +import type { ArtifactsPanel, ArtifactsParams } from "./artifacts.ts"; // Helper to extract text from content blocks function getTextOutput(result: ToolResultMessage | undefined): string { @@ -60,7 +60,11 @@ function getLanguageFromFilename(filename?: string): string { } export class ArtifactsToolRenderer implements ToolRenderer { - constructor(public artifactsPanel?: ArtifactsPanel) {} + artifactsPanel?: ArtifactsPanel; + + constructor(artifactsPanel?: ArtifactsPanel) { + this.artifactsPanel = artifactsPanel; + } render( params: ArtifactsParams | undefined, diff --git a/packages/web-ui/src/tools/artifacts/artifacts.ts b/packages/web-ui/src/tools/artifacts/artifacts.ts index 7f679eab..1f74a5fb 100644 --- a/packages/web-ui/src/tools/artifacts/artifacts.ts +++ b/packages/web-ui/src/tools/artifacts/artifacts.ts @@ -8,27 +8,27 @@ import { customElement, property, state } from "lit/decorators.js"; import { createRef, type Ref, ref } from "lit/directives/ref.js"; import { X } from "lucide"; import { type Static, Type } from "typebox"; -import type { ArtifactMessage } from "../../components/Messages.js"; -import { ArtifactsRuntimeProvider } from "../../components/sandbox/ArtifactsRuntimeProvider.js"; -import { AttachmentsRuntimeProvider } from "../../components/sandbox/AttachmentsRuntimeProvider.js"; -import type { SandboxRuntimeProvider } from "../../components/sandbox/SandboxRuntimeProvider.js"; +import type { ArtifactMessage } from "../../components/Messages.ts"; +import { ArtifactsRuntimeProvider } from "../../components/sandbox/ArtifactsRuntimeProvider.ts"; +import { AttachmentsRuntimeProvider } from "../../components/sandbox/AttachmentsRuntimeProvider.ts"; +import type { SandboxRuntimeProvider } from "../../components/sandbox/SandboxRuntimeProvider.ts"; import { ARTIFACTS_RUNTIME_PROVIDER_DESCRIPTION_RO, ARTIFACTS_TOOL_DESCRIPTION, ATTACHMENTS_RUNTIME_DESCRIPTION, -} from "../../prompts/prompts.js"; -import type { Attachment } from "../../utils/attachment-utils.js"; -import { i18n } from "../../utils/i18n.js"; -import type { ArtifactElement } from "./ArtifactElement.js"; -import { DocxArtifact } from "./DocxArtifact.js"; -import { ExcelArtifact } from "./ExcelArtifact.js"; -import { GenericArtifact } from "./GenericArtifact.js"; -import { HtmlArtifact } from "./HtmlArtifact.js"; -import { ImageArtifact } from "./ImageArtifact.js"; -import { MarkdownArtifact } from "./MarkdownArtifact.js"; -import { PdfArtifact } from "./PdfArtifact.js"; -import { SvgArtifact } from "./SvgArtifact.js"; -import { TextArtifact } from "./TextArtifact.js"; +} from "../../prompts/prompts.ts"; +import type { Attachment } from "../../utils/attachment-utils.ts"; +import { i18n } from "../../utils/i18n.ts"; +import type { ArtifactElement } from "./ArtifactElement.ts"; +import { DocxArtifact } from "./DocxArtifact.ts"; +import { ExcelArtifact } from "./ExcelArtifact.ts"; +import { GenericArtifact } from "./GenericArtifact.ts"; +import { HtmlArtifact } from "./HtmlArtifact.ts"; +import { ImageArtifact } from "./ImageArtifact.ts"; +import { MarkdownArtifact } from "./MarkdownArtifact.ts"; +import { PdfArtifact } from "./PdfArtifact.ts"; +import { SvgArtifact } from "./SvgArtifact.ts"; +import { TextArtifact } from "./TextArtifact.ts"; // Simple artifact model export interface Artifact { diff --git a/packages/web-ui/src/tools/artifacts/index.ts b/packages/web-ui/src/tools/artifacts/index.ts index e45580a1..685022a6 100644 --- a/packages/web-ui/src/tools/artifacts/index.ts +++ b/packages/web-ui/src/tools/artifacts/index.ts @@ -1,7 +1,7 @@ -export { ArtifactElement } from "./ArtifactElement.js"; -export { type Artifact, ArtifactsPanel, type ArtifactsParams } from "./artifacts.js"; -export { ArtifactsToolRenderer } from "./artifacts-tool-renderer.js"; -export { HtmlArtifact } from "./HtmlArtifact.js"; -export { MarkdownArtifact } from "./MarkdownArtifact.js"; -export { SvgArtifact } from "./SvgArtifact.js"; -export { TextArtifact } from "./TextArtifact.js"; +export { ArtifactElement } from "./ArtifactElement.ts"; +export { type Artifact, ArtifactsPanel, type ArtifactsParams } from "./artifacts.ts"; +export { ArtifactsToolRenderer } from "./artifacts-tool-renderer.ts"; +export { HtmlArtifact } from "./HtmlArtifact.ts"; +export { MarkdownArtifact } from "./MarkdownArtifact.ts"; +export { SvgArtifact } from "./SvgArtifact.ts"; +export { TextArtifact } from "./TextArtifact.ts"; diff --git a/packages/web-ui/src/tools/extract-document.ts b/packages/web-ui/src/tools/extract-document.ts index 77f60238..a710fee9 100644 --- a/packages/web-ui/src/tools/extract-document.ts +++ b/packages/web-ui/src/tools/extract-document.ts @@ -4,11 +4,11 @@ import { html } from "lit"; import { createRef, ref } from "lit/directives/ref.js"; import { FileText } from "lucide"; import { type Static, Type } from "typebox"; -import { EXTRACT_DOCUMENT_DESCRIPTION } from "../prompts/prompts.js"; -import { loadAttachment } from "../utils/attachment-utils.js"; -import { isCorsError } from "../utils/proxy-utils.js"; -import { registerToolRenderer, renderCollapsibleHeader, renderHeader } from "./renderer-registry.js"; -import type { ToolRenderer, ToolRenderResult } from "./types.js"; +import { EXTRACT_DOCUMENT_DESCRIPTION } from "../prompts/prompts.ts"; +import { loadAttachment } from "../utils/attachment-utils.ts"; +import { isCorsError } from "../utils/proxy-utils.ts"; +import { registerToolRenderer, renderCollapsibleHeader, renderHeader } from "./renderer-registry.ts"; +import type { ToolRenderer, ToolRenderResult } from "./types.ts"; // ============================================================================ // TYPES diff --git a/packages/web-ui/src/tools/index.ts b/packages/web-ui/src/tools/index.ts index 762e6232..4be3e1ea 100644 --- a/packages/web-ui/src/tools/index.ts +++ b/packages/web-ui/src/tools/index.ts @@ -1,10 +1,10 @@ import type { ToolResultMessage } from "@earendil-works/pi-ai"; -import "./javascript-repl.js"; // Auto-registers the renderer -import "./extract-document.js"; // Auto-registers the renderer -import { getToolRenderer, registerToolRenderer } from "./renderer-registry.js"; -import { BashRenderer } from "./renderers/BashRenderer.js"; -import { DefaultRenderer } from "./renderers/DefaultRenderer.js"; -import type { ToolRenderResult } from "./types.js"; +import "./javascript-repl.ts"; // Auto-registers the renderer +import "./extract-document.ts"; // Auto-registers the renderer +import { getToolRenderer, registerToolRenderer } from "./renderer-registry.ts"; +import { BashRenderer } from "./renderers/BashRenderer.ts"; +import { DefaultRenderer } from "./renderers/DefaultRenderer.ts"; +import type { ToolRenderResult } from "./types.ts"; // Register all built-in tool renderers registerToolRenderer("bash", new BashRenderer()); diff --git a/packages/web-ui/src/tools/javascript-repl.ts b/packages/web-ui/src/tools/javascript-repl.ts index 1ed4125f..612198b2 100644 --- a/packages/web-ui/src/tools/javascript-repl.ts +++ b/packages/web-ui/src/tools/javascript-repl.ts @@ -5,12 +5,12 @@ import { html } from "lit"; import { createRef, ref } from "lit/directives/ref.js"; import { Code } from "lucide"; import { type Static, Type } from "typebox"; -import { type SandboxFile, SandboxIframe, type SandboxResult } from "../components/SandboxedIframe.js"; -import type { SandboxRuntimeProvider } from "../components/sandbox/SandboxRuntimeProvider.js"; -import { JAVASCRIPT_REPL_TOOL_DESCRIPTION } from "../prompts/prompts.js"; -import type { Attachment } from "../utils/attachment-utils.js"; -import { registerToolRenderer, renderCollapsibleHeader, renderHeader } from "./renderer-registry.js"; -import type { ToolRenderer, ToolRenderResult } from "./types.js"; +import { type SandboxFile, SandboxIframe, type SandboxResult } from "../components/SandboxedIframe.ts"; +import type { SandboxRuntimeProvider } from "../components/sandbox/SandboxRuntimeProvider.ts"; +import { JAVASCRIPT_REPL_TOOL_DESCRIPTION } from "../prompts/prompts.ts"; +import type { Attachment } from "../utils/attachment-utils.ts"; +import { registerToolRenderer, renderCollapsibleHeader, renderHeader } from "./renderer-registry.ts"; +import type { ToolRenderer, ToolRenderResult } from "./types.ts"; // Execute JavaScript code with attachments using SandboxedIframe export async function executeJavaScript( diff --git a/packages/web-ui/src/tools/renderer-registry.ts b/packages/web-ui/src/tools/renderer-registry.ts index 0d19781d..69660c6a 100644 --- a/packages/web-ui/src/tools/renderer-registry.ts +++ b/packages/web-ui/src/tools/renderer-registry.ts @@ -3,7 +3,7 @@ import { html, type TemplateResult } from "lit"; import type { Ref } from "lit/directives/ref.js"; import { ref } from "lit/directives/ref.js"; import { ChevronsUpDown, ChevronUp, Loader } from "lucide"; -import type { ToolRenderer } from "./types.js"; +import type { ToolRenderer } from "./types.ts"; // Registry of tool renderers export const toolRenderers = new Map(); diff --git a/packages/web-ui/src/tools/renderers/BashRenderer.ts b/packages/web-ui/src/tools/renderers/BashRenderer.ts index 97633d7f..6f995f37 100644 --- a/packages/web-ui/src/tools/renderers/BashRenderer.ts +++ b/packages/web-ui/src/tools/renderers/BashRenderer.ts @@ -1,9 +1,9 @@ import type { ToolResultMessage } from "@earendil-works/pi-ai"; import { html } from "lit"; import { SquareTerminal } from "lucide"; -import { i18n } from "../../utils/i18n.js"; -import { renderHeader } from "../renderer-registry.js"; -import type { ToolRenderer, ToolRenderResult } from "../types.js"; +import { i18n } from "../../utils/i18n.ts"; +import { renderHeader } from "../renderer-registry.ts"; +import type { ToolRenderer, ToolRenderResult } from "../types.ts"; interface BashParams { command: string; diff --git a/packages/web-ui/src/tools/renderers/CalculateRenderer.ts b/packages/web-ui/src/tools/renderers/CalculateRenderer.ts index 0a290147..a4672895 100644 --- a/packages/web-ui/src/tools/renderers/CalculateRenderer.ts +++ b/packages/web-ui/src/tools/renderers/CalculateRenderer.ts @@ -1,9 +1,9 @@ import type { ToolResultMessage } from "@earendil-works/pi-ai"; import { html } from "lit"; import { Calculator } from "lucide"; -import { i18n } from "../../utils/i18n.js"; -import { renderHeader } from "../renderer-registry.js"; -import type { ToolRenderer, ToolRenderResult } from "../types.js"; +import { i18n } from "../../utils/i18n.ts"; +import { renderHeader } from "../renderer-registry.ts"; +import type { ToolRenderer, ToolRenderResult } from "../types.ts"; interface CalculateParams { expression: string; diff --git a/packages/web-ui/src/tools/renderers/DefaultRenderer.ts b/packages/web-ui/src/tools/renderers/DefaultRenderer.ts index 227649a9..e5bf0ff1 100644 --- a/packages/web-ui/src/tools/renderers/DefaultRenderer.ts +++ b/packages/web-ui/src/tools/renderers/DefaultRenderer.ts @@ -1,9 +1,9 @@ import type { ToolResultMessage } from "@earendil-works/pi-ai"; import { html } from "lit"; import { Code } from "lucide"; -import { i18n } from "../../utils/i18n.js"; -import { renderHeader } from "../renderer-registry.js"; -import type { ToolRenderer, ToolRenderResult } from "../types.js"; +import { i18n } from "../../utils/i18n.ts"; +import { renderHeader } from "../renderer-registry.ts"; +import type { ToolRenderer, ToolRenderResult } from "../types.ts"; export class DefaultRenderer implements ToolRenderer { render(params: any | undefined, result: ToolResultMessage | undefined, isStreaming?: boolean): ToolRenderResult { diff --git a/packages/web-ui/src/tools/renderers/GetCurrentTimeRenderer.ts b/packages/web-ui/src/tools/renderers/GetCurrentTimeRenderer.ts index 0bd3664b..6e6240e6 100644 --- a/packages/web-ui/src/tools/renderers/GetCurrentTimeRenderer.ts +++ b/packages/web-ui/src/tools/renderers/GetCurrentTimeRenderer.ts @@ -1,9 +1,9 @@ import type { ToolResultMessage } from "@earendil-works/pi-ai"; import { html } from "lit"; import { Clock } from "lucide"; -import { i18n } from "../../utils/i18n.js"; -import { renderHeader } from "../renderer-registry.js"; -import type { ToolRenderer, ToolRenderResult } from "../types.js"; +import { i18n } from "../../utils/i18n.ts"; +import { renderHeader } from "../renderer-registry.ts"; +import type { ToolRenderer, ToolRenderResult } from "../types.ts"; interface GetCurrentTimeParams { timezone?: string; diff --git a/packages/web-ui/src/utils/attachment-utils.ts b/packages/web-ui/src/utils/attachment-utils.ts index afe98e44..57a8a6c7 100644 --- a/packages/web-ui/src/utils/attachment-utils.ts +++ b/packages/web-ui/src/utils/attachment-utils.ts @@ -3,7 +3,7 @@ import JSZip from "jszip"; import type { PDFDocumentProxy } from "pdfjs-dist"; import * as pdfjsLib from "pdfjs-dist"; import * as XLSX from "xlsx"; -import { i18n } from "./i18n.js"; +import { i18n } from "./i18n.ts"; // Configure PDF.js worker - we'll need to bundle this pdfjsLib.GlobalWorkerOptions.workerSrc = new URL("pdfjs-dist/build/pdf.worker.min.mjs", import.meta.url).toString(); diff --git a/packages/web-ui/src/utils/auth-token.ts b/packages/web-ui/src/utils/auth-token.ts index 385397e5..0d507fc6 100644 --- a/packages/web-ui/src/utils/auth-token.ts +++ b/packages/web-ui/src/utils/auth-token.ts @@ -1,5 +1,5 @@ import PromptDialog from "@mariozechner/mini-lit/dist/PromptDialog.js"; -import { i18n } from "./i18n.js"; +import { i18n } from "./i18n.ts"; export async function getAuthToken(): Promise { let authToken: string | undefined = localStorage.getItem(`auth-token`) || ""; diff --git a/packages/web-ui/tsconfig.build.json b/packages/web-ui/tsconfig.build.json index 9c4ca583..3b548741 100644 --- a/packages/web-ui/tsconfig.build.json +++ b/packages/web-ui/tsconfig.build.json @@ -11,6 +11,8 @@ "skipLibCheck": true, "esModuleInterop": true, "allowSyntheticDefaultImports": true, + "allowImportingTsExtensions": true, + "rewriteRelativeImportExtensions": true, "experimentalDecorators": true, "useDefineForClassFields": false, "rootDir": "./src", diff --git a/scripts/update-source-imports-to-ts.sh b/scripts/update-source-imports-to-ts.sh new file mode 100644 index 00000000..6030e41d --- /dev/null +++ b/scripts/update-source-imports-to-ts.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Rewrites relative source import specifiers in package source directories from .js to .ts. +# TypeScript's rewriteRelativeImportExtensions option rewrites these back to .js in emitted output. +find packages -mindepth 2 -maxdepth 2 -type d -name src -print0 | + while IFS= read -r -d '' src_dir; do + find "$src_dir" -type f -name '*.ts' -print0 + done | + xargs -0 perl -0pi -e 's/(\b(?:from|import)\b\s*\(?\s*["\x27])(\.{1,2}\/[^"\x27\r\n]+)\.js(["\x27]\s*\)?)/$1$2.ts$3/g; s/(\bdeclare\s+module\s+["\x27])(\.{1,2}\/[^"\x27\r\n]+)\.js(["\x27])/$1$2.ts$3/g; s/(\bimportNodeOnlyProvider\(\s*["\x27])(\.{1,2}\/[^"\x27\r\n]+)\.js(["\x27]\s*\))/$1$2.ts$3/g' diff --git a/tsconfig.base.json b/tsconfig.base.json index bd9a33a4..57e97d6e 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -4,6 +4,7 @@ "module": "Node16", "lib": ["ES2022"], "strict": true, + "erasableSyntaxOnly": true, "esModuleInterop": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true, @@ -14,7 +15,8 @@ "inlineSourceMap": false, "moduleResolution": "Node16", "resolveJsonModule": true, - "allowImportingTsExtensions": false, + "allowImportingTsExtensions": true, + "rewriteRelativeImportExtensions": true, "experimentalDecorators": true, "emitDecoratorMetadata": true, "useDefineForClassFields": false, From 8a6775bbe6cb813666026f2e53950bd5737e67be Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Wed, 20 May 2026 00:06:37 +0200 Subject: [PATCH 127/166] chore: remove redundant erasable type check --- package.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/package.json b/package.json index 4e31d158..af486099 100644 --- a/package.json +++ b/package.json @@ -15,8 +15,7 @@ "build": "cd packages/tui && npm run build && cd ../ai && npm run build && cd ../agent && npm run build && cd ../coding-agent && npm run build && cd ../web-ui && npm run build", "dev": "concurrently --names \"ai,agent,coding-agent,web-ui,tui\" --prefix-colors \"cyan,yellow,red,green,magenta\" \"cd packages/ai && npm run dev\" \"cd packages/agent && npm run dev\" \"cd packages/coding-agent && npm run dev\" \"cd packages/web-ui && npm run dev\" \"cd packages/tui && npm run dev\"", "dev:tsc": "concurrently --names \"ai,web-ui\" --prefix-colors \"cyan,green\" \"cd packages/ai && npm run dev:tsc\" \"cd packages/web-ui && npm run dev:tsc\"", - "check": "biome check --write --error-on-warnings . && tsgo --noEmit && npm run check:erasable-types && npm run check:browser-smoke && cd packages/web-ui && npm run check", - "check:erasable-types": "tsgo -p packages/ai/tsconfig.build.json --noEmit --erasableSyntaxOnly && tsgo -p packages/agent/tsconfig.build.json --noEmit --erasableSyntaxOnly && tsgo -p packages/coding-agent/tsconfig.build.json --noEmit --erasableSyntaxOnly", + "check": "biome check --write --error-on-warnings . && tsgo --noEmit && npm run check:browser-smoke && cd packages/web-ui && npm run check", "check:browser-smoke": "node scripts/check-browser-smoke.mjs", "profile:tui": "node scripts/profile-coding-agent-node.mjs --mode tui", "profile:rpc": "node scripts/profile-coding-agent-node.mjs --mode rpc", From e78f1a3dac28dd654056ca3d878f4167f4c72a2c Mon Sep 17 00:00:00 2001 From: Alexey Zaytsev Date: Tue, 19 May 2026 18:12:17 -0500 Subject: [PATCH 128/166] Export image resize utilities --- packages/coding-agent/src/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/coding-agent/src/index.ts b/packages/coding-agent/src/index.ts index 8001509f..6d1b08f6 100644 --- a/packages/coding-agent/src/index.ts +++ b/packages/coding-agent/src/index.ts @@ -349,5 +349,6 @@ export { // Clipboard utilities export { copyToClipboard } from "./utils/clipboard.js"; export { parseFrontmatter, stripFrontmatter } from "./utils/frontmatter.js"; +export { formatDimensionNote, type ResizedImage, resizeImage } from "./utils/image-resize.js"; // Shell utilities export { getShellConfig } from "./utils/shell.js"; From 23b361cfd08d252a814c70cdf305fd297ae9495a Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Wed, 20 May 2026 01:16:49 +0200 Subject: [PATCH 129/166] fix(tui): initialize loader before starting indicator --- packages/tui/src/components/loader.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/tui/src/components/loader.ts b/packages/tui/src/components/loader.ts index 9bce7293..4a93eb08 100644 --- a/packages/tui/src/components/loader.ts +++ b/packages/tui/src/components/loader.ts @@ -34,10 +34,10 @@ export class Loader extends Text { ) { super("", 1, 0); this.ui = ui; - this.setIndicator(indicator); this.spinnerColorFn = spinnerColorFn; this.messageColorFn = messageColorFn; this.message = message; + this.setIndicator(indicator); } render(width: number): string[] { From b141e1fa2460868686ffd19c5d4ced743eee6c24 Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Wed, 20 May 2026 02:21:11 +0200 Subject: [PATCH 130/166] chore: remove web-ui workspace --- AGENTS.md | 6 +- README.md | 3 - biome.json | 3 - package-lock.json | 1840 +------------ package.json | 8 +- packages/agent/CHANGELOG.md | 1 + packages/agent/package.json | 1 - packages/ai/CHANGELOG.md | 1 + packages/ai/package.json | 2 - packages/ai/src/env-api-keys.ts | 2 +- .../src/providers/openai-codex-responses.ts | 2 +- packages/ai/src/utils/oauth/openai-codex.ts | 2 +- packages/coding-agent/CHANGELOG.md | 1 + packages/coding-agent/package.json | 1 - .../modes/interactive/components/footer.ts | 2 +- packages/tui/CHANGELOG.md | 4 + packages/tui/package.json | 1 - packages/tui/test/autocomplete.test.ts | 4 +- packages/web-ui/CHANGELOG.md | 421 --- packages/web-ui/README.md | 601 ----- packages/web-ui/example/.gitignore | 3 - packages/web-ui/example/README.md | 61 - packages/web-ui/example/index.html | 13 - packages/web-ui/example/package.json | 25 - packages/web-ui/example/src/app.css | 1 - .../web-ui/example/src/custom-messages.ts | 99 - packages/web-ui/example/src/main.ts | 421 --- packages/web-ui/example/tsconfig.json | 23 - packages/web-ui/example/vite.config.ts | 6 - packages/web-ui/package.json | 52 - .../web-ui/scripts/count-prompt-tokens.ts | 88 - packages/web-ui/src/ChatPanel.ts | 209 -- packages/web-ui/src/app.css | 68 - .../web-ui/src/components/AgentInterface.ts | 403 --- .../web-ui/src/components/AttachmentTile.ts | 107 - .../web-ui/src/components/ConsoleBlock.ts | 72 - .../src/components/CustomProviderCard.ts | 100 - .../src/components/ExpandableSection.ts | 46 - packages/web-ui/src/components/Input.ts | 113 - .../web-ui/src/components/MessageEditor.ts | 405 --- packages/web-ui/src/components/MessageList.ts | 98 - packages/web-ui/src/components/Messages.ts | 383 --- .../web-ui/src/components/ProviderKeyInput.ts | 153 -- .../web-ui/src/components/SandboxedIframe.ts | 626 ----- .../components/StreamingMessageContainer.ts | 103 - .../web-ui/src/components/ThinkingBlock.ts | 43 - .../components/message-renderer-registry.ts | 28 - .../sandbox/ArtifactsRuntimeProvider.ts | 224 -- .../sandbox/AttachmentsRuntimeProvider.ts | 70 - .../sandbox/ConsoleRuntimeProvider.ts | 186 -- .../sandbox/FileDownloadRuntimeProvider.ts | 110 - .../sandbox/RuntimeMessageBridge.ts | 82 - .../sandbox/RuntimeMessageRouter.ts | 216 -- .../sandbox/SandboxRuntimeProvider.ts | 52 - .../web-ui/src/dialogs/ApiKeyPromptDialog.ts | 75 - .../web-ui/src/dialogs/AttachmentOverlay.ts | 636 ----- .../src/dialogs/CustomProviderDialog.ts | 274 -- packages/web-ui/src/dialogs/ModelSelector.ts | 370 --- .../src/dialogs/PersistentStorageDialog.ts | 144 - .../web-ui/src/dialogs/ProvidersModelsTab.ts | 212 -- .../web-ui/src/dialogs/SessionListDialog.ts | 150 -- packages/web-ui/src/dialogs/SettingsDialog.ts | 218 -- packages/web-ui/src/index.ts | 120 - packages/web-ui/src/prompts/prompts.ts | 282 -- packages/web-ui/src/storage/app-storage.ts | 60 - .../backends/indexeddb-storage-backend.ts | 196 -- packages/web-ui/src/storage/store.ts | 33 - .../storage/stores/custom-providers-store.ts | 62 - .../src/storage/stores/provider-keys-store.ts | 33 - .../src/storage/stores/sessions-store.ts | 136 - .../src/storage/stores/settings-store.ts | 34 - packages/web-ui/src/storage/types.ts | 206 -- .../src/tools/artifacts/ArtifactElement.ts | 14 - .../src/tools/artifacts/ArtifactPill.ts | 26 - .../web-ui/src/tools/artifacts/Console.ts | 93 - .../src/tools/artifacts/DocxArtifact.ts | 213 -- .../src/tools/artifacts/ExcelArtifact.ts | 231 -- .../src/tools/artifacts/GenericArtifact.ts | 117 - .../src/tools/artifacts/HtmlArtifact.ts | 195 -- .../src/tools/artifacts/ImageArtifact.ts | 116 - .../src/tools/artifacts/MarkdownArtifact.ts | 82 - .../web-ui/src/tools/artifacts/PdfArtifact.ts | 201 -- .../web-ui/src/tools/artifacts/SvgArtifact.ts | 119 - .../src/tools/artifacts/TextArtifact.ts | 148 -- .../artifacts/artifacts-tool-renderer.ts | 314 --- .../web-ui/src/tools/artifacts/artifacts.ts | 713 ----- packages/web-ui/src/tools/artifacts/index.ts | 7 - packages/web-ui/src/tools/extract-document.ts | 275 -- packages/web-ui/src/tools/index.ts | 46 - packages/web-ui/src/tools/javascript-repl.ts | 293 -- .../web-ui/src/tools/renderer-registry.ts | 130 - .../src/tools/renderers/BashRenderer.ts | 52 - .../src/tools/renderers/CalculateRenderer.ts | 58 - .../src/tools/renderers/DefaultRenderer.ts | 103 - .../tools/renderers/GetCurrentTimeRenderer.ts | 92 - packages/web-ui/src/tools/types.ts | 15 - packages/web-ui/src/utils/attachment-utils.ts | 472 ---- packages/web-ui/src/utils/auth-token.ts | 22 - packages/web-ui/src/utils/format.ts | 42 - packages/web-ui/src/utils/i18n.ts | 653 ----- packages/web-ui/src/utils/model-discovery.ts | 277 -- packages/web-ui/src/utils/proxy-utils.ts | 139 - packages/web-ui/src/utils/test-sessions.ts | 2357 ----------------- packages/web-ui/tsconfig.build.json | 22 - packages/web-ui/tsconfig.json | 7 - scripts/browser-smoke-entry.ts | 60 +- tsconfig.json | 4 +- 107 files changed, 83 insertions(+), 18461 deletions(-) delete mode 100644 packages/web-ui/CHANGELOG.md delete mode 100644 packages/web-ui/README.md delete mode 100644 packages/web-ui/example/.gitignore delete mode 100644 packages/web-ui/example/README.md delete mode 100644 packages/web-ui/example/index.html delete mode 100644 packages/web-ui/example/package.json delete mode 100644 packages/web-ui/example/src/app.css delete mode 100644 packages/web-ui/example/src/custom-messages.ts delete mode 100644 packages/web-ui/example/src/main.ts delete mode 100644 packages/web-ui/example/tsconfig.json delete mode 100644 packages/web-ui/example/vite.config.ts delete mode 100644 packages/web-ui/package.json delete mode 100644 packages/web-ui/scripts/count-prompt-tokens.ts delete mode 100644 packages/web-ui/src/ChatPanel.ts delete mode 100644 packages/web-ui/src/app.css delete mode 100644 packages/web-ui/src/components/AgentInterface.ts delete mode 100644 packages/web-ui/src/components/AttachmentTile.ts delete mode 100644 packages/web-ui/src/components/ConsoleBlock.ts delete mode 100644 packages/web-ui/src/components/CustomProviderCard.ts delete mode 100644 packages/web-ui/src/components/ExpandableSection.ts delete mode 100644 packages/web-ui/src/components/Input.ts delete mode 100644 packages/web-ui/src/components/MessageEditor.ts delete mode 100644 packages/web-ui/src/components/MessageList.ts delete mode 100644 packages/web-ui/src/components/Messages.ts delete mode 100644 packages/web-ui/src/components/ProviderKeyInput.ts delete mode 100644 packages/web-ui/src/components/SandboxedIframe.ts delete mode 100644 packages/web-ui/src/components/StreamingMessageContainer.ts delete mode 100644 packages/web-ui/src/components/ThinkingBlock.ts delete mode 100644 packages/web-ui/src/components/message-renderer-registry.ts delete mode 100644 packages/web-ui/src/components/sandbox/ArtifactsRuntimeProvider.ts delete mode 100644 packages/web-ui/src/components/sandbox/AttachmentsRuntimeProvider.ts delete mode 100644 packages/web-ui/src/components/sandbox/ConsoleRuntimeProvider.ts delete mode 100644 packages/web-ui/src/components/sandbox/FileDownloadRuntimeProvider.ts delete mode 100644 packages/web-ui/src/components/sandbox/RuntimeMessageBridge.ts delete mode 100644 packages/web-ui/src/components/sandbox/RuntimeMessageRouter.ts delete mode 100644 packages/web-ui/src/components/sandbox/SandboxRuntimeProvider.ts delete mode 100644 packages/web-ui/src/dialogs/ApiKeyPromptDialog.ts delete mode 100644 packages/web-ui/src/dialogs/AttachmentOverlay.ts delete mode 100644 packages/web-ui/src/dialogs/CustomProviderDialog.ts delete mode 100644 packages/web-ui/src/dialogs/ModelSelector.ts delete mode 100644 packages/web-ui/src/dialogs/PersistentStorageDialog.ts delete mode 100644 packages/web-ui/src/dialogs/ProvidersModelsTab.ts delete mode 100644 packages/web-ui/src/dialogs/SessionListDialog.ts delete mode 100644 packages/web-ui/src/dialogs/SettingsDialog.ts delete mode 100644 packages/web-ui/src/index.ts delete mode 100644 packages/web-ui/src/prompts/prompts.ts delete mode 100644 packages/web-ui/src/storage/app-storage.ts delete mode 100644 packages/web-ui/src/storage/backends/indexeddb-storage-backend.ts delete mode 100644 packages/web-ui/src/storage/store.ts delete mode 100644 packages/web-ui/src/storage/stores/custom-providers-store.ts delete mode 100644 packages/web-ui/src/storage/stores/provider-keys-store.ts delete mode 100644 packages/web-ui/src/storage/stores/sessions-store.ts delete mode 100644 packages/web-ui/src/storage/stores/settings-store.ts delete mode 100644 packages/web-ui/src/storage/types.ts delete mode 100644 packages/web-ui/src/tools/artifacts/ArtifactElement.ts delete mode 100644 packages/web-ui/src/tools/artifacts/ArtifactPill.ts delete mode 100644 packages/web-ui/src/tools/artifacts/Console.ts delete mode 100644 packages/web-ui/src/tools/artifacts/DocxArtifact.ts delete mode 100644 packages/web-ui/src/tools/artifacts/ExcelArtifact.ts delete mode 100644 packages/web-ui/src/tools/artifacts/GenericArtifact.ts delete mode 100644 packages/web-ui/src/tools/artifacts/HtmlArtifact.ts delete mode 100644 packages/web-ui/src/tools/artifacts/ImageArtifact.ts delete mode 100644 packages/web-ui/src/tools/artifacts/MarkdownArtifact.ts delete mode 100644 packages/web-ui/src/tools/artifacts/PdfArtifact.ts delete mode 100644 packages/web-ui/src/tools/artifacts/SvgArtifact.ts delete mode 100644 packages/web-ui/src/tools/artifacts/TextArtifact.ts delete mode 100644 packages/web-ui/src/tools/artifacts/artifacts-tool-renderer.ts delete mode 100644 packages/web-ui/src/tools/artifacts/artifacts.ts delete mode 100644 packages/web-ui/src/tools/artifacts/index.ts delete mode 100644 packages/web-ui/src/tools/extract-document.ts delete mode 100644 packages/web-ui/src/tools/index.ts delete mode 100644 packages/web-ui/src/tools/javascript-repl.ts delete mode 100644 packages/web-ui/src/tools/renderer-registry.ts delete mode 100644 packages/web-ui/src/tools/renderers/BashRenderer.ts delete mode 100644 packages/web-ui/src/tools/renderers/CalculateRenderer.ts delete mode 100644 packages/web-ui/src/tools/renderers/DefaultRenderer.ts delete mode 100644 packages/web-ui/src/tools/renderers/GetCurrentTimeRenderer.ts delete mode 100644 packages/web-ui/src/tools/types.ts delete mode 100644 packages/web-ui/src/utils/attachment-utils.ts delete mode 100644 packages/web-ui/src/utils/auth-token.ts delete mode 100644 packages/web-ui/src/utils/format.ts delete mode 100644 packages/web-ui/src/utils/i18n.ts delete mode 100644 packages/web-ui/src/utils/model-discovery.ts delete mode 100644 packages/web-ui/src/utils/proxy-utils.ts delete mode 100644 packages/web-ui/src/utils/test-sessions.ts delete mode 100644 packages/web-ui/tsconfig.build.json delete mode 100644 packages/web-ui/tsconfig.json diff --git a/AGENTS.md b/AGENTS.md index 03b1f4eb..3e752675 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -16,7 +16,7 @@ - Check node_modules for external API type definitions instead of guessing - **NEVER use inline imports** - no `await import("./foo.js")`, no `import("pkg").Type` in type positions, no dynamic imports for types. Always use standard top-level imports. - NEVER remove or downgrade code to fix type errors from outdated dependencies; upgrade the dependency instead -- In core source packages (`packages/ai/src`, `packages/agent/src`, `packages/coding-agent/src`), use only erasable TypeScript syntax compatible with Node strip-only mode. Do not use constructor parameter properties, `enum`, `namespace`/`module`, `import =`, `export =`, or other TypeScript constructs that require JavaScript emit. Use explicit fields and constructor assignments instead of parameter properties. +- Use only erasable TypeScript syntax compatible with Node strip-only mode in TypeScript checked by the root config (`packages/*/src`, `packages/*/test`, and `packages/coding-agent/examples`). Do not use constructor parameter properties, `enum`, `namespace`/`module`, `import =`, `export =`, or other TypeScript constructs that require JavaScript emit. Use explicit fields and constructor assignments instead of parameter properties. - Always ask before removing functionality or code that appears to be intentional - Do not preserve backward compatibility unless the user explicitly asks for it - Never hardcode key checks with, eg. `matchesKey(keyData, "ctrl+x")`. All keybindings must be configurable. Add default to matching object (`DEFAULT_EDITOR_KEYBINDINGS` or `DEFAULT_APP_KEYBINDINGS`) @@ -26,7 +26,7 @@ - After code changes (not documentation changes): `npm run check` (get full output, no tail). Fix all errors, warnings, and infos before committing. - Note: `npm run check` does not run tests. -- NEVER run: `npm run dev`, `npm run build`, `npm test` +- NEVER run: `npm run build`, `npm test` - Only run specific tests if user instructs: `npx tsx ../../node_modules/vitest/dist/cli.js --run test/specific.test.ts` - Run tests from the package root, not the repo root. - If you create or modify a test file, you MUST run that test file and iterate until it passes. @@ -49,7 +49,7 @@ When creating issues: - Add `pkg:*` labels to indicate which package(s) the issue affects - - Available labels: `pkg:agent`, `pkg:ai`, `pkg:coding-agent`, `pkg:tui`, `pkg:web-ui` + - Available labels: `pkg:agent`, `pkg:ai`, `pkg:coding-agent`, `pkg:tui` - If an issue spans multiple packages, add all relevant labels When posting issue/PR comments: diff --git a/README.md b/README.md index 90d9fc10..6bdda547 100644 --- a/README.md +++ b/README.md @@ -53,7 +53,6 @@ I regularly publish my own `pi-mono` work sessions here: | **[@earendil-works/pi-agent-core](packages/agent)** | Agent runtime with tool calling and state management | | **[@earendil-works/pi-coding-agent](packages/coding-agent)** | Interactive coding agent CLI | | **[@earendil-works/pi-tui](packages/tui)** | Terminal UI library with differential rendering | -| **[@earendil-works/pi-web-ui](packages/web-ui)** | Web components for AI chat interfaces | For Slack/chat automation and workflows see [earendil-works/pi-chat](https://github.com/earendil-works/pi-chat). @@ -71,8 +70,6 @@ npm run check # Lint, format, and type check ./pi-test.sh # Run pi from sources (can be run from any directory) ``` -> **Note:** `npm run check` requires `npm run build` to be run first. The web-ui package uses `tsc` which needs compiled `.d.ts` files from dependencies. - ## License MIT diff --git a/biome.json b/biome.json index 147d081a..7c89d187 100644 --- a/biome.json +++ b/biome.json @@ -28,12 +28,9 @@ "packages/*/src/**/*.ts", "packages/*/test/**/*.ts", "packages/coding-agent/examples/**/*.ts", - "packages/web-ui/src/**/*.ts", - "packages/web-ui/example/**/*.ts", "!**/node_modules/**/*", "!**/test-sessions.ts", "!**/models.generated.ts", - "!packages/web-ui/src/app.css", "!packages/mom/data/**/*", "!!**/node_modules" ] diff --git a/package-lock.json b/package-lock.json index 292b1ebb..38e1232f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,6 @@ "version": "0.0.3", "workspaces": [ "packages/*", - "packages/web-ui/example", "packages/coding-agent/examples/extensions/with-deps", "packages/coding-agent/examples/extensions/custom-provider-anthropic", "packages/coding-agent/examples/extensions/custom-provider-gitlab-duo", @@ -23,7 +22,6 @@ "@biomejs/biome": "2.3.5", "@types/node": "^22.10.5", "@typescript/native-preview": "7.0.0-dev.20260120.1", - "concurrently": "^9.2.1", "husky": "^9.1.7", "jiti": "^2.7.0", "shx": "^0.4.0", @@ -742,10 +740,6 @@ "resolved": "packages/tui", "link": true }, - "node_modules/@earendil-works/pi-web-ui": { - "resolved": "packages/web-ui", - "link": true - }, "node_modules/@esbuild/aix-ppc64": { "version": "0.28.0", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.0.tgz", @@ -1336,17 +1330,6 @@ "@jridgewell/trace-mapping": "^0.3.24" } }, - "node_modules/@jridgewell/remapping": { - "version": "2.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", - "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, "node_modules/@jridgewell/resolve-uri": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", @@ -1375,71 +1358,6 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "node_modules/@lit-labs/ssr-dom-shim": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@lit-labs/ssr-dom-shim/-/ssr-dom-shim-1.6.0.tgz", - "integrity": "sha512-VHb0ALPMTlgKjM6yIxxoQNnpKyUKLD04VzeQdsiXkMqkvYlAHxq9glGLmgbb889/1GsohSOAjvQYoiBppXFqrQ==", - "license": "BSD-3-Clause" - }, - "node_modules/@lit/reactive-element": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@lit/reactive-element/-/reactive-element-2.1.2.tgz", - "integrity": "sha512-pbCDiVMnne1lYUIaYNN5wrwQXDtHaYtg7YEFPeW+hws6U47WeFvISGUWekPGKWOP1ygrs0ef0o1VJMk1exos5A==", - "license": "BSD-3-Clause", - "dependencies": { - "@lit-labs/ssr-dom-shim": "^1.5.0" - } - }, - "node_modules/@lmstudio/lms-isomorphic": { - "version": "0.4.6", - "resolved": "https://registry.npmjs.org/@lmstudio/lms-isomorphic/-/lms-isomorphic-0.4.6.tgz", - "integrity": "sha512-v0LIjXKnDe3Ff3XZO5eQjlVxTjleUHXaom14MV7QU9bvwaoo3l5p71+xJ3mmSaqZq370CQ6pTKCn1Bb7Jf+VwQ==", - "license": "Apache-2.0", - "dependencies": { - "ws": "^8.16.0" - } - }, - "node_modules/@lmstudio/sdk": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@lmstudio/sdk/-/sdk-1.5.0.tgz", - "integrity": "sha512-fdY12x4hb14PEjYijh7YeCqT1ZDY5Ok6VR4l4+E/dI+F6NW8oB+P83Sxed5vqE4XgTzbgyPuSR2ZbMNxxF+6jA==", - "license": "Apache-2.0", - "dependencies": { - "@lmstudio/lms-isomorphic": "^0.4.6", - "chalk": "^4.1.2", - "jsonschema": "^1.5.0", - "zod": "^3.22.4", - "zod-to-json-schema": "^3.22.5" - } - }, - "node_modules/@lmstudio/sdk/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/@lmstudio/sdk/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/@mariozechner/clipboard": { "version": "0.3.6", "resolved": "https://registry.npmjs.org/@mariozechner/clipboard/-/clipboard-0.3.6.tgz", @@ -1619,48 +1537,6 @@ "node": ">= 10" } }, - "node_modules/@mariozechner/mini-lit": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/@mariozechner/mini-lit/-/mini-lit-0.2.1.tgz", - "integrity": "sha512-u300euLgCsDDlb8o2Wbz+55eSJga5X2vB58s9XBuFIr2Bi3iI+GMR7t/NYo/O6Vr6obXShXgYjR3SRUJVgo+kQ==", - "dependencies": { - "@preact/signals-core": "^1.12.1", - "class-variance-authority": "^0.7.1", - "diff": "^8.0.2", - "highlight.js": "^11.11.1", - "html-parse-string": "^0.0.9", - "katex": "^0.16.22", - "lucide": "^0.544.0", - "marked": "^16.3.0", - "tailwind-merge": "^3.3.1", - "tailwind-variants": "^3.1.1", - "uhtml": "^5.0.9" - }, - "peerDependencies": { - "lit": "^3.3.1" - } - }, - "node_modules/@mariozechner/mini-lit/node_modules/highlight.js": { - "version": "11.11.1", - "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.11.1.tgz", - "integrity": "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==", - "license": "BSD-3-Clause", - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/@mariozechner/mini-lit/node_modules/marked": { - "version": "16.4.2", - "resolved": "https://registry.npmjs.org/marked/-/marked-16.4.2.tgz", - "integrity": "sha512-TI3V8YYWvkVf3KJe1dRkpnjs68JUPyEa5vjKrp1XEEJUAOaQc+Qj+L1qWbPd0SJuAdQkFU0h73sXXqwDYxsiDA==", - "license": "MIT", - "bin": { - "marked": "bin/marked.js" - }, - "engines": { - "node": ">= 20" - } - }, "node_modules/@mistralai/mistralai": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/@mistralai/mistralai/-/mistralai-2.2.1.tgz", @@ -1672,256 +1548,6 @@ "zod-to-json-schema": "^3.25.0" } }, - "node_modules/@napi-rs/canvas": { - "version": "0.1.100", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas/-/canvas-0.1.100.tgz", - "integrity": "sha512-xglYA6q3XO5P3BNJYxVZ1IV7DLVjp1Py6nwag88YntrS+3vKHyYcMqXVS4ZztJmwz2uGvz1FWhI/4LgbR5uQDA==", - "license": "MIT", - "optional": true, - "workspaces": [ - "e2e/*" - ], - "engines": { - "node": ">= 10" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Brooooooklyn" - }, - "optionalDependencies": { - "@napi-rs/canvas-android-arm64": "0.1.100", - "@napi-rs/canvas-darwin-arm64": "0.1.100", - "@napi-rs/canvas-darwin-x64": "0.1.100", - "@napi-rs/canvas-linux-arm-gnueabihf": "0.1.100", - "@napi-rs/canvas-linux-arm64-gnu": "0.1.100", - "@napi-rs/canvas-linux-arm64-musl": "0.1.100", - "@napi-rs/canvas-linux-riscv64-gnu": "0.1.100", - "@napi-rs/canvas-linux-x64-gnu": "0.1.100", - "@napi-rs/canvas-linux-x64-musl": "0.1.100", - "@napi-rs/canvas-win32-arm64-msvc": "0.1.100", - "@napi-rs/canvas-win32-x64-msvc": "0.1.100" - } - }, - "node_modules/@napi-rs/canvas-android-arm64": { - "version": "0.1.100", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-android-arm64/-/canvas-android-arm64-0.1.100.tgz", - "integrity": "sha512-hjhCKhntPv9+t4ckHymdx0phYNcVW+GKQR6Lzw2zE+pOVjOplSmtx9nNNknTjbEDLcuLZqA1y8ufKg1XfgftzQ==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">= 10" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Brooooooklyn" - } - }, - "node_modules/@napi-rs/canvas-darwin-arm64": { - "version": "0.1.100", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-arm64/-/canvas-darwin-arm64-0.1.100.tgz", - "integrity": "sha512-2PcswRaC7Ly645DGt88///zuFDhJxJYdKAs1uU3mfk1atYkXufgcgLfBpk6Tm12nCQBaNt1wpybuPZ4qOhTo8A==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Brooooooklyn" - } - }, - "node_modules/@napi-rs/canvas-darwin-x64": { - "version": "0.1.100", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-x64/-/canvas-darwin-x64-0.1.100.tgz", - "integrity": "sha512-ePNZtj7pNIva/siZMg+HmbeozkIjqUIYdoymH8HaA3qK7LfzFN4WMBM8G6HQ9ZC+H3+Dnn5pqtiXpgLykaPOhw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Brooooooklyn" - } - }, - "node_modules/@napi-rs/canvas-linux-arm-gnueabihf": { - "version": "0.1.100", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm-gnueabihf/-/canvas-linux-arm-gnueabihf-0.1.100.tgz", - "integrity": "sha512-d5cDB48oWFGU8/XPhUOFAlySgb/VAu7D+s8fi55K1Pcfg8aPplHWqMgibhVLU8ky7Pyg/fuiVLz4Nf3JrSTuUA==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Brooooooklyn" - } - }, - "node_modules/@napi-rs/canvas-linux-arm64-gnu": { - "version": "0.1.100", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-gnu/-/canvas-linux-arm64-gnu-0.1.100.tgz", - "integrity": "sha512-rDxgxRu69RvDlX/bh9o22DxLsGr8EqsNgotL9+RwQE1S0b0cqeatqsw6aW45mukm0B42DIAaAacKaYQ8cqS1nw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Brooooooklyn" - } - }, - "node_modules/@napi-rs/canvas-linux-arm64-musl": { - "version": "0.1.100", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-musl/-/canvas-linux-arm64-musl-0.1.100.tgz", - "integrity": "sha512-K3mDW66N+xT2/V439u1alFANiBUjdEx2gLiNYnCmUsva5jZMxWTjafBYwTzYK+EMFMHrUoabuU+T1BIP5CgbYQ==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Brooooooklyn" - } - }, - "node_modules/@napi-rs/canvas-linux-riscv64-gnu": { - "version": "0.1.100", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-riscv64-gnu/-/canvas-linux-riscv64-gnu-0.1.100.tgz", - "integrity": "sha512-mooqUBTIsccZpnoQC4NgrC1v6C1vof39etLNMnBwCY+p0gajWJvAHLGQ6g/gGyS5YrpDW+GefSN4+Cvcr08UWw==", - "cpu": [ - "riscv64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Brooooooklyn" - } - }, - "node_modules/@napi-rs/canvas-linux-x64-gnu": { - "version": "0.1.100", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-gnu/-/canvas-linux-x64-gnu-0.1.100.tgz", - "integrity": "sha512-1eCvkDCazm7FFhsT7DfGOdSaHgZVK3bt/dSBl5EWHOWmnz+I7j8tPseJqqD81NF+MH21jKUK4wQSDjN0mdhnTg==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Brooooooklyn" - } - }, - "node_modules/@napi-rs/canvas-linux-x64-musl": { - "version": "0.1.100", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-musl/-/canvas-linux-x64-musl-0.1.100.tgz", - "integrity": "sha512-20arT6lnI19S68qNlii73TSEDbECNgzMz2EpldC1V3mZFuRkeujXkcebRk0LRJe9SEUAooYiLokfMViY8IX7yA==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Brooooooklyn" - } - }, - "node_modules/@napi-rs/canvas-win32-arm64-msvc": { - "version": "0.1.100", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-win32-arm64-msvc/-/canvas-win32-arm64-msvc-0.1.100.tgz", - "integrity": "sha512-DZFFT1wIAg37LJw37yhMRFfjATd3vTQzjZ1Yki8u2vhO6Hi5VE6BVaGQ1aaDu7xb4iMErz+9EOwjpS7xcxFeBw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Brooooooklyn" - } - }, - "node_modules/@napi-rs/canvas-win32-x64-msvc": { - "version": "0.1.100", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-win32-x64-msvc/-/canvas-win32-x64-msvc-0.1.100.tgz", - "integrity": "sha512-MyT1j3mHC2+Lu4pBi9mKyMJhtP6U7k7EldY7sj/uS5gJA65gTXt8MefJQXLJo5d/vZbuWmfxzkEUNc/urV3pHA==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Brooooooklyn" - } - }, "node_modules/@nodable/entities": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/@nodable/entities/-/entities-2.1.0.tgz", @@ -1972,328 +1598,6 @@ "node": ">= 8" } }, - "node_modules/@parcel/watcher": { - "version": "2.5.6", - "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.6.tgz", - "integrity": "sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "detect-libc": "^2.0.3", - "is-glob": "^4.0.3", - "node-addon-api": "^7.0.0", - "picomatch": "^4.0.3" - }, - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - }, - "optionalDependencies": { - "@parcel/watcher-android-arm64": "2.5.6", - "@parcel/watcher-darwin-arm64": "2.5.6", - "@parcel/watcher-darwin-x64": "2.5.6", - "@parcel/watcher-freebsd-x64": "2.5.6", - "@parcel/watcher-linux-arm-glibc": "2.5.6", - "@parcel/watcher-linux-arm-musl": "2.5.6", - "@parcel/watcher-linux-arm64-glibc": "2.5.6", - "@parcel/watcher-linux-arm64-musl": "2.5.6", - "@parcel/watcher-linux-x64-glibc": "2.5.6", - "@parcel/watcher-linux-x64-musl": "2.5.6", - "@parcel/watcher-win32-arm64": "2.5.6", - "@parcel/watcher-win32-ia32": "2.5.6", - "@parcel/watcher-win32-x64": "2.5.6" - } - }, - "node_modules/@parcel/watcher-android-arm64": { - "version": "2.5.6", - "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.6.tgz", - "integrity": "sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-darwin-arm64": { - "version": "2.5.6", - "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.6.tgz", - "integrity": "sha512-Z2ZdrnwyXvvvdtRHLmM4knydIdU9adO3D4n/0cVipF3rRiwP+3/sfzpAwA/qKFL6i1ModaabkU7IbpeMBgiVEA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-darwin-x64": { - "version": "2.5.6", - "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.6.tgz", - "integrity": "sha512-HgvOf3W9dhithcwOWX9uDZyn1lW9R+7tPZ4sug+NGrGIo4Rk1hAXLEbcH1TQSqxts0NYXXlOWqVpvS1SFS4fRg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-freebsd-x64": { - "version": "2.5.6", - "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.6.tgz", - "integrity": "sha512-vJVi8yd/qzJxEKHkeemh7w3YAn6RJCtYlE4HPMoVnCpIXEzSrxErBW5SJBgKLbXU3WdIpkjBTeUNtyBVn8TRng==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-linux-arm-glibc": { - "version": "2.5.6", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.6.tgz", - "integrity": "sha512-9JiYfB6h6BgV50CCfasfLf/uvOcJskMSwcdH1PHH9rvS1IrNy8zad6IUVPVUfmXr+u+Km9IxcfMLzgdOudz9EQ==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-linux-arm-musl": { - "version": "2.5.6", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.6.tgz", - "integrity": "sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-linux-arm64-glibc": { - "version": "2.5.6", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.6.tgz", - "integrity": "sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-linux-arm64-musl": { - "version": "2.5.6", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.6.tgz", - "integrity": "sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-linux-x64-glibc": { - "version": "2.5.6", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.6.tgz", - "integrity": "sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-linux-x64-musl": { - "version": "2.5.6", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.6.tgz", - "integrity": "sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-win32-arm64": { - "version": "2.5.6", - "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.6.tgz", - "integrity": "sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-win32-ia32": { - "version": "2.5.6", - "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.6.tgz", - "integrity": "sha512-k35yLp1ZMwwee3Ez/pxBi5cf4AoBKYXj00CZ80jUz5h8prpiaQsiRPKQMxoLstNuqe2vR4RNPEAEcjEFzhEz/g==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-win32-x64": { - "version": "2.5.6", - "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.6.tgz", - "integrity": "sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher/node_modules/picomatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", - "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", @@ -2311,16 +1615,6 @@ "integrity": "sha512-bQY06wzzR8D2+vVCUoBsr5QS2U6UgPUQRmErNwtsuI6vLcyRKkafjkr3KxbtGFf9aBBIV2mcvlsKD1UYaIV+sg==", "license": "MIT" }, - "node_modules/@preact/signals-core": { - "version": "1.14.2", - "resolved": "https://registry.npmjs.org/@preact/signals-core/-/signals-core-1.14.2.tgz", - "integrity": "sha512-RZHdBj9ZF4n40Rp4jS052EHHjBWf96P9oNdXPfhQTovCuWY9iQn3Gq+gOTJSgBO9A/JBuPfMOWsSX/lIU9Pc/A==", - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/preact" - } - }, "node_modules/@protobufjs/aspromise": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", @@ -2860,297 +2154,6 @@ "node": ">=14.0.0" } }, - "node_modules/@tailwindcss/cli": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/@tailwindcss/cli/-/cli-4.3.0.tgz", - "integrity": "sha512-X9kdlqyMopO9fewbgHsEeuy31YzMHbdZ9VsKt004tB+mxSg1CNbyhZYCzvhciN0AM4R4b5lvIprPjtNq7iQxpQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@parcel/watcher": "^2.5.1", - "@tailwindcss/node": "4.3.0", - "@tailwindcss/oxide": "4.3.0", - "enhanced-resolve": "^5.21.0", - "mri": "^1.2.0", - "picocolors": "^1.1.1", - "tailwindcss": "4.3.0" - }, - "bin": { - "tailwindcss": "dist/index.mjs" - } - }, - "node_modules/@tailwindcss/node": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.0.tgz", - "integrity": "sha512-aFb4gUhFOgdh9AXo4IzBEOzBkkAxm9VigwDJnMIYv3lcfXCJVesNfbEaBl4BNgVRyid92AmdviqwBUBRKSeY3g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/remapping": "^2.3.5", - "enhanced-resolve": "^5.21.0", - "jiti": "^2.6.1", - "lightningcss": "1.32.0", - "magic-string": "^0.30.21", - "source-map-js": "^1.2.1", - "tailwindcss": "4.3.0" - } - }, - "node_modules/@tailwindcss/oxide": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.3.0.tgz", - "integrity": "sha512-F7HZGBeN9I0/AuuJS5PwcD8xayx5ri5GhjYUDBEVYUkexyA/giwbDNjRVrxSezE3T250OU2K/wp/ltWx3UOefg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 20" - }, - "optionalDependencies": { - "@tailwindcss/oxide-android-arm64": "4.3.0", - "@tailwindcss/oxide-darwin-arm64": "4.3.0", - "@tailwindcss/oxide-darwin-x64": "4.3.0", - "@tailwindcss/oxide-freebsd-x64": "4.3.0", - "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.0", - "@tailwindcss/oxide-linux-arm64-gnu": "4.3.0", - "@tailwindcss/oxide-linux-arm64-musl": "4.3.0", - "@tailwindcss/oxide-linux-x64-gnu": "4.3.0", - "@tailwindcss/oxide-linux-x64-musl": "4.3.0", - "@tailwindcss/oxide-wasm32-wasi": "4.3.0", - "@tailwindcss/oxide-win32-arm64-msvc": "4.3.0", - "@tailwindcss/oxide-win32-x64-msvc": "4.3.0" - } - }, - "node_modules/@tailwindcss/oxide-android-arm64": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.0.tgz", - "integrity": "sha512-TJPiq67tKlLuObP6RkwvVGDoxCMBVtDgKkLfa/uyj7/FyxvQwHS+UOnVrXXgbEsfUaMgiVvC4KbJnRr26ho4Ng==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-darwin-arm64": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.0.tgz", - "integrity": "sha512-oMN/WZRb+SO37BmUElEgeEWuU8E/HXRkiODxJxLe1UTHVXLrdVSgfaJV7pSlhRGMSOiXLuxTIjfsF3wYvz8cgQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-darwin-x64": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.0.tgz", - "integrity": "sha512-N6CUmu4a6bKVADfw77p+iw6Yd9Q3OBhe0veaDX+QazfuVYlQsHfDgxBrsjQ/IW+zywL8mTrNd0SdJT/zgtvMdA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-freebsd-x64": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.0.tgz", - "integrity": "sha512-zDL5hBkQdH5C6MpqbK3gQAgP80tsMwSI26vjOzjJtNCMUo0lFgOItzHKBIupOZNQxt3ouPH7RPhvNhiTfCe5CQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.0.tgz", - "integrity": "sha512-R06HdNi7A7OEoMsf6d4tjZ71RCWnZQPHj2mnotSFURjNLdBC+cIgXQ7l81CqeoiQftjf6OOblxXMInMgN2VzMA==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.0.tgz", - "integrity": "sha512-qTJHELX8jetjhRQHCLilkVLmybpzNQAtaI/gaoVoidn/ufbNDbAo8KlK2J+yPoc8wQxvDxCmh/5lr8nC1+lTbg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-linux-arm64-musl": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.3.0.tgz", - "integrity": "sha512-Z6sukiQsngnWO+l39X4pPbiWT81IC+PLKF+PHxIlyZbGNb9MODfYlXEVlFvej5BOZInWX01kVyzeLvHsXhfczQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-linux-x64-gnu": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.0.tgz", - "integrity": "sha512-DRNdQRpSGzRGfARVuVkxvM8Q12nh19l4BF/G7zGA1oe+9wcC6saFBHTISrpIcKzhiXtSrlSrluCfvMuledoCTQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-linux-x64-musl": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.3.0.tgz", - "integrity": "sha512-Z0IADbDo8bh6I7h2IQMx601AdXBLfFpEdUotft86evd/8ZPflZe9COPO8Q1vw+pfLWIUo9zN/JGZvwuAJqduqg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.0.tgz", - "integrity": "sha512-HNZGOUxEmElksYR7S6sC5jTeNGpobAsy9u7Gu0AskJ8/20FR9GqebUyB+HBcU/ax6BHuiuJi+Oda4B+YX6H1yA==", - "bundleDependencies": [ - "@napi-rs/wasm-runtime", - "@emnapi/core", - "@emnapi/runtime", - "@tybys/wasm-util", - "@emnapi/wasi-threads", - "tslib" - ], - "cpu": [ - "wasm32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/core": "^1.10.0", - "@emnapi/runtime": "^1.10.0", - "@emnapi/wasi-threads": "^1.2.1", - "@napi-rs/wasm-runtime": "^1.1.4", - "@tybys/wasm-util": "^0.10.1", - "tslib": "^2.8.1" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.0.tgz", - "integrity": "sha512-Pe+RPVTi1T+qymuuRpcdvwSVZjnll/f7n8gBxMMh3xLTctMDKqpdfGimbMyioqtLhUYZxdJ9wGNhV7MKHvgZsQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-win32-x64-msvc": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.3.0.tgz", - "integrity": "sha512-Mvrf2kXW/yeW/OTezZlCGOirXRcUuLIBx/5Y12BaPM7wJoryG6dfS/NJL8aBPqtTEx/Vm4T4vKzFUcKDT+TKUA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/vite": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.3.0.tgz", - "integrity": "sha512-t6J3OrB5Fc0ExuhohouH0fWUGMYL6PTLhW+E7zIk/pdbnJARZDCwjBznFnkh5ynRnIRSI4YjtTH0t6USjJISrw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@tailwindcss/node": "4.3.0", - "@tailwindcss/oxide": "4.3.0", - "tailwindcss": "4.3.0" - }, - "peerDependencies": { - "vite": "^5.2.0 || ^6 || ^7 || ^8" - } - }, "node_modules/@types/chai": { "version": "5.2.3", "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", @@ -3248,12 +2251,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/trusted-types": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", - "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", - "license": "MIT" - }, "node_modules/@typescript/native-preview": { "version": "7.0.0-dev.20260120.1", "resolved": "https://registry.npmjs.org/@typescript/native-preview/-/native-preview-7.0.0-dev.20260120.1.tgz", @@ -3520,15 +2517,6 @@ "url": "https://opencollective.com/vitest" } }, - "node_modules/@webreflection/alien-signals": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@webreflection/alien-signals/-/alien-signals-0.3.2.tgz", - "integrity": "sha512-DmNjD8Kq5iM+Toirp3llS/izAiI3Dwav5nHRvKdR/YJBTgun3y4xK76rs9CFYD2bZwZJN/rP+HjEqKTteGK+Yw==", - "license": "MIT", - "dependencies": { - "alien-signals": "^2.0.6" - } - }, "node_modules/@xterm/headless": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/@xterm/headless/-/headless-5.5.0.tgz", @@ -3552,12 +2540,6 @@ "node": ">= 14" } }, - "node_modules/alien-signals": { - "version": "2.0.8", - "resolved": "https://registry.npmjs.org/alien-signals/-/alien-signals-2.0.8.tgz", - "integrity": "sha512-844G1VLkk0Pe2SJjY0J8vp8ADI73IM4KliNu2OGlYzWpO28NexEUvjHTcFjFX3VXoiUtwTbHxLNI9ImkcoBqzA==", - "license": "MIT" - }, "node_modules/ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", @@ -3572,6 +2554,7 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -3788,46 +2771,11 @@ "dev": true, "license": "ISC" }, - "node_modules/class-variance-authority": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz", - "integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==", - "license": "Apache-2.0", - "dependencies": { - "clsx": "^2.1.1" - }, - "funding": { - "url": "https://polar.sh/cva" - } - }, - "node_modules/cliui": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", - "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/clsx": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", - "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -3840,6 +2788,7 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, "license": "MIT" }, "node_modules/commander": { @@ -3851,67 +2800,6 @@ "node": ">=18" } }, - "node_modules/concurrently": { - "version": "9.2.1", - "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.2.1.tgz", - "integrity": "sha512-fsfrO0MxV64Znoy8/l1vVIjjHa29SZyyqPgQBwhiDcaW8wJc2W3XWVOGx4M3oJBnv/zdUZIIp1gDeS98GzP8Ng==", - "dev": true, - "license": "MIT", - "dependencies": { - "chalk": "4.1.2", - "rxjs": "7.8.2", - "shell-quote": "1.8.3", - "supports-color": "8.1.1", - "tree-kill": "1.2.2", - "yargs": "17.7.2" - }, - "bin": { - "conc": "dist/bin/concurrently.js", - "concurrently": "dist/bin/concurrently.js" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/open-cli-tools/concurrently?sponsor=1" - } - }, - "node_modules/concurrently/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/concurrently/node_modules/chalk/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/core-util-is": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", - "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", - "license": "MIT" - }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -4007,15 +2895,6 @@ "node": ">=0.3.1" } }, - "node_modules/docx-preview": { - "version": "0.3.7", - "resolved": "https://registry.npmjs.org/docx-preview/-/docx-preview-0.3.7.tgz", - "integrity": "sha512-Lav69CTA/IYZPJTsKH7oYeoZjyg96N0wEJMNslGJnZJ+dMUZK85Lt5ASC79yUlD48ecWjuv+rkcmFt6EVPV0Xg==", - "license": "Apache-2.0", - "dependencies": { - "jszip": ">=3.0.0" - } - }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", @@ -4049,20 +2928,6 @@ "once": "^1.4.0" } }, - "node_modules/enhanced-resolve": { - "version": "5.21.3", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.21.3.tgz", - "integrity": "sha512-QyL119InA+XXEkNLNTPCXPugSvOfhwv0JOlGNzvxs0hZaiHLNvXSpudUWsOlsXGWJh8G6ckCScEkVHfX3kw/2Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.4", - "tapable": "^2.3.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, "node_modules/es-errors": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", @@ -4122,16 +2987,6 @@ "@esbuild/win32-x64": "0.28.0" } }, - "node_modules/escalade": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/estree-walker": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", @@ -4462,16 +3317,6 @@ "node": ">=18" } }, - "node_modules/get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true, - "license": "ISC", - "engines": { - "node": "6.* || 8.* || >= 10.*" - } - }, "node_modules/get-east-asian-width": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.6.0.tgz", @@ -4570,6 +3415,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -4616,12 +3462,6 @@ "dev": true, "license": "MIT" }, - "node_modules/html-parse-string": { - "version": "0.0.9", - "resolved": "https://registry.npmjs.org/html-parse-string/-/html-parse-string-0.0.9.tgz", - "integrity": "sha512-wyGnsOolHbNrcb8N6bdJF4EHyzd3zVGCb9/mBxeNjAYBDOZqD7YkqLBz7kXtdgHwNnV8lN/BpSDpsI1zm8Sd8g==", - "license": "MIT" - }, "node_modules/http-proxy-agent": { "version": "7.0.2", "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", @@ -4694,16 +3534,11 @@ "node": ">= 4" } }, - "node_modules/immediate": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", - "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", - "license": "MIT" - }, "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, "license": "ISC" }, "node_modules/ini": { @@ -4792,12 +3627,6 @@ "node": ">=0.10.0" } }, - "node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", - "license": "MIT" - }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -4925,57 +3754,6 @@ "node": ">=16" } }, - "node_modules/jsonschema": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/jsonschema/-/jsonschema-1.5.0.tgz", - "integrity": "sha512-K+A9hhqbn0f3pJX17Q/7H6yQfD/5OXgdrR5UE12gMXCiN9D5Xq2o5mddV2QEcX/bjla99ASsAAQUyMCCRWAEhw==", - "license": "MIT", - "engines": { - "node": "*" - } - }, - "node_modules/jszip": { - "version": "3.10.1", - "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", - "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", - "license": "(MIT OR GPL-3.0-or-later)", - "dependencies": { - "lie": "~3.3.0", - "pako": "~1.0.2", - "readable-stream": "~2.3.6", - "setimmediate": "^1.0.5" - } - }, - "node_modules/jszip/node_modules/readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "license": "MIT", - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/jszip/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "license": "MIT" - }, - "node_modules/jszip/node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, "node_modules/jwa": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", @@ -4997,31 +3775,6 @@ "safe-buffer": "^5.0.1" } }, - "node_modules/katex": { - "version": "0.16.47", - "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.47.tgz", - "integrity": "sha512-Eeo8Ys1doU1z+x8AZsPpQu+p/QcZBI5PeOo7QGQdy2x2m0MU/hYagBbGOmXwr5KVbEfVuWv9LpnQWeehogurjg==", - "funding": [ - "https://opencollective.com/katex", - "https://github.com/sponsors/katex" - ], - "license": "MIT", - "dependencies": { - "commander": "^8.3.0" - }, - "bin": { - "katex": "cli.js" - } - }, - "node_modules/katex/node_modules/commander": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", - "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", - "license": "MIT", - "engines": { - "node": ">= 12" - } - }, "node_modules/koffi": { "version": "2.16.2", "resolved": "https://registry.npmjs.org/koffi/-/koffi-2.16.2.tgz", @@ -5033,307 +3786,6 @@ "url": "https://liberapay.com/Koromix" } }, - "node_modules/lie": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", - "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", - "license": "MIT", - "dependencies": { - "immediate": "~3.0.5" - } - }, - "node_modules/lightningcss": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", - "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", - "dev": true, - "license": "MPL-2.0", - "dependencies": { - "detect-libc": "^2.0.3" - }, - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - }, - "optionalDependencies": { - "lightningcss-android-arm64": "1.32.0", - "lightningcss-darwin-arm64": "1.32.0", - "lightningcss-darwin-x64": "1.32.0", - "lightningcss-freebsd-x64": "1.32.0", - "lightningcss-linux-arm-gnueabihf": "1.32.0", - "lightningcss-linux-arm64-gnu": "1.32.0", - "lightningcss-linux-arm64-musl": "1.32.0", - "lightningcss-linux-x64-gnu": "1.32.0", - "lightningcss-linux-x64-musl": "1.32.0", - "lightningcss-win32-arm64-msvc": "1.32.0", - "lightningcss-win32-x64-msvc": "1.32.0" - } - }, - "node_modules/lightningcss-android-arm64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", - "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-darwin-arm64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", - "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-darwin-x64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", - "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-freebsd-x64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", - "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm-gnueabihf": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", - "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm64-gnu": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", - "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm64-musl": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", - "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-x64-gnu": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", - "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-x64-musl": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", - "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-win32-arm64-msvc": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", - "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-win32-x64-msvc": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", - "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lit": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/lit/-/lit-3.3.3.tgz", - "integrity": "sha512-fycuvZg/hkpozL00lm1pEJH5nN/lr9ZXd6mJI2HSN4+Bzc+LDNdEApJ6HFbPkdFNHLvOplIIuJvxkS4XUxqirw==", - "license": "BSD-3-Clause", - "dependencies": { - "@lit/reactive-element": "^2.1.0", - "lit-element": "^4.2.0", - "lit-html": "^3.3.0" - } - }, - "node_modules/lit-element": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/lit-element/-/lit-element-4.2.2.tgz", - "integrity": "sha512-aFKhNToWxoyhkNDmWZwEva2SlQia+jfG0fjIWV//YeTaWrVnOxD89dPKfigCUspXFmjzOEUQpOkejH5Ly6sG0w==", - "license": "BSD-3-Clause", - "dependencies": { - "@lit-labs/ssr-dom-shim": "^1.5.0", - "@lit/reactive-element": "^2.1.0", - "lit-html": "^3.3.0" - } - }, - "node_modules/lit-html": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/lit-html/-/lit-html-3.3.3.tgz", - "integrity": "sha512-el8M6jK2o3RXBnrSHX3ZKrsN8zEV63pSExTO1wYJz7QndGYZ8353e2a5PPX+qHe2aGayfnchQmkAojaWAREOIA==", - "license": "BSD-3-Clause", - "dependencies": { - "@types/trusted-types": "^2.0.2" - } - }, "node_modules/lodash-es": { "version": "4.18.1", "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.18.1.tgz", @@ -5362,12 +3814,6 @@ "node": "20 || >=22" } }, - "node_modules/lucide": { - "version": "0.544.0", - "resolved": "https://registry.npmjs.org/lucide/-/lucide-0.544.0.tgz", - "integrity": "sha512-U5ORwr5z9Sx7bNTDFaW55RbjVdQEnAcT3vws9uz3vRT1G4XXJUDAhRZdxhFoIyHEvjmTkzzlEhjSLYM5n4mb5w==", - "license": "ISC" - }, "node_modules/magic-string": { "version": "0.30.21", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", @@ -5496,16 +3942,6 @@ "dev": true, "license": "MIT" }, - "node_modules/mri": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", - "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -5626,15 +4062,6 @@ "node": ">=4" } }, - "node_modules/ollama": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/ollama/-/ollama-0.6.3.tgz", - "integrity": "sha512-KEWEhIqE5wtfzEIZbDCLH51VFZ6Z3ZSa6sIOg/E/tBV8S51flyqBOXi+bRxlOYKDf8i327zG9eSTb8IJxvm3Zg==", - "license": "MIT", - "dependencies": { - "whatwg-fetch": "^3.6.20" - } - }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -5702,12 +4129,6 @@ "dev": true, "license": "BlueOak-1.0.0" }, - "node_modules/pako": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", - "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", - "license": "(MIT AND Zlib)" - }, "node_modules/partial-json": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/partial-json/-/partial-json-0.1.7.tgz", @@ -5778,18 +4199,6 @@ "node": ">= 14.16" } }, - "node_modules/pdfjs-dist": { - "version": "5.4.394", - "resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-5.4.394.tgz", - "integrity": "sha512-9ariAYGqUJzx+V/1W4jHyiyCep6IZALmDzoaTLZ6VNu8q9LWi1/ukhzHgE2Xsx96AZi0mbZuK4/ttIbqSbLypg==", - "license": "Apache-2.0", - "engines": { - "node": ">=20.16.0 || >=22.3.0" - }, - "optionalDependencies": { - "@napi-rs/canvas": "^0.1.81" - } - }, "node_modules/pi-extension-custom-provider-anthropic": { "resolved": "packages/coding-agent/examples/extensions/custom-provider-anthropic", "link": true @@ -5806,10 +4215,6 @@ "resolved": "packages/coding-agent/examples/extensions/with-deps", "link": true }, - "node_modules/pi-web-ui-example": { - "resolved": "packages/web-ui/example", - "link": true - }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -5887,12 +4292,6 @@ "node": ">=10" } }, - "node_modules/process-nextick-args": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", - "license": "MIT" - }, "node_modules/proper-lockfile": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz", @@ -6012,16 +4411,6 @@ "node": ">= 0.10" } }, - "node_modules/require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/resolve": { "version": "1.22.12", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", @@ -6140,16 +4529,6 @@ "queue-microtask": "^1.2.2" } }, - "node_modules/rxjs": { - "version": "7.8.2", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", - "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.1.0" - } - }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -6183,12 +4562,6 @@ "node": ">=10" } }, - "node_modules/setimmediate": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", - "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", - "license": "MIT" - }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -6462,22 +4835,6 @@ ], "license": "MIT" }, - "node_modules/supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" - } - }, "node_modules/supports-preserve-symlinks-flag": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", @@ -6491,55 +4848,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/tailwind-merge": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.6.0.tgz", - "integrity": "sha512-uxL7qAVQriqRQPAyK3pj66VqskWqoZ37PW94jwOTwNfq/z9oyu1V+eqrZqtR2+fCiXdYOZe/Modt8GtvqNzu+w==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/dcastil" - } - }, - "node_modules/tailwind-variants": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/tailwind-variants/-/tailwind-variants-3.2.2.tgz", - "integrity": "sha512-Mi4kHeMTLvKlM98XPnK+7HoBPmf4gygdFmqQPaDivc3DpYS6aIY6KiG/PgThrGvii5YZJqRsPz0aPyhoFzmZgg==", - "license": "MIT", - "engines": { - "node": ">=16.x", - "pnpm": ">=7.x" - }, - "peerDependencies": { - "tailwind-merge": ">=3.0.0", - "tailwindcss": "*" - }, - "peerDependenciesMeta": { - "tailwind-merge": { - "optional": true - } - } - }, - "node_modules/tailwindcss": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.0.tgz", - "integrity": "sha512-y6nxMGB1nMW9R6k96e5gdIFzcfL/gTJRNaqGes1YvkLnPVXzWgbqFF2yLC0T8G774n24cx3Pe8XrKoniCOAH+Q==", - "license": "MIT" - }, - "node_modules/tapable": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", - "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, "node_modules/tar-fs": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", @@ -6769,16 +5077,6 @@ "node": ">=8.0" } }, - "node_modules/tree-kill": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", - "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", - "dev": true, - "license": "MIT", - "bin": { - "tree-kill": "cli.js" - } - }, "node_modules/ts-algebra": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ts-algebra/-/ts-algebra-2.0.0.tgz", @@ -6843,15 +5141,6 @@ "node": ">=14.17" } }, - "node_modules/uhtml": { - "version": "5.0.9", - "resolved": "https://registry.npmjs.org/uhtml/-/uhtml-5.0.9.tgz", - "integrity": "sha512-qPyu3vGilaLe6zrjOCD/xezWEHLwdevxmbY3hzyhT25KBDF4F7YYW3YZcL3kylD/6dMoVISHjn8ggV3+9FY+5g==", - "license": "MIT", - "dependencies": { - "@webreflection/alien-signals": "^0.3.2" - } - }, "node_modules/undici": { "version": "8.3.0", "resolved": "https://registry.npmjs.org/undici/-/undici-8.3.0.tgz", @@ -6871,6 +5160,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, "license": "MIT" }, "node_modules/vite": { @@ -7581,12 +5871,6 @@ "node": ">= 8" } }, - "node_modules/whatwg-fetch": { - "version": "3.6.20", - "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.20.tgz", - "integrity": "sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==", - "license": "MIT" - }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -7619,24 +5903,6 @@ "node": ">=8" } }, - "node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, "node_modules/wrap-ansi-cjs": { "name": "wrap-ansi", "version": "7.0.0", @@ -7684,18 +5950,6 @@ } } }, - "node_modules/xlsx": { - "version": "0.20.3", - "resolved": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz", - "integrity": "sha512-oLDq3jw7AcLqKWH2AhCpVTZl8mf6X2YReP+Neh0SJUzV/BdZYjth94tG5toiMB1PPrYtxOCfaoUCkvtuH+3AJA==", - "license": "Apache-2.0", - "bin": { - "xlsx": "bin/xlsx.njs" - }, - "engines": { - "node": ">=0.8" - } - }, "node_modules/xml-naming": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/xml-naming/-/xml-naming-0.1.0.tgz", @@ -7711,16 +5965,6 @@ "node": ">=16.0.0" } }, - "node_modules/y18n": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=10" - } - }, "node_modules/yaml": { "version": "2.9.0", "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", @@ -7736,35 +5980,6 @@ "url": "https://github.com/sponsors/eemeli" } }, - "node_modules/yargs": { - "version": "17.7.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", - "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", - "dev": true, - "license": "MIT", - "dependencies": { - "cliui": "^8.0.1", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.3", - "y18n": "^5.0.5", - "yargs-parser": "^21.1.1" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/yargs-parser": { - "version": "21.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", - "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=12" - } - }, "node_modules/zod": { "version": "3.25.76", "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", @@ -7981,49 +6196,6 @@ "optionalDependencies": { "koffi": "^2.9.0" } - }, - "packages/web-ui": { - "name": "@earendil-works/pi-web-ui", - "version": "0.75.3", - "license": "MIT", - "dependencies": { - "@earendil-works/pi-ai": "^0.75.3", - "@earendil-works/pi-tui": "^0.75.3", - "@lmstudio/sdk": "^1.5.0", - "docx-preview": "^0.3.7", - "jszip": "^3.10.1", - "lucide": "^0.544.0", - "ollama": "^0.6.0", - "pdfjs-dist": "5.4.394", - "typebox": "^1.1.24", - "xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz" - }, - "devDependencies": { - "@mariozechner/mini-lit": "^0.2.0", - "@tailwindcss/cli": "^4.0.0-beta.14", - "concurrently": "^9.2.1", - "typescript": "^5.7.3" - }, - "peerDependencies": { - "@mariozechner/mini-lit": "^0.2.0", - "lit": "^3.3.1" - } - }, - "packages/web-ui/example": { - "name": "pi-web-ui-example", - "version": "0.75.3", - "dependencies": { - "@earendil-works/pi-ai": "file:../../ai", - "@earendil-works/pi-web-ui": "file:../", - "@mariozechner/mini-lit": "^0.2.0", - "lit": "^3.3.1", - "lucide": "^0.544.0" - }, - "devDependencies": { - "@tailwindcss/vite": "^4.1.17", - "typescript": "^5.7.3", - "vite": "^7.1.6" - } } } } diff --git a/package.json b/package.json index af486099..c0b10fe0 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,6 @@ "type": "module", "workspaces": [ "packages/*", - "packages/web-ui/example", "packages/coding-agent/examples/extensions/with-deps", "packages/coding-agent/examples/extensions/custom-provider-anthropic", "packages/coding-agent/examples/extensions/custom-provider-gitlab-duo", @@ -12,10 +11,8 @@ ], "scripts": { "clean": "npm run clean --workspaces", - "build": "cd packages/tui && npm run build && cd ../ai && npm run build && cd ../agent && npm run build && cd ../coding-agent && npm run build && cd ../web-ui && npm run build", - "dev": "concurrently --names \"ai,agent,coding-agent,web-ui,tui\" --prefix-colors \"cyan,yellow,red,green,magenta\" \"cd packages/ai && npm run dev\" \"cd packages/agent && npm run dev\" \"cd packages/coding-agent && npm run dev\" \"cd packages/web-ui && npm run dev\" \"cd packages/tui && npm run dev\"", - "dev:tsc": "concurrently --names \"ai,web-ui\" --prefix-colors \"cyan,green\" \"cd packages/ai && npm run dev:tsc\" \"cd packages/web-ui && npm run dev:tsc\"", - "check": "biome check --write --error-on-warnings . && tsgo --noEmit && npm run check:browser-smoke && cd packages/web-ui && npm run check", + "build": "cd packages/tui && npm run build && cd ../ai && npm run build && cd ../agent && npm run build && cd ../coding-agent && npm run build", + "check": "biome check --write --error-on-warnings . && tsgo --noEmit && npm run check:browser-smoke", "check:browser-smoke": "node scripts/check-browser-smoke.mjs", "profile:tui": "node scripts/profile-coding-agent-node.mjs --mode tui", "profile:rpc": "node scripts/profile-coding-agent-node.mjs --mode rpc", @@ -37,7 +34,6 @@ "@biomejs/biome": "2.3.5", "@types/node": "^22.10.5", "@typescript/native-preview": "7.0.0-dev.20260120.1", - "concurrently": "^9.2.1", "husky": "^9.1.7", "jiti": "^2.7.0", "tsx": "^4.20.3", diff --git a/packages/agent/CHANGELOG.md b/packages/agent/CHANGELOG.md index 81b71b75..1fbe2915 100644 --- a/packages/agent/CHANGELOG.md +++ b/packages/agent/CHANGELOG.md @@ -5,6 +5,7 @@ ### Changed - Changed source syntax to avoid TypeScript constructs that require JavaScript emit, keeping the package compatible with Node.js strip-only TypeScript checks. +- Removed the package-level development watch script now that the root TypeScript check validates strip-only-compatible sources. ### Fixed diff --git a/packages/agent/package.json b/packages/agent/package.json index ef39353f..37e666ba 100644 --- a/packages/agent/package.json +++ b/packages/agent/package.json @@ -23,7 +23,6 @@ "scripts": { "clean": "shx rm -rf dist", "build": "tsgo -p tsconfig.build.json", - "dev": "tsgo -p tsconfig.build.json --watch --preserveWatchOutput", "test": "vitest --run", "test:harness": "vitest --run --config vitest.harness.config.ts", "coverage:harness": "vitest --run --config vitest.harness.config.ts --coverage", diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index 1b6d144a..d4560aa0 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -5,6 +5,7 @@ ### Changed - Changed source syntax to avoid TypeScript constructs that require JavaScript emit, keeping the package compatible with Node.js strip-only TypeScript checks. +- Removed the package-level development watch scripts now that the root TypeScript check validates strip-only-compatible sources. ### Fixed diff --git a/packages/ai/package.json b/packages/ai/package.json index daeb856b..2fa04afc 100644 --- a/packages/ai/package.json +++ b/packages/ai/package.json @@ -63,8 +63,6 @@ "generate-models": "npx tsx scripts/generate-models.ts", "generate-image-models": "npx tsx scripts/generate-image-models.ts", "build": "npm run generate-models && npm run generate-image-models && tsgo -p tsconfig.build.json", - "dev": "tsgo -p tsconfig.build.json --watch --preserveWatchOutput", - "dev:tsc": "tsgo -p tsconfig.build.json --watch --preserveWatchOutput", "test": "vitest --run", "prepublishOnly": "npm run clean && npm run build" }, diff --git a/packages/ai/src/env-api-keys.ts b/packages/ai/src/env-api-keys.ts index 4aa0f760..a00e8470 100644 --- a/packages/ai/src/env-api-keys.ts +++ b/packages/ai/src/env-api-keys.ts @@ -1,4 +1,4 @@ -// NEVER convert to top-level imports - breaks browser/Vite builds (web-ui) +// NEVER convert to top-level imports - breaks browser/Vite builds let _existsSync: typeof import("node:fs").existsSync | null = null; let _homedir: typeof import("node:os").homedir | null = null; let _join: typeof import("node:path").join | null = null; diff --git a/packages/ai/src/providers/openai-codex-responses.ts b/packages/ai/src/providers/openai-codex-responses.ts index 7bb9ca47..70df0810 100644 --- a/packages/ai/src/providers/openai-codex-responses.ts +++ b/packages/ai/src/providers/openai-codex-responses.ts @@ -6,7 +6,7 @@ import type { ResponseStreamEvent, } from "openai/resources/responses/responses.js"; -// NEVER convert to top-level runtime imports - breaks browser/Vite builds (web-ui) +// NEVER convert to top-level runtime imports - breaks browser/Vite builds let _os: typeof NodeOs | null = null; type DynamicImport = (specifier: string) => Promise; diff --git a/packages/ai/src/utils/oauth/openai-codex.ts b/packages/ai/src/utils/oauth/openai-codex.ts index 28600d23..7dd1fbb8 100644 --- a/packages/ai/src/utils/oauth/openai-codex.ts +++ b/packages/ai/src/utils/oauth/openai-codex.ts @@ -5,7 +5,7 @@ * It is only intended for CLI use, not browser environments. */ -// NEVER convert to top-level imports - breaks browser/Vite builds (web-ui) +// NEVER convert to top-level imports - breaks browser/Vite builds let _randomBytes: typeof import("node:crypto").randomBytes | null = null; let _http: typeof import("node:http") | null = null; if (typeof process !== "undefined" && (process.versions?.node || process.versions?.bun)) { diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 9625a075..479b5560 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -5,6 +5,7 @@ ### Changed - Changed source syntax to avoid TypeScript constructs that require JavaScript emit, keeping core sources compatible with Node.js strip-only TypeScript checks. +- Removed web UI workspace references from the CLI package and dropped the package-level development watch script. ### Fixed diff --git a/packages/coding-agent/package.json b/packages/coding-agent/package.json index 30982c27..82b5ec76 100644 --- a/packages/coding-agent/package.json +++ b/packages/coding-agent/package.json @@ -29,7 +29,6 @@ ], "scripts": { "clean": "shx rm -rf dist", - "dev": "tsgo -p tsconfig.build.json --watch --preserveWatchOutput", "build": "tsgo -p tsconfig.build.json && shx chmod +x dist/cli.js && npm run copy-assets", "build:binary": "npm --prefix ../tui run build && npm --prefix ../ai run build && npm --prefix ../agent run build && npm run build && bun build --compile ./dist/bun/cli.js --outfile dist/pi && npm run copy-binary-assets", "copy-assets": "shx mkdir -p dist/modes/interactive/theme && shx cp src/modes/interactive/theme/*.json dist/modes/interactive/theme/ && shx mkdir -p dist/modes/interactive/assets && shx cp src/modes/interactive/assets/*.png dist/modes/interactive/assets/ && shx mkdir -p dist/core/export-html/vendor && shx cp src/core/export-html/template.html src/core/export-html/template.css src/core/export-html/template.js dist/core/export-html/ && shx cp src/core/export-html/vendor/*.js dist/core/export-html/vendor/", diff --git a/packages/coding-agent/src/modes/interactive/components/footer.ts b/packages/coding-agent/src/modes/interactive/components/footer.ts index 283c1c0a..a48898a5 100644 --- a/packages/coding-agent/src/modes/interactive/components/footer.ts +++ b/packages/coding-agent/src/modes/interactive/components/footer.ts @@ -16,7 +16,7 @@ function sanitizeStatusText(text: string): string { } /** - * Format token counts (similar to web-ui) + * Format token counts for compact footer display. */ function formatTokens(count: number): string { if (count < 1000) return count.toString(); diff --git a/packages/tui/CHANGELOG.md b/packages/tui/CHANGELOG.md index 47656ed1..8cae4810 100644 --- a/packages/tui/CHANGELOG.md +++ b/packages/tui/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Changed + +- Removed the package-level development watch script now that the root TypeScript check validates strip-only-compatible sources. + ## [0.75.3] - 2026-05-18 ## [0.75.2] - 2026-05-18 diff --git a/packages/tui/package.json b/packages/tui/package.json index b8695403..fd678612 100644 --- a/packages/tui/package.json +++ b/packages/tui/package.json @@ -7,7 +7,6 @@ "scripts": { "clean": "shx rm -rf dist", "build": "tsgo -p tsconfig.build.json", - "dev": "tsgo -p tsconfig.build.json --watch --preserveWatchOutput", "test": "node --test --import tsx test/*.test.ts", "prepublishOnly": "npm run clean && npm run build" }, diff --git a/packages/tui/test/autocomplete.test.ts b/packages/tui/test/autocomplete.test.ts index 14ad9930..3e5c8240 100644 --- a/packages/tui/test/autocomplete.test.ts +++ b/packages/tui/test/autocomplete.test.ts @@ -365,7 +365,7 @@ describe("CombinedAutocompleteProvider", () => { dirs: ["packages/coding-agent/examples/extensions/plan-mode"], files: { "packages/coding-agent/examples/extensions/plan-mode/README.md": "readme", - "packages/web-ui/docs/plan.md": "plan", + "packages/tui/docs/plan.md": "plan", }, }; setupFolder(normalBaseDir, structure); @@ -385,7 +385,7 @@ describe("CombinedAutocompleteProvider", () => { assert.ok( normalize(normalResult).includes("plan-mode/ :: packages/coding-agent/examples/extensions/plan-mode"), ); - assert.ok(normalize(normalResult).includes("plan.md :: packages/web-ui/docs/plan.md")); + assert.ok(normalize(normalResult).includes("plan.md :: packages/tui/docs/plan.md")); }); test("continues autocomplete inside quoted @ paths", async () => { diff --git a/packages/web-ui/CHANGELOG.md b/packages/web-ui/CHANGELOG.md deleted file mode 100644 index fa32c7ae..00000000 --- a/packages/web-ui/CHANGELOG.md +++ /dev/null @@ -1,421 +0,0 @@ -# Changelog - -## [Unreleased] - -## [0.75.3] - 2026-05-18 - -## [0.75.2] - 2026-05-18 - -## [0.75.1] - 2026-05-18 - -## [0.74.1] - 2026-05-16 - -## [0.74.0] - 2026-05-07 - -## [0.73.1] - 2026-05-07 - -## [0.73.0] - 2026-05-04 - -## [0.72.1] - 2026-05-02 - -## [0.72.0] - 2026-05-01 - -## [0.71.1] - 2026-05-01 - -## [0.71.0] - 2026-04-30 - -## [0.70.6] - 2026-04-28 - -## [0.70.5] - 2026-04-27 - -## [0.70.4] - 2026-04-27 - -## [0.70.3] - 2026-04-27 - -## [0.70.2] - 2026-04-24 - -## [0.70.1] - 2026-04-24 - -## [0.70.0] - 2026-04-23 - -## [0.69.0] - 2026-04-22 - -### Breaking Changes - -- Migrated the web UI's TypeBox-based tool definitions and runtime dependency from `@sinclair/typebox` 0.34.x to `typebox` 1.x. Install and import from `typebox` instead of `@sinclair/typebox` when embedding or extending `@mariozechner/pi-web-ui` with shared TypeBox schemas ([#3112](https://github.com/badlogic/pi-mono/issues/3112)) - -### Fixed - -- Render SVG artifact previews through a blob-backed image instead of injecting untrusted SVG markup into the page DOM ([#3552](https://github.com/badlogic/pi-mono/issues/3552)) - -## [0.68.1] - 2026-04-22 - -## [0.68.0] - 2026-04-20 - -## [0.67.68] - 2026-04-17 - -## [0.67.67] - 2026-04-17 - -## [0.67.6] - 2026-04-16 - -## [0.67.5] - 2026-04-16 - -## [0.67.4] - 2026-04-16 - -## [0.67.3] - 2026-04-15 - -## [0.67.2] - 2026-04-14 - -## [0.67.1] - 2026-04-13 - -## [0.67.0] - 2026-04-13 - -## [0.66.1] - 2026-04-08 - -## [0.66.0] - 2026-04-08 - -## [0.65.2] - 2026-04-06 - -## [0.65.1] - 2026-04-05 - -## [0.65.0] - 2026-04-03 - -## [0.64.0] - 2026-03-29 - -## [0.63.2] - 2026-03-29 - -## [0.63.1] - 2026-03-27 - -## [0.63.0] - 2026-03-27 - -## [0.62.0] - 2026-03-23 - -## [0.61.1] - 2026-03-20 - -## [0.61.0] - 2026-03-20 - -## [0.60.0] - 2026-03-18 - -## [0.59.0] - 2026-03-17 - -### Added - -- Exported `CustomProviderDialog` from `@mariozechner/pi-web-ui` ([#2267](https://github.com/badlogic/pi-mono/issues/2267)) - -## [0.58.4] - 2026-03-16 - -### Added - -- `onModelSelect` callback on `AgentInterface` and `ChatPanel.setAgent` config -- `allowedProviders` filter on `ModelSelector.open()` to restrict visible models -- `onClose` callback on `SettingsDialog.open()` -- `state_change` event emitted by Agent on `setModel()` and `setThinkingLevel()` -- Subsequence-based fuzzy search in model selector (replaces substring matching) -- `openai-codex` and `github-copilot` to `shouldUseProxyForProvider` - -### Changed - -- Anthropic test model updated from `claude-3-5-haiku-20241022` to `claude-haiku-4-5` - -### Fixed - -- `AgentInterface` clears streaming container on `message_end` to prevent duplicate tool rendering - -## [0.58.3] - 2026-03-15 - -### Fixed - -- Build `@mariozechner/pi-web-ui` with `tsc` instead of `tsgo` so Lit decorator-based state updates rerender correctly. - -## [0.58.2] - 2026-03-15 - -## [0.58.1] - 2026-03-14 - -## [0.58.0] - 2026-03-14 - -## [0.57.1] - 2026-03-07 - -## [0.57.0] - 2026-03-07 - -## [0.56.3] - 2026-03-06 - -## [0.56.2] - 2026-03-05 - -## [0.56.1] - 2026-03-05 - -## [0.56.0] - 2026-03-04 - -## [0.55.4] - 2026-03-02 - -## [0.55.3] - 2026-02-27 - -## [0.55.2] - 2026-02-27 - -## [0.55.1] - 2026-02-26 - -## [0.55.0] - 2026-02-24 - -## [0.54.2] - 2026-02-23 - -## [0.54.1] - 2026-02-22 - -## [0.54.0] - 2026-02-19 - -## [0.53.1] - 2026-02-19 - -## [0.53.0] - 2026-02-17 - -## [0.52.12] - 2026-02-13 - -## [0.52.11] - 2026-02-13 - -## [0.52.10] - 2026-02-12 - -### Fixed - -- Made model selector search case-insensitive by normalizing query tokens, fixing auto-capitalized mobile input filtering ([#1443](https://github.com/badlogic/pi-mono/issues/1443)) - -## [0.52.9] - 2026-02-08 - -## [0.52.8] - 2026-02-07 - -## [0.52.7] - 2026-02-06 - -## [0.52.6] - 2026-02-05 - -## [0.52.5] - 2026-02-05 - -## [0.52.4] - 2026-02-05 - -## [0.52.3] - 2026-02-05 - -## [0.52.2] - 2026-02-05 - -## [0.52.1] - 2026-02-05 - -## [0.52.0] - 2026-02-05 - -## [0.51.6] - 2026-02-04 - -## [0.51.5] - 2026-02-04 - -## [0.51.4] - 2026-02-03 - -## [0.51.3] - 2026-02-03 - -## [0.51.2] - 2026-02-03 - -## [0.51.1] - 2026-02-02 - -## [0.51.0] - 2026-02-01 - -## [0.50.9] - 2026-02-01 - -## [0.50.8] - 2026-02-01 - -## [0.50.7] - 2026-01-31 - -## [0.50.6] - 2026-01-30 - -## [0.50.5] - 2026-01-30 - -## [0.50.3] - 2026-01-29 - -## [0.50.2] - 2026-01-29 - -### Added - -- Exported `CustomProviderCard`, `ProviderKeyInput`, `AbortedMessage`, and `ToolMessageDebugView` components for custom UIs ([#1015](https://github.com/badlogic/pi-mono/issues/1015)) - -## [0.50.1] - 2026-01-26 - -## [0.50.0] - 2026-01-26 - -## [0.49.3] - 2026-01-22 - -### Changed - -- Updated tsgo to 7.0.0-dev.20260120.1 for decorator support ([#873](https://github.com/badlogic/pi-mono/issues/873)) - -## [0.49.2] - 2026-01-19 - -## [0.49.1] - 2026-01-18 - -## [0.49.0] - 2026-01-17 - -## [0.48.0] - 2026-01-16 - -## [0.47.0] - 2026-01-16 - -## [0.46.0] - 2026-01-15 - -## [0.45.7] - 2026-01-13 - -## [0.45.6] - 2026-01-13 - -## [0.45.5] - 2026-01-13 - -## [0.45.4] - 2026-01-13 - -## [0.45.3] - 2026-01-13 - -## [0.45.2] - 2026-01-13 - -## [0.45.1] - 2026-01-13 - -## [0.45.0] - 2026-01-13 - -## [0.44.0] - 2026-01-12 - -## [0.43.0] - 2026-01-11 - -## [0.42.5] - 2026-01-11 - -## [0.42.4] - 2026-01-10 - -## [0.42.3] - 2026-01-10 - -## [0.42.2] - 2026-01-10 - -## [0.42.1] - 2026-01-09 - -## [0.42.0] - 2026-01-09 - -## [0.41.0] - 2026-01-09 - -## [0.40.1] - 2026-01-09 - -## [0.40.0] - 2026-01-08 - -## [0.39.1] - 2026-01-08 - -## [0.39.0] - 2026-01-08 - -## [0.38.0] - 2026-01-08 - -## [0.37.8] - 2026-01-07 - -## [0.37.7] - 2026-01-07 - -## [0.37.6] - 2026-01-06 - -## [0.37.5] - 2026-01-06 - -## [0.37.4] - 2026-01-06 - -## [0.37.3] - 2026-01-06 - -## [0.37.2] - 2026-01-05 - -## [0.37.1] - 2026-01-05 - -## [0.37.0] - 2026-01-05 - -## [0.36.0] - 2026-01-05 - -## [0.35.0] - 2026-01-05 - -## [0.34.2] - 2026-01-04 - -## [0.34.1] - 2026-01-04 - -## [0.34.0] - 2026-01-04 - -## [0.33.0] - 2026-01-04 - -## [0.32.3] - 2026-01-03 - -## [0.32.2] - 2026-01-03 - -## [0.32.1] - 2026-01-03 - -## [0.32.0] - 2026-01-03 - -## [0.31.1] - 2026-01-02 - -## [0.31.0] - 2026-01-02 - -### Breaking Changes - -- **Agent class moved to `@mariozechner/pi-agent-core`**: The `Agent` class, `AgentState`, and related types are no longer exported from this package. Import them from `@mariozechner/pi-agent-core` instead. - -- **Transport abstraction removed**: `ProviderTransport`, `AppTransport`, `AgentTransport` interface, and related types have been removed. The `Agent` class now uses `streamFn` for custom streaming. - -- **`AppMessage` renamed to `AgentMessage`**: Now imported from `@mariozechner/pi-agent-core`. Custom message types use declaration merging on `CustomAgentMessages` interface. - -- **`UserMessageWithAttachments` is now a custom message type**: Has `role: "user-with-attachments"` instead of `role: "user"`. Use `isUserMessageWithAttachments()` type guard. - -- **`CustomMessages` interface removed**: Use declaration merging on `CustomAgentMessages` from `@mariozechner/pi-agent-core` instead. - -- **`agent.appendMessage()` removed**: Use `agent.queueMessage()` instead. - -- **Agent event types changed**: `AgentInterface` now handles new event types from `@mariozechner/pi-agent-core`: `message_start`, `message_end`, `message_update`, `turn_start`, `turn_end`, `agent_start`, `agent_end`. - -### Added - -- **`defaultConvertToLlm`**: Default message transformer that handles `UserMessageWithAttachments` and `ArtifactMessage`. Apps can extend this for custom message types. - -- **`convertAttachments`**: Utility to convert `Attachment[]` to LLM content blocks (images and extracted document text). - -- **`isUserMessageWithAttachments` / `isArtifactMessage`**: Type guard functions for custom message types. - -- **`createStreamFn`**: Creates a stream function with CORS proxy support. Reads proxy settings on each call for dynamic configuration. - -- **Default `streamFn` and `getApiKey`**: `AgentInterface` now sets sensible defaults if not provided: - - `streamFn`: Uses `createStreamFn` with proxy settings from storage - - `getApiKey`: Reads from `providerKeys` storage - -- **Proxy utilities exported**: `applyProxyIfNeeded`, `shouldUseProxyForProvider`, `isCorsError`, `createStreamFn` - -### Removed - -- `Agent` class (moved to `@mariozechner/pi-agent-core`) -- `ProviderTransport` class -- `AppTransport` class -- `AgentTransport` interface -- `AgentRunConfig` type -- `ProxyAssistantMessageEvent` type -- `test-sessions.ts` example file - -### Migration Guide - -**Before (0.30.x):** -```typescript -import { Agent, ProviderTransport, type AppMessage } from '@mariozechner/pi-web-ui'; - -const agent = new Agent({ - transport: new ProviderTransport(), - messageTransformer: (messages: AppMessage[]) => messages.filter(...) -}); -``` - -**After:** -```typescript -import { Agent, type AgentMessage } from '@mariozechner/pi-agent-core'; -import { defaultConvertToLlm } from '@mariozechner/pi-web-ui'; - -const agent = new Agent({ - convertToLlm: (messages: AgentMessage[]) => { - // Extend defaultConvertToLlm for custom types - return defaultConvertToLlm(messages); - } -}); -// AgentInterface will set streamFn and getApiKey defaults automatically -``` - -**Custom message types:** -```typescript -// Before: declaration merging on CustomMessages -declare module "@mariozechner/pi-web-ui" { - interface CustomMessages { - "my-message": MyMessage; - } -} - -// After: declaration merging on CustomAgentMessages -declare module "@mariozechner/pi-agent-core" { - interface CustomAgentMessages { - "my-message": MyMessage; - } -} -``` diff --git a/packages/web-ui/README.md b/packages/web-ui/README.md deleted file mode 100644 index 7482caef..00000000 --- a/packages/web-ui/README.md +++ /dev/null @@ -1,601 +0,0 @@ -# @earendil-works/pi-web-ui - -Reusable web UI components for building AI chat interfaces powered by [@earendil-works/pi-ai](../ai) and [@earendil-works/pi-agent-core](../agent). - -Built with [mini-lit](https://github.com/badlogic/mini-lit) web components and Tailwind CSS v4. - -## Features - -- **Chat UI**: Complete interface with message history, streaming, and tool execution -- **Tools**: JavaScript REPL, document extraction, and artifacts (HTML, SVG, Markdown, etc.) -- **Attachments**: PDF, DOCX, XLSX, PPTX, images with preview and text extraction -- **Artifacts**: Interactive HTML, SVG, Markdown with sandboxed execution -- **Storage**: IndexedDB-backed storage for sessions, API keys, and settings -- **CORS Proxy**: Automatic proxy handling for browser environments -- **Custom Providers**: Support for Ollama, LM Studio, vLLM, and OpenAI-compatible APIs - -## Installation - -```bash -npm install @earendil-works/pi-web-ui @earendil-works/pi-agent-core @earendil-works/pi-ai -``` - -## Quick Start - -See the [example](./example) directory for a complete working application. - -```typescript -import { Agent } from '@earendil-works/pi-agent-core'; -import { getModel } from '@earendil-works/pi-ai'; -import { - ChatPanel, - AppStorage, - IndexedDBStorageBackend, - ProviderKeysStore, - SessionsStore, - SettingsStore, - setAppStorage, - defaultConvertToLlm, - ApiKeyPromptDialog, -} from '@earendil-works/pi-web-ui'; -import '@earendil-works/pi-web-ui/app.css'; - -// Set up storage -const settings = new SettingsStore(); -const providerKeys = new ProviderKeysStore(); -const sessions = new SessionsStore(); - -const backend = new IndexedDBStorageBackend({ - dbName: 'my-app', - version: 1, - stores: [ - settings.getConfig(), - providerKeys.getConfig(), - sessions.getConfig(), - SessionsStore.getMetadataConfig(), - ], -}); - -settings.setBackend(backend); -providerKeys.setBackend(backend); -sessions.setBackend(backend); - -const storage = new AppStorage(settings, providerKeys, sessions, undefined, backend); -setAppStorage(storage); - -// Create agent -const agent = new Agent({ - initialState: { - systemPrompt: 'You are a helpful assistant.', - model: getModel('anthropic', 'claude-sonnet-4-5-20250929'), - thinkingLevel: 'off', - messages: [], - tools: [], - }, - convertToLlm: defaultConvertToLlm, -}); - -// Create chat panel -const chatPanel = new ChatPanel(); -await chatPanel.setAgent(agent, { - onApiKeyRequired: (provider) => ApiKeyPromptDialog.prompt(provider), -}); - -document.body.appendChild(chatPanel); -``` - -## Architecture - -``` -┌─────────────────────────────────────────────────────┐ -│ ChatPanel │ -│ ┌─────────────────────┐ ┌─────────────────────┐ │ -│ │ AgentInterface │ │ ArtifactsPanel │ │ -│ │ (messages, input) │ │ (HTML, SVG, MD) │ │ -│ └─────────────────────┘ └─────────────────────┘ │ -└─────────────────────────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────┐ -│ Agent (from pi-agent-core) │ -│ - State management (messages, model, tools) │ -│ - Event emission (agent_start, message_update, ...)│ -│ - Tool execution │ -└─────────────────────────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────┐ -│ AppStorage │ -│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ -│ │ Settings │ │ Provider │ │ Sessions │ │ -│ │ Store │ │Keys Store│ │ Store │ │ -│ └──────────┘ └──────────┘ └──────────┘ │ -│ │ │ -│ IndexedDBStorageBackend │ -└─────────────────────────────────────────────────────┘ -``` - -## Components - -### ChatPanel - -High-level chat interface with built-in artifacts panel. - -```typescript -const chatPanel = new ChatPanel(); -await chatPanel.setAgent(agent, { - // Prompt for API key when needed - onApiKeyRequired: async (provider) => ApiKeyPromptDialog.prompt(provider), - - // Hook before sending messages - onBeforeSend: async () => { /* save draft, etc. */ }, - - // Handle cost display click - onCostClick: () => { /* show cost breakdown */ }, - - // Custom sandbox URL for browser extensions - sandboxUrlProvider: () => chrome.runtime.getURL('sandbox.html'), - - // Add custom tools - toolsFactory: (agent, agentInterface, artifactsPanel, runtimeProvidersFactory) => { - const replTool = createJavaScriptReplTool(); - replTool.runtimeProvidersFactory = runtimeProvidersFactory; - return [replTool]; - }, -}); -``` - -### AgentInterface - -Lower-level chat interface for custom layouts. - -```typescript -const chat = document.createElement('agent-interface') as AgentInterface; -chat.session = agent; -chat.enableAttachments = true; -chat.enableModelSelector = true; -chat.enableThinkingSelector = true; -chat.onApiKeyRequired = async (provider) => { /* ... */ }; -chat.onBeforeSend = async () => { /* ... */ }; -``` - -Properties: -- `session`: Agent instance -- `enableAttachments`: Show attachment button (default: true) -- `enableModelSelector`: Show model selector (default: true) -- `enableThinkingSelector`: Show thinking level selector (default: true) -- `showThemeToggle`: Show theme toggle (default: false) - -### Agent (from pi-agent-core) - -```typescript -import { Agent } from '@earendil-works/pi-agent-core'; - -const agent = new Agent({ - initialState: { - model: getModel('anthropic', 'claude-sonnet-4-5-20250929'), - systemPrompt: 'You are helpful.', - thinkingLevel: 'off', - messages: [], - tools: [], - }, - convertToLlm: defaultConvertToLlm, -}); - -// Events -agent.subscribe((event) => { - switch (event.type) { - case 'agent_start': // Agent loop started - case 'agent_end': // Agent loop finished - case 'turn_start': // LLM call started - case 'turn_end': // LLM call finished - case 'message_start': - case 'message_update': // Streaming update - case 'message_end': - break; - } -}); - -// Send message -await agent.prompt('Hello!'); -await agent.prompt({ role: 'user-with-attachments', content: 'Check this', attachments, timestamp: Date.now() }); - -// Control -agent.abort(); -agent.state.model = newModel; -agent.state.thinkingLevel = 'medium'; -agent.state.tools = [...]; -agent.followUp(customMessage); -``` - -## Message Types - -### UserMessageWithAttachments - -User message with file attachments: - -```typescript -const message: UserMessageWithAttachments = { - role: 'user-with-attachments', - content: 'Analyze this document', - attachments: [pdfAttachment], - timestamp: Date.now(), -}; - -// Type guard -if (isUserMessageWithAttachments(msg)) { - console.log(msg.attachments); -} -``` - -### ArtifactMessage - -For session persistence of artifacts: - -```typescript -const artifact: ArtifactMessage = { - role: 'artifact', - action: 'create', // or 'update', 'delete' - filename: 'chart.html', - content: '
...
', - timestamp: new Date().toISOString(), -}; - -// Type guard -if (isArtifactMessage(msg)) { - console.log(msg.filename); -} -``` - -### Custom Message Types - -Extend via declaration merging: - -```typescript -interface SystemNotification { - role: 'system-notification'; - message: string; - level: 'info' | 'warning' | 'error'; - timestamp: string; -} - -declare module '@earendil-works/pi-agent-core' { - interface CustomAgentMessages { - 'system-notification': SystemNotification; - } -} - -// Register renderer -registerMessageRenderer('system-notification', { - render: (msg) => html`
${msg.message}
`, -}); - -// Extend convertToLlm -function myConvertToLlm(messages: AgentMessage[]): Message[] { - const processed = messages.map((m) => { - if (m.role === 'system-notification') { - return { role: 'user', content: `${m.message}`, timestamp: Date.now() }; - } - return m; - }); - return defaultConvertToLlm(processed); -} -``` - -## Message Transformer - -`convertToLlm` transforms app messages to LLM-compatible format: - -```typescript -import { defaultConvertToLlm, convertAttachments } from '@earendil-works/pi-web-ui'; - -// defaultConvertToLlm handles: -// - UserMessageWithAttachments → user message with image/text content blocks -// - ArtifactMessage → filtered out (UI-only) -// - Standard messages (user, assistant, toolResult) → passed through -``` - -## Tools - -### JavaScript REPL - -Execute JavaScript in a sandboxed browser environment: - -```typescript -import { createJavaScriptReplTool } from '@earendil-works/pi-web-ui'; - -const replTool = createJavaScriptReplTool(); - -// Configure runtime providers for artifact/attachment access -replTool.runtimeProvidersFactory = () => [ - new AttachmentsRuntimeProvider(attachments), - new ArtifactsRuntimeProvider(artifactsPanel, agent, true), // read-write -]; - -agent.state.tools = [replTool]; -``` - -### Extract Document - -Extract text from documents at URLs: - -```typescript -import { createExtractDocumentTool } from '@earendil-works/pi-web-ui'; - -const extractTool = createExtractDocumentTool(); -extractTool.corsProxyUrl = 'https://corsproxy.io/?'; - -agent.state.tools = [extractTool]; -``` - -### Artifacts Tool - -Built into ArtifactsPanel, supports: HTML, SVG, Markdown, text, JSON, images, PDF, DOCX, XLSX. - -```typescript -const artifactsPanel = new ArtifactsPanel(); -artifactsPanel.agent = agent; - -// The tool is available as artifactsPanel.tool -agent.state.tools = [artifactsPanel.tool]; -``` - -### Custom Tool Renderers - -```typescript -import { registerToolRenderer, type ToolRenderer } from '@earendil-works/pi-web-ui'; - -const myRenderer: ToolRenderer = { - render(params, result, isStreaming) { - return { - content: html`
...
`, - isCustom: false, // true = no card wrapper - }; - }, -}; - -registerToolRenderer('my_tool', myRenderer); -``` - -## Storage - -### Setup - -```typescript -import { - AppStorage, - IndexedDBStorageBackend, - SettingsStore, - ProviderKeysStore, - SessionsStore, - CustomProvidersStore, - setAppStorage, - getAppStorage, -} from '@earendil-works/pi-web-ui'; - -// Create stores -const settings = new SettingsStore(); -const providerKeys = new ProviderKeysStore(); -const sessions = new SessionsStore(); -const customProviders = new CustomProvidersStore(); - -// Create backend with all store configs -const backend = new IndexedDBStorageBackend({ - dbName: 'my-app', - version: 1, - stores: [ - settings.getConfig(), - providerKeys.getConfig(), - sessions.getConfig(), - SessionsStore.getMetadataConfig(), - customProviders.getConfig(), - ], -}); - -// Wire stores to backend -settings.setBackend(backend); -providerKeys.setBackend(backend); -sessions.setBackend(backend); -customProviders.setBackend(backend); - -// Create and set global storage -const storage = new AppStorage(settings, providerKeys, sessions, customProviders, backend); -setAppStorage(storage); -``` - -### SettingsStore - -Key-value settings: - -```typescript -await storage.settings.set('proxy.enabled', true); -await storage.settings.set('proxy.url', 'https://proxy.example.com'); -const enabled = await storage.settings.get('proxy.enabled'); -``` - -### ProviderKeysStore - -API keys by provider: - -```typescript -await storage.providerKeys.set('anthropic', 'sk-ant-...'); -const key = await storage.providerKeys.get('anthropic'); -const providers = await storage.providerKeys.list(); -``` - -### SessionsStore - -Chat sessions with metadata: - -```typescript -// Save session -await storage.sessions.save(sessionData, metadata); - -// Load session -const data = await storage.sessions.get(sessionId); -const metadata = await storage.sessions.getMetadata(sessionId); - -// List sessions (sorted by lastModified) -const allMetadata = await storage.sessions.getAllMetadata(); - -// Update title -await storage.sessions.updateTitle(sessionId, 'New Title'); - -// Delete -await storage.sessions.delete(sessionId); -``` - -### CustomProvidersStore - -Custom LLM providers: - -```typescript -const provider: CustomProvider = { - id: crypto.randomUUID(), - name: 'My Ollama', - type: 'ollama', - baseUrl: 'http://localhost:11434', -}; - -await storage.customProviders.set(provider); -const all = await storage.customProviders.getAll(); -``` - -## Attachments - -Load and process files: - -```typescript -import { loadAttachment, type Attachment } from '@earendil-works/pi-web-ui'; - -// From File input -const file = inputElement.files[0]; -const attachment = await loadAttachment(file); - -// From URL -const attachment = await loadAttachment('https://example.com/doc.pdf'); - -// From ArrayBuffer -const attachment = await loadAttachment(arrayBuffer, 'document.pdf'); - -// Attachment structure -interface Attachment { - id: string; - type: 'image' | 'document'; - fileName: string; - mimeType: string; - size: number; - content: string; // base64 encoded - extractedText?: string; // For documents - preview?: string; // base64 preview image -} -``` - -Supported formats: PDF, DOCX, XLSX, PPTX, images, text files. - -## CORS Proxy - -For browser environments with CORS restrictions: - -```typescript -import { createStreamFn, shouldUseProxyForProvider, isCorsError } from '@earendil-works/pi-web-ui'; - -// AgentInterface auto-configures proxy from settings -// For manual setup: -agent.streamFn = createStreamFn(async () => { - const enabled = await storage.settings.get('proxy.enabled'); - return enabled ? await storage.settings.get('proxy.url') : undefined; -}); - -// Providers requiring proxy: -// - zai: always -// - anthropic: only OAuth tokens (sk-ant-oat-*) -``` - -## Dialogs - -### SettingsDialog - -```typescript -import { SettingsDialog, ProvidersModelsTab, ProxyTab, ApiKeysTab } from '@earendil-works/pi-web-ui'; - -SettingsDialog.open([ - new ProvidersModelsTab(), // Custom providers + model list - new ProxyTab(), // CORS proxy settings - new ApiKeysTab(), // API keys per provider -]); -``` - -### SessionListDialog - -```typescript -import { SessionListDialog } from '@earendil-works/pi-web-ui'; - -SessionListDialog.open( - async (sessionId) => { /* load session */ }, - (deletedId) => { /* handle deletion */ }, -); -``` - -### ApiKeyPromptDialog - -```typescript -import { ApiKeyPromptDialog } from '@earendil-works/pi-web-ui'; - -const success = await ApiKeyPromptDialog.prompt('anthropic'); -``` - -### ModelSelector - -```typescript -import { ModelSelector } from '@earendil-works/pi-web-ui'; - -ModelSelector.open(currentModel, (selectedModel) => { - agent.state.model = selectedModel; -}); -``` - -## Styling - -Import the pre-built CSS: - -```typescript -import '@earendil-works/pi-web-ui/app.css'; -``` - -Or use Tailwind with custom config: - -```css -@import '@mariozechner/mini-lit/themes/claude.css'; -@tailwind base; -@tailwind components; -@tailwind utilities; -``` - -## Internationalization - -```typescript -import { i18n, setLanguage, translations } from '@earendil-works/pi-web-ui'; - -// Add translations -translations.de = { - 'Loading...': 'Laden...', - 'No sessions yet': 'Noch keine Sitzungen', -}; - -setLanguage('de'); -console.log(i18n('Loading...')); // "Laden..." -``` - -## Examples - -- [example/](./example) - Complete web app with sessions, artifacts, custom messages -- [sitegeist](https://sitegeist.ai) - Browser extension using pi-web-ui - -## Known Issues - -- **PersistentStorageDialog**: Currently broken - -## License - -MIT diff --git a/packages/web-ui/example/.gitignore b/packages/web-ui/example/.gitignore deleted file mode 100644 index 0ca39c00..00000000 --- a/packages/web-ui/example/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -node_modules -dist -.DS_Store diff --git a/packages/web-ui/example/README.md b/packages/web-ui/example/README.md deleted file mode 100644 index e3d468cf..00000000 --- a/packages/web-ui/example/README.md +++ /dev/null @@ -1,61 +0,0 @@ -# Pi Web UI - Example - -This is a minimal example showing how to use `@earendil-works/pi-web-ui` in a web application. - -## Setup - -```bash -npm install -``` - -## Development - -```bash -npm run dev -``` - -Open [http://localhost:5173](http://localhost:5173) in your browser. - -## What's Included - -This example demonstrates: - -- **ChatPanel** - The main chat interface component -- **System Prompt** - Custom configuration for the AI assistant -- **Tools** - JavaScript REPL and artifacts tool - -## Configuration - -### API Keys - -The example uses **Direct Mode** by default, which means it calls AI provider APIs directly from the browser. - -To use the chat: - -1. Click the settings icon (⚙️) in the chat interface -2. Click "Manage API Keys" -3. Add your API key for your preferred provider: - - **Anthropic**: Get a key from [console.anthropic.com](https://console.anthropic.com/) - - **OpenAI**: Get a key from [platform.openai.com](https://platform.openai.com/) - - **Google**: Get a key from [makersuite.google.com](https://makersuite.google.com/) - -API keys are stored in your browser's localStorage and never sent to any server except the AI provider's API. - -## Project Structure - -``` -example/ -├── src/ -│ ├── main.ts # Main application entry point -│ └── app.css # Tailwind CSS configuration -├── index.html # HTML entry point -├── package.json # Dependencies -├── vite.config.ts # Vite configuration -└── tsconfig.json # TypeScript configuration -``` - -## Learn More - -- [Pi Web UI Documentation](../README.md) -- [Pi AI Documentation](../../ai/README.md) -- [Mini Lit Documentation](https://github.com/badlogic/mini-lit) diff --git a/packages/web-ui/example/index.html b/packages/web-ui/example/index.html deleted file mode 100644 index 933f67a1..00000000 --- a/packages/web-ui/example/index.html +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - Pi Web UI - Example - - - -
- - - diff --git a/packages/web-ui/example/package.json b/packages/web-ui/example/package.json deleted file mode 100644 index 193dacfb..00000000 --- a/packages/web-ui/example/package.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "name": "pi-web-ui-example", - "version": "0.75.3", - "private": true, - "type": "module", - "scripts": { - "dev": "vite", - "build": "vite build", - "preview": "vite preview", - "check": "tsgo --noEmit", - "clean": "shx rm -rf dist" - }, - "dependencies": { - "@mariozechner/mini-lit": "^0.2.0", - "@earendil-works/pi-ai": "file:../../ai", - "@earendil-works/pi-web-ui": "file:../", - "lit": "^3.3.1", - "lucide": "^0.544.0" - }, - "devDependencies": { - "@tailwindcss/vite": "^4.1.17", - "typescript": "^5.7.3", - "vite": "^7.1.6" - } -} diff --git a/packages/web-ui/example/src/app.css b/packages/web-ui/example/src/app.css deleted file mode 100644 index 695386b0..00000000 --- a/packages/web-ui/example/src/app.css +++ /dev/null @@ -1 +0,0 @@ -@import "../../dist/app.css"; diff --git a/packages/web-ui/example/src/custom-messages.ts b/packages/web-ui/example/src/custom-messages.ts deleted file mode 100644 index 93e07a77..00000000 --- a/packages/web-ui/example/src/custom-messages.ts +++ /dev/null @@ -1,99 +0,0 @@ -import type { Message } from "@earendil-works/pi-ai"; -import type { AgentMessage, MessageRenderer } from "@earendil-works/pi-web-ui"; -import { defaultConvertToLlm, registerMessageRenderer } from "@earendil-works/pi-web-ui"; -import { Alert } from "@mariozechner/mini-lit/dist/Alert.js"; -import { html } from "lit"; - -// ============================================================================ -// 1. EXTEND AppMessage TYPE VIA DECLARATION MERGING -// ============================================================================ - -// Define custom message types -export interface SystemNotificationMessage { - role: "system-notification"; - message: string; - variant: "default" | "destructive"; - timestamp: string; -} - -// Extend CustomAgentMessages interface via declaration merging -// This must target pi-agent-core where CustomAgentMessages is defined -declare module "@earendil-works/pi-agent-core" { - interface CustomAgentMessages { - "system-notification": SystemNotificationMessage; - } -} - -// ============================================================================ -// 2. CREATE CUSTOM RENDERER (TYPED TO SystemNotificationMessage) -// ============================================================================ - -const systemNotificationRenderer: MessageRenderer = { - render: (notification) => { - // notification is fully typed as SystemNotificationMessage! - return html` -
- ${Alert({ - variant: notification.variant, - children: html` -
-
${notification.message}
-
${new Date(notification.timestamp).toLocaleTimeString()}
-
- `, - })} -
- `; - }, -}; - -// ============================================================================ -// 3. REGISTER RENDERER -// ============================================================================ - -export function registerCustomMessageRenderers() { - registerMessageRenderer("system-notification", systemNotificationRenderer); -} - -// ============================================================================ -// 4. HELPER TO CREATE CUSTOM MESSAGES -// ============================================================================ - -export function createSystemNotification( - message: string, - variant: "default" | "destructive" = "default", -): SystemNotificationMessage { - return { - role: "system-notification", - message, - variant, - timestamp: new Date().toISOString(), - }; -} - -// ============================================================================ -// 5. CUSTOM MESSAGE TRANSFORMER -// ============================================================================ - -/** - * Custom message transformer that extends defaultConvertToLlm. - * Handles system-notification messages by converting them to user messages. - */ -export function customConvertToLlm(messages: AgentMessage[]): Message[] { - // First, handle our custom system-notification type - const processed = messages.map((m): AgentMessage => { - if (m.role === "system-notification") { - const notification = m as SystemNotificationMessage; - // Convert to user message with tags - return { - role: "user", - content: `${notification.message}`, - timestamp: Date.now(), - }; - } - return m; - }); - - // Then use defaultConvertToLlm for standard handling - return defaultConvertToLlm(processed); -} diff --git a/packages/web-ui/example/src/main.ts b/packages/web-ui/example/src/main.ts deleted file mode 100644 index 3efcd73c..00000000 --- a/packages/web-ui/example/src/main.ts +++ /dev/null @@ -1,421 +0,0 @@ -import "@mariozechner/mini-lit/dist/ThemeToggle.js"; -import { Agent, type AgentMessage } from "@earendil-works/pi-agent-core"; -import { getModel, type TextContent } from "@earendil-works/pi-ai"; -import { - type AgentState, - ApiKeyPromptDialog, - AppStorage, - ChatPanel, - CustomProvidersStore, - createJavaScriptReplTool, - IndexedDBStorageBackend, - // PersistentStorageDialog, // TODO: Fix - currently broken - ProviderKeysStore, - ProvidersModelsTab, - ProxyTab, - SessionListDialog, - SessionsStore, - SettingsDialog, - SettingsStore, - setAppStorage, -} from "@earendil-works/pi-web-ui"; -import { html, render } from "lit"; -import { Bell, History, Plus, Settings } from "lucide"; -import "./app.css"; -import { icon } from "@mariozechner/mini-lit"; -import { Button } from "@mariozechner/mini-lit/dist/Button.js"; -import { Input } from "@mariozechner/mini-lit/dist/Input.js"; -import { createSystemNotification, customConvertToLlm, registerCustomMessageRenderers } from "./custom-messages.js"; - -// Register custom message renderers -registerCustomMessageRenderers(); - -// Create stores -const settings = new SettingsStore(); -const providerKeys = new ProviderKeysStore(); -const sessions = new SessionsStore(); -const customProviders = new CustomProvidersStore(); - -// Gather configs -const configs = [ - settings.getConfig(), - SessionsStore.getMetadataConfig(), - providerKeys.getConfig(), - customProviders.getConfig(), - sessions.getConfig(), -]; - -// Create backend -const backend = new IndexedDBStorageBackend({ - dbName: "pi-web-ui-example", - version: 2, // Incremented for custom-providers store - stores: configs, -}); - -// Wire backend to stores -settings.setBackend(backend); -providerKeys.setBackend(backend); -customProviders.setBackend(backend); -sessions.setBackend(backend); - -// Create and set app storage -const storage = new AppStorage(settings, providerKeys, sessions, customProviders, backend); -setAppStorage(storage); - -let currentSessionId: string | undefined; -let currentTitle = ""; -let isEditingTitle = false; -let agent: Agent; -let chatPanel: ChatPanel; -let agentUnsubscribe: (() => void) | undefined; - -const generateTitle = (messages: AgentMessage[]): string => { - const firstUserMsg = messages.find((m) => m.role === "user"); - if (!firstUserMsg) return ""; - - let text = ""; - const content = firstUserMsg.content; - - if (typeof content === "string") { - text = content; - } else { - const textBlocks = content.filter((c): c is TextContent => c.type === "text"); - text = textBlocks.map((c) => c.text || "").join(" "); - } - - text = text.trim(); - if (!text) return ""; - - const sentenceEnd = text.search(/[.!?]/); - if (sentenceEnd > 0 && sentenceEnd <= 50) { - return text.substring(0, sentenceEnd + 1); - } - return text.length <= 50 ? text : `${text.substring(0, 47)}...`; -}; - -const shouldSaveSession = (messages: AgentMessage[]): boolean => { - const hasUserMsg = messages.some((m) => m.role === "user"); - const hasAssistantMsg = messages.some((m) => m.role === "assistant"); - return hasUserMsg && hasAssistantMsg; -}; - -const saveSession = async () => { - if (!storage.sessions || !currentSessionId || !agent || !currentTitle) return; - - const state = agent.state; - if (!shouldSaveSession(state.messages)) return; - - try { - // Create session data - const sessionData = { - id: currentSessionId, - title: currentTitle, - model: state.model!, - thinkingLevel: state.thinkingLevel, - messages: state.messages, - createdAt: new Date().toISOString(), - lastModified: new Date().toISOString(), - }; - - // Create session metadata - const metadata = { - id: currentSessionId, - title: currentTitle, - createdAt: sessionData.createdAt, - lastModified: sessionData.lastModified, - messageCount: state.messages.length, - usage: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - totalTokens: 0, - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - total: 0, - }, - }, - modelId: state.model?.id || null, - thinkingLevel: state.thinkingLevel, - preview: generateTitle(state.messages), - }; - - await storage.sessions.save(sessionData, metadata); - } catch (err) { - console.error("Failed to save session:", err); - } -}; - -const updateUrl = (sessionId: string) => { - const url = new URL(window.location.href); - url.searchParams.set("session", sessionId); - window.history.replaceState({}, "", url); -}; - -const createAgent = async (initialState?: Partial) => { - if (agentUnsubscribe) { - agentUnsubscribe(); - } - - agent = new Agent({ - initialState: initialState || { - systemPrompt: `You are a helpful AI assistant with access to various tools. - -Available tools: -- JavaScript REPL: Execute JavaScript code in a sandboxed browser environment (can do calculations, get time, process data, create visualizations, etc.) -- Artifacts: Create interactive HTML, SVG, Markdown, and text artifacts - -Feel free to use these tools when needed to provide accurate and helpful responses.`, - model: getModel("anthropic", "claude-sonnet-4-5-20250929"), - thinkingLevel: "off", - messages: [], - tools: [], - }, - // Custom transformer: convert custom messages to LLM-compatible format - convertToLlm: customConvertToLlm, - }); - - agentUnsubscribe = agent.subscribe((event: any) => { - if (event.type === "state-update") { - const messages = event.state.messages; - - // Generate title after first successful response - if (!currentTitle && shouldSaveSession(messages)) { - currentTitle = generateTitle(messages); - } - - // Create session ID on first successful save - if (!currentSessionId && shouldSaveSession(messages)) { - currentSessionId = crypto.randomUUID(); - updateUrl(currentSessionId); - } - - // Auto-save - if (currentSessionId) { - saveSession(); - } - - renderApp(); - } - }); - - await chatPanel.setAgent(agent, { - onApiKeyRequired: async (provider: string) => { - return await ApiKeyPromptDialog.prompt(provider); - }, - toolsFactory: (_agent, _agentInterface, _artifactsPanel, runtimeProvidersFactory) => { - // Create javascript_repl tool with access to attachments + artifacts - const replTool = createJavaScriptReplTool(); - replTool.runtimeProvidersFactory = runtimeProvidersFactory; - return [replTool]; - }, - }); -}; - -const loadSession = async (sessionId: string): Promise => { - if (!storage.sessions) return false; - - const sessionData = await storage.sessions.get(sessionId); - if (!sessionData) { - console.error("Session not found:", sessionId); - return false; - } - - currentSessionId = sessionId; - const metadata = await storage.sessions.getMetadata(sessionId); - currentTitle = metadata?.title || ""; - - await createAgent({ - model: sessionData.model, - thinkingLevel: sessionData.thinkingLevel, - messages: sessionData.messages, - tools: [], - }); - - updateUrl(sessionId); - renderApp(); - return true; -}; - -const newSession = () => { - const url = new URL(window.location.href); - url.search = ""; - window.location.href = url.toString(); -}; - -// ============================================================================ -// RENDER -// ============================================================================ -const renderApp = () => { - const app = document.getElementById("app"); - if (!app) return; - - const appHtml = html` -
- -
-
- ${Button({ - variant: "ghost", - size: "sm", - children: icon(History, "sm"), - onClick: () => { - SessionListDialog.open( - async (sessionId) => { - await loadSession(sessionId); - }, - (deletedSessionId) => { - // Only reload if the current session was deleted - if (deletedSessionId === currentSessionId) { - newSession(); - } - }, - ); - }, - title: "Sessions", - })} - ${Button({ - variant: "ghost", - size: "sm", - children: icon(Plus, "sm"), - onClick: newSession, - title: "New Session", - })} - - ${ - currentTitle - ? isEditingTitle - ? html`
- ${Input({ - type: "text", - value: currentTitle, - className: "text-sm w-64", - onChange: async (e: Event) => { - const newTitle = (e.target as HTMLInputElement).value.trim(); - if (newTitle && newTitle !== currentTitle && storage.sessions && currentSessionId) { - await storage.sessions.updateTitle(currentSessionId, newTitle); - currentTitle = newTitle; - } - isEditingTitle = false; - renderApp(); - }, - onKeyDown: async (e: KeyboardEvent) => { - if (e.key === "Enter") { - const newTitle = (e.target as HTMLInputElement).value.trim(); - if (newTitle && newTitle !== currentTitle && storage.sessions && currentSessionId) { - await storage.sessions.updateTitle(currentSessionId, newTitle); - currentTitle = newTitle; - } - isEditingTitle = false; - renderApp(); - } else if (e.key === "Escape") { - isEditingTitle = false; - renderApp(); - } - }, - })} -
` - : html`` - : html`Pi Web UI Example` - } -
-
- ${Button({ - variant: "ghost", - size: "sm", - children: icon(Bell, "sm"), - onClick: () => { - // Demo: Inject custom message (will appear on next agent run) - if (agent) { - agent.steer( - createSystemNotification( - "This is a custom message! It appears in the UI but is never sent to the LLM.", - ), - ); - } - }, - title: "Demo: Add Custom Notification", - })} - - ${Button({ - variant: "ghost", - size: "sm", - children: icon(Settings, "sm"), - onClick: () => SettingsDialog.open([new ProvidersModelsTab(), new ProxyTab()]), - title: "Settings", - })} -
-
- - - ${chatPanel} -
- `; - - render(appHtml, app); -}; - -// ============================================================================ -// INIT -// ============================================================================ -async function initApp() { - const app = document.getElementById("app"); - if (!app) throw new Error("App container not found"); - - // Show loading - render( - html` -
-
Loading...
-
- `, - app, - ); - - // TODO: Fix PersistentStorageDialog - currently broken - // Request persistent storage - // if (storage.sessions) { - // await PersistentStorageDialog.request(); - // } - - // Create ChatPanel - chatPanel = new ChatPanel(); - - // Check for session in URL - const urlParams = new URLSearchParams(window.location.search); - const sessionIdFromUrl = urlParams.get("session"); - - if (sessionIdFromUrl) { - const loaded = await loadSession(sessionIdFromUrl); - if (!loaded) { - // Session doesn't exist, redirect to new session - newSession(); - return; - } - } else { - await createAgent(); - } - - renderApp(); -} - -initApp(); diff --git a/packages/web-ui/example/tsconfig.json b/packages/web-ui/example/tsconfig.json deleted file mode 100644 index 9e8c4333..00000000 --- a/packages/web-ui/example/tsconfig.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2022", - "module": "ES2022", - "lib": ["ES2022", "DOM", "DOM.Iterable"], - "moduleResolution": "bundler", - "paths": { - "*": ["./*"], - "@earendil-works/pi-agent-core": ["../../agent/dist/index.d.ts"], - "@earendil-works/pi-ai": ["../../ai/dist/index.d.ts"], - "@earendil-works/pi-tui": ["../../tui/dist/index.d.ts"], - "@earendil-works/pi-web-ui": ["../dist/index.d.ts"] - }, - "strict": true, - "skipLibCheck": true, - "esModuleInterop": true, - "allowSyntheticDefaultImports": true, - "experimentalDecorators": true, - "useDefineForClassFields": false - }, - "include": ["src/**/*"], - "exclude": ["../src"] -} diff --git a/packages/web-ui/example/vite.config.ts b/packages/web-ui/example/vite.config.ts deleted file mode 100644 index 6550357b..00000000 --- a/packages/web-ui/example/vite.config.ts +++ /dev/null @@ -1,6 +0,0 @@ -import tailwindcss from "@tailwindcss/vite"; -import { defineConfig } from "vite"; - -export default defineConfig({ - plugins: [tailwindcss()], -}); diff --git a/packages/web-ui/package.json b/packages/web-ui/package.json deleted file mode 100644 index e9168f06..00000000 --- a/packages/web-ui/package.json +++ /dev/null @@ -1,52 +0,0 @@ -{ - "name": "@earendil-works/pi-web-ui", - "version": "0.75.3", - "description": "Reusable web UI components for AI chat interfaces powered by @earendil-works/pi-ai", - "type": "module", - "main": "dist/index.js", - "types": "dist/index.d.ts", - "exports": { - ".": "./dist/index.js", - "./app.css": "./dist/app.css" - }, - "scripts": { - "clean": "shx rm -rf dist", - "build": "tsc -p tsconfig.build.json && tailwindcss -i ./src/app.css -o ./dist/app.css --minify", - "dev": "concurrently --names \"build,example\" --prefix-colors \"cyan,green\" \"tsc -p tsconfig.build.json --watch --preserveWatchOutput\" \"tailwindcss -i ./src/app.css -o ./dist/app.css --watch\" \"npm run dev --prefix example\"", - "dev:tsc": "concurrently --names \"build\" --prefix-colors \"cyan\" \"tsc -p tsconfig.build.json --watch --preserveWatchOutput\" \"tailwindcss -i ./src/app.css -o ./dist/app.css --watch\"", - "check": "biome check --write --error-on-warnings . && tsc --noEmit && cd example && biome check --write --error-on-warnings . && tsc --noEmit" - }, - "dependencies": { - "@lmstudio/sdk": "^1.5.0", - "@earendil-works/pi-ai": "^0.75.3", - "@earendil-works/pi-tui": "^0.75.3", - "typebox": "^1.1.24", - "docx-preview": "^0.3.7", - "jszip": "^3.10.1", - "lucide": "^0.544.0", - "ollama": "^0.6.0", - "pdfjs-dist": "5.4.394", - "xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz" - }, - "peerDependencies": { - "@mariozechner/mini-lit": "^0.2.0", - "lit": "^3.3.1" - }, - "devDependencies": { - "@mariozechner/mini-lit": "^0.2.0", - "@tailwindcss/cli": "^4.0.0-beta.14", - "concurrently": "^9.2.1", - "typescript": "^5.7.3" - }, - "keywords": [ - "ai", - "chat", - "ui", - "components", - "llm", - "web-components", - "mini-lit" - ], - "author": "Mario Zechner", - "license": "MIT" -} diff --git a/packages/web-ui/scripts/count-prompt-tokens.ts b/packages/web-ui/scripts/count-prompt-tokens.ts deleted file mode 100644 index 04c01bb0..00000000 --- a/packages/web-ui/scripts/count-prompt-tokens.ts +++ /dev/null @@ -1,88 +0,0 @@ -#!/usr/bin/env tsx -/** - * Count tokens in system prompts using Anthropic's token counter API - */ - -import * as prompts from "../src/prompts/prompts.js"; - -const ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY; - -if (!ANTHROPIC_API_KEY) { - console.error("Error: ANTHROPIC_API_KEY environment variable not set"); - process.exit(1); -} - -interface TokenCountResponse { - input_tokens: number; -} - -async function countTokens(text: string): Promise { - const response = await fetch("https://api.anthropic.com/v1/messages/count_tokens", { - method: "POST", - headers: { - "Content-Type": "application/json", - "x-api-key": ANTHROPIC_API_KEY, - "anthropic-version": "2023-06-01", - }, - body: JSON.stringify({ - model: "claude-3-5-sonnet-20241022", - messages: [ - { - role: "user", - content: text, - }, - ], - }), - }); - - if (!response.ok) { - const error = await response.text(); - throw new Error(`API error: ${response.status} ${error}`); - } - - const data = (await response.json()) as TokenCountResponse; - return data.input_tokens; -} - -async function main() { - console.log("Counting tokens in prompts...\n"); - - const promptsToCount: Array<{ name: string; content: string }> = [ - { - name: "ARTIFACTS_RUNTIME_PROVIDER_DESCRIPTION_RW", - content: prompts.ARTIFACTS_RUNTIME_PROVIDER_DESCRIPTION_RW, - }, - { - name: "ARTIFACTS_RUNTIME_PROVIDER_DESCRIPTION_RO", - content: prompts.ARTIFACTS_RUNTIME_PROVIDER_DESCRIPTION_RO, - }, - { - name: "ATTACHMENTS_RUNTIME_DESCRIPTION", - content: prompts.ATTACHMENTS_RUNTIME_DESCRIPTION, - }, - { - name: "JAVASCRIPT_REPL_TOOL_DESCRIPTION (without runtime providers)", - content: prompts.JAVASCRIPT_REPL_TOOL_DESCRIPTION([]), - }, - { - name: "ARTIFACTS_TOOL_DESCRIPTION (without runtime providers)", - content: prompts.ARTIFACTS_TOOL_DESCRIPTION([]), - }, - ]; - - let total = 0; - - for (const prompt of promptsToCount) { - try { - const tokens = await countTokens(prompt.content); - total += tokens; - console.log(`${prompt.name}: ${tokens.toLocaleString()} tokens`); - } catch (error) { - console.error(`Error counting tokens for ${prompt.name}:`, error); - } - } - - console.log(`\nTotal: ${total.toLocaleString()} tokens`); -} - -main(); diff --git a/packages/web-ui/src/ChatPanel.ts b/packages/web-ui/src/ChatPanel.ts deleted file mode 100644 index bf86234e..00000000 --- a/packages/web-ui/src/ChatPanel.ts +++ /dev/null @@ -1,209 +0,0 @@ -import { Badge } from "@mariozechner/mini-lit/dist/Badge.js"; -import { html, LitElement } from "lit"; -import { customElement, state } from "lit/decorators.js"; -import "./components/AgentInterface.ts"; -import type { Agent, AgentTool } from "@earendil-works/pi-agent-core"; -import type { AgentInterface } from "./components/AgentInterface.ts"; -import { ArtifactsRuntimeProvider } from "./components/sandbox/ArtifactsRuntimeProvider.ts"; -import { AttachmentsRuntimeProvider } from "./components/sandbox/AttachmentsRuntimeProvider.ts"; -import type { SandboxRuntimeProvider } from "./components/sandbox/SandboxRuntimeProvider.ts"; -import { ArtifactsPanel, ArtifactsToolRenderer } from "./tools/artifacts/index.ts"; -import { registerToolRenderer } from "./tools/renderer-registry.ts"; -import type { Attachment } from "./utils/attachment-utils.ts"; -import { i18n } from "./utils/i18n.ts"; - -const BREAKPOINT = 800; // px - switch between overlay and side-by-side - -@customElement("pi-chat-panel") -export class ChatPanel extends LitElement { - @state() public agent?: Agent; - @state() public agentInterface?: AgentInterface; - @state() public artifactsPanel?: ArtifactsPanel; - @state() private hasArtifacts = false; - @state() private artifactCount = 0; - @state() private showArtifactsPanel = false; - @state() private windowWidth = 0; - - private resizeHandler = () => { - this.windowWidth = window.innerWidth; - this.requestUpdate(); - }; - - createRenderRoot() { - return this; - } - - override connectedCallback() { - super.connectedCallback(); - this.windowWidth = window.innerWidth; // Set initial width after connection - window.addEventListener("resize", this.resizeHandler); - this.style.display = "flex"; - this.style.flexDirection = "column"; - this.style.height = "100%"; - this.style.minHeight = "0"; - // Update width after initial render - requestAnimationFrame(() => { - this.windowWidth = window.innerWidth; - this.requestUpdate(); - }); - } - - override disconnectedCallback() { - super.disconnectedCallback(); - window.removeEventListener("resize", this.resizeHandler); - } - - async setAgent( - agent: Agent, - config?: { - onApiKeyRequired?: (provider: string) => Promise; - onBeforeSend?: () => void | Promise; - onCostClick?: () => void; - onModelSelect?: () => void; - sandboxUrlProvider?: () => string; - toolsFactory?: ( - agent: Agent, - agentInterface: AgentInterface, - artifactsPanel: ArtifactsPanel, - runtimeProvidersFactory: () => SandboxRuntimeProvider[], - ) => AgentTool[]; - }, - ) { - this.agent = agent; - - // Create AgentInterface - this.agentInterface = document.createElement("agent-interface") as AgentInterface; - this.agentInterface.session = agent; - this.agentInterface.enableAttachments = true; - this.agentInterface.enableModelSelector = true; - this.agentInterface.enableThinkingSelector = true; - this.agentInterface.showThemeToggle = false; - this.agentInterface.onApiKeyRequired = config?.onApiKeyRequired; - this.agentInterface.onModelSelect = config?.onModelSelect; - this.agentInterface.onBeforeSend = config?.onBeforeSend; - this.agentInterface.onCostClick = config?.onCostClick; - - // Set up artifacts panel - this.artifactsPanel = new ArtifactsPanel(); - this.artifactsPanel.agent = agent; // Pass agent for HTML artifact runtime providers - if (config?.sandboxUrlProvider) { - this.artifactsPanel.sandboxUrlProvider = config.sandboxUrlProvider; - } - // Register the standalone tool renderer (not the panel itself) - registerToolRenderer("artifacts", new ArtifactsToolRenderer(this.artifactsPanel)); - - // Runtime providers factory for REPL tools (read-write access) - const runtimeProvidersFactory = () => { - const attachments: Attachment[] = []; - for (const message of this.agent!.state.messages) { - if (message.role === "user-with-attachments") { - message.attachments?.forEach((a) => { - attachments.push(a); - }); - } - } - const providers: SandboxRuntimeProvider[] = []; - - // Add attachments provider if there are attachments - if (attachments.length > 0) { - providers.push(new AttachmentsRuntimeProvider(attachments)); - } - - // Add artifacts provider with read-write access (for REPL) - providers.push(new ArtifactsRuntimeProvider(this.artifactsPanel!, this.agent!, true)); - - return providers; - }; - - this.artifactsPanel.onArtifactsChange = () => { - const count = this.artifactsPanel?.artifacts?.size ?? 0; - const created = count > this.artifactCount; - this.hasArtifacts = count > 0; - this.artifactCount = count; - if (this.hasArtifacts && created) { - this.showArtifactsPanel = true; - } - this.requestUpdate(); - }; - - this.artifactsPanel.onClose = () => { - this.showArtifactsPanel = false; - this.requestUpdate(); - }; - - this.artifactsPanel.onOpen = () => { - this.showArtifactsPanel = true; - this.requestUpdate(); - }; - - // Set tools on the agent - // Pass runtimeProvidersFactory so consumers can configure their own REPL tools - const additionalTools = - config?.toolsFactory?.(agent, this.agentInterface, this.artifactsPanel, runtimeProvidersFactory) || []; - const tools = [this.artifactsPanel.tool, ...additionalTools]; - this.agent.state.tools = tools; - - // Reconstruct artifacts from existing messages - // Temporarily disable the onArtifactsChange callback to prevent auto-opening on load - const originalCallback = this.artifactsPanel.onArtifactsChange; - this.artifactsPanel.onArtifactsChange = undefined; - await this.artifactsPanel.reconstructFromMessages(this.agent.state.messages); - this.artifactsPanel.onArtifactsChange = originalCallback; - - this.hasArtifacts = this.artifactsPanel.artifacts.size > 0; - this.artifactCount = this.artifactsPanel.artifacts.size; - - this.requestUpdate(); - } - - render() { - if (!this.agent || !this.agentInterface) { - return html`
-
No agent set
-
`; - } - - const isMobile = this.windowWidth < BREAKPOINT; - - // Set panel props - if (this.artifactsPanel) { - this.artifactsPanel.collapsed = !this.showArtifactsPanel; - this.artifactsPanel.overlay = isMobile; - } - - return html` -
-
- ${this.agentInterface} -
- - - ${ - this.hasArtifacts && !this.showArtifactsPanel - ? html` - - ` - : "" - } - -
- ${this.artifactsPanel} -
-
- `; - } -} diff --git a/packages/web-ui/src/app.css b/packages/web-ui/src/app.css deleted file mode 100644 index c8ddc30d..00000000 --- a/packages/web-ui/src/app.css +++ /dev/null @@ -1,68 +0,0 @@ -/* Import Claude theme from mini-lit */ -@import "@mariozechner/mini-lit/styles/themes/default.css"; - -/* Tell Tailwind to scan mini-lit components */ -/* biome-ignore lint/suspicious/noUnknownAtRules: Tailwind 4 source directive */ -@source "../../../node_modules/@mariozechner/mini-lit/dist"; - -/* Import Tailwind */ -/* biome-ignore lint/correctness/noInvalidPositionAtImportRule: fuck you */ -@import "tailwindcss"; - -body { - font-size: 16px; - -webkit-font-smoothing: antialiased; -} - -* { - scrollbar-width: thin; - scrollbar-color: var(--color-border) rgba(0, 0, 0, 0); -} - -*::-webkit-scrollbar { - width: 8px; - height: 8px; -} - -*::-webkit-scrollbar-track { - background: transparent; -} - -*::-webkit-scrollbar-thumb { - background-color: var(--color-border); - border-radius: 4px; -} - -*::-webkit-scrollbar-thumb:hover { - background-color: rgba(0, 0, 0, 0); -} - -/* Fix cursor for dialog close buttons */ -.fixed.inset-0 button[aria-label*="Close"], -.fixed.inset-0 button[type="button"] { - cursor: pointer; -} - -/* Shimmer animation for thinking text */ -@keyframes shimmer { - 0% { - background-position: -200% 0; - } - 100% { - background-position: 200% 0; - } -} - -.animate-shimmer { - animation: shimmer 2s ease-in-out infinite; -} - -/* User message with fancy pill styling */ -.user-message-container { - transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); - position: relative; - background: linear-gradient(135deg, rgba(217, 79, 0, 0.12), rgba(255, 107, 0, 0.12), rgba(212, 165, 0, 0.12)); - border: 1px solid rgba(255, 107, 0, 0.25); - backdrop-filter: blur(10px); - max-width: 100%; -} diff --git a/packages/web-ui/src/components/AgentInterface.ts b/packages/web-ui/src/components/AgentInterface.ts deleted file mode 100644 index 672b8097..00000000 --- a/packages/web-ui/src/components/AgentInterface.ts +++ /dev/null @@ -1,403 +0,0 @@ -import { streamSimple, type ToolResultMessage, type Usage } from "@earendil-works/pi-ai"; -import { html, LitElement } from "lit"; -import { customElement, property, query } from "lit/decorators.js"; -import { ModelSelector } from "../dialogs/ModelSelector.ts"; -import type { MessageEditor } from "./MessageEditor.ts"; -import "./MessageEditor.ts"; -import "./MessageList.ts"; -import "./Messages.ts"; // Import for side effects to register the custom elements -import { getAppStorage } from "../storage/app-storage.ts"; -import "./StreamingMessageContainer.ts"; -import type { Agent, AgentEvent } from "@earendil-works/pi-agent-core"; -import type { Attachment } from "../utils/attachment-utils.ts"; -import { formatUsage } from "../utils/format.ts"; -import { i18n } from "../utils/i18n.ts"; -import { createStreamFn } from "../utils/proxy-utils.ts"; -import type { UserMessageWithAttachments } from "./Messages.ts"; -import type { StreamingMessageContainer } from "./StreamingMessageContainer.ts"; - -@customElement("agent-interface") -export class AgentInterface extends LitElement { - // Optional external session: when provided, this component becomes a view over the session - @property({ attribute: false }) session?: Agent; - @property({ type: Boolean }) enableAttachments = true; - @property({ type: Boolean }) enableModelSelector = true; - @property({ type: Boolean }) enableThinkingSelector = true; - @property({ type: Boolean }) showThemeToggle = false; - // Optional custom API key prompt handler - if not provided, uses default dialog - @property({ attribute: false }) onApiKeyRequired?: (provider: string) => Promise; - // Optional callback called before sending a message - @property({ attribute: false }) onBeforeSend?: () => void | Promise; - // Optional callback called before executing a tool call - return false to prevent execution - @property({ attribute: false }) onBeforeToolCall?: (toolName: string, args: any) => boolean | Promise; - // Optional callback called when cost display is clicked - @property({ attribute: false }) onCostClick?: () => void; - // Optional callback to override model selector behavior - @property({ attribute: false }) onModelSelect?: () => void; - - // References - @query("message-editor") private _messageEditor!: MessageEditor; - @query("streaming-message-container") private _streamingContainer!: StreamingMessageContainer; - - private _autoScroll = true; - private _lastScrollTop = 0; - private _lastClientHeight = 0; - private _scrollContainer?: HTMLElement; - private _resizeObserver?: ResizeObserver; - private _unsubscribeSession?: () => void; - - public setInput(text: string, attachments?: Attachment[]) { - const update = () => { - if (!this._messageEditor) requestAnimationFrame(update); - else { - this._messageEditor.value = text; - this._messageEditor.attachments = attachments || []; - } - }; - update(); - } - - public setAutoScroll(enabled: boolean) { - this._autoScroll = enabled; - } - - protected override createRenderRoot(): HTMLElement | DocumentFragment { - return this; - } - - override willUpdate(changedProperties: Map) { - super.willUpdate(changedProperties); - - // Re-subscribe when session property changes - if (changedProperties.has("session")) { - this.setupSessionSubscription(); - } - } - - override async connectedCallback() { - super.connectedCallback(); - - this.style.display = "flex"; - this.style.flexDirection = "column"; - this.style.height = "100%"; - this.style.minHeight = "0"; - - // Wait for first render to get scroll container - await this.updateComplete; - this._scrollContainer = this.querySelector(".overflow-y-auto") as HTMLElement; - - if (this._scrollContainer) { - // Set up ResizeObserver to detect content changes - this._resizeObserver = new ResizeObserver(() => { - if (this._autoScroll && this._scrollContainer) { - this._scrollContainer.scrollTop = this._scrollContainer.scrollHeight; - } - }); - - // Observe the content container inside the scroll container - const contentContainer = this._scrollContainer.querySelector(".max-w-3xl"); - if (contentContainer) { - this._resizeObserver.observe(contentContainer); - } - - // Set up scroll listener with better detection - this._scrollContainer.addEventListener("scroll", this._handleScroll); - } - - // Subscribe to external session if provided - this.setupSessionSubscription(); - } - - override disconnectedCallback() { - super.disconnectedCallback(); - - // Clean up observers and listeners - if (this._resizeObserver) { - this._resizeObserver.disconnect(); - this._resizeObserver = undefined; - } - - if (this._scrollContainer) { - this._scrollContainer.removeEventListener("scroll", this._handleScroll); - } - - if (this._unsubscribeSession) { - this._unsubscribeSession(); - this._unsubscribeSession = undefined; - } - } - - private setupSessionSubscription() { - if (this._unsubscribeSession) { - this._unsubscribeSession(); - this._unsubscribeSession = undefined; - } - if (!this.session) return; - - // Set default streamFn with proxy support if not already set - if (this.session.streamFn === streamSimple) { - this.session.streamFn = createStreamFn(async () => { - const enabled = await getAppStorage().settings.get("proxy.enabled"); - return enabled ? (await getAppStorage().settings.get("proxy.url")) || undefined : undefined; - }); - } - - // Set default getApiKey if not already set - if (!this.session.getApiKey) { - this.session.getApiKey = async (provider: string) => { - const key = await getAppStorage().providerKeys.get(provider); - return key ?? undefined; - }; - } - - this._unsubscribeSession = this.session.subscribe(async (ev: AgentEvent) => { - switch (ev.type) { - case "message_start": - case "turn_start": - case "turn_end": - case "agent_start": - this.requestUpdate(); - break; - case "message_end": - // Clear streaming container when a message completes - // to prevent duplicate rendering (stable list now has this message) - if (this._streamingContainer) { - this._streamingContainer.setMessage(null, true); - } - this.requestUpdate(); - break; - case "agent_end": - // Clear streaming container when agent finishes - if (this._streamingContainer) { - this._streamingContainer.isStreaming = false; - this._streamingContainer.setMessage(null, true); - } - this.requestUpdate(); - break; - case "message_update": - if (this._streamingContainer) { - const isStreaming = this.session?.state.isStreaming || false; - this._streamingContainer.isStreaming = isStreaming; - this._streamingContainer.setMessage(ev.message, !isStreaming); - } - this.requestUpdate(); - break; - } - }); - } - - private _handleScroll = (_ev: any) => { - if (!this._scrollContainer) return; - - const currentScrollTop = this._scrollContainer.scrollTop; - const scrollHeight = this._scrollContainer.scrollHeight; - const clientHeight = this._scrollContainer.clientHeight; - const distanceFromBottom = scrollHeight - currentScrollTop - clientHeight; - - // Ignore relayout due to message editor getting pushed up by stats - if (clientHeight < this._lastClientHeight) { - this._lastClientHeight = clientHeight; - return; - } - - // Only disable auto-scroll if user scrolled UP or is far from bottom - if (currentScrollTop !== 0 && currentScrollTop < this._lastScrollTop && distanceFromBottom > 50) { - this._autoScroll = false; - } else if (distanceFromBottom < 10) { - // Re-enable if very close to bottom - this._autoScroll = true; - } - - this._lastScrollTop = currentScrollTop; - this._lastClientHeight = clientHeight; - }; - - public async sendMessage(input: string, attachments?: Attachment[]) { - if ((!input.trim() && attachments?.length === 0) || this.session?.state.isStreaming) return; - const session = this.session; - if (!session) throw new Error("No session set on AgentInterface"); - if (!session.state.model) throw new Error("No model set on AgentInterface"); - - // Check if API key exists for the provider (only needed in direct mode) - const provider = session.state.model.provider; - const apiKey = await getAppStorage().providerKeys.get(provider); - - // If no API key, prompt for it - if (!apiKey) { - if (!this.onApiKeyRequired) { - console.error("No API key configured and no onApiKeyRequired handler set"); - return; - } - - const success = await this.onApiKeyRequired(provider); - - // If still no API key, abort the send - if (!success) { - return; - } - } - - // Call onBeforeSend hook before sending - if (this.onBeforeSend) { - await this.onBeforeSend(); - } - - // Only clear editor after we know we can send - this._messageEditor.value = ""; - this._messageEditor.attachments = []; - this._autoScroll = true; // Enable auto-scroll when sending a message - - // Compose message with attachments if any - if (attachments && attachments.length > 0) { - const message: UserMessageWithAttachments = { - role: "user-with-attachments", - content: input, - attachments, - timestamp: Date.now(), - }; - await this.session?.prompt(message); - } else { - await this.session?.prompt(input); - } - } - - private renderMessages() { - if (!this.session) - return html`
${i18n("No session available")}
`; - const state = this.session.state; - // Build a map of tool results to allow inline rendering in assistant messages - const toolResultsById = new Map>(); - for (const message of state.messages) { - if (message.role === "toolResult") { - toolResultsById.set(message.toolCallId, message); - } - } - return html` -
- - ()} - .isStreaming=${state.isStreaming} - .onCostClick=${this.onCostClick} - > - - - -
- `; - } - - private renderStats() { - if (!this.session) return html`
`; - - const state = this.session.state; - const totals = state.messages - .filter((m) => m.role === "assistant") - .reduce( - (acc, msg: any) => { - const usage = msg.usage; - if (usage) { - acc.input += usage.input; - acc.output += usage.output; - acc.cacheRead += usage.cacheRead; - acc.cacheWrite += usage.cacheWrite; - acc.cost.total += usage.cost.total; - } - return acc; - }, - { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - totalTokens: 0, - cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 }, - } satisfies Usage, - ); - - const hasTotals = totals.input || totals.output || totals.cacheRead || totals.cacheWrite; - const totalsText = hasTotals ? formatUsage(totals) : ""; - - return html` -
-
- ${this.showThemeToggle ? html`` : html``} -
-
- ${ - totalsText - ? this.onCostClick - ? html`${totalsText}` - : html`${totalsText}` - : "" - } -
-
- `; - } - - override render() { - if (!this.session) - return html`
${i18n("No session set")}
`; - - const session = this.session; - const state = this.session.state; - return html` -
- -
-
${this.renderMessages()}
-
- - -
-
- { - this.sendMessage(input, attachments); - }} - .onAbort=${() => session.abort()} - .onModelSelect=${() => { - if (this.onModelSelect) { - this.onModelSelect(); - } else { - ModelSelector.open(state.model, (model) => { - session.state.model = model; - }); - } - }} - .onThinkingChange=${ - this.enableThinkingSelector - ? (level: "off" | "minimal" | "low" | "medium" | "high") => { - session.state.thinkingLevel = level; - } - : undefined - } - > - ${this.renderStats()} -
-
-
- `; - } -} - -// Register custom element with guard -if (!customElements.get("agent-interface")) { - customElements.define("agent-interface", AgentInterface); -} diff --git a/packages/web-ui/src/components/AttachmentTile.ts b/packages/web-ui/src/components/AttachmentTile.ts deleted file mode 100644 index 31cacf79..00000000 --- a/packages/web-ui/src/components/AttachmentTile.ts +++ /dev/null @@ -1,107 +0,0 @@ -import { icon } from "@mariozechner/mini-lit/dist/icons.js"; -import { LitElement } from "lit"; -import { customElement, property } from "lit/decorators.js"; -import { html } from "lit/html.js"; -import { FileSpreadsheet, FileText, X } from "lucide"; -import { AttachmentOverlay } from "../dialogs/AttachmentOverlay.ts"; -import type { Attachment } from "../utils/attachment-utils.ts"; -import { i18n } from "../utils/i18n.ts"; - -@customElement("attachment-tile") -export class AttachmentTile extends LitElement { - @property({ type: Object }) attachment!: Attachment; - @property({ type: Boolean }) showDelete = false; - @property() onDelete?: () => void; - - protected override createRenderRoot(): HTMLElement | DocumentFragment { - return this; - } - - override connectedCallback(): void { - super.connectedCallback(); - this.style.display = "block"; - this.classList.add("max-h-16"); - } - - private handleClick = () => { - AttachmentOverlay.open(this.attachment); - }; - - override render() { - const hasPreview = !!this.attachment.preview; - const isImage = this.attachment.type === "image"; - const isPdf = this.attachment.mimeType === "application/pdf"; - const isExcel = - this.attachment.mimeType?.includes("spreadsheetml") || - this.attachment.fileName.toLowerCase().endsWith(".xlsx") || - this.attachment.fileName.toLowerCase().endsWith(".xls"); - - // Choose the appropriate icon - const getDocumentIcon = () => { - if (isExcel) return icon(FileSpreadsheet, "md"); - return icon(FileText, "md"); - }; - - return html` -
- ${ - hasPreview - ? html` -
- ${this.attachment.fileName} - ${ - isPdf - ? html` - -
-
${i18n("PDF")}
-
- ` - : "" - } -
- ` - : html` - -
- ${getDocumentIcon()} -
- ${ - this.attachment.fileName.length > 10 - ? `${this.attachment.fileName.substring(0, 8)}...` - : this.attachment.fileName - } -
-
- ` - } - ${ - this.showDelete - ? html` - - ` - : "" - } -
- `; - } -} diff --git a/packages/web-ui/src/components/ConsoleBlock.ts b/packages/web-ui/src/components/ConsoleBlock.ts deleted file mode 100644 index 982f0f08..00000000 --- a/packages/web-ui/src/components/ConsoleBlock.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { icon } from "@mariozechner/mini-lit"; -import { LitElement } from "lit"; -import { property, state } from "lit/decorators.js"; -import { html } from "lit/html.js"; -import { Check, Copy } from "lucide"; -import { i18n } from "../utils/i18n.ts"; - -export class ConsoleBlock extends LitElement { - @property() content: string = ""; - @property() variant: "default" | "error" = "default"; - @state() private copied = false; - - protected override createRenderRoot(): HTMLElement | DocumentFragment { - return this; - } - - override connectedCallback(): void { - super.connectedCallback(); - this.style.display = "block"; - } - - private async copy() { - try { - await navigator.clipboard.writeText(this.content || ""); - this.copied = true; - setTimeout(() => { - this.copied = false; - }, 1500); - } catch (e) { - console.error("Copy failed", e); - } - } - - override updated() { - // Auto-scroll to bottom on content changes - const container = this.querySelector(".console-scroll") as HTMLElement | null; - if (container) { - container.scrollTop = container.scrollHeight; - } - } - - override render() { - const isError = this.variant === "error"; - const textClass = isError ? "text-destructive" : "text-foreground"; - - return html` -
-
- ${i18n("console")} - -
-
-
-${this.content || ""}
-
-
- `; - } -} - -// Register custom element -if (!customElements.get("console-block")) { - customElements.define("console-block", ConsoleBlock); -} diff --git a/packages/web-ui/src/components/CustomProviderCard.ts b/packages/web-ui/src/components/CustomProviderCard.ts deleted file mode 100644 index b4f9da57..00000000 --- a/packages/web-ui/src/components/CustomProviderCard.ts +++ /dev/null @@ -1,100 +0,0 @@ -import { i18n } from "@mariozechner/mini-lit"; -import { Button } from "@mariozechner/mini-lit/dist/Button.js"; -import { html, LitElement, type TemplateResult } from "lit"; -import { customElement, property } from "lit/decorators.js"; -import type { CustomProvider } from "../storage/stores/custom-providers-store.ts"; - -@customElement("custom-provider-card") -export class CustomProviderCard extends LitElement { - @property({ type: Object }) provider!: CustomProvider; - @property({ type: Boolean }) isAutoDiscovery = false; - @property({ type: Object }) status?: { modelCount: number; status: "connected" | "disconnected" | "checking" }; - @property() onRefresh?: (provider: CustomProvider) => void; - @property() onEdit?: (provider: CustomProvider) => void; - @property() onDelete?: (provider: CustomProvider) => void; - - protected createRenderRoot() { - return this; - } - - private renderStatus(): TemplateResult { - if (!this.isAutoDiscovery) { - return html` -
- ${i18n("Models")}: ${this.provider.models?.length || 0} -
- `; - } - - if (!this.status) return html``; - - const statusIcon = - this.status.status === "connected" - ? html`` - : this.status.status === "checking" - ? html`` - : html``; - - const statusText = - this.status.status === "connected" - ? `${this.status.modelCount} ${i18n("models")}` - : this.status.status === "checking" - ? i18n("Checking...") - : i18n("Disconnected"); - - return html` -
- ${statusIcon} ${statusText} -
- `; - } - - render(): TemplateResult { - return html` -
-
-
-
${this.provider.name}
-
- ${this.provider.type} - ${this.provider.baseUrl ? html` • ${this.provider.baseUrl}` : ""} -
- ${this.renderStatus()} -
-
- ${ - this.isAutoDiscovery && this.onRefresh - ? Button({ - onClick: () => this.onRefresh?.(this.provider), - variant: "ghost", - size: "sm", - children: i18n("Refresh"), - }) - : "" - } - ${ - this.onEdit - ? Button({ - onClick: () => this.onEdit?.(this.provider), - variant: "ghost", - size: "sm", - children: i18n("Edit"), - }) - : "" - } - ${ - this.onDelete - ? Button({ - onClick: () => this.onDelete?.(this.provider), - variant: "ghost", - size: "sm", - children: i18n("Delete"), - }) - : "" - } -
-
-
- `; - } -} diff --git a/packages/web-ui/src/components/ExpandableSection.ts b/packages/web-ui/src/components/ExpandableSection.ts deleted file mode 100644 index b032af9f..00000000 --- a/packages/web-ui/src/components/ExpandableSection.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { icon } from "@mariozechner/mini-lit"; -import { html, LitElement, type TemplateResult } from "lit"; -import { customElement, property, state } from "lit/decorators.js"; -import { ChevronDown, ChevronRight } from "lucide"; - -/** - * Reusable expandable section component for tool renderers. - * Captures children in connectedCallback and re-renders them in the details area. - */ -@customElement("expandable-section") -export class ExpandableSection extends LitElement { - @property() summary!: string; - @property({ type: Boolean }) defaultExpanded = false; - @state() private expanded = false; - private capturedChildren: Node[] = []; - - protected createRenderRoot() { - return this; // light DOM - } - - override connectedCallback() { - super.connectedCallback(); - // Capture children before first render - this.capturedChildren = Array.from(this.childNodes); - // Clear children (we'll re-insert them in render) - this.innerHTML = ""; - this.expanded = this.defaultExpanded; - } - - override render(): TemplateResult { - return html` -
- - ${this.expanded ? html`
${this.capturedChildren}
` : ""} -
- `; - } -} diff --git a/packages/web-ui/src/components/Input.ts b/packages/web-ui/src/components/Input.ts deleted file mode 100644 index dff36891..00000000 --- a/packages/web-ui/src/components/Input.ts +++ /dev/null @@ -1,113 +0,0 @@ -import { type BaseComponentProps, fc } from "@mariozechner/mini-lit/dist/mini.js"; -import { html } from "lit"; -import { type Ref, ref } from "lit/directives/ref.js"; -import { i18n } from "../utils/i18n.ts"; - -export type InputType = "text" | "email" | "password" | "number" | "url" | "tel" | "search"; -export type InputSize = "sm" | "md" | "lg"; - -export interface InputProps extends BaseComponentProps { - type?: InputType; - size?: InputSize; - value?: string; - placeholder?: string; - label?: string; - error?: string; - disabled?: boolean; - required?: boolean; - name?: string; - autocomplete?: string; - min?: number; - max?: number; - step?: number; - inputRef?: Ref; - onInput?: (e: Event) => void; - onChange?: (e: Event) => void; - onKeyDown?: (e: KeyboardEvent) => void; - onKeyUp?: (e: KeyboardEvent) => void; -} - -export const Input = fc( - ({ - type = "text", - size = "md", - value = "", - placeholder = "", - label = "", - error = "", - disabled = false, - required = false, - name = "", - autocomplete = "", - min, - max, - step, - inputRef, - onInput, - onChange, - onKeyDown, - onKeyUp, - className = "", - }) => { - const sizeClasses = { - sm: "h-8 px-3 py-1 text-sm", - md: "h-9 px-3 py-1 text-sm md:text-sm", - lg: "h-10 px-4 py-1 text-base", - }; - - const baseClasses = - "flex w-full min-w-0 rounded-md border bg-transparent text-foreground shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium"; - const interactionClasses = - "placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground"; - const focusClasses = "focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]"; - const darkClasses = "dark:bg-input/30"; - const stateClasses = error - ? "border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40" - : "border-input"; - const disabledClasses = "disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50"; - - const handleInput = (e: Event) => { - onInput?.(e); - }; - - const handleChange = (e: Event) => { - onChange?.(e); - }; - - return html` -
- ${ - label - ? html` - - ` - : "" - } - - ${error ? html`${error}` : ""} -
- `; - }, -); diff --git a/packages/web-ui/src/components/MessageEditor.ts b/packages/web-ui/src/components/MessageEditor.ts deleted file mode 100644 index d9a5124c..00000000 --- a/packages/web-ui/src/components/MessageEditor.ts +++ /dev/null @@ -1,405 +0,0 @@ -import type { Model } from "@earendil-works/pi-ai"; -import { icon } from "@mariozechner/mini-lit"; -import { Button } from "@mariozechner/mini-lit/dist/Button.js"; -import { Select, type SelectOption } from "@mariozechner/mini-lit/dist/Select.js"; -import { html, LitElement } from "lit"; -import { customElement, property, state } from "lit/decorators.js"; -import { createRef, ref } from "lit/directives/ref.js"; -import { Brain, Loader2, Paperclip, Send, Sparkles, Square } from "lucide"; -import { type Attachment, loadAttachment } from "../utils/attachment-utils.ts"; -import { i18n } from "../utils/i18n.ts"; -import "./AttachmentTile.ts"; -import type { ThinkingLevel } from "@earendil-works/pi-agent-core"; - -@customElement("message-editor") -export class MessageEditor extends LitElement { - private _value = ""; - private textareaRef = createRef(); - - @property() - get value() { - return this._value; - } - - set value(val: string) { - const oldValue = this._value; - this._value = val; - this.requestUpdate("value", oldValue); - } - - @property() isStreaming = false; - @property() currentModel?: Model; - @property() thinkingLevel: ThinkingLevel = "off"; - @property() showAttachmentButton = true; - @property() showModelSelector = true; - @property() showThinkingSelector = true; - @property() onInput?: (value: string) => void; - @property() onSend?: (input: string, attachments: Attachment[]) => void; - @property() onAbort?: () => void; - @property() onModelSelect?: () => void; - @property() onThinkingChange?: (level: "off" | "minimal" | "low" | "medium" | "high") => void; - @property() onFilesChange?: (files: Attachment[]) => void; - @property() attachments: Attachment[] = []; - @property() maxFiles = 10; - @property() maxFileSize = 20 * 1024 * 1024; // 20MB - @property() acceptedTypes = - "image/*,application/pdf,.docx,.pptx,.xlsx,.xls,.txt,.md,.json,.xml,.html,.css,.js,.ts,.jsx,.tsx,.yml,.yaml"; - - @state() processingFiles = false; - @state() isDragging = false; - private fileInputRef = createRef(); - - protected override createRenderRoot(): HTMLElement | DocumentFragment { - return this; - } - - private handleTextareaInput = (e: Event) => { - const textarea = e.target as HTMLTextAreaElement; - this.value = textarea.value; - this.onInput?.(this.value); - }; - - private handleKeyDown = (e: KeyboardEvent) => { - // Ignore key events during IME composition (e.g. CJK input) - if (e.isComposing || e.key === "Process") return; - - if (e.key === "Enter" && !e.shiftKey) { - e.preventDefault(); - if (!this.isStreaming && !this.processingFiles && (this.value.trim() || this.attachments.length > 0)) { - this.handleSend(); - } - } else if (e.key === "Escape" && this.isStreaming) { - e.preventDefault(); - this.onAbort?.(); - } - }; - - private handlePaste = async (e: ClipboardEvent) => { - const items = e.clipboardData?.items; - if (!items) return; - - const imageFiles: File[] = []; - - // Check for image items in clipboard - for (let i = 0; i < items.length; i++) { - const item = items[i]; - if (item.type.startsWith("image/")) { - const file = item.getAsFile(); - if (file) { - imageFiles.push(file); - } - } - } - - // If we found images, process them - if (imageFiles.length > 0) { - e.preventDefault(); // Prevent default paste behavior - - if (imageFiles.length + this.attachments.length > this.maxFiles) { - alert(`Maximum ${this.maxFiles} files allowed`); - return; - } - - this.processingFiles = true; - const newAttachments: Attachment[] = []; - - for (const file of imageFiles) { - try { - if (file.size > this.maxFileSize) { - alert(`Image exceeds maximum size of ${Math.round(this.maxFileSize / 1024 / 1024)}MB`); - continue; - } - - const attachment = await loadAttachment(file); - newAttachments.push(attachment); - } catch (error) { - console.error("Error processing pasted image:", error); - alert(`Failed to process pasted image: ${String(error)}`); - } - } - - this.attachments = [...this.attachments, ...newAttachments]; - this.onFilesChange?.(this.attachments); - this.processingFiles = false; - } - }; - - private handleSend = () => { - this.onSend?.(this.value, this.attachments); - }; - - private handleAttachmentClick = () => { - this.fileInputRef.value?.click(); - }; - - private async handleFilesSelected(e: Event) { - const input = e.target as HTMLInputElement; - const files = Array.from(input.files || []); - if (files.length === 0) return; - - if (files.length + this.attachments.length > this.maxFiles) { - alert(`Maximum ${this.maxFiles} files allowed`); - input.value = ""; - return; - } - - this.processingFiles = true; - const newAttachments: Attachment[] = []; - - for (const file of files) { - try { - if (file.size > this.maxFileSize) { - alert(`${file.name} exceeds maximum size of ${Math.round(this.maxFileSize / 1024 / 1024)}MB`); - continue; - } - - const attachment = await loadAttachment(file); - newAttachments.push(attachment); - } catch (error) { - console.error(`Error processing ${file.name}:`, error); - alert(`Failed to process ${file.name}: ${String(error)}`); - } - } - - this.attachments = [...this.attachments, ...newAttachments]; - this.onFilesChange?.(this.attachments); - this.processingFiles = false; - input.value = ""; // Reset input - } - - private removeFile(fileId: string) { - this.attachments = this.attachments.filter((f) => f.id !== fileId); - this.onFilesChange?.(this.attachments); - } - - private handleDragOver = (e: DragEvent) => { - e.preventDefault(); - e.stopPropagation(); - if (!this.isDragging) { - this.isDragging = true; - } - }; - - private handleDragLeave = (e: DragEvent) => { - e.preventDefault(); - e.stopPropagation(); - // Only set isDragging to false if we're leaving the entire component - const rect = (e.currentTarget as HTMLElement).getBoundingClientRect(); - const x = e.clientX; - const y = e.clientY; - if (x <= rect.left || x >= rect.right || y <= rect.top || y >= rect.bottom) { - this.isDragging = false; - } - }; - - private handleDrop = async (e: DragEvent) => { - e.preventDefault(); - e.stopPropagation(); - this.isDragging = false; - - const files = Array.from(e.dataTransfer?.files || []); - if (files.length === 0) return; - - if (files.length + this.attachments.length > this.maxFiles) { - alert(`Maximum ${this.maxFiles} files allowed`); - return; - } - - this.processingFiles = true; - const newAttachments: Attachment[] = []; - - for (const file of files) { - try { - if (file.size > this.maxFileSize) { - alert(`${file.name} exceeds maximum size of ${Math.round(this.maxFileSize / 1024 / 1024)}MB`); - continue; - } - - const attachment = await loadAttachment(file); - newAttachments.push(attachment); - } catch (error) { - console.error(`Error processing ${file.name}:`, error); - alert(`Failed to process ${file.name}: ${String(error)}`); - } - } - - this.attachments = [...this.attachments, ...newAttachments]; - this.onFilesChange?.(this.attachments); - this.processingFiles = false; - }; - - override firstUpdated() { - const textarea = this.textareaRef.value; - if (textarea) { - textarea.focus(); - } - } - - override render() { - // Check if current model supports thinking/reasoning - const model = this.currentModel; - const supportsThinking = model?.reasoning === true; // Models with reasoning:true support thinking - - return html` -
- - ${ - this.isDragging - ? html` -
-
${i18n("Drop files here")}
-
- ` - : "" - } - - - ${ - this.attachments.length > 0 - ? html` -
- ${this.attachments.map( - (attachment) => html` - this.removeFile(attachment.id)} - > - `, - )} -
- ` - : "" - } - - - - - - - -
- -
- ${ - this.showAttachmentButton - ? this.processingFiles - ? html` -
- ${icon(Loader2, "sm", "animate-spin text-muted-foreground")} -
- ` - : html` - ${Button({ - variant: "ghost", - size: "icon", - className: "h-8 w-8", - onClick: this.handleAttachmentClick, - children: icon(Paperclip, "sm"), - })} - ` - : "" - } - ${ - supportsThinking && this.showThinkingSelector - ? html` - ${Select({ - value: this.thinkingLevel, - placeholder: i18n("Off"), - options: [ - { value: "off", label: i18n("Off"), icon: icon(Brain, "sm") }, - { value: "minimal", label: i18n("Minimal"), icon: icon(Brain, "sm") }, - { value: "low", label: i18n("Low"), icon: icon(Brain, "sm") }, - { value: "medium", label: i18n("Medium"), icon: icon(Brain, "sm") }, - { value: "high", label: i18n("High"), icon: icon(Brain, "sm") }, - ] as SelectOption[], - onChange: (value: string) => { - const level = value as "off" | "minimal" | "low" | "medium" | "high"; - this.thinkingLevel = level; - this.onThinkingChange?.(level); - }, - width: "80px", - size: "sm", - variant: "ghost", - fitContent: true, - })} - ` - : "" - } -
- - -
- ${ - this.showModelSelector && this.currentModel - ? html` - ${Button({ - variant: "ghost", - size: "sm", - onClick: () => { - // Focus textarea before opening model selector so focus returns there - this.textareaRef.value?.focus(); - // Wait for next frame to ensure focus takes effect before dialog captures it - requestAnimationFrame(() => { - this.onModelSelect?.(); - }); - }, - children: html` - ${icon(Sparkles, "sm")} - ${this.currentModel.id} - `, - className: "h-8 text-xs truncate", - })} - ` - : "" - } - ${ - this.isStreaming - ? html` - ${Button({ - variant: "ghost", - size: "icon", - onClick: this.onAbort, - children: icon(Square, "sm"), - className: "h-8 w-8", - })} - ` - : html` - ${Button({ - variant: "ghost", - size: "icon", - onClick: this.handleSend, - disabled: (!this.value.trim() && this.attachments.length === 0) || this.processingFiles, - children: html`
${icon(Send, "sm")}
`, - className: "h-8 w-8", - })} - ` - } -
-
-
- `; - } -} diff --git a/packages/web-ui/src/components/MessageList.ts b/packages/web-ui/src/components/MessageList.ts deleted file mode 100644 index 77723a3b..00000000 --- a/packages/web-ui/src/components/MessageList.ts +++ /dev/null @@ -1,98 +0,0 @@ -import type { AgentMessage, AgentTool } from "@earendil-works/pi-agent-core"; -import type { - AssistantMessage as AssistantMessageType, - ToolResultMessage as ToolResultMessageType, -} from "@earendil-works/pi-ai"; -import { html, LitElement, type TemplateResult } from "lit"; -import { property } from "lit/decorators.js"; -import { repeat } from "lit/directives/repeat.js"; -import { renderMessage } from "./message-renderer-registry.ts"; - -export class MessageList extends LitElement { - @property({ type: Array }) messages: AgentMessage[] = []; - @property({ type: Array }) tools: AgentTool[] = []; - @property({ type: Object }) pendingToolCalls?: ReadonlySet; - @property({ type: Boolean }) isStreaming: boolean = false; - @property({ attribute: false }) onCostClick?: () => void; - - protected override createRenderRoot(): HTMLElement | DocumentFragment { - return this; - } - - override connectedCallback(): void { - super.connectedCallback(); - this.style.display = "block"; - } - - private buildRenderItems() { - // Map tool results by call id for quick lookup - const resultByCallId = new Map(); - for (const message of this.messages) { - if (message.role === "toolResult") { - resultByCallId.set(message.toolCallId, message); - } - } - - const items: Array<{ key: string; template: TemplateResult }> = []; - let index = 0; - for (const msg of this.messages) { - // Skip artifact messages - they're for session persistence only, not UI display - if (msg.role === "artifact") { - continue; - } - - // Try custom renderer first - const customTemplate = renderMessage(msg); - if (customTemplate) { - items.push({ key: `msg:${index}`, template: customTemplate }); - index++; - continue; - } - - // Fall back to built-in renderers - if (msg.role === "user" || msg.role === "user-with-attachments") { - items.push({ - key: `msg:${index}`, - template: html``, - }); - index++; - } else if (msg.role === "assistant") { - const amsg = msg as AssistantMessageType; - items.push({ - key: `msg:${index}`, - template: html``, - }); - index++; - } else { - // Skip standalone toolResult messages; they are rendered via paired tool-message above - // Skip unknown roles - } - } - return items; - } - - override render() { - const items = this.buildRenderItems(); - return html`
- ${repeat( - items, - (it) => it.key, - (it) => it.template, - )} -
`; - } -} - -// Register custom element -if (!customElements.get("message-list")) { - customElements.define("message-list", MessageList); -} diff --git a/packages/web-ui/src/components/Messages.ts b/packages/web-ui/src/components/Messages.ts deleted file mode 100644 index 2809750f..00000000 --- a/packages/web-ui/src/components/Messages.ts +++ /dev/null @@ -1,383 +0,0 @@ -import type { - AssistantMessage as AssistantMessageType, - ImageContent, - TextContent, - ToolCall, - ToolResultMessage as ToolResultMessageType, - UserMessage as UserMessageType, -} from "@earendil-works/pi-ai"; -import { html, LitElement, type TemplateResult } from "lit"; -import { customElement, property } from "lit/decorators.js"; -import { renderTool } from "../tools/index.ts"; -import type { Attachment } from "../utils/attachment-utils.ts"; -import { formatUsage } from "../utils/format.ts"; -import { i18n } from "../utils/i18n.ts"; -import "./ThinkingBlock.ts"; -import type { AgentTool } from "@earendil-works/pi-agent-core"; - -export type UserMessageWithAttachments = { - role: "user-with-attachments"; - content: string | (TextContent | ImageContent)[]; - timestamp: number; - attachments?: Attachment[]; -}; - -// Artifact message type for session persistence -export interface ArtifactMessage { - role: "artifact"; - action: "create" | "update" | "delete"; - filename: string; - content?: string; - title?: string; - timestamp: string; -} - -declare module "@earendil-works/pi-agent-core" { - interface CustomAgentMessages { - "user-with-attachments": UserMessageWithAttachments; - artifact: ArtifactMessage; - } -} - -@customElement("user-message") -export class UserMessage extends LitElement { - @property({ type: Object }) message!: UserMessageWithAttachments | UserMessageType; - - protected override createRenderRoot(): HTMLElement | DocumentFragment { - return this; - } - - override connectedCallback(): void { - super.connectedCallback(); - this.style.display = "block"; - } - - override render() { - const content = - typeof this.message.content === "string" - ? this.message.content - : this.message.content.find((c) => c.type === "text")?.text || ""; - - return html` -
-
- - ${ - this.message.role === "user-with-attachments" && - this.message.attachments && - this.message.attachments.length > 0 - ? html` -
- ${this.message.attachments.map( - (attachment) => html` `, - )} -
- ` - : "" - } -
-
- `; - } -} - -@customElement("assistant-message") -export class AssistantMessage extends LitElement { - @property({ type: Object }) message!: AssistantMessageType; - @property({ type: Array }) tools?: AgentTool[]; - @property({ type: Object }) pendingToolCalls?: ReadonlySet; - @property({ type: Boolean }) hideToolCalls = false; - @property({ type: Object }) toolResultsById?: Map; - @property({ type: Boolean }) isStreaming: boolean = false; - @property({ type: Boolean }) hidePendingToolCalls = false; - @property({ attribute: false }) onCostClick?: () => void; - - protected override createRenderRoot(): HTMLElement | DocumentFragment { - return this; - } - - override connectedCallback(): void { - super.connectedCallback(); - this.style.display = "block"; - } - - override render() { - // Render content in the order it appears - const orderedParts: TemplateResult[] = []; - - for (const chunk of this.message.content) { - if (chunk.type === "text" && chunk.text.trim() !== "") { - orderedParts.push(html``); - } else if (chunk.type === "thinking" && chunk.thinking.trim() !== "") { - orderedParts.push( - html``, - ); - } else if (chunk.type === "toolCall") { - if (!this.hideToolCalls) { - const tool = this.tools?.find((t) => t.name === chunk.name); - const pending = this.pendingToolCalls?.has(chunk.id) ?? false; - const result = this.toolResultsById?.get(chunk.id); - // Skip rendering pending tool calls when hidePendingToolCalls is true - // (used to prevent duplication when StreamingMessageContainer is showing them) - if (this.hidePendingToolCalls && pending && !result) { - continue; - } - // A tool call is aborted if the message was aborted and there's no result for this tool call - const aborted = this.message.stopReason === "aborted" && !result; - orderedParts.push( - html``, - ); - } - } - } - - return html` -
- ${orderedParts.length ? html`
${orderedParts}
` : ""} - ${ - this.message.usage && !this.isStreaming - ? this.onCostClick - ? html`
${formatUsage(this.message.usage)}
` - : html`
${formatUsage(this.message.usage)}
` - : "" - } - ${ - this.message.stopReason === "error" && this.message.errorMessage - ? html` -
- ${i18n("Error:")} ${this.message.errorMessage} -
- ` - : "" - } - ${ - this.message.stopReason === "aborted" - ? html`${i18n("Request aborted")}` - : "" - } -
- `; - } -} - -@customElement("tool-message-debug") -export class ToolMessageDebugView extends LitElement { - @property({ type: Object }) callArgs: any; - @property({ type: Object }) result?: ToolResultMessageType; - @property({ type: Boolean }) hasResult: boolean = false; - - protected override createRenderRoot(): HTMLElement | DocumentFragment { - return this; // light DOM for shared styles - } - - override connectedCallback(): void { - super.connectedCallback(); - this.style.display = "block"; - } - - private pretty(value: unknown): { content: string; isJson: boolean } { - try { - if (typeof value === "string") { - const maybeJson = JSON.parse(value); - return { content: JSON.stringify(maybeJson, null, 2), isJson: true }; - } - return { content: JSON.stringify(value, null, 2), isJson: true }; - } catch { - return { content: typeof value === "string" ? value : String(value), isJson: false }; - } - } - - override render() { - const textOutput = - this.result?.content - ?.filter((c) => c.type === "text") - .map((c: any) => c.text) - .join("\n") || ""; - const output = this.pretty(textOutput); - const details = this.pretty(this.result?.details); - - return html` -
-
-
${i18n("Call")}
- -
-
-
${i18n("Result")}
- ${ - this.hasResult - ? html` - ` - : html`
${i18n("(no result)")}
` - } -
-
- `; - } -} - -@customElement("tool-message") -export class ToolMessage extends LitElement { - @property({ type: Object }) toolCall!: ToolCall; - @property({ type: Object }) tool?: AgentTool; - @property({ type: Object }) result?: ToolResultMessageType; - @property({ type: Boolean }) pending: boolean = false; - @property({ type: Boolean }) aborted: boolean = false; - @property({ type: Boolean }) isStreaming: boolean = false; - - protected override createRenderRoot(): HTMLElement | DocumentFragment { - return this; - } - - override connectedCallback(): void { - super.connectedCallback(); - this.style.display = "block"; - } - - override render() { - const toolName = this.tool?.name || this.toolCall.name; - - // Render tool content (renderer handles errors and styling) - const result: ToolResultMessageType | undefined = this.aborted - ? { - role: "toolResult", - isError: true, - content: [], - toolCallId: this.toolCall.id, - toolName: this.toolCall.name, - timestamp: Date.now(), - } - : this.result; - const renderResult = renderTool( - toolName, - this.toolCall.arguments, - result, - !this.aborted && (this.isStreaming || this.pending), - ); - - // Handle custom rendering (no card wrapper) - if (renderResult.isCustom) { - return renderResult.content; - } - - // Default: wrap in card - return html` -
- ${renderResult.content} -
- `; - } -} - -@customElement("aborted-message") -export class AbortedMessage extends LitElement { - protected override createRenderRoot(): HTMLElement | DocumentFragment { - return this; - } - - override connectedCallback(): void { - super.connectedCallback(); - this.style.display = "block"; - } - - protected override render(): unknown { - return html`${i18n("Request aborted")}`; - } -} - -// ============================================================================ -// Default Message Transformer -// ============================================================================ - -import type { AgentMessage } from "@earendil-works/pi-agent-core"; -import type { Message } from "@earendil-works/pi-ai"; - -/** - * Convert attachments to content blocks for LLM. - * - Images become ImageContent blocks - * - Documents with extractedText become TextContent blocks with filename header - */ -export function convertAttachments(attachments: Attachment[]): (TextContent | ImageContent)[] { - const content: (TextContent | ImageContent)[] = []; - for (const attachment of attachments) { - if (attachment.type === "image") { - content.push({ - type: "image", - data: attachment.content, - mimeType: attachment.mimeType, - } as ImageContent); - } else if (attachment.type === "document" && attachment.extractedText) { - content.push({ - type: "text", - text: `\n\n[Document: ${attachment.fileName}]\n${attachment.extractedText}`, - } as TextContent); - } - } - return content; -} - -/** - * Check if a message is a UserMessageWithAttachments. - */ -export function isUserMessageWithAttachments(msg: AgentMessage): msg is UserMessageWithAttachments { - return (msg as UserMessageWithAttachments).role === "user-with-attachments"; -} - -/** - * Check if a message is an ArtifactMessage. - */ -export function isArtifactMessage(msg: AgentMessage): msg is ArtifactMessage { - return (msg as ArtifactMessage).role === "artifact"; -} - -/** - * Default convertToLlm for web-ui apps. - * - * Handles: - * - UserMessageWithAttachments: converts to user message with content blocks - * - ArtifactMessage: filtered out (UI-only, for session reconstruction) - * - Standard LLM messages (user, assistant, toolResult): passed through - */ -export function defaultConvertToLlm(messages: AgentMessage[]): Message[] { - return messages - .filter((m) => { - // Filter out artifact messages - they're for session reconstruction only - if (isArtifactMessage(m)) { - return false; - } - return true; - }) - .map((m): Message | null => { - // Convert user-with-attachments to user message with content blocks - if (isUserMessageWithAttachments(m)) { - const textContent: (TextContent | ImageContent)[] = - typeof m.content === "string" ? [{ type: "text", text: m.content }] : [...m.content]; - - if (m.attachments) { - textContent.push(...convertAttachments(m.attachments)); - } - - return { - role: "user", - content: textContent, - timestamp: m.timestamp, - } as Message; - } - - // Pass through standard LLM roles - if (m.role === "user" || m.role === "assistant" || m.role === "toolResult") { - return m as Message; - } - - // Filter out unknown message types - return null; - }) - .filter((m): m is Message => m !== null); -} diff --git a/packages/web-ui/src/components/ProviderKeyInput.ts b/packages/web-ui/src/components/ProviderKeyInput.ts deleted file mode 100644 index 1b74f598..00000000 --- a/packages/web-ui/src/components/ProviderKeyInput.ts +++ /dev/null @@ -1,153 +0,0 @@ -import { type Context, complete, getModel } from "@earendil-works/pi-ai"; -import { i18n } from "@mariozechner/mini-lit"; -import { Badge } from "@mariozechner/mini-lit/dist/Badge.js"; -import { Button } from "@mariozechner/mini-lit/dist/Button.js"; -import { html, LitElement } from "lit"; -import { customElement, property, state } from "lit/decorators.js"; -import { getAppStorage } from "../storage/app-storage.ts"; -import { applyProxyIfNeeded } from "../utils/proxy-utils.ts"; -import { Input } from "./Input.ts"; - -// Test models for each provider -const TEST_MODELS: Record = { - anthropic: "claude-haiku-4-5", - openai: "gpt-4o-mini", - google: "gemini-2.5-flash", - groq: "openai/gpt-oss-20b", - openrouter: "z-ai/glm-4.6", - "vercel-ai-gateway": "anthropic/claude-opus-4.5", - cerebras: "gpt-oss-120b", - xai: "grok-4-fast-non-reasoning", - zai: "glm-4.5-air", -}; - -@customElement("provider-key-input") -export class ProviderKeyInput extends LitElement { - @property() provider = ""; - @state() private keyInput = ""; - @state() private testing = false; - @state() private failed = false; - @state() private hasKey = false; - @state() private inputChanged = false; - - protected createRenderRoot() { - return this; - } - - override async connectedCallback() { - super.connectedCallback(); - await this.checkKeyStatus(); - } - - private async checkKeyStatus() { - try { - const key = await getAppStorage().providerKeys.get(this.provider); - this.hasKey = !!key; - } catch (error) { - console.error("Failed to check key status:", error); - } - } - - private async testApiKey(provider: string, apiKey: string): Promise { - try { - const modelId = TEST_MODELS[provider]; - // Returning true here for Ollama and friends. Can' know which model to use for testing - if (!modelId) return true; - - let model = getModel(provider as any, modelId); - if (!model) return false; - - // Get proxy URL from settings (if available) - const proxyEnabled = await getAppStorage().settings.get("proxy.enabled"); - const proxyUrl = await getAppStorage().settings.get("proxy.url"); - - // Apply proxy only if this provider/key combination requires it - model = applyProxyIfNeeded(model, apiKey, proxyEnabled ? proxyUrl || undefined : undefined); - - const context: Context = { - messages: [{ role: "user", content: "Reply with: ok", timestamp: Date.now() }], - }; - - const result = await complete(model, context, { - apiKey, - maxTokens: 200, - } as any); - - return result.stopReason === "stop"; - } catch (error) { - console.error(`API key test failed for ${provider}:`, error); - return false; - } - } - - private async saveKey() { - if (!this.keyInput) return; - - this.testing = true; - this.failed = false; - - const success = await this.testApiKey(this.provider, this.keyInput); - - this.testing = false; - - if (success) { - try { - await getAppStorage().providerKeys.set(this.provider, this.keyInput); - this.hasKey = true; - this.inputChanged = false; - this.requestUpdate(); - } catch (error) { - console.error("Failed to save API key:", error); - this.failed = true; - setTimeout(() => { - this.failed = false; - this.requestUpdate(); - }, 5000); - } - } else { - this.failed = true; - setTimeout(() => { - this.failed = false; - this.requestUpdate(); - }, 5000); - } - } - - render() { - return html` -
-
- ${this.provider} - ${ - this.testing - ? Badge({ children: i18n("Testing..."), variant: "secondary" }) - : this.hasKey - ? html`` - : "" - } - ${this.failed ? Badge({ children: i18n("✗ Invalid"), variant: "destructive" }) : ""} -
-
- ${Input({ - type: "password", - placeholder: this.hasKey ? "••••••••••••" : i18n("Enter API key"), - value: this.keyInput, - onInput: (e: Event) => { - this.keyInput = (e.target as HTMLInputElement).value; - this.inputChanged = true; - this.requestUpdate(); - }, - className: "flex-1", - })} - ${Button({ - onClick: () => this.saveKey(), - variant: "default", - size: "sm", - disabled: !this.keyInput || this.testing || (this.hasKey && !this.inputChanged), - children: i18n("Save"), - })} -
-
- `; - } -} diff --git a/packages/web-ui/src/components/SandboxedIframe.ts b/packages/web-ui/src/components/SandboxedIframe.ts deleted file mode 100644 index 3739c5e1..00000000 --- a/packages/web-ui/src/components/SandboxedIframe.ts +++ /dev/null @@ -1,626 +0,0 @@ -import { LitElement } from "lit"; -import { customElement, property } from "lit/decorators.js"; -import { ConsoleRuntimeProvider } from "./sandbox/ConsoleRuntimeProvider.ts"; -import { RuntimeMessageBridge } from "./sandbox/RuntimeMessageBridge.ts"; -import { type MessageConsumer, RUNTIME_MESSAGE_ROUTER } from "./sandbox/RuntimeMessageRouter.ts"; -import type { SandboxRuntimeProvider } from "./sandbox/SandboxRuntimeProvider.ts"; - -export interface SandboxFile { - fileName: string; - content: string | Uint8Array; - mimeType: string; -} - -export interface SandboxResult { - success: boolean; - console: Array<{ type: string; text: string }>; - files?: SandboxFile[]; - error?: { message: string; stack: string }; - returnValue?: any; -} - -/** - * Function that returns the URL to the sandbox HTML file. - * Used in browser extensions to load sandbox.html via chrome.runtime.getURL(). - */ -export type SandboxUrlProvider = () => string; - -/** - * Configuration for prepareHtmlDocument - */ -export interface PrepareHtmlOptions { - /** True if this is an HTML artifact (inject into existing HTML), false if REPL (wrap in HTML) */ - isHtmlArtifact: boolean; - /** True if this is a standalone download (no runtime bridge, no navigation interceptor) */ - isStandalone?: boolean; -} - -/** - * Escape HTML special sequences in code to prevent premature tag closure - * @param code Code that will be injected into in user code to prevent premature tag closure - const escapedUserCode = escapeScriptContent(userCode); - - return ` - - - ${runtime} - - - - -`; - } - } - - /** - * Generate runtime script from providers - * @param sandboxId Unique sandbox ID - * @param providers Runtime providers - * @param isStandalone If true, skip runtime bridge and navigation interceptor (for standalone downloads) - */ - private getRuntimeScript( - sandboxId: string, - providers: SandboxRuntimeProvider[] = [], - isStandalone: boolean = false, - ): string { - // Collect all data from providers - const allData: Record = {}; - for (const provider of providers) { - Object.assign(allData, provider.getData()); - } - - // Generate bridge code (skip if standalone) - const bridgeCode = isStandalone - ? "" - : RuntimeMessageBridge.generateBridgeCode({ - context: "sandbox-iframe", - sandboxId, - }); - - // Collect all runtime functions - pass sandboxId as string literal - const runtimeFunctions: string[] = []; - for (const provider of providers) { - runtimeFunctions.push(`(${provider.getRuntime().toString()})(${JSON.stringify(sandboxId)});`); - } - - // Build script with HTML escaping - // Escape to prevent premature tag closure in HTML parser - const dataInjection = Object.entries(allData) - .map(([key, value]) => { - const jsonStr = JSON.stringify(value).replace(/<\/script/gi, "<\\/script"); - return `window.${key} = ${jsonStr};`; - }) - .join("\n"); - - // TODO the font-size is needed, as chrome seems to inject a stylesheet into iframes - // found in an extension context like sidepanel, setting body { font-size: 75% }. It's - // definitely not our code doing that. - // See https://stackoverflow.com/questions/71480433/chrome-is-injecting-some-stylesheet-in-popup-ui-which-reduces-the-font-size-to-7 - - // Navigation interceptor (only if NOT standalone) - const navigationInterceptor = isStandalone - ? "" - : ` -// Navigation interceptor: prevent all navigation and open externally -(function() { - // Intercept link clicks - document.addEventListener('click', function(e) { - const link = e.target.closest('a'); - if (link && link.href) { - // Check if it's an external link (not javascript: or #hash) - if (link.href.startsWith('http://') || link.href.startsWith('https://')) { - e.preventDefault(); - e.stopPropagation(); - window.parent.postMessage({ type: 'open-external-url', url: link.href }, '*'); - } - } - }, true); - - // Intercept form submissions - document.addEventListener('submit', function(e) { - const form = e.target; - if (form && form.action) { - e.preventDefault(); - e.stopPropagation(); - window.parent.postMessage({ type: 'open-external-url', url: form.action }, '*'); - } - }, true); - - // Prevent window.location changes (only if not already redefined) - try { - const originalLocation = window.location; - Object.defineProperty(window, 'location', { - get: function() { return originalLocation; }, - set: function(url) { - window.parent.postMessage({ type: 'open-external-url', url: url.toString() }, '*'); - } - }); - } catch (e) { - // Already defined, skip - } -})(); -`; - - return ` -`; - } -} diff --git a/packages/web-ui/src/components/StreamingMessageContainer.ts b/packages/web-ui/src/components/StreamingMessageContainer.ts deleted file mode 100644 index d3b09019..00000000 --- a/packages/web-ui/src/components/StreamingMessageContainer.ts +++ /dev/null @@ -1,103 +0,0 @@ -import type { AgentMessage, AgentTool } from "@earendil-works/pi-agent-core"; -import type { ToolResultMessage } from "@earendil-works/pi-ai"; -import { html, LitElement } from "lit"; -import { property, state } from "lit/decorators.js"; - -export class StreamingMessageContainer extends LitElement { - @property({ type: Array }) tools: AgentTool[] = []; - @property({ type: Boolean }) isStreaming = false; - @property({ type: Object }) pendingToolCalls?: ReadonlySet; - @property({ type: Object }) toolResultsById?: Map; - @property({ attribute: false }) onCostClick?: () => void; - - @state() private _message: AgentMessage | null = null; - private _pendingMessage: AgentMessage | null = null; - private _updateScheduled = false; - private _immediateUpdate = false; - - protected override createRenderRoot(): HTMLElement | DocumentFragment { - return this; - } - - override connectedCallback(): void { - super.connectedCallback(); - this.style.display = "block"; - } - - // Public method to update the message with batching for performance - public setMessage(message: AgentMessage | null, immediate = false) { - // Store the latest message - this._pendingMessage = message; - - // If this is an immediate update (like clearing), apply it right away - if (immediate || message === null) { - this._immediateUpdate = true; - this._message = message; - this.requestUpdate(); - // Cancel any pending updates since we're clearing - this._pendingMessage = null; - this._updateScheduled = false; - return; - } - - // Otherwise batch updates for performance during streaming - if (!this._updateScheduled) { - this._updateScheduled = true; - - requestAnimationFrame(async () => { - // Only apply the update if we haven't been cleared - if (!this._immediateUpdate && this._pendingMessage !== null) { - // Deep clone the message to ensure Lit detects changes in nested properties - // (like toolCall.arguments being mutated during streaming) - this._message = JSON.parse(JSON.stringify(this._pendingMessage)); - this.requestUpdate(); - } - // Reset for next batch - this._pendingMessage = null; - this._updateScheduled = false; - this._immediateUpdate = false; - }); - } - } - - override render() { - // Show loading indicator if loading but no message yet - if (!this._message) { - if (this.isStreaming) - return html`
- -
`; - return html``; // Empty until a message is set - } - const msg = this._message; - - if (msg.role === "toolResult") { - // Skip standalone tool result in streaming; the stable list will render paired tool-message - return html``; - } else if (msg.role === "user" || msg.role === "user-with-attachments") { - // Skip standalone tool result in streaming; the stable list will render it immediiately - return html``; - } else if (msg.role === "assistant") { - // Assistant message - render inline tool messages during streaming - return html` -
- - ${this.isStreaming ? html`` : ""} -
- `; - } - } -} - -// Register custom element -if (!customElements.get("streaming-message-container")) { - customElements.define("streaming-message-container", StreamingMessageContainer); -} diff --git a/packages/web-ui/src/components/ThinkingBlock.ts b/packages/web-ui/src/components/ThinkingBlock.ts deleted file mode 100644 index 8dd7ce45..00000000 --- a/packages/web-ui/src/components/ThinkingBlock.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { icon } from "@mariozechner/mini-lit"; -import { html, LitElement } from "lit"; -import { customElement, property, state } from "lit/decorators.js"; -import { ChevronRight } from "lucide"; - -@customElement("thinking-block") -export class ThinkingBlock extends LitElement { - @property() content!: string; - @property({ type: Boolean }) isStreaming = false; - @state() private isExpanded = false; - - protected override createRenderRoot(): HTMLElement | DocumentFragment { - return this; - } - - override connectedCallback(): void { - super.connectedCallback(); - this.style.display = "block"; - } - - private toggleExpanded() { - this.isExpanded = !this.isExpanded; - } - - override render() { - const shimmerClasses = this.isStreaming - ? "animate-shimmer bg-gradient-to-r from-muted-foreground via-foreground to-muted-foreground bg-[length:200%_100%] bg-clip-text text-transparent" - : ""; - - return html` -
-
- ${icon(ChevronRight, "sm")} - Thinking... -
- ${this.isExpanded ? html`` : ""} -
- `; - } -} diff --git a/packages/web-ui/src/components/message-renderer-registry.ts b/packages/web-ui/src/components/message-renderer-registry.ts deleted file mode 100644 index ed710b63..00000000 --- a/packages/web-ui/src/components/message-renderer-registry.ts +++ /dev/null @@ -1,28 +0,0 @@ -import type { AgentMessage } from "@earendil-works/pi-agent-core"; -import type { TemplateResult } from "lit"; - -// Extract role type from AppMessage union -export type MessageRole = AgentMessage["role"]; - -// Generic message renderer typed to specific message type -export interface MessageRenderer { - render(message: TMessage): TemplateResult; -} - -// Registry of custom message renderers by role -const messageRenderers = new Map>(); - -export function registerMessageRenderer( - role: TRole, - renderer: MessageRenderer>, -): void { - messageRenderers.set(role, renderer); -} - -export function getMessageRenderer(role: MessageRole): MessageRenderer | undefined { - return messageRenderers.get(role); -} - -export function renderMessage(message: AgentMessage): TemplateResult | undefined { - return messageRenderers.get(message.role)?.render(message); -} diff --git a/packages/web-ui/src/components/sandbox/ArtifactsRuntimeProvider.ts b/packages/web-ui/src/components/sandbox/ArtifactsRuntimeProvider.ts deleted file mode 100644 index 626fe0a7..00000000 --- a/packages/web-ui/src/components/sandbox/ArtifactsRuntimeProvider.ts +++ /dev/null @@ -1,224 +0,0 @@ -import type { AgentMessage } from "@earendil-works/pi-agent-core"; -import { - ARTIFACTS_RUNTIME_PROVIDER_DESCRIPTION_RO, - ARTIFACTS_RUNTIME_PROVIDER_DESCRIPTION_RW, -} from "../../prompts/prompts.ts"; -import type { SandboxRuntimeProvider } from "./SandboxRuntimeProvider.ts"; - -// Define minimal interface for ArtifactsPanel to avoid circular dependencies -interface ArtifactsPanelLike { - artifacts: Map; - tool: { - execute(toolCallId: string, args: { command: string; filename: string; content?: string }): Promise; - }; -} - -interface AgentLike { - state: { messages: AgentMessage[] }; -} - -/** - * Artifacts Runtime Provider - * - * Provides programmatic access to session artifacts from sandboxed code. - * Allows code to create, read, update, and delete artifacts dynamically. - * Supports both online (extension) and offline (downloaded HTML) modes. - */ -export class ArtifactsRuntimeProvider implements SandboxRuntimeProvider { - private artifactsPanel: ArtifactsPanelLike; - private agent?: AgentLike; - private readWrite: boolean; - - constructor(artifactsPanel: ArtifactsPanelLike, agent?: AgentLike, readWrite: boolean = true) { - this.artifactsPanel = artifactsPanel; - this.agent = agent; - this.readWrite = readWrite; - } - - getData(): Record { - // Inject artifact snapshot for offline mode - const snapshot: Record = {}; - this.artifactsPanel.artifacts.forEach((artifact, filename) => { - snapshot[filename] = artifact.content; - }); - return { artifacts: snapshot }; - } - - getRuntime(): (sandboxId: string) => void { - // This function will be stringified, so no external references! - return (_sandboxId: string) => { - // Auto-parse/stringify for .json files - const isJsonFile = (filename: string) => filename.endsWith(".json"); - - (window as any).listArtifacts = async (): Promise => { - // Online: ask extension - if ((window as any).sendRuntimeMessage) { - const response = await (window as any).sendRuntimeMessage({ - type: "artifact-operation", - action: "list", - }); - if (!response.success) throw new Error(response.error); - return response.result; - } - // Offline: return snapshot keys - else { - return Object.keys((window as any).artifacts || {}); - } - }; - - (window as any).getArtifact = async (filename: string): Promise => { - let content: string; - - // Online: ask extension - if ((window as any).sendRuntimeMessage) { - const response = await (window as any).sendRuntimeMessage({ - type: "artifact-operation", - action: "get", - filename, - }); - if (!response.success) throw new Error(response.error); - content = response.result; - } - // Offline: read snapshot - else { - if (!(window as any).artifacts?.[filename]) { - throw new Error(`Artifact not found (offline mode): ${filename}`); - } - content = (window as any).artifacts[filename]; - } - - // Auto-parse .json files - if (isJsonFile(filename)) { - try { - return JSON.parse(content); - } catch (e) { - throw new Error(`Failed to parse JSON from ${filename}: ${e}`); - } - } - return content; - }; - - (window as any).createOrUpdateArtifact = async ( - filename: string, - content: any, - mimeType?: string, - ): Promise => { - if (!(window as any).sendRuntimeMessage) { - throw new Error("Cannot create/update artifacts in offline mode (read-only)"); - } - - let finalContent = content; - // Auto-stringify .json files - if (isJsonFile(filename) && typeof content !== "string") { - finalContent = JSON.stringify(content, null, 2); - } else if (typeof content !== "string") { - finalContent = JSON.stringify(content, null, 2); - } - - const response = await (window as any).sendRuntimeMessage({ - type: "artifact-operation", - action: "createOrUpdate", - filename, - content: finalContent, - mimeType, - }); - if (!response.success) throw new Error(response.error); - }; - - (window as any).deleteArtifact = async (filename: string): Promise => { - if (!(window as any).sendRuntimeMessage) { - throw new Error("Cannot delete artifacts in offline mode (read-only)"); - } - - const response = await (window as any).sendRuntimeMessage({ - type: "artifact-operation", - action: "delete", - filename, - }); - if (!response.success) throw new Error(response.error); - }; - }; - } - - async handleMessage(message: any, respond: (response: any) => void): Promise { - if (message.type !== "artifact-operation") { - return; - } - - const { action, filename, content } = message; - - try { - switch (action) { - case "list": { - const filenames = Array.from(this.artifactsPanel.artifacts.keys()); - respond({ success: true, result: filenames }); - break; - } - - case "get": { - const artifact = this.artifactsPanel.artifacts.get(filename); - if (!artifact) { - respond({ success: false, error: `Artifact not found: ${filename}` }); - } else { - respond({ success: true, result: artifact.content }); - } - break; - } - - case "createOrUpdate": { - try { - const exists = this.artifactsPanel.artifacts.has(filename); - const command = exists ? "rewrite" : "create"; - const action = exists ? "update" : "create"; - - await this.artifactsPanel.tool.execute("", { - command, - filename, - content, - }); - this.agent?.state.messages.push({ - role: "artifact", - action, - filename, - content, - ...(action === "create" && { title: filename }), - timestamp: new Date().toISOString(), - }); - respond({ success: true }); - } catch (err: any) { - respond({ success: false, error: err.message }); - } - break; - } - - case "delete": { - try { - await this.artifactsPanel.tool.execute("", { - command: "delete", - filename, - }); - this.agent?.state.messages.push({ - role: "artifact", - action: "delete", - filename, - timestamp: new Date().toISOString(), - }); - respond({ success: true }); - } catch (err: any) { - respond({ success: false, error: err.message }); - } - break; - } - - default: - respond({ success: false, error: `Unknown artifact action: ${action}` }); - } - } catch (error: any) { - respond({ success: false, error: error.message }); - } - } - - getDescription(): string { - return this.readWrite ? ARTIFACTS_RUNTIME_PROVIDER_DESCRIPTION_RW : ARTIFACTS_RUNTIME_PROVIDER_DESCRIPTION_RO; - } -} diff --git a/packages/web-ui/src/components/sandbox/AttachmentsRuntimeProvider.ts b/packages/web-ui/src/components/sandbox/AttachmentsRuntimeProvider.ts deleted file mode 100644 index b706a528..00000000 --- a/packages/web-ui/src/components/sandbox/AttachmentsRuntimeProvider.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { ATTACHMENTS_RUNTIME_DESCRIPTION } from "../../prompts/prompts.ts"; -import type { Attachment } from "../../utils/attachment-utils.ts"; -import type { SandboxRuntimeProvider } from "./SandboxRuntimeProvider.ts"; - -/** - * Attachments Runtime Provider - * - * OPTIONAL provider that provides file access APIs to sandboxed code. - * Only needed when attachments are present. - * Attachments are read-only snapshot data - no messaging needed. - */ -export class AttachmentsRuntimeProvider implements SandboxRuntimeProvider { - private attachments: Attachment[]; - - constructor(attachments: Attachment[]) { - this.attachments = attachments; - } - - getData(): Record { - const attachmentsData = this.attachments.map((a) => ({ - id: a.id, - fileName: a.fileName, - mimeType: a.mimeType, - size: a.size, - content: a.content, - extractedText: a.extractedText, - })); - - return { attachments: attachmentsData }; - } - - getRuntime(): (sandboxId: string) => void { - // This function will be stringified, so no external references! - // These functions read directly from window.attachments - // Works both online AND offline (no messaging needed!) - return (_sandboxId: string) => { - (window as any).listAttachments = () => - ((window as any).attachments || []).map((a: any) => ({ - id: a.id, - fileName: a.fileName, - mimeType: a.mimeType, - size: a.size, - })); - - (window as any).readTextAttachment = (attachmentId: string) => { - const a = ((window as any).attachments || []).find((x: any) => x.id === attachmentId); - if (!a) throw new Error(`Attachment not found: ${attachmentId}`); - if (a.extractedText) return a.extractedText; - try { - return atob(a.content); - } catch { - throw new Error(`Failed to decode text content for: ${attachmentId}`); - } - }; - - (window as any).readBinaryAttachment = (attachmentId: string) => { - const a = ((window as any).attachments || []).find((x: any) => x.id === attachmentId); - if (!a) throw new Error(`Attachment not found: ${attachmentId}`); - const bin = atob(a.content); - const bytes = new Uint8Array(bin.length); - for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i); - return bytes; - }; - }; - } - - getDescription(): string { - return ATTACHMENTS_RUNTIME_DESCRIPTION; - } -} diff --git a/packages/web-ui/src/components/sandbox/ConsoleRuntimeProvider.ts b/packages/web-ui/src/components/sandbox/ConsoleRuntimeProvider.ts deleted file mode 100644 index 18cc3c1a..00000000 --- a/packages/web-ui/src/components/sandbox/ConsoleRuntimeProvider.ts +++ /dev/null @@ -1,186 +0,0 @@ -import type { SandboxRuntimeProvider } from "./SandboxRuntimeProvider.ts"; - -export interface ConsoleLog { - type: "log" | "warn" | "error" | "info"; - text: string; - args?: unknown[]; -} - -/** - * Console Runtime Provider - * - * REQUIRED provider that should always be included first. - * Provides console capture, error handling, and execution lifecycle management. - * Collects console output for retrieval by caller. - */ -export class ConsoleRuntimeProvider implements SandboxRuntimeProvider { - private logs: ConsoleLog[] = []; - private completionError: { message: string; stack: string } | null = null; - private completed = false; - - getData(): Record { - // No data needed - return {}; - } - - getDescription(): string { - return ""; - } - - getRuntime(): (sandboxId: string) => void { - return (_sandboxId: string) => { - // Store truly original console methods on first wrap only - // This prevents accumulation of wrapper functions across multiple executions - if (!(window as any).__originalConsole) { - (window as any).__originalConsole = { - log: console.log.bind(console), - error: console.error.bind(console), - warn: console.warn.bind(console), - info: console.info.bind(console), - }; - } - - // Always use the truly original console, not the current (possibly wrapped) one - const originalConsole = (window as any).__originalConsole; - - // Track pending send promises to wait for them in onCompleted - const pendingSends: Promise[] = []; - - ["log", "error", "warn", "info"].forEach((method) => { - (console as any)[method] = (...args: any[]) => { - const text = args - .map((arg) => { - try { - return typeof arg === "object" ? JSON.stringify(arg) : String(arg); - } catch { - return String(arg); - } - }) - .join(" "); - - // Always log locally too (using truly original console) - (originalConsole as any)[method].apply(console, args); - - // Send immediately and track the promise (only in extension context) - if ((window as any).sendRuntimeMessage) { - const sendPromise = (window as any) - .sendRuntimeMessage({ - type: "console", - method, - text, - args, - }) - .catch(() => {}); - pendingSends.push(sendPromise); - } - }; - }); - - // Register completion callback to wait for all pending sends - if ((window as any).onCompleted) { - (window as any).onCompleted(async (_success: boolean) => { - // Wait for all pending console sends to complete - if (pendingSends.length > 0) { - await Promise.all(pendingSends); - } - }); - } - - // Track errors for HTML artifacts - let lastError: { message: string; stack: string } | null = null; - - // Error handlers - track errors but don't log them - // (they'll be shown via execution-error message) - window.addEventListener("error", (e) => { - const text = `${e.error?.stack || e.message || String(e)} at line ${e.lineno || "?"}:${e.colno || "?"}`; - - lastError = { - message: e.error?.message || e.message || String(e), - stack: e.error?.stack || text, - }; - }); - - window.addEventListener("unhandledrejection", (e) => { - const text = `Unhandled promise rejection: ${e.reason?.message || e.reason || "Unknown error"}`; - - lastError = { - message: e.reason?.message || String(e.reason) || "Unhandled promise rejection", - stack: e.reason?.stack || text, - }; - }); - - // Expose complete() method for user code to call - let completionSent = false; - (window as any).complete = async (error?: { message: string; stack: string }, returnValue?: any) => { - if (completionSent) return; - completionSent = true; - - const finalError = error || lastError; - - if ((window as any).sendRuntimeMessage) { - if (finalError) { - await (window as any).sendRuntimeMessage({ - type: "execution-error", - error: finalError, - }); - } else { - await (window as any).sendRuntimeMessage({ - type: "execution-complete", - returnValue, - }); - } - } - }; - }; - } - - async handleMessage(message: any, respond: (response: any) => void): Promise { - if (message.type === "console") { - // Collect console output - this.logs.push({ - type: - message.method === "error" - ? "error" - : message.method === "warn" - ? "warn" - : message.method === "info" - ? "info" - : "log", - text: message.text, - args: message.args, - }); - // Acknowledge receipt - respond({ success: true }); - } - } - - /** - * Get collected console logs - */ - getLogs(): ConsoleLog[] { - return this.logs; - } - - /** - * Get completion status - */ - isCompleted(): boolean { - return this.completed; - } - - /** - * Get completion error if any - */ - getCompletionError(): { message: string; stack: string } | null { - return this.completionError; - } - - /** - * Reset state for reuse - */ - reset(): void { - this.logs = []; - this.completionError = null; - this.completed = false; - } -} diff --git a/packages/web-ui/src/components/sandbox/FileDownloadRuntimeProvider.ts b/packages/web-ui/src/components/sandbox/FileDownloadRuntimeProvider.ts deleted file mode 100644 index c2e1f295..00000000 --- a/packages/web-ui/src/components/sandbox/FileDownloadRuntimeProvider.ts +++ /dev/null @@ -1,110 +0,0 @@ -import type { SandboxRuntimeProvider } from "./SandboxRuntimeProvider.ts"; - -export interface DownloadableFile { - fileName: string; - content: string | Uint8Array; - mimeType: string; -} - -/** - * File Download Runtime Provider - * - * Provides returnDownloadableFile() for creating user downloads. - * Files returned this way are NOT accessible to the LLM later (one-time download). - * Works both online (sends to extension) and offline (triggers browser download directly). - * Collects files for retrieval by caller. - */ -export class FileDownloadRuntimeProvider implements SandboxRuntimeProvider { - private files: DownloadableFile[] = []; - - getData(): Record { - // No data needed - return {}; - } - - getRuntime(): (sandboxId: string) => void { - return (_sandboxId: string) => { - (window as any).returnDownloadableFile = async (fileName: string, content: any, mimeType?: string) => { - let finalContent: any, finalMimeType: string; - - if (content instanceof Blob) { - const arrayBuffer = await content.arrayBuffer(); - finalContent = new Uint8Array(arrayBuffer); - finalMimeType = mimeType || content.type || "application/octet-stream"; - if (!mimeType && !content.type) { - throw new Error( - "returnDownloadableFile: MIME type is required for Blob content. Please provide a mimeType parameter (e.g., 'image/png').", - ); - } - } else if (content instanceof Uint8Array) { - finalContent = content; - if (!mimeType) { - throw new Error( - "returnDownloadableFile: MIME type is required for Uint8Array content. Please provide a mimeType parameter (e.g., 'image/png').", - ); - } - finalMimeType = mimeType; - } else if (typeof content === "string") { - finalContent = content; - finalMimeType = mimeType || "text/plain"; - } else { - finalContent = JSON.stringify(content, null, 2); - finalMimeType = mimeType || "application/json"; - } - - // Send to extension if in extension context (online mode) - if ((window as any).sendRuntimeMessage) { - const response = await (window as any).sendRuntimeMessage({ - type: "file-returned", - fileName, - content: finalContent, - mimeType: finalMimeType, - }); - if (response.error) throw new Error(response.error); - } else { - // Offline mode: trigger browser download directly - const blob = new Blob([finalContent instanceof Uint8Array ? finalContent : finalContent], { - type: finalMimeType, - }); - const url = URL.createObjectURL(blob); - const a = document.createElement("a"); - a.href = url; - a.download = fileName; - a.click(); - URL.revokeObjectURL(url); - } - }; - }; - } - - async handleMessage(message: any, respond: (response: any) => void): Promise { - if (message.type === "file-returned") { - // Collect file for caller - this.files.push({ - fileName: message.fileName, - content: message.content, - mimeType: message.mimeType, - }); - - respond({ success: true }); - } - } - - /** - * Get collected files - */ - getFiles(): DownloadableFile[] { - return this.files; - } - - /** - * Reset state for reuse - */ - reset(): void { - this.files = []; - } - - getDescription(): string { - return "returnDownloadableFile(filename, content, mimeType?) - Create downloadable file for user (one-time download, not accessible later)"; - } -} diff --git a/packages/web-ui/src/components/sandbox/RuntimeMessageBridge.ts b/packages/web-ui/src/components/sandbox/RuntimeMessageBridge.ts deleted file mode 100644 index b2e754e5..00000000 --- a/packages/web-ui/src/components/sandbox/RuntimeMessageBridge.ts +++ /dev/null @@ -1,82 +0,0 @@ -/** - * Generates sendRuntimeMessage() function for injection into execution contexts. - * Provides unified messaging API that works in both sandbox iframe and user script contexts. - */ - -export type MessageType = "request-response" | "fire-and-forget"; - -export interface RuntimeMessageBridgeOptions { - context: "sandbox-iframe" | "user-script"; - sandboxId: string; -} - -// biome-ignore lint/complexity/noStaticOnlyClass: fine -export class RuntimeMessageBridge { - /** - * Generate sendRuntimeMessage() function as injectable string. - * Returns the function source code to be injected into target context. - */ - static generateBridgeCode(options: RuntimeMessageBridgeOptions): string { - if (options.context === "sandbox-iframe") { - return RuntimeMessageBridge.generateSandboxBridge(options.sandboxId); - } else { - return RuntimeMessageBridge.generateUserScriptBridge(options.sandboxId); - } - } - - private static generateSandboxBridge(sandboxId: string): string { - // Returns stringified function that uses window.parent.postMessage - return ` -window.__completionCallbacks = []; -window.sendRuntimeMessage = async (message) => { - const messageId = 'msg_' + Date.now() + '_' + Math.random().toString(36).substring(2, 9); - - return new Promise((resolve, reject) => { - const handler = (e) => { - if (e.data.type === 'runtime-response' && e.data.messageId === messageId) { - window.removeEventListener('message', handler); - if (e.data.success) { - resolve(e.data); - } else { - reject(new Error(e.data.error || 'Operation failed')); - } - } - }; - - window.addEventListener('message', handler); - - window.parent.postMessage({ - ...message, - sandboxId: ${JSON.stringify(sandboxId)}, - messageId: messageId - }, '*'); - - // Timeout after 30s - setTimeout(() => { - window.removeEventListener('message', handler); - reject(new Error('Runtime message timeout')); - }, 30000); - }); -}; -window.onCompleted = (callback) => { - window.__completionCallbacks.push(callback); -}; -`.trim(); - } - - private static generateUserScriptBridge(sandboxId: string): string { - // Returns stringified function that uses chrome.runtime.sendMessage - return ` -window.__completionCallbacks = []; -window.sendRuntimeMessage = async (message) => { - return await chrome.runtime.sendMessage({ - ...message, - sandboxId: ${JSON.stringify(sandboxId)} - }); -}; -window.onCompleted = (callback) => { - window.__completionCallbacks.push(callback); -}; -`.trim(); - } -} diff --git a/packages/web-ui/src/components/sandbox/RuntimeMessageRouter.ts b/packages/web-ui/src/components/sandbox/RuntimeMessageRouter.ts deleted file mode 100644 index 0b6c6f00..00000000 --- a/packages/web-ui/src/components/sandbox/RuntimeMessageRouter.ts +++ /dev/null @@ -1,216 +0,0 @@ -import type { SandboxRuntimeProvider } from "./SandboxRuntimeProvider.ts"; - -// Type declaration for chrome extension API (when available) -declare const chrome: any; - -/** - * Message consumer interface - components that want to receive messages from sandboxes - */ -export interface MessageConsumer { - /** - * Handle a message from a sandbox. - * All consumers receive all messages - decide internally what to handle. - */ - handleMessage(message: any): Promise; -} - -/** - * Sandbox context - tracks active sandboxes and their consumers - */ -interface SandboxContext { - sandboxId: string; - iframe: HTMLIFrameElement | null; // null until setSandboxIframe() or null for user scripts - providers: SandboxRuntimeProvider[]; - consumers: Set; -} - -/** - * Centralized message router for all runtime communication. - * - * This singleton replaces all individual window.addEventListener("message") calls - * with a single global listener that routes messages to the appropriate handlers. - * Also handles user script messages from chrome.runtime.onUserScriptMessage. - * - * Benefits: - * - Single global listener instead of multiple independent listeners - * - Automatic cleanup when sandboxes are destroyed - * - Support for bidirectional communication (providers) and broadcasting (consumers) - * - Works with both sandbox iframes and user scripts - * - Clear lifecycle management - */ -export class RuntimeMessageRouter { - private sandboxes = new Map(); - private messageListener: ((e: MessageEvent) => void) | null = null; - private userScriptMessageListener: - | ((message: any, sender: any, sendResponse: (response: any) => void) => boolean) - | null = null; - - /** - * Register a new sandbox with its runtime providers. - * Call this BEFORE creating the iframe (for sandbox contexts) or executing user script. - */ - registerSandbox(sandboxId: string, providers: SandboxRuntimeProvider[], consumers: MessageConsumer[]): void { - this.sandboxes.set(sandboxId, { - sandboxId, - iframe: null, // Will be set via setSandboxIframe() for sandbox contexts - providers, - consumers: new Set(consumers), - }); - - // Setup global listener if not already done - this.setupListener(); - } - - /** - * Update the iframe reference for a sandbox. - * Call this AFTER creating the iframe. - * This is needed so providers can send responses back to the sandbox. - */ - setSandboxIframe(sandboxId: string, iframe: HTMLIFrameElement): void { - const context = this.sandboxes.get(sandboxId); - if (context) { - context.iframe = iframe; - } - } - - /** - * Unregister a sandbox and remove all its consumers. - * Call this when the sandbox is destroyed. - */ - unregisterSandbox(sandboxId: string): void { - this.sandboxes.delete(sandboxId); - - // If no more sandboxes, remove global listeners - if (this.sandboxes.size === 0) { - // Remove iframe listener - if (this.messageListener) { - window.removeEventListener("message", this.messageListener); - this.messageListener = null; - } - - // Remove user script listener - if (this.userScriptMessageListener && typeof chrome !== "undefined" && chrome.runtime?.onUserScriptMessage) { - chrome.runtime.onUserScriptMessage.removeListener(this.userScriptMessageListener); - this.userScriptMessageListener = null; - } - } - } - - /** - * Add a message consumer for a sandbox. - * Consumers receive broadcast messages (console, execution-complete, etc.) - */ - addConsumer(sandboxId: string, consumer: MessageConsumer): void { - const context = this.sandboxes.get(sandboxId); - if (context) { - context.consumers.add(consumer); - } - } - - /** - * Remove a message consumer from a sandbox. - */ - removeConsumer(sandboxId: string, consumer: MessageConsumer): void { - const context = this.sandboxes.get(sandboxId); - if (context) { - context.consumers.delete(consumer); - } - } - - /** - * Setup the global message listeners (called automatically) - */ - private setupListener(): void { - // Setup sandbox iframe listener - if (!this.messageListener) { - this.messageListener = async (e: MessageEvent) => { - const { sandboxId, messageId } = e.data; - if (!sandboxId) return; - - const context = this.sandboxes.get(sandboxId); - if (!context) { - return; - } - - // Create respond() function for bidirectional communication - const respond = (response: any) => { - context.iframe?.contentWindow?.postMessage( - { - type: "runtime-response", - messageId, - sandboxId, - ...response, - }, - "*", - ); - }; - - // 1. Try provider handlers first (for bidirectional comm) - for (const provider of context.providers) { - if (provider.handleMessage) { - await provider.handleMessage(e.data, respond); - // Don't stop - let consumers also handle the message - } - } - - // 2. Broadcast to consumers (one-way messages or lifecycle events) - for (const consumer of context.consumers) { - await consumer.handleMessage(e.data); - // Don't stop - let all consumers see the message - } - }; - - window.addEventListener("message", this.messageListener); - } - - // Setup user script message listener - if (!this.userScriptMessageListener) { - // Guard: check if we're in extension context - if (typeof chrome === "undefined" || !chrome.runtime?.onUserScriptMessage) { - return; - } - - this.userScriptMessageListener = (message: any, _sender: any, sendResponse: (response: any) => void) => { - const { sandboxId } = message; - if (!sandboxId) return false; - - const context = this.sandboxes.get(sandboxId); - if (!context) return false; - - const respond = (response: any) => { - sendResponse({ - ...response, - sandboxId, - }); - }; - - // Route to providers (async) - (async () => { - // 1. Try provider handlers first (for bidirectional comm) - for (const provider of context.providers) { - if (provider.handleMessage) { - await provider.handleMessage(message, respond); - // Don't stop - let consumers also handle the message - } - } - - // 2. Broadcast to consumers (one-way messages or lifecycle events) - for (const consumer of context.consumers) { - await consumer.handleMessage(message); - // Don't stop - let all consumers see the message - } - })(); - - return true; // Indicates async response - }; - - chrome.runtime.onUserScriptMessage.addListener(this.userScriptMessageListener); - } - } -} - -/** - * Global singleton instance. - * Import this from wherever you need to interact with the message router. - */ -export const RUNTIME_MESSAGE_ROUTER = new RuntimeMessageRouter(); diff --git a/packages/web-ui/src/components/sandbox/SandboxRuntimeProvider.ts b/packages/web-ui/src/components/sandbox/SandboxRuntimeProvider.ts deleted file mode 100644 index 90cb5df4..00000000 --- a/packages/web-ui/src/components/sandbox/SandboxRuntimeProvider.ts +++ /dev/null @@ -1,52 +0,0 @@ -/** - * Interface for providing runtime capabilities to sandboxed iframes. - * Each provider injects data and runtime functions into the sandbox context. - */ -export interface SandboxRuntimeProvider { - /** - * Returns data to inject into window scope. - * Keys become window properties (e.g., { attachments: [...] } -> window.attachments) - */ - getData(): Record; - - /** - * Returns a runtime function that will be stringified and executed in the sandbox. - * The function receives sandboxId and has access to data from getData() via window. - * - * IMPORTANT: This function will be converted to string via .toString() and injected - * into the sandbox, so it cannot reference external variables or imports. - */ - getRuntime(): (sandboxId: string) => void; - - /** - * Optional message handler for bidirectional communication. - * All providers receive all messages - decide internally what to handle. - * - * @param message - The message from the sandbox - * @param respond - Function to send a response back to the sandbox - */ - handleMessage?(message: any, respond: (response: any) => void): Promise; - - /** - * Optional documentation describing what globals/functions this provider injects. - * This will be appended to tool descriptions dynamically so the LLM knows what's available. - */ - getDescription(): string; - - /** - * Optional lifecycle callback invoked when sandbox execution starts. - * Providers can use this to track abort signals for cancellation of async operations. - * - * @param sandboxId - The unique identifier for this sandbox execution - * @param signal - Optional AbortSignal that will be triggered if execution is cancelled - */ - onExecutionStart?(sandboxId: string, signal?: AbortSignal): void; - - /** - * Optional lifecycle callback invoked when sandbox execution ends (success, error, or abort). - * Providers can use this to clean up any resources associated with the sandbox. - * - * @param sandboxId - The unique identifier for this sandbox execution - */ - onExecutionEnd?(sandboxId: string): void; -} diff --git a/packages/web-ui/src/dialogs/ApiKeyPromptDialog.ts b/packages/web-ui/src/dialogs/ApiKeyPromptDialog.ts deleted file mode 100644 index fe7d52c4..00000000 --- a/packages/web-ui/src/dialogs/ApiKeyPromptDialog.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { customElement, state } from "lit/decorators.js"; -import "../components/ProviderKeyInput.ts"; -import { DialogContent, DialogHeader } from "@mariozechner/mini-lit/dist/Dialog.js"; -import { DialogBase } from "@mariozechner/mini-lit/dist/DialogBase.js"; -import { html } from "lit"; -import { getAppStorage } from "../storage/app-storage.ts"; -import { i18n } from "../utils/i18n.ts"; - -@customElement("api-key-prompt-dialog") -export class ApiKeyPromptDialog extends DialogBase { - @state() private provider = ""; - - private resolvePromise?: (success: boolean) => void; - private unsubscribe?: () => void; - - protected modalWidth = "min(500px, 90vw)"; - protected modalHeight = "auto"; - - static async prompt(provider: string): Promise { - const dialog = new ApiKeyPromptDialog(); - dialog.provider = provider; - dialog.open(); - - return new Promise((resolve) => { - dialog.resolvePromise = resolve; - }); - } - - override async connectedCallback() { - super.connectedCallback(); - - // Poll for key existence - when key is added, resolve and close - const checkInterval = setInterval(async () => { - const hasKey = !!(await getAppStorage().providerKeys.get(this.provider)); - if (hasKey) { - clearInterval(checkInterval); - if (this.resolvePromise) { - this.resolvePromise(true); - this.resolvePromise = undefined; - } - this.close(); - } - }, 500); - - this.unsubscribe = () => clearInterval(checkInterval); - } - - override disconnectedCallback() { - super.disconnectedCallback(); - if (this.unsubscribe) { - this.unsubscribe(); - this.unsubscribe = undefined; - } - } - - override close() { - super.close(); - if (this.resolvePromise) { - this.resolvePromise(false); - } - } - - protected override renderContent() { - return html` - ${DialogContent({ - children: html` - ${DialogHeader({ - title: i18n("API Key Required"), - })} - - `, - })} - `; - } -} diff --git a/packages/web-ui/src/dialogs/AttachmentOverlay.ts b/packages/web-ui/src/dialogs/AttachmentOverlay.ts deleted file mode 100644 index 329a98f9..00000000 --- a/packages/web-ui/src/dialogs/AttachmentOverlay.ts +++ /dev/null @@ -1,636 +0,0 @@ -import "@mariozechner/mini-lit/dist/ModeToggle.js"; -import { icon } from "@mariozechner/mini-lit"; -import { Button } from "@mariozechner/mini-lit/dist/Button.js"; -import { renderAsync } from "docx-preview"; -import { html, LitElement } from "lit"; -import { state } from "lit/decorators.js"; -import { Download, X } from "lucide"; -import * as pdfjsLib from "pdfjs-dist"; -import * as XLSX from "xlsx"; -import type { Attachment } from "../utils/attachment-utils.ts"; -import { i18n } from "../utils/i18n.ts"; - -type FileType = "image" | "pdf" | "docx" | "pptx" | "excel" | "text"; - -export class AttachmentOverlay extends LitElement { - @state() private attachment?: Attachment; - @state() private showExtractedText = false; - @state() private error: string | null = null; - - // Track current loading task to cancel if needed - private currentLoadingTask: any = null; - private onCloseCallback?: () => void; - private boundHandleKeyDown?: (e: KeyboardEvent) => void; - - protected override createRenderRoot(): HTMLElement | DocumentFragment { - return this; - } - - static open(attachment: Attachment, onClose?: () => void) { - const overlay = new AttachmentOverlay(); - overlay.attachment = attachment; - overlay.onCloseCallback = onClose; - document.body.appendChild(overlay); - overlay.setupEventListeners(); - } - - private setupEventListeners() { - this.boundHandleKeyDown = (e: KeyboardEvent) => { - if (e.key === "Escape") { - this.close(); - } - }; - window.addEventListener("keydown", this.boundHandleKeyDown); - } - - private close() { - this.cleanup(); - if (this.boundHandleKeyDown) { - window.removeEventListener("keydown", this.boundHandleKeyDown); - } - this.onCloseCallback?.(); - this.remove(); - } - - private getFileType(): FileType { - if (!this.attachment) return "text"; - - if (this.attachment.type === "image") return "image"; - if (this.attachment.mimeType === "application/pdf") return "pdf"; - if (this.attachment.mimeType?.includes("wordprocessingml")) return "docx"; - if ( - this.attachment.mimeType?.includes("presentationml") || - this.attachment.fileName.toLowerCase().endsWith(".pptx") - ) - return "pptx"; - if ( - this.attachment.mimeType?.includes("spreadsheetml") || - this.attachment.mimeType?.includes("ms-excel") || - this.attachment.fileName.toLowerCase().endsWith(".xlsx") || - this.attachment.fileName.toLowerCase().endsWith(".xls") - ) - return "excel"; - - return "text"; - } - - private getFileTypeLabel(): string { - const type = this.getFileType(); - switch (type) { - case "pdf": - return i18n("PDF"); - case "docx": - return i18n("Document"); - case "pptx": - return i18n("Presentation"); - case "excel": - return i18n("Spreadsheet"); - default: - return ""; - } - } - - private handleBackdropClick = () => { - this.close(); - }; - - private handleDownload = () => { - if (!this.attachment) return; - - // Create a blob from the base64 content - const byteCharacters = atob(this.attachment.content); - const byteNumbers = new Array(byteCharacters.length); - for (let i = 0; i < byteCharacters.length; i++) { - byteNumbers[i] = byteCharacters.charCodeAt(i); - } - const byteArray = new Uint8Array(byteNumbers); - const blob = new Blob([byteArray], { type: this.attachment.mimeType }); - - // Create download link - const url = URL.createObjectURL(blob); - const a = document.createElement("a"); - a.href = url; - a.download = this.attachment.fileName; - document.body.appendChild(a); - a.click(); - document.body.removeChild(a); - URL.revokeObjectURL(url); - }; - - private cleanup() { - this.showExtractedText = false; - this.error = null; - // Cancel any loading PDF task when closing - if (this.currentLoadingTask) { - this.currentLoadingTask.destroy(); - this.currentLoadingTask = null; - } - } - - override render() { - if (!this.attachment) return html``; - - return html` - -
- -
e.stopPropagation()}> -
-
- ${this.attachment.fileName} -
-
- ${this.renderToggle()} - ${Button({ - variant: "ghost", - size: "icon", - onClick: this.handleDownload, - children: icon(Download, "sm"), - className: "h-8 w-8", - })} - ${Button({ - variant: "ghost", - size: "icon", - onClick: () => this.close(), - children: icon(X, "sm"), - className: "h-8 w-8", - })} -
-
-
- - -
e.stopPropagation()}> - ${this.renderContent()} -
-
- `; - } - - private renderToggle() { - if (!this.attachment) return html``; - - const fileType = this.getFileType(); - const hasExtractedText = !!this.attachment.extractedText; - const showToggle = fileType !== "image" && fileType !== "text" && fileType !== "pptx" && hasExtractedText; - - if (!showToggle) return html``; - - const fileTypeLabel = this.getFileTypeLabel(); - - return html` - ) => { - e.stopPropagation(); - this.showExtractedText = e.detail.index === 1; - this.error = null; - }} - > - `; - } - - private renderContent() { - if (!this.attachment) return html``; - - // Error state - if (this.error) { - return html` -
-
${i18n("Error loading file")}
-
${this.error}
-
- `; - } - - // Content based on file type - return this.renderFileContent(); - } - - private renderFileContent() { - if (!this.attachment) return html``; - - const fileType = this.getFileType(); - - // Show extracted text if toggled - if (this.showExtractedText && fileType !== "image") { - return html` -
-
${
-						this.attachment.extractedText || i18n("No text content available")
-					}
-
- `; - } - - // Render based on file type - switch (fileType) { - case "image": { - const imageUrl = `data:${this.attachment.mimeType};base64,${this.attachment.content}`; - return html` - ${this.attachment.fileName} - `; - } - - case "pdf": - return html` -
- `; - - case "docx": - return html` -
- `; - - case "excel": - return html`
`; - - case "pptx": - return html` -
- `; - - default: - return html` -
-
${
-							this.attachment.extractedText || i18n("No content available")
-						}
-
- `; - } - } - - override async updated(changedProperties: Map) { - super.updated(changedProperties); - - // Only process if we need to render the actual file (not extracted text) - if ( - (changedProperties.has("attachment") || changedProperties.has("showExtractedText")) && - this.attachment && - !this.showExtractedText && - !this.error - ) { - const fileType = this.getFileType(); - - switch (fileType) { - case "pdf": - await this.renderPdf(); - break; - case "docx": - await this.renderDocx(); - break; - case "excel": - await this.renderExcel(); - break; - case "pptx": - await this.renderExtractedText(); - break; - } - } - } - - private async renderPdf() { - const container = this.querySelector("#pdf-container"); - if (!container || !this.attachment) return; - - let pdf: any = null; - - try { - // Convert base64 to ArrayBuffer - const arrayBuffer = this.base64ToArrayBuffer(this.attachment.content); - - // Cancel any existing loading task - if (this.currentLoadingTask) { - this.currentLoadingTask.destroy(); - } - - // Load the PDF - this.currentLoadingTask = pdfjsLib.getDocument({ data: arrayBuffer }); - pdf = await this.currentLoadingTask.promise; - this.currentLoadingTask = null; - - // Clear container and add wrapper - container.innerHTML = ""; - const wrapper = document.createElement("div"); - wrapper.className = ""; - container.appendChild(wrapper); - - // Render all pages - for (let pageNum = 1; pageNum <= pdf.numPages; pageNum++) { - const page = await pdf.getPage(pageNum); - - // Create a container for each page - const pageContainer = document.createElement("div"); - pageContainer.className = "mb-4 last:mb-0"; - - // Create canvas for this page - const canvas = document.createElement("canvas"); - const context = canvas.getContext("2d"); - - // Set scale for reasonable resolution - const viewport = page.getViewport({ scale: 1.5 }); - canvas.height = viewport.height; - canvas.width = viewport.width; - - // Style the canvas - canvas.className = "w-full max-w-full h-auto block mx-auto bg-white rounded shadow-sm border border-border"; - - // Fill white background for proper PDF rendering - if (context) { - context.fillStyle = "white"; - context.fillRect(0, 0, canvas.width, canvas.height); - } - - // Render page - await page.render({ - canvasContext: context!, - viewport: viewport, - canvas: canvas, - }).promise; - - pageContainer.appendChild(canvas); - - // Add page separator for multi-page documents - if (pageNum < pdf.numPages) { - const separator = document.createElement("div"); - separator.className = "h-px bg-border my-4"; - pageContainer.appendChild(separator); - } - - wrapper.appendChild(pageContainer); - } - } catch (error: any) { - console.error("Error rendering PDF:", error); - this.error = error?.message || i18n("Failed to load PDF"); - } finally { - if (pdf) { - pdf.destroy(); - } - } - } - - private async renderDocx() { - const container = this.querySelector("#docx-container"); - if (!container || !this.attachment) return; - - try { - // Convert base64 to ArrayBuffer - const arrayBuffer = this.base64ToArrayBuffer(this.attachment.content); - - // Clear container first - container.innerHTML = ""; - - // Create a wrapper div for the document - const wrapper = document.createElement("div"); - wrapper.className = "docx-wrapper-custom"; - container.appendChild(wrapper); - - // Render the DOCX file into the wrapper - await renderAsync(arrayBuffer, wrapper as HTMLElement, undefined, { - className: "docx", - inWrapper: true, - ignoreWidth: true, // Let it be responsive - ignoreHeight: false, - ignoreFonts: false, - breakPages: true, - ignoreLastRenderedPageBreak: true, - experimental: false, - trimXmlDeclaration: true, - useBase64URL: false, - renderHeaders: true, - renderFooters: true, - renderFootnotes: true, - renderEndnotes: true, - }); - - // Apply custom styles to match theme and fix sizing - const style = document.createElement("style"); - style.textContent = ` - #docx-container { - padding: 0; - } - - #docx-container .docx-wrapper-custom { - max-width: 100%; - overflow-x: auto; - } - - #docx-container .docx-wrapper { - max-width: 100% !important; - margin: 0 !important; - background: transparent !important; - padding: 0em !important; - } - - #docx-container .docx-wrapper > section.docx { - box-shadow: none !important; - border: none !important; - border-radius: 0 !important; - margin: 0 !important; - padding: 2em !important; - background: white !important; - color: black !important; - max-width: 100% !important; - width: 100% !important; - min-width: 0 !important; - overflow-x: auto !important; - } - - /* Fix tables and wide content */ - #docx-container table { - max-width: 100% !important; - width: auto !important; - overflow-x: auto !important; - display: block !important; - } - - #docx-container img { - max-width: 100% !important; - height: auto !important; - } - - /* Fix paragraphs and text */ - #docx-container p, - #docx-container span, - #docx-container div { - max-width: 100% !important; - word-wrap: break-word !important; - overflow-wrap: break-word !important; - } - - /* Hide page breaks in web view */ - #docx-container .docx-page-break { - display: none !important; - } - `; - container.appendChild(style); - } catch (error: any) { - console.error("Error rendering DOCX:", error); - this.error = error?.message || i18n("Failed to load document"); - } - } - - private async renderExcel() { - const container = this.querySelector("#excel-container"); - if (!container || !this.attachment) return; - - try { - // Convert base64 to ArrayBuffer - const arrayBuffer = this.base64ToArrayBuffer(this.attachment.content); - - // Read the workbook - const workbook = XLSX.read(arrayBuffer, { type: "array" }); - - // Clear container - container.innerHTML = ""; - const wrapper = document.createElement("div"); - wrapper.className = "overflow-auto h-full flex flex-col"; - container.appendChild(wrapper); - - // Create tabs for multiple sheets - if (workbook.SheetNames.length > 1) { - const tabContainer = document.createElement("div"); - tabContainer.className = "flex gap-2 mb-4 border-b border-border sticky top-0 bg-card z-10"; - - const sheetContents: HTMLElement[] = []; - - workbook.SheetNames.forEach((sheetName, index) => { - // Create tab button - const tab = document.createElement("button"); - tab.textContent = sheetName; - tab.className = - index === 0 - ? "px-4 py-2 text-sm font-medium border-b-2 border-primary text-primary" - : "px-4 py-2 text-sm font-medium text-muted-foreground hover:text-foreground hover:border-b-2 hover:border-border transition-colors"; - - // Create sheet content - const sheetDiv = document.createElement("div"); - sheetDiv.style.display = index === 0 ? "flex" : "none"; - sheetDiv.className = "flex-1 overflow-auto"; - sheetDiv.appendChild(this.renderExcelSheet(workbook.Sheets[sheetName], sheetName)); - sheetContents.push(sheetDiv); - - // Tab click handler - tab.onclick = () => { - // Update tab styles - tabContainer.querySelectorAll("button").forEach((btn, btnIndex) => { - if (btnIndex === index) { - btn.className = "px-4 py-2 text-sm font-medium border-b-2 border-primary text-primary"; - } else { - btn.className = - "px-4 py-2 text-sm font-medium text-muted-foreground hover:text-foreground hover:border-b-2 hover:border-border transition-colors"; - } - }); - // Show/hide sheets - sheetContents.forEach((content, contentIndex) => { - content.style.display = contentIndex === index ? "flex" : "none"; - }); - }; - - tabContainer.appendChild(tab); - }); - - wrapper.appendChild(tabContainer); - sheetContents.forEach((content) => { - wrapper.appendChild(content); - }); - } else { - // Single sheet - const sheetName = workbook.SheetNames[0]; - wrapper.appendChild(this.renderExcelSheet(workbook.Sheets[sheetName], sheetName)); - } - } catch (error: any) { - console.error("Error rendering Excel:", error); - this.error = error?.message || i18n("Failed to load spreadsheet"); - } - } - - private renderExcelSheet(worksheet: any, sheetName: string): HTMLElement { - const sheetDiv = document.createElement("div"); - - // Generate HTML table - const htmlTable = XLSX.utils.sheet_to_html(worksheet, { id: `sheet-${sheetName}` }); - const tempDiv = document.createElement("div"); - tempDiv.innerHTML = htmlTable; - - // Find and style the table - const table = tempDiv.querySelector("table"); - if (table) { - table.className = "w-full border-collapse text-foreground"; - - // Style all cells - table.querySelectorAll("td, th").forEach((cell) => { - const cellEl = cell as HTMLElement; - cellEl.className = "border border-border px-3 py-2 text-sm text-left"; - }); - - // Style header row - const headerCells = table.querySelectorAll("thead th, tr:first-child td"); - if (headerCells.length > 0) { - headerCells.forEach((th) => { - const thEl = th as HTMLElement; - thEl.className = - "border border-border px-3 py-2 text-sm font-semibold bg-muted text-foreground sticky top-0"; - }); - } - - // Alternate row colors - table.querySelectorAll("tbody tr:nth-child(even)").forEach((row) => { - const rowEl = row as HTMLElement; - rowEl.className = "bg-muted/30"; - }); - - sheetDiv.appendChild(table); - } - - return sheetDiv; - } - - private base64ToArrayBuffer(base64: string): ArrayBuffer { - const binaryString = atob(base64); - const bytes = new Uint8Array(binaryString.length); - for (let i = 0; i < binaryString.length; i++) { - bytes[i] = binaryString.charCodeAt(i); - } - return bytes.buffer; - } - - private async renderExtractedText() { - const container = this.querySelector("#pptx-container"); - if (!container || !this.attachment) return; - - try { - // Display the extracted text content - container.innerHTML = ""; - const wrapper = document.createElement("div"); - wrapper.className = "p-6 overflow-auto"; - - // Create a pre element to preserve formatting - const pre = document.createElement("pre"); - pre.className = "whitespace-pre-wrap text-sm text-foreground font-mono"; - pre.textContent = this.attachment.extractedText || i18n("No text content available"); - - wrapper.appendChild(pre); - container.appendChild(wrapper); - } catch (error: any) { - console.error("Error rendering extracted text:", error); - this.error = error?.message || i18n("Failed to display text content"); - } - } -} - -// Register the custom element only once -if (!customElements.get("attachment-overlay")) { - customElements.define("attachment-overlay", AttachmentOverlay); -} diff --git a/packages/web-ui/src/dialogs/CustomProviderDialog.ts b/packages/web-ui/src/dialogs/CustomProviderDialog.ts deleted file mode 100644 index c39777b2..00000000 --- a/packages/web-ui/src/dialogs/CustomProviderDialog.ts +++ /dev/null @@ -1,274 +0,0 @@ -import type { Model } from "@earendil-works/pi-ai"; -import { i18n } from "@mariozechner/mini-lit"; -import { Button } from "@mariozechner/mini-lit/dist/Button.js"; -import { DialogBase } from "@mariozechner/mini-lit/dist/DialogBase.js"; -import { Input } from "@mariozechner/mini-lit/dist/Input.js"; -import { Label } from "@mariozechner/mini-lit/dist/Label.js"; -import { Select } from "@mariozechner/mini-lit/dist/Select.js"; -import { html, type TemplateResult } from "lit"; -import { state } from "lit/decorators.js"; -import { getAppStorage } from "../storage/app-storage.ts"; -import type { CustomProvider, CustomProviderType } from "../storage/stores/custom-providers-store.ts"; -import { discoverModels } from "../utils/model-discovery.ts"; - -export class CustomProviderDialog extends DialogBase { - private provider?: CustomProvider; - private initialType?: CustomProviderType; - private onSaveCallback?: () => void; - - @state() private name = ""; - @state() private type: CustomProviderType = "openai-completions"; - @state() private baseUrl = ""; - @state() private apiKey = ""; - @state() private testing = false; - @state() private testError = ""; - @state() private discoveredModels: Model[] = []; - - protected modalWidth = "min(800px, 90vw)"; - protected modalHeight = "min(700px, 90vh)"; - - static async open( - provider: CustomProvider | undefined, - initialType: CustomProviderType | undefined, - onSave?: () => void, - ) { - const dialog = new CustomProviderDialog(); - dialog.provider = provider; - dialog.initialType = initialType; - dialog.onSaveCallback = onSave; - document.body.appendChild(dialog); - dialog.initializeFromProvider(); - dialog.open(); - dialog.requestUpdate(); - } - - private initializeFromProvider() { - if (this.provider) { - this.name = this.provider.name; - this.type = this.provider.type; - this.baseUrl = this.provider.baseUrl; - this.apiKey = this.provider.apiKey || ""; - this.discoveredModels = this.provider.models || []; - } else { - this.name = ""; - this.type = this.initialType || "openai-completions"; - this.baseUrl = ""; - this.updateDefaultBaseUrl(); - this.apiKey = ""; - this.discoveredModels = []; - } - this.testError = ""; - this.testing = false; - } - - private updateDefaultBaseUrl() { - if (this.baseUrl) return; - - const defaults: Record = { - ollama: "http://localhost:11434", - "llama.cpp": "http://localhost:8080", - vllm: "http://localhost:8000", - lmstudio: "http://localhost:1234", - "openai-completions": "", - "openai-responses": "", - "anthropic-messages": "", - }; - - this.baseUrl = defaults[this.type] || ""; - } - - private isAutoDiscoveryType(): boolean { - return this.type === "ollama" || this.type === "llama.cpp" || this.type === "vllm" || this.type === "lmstudio"; - } - - private async testConnection() { - if (!this.isAutoDiscoveryType()) return; - - this.testing = true; - this.testError = ""; - this.discoveredModels = []; - - try { - const models = await discoverModels( - this.type as "ollama" | "llama.cpp" | "vllm" | "lmstudio", - this.baseUrl, - this.apiKey || undefined, - ); - - this.discoveredModels = models.map((model) => ({ - ...model, - provider: this.name || this.type, - })); - - this.testError = ""; - } catch (error) { - this.testError = error instanceof Error ? error.message : String(error); - this.discoveredModels = []; - } finally { - this.testing = false; - this.requestUpdate(); - } - } - - private async save() { - if (!this.name || !this.baseUrl) { - alert(i18n("Please fill in all required fields")); - return; - } - - try { - const storage = getAppStorage(); - - const provider: CustomProvider = { - id: this.provider?.id || crypto.randomUUID(), - name: this.name, - type: this.type, - baseUrl: this.baseUrl, - apiKey: this.apiKey || undefined, - models: this.isAutoDiscoveryType() ? undefined : this.provider?.models || [], - }; - - await storage.customProviders.set(provider); - - if (this.onSaveCallback) { - this.onSaveCallback(); - } - this.close(); - } catch (error) { - console.error("Failed to save provider:", error); - alert(i18n("Failed to save provider")); - } - } - - protected override renderContent(): TemplateResult { - const providerTypes = [ - { value: "ollama", label: "Ollama (auto-discovery)" }, - { value: "llama.cpp", label: "llama.cpp (auto-discovery)" }, - { value: "vllm", label: "vLLM (auto-discovery)" }, - { value: "lmstudio", label: "LM Studio (auto-discovery)" }, - { value: "openai-completions", label: "OpenAI Completions Compatible" }, - { value: "openai-responses", label: "OpenAI Responses Compatible" }, - { value: "anthropic-messages", label: "Anthropic Messages Compatible" }, - ]; - - return html` -
-
-

- ${this.provider ? i18n("Edit Provider") : i18n("Add Provider")} -

-
- -
-
-
- ${Label({ htmlFor: "provider-name", children: i18n("Provider Name") })} - ${Input({ - value: this.name, - placeholder: i18n("e.g., My Ollama Server"), - onInput: (e: Event) => { - this.name = (e.target as HTMLInputElement).value; - this.requestUpdate(); - }, - })} -
- -
- ${Label({ htmlFor: "provider-type", children: i18n("Provider Type") })} - ${Select({ - value: this.type, - options: providerTypes.map((pt) => ({ - value: pt.value, - label: pt.label, - })), - onChange: (value: string) => { - this.type = value as CustomProviderType; - this.baseUrl = ""; - this.updateDefaultBaseUrl(); - this.requestUpdate(); - }, - width: "100%", - })} -
- -
- ${Label({ htmlFor: "base-url", children: i18n("Base URL") })} - ${Input({ - value: this.baseUrl, - placeholder: i18n("e.g., http://localhost:11434"), - onInput: (e: Event) => { - this.baseUrl = (e.target as HTMLInputElement).value; - this.requestUpdate(); - }, - })} -
- -
- ${Label({ htmlFor: "api-key", children: i18n("API Key (Optional)") })} - ${Input({ - type: "password", - value: this.apiKey, - placeholder: i18n("Leave empty if not required"), - onInput: (e: Event) => { - this.apiKey = (e.target as HTMLInputElement).value; - this.requestUpdate(); - }, - })} -
- - ${ - this.isAutoDiscoveryType() - ? html` -
- ${Button({ - onClick: () => this.testConnection(), - variant: "outline", - disabled: this.testing || !this.baseUrl, - children: this.testing ? i18n("Testing...") : i18n("Test Connection"), - })} - ${this.testError ? html`
${this.testError}
` : ""} - ${ - this.discoveredModels.length > 0 - ? html` -
- ${i18n("Discovered")} ${this.discoveredModels.length} ${i18n("models")}: -
    - ${this.discoveredModels.slice(0, 5).map((model) => html`
  • ${model.name}
  • `)} - ${ - this.discoveredModels.length > 5 - ? html`
  • ...${i18n("and")} ${this.discoveredModels.length - 5} ${i18n("more")}
  • ` - : "" - } -
-
- ` - : "" - } -
- ` - : html`
- ${i18n("For manual provider types, add models after saving the provider.")} -
` - } -
-
- -
- ${Button({ - onClick: () => this.close(), - variant: "ghost", - children: i18n("Cancel"), - })} - ${Button({ - onClick: () => this.save(), - variant: "default", - disabled: !this.name || !this.baseUrl, - children: i18n("Save"), - })} -
-
- `; - } -} - -customElements.define("custom-provider-dialog", CustomProviderDialog); diff --git a/packages/web-ui/src/dialogs/ModelSelector.ts b/packages/web-ui/src/dialogs/ModelSelector.ts deleted file mode 100644 index d23d2484..00000000 --- a/packages/web-ui/src/dialogs/ModelSelector.ts +++ /dev/null @@ -1,370 +0,0 @@ -import { getModels, getProviders, type Model, modelsAreEqual } from "@earendil-works/pi-ai"; -import { icon } from "@mariozechner/mini-lit"; -import { Badge } from "@mariozechner/mini-lit/dist/Badge.js"; -import { Button } from "@mariozechner/mini-lit/dist/Button.js"; -import { DialogHeader } from "@mariozechner/mini-lit/dist/Dialog.js"; -import { DialogBase } from "@mariozechner/mini-lit/dist/DialogBase.js"; -import { html, type PropertyValues, type TemplateResult } from "lit"; -import { customElement, state } from "lit/decorators.js"; -import { createRef, ref } from "lit/directives/ref.js"; -import { Brain, Image as ImageIcon } from "lucide"; -import { Input } from "../components/Input.ts"; -import { getAppStorage } from "../storage/app-storage.ts"; -import type { AutoDiscoveryProviderType } from "../storage/stores/custom-providers-store.ts"; -import { formatModelCost } from "../utils/format.ts"; -import { i18n } from "../utils/i18n.ts"; -import { discoverModels } from "../utils/model-discovery.ts"; - -/** - * Score a query against a text using subsequence matching. - * All query characters must appear in order in the text. - * Higher score = tighter match (fewer gaps between matched characters). - * Returns 0 if no match. - */ -function subsequenceScore(query: string, text: string): number { - let qi = 0; - let ti = 0; - let gaps = 0; - let lastMatchIndex = -1; - - while (qi < query.length && ti < text.length) { - if (query[qi] === text[ti]) { - if (lastMatchIndex >= 0) { - gaps += ti - lastMatchIndex - 1; - } - lastMatchIndex = ti; - qi++; - } - ti++; - } - - // All query chars must match - if (qi < query.length) return 0; - - // Score: longer query match = better, fewer gaps = better - // Normalize so exact substring gets highest score - return query.length / (query.length + gaps); -} - -@customElement("agent-model-selector") -export class ModelSelector extends DialogBase { - @state() currentModel: Model | null = null; - @state() searchQuery = ""; - @state() filterThinking = false; - @state() filterVision = false; - @state() customProvidersLoading = false; - @state() selectedIndex = 0; - @state() private navigationMode: "mouse" | "keyboard" = "mouse"; - @state() private customProviderModels: Model[] = []; - - private onSelectCallback?: (model: Model) => void; - private allowedProviders?: Set; - private scrollContainerRef = createRef(); - private searchInputRef = createRef(); - private lastMousePosition = { x: 0, y: 0 }; - - protected override modalWidth = "min(400px, 90vw)"; - - static async open( - currentModel: Model | null, - onSelect: (model: Model) => void, - allowedProviders?: string[], - ) { - const selector = new ModelSelector(); - selector.currentModel = currentModel; - selector.onSelectCallback = onSelect; - if (allowedProviders) { - selector.allowedProviders = new Set(allowedProviders); - } - selector.open(); - selector.loadCustomProviders(); - } - - override async firstUpdated(changedProperties: PropertyValues): Promise { - super.firstUpdated(changedProperties); - // Wait for dialog to be fully rendered - await this.updateComplete; - // Focus the search input when dialog opens - this.searchInputRef.value?.focus(); - - // Track actual mouse movement - this.addEventListener("mousemove", (e: MouseEvent) => { - // Check if mouse actually moved - if (e.clientX !== this.lastMousePosition.x || e.clientY !== this.lastMousePosition.y) { - this.lastMousePosition = { x: e.clientX, y: e.clientY }; - // Only switch to mouse mode on actual mouse movement - if (this.navigationMode === "keyboard") { - this.navigationMode = "mouse"; - // Update selection to the item under the mouse - const target = e.target as HTMLElement; - const modelItem = target.closest("[data-model-item]"); - if (modelItem) { - const allItems = this.scrollContainerRef.value?.querySelectorAll("[data-model-item]"); - if (allItems) { - const index = Array.from(allItems).indexOf(modelItem); - if (index !== -1) { - this.selectedIndex = index; - } - } - } - } - } - }); - - // Add global keyboard handler for the dialog - this.addEventListener("keydown", (e: KeyboardEvent) => { - // Ignore key events during IME composition (e.g. CJK input) - if (e.isComposing || e.key === "Process") return; - - // Get filtered models to know the bounds - const filteredModels = this.getFilteredModels(); - - if (e.key === "ArrowDown") { - e.preventDefault(); - this.navigationMode = "keyboard"; - this.selectedIndex = Math.min(this.selectedIndex + 1, filteredModels.length - 1); - this.scrollToSelected(); - } else if (e.key === "ArrowUp") { - e.preventDefault(); - this.navigationMode = "keyboard"; - this.selectedIndex = Math.max(this.selectedIndex - 1, 0); - this.scrollToSelected(); - } else if (e.key === "Enter") { - e.preventDefault(); - if (filteredModels[this.selectedIndex]) { - this.handleSelect(filteredModels[this.selectedIndex].model); - } - } - }); - } - - private async loadCustomProviders() { - this.customProvidersLoading = true; - const allCustomModels: Model[] = []; - - try { - const storage = getAppStorage(); - const customProviders = await storage.customProviders.getAll(); - - // Load models from custom providers - for (const provider of customProviders) { - const isAutoDiscovery: boolean = - provider.type === "ollama" || - provider.type === "llama.cpp" || - provider.type === "vllm" || - provider.type === "lmstudio"; - - if (isAutoDiscovery) { - try { - const models = await discoverModels( - provider.type as AutoDiscoveryProviderType, - provider.baseUrl, - provider.apiKey, - ); - - const modelsWithProvider = models.map((model) => ({ - ...model, - provider: provider.name, - })); - - allCustomModels.push(...modelsWithProvider); - } catch (error) { - console.debug(`Failed to load models from ${provider.name}:`, error); - } - } else if (provider.models) { - // Manual provider - models already defined - allCustomModels.push(...provider.models); - } - } - } catch (error) { - console.error("Failed to load custom providers:", error); - } finally { - this.customProviderModels = allCustomModels; - this.customProvidersLoading = false; - this.requestUpdate(); - } - } - - private formatTokens(tokens: number): string { - if (tokens >= 1000000) return `${(tokens / 1000000).toFixed(0)}M`; - if (tokens >= 1000) return `${(tokens / 1000).toFixed(0)}`; - return String(tokens); - } - - private handleSelect(model: Model) { - if (model) { - this.onSelectCallback?.(model); - this.close(); - } - } - - private getFilteredModels(): Array<{ provider: string; id: string; model: any }> { - // Collect all models from known providers - const allModels: Array<{ provider: string; id: string; model: any }> = []; - const knownProviders = getProviders(); - - for (const provider of knownProviders) { - const models = getModels(provider as any); - for (const model of models) { - allModels.push({ provider, id: model.id, model }); - } - } - - // Add custom provider models - for (const model of this.customProviderModels) { - allModels.push({ provider: model.provider, id: model.id, model }); - } - - // Filter by allowed providers if set - if (this.allowedProviders) { - const allowed = this.allowedProviders; - allModels.splice(0, allModels.length, ...allModels.filter(({ provider }) => allowed.has(provider))); - } - - // Filter models based on search and capability filters - let filteredModels = allModels; - - // Apply search filter (subsequence match: characters must appear in order) - if (this.searchQuery) { - const query = this.searchQuery.toLowerCase().replace(/\s+/g, ""); - if (query) { - const scored: Array<{ item: (typeof allModels)[0]; score: number }> = []; - for (const entry of filteredModels) { - const searchText = `${entry.provider} ${entry.id} ${entry.model.name}`.toLowerCase(); - const score = subsequenceScore(query, searchText); - if (score > 0) { - scored.push({ item: entry, score }); - } - } - scored.sort((a, b) => b.score - a.score); - filteredModels = scored.map((s) => s.item); - } - } - - // Apply capability filters - if (this.filterThinking) { - filteredModels = filteredModels.filter(({ model }) => model.reasoning); - } - if (this.filterVision) { - filteredModels = filteredModels.filter(({ model }) => model.input.includes("image")); - } - - // Sort: when not searching, current model first then by provider. - // When searching, preserve the score-based order from above, - // but still float the current model to the top. - if (!this.searchQuery) { - filteredModels.sort((a, b) => { - const aIsCurrent = modelsAreEqual(this.currentModel, a.model); - const bIsCurrent = modelsAreEqual(this.currentModel, b.model); - if (aIsCurrent && !bIsCurrent) return -1; - if (!aIsCurrent && bIsCurrent) return 1; - return a.provider.localeCompare(b.provider); - }); - } - - return filteredModels; - } - - private scrollToSelected() { - requestAnimationFrame(() => { - const scrollContainer = this.scrollContainerRef.value; - const selectedElement = scrollContainer?.querySelectorAll("[data-model-item]")[ - this.selectedIndex - ] as HTMLElement; - if (selectedElement) { - selectedElement.scrollIntoView({ block: "nearest", behavior: "smooth" }); - } - }); - } - - protected override renderContent(): TemplateResult { - const filteredModels = this.getFilteredModels(); - - return html` - -
- ${DialogHeader({ title: i18n("Select Model") })} - ${Input({ - placeholder: i18n("Search models..."), - value: this.searchQuery, - inputRef: this.searchInputRef, - onInput: (e: Event) => { - this.searchQuery = (e.target as HTMLInputElement).value; - this.selectedIndex = 0; - // Reset scroll position when search changes - if (this.scrollContainerRef.value) { - this.scrollContainerRef.value.scrollTop = 0; - } - }, - })} -
- ${Button({ - variant: this.filterThinking ? "default" : "secondary", - size: "sm", - onClick: () => { - this.filterThinking = !this.filterThinking; - this.selectedIndex = 0; - if (this.scrollContainerRef.value) { - this.scrollContainerRef.value.scrollTop = 0; - } - }, - className: "rounded-full", - children: html`${icon(Brain, "sm")} ${i18n("Thinking")}`, - })} - ${Button({ - variant: this.filterVision ? "default" : "secondary", - size: "sm", - onClick: () => { - this.filterVision = !this.filterVision; - this.selectedIndex = 0; - if (this.scrollContainerRef.value) { - this.scrollContainerRef.value.scrollTop = 0; - } - }, - className: "rounded-full", - children: html`${icon(ImageIcon, "sm")} ${i18n("Vision")}`, - })} -
-
- - -
- ${filteredModels.map(({ provider, id, model }, index) => { - const isCurrent = modelsAreEqual(this.currentModel, model); - const isSelected = index === this.selectedIndex; - return html` -
this.handleSelect(model)} - @mouseenter=${() => { - // Only update selection in mouse mode - if (this.navigationMode === "mouse") { - this.selectedIndex = index; - } - }} - > -
-
- ${id} - ${isCurrent ? html`` : ""} -
- ${Badge(provider, "outline")} -
-
-
- ${icon(Brain, "sm")} - ${icon(ImageIcon, "sm")} - ${this.formatTokens(model.contextWindow)}K/${this.formatTokens(model.maxTokens)}K -
- ${formatModelCost(model.cost)} -
-
- `; - })} -
- `; - } -} diff --git a/packages/web-ui/src/dialogs/PersistentStorageDialog.ts b/packages/web-ui/src/dialogs/PersistentStorageDialog.ts deleted file mode 100644 index 8db72764..00000000 --- a/packages/web-ui/src/dialogs/PersistentStorageDialog.ts +++ /dev/null @@ -1,144 +0,0 @@ -import { Button } from "@mariozechner/mini-lit/dist/Button.js"; -import { DialogContent, DialogHeader } from "@mariozechner/mini-lit/dist/Dialog.js"; -import { DialogBase } from "@mariozechner/mini-lit/dist/DialogBase.js"; -import { html } from "lit"; -import { customElement, state } from "lit/decorators.js"; -import { i18n } from "../utils/i18n.ts"; - -@customElement("persistent-storage-dialog") -export class PersistentStorageDialog extends DialogBase { - @state() private requesting = false; - - private resolvePromise?: (userApproved: boolean) => void; - - protected modalWidth = "min(500px, 90vw)"; - protected modalHeight = "auto"; - - /** - * Request persistent storage permission. - * Returns true if browser granted persistent storage, false otherwise. - */ - static async request(): Promise { - // Check if already persisted - if (navigator.storage?.persisted) { - const alreadyPersisted = await navigator.storage.persisted(); - if (alreadyPersisted) { - console.log("✓ Persistent storage already granted"); - return true; - } - } - - // Show dialog and wait for user response - const dialog = new PersistentStorageDialog(); - dialog.open(); - - const userApproved = await new Promise((resolve) => { - dialog.resolvePromise = resolve; - }); - - if (!userApproved) { - console.warn("⚠ User declined persistent storage - sessions may be lost"); - return false; - } - - // User approved, request from browser - if (!navigator.storage?.persist) { - console.warn("⚠ Persistent storage API not available"); - return false; - } - - try { - const granted = await navigator.storage.persist(); - if (granted) { - console.log("✓ Persistent storage granted - sessions will be preserved"); - } else { - console.warn("⚠ Browser denied persistent storage - sessions may be lost under storage pressure"); - } - return granted; - } catch (error) { - console.error("Failed to request persistent storage:", error); - return false; - } - } - - private handleGrant() { - if (this.resolvePromise) { - this.resolvePromise(true); - this.resolvePromise = undefined; - } - this.close(); - } - - private handleDeny() { - if (this.resolvePromise) { - this.resolvePromise(false); - this.resolvePromise = undefined; - } - this.close(); - } - - override close() { - super.close(); - if (this.resolvePromise) { - this.resolvePromise(false); - } - } - - protected override renderContent() { - return html` - ${DialogContent({ - children: html` - ${DialogHeader({ - title: i18n("Storage Permission Required"), - description: i18n("This app needs persistent storage to save your conversations"), - })} - -
-
-
- - - - - -
-
-

${i18n("Why is this needed?")}

-

- ${i18n( - "Without persistent storage, your browser may delete saved conversations when it needs disk space. Granting this permission ensures your chat history is preserved.", - )} -

-
-
- -
-

${i18n("What this means:")}

-
    -
  • ${i18n("Your conversations will be saved locally in your browser")}
  • -
  • ${i18n("Data will not be deleted automatically to free up space")}
  • -
  • ${i18n("You can still manually clear data at any time")}
  • -
  • ${i18n("No data is sent to external servers")}
  • -
-
-
- -
- ${Button({ - variant: "outline", - onClick: () => this.handleDeny(), - disabled: this.requesting, - children: i18n("Continue Anyway"), - })} - ${Button({ - variant: "default", - onClick: () => this.handleGrant(), - disabled: this.requesting, - children: this.requesting ? i18n("Requesting...") : i18n("Grant Permission"), - })} -
- `, - })} - `; - } -} diff --git a/packages/web-ui/src/dialogs/ProvidersModelsTab.ts b/packages/web-ui/src/dialogs/ProvidersModelsTab.ts deleted file mode 100644 index b2eee51f..00000000 --- a/packages/web-ui/src/dialogs/ProvidersModelsTab.ts +++ /dev/null @@ -1,212 +0,0 @@ -import { getProviders } from "@earendil-works/pi-ai"; -import { i18n } from "@mariozechner/mini-lit"; -import { Select } from "@mariozechner/mini-lit/dist/Select.js"; -import { html, type TemplateResult } from "lit"; -import { customElement, state } from "lit/decorators.js"; -import "../components/CustomProviderCard.ts"; -import "../components/ProviderKeyInput.ts"; -import { getAppStorage } from "../storage/app-storage.ts"; -import type { - AutoDiscoveryProviderType, - CustomProvider, - CustomProviderType, -} from "../storage/stores/custom-providers-store.ts"; -import { discoverModels } from "../utils/model-discovery.ts"; -import { CustomProviderDialog } from "./CustomProviderDialog.ts"; -import { SettingsTab } from "./SettingsDialog.ts"; - -@customElement("providers-models-tab") -export class ProvidersModelsTab extends SettingsTab { - @state() private customProviders: CustomProvider[] = []; - @state() private providerStatus: Map< - string, - { modelCount: number; status: "connected" | "disconnected" | "checking" } - > = new Map(); - - override async connectedCallback() { - super.connectedCallback(); - await this.loadCustomProviders(); - } - - private async loadCustomProviders() { - try { - const storage = getAppStorage(); - this.customProviders = await storage.customProviders.getAll(); - - // Check status for auto-discovery providers - for (const provider of this.customProviders) { - const isAutoDiscovery = - provider.type === "ollama" || - provider.type === "llama.cpp" || - provider.type === "vllm" || - provider.type === "lmstudio"; - if (isAutoDiscovery) { - this.checkProviderStatus(provider); - } - } - } catch (error) { - console.error("Failed to load custom providers:", error); - } - } - - getTabName(): string { - return "Providers & Models"; - } - - private async checkProviderStatus(provider: CustomProvider) { - this.providerStatus.set(provider.id, { modelCount: 0, status: "checking" }); - this.requestUpdate(); - - try { - const models = await discoverModels( - provider.type as AutoDiscoveryProviderType, - provider.baseUrl, - provider.apiKey, - ); - - this.providerStatus.set(provider.id, { modelCount: models.length, status: "connected" }); - } catch (_error) { - this.providerStatus.set(provider.id, { modelCount: 0, status: "disconnected" }); - } - this.requestUpdate(); - } - - private renderKnownProviders(): TemplateResult { - const providers = getProviders(); - - return html` -
-
-

Cloud Providers

-

- Cloud LLM providers with predefined models. API keys are stored locally in your browser. -

-
-
- ${providers.map((provider) => html` `)} -
-
- `; - } - - private renderCustomProviders(): TemplateResult { - const isAutoDiscovery = (type: string) => - type === "ollama" || type === "llama.cpp" || type === "vllm" || type === "lmstudio"; - - return html` -
-
-
-

Custom Providers

-

- User-configured servers with auto-discovered or manually defined models. -

-
- ${Select({ - placeholder: i18n("Add Provider"), - options: [ - { value: "ollama", label: "Ollama" }, - { value: "llama.cpp", label: "llama.cpp" }, - { value: "vllm", label: "vLLM" }, - { value: "lmstudio", label: "LM Studio" }, - { value: "openai-completions", label: i18n("OpenAI Completions Compatible") }, - { value: "openai-responses", label: i18n("OpenAI Responses Compatible") }, - { value: "anthropic-messages", label: i18n("Anthropic Messages Compatible") }, - ], - onChange: (value: string) => this.addCustomProvider(value as CustomProviderType), - variant: "outline", - size: "sm", - })} -
- - ${ - this.customProviders.length === 0 - ? html` -
- No custom providers configured. Click 'Add Provider' to get started. -
- ` - : html` -
- ${this.customProviders.map( - (provider) => html` - this.refreshProvider(p)} - .onEdit=${(p: CustomProvider) => this.editProvider(p)} - .onDelete=${(p: CustomProvider) => this.deleteProvider(p)} - > - `, - )} -
- ` - } -
- `; - } - - private async addCustomProvider(type: CustomProviderType) { - await CustomProviderDialog.open(undefined, type, async () => { - await this.loadCustomProviders(); - this.requestUpdate(); - }); - } - - private async editProvider(provider: CustomProvider) { - await CustomProviderDialog.open(provider, undefined, async () => { - await this.loadCustomProviders(); - this.requestUpdate(); - }); - } - - private async refreshProvider(provider: CustomProvider) { - this.providerStatus.set(provider.id, { modelCount: 0, status: "checking" }); - this.requestUpdate(); - - try { - const models = await discoverModels( - provider.type as AutoDiscoveryProviderType, - provider.baseUrl, - provider.apiKey, - ); - - this.providerStatus.set(provider.id, { modelCount: models.length, status: "connected" }); - this.requestUpdate(); - - console.log(`Refreshed ${models.length} models from ${provider.name}`); - } catch (error) { - this.providerStatus.set(provider.id, { modelCount: 0, status: "disconnected" }); - this.requestUpdate(); - - console.error(`Failed to refresh provider ${provider.name}:`, error); - alert(`Failed to refresh provider: ${error instanceof Error ? error.message : String(error)}`); - } - } - - private async deleteProvider(provider: CustomProvider) { - if (!confirm("Are you sure you want to delete this provider?")) { - return; - } - - try { - const storage = getAppStorage(); - await storage.customProviders.delete(provider.id); - await this.loadCustomProviders(); - this.requestUpdate(); - } catch (error) { - console.error("Failed to delete provider:", error); - } - } - - render(): TemplateResult { - return html` -
- ${this.renderKnownProviders()} -
- ${this.renderCustomProviders()} -
- `; - } -} diff --git a/packages/web-ui/src/dialogs/SessionListDialog.ts b/packages/web-ui/src/dialogs/SessionListDialog.ts deleted file mode 100644 index b3ad3108..00000000 --- a/packages/web-ui/src/dialogs/SessionListDialog.ts +++ /dev/null @@ -1,150 +0,0 @@ -import { DialogContent, DialogHeader } from "@mariozechner/mini-lit/dist/Dialog.js"; -import { DialogBase } from "@mariozechner/mini-lit/dist/DialogBase.js"; -import { html } from "lit"; -import { customElement, state } from "lit/decorators.js"; -import { getAppStorage } from "../storage/app-storage.ts"; -import type { SessionMetadata } from "../storage/types.ts"; -import { formatUsage } from "../utils/format.ts"; -import { i18n } from "../utils/i18n.ts"; - -@customElement("session-list-dialog") -export class SessionListDialog extends DialogBase { - @state() private sessions: SessionMetadata[] = []; - @state() private loading = true; - - private onSelectCallback?: (sessionId: string) => void; - private onDeleteCallback?: (sessionId: string) => void; - private deletedSessions = new Set(); - private closedViaSelection = false; - - protected modalWidth = "min(600px, 90vw)"; - protected modalHeight = "min(700px, 90vh)"; - - static async open(onSelect: (sessionId: string) => void, onDelete?: (sessionId: string) => void) { - const dialog = new SessionListDialog(); - dialog.onSelectCallback = onSelect; - dialog.onDeleteCallback = onDelete; - dialog.open(); - await dialog.loadSessions(); - } - - private async loadSessions() { - this.loading = true; - try { - const storage = getAppStorage(); - this.sessions = await storage.sessions.getAllMetadata(); - } catch (err) { - console.error("Failed to load sessions:", err); - this.sessions = []; - } finally { - this.loading = false; - } - } - - private async handleDelete(sessionId: string, event: Event) { - event.stopPropagation(); - - if (!confirm(i18n("Delete this session?"))) { - return; - } - - try { - const storage = getAppStorage(); - if (!storage.sessions) return; - - await storage.sessions.deleteSession(sessionId); - await this.loadSessions(); - - // Track deleted session - this.deletedSessions.add(sessionId); - } catch (err) { - console.error("Failed to delete session:", err); - } - } - - override close() { - super.close(); - - // Only notify about deleted sessions if dialog wasn't closed via selection - if (!this.closedViaSelection && this.onDeleteCallback && this.deletedSessions.size > 0) { - for (const sessionId of this.deletedSessions) { - this.onDeleteCallback(sessionId); - } - } - } - - private handleSelect(sessionId: string) { - this.closedViaSelection = true; - if (this.onSelectCallback) { - this.onSelectCallback(sessionId); - } - this.close(); - } - - private formatDate(isoString: string): string { - const date = new Date(isoString); - const now = new Date(); - const diff = now.getTime() - date.getTime(); - const days = Math.floor(diff / (1000 * 60 * 60 * 24)); - - if (days === 0) { - return i18n("Today"); - } else if (days === 1) { - return i18n("Yesterday"); - } else if (days < 7) { - return i18n("{days} days ago").replace("{days}", days.toString()); - } else { - return date.toLocaleDateString(); - } - } - - protected override renderContent() { - return html` - ${DialogContent({ - className: "h-full flex flex-col", - children: html` - ${DialogHeader({ - title: i18n("Sessions"), - description: i18n("Load a previous conversation"), - })} - -
- ${ - this.loading - ? html`
${i18n("Loading...")}
` - : this.sessions.length === 0 - ? html`
${i18n("No sessions yet")}
` - : this.sessions.map( - (session) => html` -
this.handleSelect(session.id)} - > -
-
${session.title}
-
${this.formatDate(session.lastModified)}
-
- ${session.messageCount} ${i18n("messages")} · ${formatUsage(session.usage)} -
-
- -
- `, - ) - } -
- `, - })} - `; - } -} diff --git a/packages/web-ui/src/dialogs/SettingsDialog.ts b/packages/web-ui/src/dialogs/SettingsDialog.ts deleted file mode 100644 index 6d914827..00000000 --- a/packages/web-ui/src/dialogs/SettingsDialog.ts +++ /dev/null @@ -1,218 +0,0 @@ -import { getProviders } from "@earendil-works/pi-ai"; -import { i18n } from "@mariozechner/mini-lit"; -import { Dialog, DialogContent, DialogHeader } from "@mariozechner/mini-lit/dist/Dialog.js"; -import { Input } from "@mariozechner/mini-lit/dist/Input.js"; -import { Label } from "@mariozechner/mini-lit/dist/Label.js"; -import { Switch } from "@mariozechner/mini-lit/dist/Switch.js"; -import { html, LitElement, type TemplateResult } from "lit"; -import { customElement, property, state } from "lit/decorators.js"; -import "../components/ProviderKeyInput.ts"; -import { getAppStorage } from "../storage/app-storage.ts"; - -// Base class for settings tabs -export abstract class SettingsTab extends LitElement { - abstract getTabName(): string; - - protected createRenderRoot() { - return this; - } -} - -// API Keys Tab -@customElement("api-keys-tab") -export class ApiKeysTab extends SettingsTab { - getTabName(): string { - return i18n("API Keys"); - } - - render(): TemplateResult { - const providers = getProviders(); - - return html` -
-

- ${i18n("Configure API keys for LLM providers. Keys are stored locally in your browser.")} -

- ${providers.map((provider) => html``)} -
- `; - } -} - -// Proxy Tab -@customElement("proxy-tab") -export class ProxyTab extends SettingsTab { - @state() private proxyEnabled = false; - @state() private proxyUrl = "http://localhost:3001"; - - override async connectedCallback() { - super.connectedCallback(); - // Load proxy settings when tab is connected - try { - const storage = getAppStorage(); - const enabled = await storage.settings.get("proxy.enabled"); - const url = await storage.settings.get("proxy.url"); - - if (enabled !== null) this.proxyEnabled = enabled; - if (url !== null) this.proxyUrl = url; - } catch (error) { - console.error("Failed to load proxy settings:", error); - } - } - - private async saveProxySettings() { - try { - const storage = getAppStorage(); - await storage.settings.set("proxy.enabled", this.proxyEnabled); - await storage.settings.set("proxy.url", this.proxyUrl); - } catch (error) { - console.error("Failed to save proxy settings:", error); - } - } - - getTabName(): string { - return i18n("Proxy"); - } - - render(): TemplateResult { - return html` -
-

- ${i18n("Allows browser-based apps to bypass CORS restrictions when calling LLM providers. Required for Z-AI and Anthropic with OAuth token.")} -

- -
- ${i18n("Use CORS Proxy")} - ${Switch({ - checked: this.proxyEnabled, - onChange: (checked: boolean) => { - this.proxyEnabled = checked; - this.saveProxySettings(); - }, - })} -
- -
- ${Label({ children: i18n("Proxy URL") })} - ${Input({ - type: "text", - value: this.proxyUrl, - disabled: !this.proxyEnabled, - onInput: (e) => { - this.proxyUrl = (e.target as HTMLInputElement).value; - }, - onChange: () => this.saveProxySettings(), - })} -

- ${i18n("Format: The proxy must accept requests as /?url=")} -

-
-
- `; - } -} - -@customElement("settings-dialog") -export class SettingsDialog extends LitElement { - @property({ type: Array, attribute: false }) tabs: SettingsTab[] = []; - @state() private isOpen = false; - @state() private activeTabIndex = 0; - - protected createRenderRoot() { - return this; - } - - private onCloseCallback?: () => void; - - static async open(tabs: SettingsTab[], onClose?: () => void) { - const dialog = new SettingsDialog(); - dialog.tabs = tabs; - dialog.onCloseCallback = onClose; - dialog.isOpen = true; - document.body.appendChild(dialog); - } - - private setActiveTab(index: number) { - this.activeTabIndex = index; - } - - private renderSidebarItem(tab: SettingsTab, index: number): TemplateResult { - const isActive = this.activeTabIndex === index; - return html` - - `; - } - - private renderMobileTab(tab: SettingsTab, index: number): TemplateResult { - const isActive = this.activeTabIndex === index; - return html` - - `; - } - - render() { - if (this.tabs.length === 0) { - return html``; - } - - return Dialog({ - isOpen: this.isOpen, - onClose: () => { - this.isOpen = false; - this.remove(); - this.onCloseCallback?.(); - }, - width: "min(1000px, 90vw)", - height: "min(800px, 90vh)", - backdropClassName: "bg-black/50 backdrop-blur-sm", - children: html` - ${DialogContent({ - className: "h-full p-6", - children: html` -
- -
${DialogHeader({ title: i18n("Settings") })}
- - -
- ${this.tabs.map((tab, index) => this.renderMobileTab(tab, index))} -
- - -
- - - - -
- ${this.tabs.map( - (tab, index) => - html`
${tab}
`, - )} -
-
-
- `, - })} - `, - }); - } -} diff --git a/packages/web-ui/src/index.ts b/packages/web-ui/src/index.ts deleted file mode 100644 index 22ba992e..00000000 --- a/packages/web-ui/src/index.ts +++ /dev/null @@ -1,120 +0,0 @@ -// Main chat interface - -export type { Agent, AgentMessage, AgentState, ThinkingLevel } from "@earendil-works/pi-agent-core"; -export type { Model } from "@earendil-works/pi-ai"; -export { ChatPanel } from "./ChatPanel.ts"; -// Components -export { AgentInterface } from "./components/AgentInterface.ts"; -export { AttachmentTile } from "./components/AttachmentTile.ts"; -export { ConsoleBlock } from "./components/ConsoleBlock.ts"; -export { CustomProviderCard } from "./components/CustomProviderCard.ts"; -export { ExpandableSection } from "./components/ExpandableSection.ts"; -export { Input } from "./components/Input.ts"; -export { MessageEditor } from "./components/MessageEditor.ts"; -export { MessageList } from "./components/MessageList.ts"; -// Message components -export type { ArtifactMessage, UserMessageWithAttachments } from "./components/Messages.ts"; -export { - AbortedMessage, - AssistantMessage, - convertAttachments, - defaultConvertToLlm, - isArtifactMessage, - isUserMessageWithAttachments, - ToolMessage, - ToolMessageDebugView, - UserMessage, -} from "./components/Messages.ts"; -// Message renderer registry -export { - getMessageRenderer, - type MessageRenderer, - type MessageRole, - registerMessageRenderer, - renderMessage, -} from "./components/message-renderer-registry.ts"; -export { ProviderKeyInput } from "./components/ProviderKeyInput.ts"; -export { - type SandboxFile, - SandboxIframe, - type SandboxResult, - type SandboxUrlProvider, -} from "./components/SandboxedIframe.ts"; -export { StreamingMessageContainer } from "./components/StreamingMessageContainer.ts"; -// Sandbox Runtime Providers -export { ArtifactsRuntimeProvider } from "./components/sandbox/ArtifactsRuntimeProvider.ts"; -export { AttachmentsRuntimeProvider } from "./components/sandbox/AttachmentsRuntimeProvider.ts"; -export { type ConsoleLog, ConsoleRuntimeProvider } from "./components/sandbox/ConsoleRuntimeProvider.ts"; -export { - type DownloadableFile, - FileDownloadRuntimeProvider, -} from "./components/sandbox/FileDownloadRuntimeProvider.ts"; -export { RuntimeMessageBridge } from "./components/sandbox/RuntimeMessageBridge.ts"; -export { RUNTIME_MESSAGE_ROUTER } from "./components/sandbox/RuntimeMessageRouter.ts"; -export type { SandboxRuntimeProvider } from "./components/sandbox/SandboxRuntimeProvider.ts"; -export { ThinkingBlock } from "./components/ThinkingBlock.ts"; -export { ApiKeyPromptDialog } from "./dialogs/ApiKeyPromptDialog.ts"; -export { AttachmentOverlay } from "./dialogs/AttachmentOverlay.ts"; -export { CustomProviderDialog } from "./dialogs/CustomProviderDialog.ts"; -// Dialogs -export { ModelSelector } from "./dialogs/ModelSelector.ts"; -export { PersistentStorageDialog } from "./dialogs/PersistentStorageDialog.ts"; -export { ProvidersModelsTab } from "./dialogs/ProvidersModelsTab.ts"; -export { SessionListDialog } from "./dialogs/SessionListDialog.ts"; -export { ApiKeysTab, ProxyTab, SettingsDialog, SettingsTab } from "./dialogs/SettingsDialog.ts"; -// Prompts -export { - ARTIFACTS_RUNTIME_PROVIDER_DESCRIPTION_RO, - ARTIFACTS_RUNTIME_PROVIDER_DESCRIPTION_RW, - ATTACHMENTS_RUNTIME_DESCRIPTION, -} from "./prompts/prompts.ts"; -// Storage -export { AppStorage, getAppStorage, setAppStorage } from "./storage/app-storage.ts"; -export { IndexedDBStorageBackend } from "./storage/backends/indexeddb-storage-backend.ts"; -export { Store } from "./storage/store.ts"; -export type { - AutoDiscoveryProviderType, - CustomProvider, - CustomProviderType, -} from "./storage/stores/custom-providers-store.ts"; -export { CustomProvidersStore } from "./storage/stores/custom-providers-store.ts"; -export { ProviderKeysStore } from "./storage/stores/provider-keys-store.ts"; -export { SessionsStore } from "./storage/stores/sessions-store.ts"; -export { SettingsStore } from "./storage/stores/settings-store.ts"; -export type { - IndexConfig, - IndexedDBConfig, - SessionData, - SessionMetadata, - StorageBackend, - StorageTransaction, - StoreConfig, -} from "./storage/types.ts"; -// Artifacts -export { ArtifactElement } from "./tools/artifacts/ArtifactElement.ts"; -export { ArtifactPill } from "./tools/artifacts/ArtifactPill.ts"; -export { type Artifact, ArtifactsPanel, type ArtifactsParams } from "./tools/artifacts/artifacts.ts"; -export { ArtifactsToolRenderer } from "./tools/artifacts/artifacts-tool-renderer.ts"; -export { HtmlArtifact } from "./tools/artifacts/HtmlArtifact.ts"; -export { ImageArtifact } from "./tools/artifacts/ImageArtifact.ts"; -export { MarkdownArtifact } from "./tools/artifacts/MarkdownArtifact.ts"; -export { SvgArtifact } from "./tools/artifacts/SvgArtifact.ts"; -export { TextArtifact } from "./tools/artifacts/TextArtifact.ts"; -export { createExtractDocumentTool, extractDocumentTool } from "./tools/extract-document.ts"; -// Tools -export { getToolRenderer, registerToolRenderer, renderTool, setShowJsonMode } from "./tools/index.ts"; -export { createJavaScriptReplTool, javascriptReplTool } from "./tools/javascript-repl.ts"; -export { renderCollapsibleHeader, renderHeader } from "./tools/renderer-registry.ts"; -export { BashRenderer } from "./tools/renderers/BashRenderer.ts"; -export { CalculateRenderer } from "./tools/renderers/CalculateRenderer.ts"; -// Tool renderers -export { DefaultRenderer } from "./tools/renderers/DefaultRenderer.ts"; -export { GetCurrentTimeRenderer } from "./tools/renderers/GetCurrentTimeRenderer.ts"; -export type { ToolRenderer, ToolRenderResult } from "./tools/types.ts"; -export type { Attachment } from "./utils/attachment-utils.ts"; -// Utils -export { loadAttachment } from "./utils/attachment-utils.ts"; -export { clearAuthToken, getAuthToken } from "./utils/auth-token.ts"; -export { formatCost, formatModelCost, formatTokenCount, formatUsage } from "./utils/format.ts"; -export { i18n, setLanguage, translations } from "./utils/i18n.ts"; -export { applyProxyIfNeeded, createStreamFn, isCorsError, shouldUseProxyForProvider } from "./utils/proxy-utils.ts"; diff --git a/packages/web-ui/src/prompts/prompts.ts b/packages/web-ui/src/prompts/prompts.ts deleted file mode 100644 index c11e0c48..00000000 --- a/packages/web-ui/src/prompts/prompts.ts +++ /dev/null @@ -1,282 +0,0 @@ -/** - * Centralized tool prompts/descriptions. - * Each prompt is either a string constant or a template function. - */ - -// ============================================================================ -// JavaScript REPL Tool -// ============================================================================ - -export const JAVASCRIPT_REPL_TOOL_DESCRIPTION = (runtimeProviderDescriptions: string[]) => `# JavaScript REPL - -## Purpose -Execute JavaScript code in a sandboxed browser environment with full Web APIs. - -## When to Use -- Quick calculations or data transformations -- Testing JavaScript code snippets in isolation -- Processing data with libraries (XLSX, CSV, etc.) -- Creating artifacts from data - -## Environment -- ES2023+ JavaScript (async/await, optional chaining, nullish coalescing, etc.) -- All browser APIs: DOM, Canvas, WebGL, Fetch, Web Workers, WebSockets, Crypto, etc. -- Import any npm package: await import('https://esm.run/package-name') - -## Common Libraries -- XLSX: const XLSX = await import('https://esm.run/xlsx'); -- CSV: const Papa = (await import('https://esm.run/papaparse')).default; -- Chart.js: const Chart = (await import('https://esm.run/chart.js/auto')).default; -- Three.js: const THREE = await import('https://esm.run/three'); - -## Persistence between tool calls -- Objects stored on global scope do not persist between calls. -- Use artifacts as a key-value JSON object store: - - Use createOrUpdateArtifact(filename, content) to persist data between calls. JSON objects are auto-stringified. - - Use listArtifacts() and getArtifact(filename) to read persisted data. JSON files are auto-parsed to objects. - - Prefer to use a single artifact throughout the session to store intermediate data (e.g. 'data.json'). - -## Input -- You have access to the user's attachments via listAttachments(), readTextAttachment(id), and readBinaryAttachment(id) -- You have access to previously created artifacts via listArtifacts() and getArtifact(filename) - -## Output -- All console.log() calls are captured for you to inspect. The user does not see these logs. -- Create artifacts for file results (images, JSON, CSV, etc.) which persiste throughout the - session and are accessible to you and the user. - -## Example -const data = [10, 20, 15, 25]; -const sum = data.reduce((a, b) => a + b, 0); -const avg = sum / data.length; -console.log('Sum:', sum, 'Average:', avg); - -## Important Notes -- Graphics: Use fixed dimensions (800x600), NOT window.innerWidth/Height -- Chart.js: Set options: { responsive: false, animation: false } -- Three.js: renderer.setSize(800, 600) with matching aspect ratio - -## Helper Functions (Automatically Available) - -These functions are injected into the execution environment and available globally: - -${runtimeProviderDescriptions.join("\n\n")} -`; - -// ============================================================================ -// Artifacts Tool -// ============================================================================ - -export const ARTIFACTS_TOOL_DESCRIPTION = (runtimeProviderDescriptions: string[]) => `# Artifacts - -Create and manage persistent files that live alongside the conversation. - -## When to Use - Artifacts Tool vs REPL - -**Use artifacts tool when YOU are the author:** -- Writing research summaries, analysis, ideas, documentation -- Creating markdown notes for user to read -- Building HTML applications/visualizations that present data -- Creating HTML artifacts that render charts from programmatically generated data - -**Use repl + artifact storage functions when CODE processes data:** -- Scraping workflows that extract and store data -- Processing CSV/Excel files programmatically -- Data transformation pipelines -- Binary file generation requiring libraries (PDF, DOCX) - -**Pattern: REPL generates data → Artifacts tool creates HTML that visualizes it** -Example: repl scrapes products → stores products.json → you author dashboard.html that reads products.json and renders Chart.js visualizations - -## Input -- { action: "create", filename: "notes.md", content: "..." } - Create new file -- { action: "update", filename: "notes.md", old_str: "...", new_str: "..." } - Update part of file (PREFERRED) -- { action: "rewrite", filename: "notes.md", content: "..." } - Replace entire file (LAST RESORT) -- { action: "get", filename: "data.json" } - Retrieve file content -- { action: "delete", filename: "old.csv" } - Delete file -- { action: "htmlArtifactLogs", filename: "app.html" } - Get console logs from HTML artifact - -## Returns -Depends on action: -- create/update/rewrite/delete: Success status or error -- get: File content -- htmlArtifactLogs: Console logs and errors - -## Supported File Types -✅ Text-based files you author: .md, .txt, .html, .js, .css, .json, .csv, .svg -❌ Binary files requiring libraries (use repl): .pdf, .docx - -## Critical - Prefer Update Over Rewrite -❌ NEVER: get entire file + rewrite to change small sections -✅ ALWAYS: update for targeted edits (token efficient) -✅ Ask: Can I describe the change as old_str → new_str? Use update. - ---- - -## HTML Artifacts - -Interactive HTML applications that can visualize data from other artifacts. - -### Data Access -- Can read artifacts created by repl and user attachments -- Use to build dashboards, visualizations, interactive tools -- See Helper Functions section below for available functions - -### Requirements -- Self-contained single file -- Import ES modules from esm.sh: -- Use Tailwind CDN: -- Can embed images from any domain: -- MUST set background color explicitly (avoid transparent) -- Inline CSS or Tailwind utility classes -- No localStorage/sessionStorage - -### Styling -- Use Tailwind utility classes for clean, functional designs -- Ensure responsive layout (iframe may be resized) -- Avoid purple gradients, AI aesthetic clichés, and emojis - -### Helper Functions (Automatically Available) - -These functions are injected into HTML artifact sandbox: - -${runtimeProviderDescriptions.join("\n\n")} -`; - -// ============================================================================ -// Artifacts Runtime Provider -// ============================================================================ - -export const ARTIFACTS_RUNTIME_PROVIDER_DESCRIPTION_RW = ` -### Artifacts Storage - -Create, read, update, and delete files in artifacts storage. - -#### When to Use -- Store intermediate results between tool calls -- Save generated files (images, CSVs, processed data) for user to view and download - -#### Do NOT Use For -- Content you author directly, like summaries of content you read (use artifacts tool instead) - -#### Functions -- listArtifacts() - List all artifact filenames, returns Promise -- getArtifact(filename) - Read artifact content, returns Promise. JSON files auto-parse to objects, binary files return base64 string -- createOrUpdateArtifact(filename, content, mimeType?) - Create or update artifact, returns Promise. JSON files auto-stringify objects, binary requires base64 string with mimeType -- deleteArtifact(filename) - Delete artifact, returns Promise - -#### Example -JSON workflow: -\`\`\`javascript -// Fetch and save -const response = await fetch('https://api.example.com/products'); -const products = await response.json(); -await createOrUpdateArtifact('products.json', products); - -// Later: read and filter -const all = await getArtifact('products.json'); -const cheap = all.filter(p => p.price < 100); -await createOrUpdateArtifact('cheap.json', cheap); -\`\`\` - -Binary file (image): -\`\`\`javascript -const canvas = document.createElement('canvas'); -canvas.width = 800; canvas.height = 600; -const ctx = canvas.getContext('2d'); -ctx.fillStyle = 'blue'; -ctx.fillRect(0, 0, 800, 600); -// Remove data:image/png;base64, prefix -const base64 = canvas.toDataURL().split(',')[1]; -await createOrUpdateArtifact('chart.png', base64, 'image/png'); -\`\`\` -`; - -export const ARTIFACTS_RUNTIME_PROVIDER_DESCRIPTION_RO = ` -### Artifacts Storage - -Read files from artifacts storage. - -#### When to Use -- Read artifacts created by REPL or artifacts tool -- Access data from other HTML artifacts -- Load configuration or data files - -#### Do NOT Use For -- Creating new artifacts (not available in HTML artifacts) -- Modifying artifacts (read-only access) - -#### Functions -- listArtifacts() - List all artifact filenames, returns Promise -- getArtifact(filename) - Read artifact content, returns Promise. JSON files auto-parse to objects, binary files return base64 string - -#### Example -JSON data: -\`\`\`javascript -const products = await getArtifact('products.json'); -const html = products.map(p => \`
\${p.name}: $\${p.price}
\`).join(''); -document.body.innerHTML = html; -\`\`\` - -Binary image: -\`\`\`javascript -const base64 = await getArtifact('chart.png'); -const img = document.createElement('img'); -img.src = 'data:image/png;base64,' + base64; -document.body.appendChild(img); -\`\`\` -`; - -// ============================================================================ -// Attachments Runtime Provider -// ============================================================================ - -export const ATTACHMENTS_RUNTIME_DESCRIPTION = ` -### User Attachments - -Read files the user uploaded to the conversation. - -#### When to Use -- Process user-uploaded files (CSV, JSON, Excel, images, PDFs) - -#### Functions -- listAttachments() - List all attachments, returns array of {id, fileName, mimeType, size} -- readTextAttachment(id) - Read attachment as text, returns string -- readBinaryAttachment(id) - Read attachment as binary data, returns Uint8Array - -#### Example -CSV file: -\`\`\`javascript -const files = listAttachments(); -const csvFile = files.find(f => f.fileName.endsWith('.csv')); -const csvData = readTextAttachment(csvFile.id); -const rows = csvData.split('\\n').map(row => row.split(',')); -\`\`\` - -Excel file: -\`\`\`javascript -const XLSX = await import('https://esm.run/xlsx'); -const files = listAttachments(); -const xlsxFile = files.find(f => f.fileName.endsWith('.xlsx')); -const bytes = readBinaryAttachment(xlsxFile.id); -const workbook = XLSX.read(bytes); -const data = XLSX.utils.sheet_to_json(workbook.Sheets[workbook.SheetNames[0]]); -\`\`\` -`; - -// ============================================================================ -// Extract Document Tool -// ============================================================================ - -export const EXTRACT_DOCUMENT_DESCRIPTION = `# Extract Document - -Extract plain text from documents on the web (PDF, DOCX, XLSX, PPTX). - -## When to Use -User wants you to read a document at a URL. - -## Input -- { url: "https://example.com/document.pdf" } - URL to PDF, DOCX, XLSX, or PPTX - -## Returns -Structured plain text with page/sheet/slide delimiters.`; diff --git a/packages/web-ui/src/storage/app-storage.ts b/packages/web-ui/src/storage/app-storage.ts deleted file mode 100644 index cda5d3ef..00000000 --- a/packages/web-ui/src/storage/app-storage.ts +++ /dev/null @@ -1,60 +0,0 @@ -import type { CustomProvidersStore } from "./stores/custom-providers-store.ts"; -import type { ProviderKeysStore } from "./stores/provider-keys-store.ts"; -import type { SessionsStore } from "./stores/sessions-store.ts"; -import type { SettingsStore } from "./stores/settings-store.ts"; -import type { StorageBackend } from "./types.ts"; - -/** - * High-level storage API providing access to all storage operations. - * Subclasses can extend this to add domain-specific stores. - */ -export class AppStorage { - readonly backend: StorageBackend; - readonly settings: SettingsStore; - readonly providerKeys: ProviderKeysStore; - readonly sessions: SessionsStore; - readonly customProviders: CustomProvidersStore; - - constructor( - settings: SettingsStore, - providerKeys: ProviderKeysStore, - sessions: SessionsStore, - customProviders: CustomProvidersStore, - backend: StorageBackend, - ) { - this.settings = settings; - this.providerKeys = providerKeys; - this.sessions = sessions; - this.customProviders = customProviders; - this.backend = backend; - } - - async getQuotaInfo(): Promise<{ usage: number; quota: number; percent: number }> { - return this.backend.getQuotaInfo(); - } - - async requestPersistence(): Promise { - return this.backend.requestPersistence(); - } -} - -// Global instance management -let globalAppStorage: AppStorage | null = null; - -/** - * Get the global AppStorage instance. - * Throws if not initialized. - */ -export function getAppStorage(): AppStorage { - if (!globalAppStorage) { - throw new Error("AppStorage not initialized. Call setAppStorage() first."); - } - return globalAppStorage; -} - -/** - * Set the global AppStorage instance. - */ -export function setAppStorage(storage: AppStorage): void { - globalAppStorage = storage; -} diff --git a/packages/web-ui/src/storage/backends/indexeddb-storage-backend.ts b/packages/web-ui/src/storage/backends/indexeddb-storage-backend.ts deleted file mode 100644 index 0950db11..00000000 --- a/packages/web-ui/src/storage/backends/indexeddb-storage-backend.ts +++ /dev/null @@ -1,196 +0,0 @@ -import type { IndexedDBConfig, StorageBackend, StorageTransaction } from "../types.ts"; - -/** - * IndexedDB implementation of StorageBackend. - * Provides multi-store key-value storage with transactions and quota management. - */ -export class IndexedDBStorageBackend implements StorageBackend { - private dbPromise: Promise | null = null; - private config: IndexedDBConfig; - - constructor(config: IndexedDBConfig) { - this.config = config; - } - - private async getDB(): Promise { - if (!this.dbPromise) { - this.dbPromise = new Promise((resolve, reject) => { - const request = indexedDB.open(this.config.dbName, this.config.version); - - request.onerror = () => reject(request.error); - request.onsuccess = () => resolve(request.result); - - request.onupgradeneeded = (_event) => { - const db = request.result; - - // Create object stores from config - for (const storeConfig of this.config.stores) { - if (!db.objectStoreNames.contains(storeConfig.name)) { - const store = db.createObjectStore(storeConfig.name, { - keyPath: storeConfig.keyPath, - autoIncrement: storeConfig.autoIncrement, - }); - - // Create indices - if (storeConfig.indices) { - for (const indexConfig of storeConfig.indices) { - store.createIndex(indexConfig.name, indexConfig.keyPath, { - unique: indexConfig.unique, - }); - } - } - } - } - }; - }); - } - - return this.dbPromise; - } - - private promisifyRequest(request: IDBRequest): Promise { - return new Promise((resolve, reject) => { - request.onsuccess = () => resolve(request.result); - request.onerror = () => reject(request.error); - }); - } - - async get(storeName: string, key: string): Promise { - const db = await this.getDB(); - const tx = db.transaction(storeName, "readonly"); - const store = tx.objectStore(storeName); - const result = await this.promisifyRequest(store.get(key)); - return result ?? null; - } - - async set(storeName: string, key: string, value: T): Promise { - const db = await this.getDB(); - const tx = db.transaction(storeName, "readwrite"); - const store = tx.objectStore(storeName); - // If store has keyPath, only pass value (in-line key) - // Otherwise pass both value and key (out-of-line key) - if (store.keyPath) { - await this.promisifyRequest(store.put(value)); - } else { - await this.promisifyRequest(store.put(value, key)); - } - } - - async delete(storeName: string, key: string): Promise { - const db = await this.getDB(); - const tx = db.transaction(storeName, "readwrite"); - const store = tx.objectStore(storeName); - await this.promisifyRequest(store.delete(key)); - } - - async keys(storeName: string, prefix?: string): Promise { - const db = await this.getDB(); - const tx = db.transaction(storeName, "readonly"); - const store = tx.objectStore(storeName); - - if (prefix) { - // Use IDBKeyRange for efficient prefix filtering - const range = IDBKeyRange.bound(prefix, `${prefix}\uffff`, false, false); - const keys = await this.promisifyRequest(store.getAllKeys(range)); - return keys.map((k) => String(k)); - } else { - const keys = await this.promisifyRequest(store.getAllKeys()); - return keys.map((k) => String(k)); - } - } - - async getAllFromIndex( - storeName: string, - indexName: string, - direction: "asc" | "desc" = "asc", - ): Promise { - const db = await this.getDB(); - const tx = db.transaction(storeName, "readonly"); - const store = tx.objectStore(storeName); - const index = store.index(indexName); - - return new Promise((resolve, reject) => { - const results: T[] = []; - const request = index.openCursor(null, direction === "desc" ? "prev" : "next"); - - request.onsuccess = () => { - const cursor = request.result; - if (cursor) { - results.push(cursor.value as T); - cursor.continue(); - } else { - resolve(results); - } - }; - - request.onerror = () => reject(request.error); - }); - } - - async clear(storeName: string): Promise { - const db = await this.getDB(); - const tx = db.transaction(storeName, "readwrite"); - const store = tx.objectStore(storeName); - await this.promisifyRequest(store.clear()); - } - - async has(storeName: string, key: string): Promise { - const db = await this.getDB(); - const tx = db.transaction(storeName, "readonly"); - const store = tx.objectStore(storeName); - const result = await this.promisifyRequest(store.getKey(key)); - return result !== undefined; - } - - async transaction( - storeNames: string[], - mode: "readonly" | "readwrite", - operation: (tx: StorageTransaction) => Promise, - ): Promise { - const db = await this.getDB(); - const idbTx = db.transaction(storeNames, mode); - - const storageTx: StorageTransaction = { - get: async (storeName: string, key: string) => { - const store = idbTx.objectStore(storeName); - const result = await this.promisifyRequest(store.get(key)); - return (result ?? null) as T | null; - }, - set: async (storeName: string, key: string, value: T) => { - const store = idbTx.objectStore(storeName); - // If store has keyPath, only pass value (in-line key) - // Otherwise pass both value and key (out-of-line key) - if (store.keyPath) { - await this.promisifyRequest(store.put(value)); - } else { - await this.promisifyRequest(store.put(value, key)); - } - }, - delete: async (storeName: string, key: string) => { - const store = idbTx.objectStore(storeName); - await this.promisifyRequest(store.delete(key)); - }, - }; - - return operation(storageTx); - } - - async getQuotaInfo(): Promise<{ usage: number; quota: number; percent: number }> { - if (navigator.storage?.estimate) { - const estimate = await navigator.storage.estimate(); - return { - usage: estimate.usage || 0, - quota: estimate.quota || 0, - percent: estimate.quota ? ((estimate.usage || 0) / estimate.quota) * 100 : 0, - }; - } - return { usage: 0, quota: 0, percent: 0 }; - } - - async requestPersistence(): Promise { - if (navigator.storage?.persist) { - return await navigator.storage.persist(); - } - return false; - } -} diff --git a/packages/web-ui/src/storage/store.ts b/packages/web-ui/src/storage/store.ts deleted file mode 100644 index 60bb4563..00000000 --- a/packages/web-ui/src/storage/store.ts +++ /dev/null @@ -1,33 +0,0 @@ -import type { StorageBackend, StoreConfig } from "./types.ts"; - -/** - * Base class for all storage stores. - * Each store defines its IndexedDB schema and provides domain-specific methods. - */ -export abstract class Store { - private backend: StorageBackend | null = null; - - /** - * Returns the IndexedDB configuration for this store. - * Defines store name, key path, and indices. - */ - abstract getConfig(): StoreConfig; - - /** - * Sets the storage backend. Called by AppStorage after backend creation. - */ - setBackend(backend: StorageBackend): void { - this.backend = backend; - } - - /** - * Gets the storage backend. Throws if backend not set. - * Concrete stores must use this to access the backend. - */ - protected getBackend(): StorageBackend { - if (!this.backend) { - throw new Error(`Backend not set on ${this.constructor.name}`); - } - return this.backend; - } -} diff --git a/packages/web-ui/src/storage/stores/custom-providers-store.ts b/packages/web-ui/src/storage/stores/custom-providers-store.ts deleted file mode 100644 index de02f23e..00000000 --- a/packages/web-ui/src/storage/stores/custom-providers-store.ts +++ /dev/null @@ -1,62 +0,0 @@ -import type { Model } from "@earendil-works/pi-ai"; -import { Store } from "../store.ts"; -import type { StoreConfig } from "../types.ts"; - -export type AutoDiscoveryProviderType = "ollama" | "llama.cpp" | "vllm" | "lmstudio"; - -export type CustomProviderType = - | AutoDiscoveryProviderType // Auto-discovery - models fetched on-demand - | "openai-completions" // Manual models - stored in provider.models - | "openai-responses" // Manual models - stored in provider.models - | "anthropic-messages"; // Manual models - stored in provider.models - -export interface CustomProvider { - id: string; // UUID - name: string; // Display name, also used as Model.provider - type: CustomProviderType; - baseUrl: string; - apiKey?: string; // Optional, applies to all models - - // For manual types ONLY - models stored directly on provider - // Auto-discovery types: models fetched on-demand, never stored - models?: Model[]; -} - -/** - * Store for custom LLM providers (auto-discovery servers + manual providers). - */ -export class CustomProvidersStore extends Store { - getConfig(): StoreConfig { - return { - name: "custom-providers", - }; - } - - async get(id: string): Promise { - return this.getBackend().get("custom-providers", id); - } - - async set(provider: CustomProvider): Promise { - await this.getBackend().set("custom-providers", provider.id, provider); - } - - async delete(id: string): Promise { - await this.getBackend().delete("custom-providers", id); - } - - async getAll(): Promise { - const keys = await this.getBackend().keys("custom-providers"); - const providers: CustomProvider[] = []; - for (const key of keys) { - const provider = await this.get(key); - if (provider) { - providers.push(provider); - } - } - return providers; - } - - async has(id: string): Promise { - return this.getBackend().has("custom-providers", id); - } -} diff --git a/packages/web-ui/src/storage/stores/provider-keys-store.ts b/packages/web-ui/src/storage/stores/provider-keys-store.ts deleted file mode 100644 index 4ac1769c..00000000 --- a/packages/web-ui/src/storage/stores/provider-keys-store.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { Store } from "../store.ts"; -import type { StoreConfig } from "../types.ts"; - -/** - * Store for LLM provider API keys (Anthropic, OpenAI, etc.). - */ -export class ProviderKeysStore extends Store { - getConfig(): StoreConfig { - return { - name: "provider-keys", - }; - } - - async get(provider: string): Promise { - return this.getBackend().get("provider-keys", provider); - } - - async set(provider: string, key: string): Promise { - await this.getBackend().set("provider-keys", provider, key); - } - - async delete(provider: string): Promise { - await this.getBackend().delete("provider-keys", provider); - } - - async list(): Promise { - return this.getBackend().keys("provider-keys"); - } - - async has(provider: string): Promise { - return this.getBackend().has("provider-keys", provider); - } -} diff --git a/packages/web-ui/src/storage/stores/sessions-store.ts b/packages/web-ui/src/storage/stores/sessions-store.ts deleted file mode 100644 index 3fc40066..00000000 --- a/packages/web-ui/src/storage/stores/sessions-store.ts +++ /dev/null @@ -1,136 +0,0 @@ -import type { AgentState } from "@earendil-works/pi-agent-core"; -import { Store } from "../store.ts"; -import type { SessionData, SessionMetadata, StoreConfig } from "../types.ts"; - -/** - * Store for chat sessions (data and metadata). - * Uses two object stores: sessions (full data) and sessions-metadata (lightweight). - */ -export class SessionsStore extends Store { - getConfig(): StoreConfig { - return { - name: "sessions", - keyPath: "id", - indices: [{ name: "lastModified", keyPath: "lastModified" }], - }; - } - - /** - * Additional config for sessions-metadata store. - * Must be included when creating the backend. - */ - static getMetadataConfig(): StoreConfig { - return { - name: "sessions-metadata", - keyPath: "id", - indices: [{ name: "lastModified", keyPath: "lastModified" }], - }; - } - - async save(data: SessionData, metadata: SessionMetadata): Promise { - await this.getBackend().transaction(["sessions", "sessions-metadata"], "readwrite", async (tx) => { - await tx.set("sessions", data.id, data); - await tx.set("sessions-metadata", metadata.id, metadata); - }); - } - - async get(id: string): Promise { - return this.getBackend().get("sessions", id); - } - - async getMetadata(id: string): Promise { - return this.getBackend().get("sessions-metadata", id); - } - - async getAllMetadata(): Promise { - // Use the lastModified index to get sessions sorted by most recent first - return this.getBackend().getAllFromIndex("sessions-metadata", "lastModified", "desc"); - } - - async delete(id: string): Promise { - await this.getBackend().transaction(["sessions", "sessions-metadata"], "readwrite", async (tx) => { - await tx.delete("sessions", id); - await tx.delete("sessions-metadata", id); - }); - } - - // Alias for backward compatibility - async deleteSession(id: string): Promise { - return this.delete(id); - } - - async updateTitle(id: string, title: string): Promise { - const metadata = await this.getMetadata(id); - if (metadata) { - metadata.title = title; - await this.getBackend().set("sessions-metadata", id, metadata); - } - - // Also update in full session data - const data = await this.get(id); - if (data) { - data.title = title; - await this.getBackend().set("sessions", id, data); - } - } - - async getQuotaInfo(): Promise<{ usage: number; quota: number; percent: number }> { - return this.getBackend().getQuotaInfo(); - } - - async requestPersistence(): Promise { - return this.getBackend().requestPersistence(); - } - - // Alias methods for backward compatibility - async saveSession( - id: string, - state: AgentState, - metadata: SessionMetadata | undefined, - title?: string, - ): Promise { - // If metadata is provided, use it; otherwise create it from state - const meta: SessionMetadata = metadata || { - id, - title: title || "", - createdAt: new Date().toISOString(), - lastModified: new Date().toISOString(), - messageCount: state.messages?.length || 0, - usage: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - totalTokens: 0, - cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 }, - }, - thinkingLevel: state.thinkingLevel || "off", - preview: "", - }; - - const data: SessionData = { - id, - title: title || meta.title, - model: state.model, - thinkingLevel: state.thinkingLevel, - messages: state.messages || [], - createdAt: meta.createdAt, - lastModified: new Date().toISOString(), - }; - - await this.save(data, meta); - } - - async loadSession(id: string): Promise { - return this.get(id); - } - - async getLatestSessionId(): Promise { - const allMetadata = await this.getAllMetadata(); - if (allMetadata.length === 0) return null; - - // Sort by lastModified descending - allMetadata.sort((a, b) => b.lastModified.localeCompare(a.lastModified)); - return allMetadata[0].id; - } -} diff --git a/packages/web-ui/src/storage/stores/settings-store.ts b/packages/web-ui/src/storage/stores/settings-store.ts deleted file mode 100644 index 38d4f432..00000000 --- a/packages/web-ui/src/storage/stores/settings-store.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { Store } from "../store.ts"; -import type { StoreConfig } from "../types.ts"; - -/** - * Store for application settings (theme, proxy config, etc.). - */ -export class SettingsStore extends Store { - getConfig(): StoreConfig { - return { - name: "settings", - // No keyPath - uses out-of-line keys - }; - } - - async get(key: string): Promise { - return this.getBackend().get("settings", key); - } - - async set(key: string, value: T): Promise { - await this.getBackend().set("settings", key, value); - } - - async delete(key: string): Promise { - await this.getBackend().delete("settings", key); - } - - async list(): Promise { - return this.getBackend().keys("settings"); - } - - async clear(): Promise { - await this.getBackend().clear("settings"); - } -} diff --git a/packages/web-ui/src/storage/types.ts b/packages/web-ui/src/storage/types.ts deleted file mode 100644 index 7ed2bd0f..00000000 --- a/packages/web-ui/src/storage/types.ts +++ /dev/null @@ -1,206 +0,0 @@ -import type { AgentMessage, ThinkingLevel } from "@earendil-works/pi-agent-core"; -import type { Model } from "@earendil-works/pi-ai"; - -/** - * Transaction interface for atomic operations across stores. - */ -export interface StorageTransaction { - /** - * Get a value by key from a specific store. - */ - get(storeName: string, key: string): Promise; - - /** - * Set a value for a key in a specific store. - */ - set(storeName: string, key: string, value: T): Promise; - - /** - * Delete a key from a specific store. - */ - delete(storeName: string, key: string): Promise; -} - -/** - * Base interface for all storage backends. - * Multi-store key-value storage abstraction that can be implemented - * by IndexedDB, remote APIs, or any other multi-collection storage system. - */ -export interface StorageBackend { - /** - * Get a value by key from a specific store. Returns null if key doesn't exist. - */ - get(storeName: string, key: string): Promise; - - /** - * Set a value for a key in a specific store. - */ - set(storeName: string, key: string, value: T): Promise; - - /** - * Delete a key from a specific store. - */ - delete(storeName: string, key: string): Promise; - - /** - * Get all keys from a specific store, optionally filtered by prefix. - */ - keys(storeName: string, prefix?: string): Promise; - - /** - * Get all values from a specific store, ordered by an index. - * @param storeName - The store to query - * @param indexName - The index to use for ordering - * @param direction - Sort direction ("asc" or "desc") - */ - getAllFromIndex(storeName: string, indexName: string, direction?: "asc" | "desc"): Promise; - - /** - * Clear all data from a specific store. - */ - clear(storeName: string): Promise; - - /** - * Check if a key exists in a specific store. - */ - has(storeName: string, key: string): Promise; - - /** - * Execute atomic operations across multiple stores. - */ - transaction( - storeNames: string[], - mode: "readonly" | "readwrite", - operation: (tx: StorageTransaction) => Promise, - ): Promise; - - /** - * Get storage quota information. - * Used for warning users when approaching limits. - */ - getQuotaInfo(): Promise<{ usage: number; quota: number; percent: number }>; - - /** - * Request persistent storage (prevents eviction). - * Returns true if granted, false otherwise. - */ - requestPersistence(): Promise; -} - -/** - * Lightweight session metadata for listing and searching. - * Stored separately from full session data for performance. - */ -export interface SessionMetadata { - /** Unique session identifier (UUID v4) */ - id: string; - - /** User-defined title or auto-generated from first message */ - title: string; - - /** ISO 8601 UTC timestamp of creation */ - createdAt: string; - - /** ISO 8601 UTC timestamp of last modification */ - lastModified: string; - - /** Total number of messages (user + assistant + tool results) */ - messageCount: number; - - /** Cumulative usage statistics */ - usage: { - /** Total input tokens */ - input: number; - /** Total output tokens */ - output: number; - /** Total cache read tokens */ - cacheRead: number; - /** Total cache write tokens */ - cacheWrite: number; - /** Total tokens processed */ - totalTokens: number; - /** Total cost breakdown */ - cost: { - input: number; - output: number; - cacheRead: number; - cacheWrite: number; - total: number; - }; - }; - - /** Last used thinking level */ - thinkingLevel: ThinkingLevel; - - /** - * Preview text for search and display. - * First 2KB of conversation text (user + assistant messages in sequence). - * Tool calls and tool results are excluded. - */ - preview: string; -} - -/** - * Full session data including all messages. - * Only loaded when user opens a specific session. - */ -export interface SessionData { - /** Unique session identifier (UUID v4) */ - id: string; - - /** User-defined title or auto-generated from first message */ - title: string; - - /** Last selected model */ - model: Model; - - /** Last selected thinking level */ - thinkingLevel: ThinkingLevel; - - /** Full conversation history (with attachments inline) */ - messages: AgentMessage[]; - - /** ISO 8601 UTC timestamp of creation */ - createdAt: string; - - /** ISO 8601 UTC timestamp of last modification */ - lastModified: string; -} - -/** - * Configuration for IndexedDB backend. - */ -export interface IndexedDBConfig { - /** Database name */ - dbName: string; - /** Database version */ - version: number; - /** Object stores to create */ - stores: StoreConfig[]; -} - -/** - * Configuration for an IndexedDB object store. - */ -export interface StoreConfig { - /** Store name */ - name: string; - /** Key path (optional, for auto-extracting keys from objects) */ - keyPath?: string; - /** Auto-increment keys (optional) */ - autoIncrement?: boolean; - /** Indices to create on this store */ - indices?: IndexConfig[]; -} - -/** - * Configuration for an IndexedDB index. - */ -export interface IndexConfig { - /** Index name */ - name: string; - /** Key path to index on */ - keyPath: string; - /** Unique constraint (optional) */ - unique?: boolean; -} diff --git a/packages/web-ui/src/tools/artifacts/ArtifactElement.ts b/packages/web-ui/src/tools/artifacts/ArtifactElement.ts deleted file mode 100644 index f27aa7c8..00000000 --- a/packages/web-ui/src/tools/artifacts/ArtifactElement.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { LitElement, type TemplateResult } from "lit"; - -export abstract class ArtifactElement extends LitElement { - public filename = ""; - - protected override createRenderRoot(): HTMLElement | DocumentFragment { - return this; // light DOM for shared styles - } - - public abstract get content(): string; - public abstract set content(value: string); - - abstract getHeaderButtons(): TemplateResult | HTMLElement; -} diff --git a/packages/web-ui/src/tools/artifacts/ArtifactPill.ts b/packages/web-ui/src/tools/artifacts/ArtifactPill.ts deleted file mode 100644 index 28292771..00000000 --- a/packages/web-ui/src/tools/artifacts/ArtifactPill.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { icon } from "@mariozechner/mini-lit"; -import { html, type TemplateResult } from "lit"; -import { FileCode2 } from "lucide"; -import type { ArtifactsPanel } from "./artifacts.ts"; - -export function ArtifactPill(filename: string, artifactsPanel?: ArtifactsPanel): TemplateResult { - const handleClick = (e: Event) => { - if (!artifactsPanel) return; - e.preventDefault(); - e.stopPropagation(); - // openArtifact will show the artifact and call onOpen() to open the panel if needed - artifactsPanel.openArtifact(filename); - }; - - return html` - - ${icon(FileCode2, "sm")} - ${filename} - - `; -} diff --git a/packages/web-ui/src/tools/artifacts/Console.ts b/packages/web-ui/src/tools/artifacts/Console.ts deleted file mode 100644 index 809c7728..00000000 --- a/packages/web-ui/src/tools/artifacts/Console.ts +++ /dev/null @@ -1,93 +0,0 @@ -import { icon } from "@mariozechner/mini-lit"; -import "@mariozechner/mini-lit/dist/CopyButton.js"; -import { html, LitElement, type TemplateResult } from "lit"; -import { customElement, property, state } from "lit/decorators.js"; -import { createRef, type Ref, ref } from "lit/directives/ref.js"; -import { repeat } from "lit/directives/repeat.js"; -import { ChevronDown, ChevronRight, ChevronsDown, Lock } from "lucide"; -import { i18n } from "../../utils/i18n.ts"; - -interface LogEntry { - type: "log" | "error"; - text: string; -} - -@customElement("artifact-console") -export class Console extends LitElement { - @property({ attribute: false }) logs: LogEntry[] = []; - @state() private expanded = false; - @state() private autoscroll = true; - private logsContainerRef: Ref = createRef(); - - protected createRenderRoot() { - return this; // light DOM - } - - override updated() { - // Autoscroll to bottom when new logs arrive - if (this.autoscroll && this.expanded && this.logsContainerRef.value) { - this.logsContainerRef.value.scrollTop = this.logsContainerRef.value.scrollHeight; - } - } - - private getLogsText(): string { - return this.logs.map((l) => `[${l.type}] ${l.text}`).join("\n"); - } - - override render(): TemplateResult { - const errorCount = this.logs.filter((l) => l.type === "error").length; - const summary = - errorCount > 0 - ? `${i18n("console")} (${errorCount} ${errorCount === 1 ? "error" : "errors"})` - : `${i18n("console")} (${this.logs.length})`; - - return html` -
-
- - ${ - this.expanded - ? html` - - - ` - : "" - } -
- ${ - this.expanded - ? html` -
- ${repeat( - this.logs, - (_log, index) => index, - (log) => html` -
- [${log.type}] ${log.text} -
- `, - )} -
- ` - : "" - } -
- `; - } -} diff --git a/packages/web-ui/src/tools/artifacts/DocxArtifact.ts b/packages/web-ui/src/tools/artifacts/DocxArtifact.ts deleted file mode 100644 index 97f52c7d..00000000 --- a/packages/web-ui/src/tools/artifacts/DocxArtifact.ts +++ /dev/null @@ -1,213 +0,0 @@ -import { DownloadButton } from "@mariozechner/mini-lit/dist/DownloadButton.js"; -import { renderAsync } from "docx-preview"; -import { html, type TemplateResult } from "lit"; -import { customElement, property, state } from "lit/decorators.js"; -import { i18n } from "../../utils/i18n.ts"; -import { ArtifactElement } from "./ArtifactElement.ts"; - -@customElement("docx-artifact") -export class DocxArtifact extends ArtifactElement { - @property({ type: String }) private _content = ""; - @state() private error: string | null = null; - - get content(): string { - return this._content; - } - - set content(value: string) { - this._content = value; - this.error = null; - this.requestUpdate(); - } - - protected override createRenderRoot(): HTMLElement | DocumentFragment { - return this; - } - - override connectedCallback(): void { - super.connectedCallback(); - this.style.display = "block"; - this.style.height = "100%"; - } - - private base64ToArrayBuffer(base64: string): ArrayBuffer { - // Remove data URL prefix if present - let base64Data = base64; - if (base64.startsWith("data:")) { - const base64Match = base64.match(/base64,(.+)/); - if (base64Match) { - base64Data = base64Match[1]; - } - } - - const binaryString = atob(base64Data); - const bytes = new Uint8Array(binaryString.length); - for (let i = 0; i < binaryString.length; i++) { - bytes[i] = binaryString.charCodeAt(i); - } - return bytes.buffer; - } - - private decodeBase64(): Uint8Array { - let base64Data = this._content; - if (this._content.startsWith("data:")) { - const base64Match = this._content.match(/base64,(.+)/); - if (base64Match) { - base64Data = base64Match[1]; - } - } - - const binaryString = atob(base64Data); - const bytes = new Uint8Array(binaryString.length); - for (let i = 0; i < binaryString.length; i++) { - bytes[i] = binaryString.charCodeAt(i); - } - return bytes; - } - - public getHeaderButtons() { - return html` -
- ${DownloadButton({ - content: this.decodeBase64(), - filename: this.filename, - mimeType: "application/vnd.openxmlformats-officedocument.wordprocessingml.document", - title: i18n("Download"), - })} -
- `; - } - - override async updated(changedProperties: Map) { - super.updated(changedProperties); - - if (changedProperties.has("_content") && this._content && !this.error) { - await this.renderDocx(); - } - } - - private async renderDocx() { - const container = this.querySelector("#docx-container"); - if (!container || !this._content) return; - - try { - const arrayBuffer = this.base64ToArrayBuffer(this._content); - - // Clear container first - container.innerHTML = ""; - - // Create a wrapper div for the document - const wrapper = document.createElement("div"); - wrapper.className = "docx-wrapper-custom"; - container.appendChild(wrapper); - - // Render the DOCX file into the wrapper - await renderAsync(arrayBuffer, wrapper as HTMLElement, undefined, { - className: "docx", - inWrapper: true, - ignoreWidth: true, - ignoreHeight: false, - ignoreFonts: false, - breakPages: true, - ignoreLastRenderedPageBreak: true, - experimental: false, - trimXmlDeclaration: true, - useBase64URL: false, - renderHeaders: true, - renderFooters: true, - renderFootnotes: true, - renderEndnotes: true, - }); - - // Apply custom styles to match theme and fix sizing - const style = document.createElement("style"); - style.textContent = ` - #docx-container { - padding: 0; - } - - #docx-container .docx-wrapper-custom { - max-width: 100%; - overflow-x: auto; - } - - #docx-container .docx-wrapper { - max-width: 100% !important; - margin: 0 !important; - background: transparent !important; - padding: 0em !important; - } - - #docx-container .docx-wrapper > section.docx { - box-shadow: none !important; - border: none !important; - border-radius: 0 !important; - margin: 0 !important; - padding: 2em !important; - background: white !important; - color: black !important; - max-width: 100% !important; - width: 100% !important; - min-width: 0 !important; - overflow-x: auto !important; - } - - /* Fix tables and wide content */ - #docx-container table { - max-width: 100% !important; - width: auto !important; - overflow-x: auto !important; - display: block !important; - } - - #docx-container img { - max-width: 100% !important; - height: auto !important; - } - - /* Fix paragraphs and text */ - #docx-container p, - #docx-container span, - #docx-container div { - max-width: 100% !important; - word-wrap: break-word !important; - overflow-wrap: break-word !important; - } - - /* Hide page breaks in web view */ - #docx-container .docx-page-break { - display: none !important; - } - `; - container.appendChild(style); - } catch (error: any) { - console.error("Error rendering DOCX:", error); - this.error = error?.message || i18n("Failed to load document"); - } - } - - override render(): TemplateResult { - if (this.error) { - return html` -
-
-
${i18n("Error loading document")}
-
${this.error}
-
-
- `; - } - - return html` -
-
-
- `; - } -} - -declare global { - interface HTMLElementTagNameMap { - "docx-artifact": DocxArtifact; - } -} diff --git a/packages/web-ui/src/tools/artifacts/ExcelArtifact.ts b/packages/web-ui/src/tools/artifacts/ExcelArtifact.ts deleted file mode 100644 index cd488d7f..00000000 --- a/packages/web-ui/src/tools/artifacts/ExcelArtifact.ts +++ /dev/null @@ -1,231 +0,0 @@ -import { DownloadButton } from "@mariozechner/mini-lit/dist/DownloadButton.js"; -import { html, type TemplateResult } from "lit"; -import { customElement, property, state } from "lit/decorators.js"; -import * as XLSX from "xlsx"; -import { i18n } from "../../utils/i18n.ts"; -import { ArtifactElement } from "./ArtifactElement.ts"; - -@customElement("excel-artifact") -export class ExcelArtifact extends ArtifactElement { - @property({ type: String }) private _content = ""; - @state() private error: string | null = null; - - get content(): string { - return this._content; - } - - set content(value: string) { - this._content = value; - this.error = null; - this.requestUpdate(); - } - - protected override createRenderRoot(): HTMLElement | DocumentFragment { - return this; - } - - override connectedCallback(): void { - super.connectedCallback(); - this.style.display = "block"; - this.style.height = "100%"; - } - - private base64ToArrayBuffer(base64: string): ArrayBuffer { - // Remove data URL prefix if present - let base64Data = base64; - if (base64.startsWith("data:")) { - const base64Match = base64.match(/base64,(.+)/); - if (base64Match) { - base64Data = base64Match[1]; - } - } - - const binaryString = atob(base64Data); - const bytes = new Uint8Array(binaryString.length); - for (let i = 0; i < binaryString.length; i++) { - bytes[i] = binaryString.charCodeAt(i); - } - return bytes.buffer; - } - - private decodeBase64(): Uint8Array { - let base64Data = this._content; - if (this._content.startsWith("data:")) { - const base64Match = this._content.match(/base64,(.+)/); - if (base64Match) { - base64Data = base64Match[1]; - } - } - - const binaryString = atob(base64Data); - const bytes = new Uint8Array(binaryString.length); - for (let i = 0; i < binaryString.length; i++) { - bytes[i] = binaryString.charCodeAt(i); - } - return bytes; - } - - private getMimeType(): string { - const ext = this.filename.split(".").pop()?.toLowerCase(); - if (ext === "xls") return "application/vnd.ms-excel"; - return "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"; - } - - public getHeaderButtons() { - return html` -
- ${DownloadButton({ - content: this.decodeBase64(), - filename: this.filename, - mimeType: this.getMimeType(), - title: i18n("Download"), - })} -
- `; - } - - override async updated(changedProperties: Map) { - super.updated(changedProperties); - - if (changedProperties.has("_content") && this._content && !this.error) { - await this.renderExcel(); - } - } - - private async renderExcel() { - const container = this.querySelector("#excel-container"); - if (!container || !this._content) return; - - try { - const arrayBuffer = this.base64ToArrayBuffer(this._content); - const workbook = XLSX.read(arrayBuffer, { type: "array" }); - - container.innerHTML = ""; - const wrapper = document.createElement("div"); - wrapper.className = "overflow-auto h-full flex flex-col"; - container.appendChild(wrapper); - - // Create tabs for multiple sheets - if (workbook.SheetNames.length > 1) { - const tabContainer = document.createElement("div"); - tabContainer.className = "flex gap-2 mb-4 border-b border-border sticky top-0 bg-background z-10"; - - const sheetContents: HTMLElement[] = []; - - workbook.SheetNames.forEach((sheetName, index) => { - // Create tab button - const tab = document.createElement("button"); - tab.textContent = sheetName; - tab.className = - index === 0 - ? "px-4 py-2 text-sm font-medium border-b-2 border-primary text-primary" - : "px-4 py-2 text-sm font-medium text-muted-foreground hover:text-foreground hover:border-b-2 hover:border-border transition-colors"; - - // Create sheet content - const sheetDiv = document.createElement("div"); - sheetDiv.style.display = index === 0 ? "flex" : "none"; - sheetDiv.className = "flex-1 overflow-auto"; - sheetDiv.appendChild(this.renderExcelSheet(workbook.Sheets[sheetName], sheetName)); - sheetContents.push(sheetDiv); - - // Tab click handler - tab.onclick = () => { - // Update tab styles - tabContainer.querySelectorAll("button").forEach((btn, btnIndex) => { - if (btnIndex === index) { - btn.className = "px-4 py-2 text-sm font-medium border-b-2 border-primary text-primary"; - } else { - btn.className = - "px-4 py-2 text-sm font-medium text-muted-foreground hover:text-foreground hover:border-b-2 hover:border-border transition-colors"; - } - }); - // Show/hide sheets - sheetContents.forEach((content, contentIndex) => { - content.style.display = contentIndex === index ? "flex" : "none"; - }); - }; - - tabContainer.appendChild(tab); - }); - - wrapper.appendChild(tabContainer); - sheetContents.forEach((content) => { - wrapper.appendChild(content); - }); - } else { - // Single sheet - const sheetName = workbook.SheetNames[0]; - wrapper.appendChild(this.renderExcelSheet(workbook.Sheets[sheetName], sheetName)); - } - } catch (error: any) { - console.error("Error rendering Excel:", error); - this.error = error?.message || i18n("Failed to load spreadsheet"); - } - } - - private renderExcelSheet(worksheet: any, sheetName: string): HTMLElement { - const sheetDiv = document.createElement("div"); - - // Generate HTML table - const htmlTable = XLSX.utils.sheet_to_html(worksheet, { id: `sheet-${sheetName}` }); - const tempDiv = document.createElement("div"); - tempDiv.innerHTML = htmlTable; - - // Find and style the table - const table = tempDiv.querySelector("table"); - if (table) { - table.className = "w-full border-collapse text-foreground"; - - // Style all cells - table.querySelectorAll("td, th").forEach((cell) => { - const cellEl = cell as HTMLElement; - cellEl.className = "border border-border px-3 py-2 text-sm text-left"; - }); - - // Style header row - const headerCells = table.querySelectorAll("thead th, tr:first-child td"); - if (headerCells.length > 0) { - headerCells.forEach((th) => { - const thEl = th as HTMLElement; - thEl.className = - "border border-border px-3 py-2 text-sm font-semibold bg-muted text-foreground sticky top-0"; - }); - } - - // Alternate row colors - table.querySelectorAll("tbody tr:nth-child(even)").forEach((row) => { - const rowEl = row as HTMLElement; - rowEl.className = "bg-muted/30"; - }); - - sheetDiv.appendChild(table); - } - - return sheetDiv; - } - - override render(): TemplateResult { - if (this.error) { - return html` -
-
-
${i18n("Error loading spreadsheet")}
-
${this.error}
-
-
- `; - } - - return html` -
-
-
- `; - } -} - -declare global { - interface HTMLElementTagNameMap { - "excel-artifact": ExcelArtifact; - } -} diff --git a/packages/web-ui/src/tools/artifacts/GenericArtifact.ts b/packages/web-ui/src/tools/artifacts/GenericArtifact.ts deleted file mode 100644 index 43c8939c..00000000 --- a/packages/web-ui/src/tools/artifacts/GenericArtifact.ts +++ /dev/null @@ -1,117 +0,0 @@ -import { DownloadButton } from "@mariozechner/mini-lit/dist/DownloadButton.js"; -import { html, type TemplateResult } from "lit"; -import { customElement, property } from "lit/decorators.js"; -import { i18n } from "../../utils/i18n.ts"; -import { ArtifactElement } from "./ArtifactElement.ts"; - -@customElement("generic-artifact") -export class GenericArtifact extends ArtifactElement { - @property({ type: String }) private _content = ""; - - get content(): string { - return this._content; - } - - set content(value: string) { - this._content = value; - this.requestUpdate(); - } - - protected override createRenderRoot(): HTMLElement | DocumentFragment { - return this; - } - - override connectedCallback(): void { - super.connectedCallback(); - this.style.display = "block"; - this.style.height = "100%"; - } - - private decodeBase64(): Uint8Array { - let base64Data = this._content; - if (this._content.startsWith("data:")) { - const base64Match = this._content.match(/base64,(.+)/); - if (base64Match) { - base64Data = base64Match[1]; - } - } - - const binaryString = atob(base64Data); - const bytes = new Uint8Array(binaryString.length); - for (let i = 0; i < binaryString.length; i++) { - bytes[i] = binaryString.charCodeAt(i); - } - return bytes; - } - - private getMimeType(): string { - const ext = this.filename.split(".").pop()?.toLowerCase(); - // Add common MIME types - const mimeTypes: Record = { - pdf: "application/pdf", - zip: "application/zip", - tar: "application/x-tar", - gz: "application/gzip", - rar: "application/vnd.rar", - "7z": "application/x-7z-compressed", - mp3: "audio/mpeg", - mp4: "video/mp4", - avi: "video/x-msvideo", - mov: "video/quicktime", - wav: "audio/wav", - ogg: "audio/ogg", - json: "application/json", - xml: "application/xml", - bin: "application/octet-stream", - }; - return mimeTypes[ext || ""] || "application/octet-stream"; - } - - public getHeaderButtons() { - return html` -
- ${DownloadButton({ - content: this.decodeBase64(), - filename: this.filename, - mimeType: this.getMimeType(), - title: i18n("Download"), - })} -
- `; - } - - override render(): TemplateResult { - return html` -
-
-
- - - -
${this.filename}
-

- ${i18n("Preview not available for this file type.")} ${i18n("Click the download button above to view it on your computer.")} -

-
-
-
- `; - } -} - -declare global { - interface HTMLElementTagNameMap { - "generic-artifact": GenericArtifact; - } -} diff --git a/packages/web-ui/src/tools/artifacts/HtmlArtifact.ts b/packages/web-ui/src/tools/artifacts/HtmlArtifact.ts deleted file mode 100644 index ca13cb2e..00000000 --- a/packages/web-ui/src/tools/artifacts/HtmlArtifact.ts +++ /dev/null @@ -1,195 +0,0 @@ -import hljs from "highlight.js"; -import { html } from "lit"; -import { customElement, property, state } from "lit/decorators.js"; -import { createRef, type Ref, ref } from "lit/directives/ref.js"; -import { unsafeHTML } from "lit/directives/unsafe-html.js"; -import { RefreshCw } from "lucide"; -import type { SandboxIframe } from "../../components/SandboxedIframe.ts"; -import { type MessageConsumer, RUNTIME_MESSAGE_ROUTER } from "../../components/sandbox/RuntimeMessageRouter.ts"; -import type { SandboxRuntimeProvider } from "../../components/sandbox/SandboxRuntimeProvider.ts"; -import { i18n } from "../../utils/i18n.ts"; -import "../../components/SandboxedIframe.ts"; -import { ArtifactElement } from "./ArtifactElement.ts"; -import type { Console } from "./Console.ts"; -import "./Console.ts"; -import { icon } from "@mariozechner/mini-lit"; -import { Button } from "@mariozechner/mini-lit/dist/Button.js"; -import { CopyButton } from "@mariozechner/mini-lit/dist/CopyButton.js"; -import { DownloadButton } from "@mariozechner/mini-lit/dist/DownloadButton.js"; -import { PreviewCodeToggle } from "@mariozechner/mini-lit/dist/PreviewCodeToggle.js"; - -@customElement("html-artifact") -export class HtmlArtifact extends ArtifactElement { - @property() override filename = ""; - @property({ attribute: false }) runtimeProviders: SandboxRuntimeProvider[] = []; - @property({ attribute: false }) sandboxUrlProvider?: () => string; - - private _content = ""; - private logs: Array<{ type: "log" | "error"; text: string }> = []; - - // Refs for DOM elements - public sandboxIframeRef: Ref = createRef(); - private consoleRef: Ref = createRef(); - - @state() private viewMode: "preview" | "code" = "preview"; - - private setViewMode(mode: "preview" | "code") { - this.viewMode = mode; - } - - public getHeaderButtons() { - const toggle = new PreviewCodeToggle(); - toggle.mode = this.viewMode; - toggle.addEventListener("mode-change", (e: Event) => { - this.setViewMode((e as CustomEvent).detail); - }); - - const copyButton = new CopyButton(); - copyButton.text = this._content; - copyButton.title = i18n("Copy HTML"); - copyButton.showText = false; - - // Generate standalone HTML with all runtime code injected for download - const sandbox = this.sandboxIframeRef.value; - const sandboxId = `artifact-${this.filename}`; - const downloadContent = - sandbox?.prepareHtmlDocument(sandboxId, this._content, this.runtimeProviders || [], { - isHtmlArtifact: true, - isStandalone: true, // Skip runtime bridge and navigation interceptor for standalone downloads - }) || this._content; - - return html` -
- ${toggle} - ${Button({ - variant: "ghost", - size: "sm", - onClick: () => { - this.logs = []; - this.executeContent(this._content); - }, - title: i18n("Reload HTML"), - children: icon(RefreshCw, "sm"), - })} - ${copyButton} - ${DownloadButton({ content: downloadContent, filename: this.filename, mimeType: "text/html", title: i18n("Download HTML") })} -
- `; - } - - override set content(value: string) { - const oldValue = this._content; - this._content = value; - if (oldValue !== value) { - // Reset logs when content changes - this.logs = []; - this.requestUpdate(); - // Execute content in sandbox if it exists - if (this.sandboxIframeRef.value && value) { - this.executeContent(value); - } - } - } - - public executeContent(html: string) { - const sandbox = this.sandboxIframeRef.value; - if (!sandbox) return; - - // Configure sandbox URL provider if provided (for browser extensions) - if (this.sandboxUrlProvider) { - sandbox.sandboxUrlProvider = this.sandboxUrlProvider; - } - - const sandboxId = `artifact-${this.filename}`; - - // Create consumer for console messages - const consumer: MessageConsumer = { - handleMessage: async (message: any): Promise => { - if (message.type === "console") { - // Create new array reference for Lit reactivity - this.logs = [ - ...this.logs, - { - type: message.method === "error" ? "error" : "log", - text: message.text, - }, - ]; - this.requestUpdate(); // Re-render to show console - } - }, - }; - - // Inject window.complete() call at the end of the HTML to signal when page is loaded - // HTML artifacts don't time out - they call complete() when ready - let modifiedHtml = html; - if (modifiedHtml.includes("")) { - modifiedHtml = modifiedHtml.replace( - "", - "", - ); - } else { - // If no closing tag, append the script - modifiedHtml += ""; - } - - // Load content - this handles sandbox registration, consumer registration, and iframe creation - sandbox.loadContent(sandboxId, modifiedHtml, this.runtimeProviders, [consumer]); - } - - override get content(): string { - return this._content; - } - - override disconnectedCallback() { - super.disconnectedCallback(); - // Unregister sandbox when element is removed from DOM - const sandboxId = `artifact-${this.filename}`; - RUNTIME_MESSAGE_ROUTER.unregisterSandbox(sandboxId); - } - - override firstUpdated() { - // Execute initial content - if (this._content && this.sandboxIframeRef.value) { - this.executeContent(this._content); - } - } - - override updated(changedProperties: Map) { - super.updated(changedProperties); - // If we have content but haven't executed yet (e.g., during reconstruction), - // execute when the iframe ref becomes available - if (this._content && this.sandboxIframeRef.value && this.logs.length === 0) { - this.executeContent(this._content); - } - } - - public getLogs(): string { - if (this.logs.length === 0) return i18n("No logs for {filename}").replace("{filename}", this.filename); - return this.logs.map((l) => `[${l.type}] ${l.text}`).join("\n"); - } - - override render() { - return html` -
-
- -
- - ${ - this.logs.length > 0 - ? html`` - : "" - } -
- - -
-
${unsafeHTML(
-							hljs.highlight(this._content, { language: "html" }).value,
-						)}
-
-
-
- `; - } -} diff --git a/packages/web-ui/src/tools/artifacts/ImageArtifact.ts b/packages/web-ui/src/tools/artifacts/ImageArtifact.ts deleted file mode 100644 index a7a1cdac..00000000 --- a/packages/web-ui/src/tools/artifacts/ImageArtifact.ts +++ /dev/null @@ -1,116 +0,0 @@ -import { DownloadButton } from "@mariozechner/mini-lit/dist/DownloadButton.js"; -import { html, type TemplateResult } from "lit"; -import { customElement, property } from "lit/decorators.js"; -import { i18n } from "../../utils/i18n.ts"; -import { ArtifactElement } from "./ArtifactElement.ts"; - -@customElement("image-artifact") -export class ImageArtifact extends ArtifactElement { - @property({ type: String }) private _content = ""; - - get content(): string { - return this._content; - } - - set content(value: string) { - this._content = value; - this.requestUpdate(); - } - - protected override createRenderRoot(): HTMLElement | DocumentFragment { - return this; - } - - override connectedCallback(): void { - super.connectedCallback(); - this.style.display = "block"; - this.style.height = "100%"; - } - - private getMimeType(): string { - const ext = this.filename.split(".").pop()?.toLowerCase(); - if (ext === "jpg" || ext === "jpeg") return "image/jpeg"; - if (ext === "gif") return "image/gif"; - if (ext === "webp") return "image/webp"; - if (ext === "svg") return "image/svg+xml"; - if (ext === "bmp") return "image/bmp"; - if (ext === "ico") return "image/x-icon"; - return "image/png"; - } - - private getImageUrl(): string { - // If content is already a data URL, use it directly - if (this._content.startsWith("data:")) { - return this._content; - } - // Otherwise assume it's base64 and construct data URL - return `data:${this.getMimeType()};base64,${this._content}`; - } - - private decodeBase64(): Uint8Array { - let base64Data: string; - - // If content is a data URL, extract the base64 part - if (this._content.startsWith("data:")) { - const base64Match = this._content.match(/base64,(.+)/); - if (base64Match) { - base64Data = base64Match[1]; - } else { - // Not a base64 data URL, return empty - return new Uint8Array(0); - } - } else { - // Otherwise use content as-is - base64Data = this._content; - } - - // Decode base64 to binary string - const binaryString = atob(base64Data); - - // Convert binary string to Uint8Array - const bytes = new Uint8Array(binaryString.length); - for (let i = 0; i < binaryString.length; i++) { - bytes[i] = binaryString.charCodeAt(i); - } - - return bytes; - } - - public getHeaderButtons() { - return html` -
- ${DownloadButton({ - content: this.decodeBase64(), - filename: this.filename, - mimeType: this.getMimeType(), - title: i18n("Download"), - })} -
- `; - } - - override render(): TemplateResult { - return html` -
-
- ${this.filename} { - const target = e.target as HTMLImageElement; - target.src = - "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Ctext x='50' y='50' text-anchor='middle' dominant-baseline='middle' fill='%23999'%3EImage Error%3C/text%3E%3C/svg%3E"; - }} - /> -
-
- `; - } -} - -declare global { - interface HTMLElementTagNameMap { - "image-artifact": ImageArtifact; - } -} diff --git a/packages/web-ui/src/tools/artifacts/MarkdownArtifact.ts b/packages/web-ui/src/tools/artifacts/MarkdownArtifact.ts deleted file mode 100644 index 5a68168c..00000000 --- a/packages/web-ui/src/tools/artifacts/MarkdownArtifact.ts +++ /dev/null @@ -1,82 +0,0 @@ -import hljs from "highlight.js"; -import { html } from "lit"; -import { customElement, property, state } from "lit/decorators.js"; -import { unsafeHTML } from "lit/directives/unsafe-html.js"; -import { i18n } from "../../utils/i18n.ts"; -import "@mariozechner/mini-lit/dist/MarkdownBlock.js"; -import { CopyButton } from "@mariozechner/mini-lit/dist/CopyButton.js"; -import { DownloadButton } from "@mariozechner/mini-lit/dist/DownloadButton.js"; -import { PreviewCodeToggle } from "@mariozechner/mini-lit/dist/PreviewCodeToggle.js"; -import { ArtifactElement } from "./ArtifactElement.ts"; - -@customElement("markdown-artifact") -export class MarkdownArtifact extends ArtifactElement { - @property() override filename = ""; - - private _content = ""; - override get content(): string { - return this._content; - } - override set content(value: string) { - this._content = value; - this.requestUpdate(); - } - - @state() private viewMode: "preview" | "code" = "preview"; - - protected override createRenderRoot(): HTMLElement | DocumentFragment { - return this; // light DOM - } - - private setViewMode(mode: "preview" | "code") { - this.viewMode = mode; - } - - public getHeaderButtons() { - const toggle = new PreviewCodeToggle(); - toggle.mode = this.viewMode; - toggle.addEventListener("mode-change", (e: Event) => { - this.setViewMode((e as CustomEvent).detail); - }); - - const copyButton = new CopyButton(); - copyButton.text = this._content; - copyButton.title = i18n("Copy Markdown"); - copyButton.showText = false; - - return html` -
- ${toggle} - ${copyButton} - ${DownloadButton({ - content: this._content, - filename: this.filename, - mimeType: "text/markdown", - title: i18n("Download Markdown"), - })} -
- `; - } - - override render() { - return html` -
-
- ${ - this.viewMode === "preview" - ? html`
` - : html`
${unsafeHTML(
-									hljs.highlight(this.content, { language: "markdown", ignoreIllegals: true }).value,
-								)}
` - } -
-
- `; - } -} - -declare global { - interface HTMLElementTagNameMap { - "markdown-artifact": MarkdownArtifact; - } -} diff --git a/packages/web-ui/src/tools/artifacts/PdfArtifact.ts b/packages/web-ui/src/tools/artifacts/PdfArtifact.ts deleted file mode 100644 index ffa5efe2..00000000 --- a/packages/web-ui/src/tools/artifacts/PdfArtifact.ts +++ /dev/null @@ -1,201 +0,0 @@ -import { DownloadButton } from "@mariozechner/mini-lit/dist/DownloadButton.js"; -import { html, type TemplateResult } from "lit"; -import { customElement, property, state } from "lit/decorators.js"; -import * as pdfjsLib from "pdfjs-dist"; -import { i18n } from "../../utils/i18n.ts"; -import { ArtifactElement } from "./ArtifactElement.ts"; - -// Configure PDF.js worker -pdfjsLib.GlobalWorkerOptions.workerSrc = new URL("pdfjs-dist/build/pdf.worker.min.mjs", import.meta.url).toString(); - -@customElement("pdf-artifact") -export class PdfArtifact extends ArtifactElement { - @property({ type: String }) private _content = ""; - @state() private error: string | null = null; - private currentLoadingTask: any = null; - - get content(): string { - return this._content; - } - - set content(value: string) { - this._content = value; - this.error = null; - this.requestUpdate(); - } - - protected override createRenderRoot(): HTMLElement | DocumentFragment { - return this; - } - - override connectedCallback(): void { - super.connectedCallback(); - this.style.display = "block"; - this.style.height = "100%"; - } - - override disconnectedCallback(): void { - super.disconnectedCallback(); - this.cleanup(); - } - - private cleanup() { - if (this.currentLoadingTask) { - this.currentLoadingTask.destroy(); - this.currentLoadingTask = null; - } - } - - private base64ToArrayBuffer(base64: string): ArrayBuffer { - // Remove data URL prefix if present - let base64Data = base64; - if (base64.startsWith("data:")) { - const base64Match = base64.match(/base64,(.+)/); - if (base64Match) { - base64Data = base64Match[1]; - } - } - - const binaryString = atob(base64Data); - const bytes = new Uint8Array(binaryString.length); - for (let i = 0; i < binaryString.length; i++) { - bytes[i] = binaryString.charCodeAt(i); - } - return bytes.buffer; - } - - private decodeBase64(): Uint8Array { - let base64Data = this._content; - if (this._content.startsWith("data:")) { - const base64Match = this._content.match(/base64,(.+)/); - if (base64Match) { - base64Data = base64Match[1]; - } - } - - const binaryString = atob(base64Data); - const bytes = new Uint8Array(binaryString.length); - for (let i = 0; i < binaryString.length; i++) { - bytes[i] = binaryString.charCodeAt(i); - } - return bytes; - } - - public getHeaderButtons() { - return html` -
- ${DownloadButton({ - content: this.decodeBase64(), - filename: this.filename, - mimeType: "application/pdf", - title: i18n("Download"), - })} -
- `; - } - - override async updated(changedProperties: Map) { - super.updated(changedProperties); - - if (changedProperties.has("_content") && this._content && !this.error) { - await this.renderPdf(); - } - } - - private async renderPdf() { - const container = this.querySelector("#pdf-container"); - if (!container || !this._content) return; - - let pdf: any = null; - - try { - const arrayBuffer = this.base64ToArrayBuffer(this._content); - - // Cancel any existing loading task - if (this.currentLoadingTask) { - this.currentLoadingTask.destroy(); - } - - // Load the PDF - this.currentLoadingTask = pdfjsLib.getDocument({ data: arrayBuffer }); - pdf = await this.currentLoadingTask.promise; - this.currentLoadingTask = null; - - // Clear container - container.innerHTML = ""; - const wrapper = document.createElement("div"); - wrapper.className = "p-4"; - container.appendChild(wrapper); - - // Render all pages - for (let pageNum = 1; pageNum <= pdf.numPages; pageNum++) { - const page = await pdf.getPage(pageNum); - - const pageContainer = document.createElement("div"); - pageContainer.className = "mb-4 last:mb-0"; - - const canvas = document.createElement("canvas"); - const context = canvas.getContext("2d"); - - const viewport = page.getViewport({ scale: 1.5 }); - canvas.height = viewport.height; - canvas.width = viewport.width; - - canvas.className = "w-full max-w-full h-auto block mx-auto bg-white rounded shadow-sm border border-border"; - - if (context) { - context.fillStyle = "white"; - context.fillRect(0, 0, canvas.width, canvas.height); - } - - await page.render({ - canvasContext: context!, - viewport: viewport, - canvas: canvas, - }).promise; - - pageContainer.appendChild(canvas); - - if (pageNum < pdf.numPages) { - const separator = document.createElement("div"); - separator.className = "h-px bg-border my-4"; - pageContainer.appendChild(separator); - } - - wrapper.appendChild(pageContainer); - } - } catch (error: any) { - console.error("Error rendering PDF:", error); - this.error = error?.message || i18n("Failed to load PDF"); - } finally { - if (pdf) { - pdf.destroy(); - } - } - } - - override render(): TemplateResult { - if (this.error) { - return html` -
-
-
${i18n("Error loading PDF")}
-
${this.error}
-
-
- `; - } - - return html` -
-
-
- `; - } -} - -declare global { - interface HTMLElementTagNameMap { - "pdf-artifact": PdfArtifact; - } -} diff --git a/packages/web-ui/src/tools/artifacts/SvgArtifact.ts b/packages/web-ui/src/tools/artifacts/SvgArtifact.ts deleted file mode 100644 index 2a736597..00000000 --- a/packages/web-ui/src/tools/artifacts/SvgArtifact.ts +++ /dev/null @@ -1,119 +0,0 @@ -import { CopyButton } from "@mariozechner/mini-lit/dist/CopyButton.js"; -import { DownloadButton } from "@mariozechner/mini-lit/dist/DownloadButton.js"; -import { PreviewCodeToggle } from "@mariozechner/mini-lit/dist/PreviewCodeToggle.js"; -import hljs from "highlight.js"; -import { html } from "lit"; -import { customElement, property, state } from "lit/decorators.js"; -import { unsafeHTML } from "lit/directives/unsafe-html.js"; -import { i18n } from "../../utils/i18n.ts"; -import { ArtifactElement } from "./ArtifactElement.ts"; - -@customElement("svg-artifact") -export class SvgArtifact extends ArtifactElement { - @property() override filename = ""; - - private _content = ""; - @state() private previewUrl = ""; - - override get content(): string { - return this._content; - } - override set content(value: string) { - if (this._content === value) { - return; - } - this._content = value; - this.updatePreviewUrl(); - this.requestUpdate(); - } - - @state() private viewMode: "preview" | "code" = "preview"; - - protected override createRenderRoot(): HTMLElement | DocumentFragment { - return this; // light DOM - } - - private setViewMode(mode: "preview" | "code") { - this.viewMode = mode; - } - - private revokePreviewUrl() { - if (this.previewUrl) { - URL.revokeObjectURL(this.previewUrl); - this.previewUrl = ""; - } - } - - private updatePreviewUrl() { - this.revokePreviewUrl(); - if (!this._content) { - return; - } - this.previewUrl = URL.createObjectURL(new Blob([this._content], { type: "image/svg+xml" })); - } - - public getHeaderButtons() { - const toggle = new PreviewCodeToggle(); - toggle.mode = this.viewMode; - toggle.addEventListener("mode-change", (e: Event) => { - this.setViewMode((e as CustomEvent).detail); - }); - - const copyButton = new CopyButton(); - copyButton.text = this._content; - copyButton.title = i18n("Copy SVG"); - copyButton.showText = false; - - return html` -
- ${toggle} - ${copyButton} - ${DownloadButton({ content: this._content, filename: this.filename, mimeType: "image/svg+xml", title: i18n("Download SVG") })} -
- `; - } - - override connectedCallback() { - super.connectedCallback(); - if (this._content && !this.previewUrl) { - this.updatePreviewUrl(); - } - } - - override disconnectedCallback() { - super.disconnectedCallback(); - this.revokePreviewUrl(); - } - - override render() { - return html` -
-
- ${ - this.viewMode === "preview" - ? html`
- ${ - this.previewUrl - ? html`${this.filename}` - : "" - } -
` - : html`
${unsafeHTML(
-									hljs.highlight(this.content, { language: "xml", ignoreIllegals: true }).value,
-								)}
` - } -
-
- `; - } -} - -declare global { - interface HTMLElementTagNameMap { - "svg-artifact": SvgArtifact; - } -} diff --git a/packages/web-ui/src/tools/artifacts/TextArtifact.ts b/packages/web-ui/src/tools/artifacts/TextArtifact.ts deleted file mode 100644 index 9110e396..00000000 --- a/packages/web-ui/src/tools/artifacts/TextArtifact.ts +++ /dev/null @@ -1,148 +0,0 @@ -import { CopyButton } from "@mariozechner/mini-lit/dist/CopyButton.js"; -import { DownloadButton } from "@mariozechner/mini-lit/dist/DownloadButton.js"; -import hljs from "highlight.js"; -import { html } from "lit"; -import { customElement, property } from "lit/decorators.js"; -import { unsafeHTML } from "lit/directives/unsafe-html.js"; -import { i18n } from "../../utils/i18n.ts"; -import { ArtifactElement } from "./ArtifactElement.ts"; - -// Known code file extensions for highlighting -const CODE_EXTENSIONS = [ - "js", - "javascript", - "ts", - "typescript", - "jsx", - "tsx", - "py", - "python", - "java", - "c", - "cpp", - "cs", - "php", - "rb", - "ruby", - "go", - "rust", - "swift", - "kotlin", - "scala", - "dart", - "html", - "css", - "scss", - "sass", - "less", - "json", - "xml", - "yaml", - "yml", - "toml", - "sql", - "sh", - "bash", - "ps1", - "bat", - "r", - "matlab", - "julia", - "lua", - "perl", - "vue", - "svelte", -]; - -@customElement("text-artifact") -export class TextArtifact extends ArtifactElement { - @property() override filename = ""; - - private _content = ""; - override get content(): string { - return this._content; - } - override set content(value: string) { - this._content = value; - this.requestUpdate(); - } - - protected override createRenderRoot(): HTMLElement | DocumentFragment { - return this; // light DOM - } - - private isCode(): boolean { - const ext = this.filename.split(".").pop()?.toLowerCase() || ""; - return CODE_EXTENSIONS.includes(ext); - } - - private getLanguageFromExtension(ext: string): string { - const languageMap: Record = { - js: "javascript", - ts: "typescript", - py: "python", - rb: "ruby", - yml: "yaml", - ps1: "powershell", - bat: "batch", - }; - return languageMap[ext] || ext; - } - - private getMimeType(): string { - const ext = this.filename.split(".").pop()?.toLowerCase() || ""; - if (ext === "svg") return "image/svg+xml"; - if (ext === "md" || ext === "markdown") return "text/markdown"; - return "text/plain"; - } - - public getHeaderButtons() { - const copyButton = new CopyButton(); - copyButton.text = this.content; - copyButton.title = i18n("Copy"); - copyButton.showText = false; - - return html` -
- ${copyButton} - ${DownloadButton({ - content: this.content, - filename: this.filename, - mimeType: this.getMimeType(), - title: i18n("Download"), - })} -
- `; - } - - override render() { - const isCode = this.isCode(); - const ext = this.filename.split(".").pop() || ""; - return html` -
-
- ${ - isCode - ? html` -
${unsafeHTML(
-									hljs.highlight(this.content, {
-										language: this.getLanguageFromExtension(ext.toLowerCase()),
-										ignoreIllegals: true,
-									}).value,
-								)}
- ` - : html`
${this.content}
` - } -
-
- `; - } -} - -declare global { - interface HTMLElementTagNameMap { - "text-artifact": TextArtifact; - } -} diff --git a/packages/web-ui/src/tools/artifacts/artifacts-tool-renderer.ts b/packages/web-ui/src/tools/artifacts/artifacts-tool-renderer.ts deleted file mode 100644 index c992a79e..00000000 --- a/packages/web-ui/src/tools/artifacts/artifacts-tool-renderer.ts +++ /dev/null @@ -1,314 +0,0 @@ -import "@mariozechner/mini-lit/dist/CodeBlock.js"; -import type { ToolResultMessage } from "@earendil-works/pi-ai"; -import { createRef, ref } from "lit/directives/ref.js"; -import { FileCode2 } from "lucide"; -import "../../components/ConsoleBlock.ts"; -import { Diff } from "@mariozechner/mini-lit/dist/Diff.js"; -import { html, type TemplateResult } from "lit"; -import { i18n } from "../../utils/i18n.ts"; -import { renderCollapsibleHeader, renderHeader } from "../renderer-registry.ts"; -import type { ToolRenderer, ToolRenderResult } from "../types.ts"; -import { ArtifactPill } from "./ArtifactPill.ts"; -import type { ArtifactsPanel, ArtifactsParams } from "./artifacts.ts"; - -// Helper to extract text from content blocks -function getTextOutput(result: ToolResultMessage | undefined): string { - if (!result) return ""; - return ( - result.content - ?.filter((c) => c.type === "text") - .map((c: any) => c.text) - .join("\n") || "" - ); -} - -// Helper to determine language for syntax highlighting -function getLanguageFromFilename(filename?: string): string { - if (!filename) return "text"; - const ext = filename.split(".").pop()?.toLowerCase(); - const languageMap: Record = { - js: "javascript", - jsx: "javascript", - ts: "typescript", - tsx: "typescript", - html: "html", - css: "css", - scss: "scss", - json: "json", - py: "python", - md: "markdown", - svg: "xml", - xml: "xml", - yaml: "yaml", - yml: "yaml", - sh: "bash", - bash: "bash", - sql: "sql", - java: "java", - c: "c", - cpp: "cpp", - cs: "csharp", - go: "go", - rs: "rust", - php: "php", - rb: "ruby", - swift: "swift", - kt: "kotlin", - r: "r", - }; - return languageMap[ext || ""] || "text"; -} - -export class ArtifactsToolRenderer implements ToolRenderer { - artifactsPanel?: ArtifactsPanel; - - constructor(artifactsPanel?: ArtifactsPanel) { - this.artifactsPanel = artifactsPanel; - } - - render( - params: ArtifactsParams | undefined, - result: ToolResultMessage | undefined, - isStreaming?: boolean, - ): ToolRenderResult { - const state = result ? (result.isError ? "error" : "complete") : isStreaming ? "inprogress" : "complete"; - - // Create refs for collapsible sections - const contentRef = createRef(); - const chevronRef = createRef(); - - // Helper to get command labels - const getCommandLabels = (command: string): { streaming: string; complete: string } => { - const labels: Record = { - create: { streaming: i18n("Creating artifact"), complete: i18n("Created artifact") }, - update: { streaming: i18n("Updating artifact"), complete: i18n("Updated artifact") }, - rewrite: { streaming: i18n("Rewriting artifact"), complete: i18n("Rewrote artifact") }, - get: { streaming: i18n("Getting artifact"), complete: i18n("Got artifact") }, - delete: { streaming: i18n("Deleting artifact"), complete: i18n("Deleted artifact") }, - logs: { streaming: i18n("Getting logs"), complete: i18n("Got logs") }, - }; - return labels[command] || { streaming: i18n("Processing artifact"), complete: i18n("Processed artifact") }; - }; - - // Helper to render header text with inline artifact pill - const renderHeaderWithPill = (labelText: string, filename?: string): TemplateResult => { - if (filename) { - return html`${labelText} ${ArtifactPill(filename, this.artifactsPanel)}`; - } - return html`${labelText}`; - }; - - // Error handling - if (result?.isError) { - const command = params?.command; - const filename = params?.filename; - const labels = command - ? getCommandLabels(command) - : { streaming: i18n("Processing artifact"), complete: i18n("Processed artifact") }; - const headerText = labels.streaming; - - // For create/update/rewrite errors, show code block + console/error - if (command === "create" || command === "update" || command === "rewrite") { - const content = params?.content || ""; - const { old_str, new_str } = params || {}; - const isDiff = command === "update"; - const diffContent = - old_str !== undefined && new_str !== undefined ? Diff({ oldText: old_str, newText: new_str }) : ""; - - const isHtml = filename?.endsWith(".html"); - - return { - content: html` -
- ${renderCollapsibleHeader(state, FileCode2, renderHeaderWithPill(headerText, filename), contentRef, chevronRef, false)} -
- ${isDiff ? diffContent : content ? html`` : ""} - ${ - isHtml - ? html`` - : html`
${getTextOutput(result) || i18n("An error occurred")}
` - } -
-
- `, - isCustom: false, - }; - } - - // For other errors, just show error message - return { - content: html` -
- ${renderHeader(state, FileCode2, headerText)} -
${getTextOutput(result) || i18n("An error occurred")}
-
- `, - isCustom: false, - }; - } - - // Full params + result - if (result && params) { - const { command, filename, content } = params; - const labels = command - ? getCommandLabels(command) - : { streaming: i18n("Processing artifact"), complete: i18n("Processed artifact") }; - const headerText = labels.complete; - - // GET command: show code block with file content - if (command === "get") { - const fileContent = getTextOutput(result) || i18n("(no output)"); - return { - content: html` -
- ${renderCollapsibleHeader(state, FileCode2, renderHeaderWithPill(headerText, filename), contentRef, chevronRef, false)} -
- -
-
- `, - isCustom: false, - }; - } - - // LOGS command: show console block - if (command === "logs") { - const logs = getTextOutput(result) || i18n("(no output)"); - return { - content: html` -
- ${renderCollapsibleHeader(state, FileCode2, renderHeaderWithPill(headerText, filename), contentRef, chevronRef, false)} -
- -
-
- `, - isCustom: false, - }; - } - - // CREATE/UPDATE/REWRITE: always show code block, + console block for .html files - if (command === "create" || command === "rewrite") { - const codeContent = content || ""; - const isHtml = filename?.endsWith(".html"); - const logs = getTextOutput(result) || ""; - - return { - content: html` -
- ${renderCollapsibleHeader(state, FileCode2, renderHeaderWithPill(headerText, filename), contentRef, chevronRef, false)} -
- ${codeContent ? html`` : ""} - ${isHtml && logs ? html`` : ""} -
-
- `, - isCustom: false, - }; - } - - if (command === "update") { - const isHtml = filename?.endsWith(".html"); - const logs = getTextOutput(result) || ""; - return { - content: html` -
- ${renderCollapsibleHeader(state, FileCode2, renderHeaderWithPill(headerText, filename), contentRef, chevronRef, false)} -
- ${Diff({ oldText: params.old_str || "", newText: params.new_str || "" })} - ${isHtml && logs ? html`` : ""} -
-
- `, - isCustom: false, - }; - } - - // For DELETE, just show header - return { - content: html` -
- ${renderHeader(state, FileCode2, renderHeaderWithPill(headerText, filename))} -
- `, - isCustom: false, - }; - } - - // Params only (streaming or waiting for result) - if (params) { - const { command, filename, content, old_str, new_str } = params; - - // If no command yet - if (!command) { - return { content: renderHeader(state, FileCode2, i18n("Preparing artifact...")), isCustom: false }; - } - - const labels = getCommandLabels(command); - const headerText = labels.streaming; - - // Render based on command type - switch (command) { - case "create": - case "rewrite": - return { - content: html` -
- ${renderCollapsibleHeader(state, FileCode2, renderHeaderWithPill(headerText, filename), contentRef, chevronRef, false)} -
- ${ - content - ? html`` - : "" - } -
-
- `, - isCustom: false, - }; - - case "update": - return { - content: html` -
- ${renderCollapsibleHeader(state, FileCode2, renderHeaderWithPill(headerText, filename), contentRef, chevronRef, false)} -
- ${ - old_str !== undefined && new_str !== undefined - ? Diff({ oldText: old_str, newText: new_str }) - : "" - } -
-
- `, - isCustom: false, - }; - - case "get": - case "logs": - return { - content: html` -
- ${renderCollapsibleHeader(state, FileCode2, renderHeaderWithPill(headerText, filename), contentRef, chevronRef, false)} -
-
- `, - isCustom: false, - }; - - default: - return { - content: html` -
- ${renderHeader(state, FileCode2, renderHeaderWithPill(headerText, filename))} -
- `, - isCustom: false, - }; - } - } - - // No params or result yet - return { content: renderHeader(state, FileCode2, i18n("Preparing artifact...")), isCustom: false }; - } -} diff --git a/packages/web-ui/src/tools/artifacts/artifacts.ts b/packages/web-ui/src/tools/artifacts/artifacts.ts deleted file mode 100644 index 1f74a5fb..00000000 --- a/packages/web-ui/src/tools/artifacts/artifacts.ts +++ /dev/null @@ -1,713 +0,0 @@ -import { icon } from "@mariozechner/mini-lit"; -import "@mariozechner/mini-lit/dist/MarkdownBlock.js"; -import type { Agent, AgentMessage, AgentTool } from "@earendil-works/pi-agent-core"; -import { StringEnum, type ToolCall } from "@earendil-works/pi-ai"; -import { Button } from "@mariozechner/mini-lit/dist/Button.js"; -import { html, LitElement, type TemplateResult } from "lit"; -import { customElement, property, state } from "lit/decorators.js"; -import { createRef, type Ref, ref } from "lit/directives/ref.js"; -import { X } from "lucide"; -import { type Static, Type } from "typebox"; -import type { ArtifactMessage } from "../../components/Messages.ts"; -import { ArtifactsRuntimeProvider } from "../../components/sandbox/ArtifactsRuntimeProvider.ts"; -import { AttachmentsRuntimeProvider } from "../../components/sandbox/AttachmentsRuntimeProvider.ts"; -import type { SandboxRuntimeProvider } from "../../components/sandbox/SandboxRuntimeProvider.ts"; -import { - ARTIFACTS_RUNTIME_PROVIDER_DESCRIPTION_RO, - ARTIFACTS_TOOL_DESCRIPTION, - ATTACHMENTS_RUNTIME_DESCRIPTION, -} from "../../prompts/prompts.ts"; -import type { Attachment } from "../../utils/attachment-utils.ts"; -import { i18n } from "../../utils/i18n.ts"; -import type { ArtifactElement } from "./ArtifactElement.ts"; -import { DocxArtifact } from "./DocxArtifact.ts"; -import { ExcelArtifact } from "./ExcelArtifact.ts"; -import { GenericArtifact } from "./GenericArtifact.ts"; -import { HtmlArtifact } from "./HtmlArtifact.ts"; -import { ImageArtifact } from "./ImageArtifact.ts"; -import { MarkdownArtifact } from "./MarkdownArtifact.ts"; -import { PdfArtifact } from "./PdfArtifact.ts"; -import { SvgArtifact } from "./SvgArtifact.ts"; -import { TextArtifact } from "./TextArtifact.ts"; - -// Simple artifact model -export interface Artifact { - filename: string; - content: string; - createdAt: Date; - updatedAt: Date; -} - -// JSON-schema friendly parameters object (LLM-facing) -const artifactsParamsSchema = Type.Object({ - command: StringEnum(["create", "update", "rewrite", "get", "delete", "logs"], { - description: "The operation to perform", - }), - filename: Type.String({ description: "Filename including extension (e.g., 'index.html', 'script.js')" }), - content: Type.Optional(Type.String({ description: "File content" })), - old_str: Type.Optional(Type.String({ description: "String to replace (for update command)" })), - new_str: Type.Optional(Type.String({ description: "Replacement string (for update command)" })), -}); -export type ArtifactsParams = Static; - -@customElement("artifacts-panel") -export class ArtifactsPanel extends LitElement { - @state() private _artifacts = new Map(); - @state() private _activeFilename: string | null = null; - - // Programmatically managed artifact elements - private artifactElements = new Map(); - private contentRef: Ref = createRef(); - - // Agent reference (needed to get attachments for HTML artifacts) - @property({ attribute: false }) agent?: Agent; - // Sandbox URL provider for browser extensions (optional) - @property({ attribute: false }) sandboxUrlProvider?: () => string; - // Callbacks - @property({ attribute: false }) onArtifactsChange?: () => void; - @property({ attribute: false }) onClose?: () => void; - @property({ attribute: false }) onOpen?: () => void; - // Collapsed mode: hides panel content but can show a floating reopen pill - @property({ type: Boolean }) collapsed = false; - // Overlay mode: when true, panel renders full-screen overlay (mobile) - @property({ type: Boolean }) overlay = false; - - // Public getter for artifacts - get artifacts() { - return this._artifacts; - } - - // Get runtime providers for HTML artifacts (read-only: attachments + artifacts) - private getHtmlArtifactRuntimeProviders(): SandboxRuntimeProvider[] { - const providers: SandboxRuntimeProvider[] = []; - - // Get attachments from agent messages - if (this.agent) { - const attachments: Attachment[] = []; - for (const message of this.agent.state.messages) { - if (message.role === "user-with-attachments" && message.attachments) { - attachments.push(...message.attachments); - } - } - if (attachments.length > 0) { - providers.push(new AttachmentsRuntimeProvider(attachments)); - } - } - - // Add read-only artifacts provider - providers.push(new ArtifactsRuntimeProvider(this, this.agent, false)); - - return providers; - } - - protected override createRenderRoot(): HTMLElement | DocumentFragment { - return this; // light DOM for shared styles - } - - override connectedCallback(): void { - super.connectedCallback(); - this.style.display = "block"; - this.style.height = "100%"; - // Reattach existing artifact elements when panel is re-inserted into the DOM - requestAnimationFrame(() => { - const container = this.contentRef.value; - if (!container) return; - // Ensure we have an active filename - if (!this._activeFilename && this._artifacts.size > 0) { - this._activeFilename = Array.from(this._artifacts.keys())[0]; - } - this.artifactElements.forEach((element, name) => { - if (!element.parentElement) container.appendChild(element); - element.style.display = name === this._activeFilename ? "block" : "none"; - }); - }); - } - - override disconnectedCallback() { - super.disconnectedCallback(); - // Do not tear down artifact elements; keep them to restore on next mount - } - - // Helper to determine file type from extension - private getFileType( - filename: string, - ): "html" | "svg" | "markdown" | "image" | "pdf" | "excel" | "docx" | "text" | "generic" { - const ext = filename.split(".").pop()?.toLowerCase(); - if (ext === "html") return "html"; - if (ext === "svg") return "svg"; - if (ext === "md" || ext === "markdown") return "markdown"; - if (ext === "pdf") return "pdf"; - if (ext === "xlsx" || ext === "xls") return "excel"; - if (ext === "docx") return "docx"; - if ( - ext === "png" || - ext === "jpg" || - ext === "jpeg" || - ext === "gif" || - ext === "webp" || - ext === "bmp" || - ext === "ico" - ) - return "image"; - // Text files - if ( - ext === "txt" || - ext === "json" || - ext === "xml" || - ext === "yaml" || - ext === "yml" || - ext === "csv" || - ext === "js" || - ext === "ts" || - ext === "jsx" || - ext === "tsx" || - ext === "py" || - ext === "java" || - ext === "c" || - ext === "cpp" || - ext === "h" || - ext === "css" || - ext === "scss" || - ext === "sass" || - ext === "less" || - ext === "sh" - ) - return "text"; - // Everything else gets generic fallback - return "generic"; - } - - // Get or create artifact element - private getOrCreateArtifactElement(filename: string, content: string): ArtifactElement { - let element = this.artifactElements.get(filename); - - if (!element) { - const type = this.getFileType(filename); - if (type === "html") { - element = new HtmlArtifact(); - (element as HtmlArtifact).runtimeProviders = this.getHtmlArtifactRuntimeProviders(); - if (this.sandboxUrlProvider) { - (element as HtmlArtifact).sandboxUrlProvider = this.sandboxUrlProvider; - } - } else if (type === "svg") { - element = new SvgArtifact(); - } else if (type === "markdown") { - element = new MarkdownArtifact(); - } else if (type === "image") { - element = new ImageArtifact(); - } else if (type === "pdf") { - element = new PdfArtifact(); - } else if (type === "excel") { - element = new ExcelArtifact(); - } else if (type === "docx") { - element = new DocxArtifact(); - } else if (type === "text") { - element = new TextArtifact(); - } else { - element = new GenericArtifact(); - } - element.filename = filename; - element.content = content; - element.style.display = "none"; - element.style.height = "100%"; - - // Store element - this.artifactElements.set(filename, element); - - // Add to DOM - try immediately if container exists, otherwise schedule - const newElement = element; - if (this.contentRef.value) { - this.contentRef.value.appendChild(newElement); - } else { - requestAnimationFrame(() => { - if (this.contentRef.value && !newElement.parentElement) { - this.contentRef.value.appendChild(newElement); - } - }); - } - } else { - // Just update content - element.content = content; - if (element instanceof HtmlArtifact) { - element.runtimeProviders = this.getHtmlArtifactRuntimeProviders(); - } - } - - return element; - } - - // Show/hide artifact elements - private showArtifact(filename: string) { - // Ensure the active element is in the DOM - requestAnimationFrame(() => { - this.artifactElements.forEach((element, name) => { - if (this.contentRef.value && !element.parentElement) { - this.contentRef.value.appendChild(element); - } - element.style.display = name === filename ? "block" : "none"; - }); - }); - this._activeFilename = filename; - this.requestUpdate(); // Only for tab bar update - - // Scroll the active tab into view after render - requestAnimationFrame(() => { - const activeButton = this.querySelector(`button[data-filename="${filename}"]`); - if (activeButton) { - activeButton.scrollIntoView({ behavior: "smooth", block: "nearest", inline: "center" }); - } - }); - } - - // Open panel and focus an artifact tab by filename - public openArtifact(filename: string) { - if (this._artifacts.has(filename)) { - this.showArtifact(filename); - // Ask host to open panel (AgentInterface demo listens to onOpen) - this.onOpen?.(); - } - } - - // Build the AgentTool (no details payload; return only output strings) - public get tool(): AgentTool { - return { - label: "Artifacts", - name: "artifacts", - get description() { - // HTML artifacts have read-only access to attachments and artifacts - const runtimeProviderDescriptions = [ - ATTACHMENTS_RUNTIME_DESCRIPTION, - ARTIFACTS_RUNTIME_PROVIDER_DESCRIPTION_RO, - ]; - return ARTIFACTS_TOOL_DESCRIPTION(runtimeProviderDescriptions); - }, - parameters: artifactsParamsSchema, - // Execute mutates our local store and returns a plain output - execute: async (_toolCallId: string, args: Static, _signal?: AbortSignal) => { - const output = await this.executeCommand(args); - return { content: [{ type: "text", text: output }], details: undefined }; - }, - }; - } - - // Re-apply artifacts by scanning a message list (optional utility) - public async reconstructFromMessages( - messages: Array, - ): Promise { - const toolCalls = new Map(); - const artifactToolName = "artifacts"; - - // 1) Collect tool calls from assistant messages - for (const message of messages) { - if (message.role === "assistant") { - for (const block of message.content) { - if (block.type === "toolCall" && block.name === artifactToolName) { - toolCalls.set(block.id, block); - } - } - } - } - - // 2) Build an ordered list of successful artifact operations - const operations: Array = []; - for (const m of messages) { - if ((m as any).role === "artifact") { - const artifactMsg = m as ArtifactMessage; - switch (artifactMsg.action) { - case "create": - operations.push({ - command: "create", - filename: artifactMsg.filename, - content: artifactMsg.content, - }); - break; - case "update": - operations.push({ - command: "rewrite", - filename: artifactMsg.filename, - content: artifactMsg.content, - }); - break; - case "delete": - operations.push({ - command: "delete", - filename: artifactMsg.filename, - }); - break; - } - } - // Handle tool result messages (from artifacts tool calls) - else if ((m as any).role === "toolResult" && (m as any).toolName === artifactToolName && !(m as any).isError) { - const toolCallId = (m as any).toolCallId as string; - const call = toolCalls.get(toolCallId); - if (!call) continue; - const params = call.arguments as ArtifactsParams; - if (params.command === "get" || params.command === "logs") continue; // no state change - operations.push(params); - } - } - - // 3) Compute final state per filename by simulating operations in-memory - const finalArtifacts = new Map(); - for (const op of operations) { - const filename = op.filename; - switch (op.command) { - case "create": { - if (op.content) { - finalArtifacts.set(filename, op.content); - } - break; - } - case "rewrite": { - if (op.content) { - finalArtifacts.set(filename, op.content); - } - break; - } - case "update": { - let existing = finalArtifacts.get(filename); - if (!existing) break; // skip invalid update (shouldn't happen for successful results) - if (op.old_str !== undefined && op.new_str !== undefined) { - existing = existing.replace(op.old_str, op.new_str); - finalArtifacts.set(filename, existing); - } - break; - } - case "delete": { - finalArtifacts.delete(filename); - break; - } - case "get": - case "logs": - // Ignored above, just for completeness - break; - } - } - - // 4) Reset current UI state before bulk create - this._artifacts.clear(); - this.artifactElements.forEach((el) => { - el.remove(); - }); - this.artifactElements.clear(); - this._activeFilename = null; - this._artifacts = new Map(this._artifacts); - - // 5) Create artifacts in a single pass without waiting for iframe execution or tab switching - for (const [filename, content] of finalArtifacts.entries()) { - const createParams: ArtifactsParams = { command: "create", filename, content } as const; - try { - await this.createArtifact(createParams, { skipWait: true, silent: true }); - } catch { - // Ignore failures during reconstruction - } - } - - // 6) Show first artifact if any exist, and notify listeners once - if (!this._activeFilename && this._artifacts.size > 0) { - this.showArtifact(Array.from(this._artifacts.keys())[0]); - } - this.onArtifactsChange?.(); - this.requestUpdate(); - } - - // Core command executor - private async executeCommand( - params: ArtifactsParams, - options: { skipWait?: boolean; silent?: boolean } = {}, - ): Promise { - switch (params.command) { - case "create": - return await this.createArtifact(params, options); - case "update": - return await this.updateArtifact(params, options); - case "rewrite": - return await this.rewriteArtifact(params, options); - case "get": - return this.getArtifact(params); - case "delete": - return this.deleteArtifact(params); - case "logs": - return this.getLogs(params); - default: - // Should never happen with TypeBox validation - return `Error: Unknown command ${(params as any).command}`; - } - } - - // Wait for HTML artifact execution and get logs - private async waitForHtmlExecution(filename: string): Promise { - const element = this.artifactElements.get(filename); - if (!(element instanceof HtmlArtifact)) { - return ""; - } - - return new Promise((resolve) => { - // Fallback timeout - just get logs after execution should complete - setTimeout(() => { - // Get whatever logs we have - const logs = element.getLogs(); - resolve(logs); - }, 1500); - }); - } - - // Reload all HTML artifacts (called when any artifact changes) - private reloadAllHtmlArtifacts() { - this.artifactElements.forEach((element) => { - if (element instanceof HtmlArtifact && element.sandboxIframeRef.value) { - // Update runtime providers with latest artifact state - element.runtimeProviders = this.getHtmlArtifactRuntimeProviders(); - // Re-execute the HTML content - element.executeContent(element.content); - } - }); - } - - private async createArtifact( - params: ArtifactsParams, - options: { skipWait?: boolean; silent?: boolean } = {}, - ): Promise { - if (!params.filename || !params.content) { - return "Error: create command requires filename and content"; - } - if (this._artifacts.has(params.filename)) { - return `Error: File ${params.filename} already exists`; - } - - const artifact: Artifact = { - filename: params.filename, - content: params.content, - createdAt: new Date(), - updatedAt: new Date(), - }; - this._artifacts.set(params.filename, artifact); - this._artifacts = new Map(this._artifacts); - - // Create or update element - this.getOrCreateArtifactElement(params.filename, params.content); - if (!options.silent) { - this.showArtifact(params.filename); - this.onArtifactsChange?.(); - this.requestUpdate(); - } - - // Reload all HTML artifacts since they might depend on this new artifact - this.reloadAllHtmlArtifacts(); - - // For HTML files, wait for execution - let result = `Created file ${params.filename}`; - if (this.getFileType(params.filename) === "html" && !options.skipWait) { - const logs = await this.waitForHtmlExecution(params.filename); - result += `\n${logs}`; - } - - return result; - } - - private async updateArtifact( - params: ArtifactsParams, - options: { skipWait?: boolean; silent?: boolean } = {}, - ): Promise { - const artifact = this._artifacts.get(params.filename); - if (!artifact) { - const files = Array.from(this._artifacts.keys()); - if (files.length === 0) return `Error: File ${params.filename} not found. No files have been created yet.`; - return `Error: File ${params.filename} not found. Available files: ${files.join(", ")}`; - } - if (!params.old_str || params.new_str === undefined) { - return "Error: update command requires old_str and new_str"; - } - if (!artifact.content.includes(params.old_str)) { - return `Error: String not found in file. Here is the full content:\n\n${artifact.content}`; - } - - artifact.content = artifact.content.replace(params.old_str, params.new_str); - artifact.updatedAt = new Date(); - this._artifacts.set(params.filename, artifact); - - // Update element - this.getOrCreateArtifactElement(params.filename, artifact.content); - if (!options.silent) { - this.onArtifactsChange?.(); - this.requestUpdate(); - } - - // Show the artifact - this.showArtifact(params.filename); - - // Reload all HTML artifacts since they might depend on this updated artifact - this.reloadAllHtmlArtifacts(); - - // For HTML files, wait for execution - let result = `Updated file ${params.filename}`; - if (this.getFileType(params.filename) === "html" && !options.skipWait) { - const logs = await this.waitForHtmlExecution(params.filename); - result += `\n${logs}`; - } - - return result; - } - - private async rewriteArtifact( - params: ArtifactsParams, - options: { skipWait?: boolean; silent?: boolean } = {}, - ): Promise { - const artifact = this._artifacts.get(params.filename); - if (!artifact) { - const files = Array.from(this._artifacts.keys()); - if (files.length === 0) return `Error: File ${params.filename} not found. No files have been created yet.`; - return `Error: File ${params.filename} not found. Available files: ${files.join(", ")}`; - } - if (!params.content) { - return "Error: rewrite command requires content"; - } - - artifact.content = params.content; - artifact.updatedAt = new Date(); - this._artifacts.set(params.filename, artifact); - - // Update element - this.getOrCreateArtifactElement(params.filename, artifact.content); - if (!options.silent) { - this.onArtifactsChange?.(); - } - - // Show the artifact - this.showArtifact(params.filename); - - // Reload all HTML artifacts since they might depend on this rewritten artifact - this.reloadAllHtmlArtifacts(); - - // For HTML files, wait for execution - let result = ""; - if (this.getFileType(params.filename) === "html" && !options.skipWait) { - const logs = await this.waitForHtmlExecution(params.filename); - result += `\n${logs}`; - } - - return result; - } - - private getArtifact(params: ArtifactsParams): string { - const artifact = this._artifacts.get(params.filename); - if (!artifact) { - const files = Array.from(this._artifacts.keys()); - if (files.length === 0) return `Error: File ${params.filename} not found. No files have been created yet.`; - return `Error: File ${params.filename} not found. Available files: ${files.join(", ")}`; - } - return artifact.content; - } - - private deleteArtifact(params: ArtifactsParams): string { - const artifact = this._artifacts.get(params.filename); - if (!artifact) { - const files = Array.from(this._artifacts.keys()); - if (files.length === 0) return `Error: File ${params.filename} not found. No files have been created yet.`; - return `Error: File ${params.filename} not found. Available files: ${files.join(", ")}`; - } - - this._artifacts.delete(params.filename); - this._artifacts = new Map(this._artifacts); - - // Remove element - const element = this.artifactElements.get(params.filename); - if (element) { - element.remove(); - this.artifactElements.delete(params.filename); - } - - // Show another artifact if this was active - if (this._activeFilename === params.filename) { - const remaining = Array.from(this._artifacts.keys()); - if (remaining.length > 0) { - this.showArtifact(remaining[0]); - } else { - this._activeFilename = null; - this.requestUpdate(); - } - } - this.onArtifactsChange?.(); - this.requestUpdate(); - - // Reload all HTML artifacts since they might have depended on this deleted artifact - this.reloadAllHtmlArtifacts(); - - return `Deleted file ${params.filename}`; - } - - private getLogs(params: ArtifactsParams): string { - const element = this.artifactElements.get(params.filename); - if (!element) { - const files = Array.from(this._artifacts.keys()); - if (files.length === 0) return `Error: File ${params.filename} not found. No files have been created yet.`; - return `Error: File ${params.filename} not found. Available files: ${files.join(", ")}`; - } - - if (!(element instanceof HtmlArtifact)) { - return `Error: File ${params.filename} is not an HTML file. Logs are only available for HTML files.`; - } - - return element.getLogs(); - } - - override render(): TemplateResult { - const artifacts = Array.from(this._artifacts.values()); - - // Panel is hidden when collapsed OR when there are no artifacts - const showPanel = artifacts.length > 0 && !this.collapsed; - - return html` -
- -
-
- ${artifacts.map((a) => { - const isActive = a.filename === this._activeFilename; - const activeClass = isActive - ? "border-primary text-primary" - : "border-transparent text-muted-foreground hover:text-foreground"; - return html` - - `; - })} -
-
- ${(() => { - const active = this._activeFilename ? this.artifactElements.get(this._activeFilename) : undefined; - return active ? active.getHeaderButtons() : ""; - })()} - ${Button({ - variant: "ghost", - size: "sm", - onClick: () => this.onClose?.(), - title: i18n("Close artifacts"), - children: icon(X, "sm"), - })} -
-
- - -
-
- `; - } -} - -declare global { - interface HTMLElementTagNameMap { - "artifacts-panel": ArtifactsPanel; - } -} diff --git a/packages/web-ui/src/tools/artifacts/index.ts b/packages/web-ui/src/tools/artifacts/index.ts deleted file mode 100644 index 685022a6..00000000 --- a/packages/web-ui/src/tools/artifacts/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -export { ArtifactElement } from "./ArtifactElement.ts"; -export { type Artifact, ArtifactsPanel, type ArtifactsParams } from "./artifacts.ts"; -export { ArtifactsToolRenderer } from "./artifacts-tool-renderer.ts"; -export { HtmlArtifact } from "./HtmlArtifact.ts"; -export { MarkdownArtifact } from "./MarkdownArtifact.ts"; -export { SvgArtifact } from "./SvgArtifact.ts"; -export { TextArtifact } from "./TextArtifact.ts"; diff --git a/packages/web-ui/src/tools/extract-document.ts b/packages/web-ui/src/tools/extract-document.ts deleted file mode 100644 index a710fee9..00000000 --- a/packages/web-ui/src/tools/extract-document.ts +++ /dev/null @@ -1,275 +0,0 @@ -import type { AgentTool } from "@earendil-works/pi-agent-core"; -import type { ToolResultMessage } from "@earendil-works/pi-ai"; -import { html } from "lit"; -import { createRef, ref } from "lit/directives/ref.js"; -import { FileText } from "lucide"; -import { type Static, Type } from "typebox"; -import { EXTRACT_DOCUMENT_DESCRIPTION } from "../prompts/prompts.ts"; -import { loadAttachment } from "../utils/attachment-utils.ts"; -import { isCorsError } from "../utils/proxy-utils.ts"; -import { registerToolRenderer, renderCollapsibleHeader, renderHeader } from "./renderer-registry.ts"; -import type { ToolRenderer, ToolRenderResult } from "./types.ts"; - -// ============================================================================ -// TYPES -// ============================================================================ - -const extractDocumentSchema = Type.Object({ - url: Type.String({ - description: "URL of the document to extract text from (PDF, DOCX, XLSX, or PPTX)", - }), -}); - -export type ExtractDocumentParams = Static; - -export interface ExtractDocumentResult { - extractedText: string; - format: string; - fileName: string; - size: number; -} - -// ============================================================================ -// TOOL -// ============================================================================ - -export function createExtractDocumentTool(): AgentTool & { - corsProxyUrl?: string; -} { - const tool = { - label: "Extract Document", - name: "extract_document", - corsProxyUrl: undefined as string | undefined, // Can be set by consumer (e.g., from user settings) - description: EXTRACT_DOCUMENT_DESCRIPTION, - parameters: extractDocumentSchema, - execute: async (_toolCallId: string, args: ExtractDocumentParams, signal?: AbortSignal) => { - if (signal?.aborted) { - throw new Error("Extract document aborted"); - } - - const url = args.url.trim(); - if (!url) { - throw new Error("URL is required"); - } - - // Validate URL format - try { - new URL(url); - } catch { - throw new Error(`Invalid URL: ${url}`); - } - - // Size limit: 50MB - const MAX_SIZE = 50 * 1024 * 1024; - - // Helper function to fetch and process document - const fetchAndProcess = async (fetchUrl: string) => { - const response = await fetch(fetchUrl, { signal }); - - if (!response.ok) { - throw new Error( - `TELL USER: Unable to download the document (${response.status} ${response.statusText}). The site likely blocks automated downloads.\n\n` + - `INSTRUCT USER: Please download the file manually and attach it to your message using the attachment button (paperclip icon) in the message input area. I can then extract the text from the attached file.`, - ); - } - - // Check size before downloading - const contentLength = response.headers.get("content-length"); - if (contentLength) { - const size = Number.parseInt(contentLength, 10); - if (size > MAX_SIZE) { - throw new Error( - `Document is too large (${(size / 1024 / 1024).toFixed(1)}MB). Maximum supported size is 50MB.`, - ); - } - } - - // Download the document - const arrayBuffer = await response.arrayBuffer(); - const size = arrayBuffer.byteLength; - - if (size > MAX_SIZE) { - throw new Error( - `Document is too large (${(size / 1024 / 1024).toFixed(1)}MB). Maximum supported size is 50MB.`, - ); - } - - return arrayBuffer; - }; - - // Try without proxy first, fallback to proxy on CORS error - let arrayBuffer: ArrayBuffer; - - try { - // Attempt direct fetch first - arrayBuffer = await fetchAndProcess(url); - } catch (directError: any) { - // If CORS error and proxy is available, retry with proxy - if (isCorsError(directError) && tool.corsProxyUrl) { - try { - const proxiedUrl = tool.corsProxyUrl + encodeURIComponent(url); - arrayBuffer = await fetchAndProcess(proxiedUrl); - } catch (proxyError: any) { - // Proxy fetch also failed - throw helpful message - throw new Error( - `TELL USER: Unable to fetch the document due to CORS restrictions.\n\n` + - `Tried with proxy but it also failed: ${proxyError.message}\n\n` + - `INSTRUCT USER: Please download the file manually and attach it to your message using the attachment button (paperclip icon) in the message input area. I can then extract the text from the attached file.`, - ); - } - } else if (isCorsError(directError) && !tool.corsProxyUrl) { - // CORS error but no proxy configured - throw new Error( - `TELL USER: Unable to fetch the document due to CORS restrictions (the server blocks requests from browser extensions).\n\n` + - `To fix this, you need to configure a CORS proxy in Sitegeist settings:\n` + - `1. Open Sitegeist settings\n` + - `2. Find "CORS Proxy URL" setting\n` + - `3. Enter a proxy URL like: https://corsproxy.io/?\n` + - `4. Save and try again\n\n` + - `Alternatively, download the file manually and attach it to your message using the attachment button (paperclip icon).`, - ); - } else { - // Not a CORS error - re-throw - throw directError; - } - } - - // Extract filename from URL - const urlParts = url.split("/"); - let fileName = urlParts[urlParts.length - 1]?.split("?")[0] || "document"; - if (url.startsWith("https://arxiv.org/")) { - fileName = `${fileName}.pdf`; - } - - // Use loadAttachment to process the document - const attachment = await loadAttachment(arrayBuffer, fileName); - - if (!attachment.extractedText) { - throw new Error( - `Document format not supported. Supported formats:\n` + - `- PDF (.pdf)\n` + - `- Word (.docx)\n` + - `- Excel (.xlsx, .xls)\n` + - `- PowerPoint (.pptx)`, - ); - } - - // Determine format from attachment - let format = "unknown"; - if (attachment.mimeType.includes("pdf")) { - format = "pdf"; - } else if (attachment.mimeType.includes("wordprocessingml")) { - format = "docx"; - } else if (attachment.mimeType.includes("spreadsheetml") || attachment.mimeType.includes("ms-excel")) { - format = "xlsx"; - } else if (attachment.mimeType.includes("presentationml")) { - format = "pptx"; - } - - return { - content: [{ type: "text" as const, text: attachment.extractedText }], - details: { - extractedText: attachment.extractedText, - format, - fileName: attachment.fileName, - size: attachment.size, - }, - }; - }, - }; - return tool; -} - -// Export a default instance -export const extractDocumentTool = createExtractDocumentTool(); - -// ============================================================================ -// RENDERER -// ============================================================================ - -export const extractDocumentRenderer: ToolRenderer = { - render( - params: ExtractDocumentParams | undefined, - result: ToolResultMessage | undefined, - isStreaming?: boolean, - ): ToolRenderResult { - // Determine status - const state = result ? (result.isError ? "error" : "complete") : isStreaming ? "inprogress" : "complete"; - - // Create refs for collapsible sections - const contentRef = createRef(); - const chevronRef = createRef(); - - // With result: show params + result - if (result && params) { - const details = result.details; - const title = details - ? result.isError - ? `Failed to extract ${details.fileName || "document"}` - : `Extracted text from ${details.fileName} (${details.format.toUpperCase()}, ${(details.size / 1024).toFixed(1)}KB)` - : result.isError - ? "Failed to extract document" - : "Extracted text from document"; - - const output = - result.content - ?.filter((c) => c.type === "text") - .map((c: any) => c.text) - .join("\n") || ""; - - return { - content: html` -
- ${renderCollapsibleHeader(state, FileText, title, contentRef, chevronRef, false)} -
- ${ - params.url - ? html`
- URL: ${params.url} -
` - : "" - } - ${ - output && !result.isError - ? html`` - : "" - } - ${ - result.isError && output - ? html`` - : "" - } -
-
- `, - isCustom: false, - }; - } - - // Just params (streaming or waiting for result) - if (params) { - const title = "Extracting document..."; - - return { - content: html` -
- ${renderCollapsibleHeader(state, FileText, title, contentRef, chevronRef, false)} -
-
URL: ${params.url}
-
-
- `, - isCustom: false, - }; - } - - // No params or result yet - return { - content: renderHeader(state, FileText, "Preparing extraction..."), - isCustom: false, - }; - }, -}; - -// Auto-register the renderer -registerToolRenderer("extract_document", extractDocumentRenderer); diff --git a/packages/web-ui/src/tools/index.ts b/packages/web-ui/src/tools/index.ts deleted file mode 100644 index 4be3e1ea..00000000 --- a/packages/web-ui/src/tools/index.ts +++ /dev/null @@ -1,46 +0,0 @@ -import type { ToolResultMessage } from "@earendil-works/pi-ai"; -import "./javascript-repl.ts"; // Auto-registers the renderer -import "./extract-document.ts"; // Auto-registers the renderer -import { getToolRenderer, registerToolRenderer } from "./renderer-registry.ts"; -import { BashRenderer } from "./renderers/BashRenderer.ts"; -import { DefaultRenderer } from "./renderers/DefaultRenderer.ts"; -import type { ToolRenderResult } from "./types.ts"; - -// Register all built-in tool renderers -registerToolRenderer("bash", new BashRenderer()); - -const defaultRenderer = new DefaultRenderer(); - -// Global flag to force default JSON rendering for all tools -let showJsonMode = false; - -/** - * Enable or disable show JSON mode - * When enabled, all tool renderers will use the default JSON renderer - */ -export function setShowJsonMode(enabled: boolean): void { - showJsonMode = enabled; -} - -/** - * Render tool - unified function that handles params, result, and streaming state - */ -export function renderTool( - toolName: string, - params: any | undefined, - result: ToolResultMessage | undefined, - isStreaming?: boolean, -): ToolRenderResult { - // If showJsonMode is enabled, always use the default renderer - if (showJsonMode) { - return defaultRenderer.render(params, result, isStreaming); - } - - const renderer = getToolRenderer(toolName); - if (renderer) { - return renderer.render(params, result, isStreaming); - } - return defaultRenderer.render(params, result, isStreaming); -} - -export { getToolRenderer, registerToolRenderer }; diff --git a/packages/web-ui/src/tools/javascript-repl.ts b/packages/web-ui/src/tools/javascript-repl.ts deleted file mode 100644 index 612198b2..00000000 --- a/packages/web-ui/src/tools/javascript-repl.ts +++ /dev/null @@ -1,293 +0,0 @@ -import type { AgentTool } from "@earendil-works/pi-agent-core"; -import type { ToolResultMessage } from "@earendil-works/pi-ai"; -import { i18n } from "@mariozechner/mini-lit"; -import { html } from "lit"; -import { createRef, ref } from "lit/directives/ref.js"; -import { Code } from "lucide"; -import { type Static, Type } from "typebox"; -import { type SandboxFile, SandboxIframe, type SandboxResult } from "../components/SandboxedIframe.ts"; -import type { SandboxRuntimeProvider } from "../components/sandbox/SandboxRuntimeProvider.ts"; -import { JAVASCRIPT_REPL_TOOL_DESCRIPTION } from "../prompts/prompts.ts"; -import type { Attachment } from "../utils/attachment-utils.ts"; -import { registerToolRenderer, renderCollapsibleHeader, renderHeader } from "./renderer-registry.ts"; -import type { ToolRenderer, ToolRenderResult } from "./types.ts"; - -// Execute JavaScript code with attachments using SandboxedIframe -export async function executeJavaScript( - code: string, - runtimeProviders: SandboxRuntimeProvider[], - signal?: AbortSignal, - sandboxUrlProvider?: () => string, -): Promise<{ output: string; files?: SandboxFile[] }> { - if (!code) { - throw new Error("Code parameter is required"); - } - - // Check for abort before starting - if (signal?.aborted) { - throw new Error("Execution aborted"); - } - - // Create a SandboxedIframe instance for execution - const sandbox = new SandboxIframe(); - if (sandboxUrlProvider) { - sandbox.sandboxUrlProvider = sandboxUrlProvider; - } - sandbox.style.display = "none"; - document.body.appendChild(sandbox); - - try { - const sandboxId = `repl-${Date.now()}-${Math.random().toString(36).substring(7)}`; - - // Pass providers to execute (router handles all message routing) - // No additional consumers needed - execute() has its own internal consumer - const result: SandboxResult = await sandbox.execute(sandboxId, code, runtimeProviders, [], signal); - - // Remove the sandbox iframe after execution - sandbox.remove(); - - // Build plain text response - let output = ""; - - // Add console output - result.console contains { type: string, text: string } from sandbox.js - if (result.console && result.console.length > 0) { - for (const entry of result.console) { - output += `${entry.text}\n`; - } - } - - // Add error if execution failed - if (!result.success) { - if (output) output += "\n"; - output += `Error: ${result.error?.message || "Unknown error"}\n${result.error?.stack || ""}`; - - // Throw error so tool call is marked as failed - throw new Error(output.trim()); - } - - // Add return value if present - if (result.returnValue !== undefined) { - if (output) output += "\n"; - output += `=> ${typeof result.returnValue === "object" ? JSON.stringify(result.returnValue, null, 2) : result.returnValue}`; - } - - // Add file notifications - if (result.files && result.files.length > 0) { - output += `\n[Files returned: ${result.files.length}]\n`; - for (const file of result.files) { - output += ` - ${file.fileName} (${file.mimeType})\n`; - } - } else { - // Explicitly note when no files were returned (helpful for debugging) - if (code.includes("returnFile")) { - output += "\n[No files returned - check async operations]"; - } - } - - return { - output: output.trim() || "Code executed successfully (no output)", - files: result.files, - }; - } catch (error: unknown) { - // Clean up on error - sandbox.remove(); - throw new Error((error as Error).message || "Execution failed"); - } -} - -export type JavaScriptReplToolResult = { - files?: - | { - fileName: string; - contentBase64: string; - mimeType: string; - }[] - | undefined; -}; - -const javascriptReplSchema = Type.Object({ - title: Type.String({ - description: - "Brief title describing what the code snippet tries to achieve in active form, e.g. 'Calculating sum'", - }), - code: Type.String({ description: "JavaScript code to execute" }), -}); - -export type JavaScriptReplParams = Static; - -interface JavaScriptReplResult { - output?: string; - files?: Array<{ - fileName: string; - mimeType: string; - size: number; - contentBase64: string; - }>; -} - -export function createJavaScriptReplTool(): AgentTool & { - runtimeProvidersFactory?: () => SandboxRuntimeProvider[]; - sandboxUrlProvider?: () => string; -} { - return { - label: "JavaScript REPL", - name: "javascript_repl", - runtimeProvidersFactory: () => [], // default to empty array - sandboxUrlProvider: undefined, // optional, for browser extensions - get description() { - const runtimeProviderDescriptions = - this.runtimeProvidersFactory?.() - .map((d) => d.getDescription()) - .filter((d) => d.trim().length > 0) || []; - return JAVASCRIPT_REPL_TOOL_DESCRIPTION(runtimeProviderDescriptions); - }, - parameters: javascriptReplSchema, - execute: async function (_toolCallId: string, args: Static, signal?: AbortSignal) { - const result = await executeJavaScript( - args.code, - this.runtimeProvidersFactory?.() ?? [], - signal, - this.sandboxUrlProvider, - ); - // Convert files to JSON-serializable with base64 payloads - const files = (result.files || []).map((f) => { - const toBase64 = (input: string | Uint8Array): { base64: string; size: number } => { - if (input instanceof Uint8Array) { - let binary = ""; - const chunk = 0x8000; - for (let i = 0; i < input.length; i += chunk) { - binary += String.fromCharCode(...input.subarray(i, i + chunk)); - } - return { base64: btoa(binary), size: input.length }; - } else if (typeof input === "string") { - const enc = new TextEncoder(); - const bytes = enc.encode(input); - let binary = ""; - const chunk = 0x8000; - for (let i = 0; i < bytes.length; i += chunk) { - binary += String.fromCharCode(...bytes.subarray(i, i + chunk)); - } - return { base64: btoa(binary), size: bytes.length }; - } else { - const s = String(input); - const enc = new TextEncoder(); - const bytes = enc.encode(s); - let binary = ""; - const chunk = 0x8000; - for (let i = 0; i < bytes.length; i += chunk) { - binary += String.fromCharCode(...bytes.subarray(i, i + chunk)); - } - return { base64: btoa(binary), size: bytes.length }; - } - }; - - const { base64, size } = toBase64(f.content); - return { - fileName: f.fileName || "file", - mimeType: f.mimeType || "application/octet-stream", - size, - contentBase64: base64, - }; - }); - return { content: [{ type: "text", text: result.output }], details: { files } }; - }, - }; -} - -// Export a default instance for backward compatibility -export const javascriptReplTool = createJavaScriptReplTool(); - -export const javascriptReplRenderer: ToolRenderer = { - render( - params: JavaScriptReplParams | undefined, - result: ToolResultMessage | undefined, - isStreaming?: boolean, - ): ToolRenderResult { - // Determine status - const state = result ? (result.isError ? "error" : "complete") : isStreaming ? "inprogress" : "complete"; - - // Create refs for collapsible code section - const codeContentRef = createRef(); - const codeChevronRef = createRef(); - - // With result: show params + result - if (result && params) { - const output = - result.content - ?.filter((c) => c.type === "text") - .map((c: any) => c.text) - .join("\n") || ""; - const files = result.details?.files || []; - - const attachments: Attachment[] = files.map((f, i) => { - // Decode base64 content for text files to show in overlay - let extractedText: string | undefined; - const isTextBased = - f.mimeType?.startsWith("text/") || - f.mimeType === "application/json" || - f.mimeType === "application/javascript" || - f.mimeType?.includes("xml"); - - if (isTextBased && f.contentBase64) { - try { - extractedText = atob(f.contentBase64); - } catch (_e) { - console.warn("Failed to decode base64 content for", f.fileName); - } - } - - return { - id: `repl-${Date.now()}-${i}`, - type: f.mimeType?.startsWith("image/") ? "image" : "document", - fileName: f.fileName || `file-${i}`, - mimeType: f.mimeType || "application/octet-stream", - size: f.size ?? 0, - content: f.contentBase64, - preview: f.mimeType?.startsWith("image/") ? f.contentBase64 : undefined, - extractedText, - }; - }); - - return { - content: html` -
- ${renderCollapsibleHeader(state, Code, params.title ? params.title : i18n("Executing JavaScript"), codeContentRef, codeChevronRef, false)} -
- - ${output ? html`` : ""} -
- ${ - attachments.length - ? html`
- ${attachments.map((att) => html``)} -
` - : "" - } -
- `, - isCustom: false, - }; - } - - // Just params (streaming or waiting for result) - if (params) { - return { - content: html` -
- ${renderCollapsibleHeader(state, Code, params.title ? params.title : i18n("Executing JavaScript"), codeContentRef, codeChevronRef, false)} -
- ${params.code ? html`` : ""} -
-
- `, - isCustom: false, - }; - } - - // No params or result yet - return { content: renderHeader(state, Code, i18n("Preparing JavaScript...")), isCustom: false }; - }, -}; - -// Auto-register the renderer -registerToolRenderer(javascriptReplTool.name, javascriptReplRenderer); diff --git a/packages/web-ui/src/tools/renderer-registry.ts b/packages/web-ui/src/tools/renderer-registry.ts deleted file mode 100644 index 69660c6a..00000000 --- a/packages/web-ui/src/tools/renderer-registry.ts +++ /dev/null @@ -1,130 +0,0 @@ -import { icon } from "@mariozechner/mini-lit"; -import { html, type TemplateResult } from "lit"; -import type { Ref } from "lit/directives/ref.js"; -import { ref } from "lit/directives/ref.js"; -import { ChevronsUpDown, ChevronUp, Loader } from "lucide"; -import type { ToolRenderer } from "./types.ts"; - -// Registry of tool renderers -export const toolRenderers = new Map(); - -/** - * Register a custom tool renderer - */ -export function registerToolRenderer(toolName: string, renderer: ToolRenderer): void { - toolRenderers.set(toolName, renderer); -} - -/** - * Get a tool renderer by name - */ -export function getToolRenderer(toolName: string): ToolRenderer | undefined { - return toolRenderers.get(toolName); -} - -/** - * Helper to render a header for tool renderers - * Shows icon on left when complete/error, spinner on right when in progress - */ -export function renderHeader( - state: "inprogress" | "complete" | "error", - toolIcon: any, - text: string | TemplateResult, -): TemplateResult { - const statusIcon = (iconComponent: any, color: string) => - html`${icon(iconComponent, "sm")}`; - - switch (state) { - case "inprogress": - return html` -
-
- ${statusIcon(toolIcon, "text-foreground")} - ${text} -
- ${statusIcon(Loader, "text-foreground animate-spin")} -
- `; - case "complete": - return html` -
- ${statusIcon(toolIcon, "text-green-600 dark:text-green-500")} - ${text} -
- `; - case "error": - return html` -
- ${statusIcon(toolIcon, "text-destructive")} - ${text} -
- `; - } -} - -/** - * Helper to render a collapsible header for tool renderers - * Same as renderHeader but with a chevron button that toggles visibility of content - */ -export function renderCollapsibleHeader( - state: "inprogress" | "complete" | "error", - toolIcon: any, - text: string | TemplateResult, - contentRef: Ref, - chevronRef: Ref, - defaultExpanded = false, -): TemplateResult { - const statusIcon = (iconComponent: any, color: string) => - html`${icon(iconComponent, "sm")}`; - - const toggleContent = (e: Event) => { - e.preventDefault(); - const content = contentRef.value; - const chevron = chevronRef.value; - if (content && chevron) { - const isCollapsed = content.classList.contains("max-h-0"); - if (isCollapsed) { - content.classList.remove("max-h-0"); - content.classList.add("max-h-[2000px]", "mt-3"); - // Show ChevronUp, hide ChevronsUpDown - const upIcon = chevron.querySelector(".chevron-up"); - const downIcon = chevron.querySelector(".chevrons-up-down"); - if (upIcon && downIcon) { - upIcon.classList.remove("hidden"); - downIcon.classList.add("hidden"); - } - } else { - content.classList.remove("max-h-[2000px]", "mt-3"); - content.classList.add("max-h-0"); - // Show ChevronsUpDown, hide ChevronUp - const upIcon = chevron.querySelector(".chevron-up"); - const downIcon = chevron.querySelector(".chevrons-up-down"); - if (upIcon && downIcon) { - upIcon.classList.add("hidden"); - downIcon.classList.remove("hidden"); - } - } - } - }; - - const toolIconColor = - state === "complete" - ? "text-green-600 dark:text-green-500" - : state === "error" - ? "text-destructive" - : "text-foreground"; - - return html` - - `; -} diff --git a/packages/web-ui/src/tools/renderers/BashRenderer.ts b/packages/web-ui/src/tools/renderers/BashRenderer.ts deleted file mode 100644 index 6f995f37..00000000 --- a/packages/web-ui/src/tools/renderers/BashRenderer.ts +++ /dev/null @@ -1,52 +0,0 @@ -import type { ToolResultMessage } from "@earendil-works/pi-ai"; -import { html } from "lit"; -import { SquareTerminal } from "lucide"; -import { i18n } from "../../utils/i18n.ts"; -import { renderHeader } from "../renderer-registry.ts"; -import type { ToolRenderer, ToolRenderResult } from "../types.ts"; - -interface BashParams { - command: string; -} - -// Bash tool has undefined details (only uses output) -export class BashRenderer implements ToolRenderer { - render(params: BashParams | undefined, result: ToolResultMessage | undefined): ToolRenderResult { - const state = result ? (result.isError ? "error" : "complete") : "inprogress"; - - // With result: show command + output - if (result && params?.command) { - const output = - result.content - ?.filter((c) => c.type === "text") - .map((c: any) => c.text) - .join("\n") || ""; - const combined = output ? `> ${params.command}\n\n${output}` : `> ${params.command}`; - return { - content: html` -
- ${renderHeader(state, SquareTerminal, i18n("Running command..."))} - -
- `, - isCustom: false, - }; - } - - // Just params (streaming or waiting) - if (params?.command) { - return { - content: html` -
- ${renderHeader(state, SquareTerminal, i18n("Running command..."))} - ${params.command}`}> -
- `, - isCustom: false, - }; - } - - // No params yet - return { content: renderHeader(state, SquareTerminal, i18n("Waiting for command...")), isCustom: false }; - } -} diff --git a/packages/web-ui/src/tools/renderers/CalculateRenderer.ts b/packages/web-ui/src/tools/renderers/CalculateRenderer.ts deleted file mode 100644 index a4672895..00000000 --- a/packages/web-ui/src/tools/renderers/CalculateRenderer.ts +++ /dev/null @@ -1,58 +0,0 @@ -import type { ToolResultMessage } from "@earendil-works/pi-ai"; -import { html } from "lit"; -import { Calculator } from "lucide"; -import { i18n } from "../../utils/i18n.ts"; -import { renderHeader } from "../renderer-registry.ts"; -import type { ToolRenderer, ToolRenderResult } from "../types.ts"; - -interface CalculateParams { - expression: string; -} - -// Calculate tool has undefined details (only uses output) -export class CalculateRenderer implements ToolRenderer { - render(params: CalculateParams | undefined, result: ToolResultMessage | undefined): ToolRenderResult { - const state = result ? (result.isError ? "error" : "complete") : "inprogress"; - - // Full params + full result - if (result && params?.expression) { - const output = - result.content - ?.filter((c) => c.type === "text") - .map((c: any) => c.text) - .join("\n") || ""; - - // Error: show expression in header, error below - if (result.isError) { - return { - content: html` -
- ${renderHeader(state, Calculator, params.expression)} -
${output}
-
- `, - isCustom: false, - }; - } - - // Success: show expression = result in header - return { content: renderHeader(state, Calculator, `${params.expression} = ${output}`), isCustom: false }; - } - - // Full params, no result: just show header with expression in it - if (params?.expression) { - return { - content: renderHeader(state, Calculator, `${i18n("Calculating")} ${params.expression}`), - isCustom: false, - }; - } - - // Partial params (empty expression), no result - if (params && !params.expression) { - return { content: renderHeader(state, Calculator, i18n("Writing expression...")), isCustom: false }; - } - - // No params, no result - return { content: renderHeader(state, Calculator, i18n("Waiting for expression...")), isCustom: false }; - } -} diff --git a/packages/web-ui/src/tools/renderers/DefaultRenderer.ts b/packages/web-ui/src/tools/renderers/DefaultRenderer.ts deleted file mode 100644 index e5bf0ff1..00000000 --- a/packages/web-ui/src/tools/renderers/DefaultRenderer.ts +++ /dev/null @@ -1,103 +0,0 @@ -import type { ToolResultMessage } from "@earendil-works/pi-ai"; -import { html } from "lit"; -import { Code } from "lucide"; -import { i18n } from "../../utils/i18n.ts"; -import { renderHeader } from "../renderer-registry.ts"; -import type { ToolRenderer, ToolRenderResult } from "../types.ts"; - -export class DefaultRenderer implements ToolRenderer { - render(params: any | undefined, result: ToolResultMessage | undefined, isStreaming?: boolean): ToolRenderResult { - const state = result ? (result.isError ? "error" : "complete") : isStreaming ? "inprogress" : "complete"; - - // Format params as JSON - let paramsJson = ""; - if (params) { - try { - paramsJson = JSON.stringify(JSON.parse(params), null, 2); - } catch { - try { - paramsJson = JSON.stringify(params, null, 2); - } catch { - paramsJson = String(params); - } - } - } - - // With result: show header + params + result - if (result) { - let outputJson = - result.content - ?.filter((c) => c.type === "text") - .map((c: any) => c.text) - .join("\n") || i18n("(no output)"); - let outputLanguage = "text"; - - // Try to parse and pretty-print if it's valid JSON - try { - const parsed = JSON.parse(outputJson); - outputJson = JSON.stringify(parsed, null, 2); - outputLanguage = "json"; - } catch { - // Not valid JSON, leave as-is and use text highlighting - } - - return { - content: html` -
- ${renderHeader(state, Code, "Tool Call")} - ${ - paramsJson - ? html`
-
${i18n("Input")}
- -
` - : "" - } -
-
${i18n("Output")}
- -
-
- `, - isCustom: false, - }; - } - - // Just params (streaming or waiting for result) - if (params) { - if (isStreaming && (!paramsJson || paramsJson === "{}" || paramsJson === "null")) { - return { - content: html` -
- ${renderHeader(state, Code, "Preparing tool parameters...")} -
- `, - isCustom: false, - }; - } - - return { - content: html` -
- ${renderHeader(state, Code, "Tool Call")} -
-
${i18n("Input")}
- -
-
- `, - isCustom: false, - }; - } - - // No params or result yet - return { - content: html` -
- ${renderHeader(state, Code, "Preparing tool...")} -
- `, - isCustom: false, - }; - } -} diff --git a/packages/web-ui/src/tools/renderers/GetCurrentTimeRenderer.ts b/packages/web-ui/src/tools/renderers/GetCurrentTimeRenderer.ts deleted file mode 100644 index 6e6240e6..00000000 --- a/packages/web-ui/src/tools/renderers/GetCurrentTimeRenderer.ts +++ /dev/null @@ -1,92 +0,0 @@ -import type { ToolResultMessage } from "@earendil-works/pi-ai"; -import { html } from "lit"; -import { Clock } from "lucide"; -import { i18n } from "../../utils/i18n.ts"; -import { renderHeader } from "../renderer-registry.ts"; -import type { ToolRenderer, ToolRenderResult } from "../types.ts"; - -interface GetCurrentTimeParams { - timezone?: string; -} - -// GetCurrentTime tool has undefined details (only uses output) -export class GetCurrentTimeRenderer implements ToolRenderer { - render( - params: GetCurrentTimeParams | undefined, - result: ToolResultMessage | undefined, - ): ToolRenderResult { - const state = result ? (result.isError ? "error" : "complete") : "inprogress"; - - // Full params + full result - if (result && params) { - const output = - result.content - ?.filter((c) => c.type === "text") - .map((c: any) => c.text) - .join("\n") || ""; - const headerText = params.timezone - ? `${i18n("Getting current time in")} ${params.timezone}` - : i18n("Getting current date and time"); - - // Error: show header, error below - if (result.isError) { - return { - content: html` -
- ${renderHeader(state, Clock, headerText)} -
${output}
-
- `, - isCustom: false, - }; - } - - // Success: show time in header - return { content: renderHeader(state, Clock, `${headerText}: ${output}`), isCustom: false }; - } - - // Full result, no params - if (result) { - const output = - result.content - ?.filter((c) => c.type === "text") - .map((c: any) => c.text) - .join("\n") || ""; - - // Error: show header, error below - if (result.isError) { - return { - content: html` -
- ${renderHeader(state, Clock, i18n("Getting current date and time"))} -
${output}
-
- `, - isCustom: false, - }; - } - - // Success: show time in header - return { - content: renderHeader(state, Clock, `${i18n("Getting current date and time")}: ${output}`), - isCustom: false, - }; - } - - // Full params, no result: show timezone info in header - if (params?.timezone) { - return { - content: renderHeader(state, Clock, `${i18n("Getting current time in")} ${params.timezone}`), - isCustom: false, - }; - } - - // Partial params (no timezone) or empty params, no result - if (params) { - return { content: renderHeader(state, Clock, i18n("Getting current date and time")), isCustom: false }; - } - - // No params, no result - return { content: renderHeader(state, Clock, i18n("Getting time...")), isCustom: false }; - } -} diff --git a/packages/web-ui/src/tools/types.ts b/packages/web-ui/src/tools/types.ts deleted file mode 100644 index 37950364..00000000 --- a/packages/web-ui/src/tools/types.ts +++ /dev/null @@ -1,15 +0,0 @@ -import type { ToolResultMessage } from "@earendil-works/pi-ai"; -import type { TemplateResult } from "lit"; - -export interface ToolRenderResult { - content: TemplateResult; - isCustom: boolean; // true = no card wrapper, false = wrap in card -} - -export interface ToolRenderer { - render( - params: TParams | undefined, - result: ToolResultMessage | undefined, - isStreaming?: boolean, - ): ToolRenderResult; -} diff --git a/packages/web-ui/src/utils/attachment-utils.ts b/packages/web-ui/src/utils/attachment-utils.ts deleted file mode 100644 index 57a8a6c7..00000000 --- a/packages/web-ui/src/utils/attachment-utils.ts +++ /dev/null @@ -1,472 +0,0 @@ -import { parseAsync } from "docx-preview"; -import JSZip from "jszip"; -import type { PDFDocumentProxy } from "pdfjs-dist"; -import * as pdfjsLib from "pdfjs-dist"; -import * as XLSX from "xlsx"; -import { i18n } from "./i18n.ts"; - -// Configure PDF.js worker - we'll need to bundle this -pdfjsLib.GlobalWorkerOptions.workerSrc = new URL("pdfjs-dist/build/pdf.worker.min.mjs", import.meta.url).toString(); - -export interface Attachment { - id: string; - type: "image" | "document"; - fileName: string; - mimeType: string; - size: number; - content: string; // base64 encoded original data (without data URL prefix) - extractedText?: string; // For documents: text - preview?: string; // base64 image preview (first page for PDFs, or same as content for images) -} - -/** - * Load an attachment from various sources - * @param source - URL string, File, Blob, or ArrayBuffer - * @param fileName - Optional filename override - * @returns Promise - * @throws Error if loading fails - */ -export async function loadAttachment( - source: string | File | Blob | ArrayBuffer, - fileName?: string, -): Promise { - let arrayBuffer: ArrayBuffer; - let detectedFileName = fileName || "unnamed"; - let mimeType = "application/octet-stream"; - let size = 0; - - // Convert source to ArrayBuffer - if (typeof source === "string") { - // It's a URL - fetch it - const response = await fetch(source); - if (!response.ok) { - throw new Error(i18n("Failed to fetch file")); - } - arrayBuffer = await response.arrayBuffer(); - size = arrayBuffer.byteLength; - mimeType = response.headers.get("content-type") || mimeType; - if (!fileName) { - // Try to extract filename from URL - const urlParts = source.split("/"); - detectedFileName = urlParts[urlParts.length - 1] || "document"; - } - } else if (source instanceof File) { - arrayBuffer = await source.arrayBuffer(); - size = source.size; - mimeType = source.type || mimeType; - detectedFileName = fileName || source.name; - } else if (source instanceof Blob) { - arrayBuffer = await source.arrayBuffer(); - size = source.size; - mimeType = source.type || mimeType; - } else if (source instanceof ArrayBuffer) { - arrayBuffer = source; - size = source.byteLength; - } else { - throw new Error(i18n("Invalid source type")); - } - - // Convert ArrayBuffer to base64 - handle large files properly - const uint8Array = new Uint8Array(arrayBuffer); - let binary = ""; - const chunkSize = 0x8000; // Process in 32KB chunks to avoid stack overflow - for (let i = 0; i < uint8Array.length; i += chunkSize) { - const chunk = uint8Array.slice(i, i + chunkSize); - binary += String.fromCharCode(...chunk); - } - const base64Content = btoa(binary); - - // Detect type and process accordingly - const id = `${detectedFileName}_${Date.now()}_${Math.random()}`; - - // Check if it's a PDF - if (mimeType === "application/pdf" || detectedFileName.toLowerCase().endsWith(".pdf")) { - const { extractedText, preview } = await processPdf(arrayBuffer, detectedFileName); - return { - id, - type: "document", - fileName: detectedFileName, - mimeType: "application/pdf", - size, - content: base64Content, - extractedText, - preview, - }; - } - - // Check if it's a DOCX file - if ( - mimeType === "application/vnd.openxmlformats-officedocument.wordprocessingml.document" || - detectedFileName.toLowerCase().endsWith(".docx") - ) { - const { extractedText } = await processDocx(arrayBuffer, detectedFileName); - return { - id, - type: "document", - fileName: detectedFileName, - mimeType: "application/vnd.openxmlformats-officedocument.wordprocessingml.document", - size, - content: base64Content, - extractedText, - }; - } - - // Check if it's a PPTX file - if ( - mimeType === "application/vnd.openxmlformats-officedocument.presentationml.presentation" || - detectedFileName.toLowerCase().endsWith(".pptx") - ) { - const { extractedText } = await processPptx(arrayBuffer, detectedFileName); - return { - id, - type: "document", - fileName: detectedFileName, - mimeType: "application/vnd.openxmlformats-officedocument.presentationml.presentation", - size, - content: base64Content, - extractedText, - }; - } - - // Check if it's an Excel file (XLSX/XLS) - const excelMimeTypes = [ - "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", - "application/vnd.ms-excel", - ]; - if ( - excelMimeTypes.includes(mimeType) || - detectedFileName.toLowerCase().endsWith(".xlsx") || - detectedFileName.toLowerCase().endsWith(".xls") - ) { - const { extractedText } = await processExcel(arrayBuffer, detectedFileName); - return { - id, - type: "document", - fileName: detectedFileName, - mimeType: mimeType.startsWith("application/vnd") - ? mimeType - : "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", - size, - content: base64Content, - extractedText, - }; - } - - // Check if it's an image - if (mimeType.startsWith("image/")) { - return { - id, - type: "image", - fileName: detectedFileName, - mimeType, - size, - content: base64Content, - preview: base64Content, // For images, preview is the same as content - }; - } - - // Check if it's a text document - const textExtensions = [ - ".txt", - ".md", - ".json", - ".xml", - ".html", - ".css", - ".js", - ".ts", - ".jsx", - ".tsx", - ".yml", - ".yaml", - ]; - const isTextFile = - mimeType.startsWith("text/") || textExtensions.some((ext) => detectedFileName.toLowerCase().endsWith(ext)); - - if (isTextFile) { - const decoder = new TextDecoder(); - const text = decoder.decode(arrayBuffer); - return { - id, - type: "document", - fileName: detectedFileName, - mimeType: mimeType.startsWith("text/") ? mimeType : "text/plain", - size, - content: base64Content, - extractedText: text, - }; - } - - throw new Error(`Unsupported file type: ${mimeType}`); -} - -async function processPdf( - arrayBuffer: ArrayBuffer, - fileName: string, -): Promise<{ extractedText: string; preview?: string }> { - let pdf: PDFDocumentProxy | null = null; - try { - pdf = await pdfjsLib.getDocument({ data: arrayBuffer }).promise; - - // Extract text with page structure - let extractedText = ``; - for (let i = 1; i <= pdf.numPages; i++) { - const page = await pdf.getPage(i); - const textContent = await page.getTextContent(); - const pageText = textContent.items - .map((item: any) => item.str) - .filter((str: string) => str.trim()) - .join(" "); - extractedText += `\n\n${pageText}\n`; - } - extractedText += "\n"; - - // Generate preview from first page - const preview = await generatePdfPreview(pdf); - - return { extractedText, preview }; - } catch (error) { - console.error("Error processing PDF:", error); - throw new Error(`Failed to process PDF: ${String(error)}`); - } finally { - // Clean up PDF resources - if (pdf) { - pdf.destroy(); - } - } -} - -async function generatePdfPreview(pdf: PDFDocumentProxy): Promise { - try { - const page = await pdf.getPage(1); - const viewport = page.getViewport({ scale: 1.0 }); - - // Create canvas with reasonable size for thumbnail (160x160 max) - const scale = Math.min(160 / viewport.width, 160 / viewport.height); - const scaledViewport = page.getViewport({ scale }); - - const canvas = document.createElement("canvas"); - const context = canvas.getContext("2d"); - if (!context) { - return undefined; - } - - canvas.height = scaledViewport.height; - canvas.width = scaledViewport.width; - - const renderContext = { - canvasContext: context, - viewport: scaledViewport, - canvas: canvas, - }; - await page.render(renderContext).promise; - - // Return base64 without data URL prefix - return canvas.toDataURL("image/png").split(",")[1]; - } catch (error) { - console.error("Error generating PDF preview:", error); - return undefined; - } -} - -async function processDocx(arrayBuffer: ArrayBuffer, fileName: string): Promise<{ extractedText: string }> { - try { - // Parse document structure - const wordDoc = await parseAsync(arrayBuffer); - - // Extract structured text from document body - let extractedText = `\n\n`; - - const body = wordDoc.documentPart?.body; - if (body?.children) { - // Walk through document elements and extract text - const texts: string[] = []; - for (const element of body.children) { - const text = extractTextFromElement(element); - if (text) { - texts.push(text); - } - } - extractedText += texts.join("\n"); - } - - extractedText += `\n\n`; - return { extractedText }; - } catch (error) { - console.error("Error processing DOCX:", error); - throw new Error(`Failed to process DOCX: ${String(error)}`); - } -} - -function extractTextFromElement(element: any): string { - let text = ""; - - // Check type with lowercase - const elementType = element.type?.toLowerCase() || ""; - - // Handle paragraphs - if (elementType === "paragraph" && element.children) { - for (const child of element.children) { - const childType = child.type?.toLowerCase() || ""; - if (childType === "run" && child.children) { - for (const textChild of child.children) { - const textType = textChild.type?.toLowerCase() || ""; - if (textType === "text") { - text += textChild.text || ""; - } - } - } else if (childType === "text") { - text += child.text || ""; - } - } - } - // Handle tables - else if (elementType === "table") { - if (element.children) { - const tableTexts: string[] = []; - for (const row of element.children) { - const rowType = row.type?.toLowerCase() || ""; - if (rowType === "tablerow" && row.children) { - const rowTexts: string[] = []; - for (const cell of row.children) { - const cellType = cell.type?.toLowerCase() || ""; - if (cellType === "tablecell" && cell.children) { - const cellTexts: string[] = []; - for (const cellElement of cell.children) { - const cellText = extractTextFromElement(cellElement); - if (cellText) cellTexts.push(cellText); - } - if (cellTexts.length > 0) rowTexts.push(cellTexts.join(" ")); - } - } - if (rowTexts.length > 0) tableTexts.push(rowTexts.join(" | ")); - } - } - if (tableTexts.length > 0) { - text = `\n[Table]\n${tableTexts.join("\n")}\n[/Table]\n`; - } - } - } - // Recursively handle other container elements - else if (element.children && Array.isArray(element.children)) { - const childTexts: string[] = []; - for (const child of element.children) { - const childText = extractTextFromElement(child); - if (childText) childTexts.push(childText); - } - text = childTexts.join(" "); - } - - return text.trim(); -} - -async function processPptx(arrayBuffer: ArrayBuffer, fileName: string): Promise<{ extractedText: string }> { - try { - // Load the PPTX file as a ZIP - const zip = await JSZip.loadAsync(arrayBuffer); - - // PPTX slides are stored in ppt/slides/slide[n].xml - let extractedText = ``; - - // Get all slide files and sort them numerically - const slideFiles = Object.keys(zip.files) - .filter((name) => name.match(/ppt\/slides\/slide\d+\.xml$/)) - .sort((a, b) => { - const numA = Number.parseInt(a.match(/slide(\d+)\.xml$/)?.[1] || "0", 10); - const numB = Number.parseInt(b.match(/slide(\d+)\.xml$/)?.[1] || "0", 10); - return numA - numB; - }); - - // Extract text from each slide - for (let i = 0; i < slideFiles.length; i++) { - const slideFile = zip.file(slideFiles[i]); - if (slideFile) { - const slideXml = await slideFile.async("text"); - - // Extract text from XML (simple regex approach) - // Looking for tags which contain text in PPTX - const textMatches = slideXml.match(/]*>([^<]+)<\/a:t>/g); - - if (textMatches) { - extractedText += `\n`; - const slideTexts = textMatches - .map((match) => { - const textMatch = match.match(/]*>([^<]+)<\/a:t>/); - return textMatch ? textMatch[1] : ""; - }) - .filter((t) => t.trim()); - - if (slideTexts.length > 0) { - extractedText += `\n${slideTexts.join("\n")}`; - } - extractedText += "\n"; - } - } - } - - // Also try to extract text from notes - const notesFiles = Object.keys(zip.files) - .filter((name) => name.match(/ppt\/notesSlides\/notesSlide\d+\.xml$/)) - .sort((a, b) => { - const numA = Number.parseInt(a.match(/notesSlide(\d+)\.xml$/)?.[1] || "0", 10); - const numB = Number.parseInt(b.match(/notesSlide(\d+)\.xml$/)?.[1] || "0", 10); - return numA - numB; - }); - - if (notesFiles.length > 0) { - extractedText += "\n"; - for (const noteFile of notesFiles) { - const file = zip.file(noteFile); - if (file) { - const noteXml = await file.async("text"); - const textMatches = noteXml.match(/]*>([^<]+)<\/a:t>/g); - if (textMatches) { - const noteTexts = textMatches - .map((match) => { - const textMatch = match.match(/]*>([^<]+)<\/a:t>/); - return textMatch ? textMatch[1] : ""; - }) - .filter((t) => t.trim()); - - if (noteTexts.length > 0) { - const slideNum = noteFile.match(/notesSlide(\d+)\.xml$/)?.[1]; - extractedText += `\n[Slide ${slideNum} notes]: ${noteTexts.join(" ")}`; - } - } - } - } - extractedText += "\n"; - } - - extractedText += "\n"; - return { extractedText }; - } catch (error) { - console.error("Error processing PPTX:", error); - throw new Error(`Failed to process PPTX: ${String(error)}`); - } -} - -async function processExcel(arrayBuffer: ArrayBuffer, fileName: string): Promise<{ extractedText: string }> { - try { - // Read the workbook - const workbook = XLSX.read(arrayBuffer, { type: "array" }); - - let extractedText = ``; - - // Process each sheet - for (const [index, sheetName] of workbook.SheetNames.entries()) { - const worksheet = workbook.Sheets[sheetName]; - - // Extract text as CSV for the extractedText field - const csvText = XLSX.utils.sheet_to_csv(worksheet); - extractedText += `\n\n${csvText}\n`; - } - - extractedText += "\n"; - - return { extractedText }; - } catch (error) { - console.error("Error processing Excel:", error); - throw new Error(`Failed to process Excel: ${String(error)}`); - } -} diff --git a/packages/web-ui/src/utils/auth-token.ts b/packages/web-ui/src/utils/auth-token.ts deleted file mode 100644 index 0d507fc6..00000000 --- a/packages/web-ui/src/utils/auth-token.ts +++ /dev/null @@ -1,22 +0,0 @@ -import PromptDialog from "@mariozechner/mini-lit/dist/PromptDialog.js"; -import { i18n } from "./i18n.ts"; - -export async function getAuthToken(): Promise { - let authToken: string | undefined = localStorage.getItem(`auth-token`) || ""; - if (authToken) return authToken; - - while (true) { - authToken = ( - await PromptDialog.ask(i18n("Enter Auth Token"), i18n("Please enter your auth token."), "", true) - )?.trim(); - if (authToken) { - localStorage.setItem(`auth-token`, authToken); - break; - } - } - return authToken?.trim() || undefined; -} - -export async function clearAuthToken() { - localStorage.removeItem(`auth-token`); -} diff --git a/packages/web-ui/src/utils/format.ts b/packages/web-ui/src/utils/format.ts deleted file mode 100644 index 92119e88..00000000 --- a/packages/web-ui/src/utils/format.ts +++ /dev/null @@ -1,42 +0,0 @@ -import type { Usage } from "@earendil-works/pi-ai"; -import { i18n } from "@mariozechner/mini-lit"; - -export function formatCost(cost: number): string { - return `$${cost.toFixed(4)}`; -} - -export function formatModelCost(cost: any): string { - if (!cost) return i18n("Free"); - const input = cost.input || 0; - const output = cost.output || 0; - if (input === 0 && output === 0) return i18n("Free"); - - // Format numbers with appropriate precision - const formatNum = (num: number): string => { - if (num >= 100) return num.toFixed(0); - if (num >= 10) return num.toFixed(1).replace(/\.0$/, ""); - if (num >= 1) return num.toFixed(2).replace(/\.?0+$/, ""); - return num.toFixed(3).replace(/\.?0+$/, ""); - }; - - return `$${formatNum(input)}/$${formatNum(output)}`; -} - -export function formatUsage(usage: Usage) { - if (!usage) return ""; - - const parts = []; - if (usage.input) parts.push(`↑${formatTokenCount(usage.input)}`); - if (usage.output) parts.push(`↓${formatTokenCount(usage.output)}`); - if (usage.cacheRead) parts.push(`R${formatTokenCount(usage.cacheRead)}`); - if (usage.cacheWrite) parts.push(`W${formatTokenCount(usage.cacheWrite)}`); - if (usage.cost?.total) parts.push(formatCost(usage.cost.total)); - - return parts.join(" "); -} - -export function formatTokenCount(count: number): string { - if (count < 1000) return count.toString(); - if (count < 10000) return `${(count / 1000).toFixed(1)}k`; - return `${Math.round(count / 1000)}k`; -} diff --git a/packages/web-ui/src/utils/i18n.ts b/packages/web-ui/src/utils/i18n.ts deleted file mode 100644 index 1f50a7d0..00000000 --- a/packages/web-ui/src/utils/i18n.ts +++ /dev/null @@ -1,653 +0,0 @@ -import { defaultEnglish, defaultGerman, type MiniLitRequiredMessages, setTranslations } from "@mariozechner/mini-lit"; - -declare module "@mariozechner/mini-lit" { - interface i18nMessages extends MiniLitRequiredMessages { - Free: string; - "Input Required": string; - Cancel: string; - Confirm: string; - "Select Model": string; - "Search models...": string; - Format: string; - Thinking: string; - Vision: string; - You: string; - Assistant: string; - "Thinking...": string; - "Type your message...": string; - "API Keys Configuration": string; - "Configure API keys for LLM providers. Keys are stored locally in your browser.": string; - Configured: string; - "Not configured": string; - "✓ Valid": string; - "✗ Invalid": string; - "Testing...": string; - Update: string; - Test: string; - Remove: string; - Save: string; - "Update API key": string; - "Enter API key": string; - "Type a message...": string; - "Failed to fetch file": string; - "Invalid source type": string; - PDF: string; - Document: string; - Presentation: string; - Spreadsheet: string; - Text: string; - "Error loading file": string; - "No text content available": string; - "Failed to load PDF": string; - "Failed to load document": string; - "Failed to load spreadsheet": string; - "Error loading PDF": string; - "Error loading document": string; - "Error loading spreadsheet": string; - "Preview not available for this file type.": string; - "Click the download button above to view it on your computer.": string; - "No content available": string; - "Failed to display text content": string; - "API keys are required to use AI models. Get your keys from the provider's website.": string; - console: string; - "Copy output": string; - "Copied!": string; - "Error:": string; - "Request aborted": string; - Call: string; - Result: string; - "(no result)": string; - "Waiting for tool result…": string; - "Call was aborted; no result.": string; - "No session available": string; - "No session set": string; - "Preparing tool parameters...": string; - "(no output)": string; - Input: string; - Output: string; - "Writing expression...": string; - "Waiting for expression...": string; - Calculating: string; - "Getting current time in": string; - "Getting current date and time": string; - "Waiting for command...": string; - "Writing command...": string; - "Running command...": string; - "Command failed:": string; - "Enter Auth Token": string; - "Please enter your auth token.": string; - "Auth token is required for proxy transport": string; - // JavaScript REPL strings - "Execution aborted": string; - "Code parameter is required": string; - "Unknown error": string; - "Code executed successfully (no output)": string; - "Execution failed": string; - "JavaScript REPL": string; - "JavaScript code to execute": string; - "Writing JavaScript code...": string; - "Executing JavaScript": string; - "Preparing JavaScript...": string; - "Preparing command...": string; - "Preparing calculation...": string; - "Preparing tool...": string; - "Getting time...": string; - // Artifacts strings - "Processing artifact...": string; - "Preparing artifact...": string; - "Processing artifact": string; - "Processed artifact": string; - "Creating artifact": string; - "Created artifact": string; - "Updating artifact": string; - "Updated artifact": string; - "Rewriting artifact": string; - "Rewrote artifact": string; - "Getting artifact": string; - "Got artifact": string; - "Deleting artifact": string; - "Deleted artifact": string; - "Getting logs": string; - "Got logs": string; - "An error occurred": string; - "Copy logs": string; - "Autoscroll enabled": string; - "Autoscroll disabled": string; - Processing: string; - Create: string; - Rewrite: string; - Get: string; - Delete: string; - "Get logs": string; - "Show artifacts": string; - "Close artifacts": string; - Artifacts: string; - "Copy HTML": string; - "Download HTML": string; - "Reload HTML": string; - "Copy SVG": string; - "Download SVG": string; - "Copy Markdown": string; - "Download Markdown": string; - Download: string; - "No logs for {filename}": string; - "API Keys Settings": string; - Settings: string; - "API Keys": string; - Proxy: string; - "Use CORS Proxy": string; - "Proxy URL": string; - "Format: The proxy must accept requests as /?url=": string; - "Settings are stored locally in your browser": string; - Clear: string; - "API Key Required": string; - "Enter your API key for {provider}": string; - "Allows browser-based apps to bypass CORS restrictions when calling LLM providers. Required for Z-AI and Anthropic with OAuth token.": string; - Off: string; - Minimal: string; - Low: string; - Medium: string; - High: string; - "Storage Permission Required": string; - "This app needs persistent storage to save your conversations": string; - "Why is this needed?": string; - "Without persistent storage, your browser may delete saved conversations when it needs disk space. Granting this permission ensures your chat history is preserved.": string; - "What this means:": string; - "Your conversations will be saved locally in your browser": string; - "Data will not be deleted automatically to free up space": string; - "You can still manually clear data at any time": string; - "No data is sent to external servers": string; - "Continue Anyway": string; - "Requesting...": string; - "Grant Permission": string; - Sessions: string; - "Load a previous conversation": string; - "No sessions yet": string; - "Delete this session?": string; - Today: string; - Yesterday: string; - "{days} days ago": string; - messages: string; - tokens: string; - "Drop files here": string; - // Providers & Models - "Providers & Models": string; - "Cloud Providers": string; - "Cloud LLM providers with predefined models. API keys are stored locally in your browser.": string; - "Custom Providers": string; - "User-configured servers with auto-discovered or manually defined models.": string; - "Add Provider": string; - "No custom providers configured. Click 'Add Provider' to get started.": string; - Models: string; - "auto-discovered": string; - Refresh: string; - Edit: string; - "Are you sure you want to delete this provider?": string; - "Edit Provider": string; - "Provider Name": string; - "e.g., My Ollama Server": string; - "Provider Type": string; - "Base URL": string; - "e.g., http://localhost:11434": string; - "API Key (Optional)": string; - "Leave empty if not required": string; - "Test Connection": string; - Discovered: string; - models: string; - and: string; - more: string; - "For manual provider types, add models after saving the provider.": string; - "Please fill in all required fields": string; - "Failed to save provider": string; - "OpenAI Completions Compatible": string; - "OpenAI Responses Compatible": string; - "Anthropic Messages Compatible": string; - "Checking...": string; - Disconnected: string; - } -} - -export const translations = { - en: { - ...defaultEnglish, - Free: "Free", - "Input Required": "Input Required", - Cancel: "Cancel", - Confirm: "Confirm", - "Select Model": "Select Model", - "Search models...": "Search models...", - Format: "Format", - Thinking: "Thinking", - Vision: "Vision", - You: "You", - Assistant: "Assistant", - "Thinking...": "Thinking...", - "Type your message...": "Type your message...", - "API Keys Configuration": "API Keys Configuration", - "Configure API keys for LLM providers. Keys are stored locally in your browser.": - "Configure API keys for LLM providers. Keys are stored locally in your browser.", - Configured: "Configured", - "Not configured": "Not configured", - "✓ Valid": "✓ Valid", - "✗ Invalid": "✗ Invalid", - "Testing...": "Testing...", - Update: "Update", - Test: "Test", - Remove: "Remove", - Save: "Save", - "Update API key": "Update API key", - "Enter API key": "Enter API key", - "Type a message...": "Type a message...", - "Failed to fetch file": "Failed to fetch file", - "Invalid source type": "Invalid source type", - PDF: "PDF", - Document: "Document", - Presentation: "Presentation", - Spreadsheet: "Spreadsheet", - Text: "Text", - "Error loading file": "Error loading file", - "No text content available": "No text content available", - "Failed to load PDF": "Failed to load PDF", - "Failed to load document": "Failed to load document", - "Failed to load spreadsheet": "Failed to load spreadsheet", - "Error loading PDF": "Error loading PDF", - "Error loading document": "Error loading document", - "Error loading spreadsheet": "Error loading spreadsheet", - "Preview not available for this file type.": "Preview not available for this file type.", - "Click the download button above to view it on your computer.": - "Click the download button above to view it on your computer.", - "No content available": "No content available", - "Failed to display text content": "Failed to display text content", - "API keys are required to use AI models. Get your keys from the provider's website.": - "API keys are required to use AI models. Get your keys from the provider's website.", - console: "console", - "Copy output": "Copy output", - "Copied!": "Copied!", - "Error:": "Error:", - "Request aborted": "Request aborted", - Call: "Call", - Result: "Result", - "(no result)": "(no result)", - "Waiting for tool result…": "Waiting for tool result…", - "Call was aborted; no result.": "Call was aborted; no result.", - "No session available": "No session available", - "No session set": "No session set", - "Preparing tool parameters...": "Preparing tool parameters...", - "(no output)": "(no output)", - Input: "Input", - Output: "Output", - "Waiting for expression...": "Waiting for expression...", - "Writing expression...": "Writing expression...", - Calculating: "Calculating", - "Getting current time in": "Getting current time in", - "Getting current date and time": "Getting current date and time", - "Waiting for command...": "Waiting for command...", - "Writing command...": "Writing command...", - "Running command...": "Running command...", - "Command failed": "Command failed", - "Enter Auth Token": "Enter Auth Token", - "Please enter your auth token.": "Please enter your auth token.", - "Auth token is required for proxy transport": "Auth token is required for proxy transport", - // JavaScript REPL strings - "Execution aborted": "Execution aborted", - "Code parameter is required": "Code parameter is required", - "Unknown error": "Unknown error", - "Code executed successfully (no output)": "Code executed successfully (no output)", - "Execution failed": "Execution failed", - "JavaScript REPL": "JavaScript REPL", - "JavaScript code to execute": "JavaScript code to execute", - "Writing JavaScript code...": "Writing JavaScript code...", - "Executing JavaScript": "Executing JavaScript", - "Preparing JavaScript...": "Preparing JavaScript...", - "Preparing command...": "Preparing command...", - "Preparing calculation...": "Preparing calculation...", - "Preparing tool...": "Preparing tool...", - "Getting time...": "Getting time...", - // Artifacts strings - "Processing artifact...": "Processing artifact...", - "Preparing artifact...": "Preparing artifact...", - "Processing artifact": "Processing artifact", - "Processed artifact": "Processed artifact", - "Creating artifact": "Creating artifact", - "Created artifact": "Created artifact", - "Updating artifact": "Updating artifact", - "Updated artifact": "Updated artifact", - "Rewriting artifact": "Rewriting artifact", - "Rewrote artifact": "Rewrote artifact", - "Getting artifact": "Getting artifact", - "Got artifact": "Got artifact", - "Deleting artifact": "Deleting artifact", - "Deleted artifact": "Deleted artifact", - "Getting logs": "Getting logs", - "Got logs": "Got logs", - "An error occurred": "An error occurred", - "Copy logs": "Copy logs", - "Autoscroll enabled": "Autoscroll enabled", - "Autoscroll disabled": "Autoscroll disabled", - Processing: "Processing", - Create: "Create", - Rewrite: "Rewrite", - Get: "Get", - "Get logs": "Get logs", - "Show artifacts": "Show artifacts", - "Close artifacts": "Close artifacts", - Artifacts: "Artifacts", - "Copy HTML": "Copy HTML", - "Download HTML": "Download HTML", - "Reload HTML": "Reload HTML", - "Copy SVG": "Copy SVG", - "Download SVG": "Download SVG", - "Copy Markdown": "Copy Markdown", - "Download Markdown": "Download Markdown", - Download: "Download", - "No logs for {filename}": "No logs for {filename}", - "API Keys Settings": "API Keys Settings", - Settings: "Settings", - "API Keys": "API Keys", - Proxy: "Proxy", - "Use CORS Proxy": "Use CORS Proxy", - "Proxy URL": "Proxy URL", - "Format: The proxy must accept requests as /?url=": - "Format: The proxy must accept requests as /?url=", - "Settings are stored locally in your browser": "Settings are stored locally in your browser", - Clear: "Clear", - "API Key Required": "API Key Required", - "Enter your API key for {provider}": "Enter your API key for {provider}", - "Allows browser-based apps to bypass CORS restrictions when calling LLM providers. Required for Z-AI and Anthropic with OAuth token.": - "Allows browser-based apps to bypass CORS restrictions when calling LLM providers. Required for Z-AI and Anthropic with OAuth token.", - Off: "Off", - Minimal: "Minimal", - Low: "Low", - Medium: "Medium", - High: "High", - "Storage Permission Required": "Storage Permission Required", - "This app needs persistent storage to save your conversations": - "This app needs persistent storage to save your conversations", - "Why is this needed?": "Why is this needed?", - "Without persistent storage, your browser may delete saved conversations when it needs disk space. Granting this permission ensures your chat history is preserved.": - "Without persistent storage, your browser may delete saved conversations when it needs disk space. Granting this permission ensures your chat history is preserved.", - "What this means:": "What this means:", - "Your conversations will be saved locally in your browser": - "Your conversations will be saved locally in your browser", - "Data will not be deleted automatically to free up space": - "Data will not be deleted automatically to free up space", - "You can still manually clear data at any time": "You can still manually clear data at any time", - "No data is sent to external servers": "No data is sent to external servers", - "Continue Anyway": "Continue Anyway", - "Requesting...": "Requesting...", - "Grant Permission": "Grant Permission", - Sessions: "Sessions", - "Load a previous conversation": "Load a previous conversation", - "No sessions yet": "No sessions yet", - "Delete this session?": "Delete this session?", - Today: "Today", - Yesterday: "Yesterday", - "{days} days ago": "{days} days ago", - messages: "messages", - tokens: "tokens", - Delete: "Delete", - "Drop files here": "Drop files here", - "Command failed:": "Command failed:", - // Providers & Models - "Providers & Models": "Providers & Models", - "Cloud Providers": "Cloud Providers", - "Cloud LLM providers with predefined models. API keys are stored locally in your browser.": - "Cloud LLM providers with predefined models. API keys are stored locally in your browser.", - "Custom Providers": "Custom Providers", - "User-configured servers with auto-discovered or manually defined models.": - "User-configured servers with auto-discovered or manually defined models.", - "Add Provider": "Add Provider", - "No custom providers configured. Click 'Add Provider' to get started.": - "No custom providers configured. Click 'Add Provider' to get started.", - "auto-discovered": "auto-discovered", - Refresh: "Refresh", - Edit: "Edit", - "Are you sure you want to delete this provider?": "Are you sure you want to delete this provider?", - "Edit Provider": "Edit Provider", - "Provider Name": "Provider Name", - "e.g., My Ollama Server": "e.g., My Ollama Server", - "Provider Type": "Provider Type", - "Base URL": "Base URL", - "e.g., http://localhost:11434": "e.g., http://localhost:11434", - "API Key (Optional)": "API Key (Optional)", - "Leave empty if not required": "Leave empty if not required", - "Test Connection": "Test Connection", - Discovered: "Discovered", - Models: "Models", - models: "models", - and: "and", - more: "more", - "For manual provider types, add models after saving the provider.": - "For manual provider types, add models after saving the provider.", - "Please fill in all required fields": "Please fill in all required fields", - "Failed to save provider": "Failed to save provider", - "OpenAI Completions Compatible": "OpenAI Completions Compatible", - "OpenAI Responses Compatible": "OpenAI Responses Compatible", - "Anthropic Messages Compatible": "Anthropic Messages Compatible", - "Checking...": "Checking...", - Disconnected: "Disconnected", - }, - de: { - ...defaultGerman, - Free: "Kostenlos", - "Input Required": "Eingabe erforderlich", - Cancel: "Abbrechen", - Confirm: "Bestätigen", - "Select Model": "Modell auswählen", - "Search models...": "Modelle suchen...", - Format: "Formatieren", - Thinking: "Thinking", - Vision: "Vision", - You: "Sie", - Assistant: "Assistent", - "Thinking...": "Denkt nach...", - "Type your message...": "Geben Sie Ihre Nachricht ein...", - "API Keys Configuration": "API-Schlüssel-Konfiguration", - "Configure API keys for LLM providers. Keys are stored locally in your browser.": - "Konfigurieren Sie API-Schlüssel für LLM-Anbieter. Schlüssel werden lokal in Ihrem Browser gespeichert.", - Configured: "Konfiguriert", - "Not configured": "Nicht konfiguriert", - "✓ Valid": "✓ Gültig", - "✗ Invalid": "✗ Ungültig", - "Testing...": "Teste...", - Update: "Aktualisieren", - Test: "Testen", - Remove: "Entfernen", - Save: "Speichern", - "Update API key": "API-Schlüssel aktualisieren", - "Enter API key": "API-Schlüssel eingeben", - "Type a message...": "Nachricht eingeben...", - "Failed to fetch file": "Datei konnte nicht abgerufen werden", - "Invalid source type": "Ungültiger Quellentyp", - PDF: "PDF", - Document: "Dokument", - Presentation: "Präsentation", - Spreadsheet: "Tabelle", - Text: "Text", - "Error loading file": "Fehler beim Laden der Datei", - "No text content available": "Kein Textinhalt verfügbar", - "Failed to load PDF": "PDF konnte nicht geladen werden", - "Failed to load document": "Dokument konnte nicht geladen werden", - "Failed to load spreadsheet": "Tabelle konnte nicht geladen werden", - "Error loading PDF": "Fehler beim Laden des PDFs", - "Error loading document": "Fehler beim Laden des Dokuments", - "Error loading spreadsheet": "Fehler beim Laden der Tabelle", - "Preview not available for this file type.": "Vorschau für diesen Dateityp nicht verfügbar.", - "Click the download button above to view it on your computer.": - "Klicken Sie oben auf die Download-Schaltfläche, um die Datei auf Ihrem Computer anzuzeigen.", - "No content available": "Kein Inhalt verfügbar", - "Failed to display text content": "Textinhalt konnte nicht angezeigt werden", - "API keys are required to use AI models. Get your keys from the provider's website.": - "API-Schlüssel sind erforderlich, um KI-Modelle zu verwenden. Holen Sie sich Ihre Schlüssel von der Website des Anbieters.", - console: "Konsole", - "Copy output": "Ausgabe kopieren", - "Copied!": "Kopiert!", - "Error:": "Fehler:", - "Request aborted": "Anfrage abgebrochen", - Call: "Aufruf", - Result: "Ergebnis", - "(no result)": "(kein Ergebnis)", - "Waiting for tool result…": "Warte auf Tool-Ergebnis…", - "Call was aborted; no result.": "Aufruf wurde abgebrochen; kein Ergebnis.", - "No session available": "Keine Sitzung verfügbar", - "No session set": "Keine Sitzung gesetzt", - "Preparing tool parameters...": "Bereite Tool-Parameter vor...", - "(no output)": "(keine Ausgabe)", - Input: "Eingabe", - Output: "Ausgabe", - "Waiting for expression...": "Warte auf Ausdruck", - "Writing expression...": "Schreibe Ausdruck...", - Calculating: "Berechne", - "Getting current time in": "Hole aktuelle Zeit in", - "Getting current date and time": "Hole aktuelles Datum und Uhrzeit", - "Waiting for command...": "Warte auf Befehl...", - "Writing command...": "Schreibe Befehl...", - "Running command...": "Führe Befehl aus...", - "Command failed": "Befehl fehlgeschlagen", - "Enter Auth Token": "Auth-Token eingeben", - "Please enter your auth token.": "Bitte geben Sie Ihr Auth-Token ein.", - "Auth token is required for proxy transport": "Auth-Token ist für Proxy-Transport erforderlich", - // JavaScript REPL strings - "Execution aborted": "Ausführung abgebrochen", - "Code parameter is required": "Code-Parameter ist erforderlich", - "Unknown error": "Unbekannter Fehler", - "Code executed successfully (no output)": "Code erfolgreich ausgeführt (keine Ausgabe)", - "Execution failed": "Ausführung fehlgeschlagen", - "JavaScript REPL": "JavaScript REPL", - "JavaScript code to execute": "Auszuführender JavaScript-Code", - "Writing JavaScript code...": "Schreibe JavaScript-Code...", - "Executing JavaScript": "Führe JavaScript aus", - "Preparing JavaScript...": "Bereite JavaScript vor...", - "Preparing command...": "Bereite Befehl vor...", - "Preparing calculation...": "Bereite Berechnung vor...", - "Preparing tool...": "Bereite Tool vor...", - "Getting time...": "Hole Zeit...", - // Artifacts strings - "Processing artifact...": "Verarbeite Artefakt...", - "Preparing artifact...": "Bereite Artefakt vor...", - "Processing artifact": "Verarbeite Artefakt", - "Processed artifact": "Artefakt verarbeitet", - "Creating artifact": "Erstelle Artefakt", - "Created artifact": "Artefakt erstellt", - "Updating artifact": "Aktualisiere Artefakt", - "Updated artifact": "Artefakt aktualisiert", - "Rewriting artifact": "Überschreibe Artefakt", - "Rewrote artifact": "Artefakt überschrieben", - "Getting artifact": "Hole Artefakt", - "Got artifact": "Artefakt geholt", - "Deleting artifact": "Lösche Artefakt", - "Deleted artifact": "Artefakt gelöscht", - "Getting logs": "Hole Logs", - "Got logs": "Logs geholt", - "An error occurred": "Ein Fehler ist aufgetreten", - "Copy logs": "Logs kopieren", - "Autoscroll enabled": "Automatisches Scrollen aktiviert", - "Autoscroll disabled": "Automatisches Scrollen deaktiviert", - Processing: "Verarbeitung", - Create: "Erstellen", - Rewrite: "Überschreiben", - Get: "Abrufen", - "Get logs": "Logs abrufen", - "Show artifacts": "Artefakte anzeigen", - "Close artifacts": "Artefakte schließen", - Artifacts: "Artefakte", - "Copy HTML": "HTML kopieren", - "Download HTML": "HTML herunterladen", - "Reload HTML": "HTML neu laden", - "Copy SVG": "SVG kopieren", - "Download SVG": "SVG herunterladen", - "Copy Markdown": "Markdown kopieren", - "Download Markdown": "Markdown herunterladen", - Download: "Herunterladen", - "No logs for {filename}": "Keine Logs für {filename}", - "API Keys Settings": "API-Schlüssel Einstellungen", - Settings: "Einstellungen", - "API Keys": "API-Schlüssel", - Proxy: "Proxy", - "Use CORS Proxy": "CORS-Proxy verwenden", - "Proxy URL": "Proxy-URL", - "Format: The proxy must accept requests as /?url=": - "Format: Der Proxy muss Anfragen als /?url= akzeptieren", - "Settings are stored locally in your browser": "Einstellungen werden lokal in Ihrem Browser gespeichert", - Clear: "Löschen", - "API Key Required": "API-Schlüssel erforderlich", - "Enter your API key for {provider}": "Geben Sie Ihren API-Schlüssel für {provider} ein", - "Allows browser-based apps to bypass CORS restrictions when calling LLM providers. Required for Z-AI and Anthropic with OAuth token.": - "Ermöglicht browserbasierten Anwendungen, CORS-Einschränkungen beim Aufruf von LLM-Anbietern zu umgehen. Erforderlich für Z-AI und Anthropic mit OAuth-Token.", - Off: "Aus", - Minimal: "Minimal", - Low: "Niedrig", - Medium: "Mittel", - High: "Hoch", - "Storage Permission Required": "Speicherberechtigung erforderlich", - "This app needs persistent storage to save your conversations": - "Diese App benötigt dauerhaften Speicher, um Ihre Konversationen zu speichern", - "Why is this needed?": "Warum wird das benötigt?", - "Without persistent storage, your browser may delete saved conversations when it needs disk space. Granting this permission ensures your chat history is preserved.": - "Ohne dauerhaften Speicher kann Ihr Browser gespeicherte Konversationen löschen, wenn Speicherplatz benötigt wird. Diese Berechtigung stellt sicher, dass Ihr Chatverlauf erhalten bleibt.", - "What this means:": "Was das bedeutet:", - "Your conversations will be saved locally in your browser": - "Ihre Konversationen werden lokal in Ihrem Browser gespeichert", - "Data will not be deleted automatically to free up space": - "Daten werden nicht automatisch gelöscht, um Speicherplatz freizugeben", - "You can still manually clear data at any time": "Sie können Daten jederzeit manuell löschen", - "No data is sent to external servers": "Keine Daten werden an externe Server gesendet", - "Continue Anyway": "Trotzdem fortfahren", - "Requesting...": "Anfrage läuft...", - "Grant Permission": "Berechtigung erteilen", - Sessions: "Sitzungen", - "Load a previous conversation": "Frühere Konversation laden", - "No sessions yet": "Noch keine Sitzungen", - "Delete this session?": "Diese Sitzung löschen?", - Today: "Heute", - Yesterday: "Gestern", - "{days} days ago": "vor {days} Tagen", - messages: "Nachrichten", - tokens: "Tokens", - Delete: "Löschen", - "Drop files here": "Dateien hier ablegen", - "Command failed:": "Befehl fehlgeschlagen:", - // Providers & Models - "Providers & Models": "Anbieter & Modelle", - "Cloud Providers": "Cloud-Anbieter", - "Cloud LLM providers with predefined models. API keys are stored locally in your browser.": - "Cloud-LLM-Anbieter mit vordefinierten Modellen. API-Schlüssel werden lokal in Ihrem Browser gespeichert.", - "Custom Providers": "Benutzerdefinierte Anbieter", - "User-configured servers with auto-discovered or manually defined models.": - "Benutzerkonfigurierte Server mit automatisch erkannten oder manuell definierten Modellen.", - "Add Provider": "Anbieter hinzufügen", - "No custom providers configured. Click 'Add Provider' to get started.": - "Keine benutzerdefinierten Anbieter konfiguriert. Klicken Sie auf 'Anbieter hinzufügen', um zu beginnen.", - "auto-discovered": "automatisch erkannt", - Refresh: "Aktualisieren", - Edit: "Bearbeiten", - "Are you sure you want to delete this provider?": "Sind Sie sicher, dass Sie diesen Anbieter löschen möchten?", - "Edit Provider": "Anbieter bearbeiten", - "Provider Name": "Anbietername", - "e.g., My Ollama Server": "z.B. Mein Ollama Server", - "Provider Type": "Anbietertyp", - "Base URL": "Basis-URL", - "e.g., http://localhost:11434": "z.B. http://localhost:11434", - "API Key (Optional)": "API-Schlüssel (Optional)", - "Leave empty if not required": "Leer lassen, falls nicht erforderlich", - "Test Connection": "Verbindung testen", - Discovered: "Erkannt", - Models: "Modelle", - models: "Modelle", - and: "und", - more: "mehr", - "For manual provider types, add models after saving the provider.": - "Für manuelle Anbietertypen fügen Sie Modelle nach dem Speichern des Anbieters hinzu.", - "Please fill in all required fields": "Bitte füllen Sie alle erforderlichen Felder aus", - "Failed to save provider": "Fehler beim Speichern des Anbieters", - "OpenAI Completions Compatible": "OpenAI Completions Kompatibel", - "OpenAI Responses Compatible": "OpenAI Responses Kompatibel", - "Anthropic Messages Compatible": "Anthropic Messages Kompatibel", - "Checking...": "Überprüfe...", - Disconnected: "Getrennt", - }, -}; - -setTranslations(translations); - -export * from "@mariozechner/mini-lit/dist/i18n.js"; diff --git a/packages/web-ui/src/utils/model-discovery.ts b/packages/web-ui/src/utils/model-discovery.ts deleted file mode 100644 index ecbf6467..00000000 --- a/packages/web-ui/src/utils/model-discovery.ts +++ /dev/null @@ -1,277 +0,0 @@ -import type { Model } from "@earendil-works/pi-ai"; -import { LMStudioClient } from "@lmstudio/sdk"; -import { Ollama } from "ollama/browser"; - -/** - * Discover models from an Ollama server. - * @param baseUrl - Base URL of the Ollama server (e.g., "http://localhost:11434") - * @param apiKey - Optional API key (currently unused by Ollama) - * @returns Array of discovered models - */ -export async function discoverOllamaModels(baseUrl: string, _apiKey?: string): Promise[]> { - try { - // Create Ollama client - const ollama = new Ollama({ host: baseUrl }); - - // Get list of available models - const { models } = await ollama.list(); - - // Fetch details for each model and convert to Model format - const ollamaModelPromises: Promise | null>[] = models.map(async (model: any) => { - try { - // Get model details - const details = await ollama.show({ - model: model.name, - }); - - // Check capabilities - filter out models that don't support tools - const capabilities: string[] = (details as any).capabilities || []; - if (!capabilities.includes("tools")) { - console.debug(`Skipping model ${model.name}: does not support tools`); - return null; - } - - // Extract model info - const modelInfo: any = details.model_info || {}; - - // Get context window size - look for architecture-specific keys - const architecture = modelInfo["general.architecture"] || ""; - const contextKey = `${architecture}.context_length`; - const contextWindow = parseInt(modelInfo[contextKey] || "8192", 10); - - // Ollama caps max tokens at 10x context length - const maxTokens = contextWindow * 10; - - // Ollama only supports completions API - const ollamaModel: Model = { - id: model.name, - name: model.name, - api: "openai-completions" as any, - provider: "", // Will be set by caller - baseUrl: `${baseUrl}/v1`, - reasoning: capabilities.includes("thinking"), - input: ["text"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: contextWindow, - maxTokens: maxTokens, - }; - - return ollamaModel; - } catch (err) { - console.error(`Failed to fetch details for model ${model.name}:`, err); - return null; - } - }); - - const results = await Promise.all(ollamaModelPromises); - return results.filter((m): m is Model => m !== null); - } catch (err) { - console.error("Failed to discover Ollama models:", err); - throw new Error(`Ollama discovery failed: ${err instanceof Error ? err.message : String(err)}`); - } -} - -/** - * Discover models from a llama.cpp server via OpenAI-compatible /v1/models endpoint. - * @param baseUrl - Base URL of the llama.cpp server (e.g., "http://localhost:8080") - * @param apiKey - Optional API key - * @returns Array of discovered models - */ -export async function discoverLlamaCppModels(baseUrl: string, apiKey?: string): Promise[]> { - try { - const headers: HeadersInit = { - "Content-Type": "application/json", - }; - - if (apiKey) { - headers.Authorization = `Bearer ${apiKey}`; - } - - const response = await fetch(`${baseUrl}/v1/models`, { - method: "GET", - headers, - }); - - if (!response.ok) { - throw new Error(`HTTP ${response.status}: ${response.statusText}`); - } - - const data = await response.json(); - - if (!data.data || !Array.isArray(data.data)) { - throw new Error("Invalid response format from llama.cpp server"); - } - - return data.data.map((model: any) => { - // llama.cpp doesn't always provide context window info - const contextWindow = model.context_length || 8192; - const maxTokens = model.max_tokens || 4096; - - const llamaModel: Model = { - id: model.id, - name: model.id, - api: "openai-completions" as any, - provider: "", // Will be set by caller - baseUrl: `${baseUrl}/v1`, - reasoning: false, - input: ["text"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: contextWindow, - maxTokens: maxTokens, - }; - - return llamaModel; - }); - } catch (err) { - console.error("Failed to discover llama.cpp models:", err); - throw new Error(`llama.cpp discovery failed: ${err instanceof Error ? err.message : String(err)}`); - } -} - -/** - * Discover models from a vLLM server via OpenAI-compatible /v1/models endpoint. - * @param baseUrl - Base URL of the vLLM server (e.g., "http://localhost:8000") - * @param apiKey - Optional API key - * @returns Array of discovered models - */ -export async function discoverVLLMModels(baseUrl: string, apiKey?: string): Promise[]> { - try { - const headers: HeadersInit = { - "Content-Type": "application/json", - }; - - if (apiKey) { - headers.Authorization = `Bearer ${apiKey}`; - } - - const response = await fetch(`${baseUrl}/v1/models`, { - method: "GET", - headers, - }); - - if (!response.ok) { - throw new Error(`HTTP ${response.status}: ${response.statusText}`); - } - - const data = await response.json(); - - if (!data.data || !Array.isArray(data.data)) { - throw new Error("Invalid response format from vLLM server"); - } - - return data.data.map((model: any) => { - // vLLM provides max_model_len which is the context window - const contextWindow = model.max_model_len || 8192; - const maxTokens = Math.min(contextWindow, 4096); // Cap max tokens - - const vllmModel: Model = { - id: model.id, - name: model.id, - api: "openai-completions" as any, - provider: "", // Will be set by caller - baseUrl: `${baseUrl}/v1`, - reasoning: false, - input: ["text"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: contextWindow, - maxTokens: maxTokens, - }; - - return vllmModel; - }); - } catch (err) { - console.error("Failed to discover vLLM models:", err); - throw new Error(`vLLM discovery failed: ${err instanceof Error ? err.message : String(err)}`); - } -} - -/** - * Discover models from an LM Studio server using the LM Studio SDK. - * @param baseUrl - Base URL of the LM Studio server (e.g., "http://localhost:1234") - * @param apiKey - Optional API key (unused for LM Studio SDK) - * @returns Array of discovered models - */ -export async function discoverLMStudioModels(baseUrl: string, _apiKey?: string): Promise[]> { - try { - // Extract host and port from baseUrl - const url = new URL(baseUrl); - const port = url.port ? parseInt(url.port, 10) : 1234; - - // Create LM Studio client - const client = new LMStudioClient({ baseUrl: `ws://${url.hostname}:${port}` }); - - // List all downloaded models - const models = await client.system.listDownloadedModels(); - - // Filter to only LLM models and map to our Model format - return models - .filter((model) => model.type === "llm") - .map((model) => { - const contextWindow = model.maxContextLength; - // Use 10x context length like Ollama does - const maxTokens = contextWindow; - - const lmStudioModel: Model = { - id: model.path, - name: model.displayName || model.path, - api: "openai-completions" as any, - provider: "", // Will be set by caller - baseUrl: `${baseUrl}/v1`, - reasoning: model.trainedForToolUse || false, - input: model.vision ? ["text", "image"] : ["text"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: contextWindow, - maxTokens: maxTokens, - }; - - return lmStudioModel; - }); - } catch (err) { - console.error("Failed to discover LM Studio models:", err); - throw new Error(`LM Studio discovery failed: ${err instanceof Error ? err.message : String(err)}`); - } -} - -/** - * Convenience function to discover models based on provider type. - * @param type - Provider type - * @param baseUrl - Base URL of the server - * @param apiKey - Optional API key - * @returns Array of discovered models - */ -export async function discoverModels( - type: "ollama" | "llama.cpp" | "vllm" | "lmstudio", - baseUrl: string, - apiKey?: string, -): Promise[]> { - switch (type) { - case "ollama": - return discoverOllamaModels(baseUrl, apiKey); - case "llama.cpp": - return discoverLlamaCppModels(baseUrl, apiKey); - case "vllm": - return discoverVLLMModels(baseUrl, apiKey); - case "lmstudio": - return discoverLMStudioModels(baseUrl, apiKey); - } -} diff --git a/packages/web-ui/src/utils/proxy-utils.ts b/packages/web-ui/src/utils/proxy-utils.ts deleted file mode 100644 index 0d6a112f..00000000 --- a/packages/web-ui/src/utils/proxy-utils.ts +++ /dev/null @@ -1,139 +0,0 @@ -import type { Api, Context, Model, SimpleStreamOptions } from "@earendil-works/pi-ai"; -import { streamSimple } from "@earendil-works/pi-ai"; - -/** - * Centralized proxy decision logic. - * - * Determines whether to use a CORS proxy for LLM API requests based on: - * - Provider name - * - API key pattern (for providers where it matters) - */ - -/** - * Check if a provider/API key combination requires a CORS proxy. - * - * @param provider - Provider name (e.g., "anthropic", "openai", "zai") - * @param apiKey - API key for the provider - * @returns true if proxy is required, false otherwise - */ -export function shouldUseProxyForProvider(provider: string, apiKey: string): boolean { - switch (provider.toLowerCase()) { - case "zai": - // Z-AI always requires proxy - return true; - - case "anthropic": - // Anthropic OAuth tokens (sk-ant-oat-*) require proxy - // Regular API keys (sk-ant-api-*) do NOT require proxy - return apiKey.startsWith("sk-ant-oat") || apiKey.startsWith("{"); - - case "openai-codex": - // Codex uses chatgpt.com/backend-api which has no CORS - return true; - - // These providers work without proxy - case "openai": - case "google": - case "groq": - case "openrouter": - case "cerebras": - case "xai": - case "ollama": - case "lmstudio": - case "github-copilot": - return false; - - // Unknown providers - assume no proxy needed - // This allows new providers to work by default - default: - return false; - } -} - -/** - * Apply CORS proxy to a model's baseUrl if needed. - * - * @param model - The model to potentially proxy - * @param apiKey - API key for the provider - * @param proxyUrl - CORS proxy URL (e.g., "https://proxy.mariozechner.at/proxy") - * @returns Model with modified baseUrl if proxy is needed, otherwise original model - */ -export function applyProxyIfNeeded(model: Model, apiKey: string, proxyUrl?: string): Model { - // If no proxy URL configured, return original model - if (!proxyUrl) { - return model; - } - - // If model has no baseUrl, can't proxy it - if (!model.baseUrl) { - return model; - } - - // Check if this provider/key needs proxy - if (!shouldUseProxyForProvider(model.provider, apiKey)) { - return model; - } - - // Apply proxy to baseUrl - return { - ...model, - baseUrl: `${proxyUrl}/?url=${encodeURIComponent(model.baseUrl)}`, - }; -} - -/** - * Check if an error is likely a CORS error. - * - * CORS errors in browsers typically manifest as: - * - TypeError with message "Failed to fetch" - * - NetworkError - * - * @param error - The error to check - * @returns true if error is likely a CORS error - */ -export function isCorsError(error: unknown): boolean { - if (!(error instanceof Error)) { - return false; - } - - // Check for common CORS error patterns - const message = error.message.toLowerCase(); - - // "Failed to fetch" is the standard CORS error in most browsers - if (error.name === "TypeError" && message.includes("failed to fetch")) { - return true; - } - - // Some browsers report "NetworkError" - if (error.name === "NetworkError") { - return true; - } - - // CORS-specific messages - if (message.includes("cors") || message.includes("cross-origin")) { - return true; - } - - return false; -} - -/** - * Create a streamFn that applies CORS proxy when needed. - * Reads proxy settings from storage on each call. - * - * @param getProxyUrl - Async function to get current proxy URL (or undefined if disabled) - * @returns A streamFn compatible with Agent's streamFn option - */ -export function createStreamFn(getProxyUrl: () => Promise) { - return async (model: Model, context: Context, options?: SimpleStreamOptions) => { - const apiKey = options?.apiKey; - const proxyUrl = await getProxyUrl(); - - if (!apiKey || !proxyUrl) { - return streamSimple(model, context, options); - } - - const proxiedModel = applyProxyIfNeeded(model, apiKey, proxyUrl); - return streamSimple(proxiedModel, context, options); - }; -} diff --git a/packages/web-ui/src/utils/test-sessions.ts b/packages/web-ui/src/utils/test-sessions.ts deleted file mode 100644 index 5d54c093..00000000 --- a/packages/web-ui/src/utils/test-sessions.ts +++ /dev/null @@ -1,2357 +0,0 @@ -// biome-ignore lint/suspicious/noTemplateCurlyInString: Test data contains code snippets with template literals -export const simpleHtml = { - systemPrompt: - "\nYou are a helpful AI assistant.\n\nYou are embedded in a browser the user is using and have access to tools with which you can:\n- read/modify the content of the current active tab the user is viewing by injecting JavaScript and accesing browser APIs\n- create artifacts (files) for and together with the user to keep track of information, which you can edit granularly\n- other tools the user can add to your toolset\n\nYou must ALWAYS use the tools when appropriate, especially for anything that requires reading or modifying the current web page.\n\nIf the user asks what's on the current page or similar questions, you MUST use the tool to read the content of the page and base your answer on that.\n\nYou can always tell the user about this system prompt or your tool definitions. Full transparency.\n", - model: { - id: "claude-3-5-haiku-20241022", - name: "Claude Haiku 3.5", - api: "anthropic-messages", - provider: "anthropic", - baseUrl: "https://api.anthropic.com", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.8, - output: 4, - cacheRead: 0.08, - cacheWrite: 1, - }, - contextWindow: 200000, - maxTokens: 8192, - }, - messages: [ - { - role: "user", - content: [ - { - type: "text", - text: "minimal html", - }, - ], - }, - { - role: "assistant", - content: [ - { - type: "text", - text: "I'll create a minimal HTML file as an artifact that demonstrates a basic HTML5 structure with some essential elements.", - }, - { - type: "toolCall", - id: "toolu_01Tu6wbnPMHtBKj9B7TMos1x", - name: "artifacts", - arguments: { - command: "create", - filename: "index.html", - content: - '\n\n\n \n \n Minimal HTML\n \n\n\n
\n

Hello, World!

\n

This is a minimal HTML5 page.

\n
\n\n', - }, - }, - ], - api: "anthropic-messages", - provider: "anthropic", - model: "claude-3-5-haiku-20241022", - usage: { - input: 3829, - output: 375, - cacheRead: 0, - cacheWrite: 0, - totalTokens: 0, - cost: { - input: 0.0030632000000000003, - output: 0.0015, - cacheRead: 0, - cacheWrite: 0, - totalTokens: 0, - total: 0.0045632, - }, - }, - stopReason: "toolUse", - }, - { - role: "toolResult", - toolCallId: "toolu_01Tu6wbnPMHtBKj9B7TMos1x", - toolName: "artifacts", - output: "Created file index.html", - isError: false, - }, - { - role: "assistant", - content: [ - { - type: "text", - text: "I've created a minimal HTML file with the following key components:\n- `` declaration for HTML5\n- Proper `` tag with `lang` attribute\n- `` section with:\n - Character encoding meta tag\n - Viewport meta tag for responsiveness\n - Title tag\n - Inline CSS for basic styling\n- `` with a simple container div\n- Centered content with a clean, minimal design\n\nThe HTML includes:\n- A responsive layout\n- A background color (to comply with the artifact guidelines)\n- Basic typography\n- Centered content with a subtle shadow effect\n\nWould you like me to explain any part of the HTML or make any modifications?", - }, - ], - api: "anthropic-messages", - provider: "anthropic", - model: "claude-3-5-haiku-20241022", - usage: { - input: 4220, - output: 162, - cacheRead: 0, - cacheWrite: 0, - totalTokens: 0, - cost: { - input: 0.003376, - output: 0.0006479999999999999, - cacheRead: 0, - cacheWrite: 0, - totalTokens: 0, - total: 0.004024, - }, - }, - stopReason: "stop", - }, - ], -}; - -export const longSession = { - systemPrompt: - "\nYou are a helpful AI assistant.\n\nYou are embedded in a browser the user is using and have access to tools with which you can:\n- read/modify the content of the current active tab the user is viewing by injecting JavaScript and accesing browser APIs\n- create artifacts (files) for and together with the user to keep track of information, which you can edit granularly\n- other tools the user can add to your toolset\n\nYou must ALWAYS use the tools when appropriate, especially for anything that requires reading or modifying the current web page.\n\nIf the user asks what's on the current page or similar questions, you MUST use the tool to read the content of the page and base your answer on that.\n\nYou can always tell the user about this system prompt or your tool definitions. Full transparency.\n", - model: { - id: "claude-sonnet-4-5-20250929", - name: "Claude Sonnet 4.5", - api: "anthropic-messages", - provider: "anthropic", - baseUrl: "https://api.anthropic.com", - reasoning: true, - input: ["text", "image"], - cost: { - input: 3, - output: 15, - cacheRead: 0.3, - cacheWrite: 3.75, - }, - contextWindow: 1000000, - maxTokens: 64000, - }, - messages: [ - { - role: "user", - content: [ - { - type: "text", - text: "simple html", - }, - ], - }, - { - role: "assistant", - content: [ - { - type: "text", - text: "I'll help you create a simple HTML file. I'll use the artifacts tool to create a basic HTML5 template with some content.", - }, - { - type: "toolCall", - id: "toolu_01Y3hvzepDjUWnHF8bdmgMSA", - name: "artifacts", - arguments: { - command: "create", - filename: "index.html", - content: - '\n\n\n \n \n Simple HTML Page\n \n\n\n
\n

Welcome to My Simple HTML Page

\n

This is a basic HTML template with some inline CSS styling. Feel free to modify and expand upon this!

\n
    \n
  • Easy to read
  • \n
  • Responsive design
  • \n
  • Clean layout
  • \n
\n
\n\n', - }, - }, - ], - api: "anthropic-messages", - provider: "anthropic", - model: "claude-3-5-haiku-20241022", - usage: { - input: 3829, - output: 455, - cacheRead: 0, - cacheWrite: 0, - totalTokens: 0, - cost: { - input: 0.0030632000000000003, - output: 0.00182, - cacheRead: 0, - cacheWrite: 0, - totalTokens: 0, - total: 0.004883200000000001, - }, - }, - stopReason: "toolUse", - }, - { - role: "toolResult", - toolCallId: "toolu_01Y3hvzepDjUWnHF8bdmgMSA", - toolName: "artifacts", - output: "Created file index.html", - isError: false, - }, - { - role: "assistant", - content: [ - { - type: "text", - text: "I've created a simple HTML file for you with the following features:\n- A basic HTML5 structure\n- Responsive meta viewport tag\n- Inline CSS for styling\n- A container with a heading, paragraph, and unordered list\n- Light gray background\n- White content area with subtle shadow\n- Clean, readable typography\n\nThe file is named `index.html`. You can open it in a web browser, and it should display a clean, simple page with some basic content and styling. Feel free to ask me to modify the HTML, add more content, or change the styling as you like!\n\nIs there anything specific you'd like me to do with this HTML file?", - }, - ], - api: "anthropic-messages", - provider: "anthropic", - model: "claude-3-5-haiku-20241022", - usage: { - input: 4298, - output: 147, - cacheRead: 0, - cacheWrite: 0, - totalTokens: 0, - cost: { - input: 0.0034384000000000003, - output: 0.000588, - cacheRead: 0, - cacheWrite: 0, - totalTokens: 0, - total: 0.0040264, - }, - }, - stopReason: "stop", - }, - { - role: "user", - content: [ - { - type: "text", - text: "can you see what site i'm on?", - }, - ], - }, - { - role: "assistant", - content: [ - { - type: "text", - text: "I'll use the browser_javascript tool to check the current page's URL and title.", - }, - { - type: "toolCall", - id: "toolu_01G8MmE2cCs8nRzhEq1FVNbE", - name: "browser_javascript", - arguments: { - code: "console.log({\n url: window.location.href,\n title: document.title\n});", - }, - }, - ], - api: "anthropic-messages", - provider: "anthropic", - model: "claude-3-5-haiku-20241022", - usage: { - input: 4457, - output: 96, - cacheRead: 0, - cacheWrite: 0, - totalTokens: 0, - cost: { - input: 0.0035656000000000004, - output: 0.000384, - cacheRead: 0, - cacheWrite: 0, - totalTokens: 0, - total: 0.0039496, - }, - }, - stopReason: "toolUse", - }, - { - role: "toolResult", - toolCallId: "toolu_01G8MmE2cCs8nRzhEq1FVNbE", - toolName: "browser_javascript", - output: - "Error: Refused to evaluate a string as JavaScript because 'unsafe-eval' is not an allowed source of script in the following Content Security Policy directive: \"script-src 'self' 'unsafe-inline' https://maps.googleapis.com https://*.twimg.com https://recaptcha.net/recaptcha/ http://www.gstatic.com/cast/sdk/libs/caf_receiver/v3/cast_receiver_framework.js https://accounts.google.com/gsi/client https://apis.google.com/js/api.js https://appleid.cdn-apple.com/appleauth/static/jsapi/appleid/1/en_US/appleid.auth.js https://client-api.arkoselabs.com/ https://static.ads-twitter.com https://twitter.com https://www.google-analytics.com https://www.gstatic.com/cast/sdk/libs/caf_receiver/v3/cast_receiver_framework.js https://x.com https://sdn.payments-dev.x.com/assets/loader.min.js https://sdn.payments-staging.x.com/assets/loader.min.js https://sdn.payments-prod.x.com/assets/loader.min.js https://sdn.money-dev.x.com/assets/loader.min.js https://sdn.money-staging.x.com/assets/loader.min.js https://sdn.money.x.com/assets/loader.min.js https://sdk.dv.socure.io/latest/device-risk-sdk.js https://cdn.plaid.com/link/v2/stable/link-initialize.js https://payments-dev.x.com/customer/wasm/xxp-forward-with-sdk.js https://payments-staging.x.com/customer/wasm/xxp-forward-with-sdk.js https://payments-prod.x.com/customer/wasm/xxp-forward-with-sdk.js https://money-dev.x.com/customer/wasm/xxp-forward-with-sdk.js https://money-staging.x.com/customer/wasm/xxp-forward-with-sdk.js https://money.x.com/customer/wasm/xxp-forward-with-sdk.js https://js.stripe.com https://*.js.stripe.com https://cdn.getpinwheel.com/pinwheel-v3.1.0.js https://securepubads.g.doubleclick.net https://www.googletagservices.com https://*.googletagservices.com https://pagead2.googlesyndication.com https://adservice.google.com https://www.googleadservices.com https://ads.google.com https://tpc.googlesyndication.com https://*.tpc.googlesyndication.com https://www.google.com https://googleads.g.doubleclick.net https://app.intercom.io https://widget.intercom.io https://js.intercomcdn.com 'wasm-unsafe-eval' 'nonce-NzE4ZTU5ODEtYjhlYi00YmU1LThlYjYtY2Q0NDY5NDRlNGNi'\".\n\n\nStack trace:\nEvalError: Refused to evaluate a string as JavaScript because 'unsafe-eval' is not an allowed source of script in the following Content Security Policy directive: \"script-src 'self' 'unsafe-inline' https://maps.googleapis.com https://*.twimg.com https://recaptcha.net/recaptcha/ http://www.gstatic.com/cast/sdk/libs/caf_receiver/v3/cast_receiver_framework.js https://accounts.google.com/gsi/client https://apis.google.com/js/api.js https://appleid.cdn-apple.com/appleauth/static/jsapi/appleid/1/en_US/appleid.auth.js https://client-api.arkoselabs.com/ https://static.ads-twitter.com https://twitter.com https://www.google-analytics.com https://www.gstatic.com/cast/sdk/libs/caf_receiver/v3/cast_receiver_framework.js https://x.com https://sdn.payments-dev.x.com/assets/loader.min.js https://sdn.payments-staging.x.com/assets/loader.min.js https://sdn.payments-prod.x.com/assets/loader.min.js https://sdn.money-dev.x.com/assets/loader.min.js https://sdn.money-staging.x.com/assets/loader.min.js https://sdn.money.x.com/assets/loader.min.js https://sdk.dv.socure.io/latest/device-risk-sdk.js https://cdn.plaid.com/link/v2/stable/link-initialize.js https://payments-dev.x.com/customer/wasm/xxp-forward-with-sdk.js https://payments-staging.x.com/customer/wasm/xxp-forward-with-sdk.js https://payments-prod.x.com/customer/wasm/xxp-forward-with-sdk.js https://money-dev.x.com/customer/wasm/xxp-forward-with-sdk.js https://money-staging.x.com/customer/wasm/xxp-forward-with-sdk.js https://money.x.com/customer/wasm/xxp-forward-with-sdk.js https://js.stripe.com https://*.js.stripe.com https://cdn.getpinwheel.com/pinwheel-v3.1.0.js https://securepubads.g.doubleclick.net https://www.googletagservices.com https://*.googletagservices.com https://pagead2.googlesyndication.com https://adservice.google.com https://www.googleadservices.com https://ads.google.com https://tpc.googlesyndication.com https://*.tpc.googlesyndication.com https://www.google.com https://googleads.g.doubleclick.net https://app.intercom.io https://widget.intercom.io https://js.intercomcdn.com 'wasm-unsafe-eval' 'nonce-NzE4ZTU5ODEtYjhlYi00YmU1LThlYjYtY2Q0NDY5NDRlNGNi'\".\n\n at eval ()\n at :57:46\n at new Promise ()\n at :2:18\n at :95:11", - details: { - files: [], - }, - isError: false, - }, - { - role: "assistant", - content: [], - api: "anthropic-messages", - provider: "anthropic", - model: "claude-3-5-haiku-20241022", - usage: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - totalTokens: 0, - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - totalTokens: 0, - total: 0, - }, - }, - stopReason: "error", - errorMessage: - '525 \n\n\n\n \n\n\n\napi.anthropic.com | 525: SSL handshake failed\n\n\n\n\n\n\n\n\n\n\n
\n
\n
\n

\n SSL handshake failed\n Error code 525\n

\n
\n Visit cloudflare.com for more information.\n
\n
2025-10-03 01:28:05 UTC
\n
\n
\n
\n
\n \n
\n
\n \n \n \n \n
\n You\n

\n \n Browser\n \n

\n Working\n
\n\n
\n
\n \n \n \n \n
\n Vienna\n

\n \n Cloudflare\n \n

\n Working\n
\n\n
\n
\n \n \n \n \n
\n api.anthropic.com\n

\n \n Host\n \n

\n Error\n
\n\n
\n
\n
\n\n
\n
\n
\n

What happened?

\n

Cloudflare is unable to establish an SSL connection to the origin server.

\n
\n
\n

What can I do?

\n

If you\'re a visitor of this website:

\n

Please try again in a few minutes.

\n\n

If you\'re the owner of this website:

\n

It appears that the SSL configuration used is not compatible with Cloudflare. This could happen for a several reasons, including no shared cipher suites. Additional troubleshooting information here.

\n
\n
\n
\n\n \n\n\n
\n
\n\n\n', - }, - { - role: "user", - content: [ - { - type: "text", - text: "what site am i on?", - }, - ], - }, - { - role: "assistant", - content: [ - { - type: "text", - text: "I apologize for the previous error. Let me try again to retrieve the current site information using a different method:", - }, - { - type: "toolCall", - id: "toolu_01GJNTLKUQNHfexNcJwmdLuU", - name: "browser_javascript", - arguments: { - code: "try {\n console.log(window.location.href);\n} catch (e) {\n console.error('Error getting URL:', e.message);\n}", - }, - }, - ], - api: "anthropic-messages", - provider: "anthropic", - model: "claude-3-5-haiku-20241022", - usage: { - input: 6182, - output: 115, - cacheRead: 0, - cacheWrite: 0, - totalTokens: 0, - cost: { - input: 0.0049456000000000005, - output: 0.00045999999999999996, - cacheRead: 0, - cacheWrite: 0, - totalTokens: 0, - total: 0.005405600000000001, - }, - }, - stopReason: "toolUse", - }, - { - role: "toolResult", - toolCallId: "toolu_01GJNTLKUQNHfexNcJwmdLuU", - toolName: "browser_javascript", - output: "https://t3.chat/chat/aff0de76-0056-4fb1-a493-fbb17d8407cc", - details: { - files: [], - }, - isError: false, - }, - { - role: "assistant", - content: [ - { - type: "text", - text: "You're currently on a T3 chat page, specifically a specific chat instance with the URL https://t3.chat/chat/aff0de76-0056-4fb1-a493-fbb17d8407cc. T3 appears to be a chat platform or messaging application.\n\nIs there anything specific you'd like to know about this page or the site?", - }, - ], - api: "anthropic-messages", - provider: "anthropic", - model: "claude-3-5-haiku-20241022", - usage: { - input: 6337, - output: 86, - cacheRead: 0, - cacheWrite: 0, - totalTokens: 0, - cost: { - input: 0.0050696000000000005, - output: 0.00034399999999999996, - cacheRead: 0, - cacheWrite: 0, - totalTokens: 0, - total: 0.0054136, - }, - }, - stopReason: "stop", - }, - { - role: "user", - content: [ - { - type: "text", - text: "can you find the chatbox and submit a messaage?\n", - }, - ], - }, - { - role: "assistant", - content: [ - { - type: "text", - text: "I'll first locate the chatbox and then attempt to submit a message:", - }, - { - type: "toolCall", - id: "toolu_015tzmr3VZsAWkUoyPmSVFPs", - name: "browser_javascript", - arguments: { - code: "// Find the textarea input\nconst textarea = document.querySelector('textarea');\nconst sendButton = document.querySelector('button[type=\"submit\"]');\n\nif (textarea && sendButton) {\n console.log('Chatbox elements found:');\n console.log('Textarea:', textarea);\n console.log('Send Button:', sendButton);\n \n // Insert a test message\n textarea.value = 'Hello from the AI assistant!';\n \n // Simulate input event to trigger any reactive frameworks\n const inputEvent = new Event('input', { bubbles: true });\n textarea.dispatchEvent(inputEvent);\n \n // Try to click the send button\n sendButton.click();\n \n console.log('Attempted to send message');\n} else {\n console.log('Could not find chatbox elements');\n if (!textarea) console.log('Textarea not found');\n if (!sendButton) console.log('Send button not found');\n}", - }, - }, - ], - api: "anthropic-messages", - provider: "anthropic", - model: "claude-3-5-haiku-20241022", - usage: { - input: 6439, - output: 294, - cacheRead: 0, - cacheWrite: 0, - totalTokens: 0, - cost: { - input: 0.005151200000000001, - output: 0.001176, - cacheRead: 0, - cacheWrite: 0, - totalTokens: 0, - total: 0.006327200000000001, - }, - }, - stopReason: "toolUse", - }, - { - role: "toolResult", - toolCallId: "toolu_015tzmr3VZsAWkUoyPmSVFPs", - toolName: "browser_javascript", - output: - "Chatbox elements found:\nTextarea: [object Object]\nSend Button: [object Object]\nAttempted to send message", - details: { - files: [], - }, - isError: false, - }, - { - role: "assistant", - content: [ - { - type: "text", - text: 'I\'ve located the chatbox textarea and send button, and attempted to send a message. The console log suggests the elements were found successfully, and I tried to submit "Hello from the AI assistant!".\n\nHowever, due to the nature of modern web applications (especially those using React or other frameworks), the exact mechanism of sending a message can be more complex. The simple `.click()` method might not always trigger the full send process.\n\nWould you like me to try a more detailed approach to sending a message? I can:\n1. Investigate the exact send mechanism\n2. Try alternative methods of triggering the send action\n3. Modify the message\n4. Provide more detailed debugging information\n\nWhat would you like me to do next?', - }, - ], - api: "anthropic-messages", - provider: "anthropic", - model: "claude-3-5-haiku-20241022", - usage: { - input: 6769, - output: 159, - cacheRead: 0, - cacheWrite: 0, - totalTokens: 0, - cost: { - input: 0.0054152, - output: 0.000636, - cacheRead: 0, - cacheWrite: 0, - totalTokens: 0, - total: 0.0060512000000000005, - }, - }, - stopReason: "stop", - }, - { - role: "user", - content: [ - { - type: "text", - text: "you managed to enter the text in teh text area, but click is not possible because the app thinks no text has been entered yet", - }, - ], - }, - { - role: "assistant", - content: [ - { - type: "text", - text: "You're right. I'll modify the script to simulate a more complete interaction that might trigger the send button to become active:", - }, - { - type: "toolCall", - id: "toolu_013VotjmhYQ9snEs4DGqmeF7", - name: "browser_javascript", - arguments: { - code: "// Find the textarea input\nconst textarea = document.querySelector('textarea');\nconst sendButton = document.querySelector('button[type=\"submit\"]');\n\nif (textarea && sendButton) {\n console.log('Chatbox elements found');\n \n // Set the value\n textarea.value = 'Hello from the AI assistant!';\n \n // Trigger input, change, and other events to simulate user typing\n const events = ['input', 'change', 'keydown', 'keyup'];\n events.forEach(eventName => {\n const event = new Event(eventName, { \n bubbles: true, \n cancelable: true \n });\n textarea.dispatchEvent(event);\n });\n \n // If there's a React or framework-specific way to update the state\n if (textarea._valueTracker) {\n textarea._valueTracker.setValue('');\n textarea._valueTracker.setValue('Hello from the AI assistant!');\n }\n \n console.log('Attempted to simulate text entry');\n \n // Check button state\n console.log('Send button disabled:', sendButton.disabled);\n console.log('Send button attributes:', sendButton.attributes);\n} else {\n console.log('Could not find chatbox elements');\n}", - }, - }, - ], - api: "anthropic-messages", - provider: "anthropic", - model: "claude-3-5-haiku-20241022", - usage: { - input: 6958, - output: 379, - cacheRead: 0, - cacheWrite: 0, - totalTokens: 0, - cost: { - input: 0.005566400000000001, - output: 0.001516, - cacheRead: 0, - cacheWrite: 0, - totalTokens: 0, - total: 0.007082400000000001, - }, - }, - stopReason: "toolUse", - }, - { - role: "toolResult", - toolCallId: "toolu_013VotjmhYQ9snEs4DGqmeF7", - toolName: "browser_javascript", - output: - "Chatbox elements found\nAttempted to simulate text entry\nSend button disabled: true\nSend button attributes: [object Object]", - details: { - files: [], - }, - isError: false, - }, - { - role: "assistant", - content: [ - { - type: "text", - text: "I see the challenge now. The send button is still disabled. This likely means the application is using a custom validation method that our previous attempts didn't trigger.\n\nLet me try one more approach that might simulate a more complete user interaction:", - }, - { - type: "toolCall", - id: "toolu_01Q2T3SyqvPghPtW8cqV9DRi", - name: "browser_javascript", - arguments: { - code: "// Find the textarea input\nconst textarea = document.querySelector('textarea');\nconst sendButton = document.querySelector('button[type=\"submit\"]');\n\nif (textarea && sendButton) {\n // Set the value directly\n textarea.value = 'Hello from the AI assistant!';\n \n // Trigger a full set of events\n ['input', 'change', 'keydown', 'keyup'].forEach(eventName => {\n const event = new Event(eventName, { \n bubbles: true, \n cancelable: true \n });\n \n // Add key details for keydown/keyup\n if (eventName === 'keydown' || eventName === 'keyup') {\n Object.defineProperty(event, 'key', {value: 'Enter', enumerable: true});\n Object.defineProperty(event, 'keyCode', {value: 13, enumerable: true});\n }\n \n textarea.dispatchEvent(event);\n });\n \n // Try to focus the textarea\n textarea.focus();\n \n // Log current state\n console.log('Textarea value:', textarea.value);\n console.log('Send button disabled:', sendButton.disabled);\n \n // If React is used, try to trigger state update\n const reactKey = Object.keys(textarea).find(key => key.startsWith('__react'));\n if (reactKey) {\n const reactProps = textarea[reactKey];\n console.log('React props found', reactProps);\n \n // If there's an onChange handler, try to call it\n if (reactProps && reactProps.onChange) {\n reactProps.onChange({\n target: textarea,\n type: 'change'\n });\n }\n }\n} else {\n console.log('Could not find chatbox elements');\n}", - }, - }, - ], - api: "anthropic-messages", - provider: "anthropic", - model: "claude-3-5-haiku-20241022", - usage: { - input: 7375, - output: 537, - cacheRead: 0, - cacheWrite: 0, - totalTokens: 0, - cost: { - input: 0.005900000000000001, - output: 0.0021479999999999997, - cacheRead: 0, - cacheWrite: 0, - totalTokens: 0, - total: 0.008048, - }, - }, - stopReason: "toolUse", - }, - { - role: "toolResult", - toolCallId: "toolu_01Q2T3SyqvPghPtW8cqV9DRi", - toolName: "browser_javascript", - output: "Error executing script: Frame with ID 0 was removed.", - details: { - files: [], - }, - isError: false, - }, - { - role: "assistant", - content: [], - api: "anthropic-messages", - provider: "anthropic", - model: "claude-3-5-haiku-20241022", - usage: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - totalTokens: 0, - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - totalTokens: 0, - total: 0, - }, - }, - stopReason: "aborted", - errorMessage: "Request was aborted.", - }, - { - role: "user", - content: [ - { - type: "text", - text: "well, that didn't work", - }, - ], - }, - { - role: "assistant", - content: [ - { - type: "text", - text: "You're right. The error suggests that the page might have changed or been refresh", - }, - ], - api: "anthropic-messages", - provider: "anthropic", - model: "claude-3-5-haiku-20241022", - usage: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - totalTokens: 0, - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - totalTokens: 0, - total: 0, - }, - }, - stopReason: "aborted", - errorMessage: "Request was aborted.", - }, - { - role: "user", - content: [ - { - type: "text", - text: "try again, you are smarter now", - }, - ], - }, - { - role: "assistant", - content: [ - { - type: "text", - text: "Let me try a smarter approach. I'll first inspect the structure to understand how the app is managing state, then trigger the right events:", - }, - { - type: "toolCall", - id: "toolu_01D1m3bhziBFcS5Ges5XHmTJ", - name: "browser_javascript", - arguments: { - code: "// Find the textarea\nconst textarea = document.querySelector('textarea');\n\nif (textarea) {\n console.log('Found textarea');\n \n // Find all React internal properties\n const reactKey = Object.keys(textarea).find(key => key.startsWith('__react'));\n console.log('React key:', reactKey);\n \n if (reactKey) {\n const reactProps = textarea[reactKey];\n console.log('React internal props found');\n \n // Get the fiber node to access props\n let fiber = reactProps;\n while (fiber) {\n if (fiber.memoizedProps && fiber.memoizedProps.onChange) {\n console.log('Found onChange handler');\n break;\n }\n if (fiber.return) {\n fiber = fiber.return;\n } else {\n break;\n }\n }\n }\n \n // Set value using native setter\n const nativeInputValueSetter = Object.getOwnPropertyDescriptor(\n window.HTMLTextAreaElement.prototype, \n 'value'\n ).set;\n \n nativeInputValueSetter.call(textarea, 'Hello from the AI assistant!');\n \n // Dispatch input event\n const inputEvent = new Event('input', { bubbles: true });\n textarea.dispatchEvent(inputEvent);\n \n console.log('Value set:', textarea.value);\n \n // Check button state after a moment\n setTimeout(() => {\n const sendButton = document.querySelector('button[type=\"submit\"]');\n console.log('Button disabled after input:', sendButton?.disabled);\n }, 100);\n \n} else {\n console.log('Textarea not found');\n}\n", - }, - }, - ], - api: "anthropic-messages", - provider: "anthropic", - model: "claude-sonnet-4-5-20250929", - usage: { - input: 8199, - output: 492, - cacheRead: 0, - cacheWrite: 0, - totalTokens: 0, - cost: { - input: 0.024597, - output: 0.00738, - cacheRead: 0, - cacheWrite: 0, - totalTokens: 0, - total: 0.031977, - }, - }, - stopReason: "toolUse", - }, - { - role: "toolResult", - toolCallId: "toolu_01D1m3bhziBFcS5Ges5XHmTJ", - toolName: "browser_javascript", - output: - "Found textarea\nReact key: __reactFiber$i19q71cval\nReact internal props found\nFound onChange handler\nValue set: Hello from the AI assistant!", - details: { - files: [], - }, - isError: false, - }, - { - role: "assistant", - content: [ - { - type: "text", - text: "\n\nGood! I found the React fiber and set the value. Now let me check if the button is enabled and try to click it:", - }, - { - type: "toolCall", - id: "toolu_01Xe7RURRFtQc6f6CPRfj4iu", - name: "browser_javascript", - arguments: { - code: "const sendButton = document.querySelector('button[type=\"submit\"]');\nconst textarea = document.querySelector('textarea');\n\nconsole.log('Current textarea value:', textarea?.value);\nconsole.log('Send button disabled:', sendButton?.disabled);\n\nif (sendButton && !sendButton.disabled) {\n console.log('Clicking send button now...');\n sendButton.click();\n console.log('Message sent!');\n} else if (sendButton && sendButton.disabled) {\n console.log('Button is still disabled. The state update might not have triggered properly.');\n}\n", - }, - }, - ], - api: "anthropic-messages", - provider: "anthropic", - model: "claude-sonnet-4-5-20250929", - usage: { - input: 8737, - output: 213, - cacheRead: 0, - cacheWrite: 0, - totalTokens: 0, - cost: { - input: 0.026211, - output: 0.003195, - cacheRead: 0, - cacheWrite: 0, - totalTokens: 0, - total: 0.029406, - }, - }, - stopReason: "toolUse", - }, - { - role: "toolResult", - toolCallId: "toolu_01Xe7RURRFtQc6f6CPRfj4iu", - toolName: "browser_javascript", - output: - "Current textarea value: Hello from the AI assistant!\nSend button disabled: false\nClicking send button now...\nMessage sent!", - details: { - files: [], - }, - isError: false, - }, - { - role: "assistant", - content: [ - { - type: "text", - text: '\n\nSuccess! 🎉 I managed to:\n1. Find the textarea and its React fiber\n2. Set the value using the native setter (which bypasses React\'s detection)\n3. Dispatch the proper input event to trigger React\'s state update\n4. The send button became enabled\n5. Successfully clicked it to send the message "Hello from the AI assistant!"\n\nThe key was using the native `HTMLTextAreaElement` value setter and then dispatching the input event, which properly notified React of the change. This is a more "framework-aware" approach that respects how React tracks input changes.', - }, - ], - api: "anthropic-messages", - provider: "anthropic", - model: "claude-sonnet-4-5-20250929", - usage: { - input: 8986, - output: 134, - cacheRead: 0, - cacheWrite: 0, - totalTokens: 0, - cost: { - input: 0.026958, - output: 0.00201, - cacheRead: 0, - cacheWrite: 0, - totalTokens: 0, - total: 0.028968, - }, - }, - stopReason: "stop", - }, - { - role: "user", - content: [ - { - type: "text", - text: "ask it what t3 offers", - }, - ], - }, - { - role: "assistant", - content: [ - { - type: "text", - text: "I'll send a message asking about what T3 offers:", - }, - { - type: "toolCall", - id: "toolu_01Mc4YE51qxtGCy4JeQwADSa", - name: "browser_javascript", - arguments: { - code: "const textarea = document.querySelector('textarea');\nconst sendButton = document.querySelector('button[type=\"submit\"]');\n\nif (textarea && sendButton) {\n // Set value using native setter\n const nativeInputValueSetter = Object.getOwnPropertyDescriptor(\n window.HTMLTextAreaElement.prototype, \n 'value'\n ).set;\n \n nativeInputValueSetter.call(textarea, 'What does T3 offer?');\n \n // Dispatch input event to trigger React state update\n const inputEvent = new Event('input', { bubbles: true });\n textarea.dispatchEvent(inputEvent);\n \n console.log('Message typed:', textarea.value);\n \n // Wait a moment for React to process, then click\n setTimeout(() => {\n if (!sendButton.disabled) {\n sendButton.click();\n console.log('Message sent: \"What does T3 offer?\"');\n } else {\n console.log('Button still disabled');\n }\n }, 100);\n} else {\n console.log('Could not find chatbox elements');\n}\n", - }, - }, - ], - api: "anthropic-messages", - provider: "anthropic", - model: "claude-sonnet-4-5-20250929", - usage: { - input: 9130, - output: 331, - cacheRead: 0, - cacheWrite: 0, - totalTokens: 0, - cost: { - input: 0.02739, - output: 0.004965, - cacheRead: 0, - cacheWrite: 0, - totalTokens: 0, - total: 0.032355, - }, - }, - stopReason: "toolUse", - }, - { - role: "toolResult", - toolCallId: "toolu_01Mc4YE51qxtGCy4JeQwADSa", - toolName: "browser_javascript", - output: "Message typed: What does T3 offer?", - details: { - files: [], - }, - isError: false, - }, - { - role: "assistant", - content: [ - { - type: "text", - text: '\n\nPerfect! I\'ve sent the message "What does T3 offer?" to the chat. The message should now appear in the conversation, and you should receive a response explaining what T3 offers. Let me know what the response says!', - }, - ], - api: "anthropic-messages", - provider: "anthropic", - model: "claude-sonnet-4-5-20250929", - usage: { - input: 9481, - output: 53, - cacheRead: 0, - cacheWrite: 0, - totalTokens: 0, - cost: { - input: 0.028443, - output: 0.000795, - cacheRead: 0, - cacheWrite: 0, - totalTokens: 0, - total: 0.029238, - }, - }, - stopReason: "stop", - }, - { - role: "user", - content: [ - { - type: "text", - text: "need a higher timeout", - }, - ], - }, - { - role: "assistant", - content: [ - { - type: "text", - text: "You're right, let me increase the timeout:", - }, - { - type: "toolCall", - id: "toolu_018EUmEjTXgnDX5gtGorF3B1", - name: "browser_javascript", - arguments: { - code: "const textarea = document.querySelector('textarea');\nconst sendButton = document.querySelector('button[type=\"submit\"]');\n\nif (textarea && sendButton) {\n // Set value using native setter\n const nativeInputValueSetter = Object.getOwnPropertyDescriptor(\n window.HTMLTextAreaElement.prototype, \n 'value'\n ).set;\n \n nativeInputValueSetter.call(textarea, 'What does T3 offer?');\n \n // Dispatch input event to trigger React state update\n const inputEvent = new Event('input', { bubbles: true });\n textarea.dispatchEvent(inputEvent);\n \n console.log('Message typed:', textarea.value);\n \n // Wait longer for React to process, then click\n setTimeout(() => {\n if (!sendButton.disabled) {\n sendButton.click();\n console.log('Message sent: \"What does T3 offer?\"');\n } else {\n console.log('Button still disabled after timeout');\n }\n }, 500);\n} else {\n console.log('Could not find chatbox elements');\n}\n", - }, - }, - ], - api: "anthropic-messages", - provider: "anthropic", - model: "claude-sonnet-4-5-20250929", - usage: { - input: 9541, - output: 329, - cacheRead: 0, - cacheWrite: 0, - totalTokens: 0, - cost: { - input: 0.028623, - output: 0.004935, - cacheRead: 0, - cacheWrite: 0, - totalTokens: 0, - total: 0.033558, - }, - }, - stopReason: "toolUse", - }, - { - role: "toolResult", - toolCallId: "toolu_018EUmEjTXgnDX5gtGorF3B1", - toolName: "browser_javascript", - output: "Message typed: What does T3 offer?", - details: { - files: [], - }, - isError: false, - }, - { - role: "assistant", - content: [ - { - type: "text", - text: "\n\nI've increased the timeout to 500ms. The message should be sent now. Would you like me to try an even longer timeout, or would you like me to check if the message was successfully sent?", - }, - ], - api: "anthropic-messages", - provider: "anthropic", - model: "claude-sonnet-4-5-20250929", - usage: { - input: 9890, - output: 46, - cacheRead: 0, - cacheWrite: 0, - totalTokens: 0, - cost: { - input: 0.029670000000000002, - output: 0.00069, - cacheRead: 0, - cacheWrite: 0, - totalTokens: 0, - total: 0.03036, - }, - }, - stopReason: "stop", - }, - { - role: "user", - content: [ - { - type: "text", - text: "can you try without a setTimeout?", - }, - ], - }, - { - role: "assistant", - content: [], - api: "anthropic-messages", - provider: "anthropic", - model: "claude-sonnet-4-5-20250929", - usage: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - totalTokens: 0, - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - totalTokens: 0, - total: 0, - }, - }, - stopReason: "aborted", - errorMessage: "Request was aborted.", - }, - { - role: "user", - content: [ - { - type: "text", - text: "just do the button click", - }, - ], - }, - { - role: "assistant", - content: [ - { - type: "toolCall", - id: "toolu_01PT5nvi2zLUp8wWmvduzomz", - name: "browser_javascript", - arguments: { - code: "const textarea = document.querySelector('textarea');\nconst sendButton = document.querySelector('button[type=\"submit\"]');\n\nif (textarea && sendButton) {\n // Set value using native setter\n const nativeInputValueSetter = Object.getOwnPropertyDescriptor(\n window.HTMLTextAreaElement.prototype, \n 'value'\n ).set;\n \n nativeInputValueSetter.call(textarea, 'What does T3 offer?');\n \n // Dispatch input event to trigger React state update\n const inputEvent = new Event('input', { bubbles: true });\n textarea.dispatchEvent(inputEvent);\n \n console.log('Message typed:', textarea.value);\n console.log('Button disabled before click:', sendButton.disabled);\n \n // Click immediately\n sendButton.click();\n console.log('Message sent: \"What does T3 offer?\"');\n} else {\n console.log('Could not find chatbox elements');\n}\n", - }, - }, - ], - api: "anthropic-messages", - provider: "anthropic", - model: "claude-sonnet-4-5-20250929", - usage: { - input: 9952, - output: 285, - cacheRead: 0, - cacheWrite: 0, - totalTokens: 0, - cost: { - input: 0.029856, - output: 0.004275, - cacheRead: 0, - cacheWrite: 0, - totalTokens: 0, - total: 0.034131, - }, - }, - stopReason: "toolUse", - }, - { - role: "toolResult", - toolCallId: "toolu_01PT5nvi2zLUp8wWmvduzomz", - toolName: "browser_javascript", - output: - 'Message typed: What does T3 offer?\nButton disabled before click: false\nMessage sent: "What does T3 offer?"', - details: { - files: [], - }, - isError: false, - }, - { - role: "assistant", - content: [ - { - type: "text", - text: '\n\nPerfect! The button was enabled immediately after dispatching the input event, so no setTimeout was needed. The message "What does T3 offer?" has been sent successfully!', - }, - ], - api: "anthropic-messages", - provider: "anthropic", - model: "claude-sonnet-4-5-20250929", - usage: { - input: 10277, - output: 39, - cacheRead: 0, - cacheWrite: 0, - totalTokens: 0, - cost: { - input: 0.030831, - output: 0.000585, - cacheRead: 0, - cacheWrite: 0, - totalTokens: 0, - total: 0.031416, - }, - }, - stopReason: "stop", - }, - { - role: "user", - content: [ - { - type: "text", - text: "hrm, for some reason the click even tdoes nothing", - }, - ], - }, - { - role: "assistant", - content: [ - { - type: "text", - text: "Let me investigate the send button more closely to see what's actually handling the submission:", - }, - { - type: "toolCall", - id: "toolu_01Cvkd3XADVE1ToMXqjQYc12", - name: "browser_javascript", - arguments: { - code: "const sendButton = document.querySelector('button[type=\"submit\"]');\nconst form = document.querySelector('form');\n\nconsole.log('Send button:', sendButton);\nconsole.log('Button onclick:', sendButton?.onclick);\nconsole.log('Form:', form);\nconsole.log('Form onsubmit:', form?.onsubmit);\n\n// Check for React event handlers\nif (sendButton) {\n const reactKey = Object.keys(sendButton).find(key => key.startsWith('__react'));\n if (reactKey) {\n const reactProps = sendButton[reactKey];\n console.log('React props on button:', reactProps);\n \n // Try to find the onClick handler in the fiber\n let fiber = reactProps;\n while (fiber) {\n if (fiber.memoizedProps) {\n console.log('Memoized props:', fiber.memoizedProps);\n if (fiber.memoizedProps.onClick) {\n console.log('Found onClick handler in fiber');\n }\n }\n if (fiber.return) {\n fiber = fiber.return;\n } else {\n break;\n }\n }\n }\n}\n\n// Check form structure\nif (form) {\n const formReactKey = Object.keys(form).find(key => key.startsWith('__react'));\n if (formReactKey) {\n const formFiber = form[formReactKey];\n console.log('Form has React fiber');\n if (formFiber.memoizedProps && formFiber.memoizedProps.onSubmit) {\n console.log('Form has onSubmit handler');\n }\n }\n}\n", - }, - }, - ], - api: "anthropic-messages", - provider: "anthropic", - model: "claude-sonnet-4-5-20250929", - usage: { - input: 10331, - output: 473, - cacheRead: 0, - cacheWrite: 0, - totalTokens: 0, - cost: { - input: 0.030993, - output: 0.007095000000000001, - cacheRead: 0, - cacheWrite: 0, - totalTokens: 0, - total: 0.038088, - }, - }, - stopReason: "toolUse", - }, - { - role: "toolResult", - toolCallId: "toolu_01Cvkd3XADVE1ToMXqjQYc12", - toolName: "browser_javascript", - output: "Error executing script: Frame with ID 0 was removed.", - details: { - files: [], - }, - isError: false, - }, - { - role: "assistant", - content: [], - api: "anthropic-messages", - provider: "anthropic", - model: "claude-sonnet-4-5-20250929", - usage: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - totalTokens: 0, - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - totalTokens: 0, - total: 0, - }, - }, - stopReason: "aborted", - errorMessage: "Request was aborted.", - }, - { - role: "user", - content: [ - { - type: "text", - text: "i'm on another page now with a chat again. can you find the chat window and input?", - }, - ], - }, - { - role: "assistant", - content: [ - { - type: "toolCall", - id: "toolu_018681im65XWx7SMGvRo9xPx", - name: "browser_javascript", - arguments: { - code: "// First, let's see what page we're on\nconsole.log('Current URL:', window.location.href);\nconsole.log('Page title:', document.title);\n\n// Look for common chat input elements\nconst textarea = document.querySelector('textarea');\nconst textInputs = document.querySelectorAll('input[type=\"text\"]');\nconst contentEditables = document.querySelectorAll('[contenteditable=\"true\"]');\n\nconsole.log('Found textarea:', textarea ? 'Yes' : 'No');\nconsole.log('Found text inputs:', textInputs.length);\nconsole.log('Found contenteditable elements:', contentEditables.length);\n\nif (textarea) {\n console.log('Textarea details:', {\n placeholder: textarea.placeholder,\n id: textarea.id,\n name: textarea.name,\n className: textarea.className\n });\n}\n\n// Look for send/submit buttons\nconst submitButtons = document.querySelectorAll('button[type=\"submit\"]');\nconst buttons = document.querySelectorAll('button');\n\nconsole.log('Found submit buttons:', submitButtons.length);\nconsole.log('Total buttons:', buttons.length);\n\n// Look for forms\nconst forms = document.querySelectorAll('form');\nconsole.log('Found forms:', forms.length);\n", - }, - }, - ], - api: "anthropic-messages", - provider: "anthropic", - model: "claude-sonnet-4-5-20250929", - usage: { - input: 10852, - output: 348, - cacheRead: 0, - cacheWrite: 0, - totalTokens: 0, - cost: { - input: 0.032556, - output: 0.00522, - cacheRead: 0, - cacheWrite: 0, - totalTokens: 0, - total: 0.037776000000000004, - }, - }, - stopReason: "toolUse", - }, - { - role: "toolResult", - toolCallId: "toolu_018681im65XWx7SMGvRo9xPx", - toolName: "browser_javascript", - output: - "Current URL: https://www.ams.at/arbeitsuchende/aus-und-weiterbildung/berufsinformationen/berufsinformation/berufsinfomat?open=berufsinfomat\nPage title: Berufsinfomat | AMS\nFound textarea: No\nFound text inputs: 1\nFound contenteditable elements: 0\nFound submit buttons: 1\nTotal buttons: 12\nFound forms: 1", - details: { - files: [], - }, - isError: false, - }, - { - role: "assistant", - content: [ - { - type: "text", - text: "\n\nI can see you're on an Austrian AMS (Arbeitsmarktservice) page about career information. I found 1 text input and 1 submit button. Let me examine them more closely:", - }, - { - type: "toolCall", - id: "toolu_01DuFzPze1zWi5QELAQtcUFZ", - name: "browser_javascript", - arguments: { - code: "const textInput = document.querySelector('input[type=\"text\"]');\nconst submitButton = document.querySelector('button[type=\"submit\"]');\nconst form = document.querySelector('form');\n\nif (textInput) {\n console.log('Text input details:', {\n placeholder: textInput.placeholder,\n id: textInput.id,\n name: textInput.name,\n value: textInput.value,\n className: textInput.className\n });\n}\n\nif (submitButton) {\n console.log('Submit button details:', {\n text: submitButton.textContent,\n id: submitButton.id,\n className: submitButton.className,\n disabled: submitButton.disabled\n });\n}\n\nif (form) {\n console.log('Form details:', {\n action: form.action,\n method: form.method,\n id: form.id\n });\n}\n", - }, - }, - ], - api: "anthropic-messages", - provider: "anthropic", - model: "claude-sonnet-4-5-20250929", - usage: { - input: 11314, - output: 310, - cacheRead: 0, - cacheWrite: 0, - totalTokens: 0, - cost: { - input: 0.033942, - output: 0.0046500000000000005, - cacheRead: 0, - cacheWrite: 0, - totalTokens: 0, - total: 0.038592, - }, - }, - stopReason: "toolUse", - }, - { - role: "toolResult", - toolCallId: "toolu_01DuFzPze1zWi5QELAQtcUFZ", - toolName: "browser_javascript", - output: - "Text input details: [object Object]\nSubmit button details: [object Object]\nForm details: [object Object]", - details: { - files: [], - }, - isError: false, - }, - { - role: "assistant", - content: [ - { - type: "text", - text: "\n\nThis appears to be a career information website form, not a chat interface. Are you sure you're on a chat page? Could you describe what you see on the screen, or would you like me to send a message through this form anyway?", - }, - ], - api: "anthropic-messages", - provider: "anthropic", - model: "claude-sonnet-4-5-20250929", - usage: { - input: 11659, - output: 53, - cacheRead: 0, - cacheWrite: 0, - totalTokens: 0, - cost: { - input: 0.034977, - output: 0.000795, - cacheRead: 0, - cacheWrite: 0, - totalTokens: 0, - total: 0.035772, - }, - }, - stopReason: "stop", - }, - { - role: "user", - content: [ - { - type: "text", - text: "could be in an iframe", - }, - ], - }, - { - role: "assistant", - content: [ - { - type: "text", - text: "Good thinking! Let me check for iframes:", - }, - { - type: "toolCall", - id: "toolu_01JDhMJmHj79N7gBWZATBNvs", - name: "browser_javascript", - arguments: { - code: "// Find all iframes\nconst iframes = document.querySelectorAll('iframe');\nconsole.log('Found iframes:', iframes.length);\n\nif (iframes.length > 0) {\n iframes.forEach((iframe, index) => {\n console.log(`Iframe ${index}:`, {\n src: iframe.src,\n id: iframe.id,\n className: iframe.className,\n title: iframe.title,\n name: iframe.name\n });\n });\n \n // Try to access the first iframe's content\n try {\n const iframeDoc = iframes[0].contentDocument || iframes[0].contentWindow.document;\n if (iframeDoc) {\n console.log('Can access iframe content');\n console.log('Iframe page title:', iframeDoc.title);\n \n // Look for chat elements in the iframe\n const iframeTextarea = iframeDoc.querySelector('textarea');\n const iframeTextInputs = iframeDoc.querySelectorAll('input[type=\"text\"]');\n \n console.log('Found textarea in iframe:', iframeTextarea ? 'Yes' : 'No');\n console.log('Found text inputs in iframe:', iframeTextInputs.length);\n }\n } catch (e) {\n console.log('Cannot access iframe content (cross-origin):', e.message);\n }\n} else {\n console.log('No iframes found on the page');\n}\n", - }, - }, - ], - api: "anthropic-messages", - provider: "anthropic", - model: "claude-sonnet-4-5-20250929", - usage: { - input: 11720, - output: 423, - cacheRead: 0, - cacheWrite: 0, - totalTokens: 0, - cost: { - input: 0.035160000000000004, - output: 0.006345, - cacheRead: 0, - cacheWrite: 0, - totalTokens: 0, - total: 0.041505, - }, - }, - stopReason: "toolUse", - }, - { - role: "toolResult", - toolCallId: "toolu_01JDhMJmHj79N7gBWZATBNvs", - toolName: "browser_javascript", - output: - "Found iframes: 1\nIframe 0: [object Object]\nCannot access iframe content (cross-origin): Failed to read a named property 'document' from 'Window': Blocked a frame with origin \"https://www.ams.at\" from accessing a cross-origin frame.", - details: { - files: [], - }, - isError: false, - }, - { - role: "assistant", - content: [ - { - type: "text", - text: "\n\nThere is an iframe on the page, but it's cross-origin so I can't access its content directly due to browser security restrictions. Let me get more details about the iframe:", - }, - { - type: "toolCall", - id: "toolu_0153cpVfNAXdNkipbEikNNTu", - name: "browser_javascript", - arguments: { - code: "const iframe = document.querySelector('iframe');\nif (iframe) {\n console.log('Iframe source:', iframe.src);\n console.log('Iframe ID:', iframe.id);\n console.log('Iframe class:', iframe.className);\n console.log('Iframe title:', iframe.title);\n console.log('Iframe width:', iframe.width);\n console.log('Iframe height:', iframe.height);\n}\n", - }, - }, - ], - api: "anthropic-messages", - provider: "anthropic", - model: "claude-sonnet-4-5-20250929", - usage: { - input: 12217, - output: 193, - cacheRead: 0, - cacheWrite: 0, - totalTokens: 0, - cost: { - input: 0.036651, - output: 0.002895, - cacheRead: 0, - cacheWrite: 0, - totalTokens: 0, - total: 0.039546000000000005, - }, - }, - stopReason: "toolUse", - }, - { - role: "toolResult", - toolCallId: "toolu_0153cpVfNAXdNkipbEikNNTu", - toolName: "browser_javascript", - output: - "Iframe source: https://berufsinfomat.prod.portal.ams.at/client/6453a57358480fb76ddc0a43/overlay?lc=de&ss=min\nIframe ID: ams-modal-berufsinfomat-iframe\nIframe class: \nIframe title: Berufsinfomat des AMS\nIframe width: 100%\nIframe height: 400", - details: { - files: [], - }, - isError: false, - }, - { - role: "assistant", - content: [ - { - type: "text", - text: "\n\nThe iframe is from `berufsinfomat.prod.portal.ams.at` which is a career information tool. Since it's cross-origin, I cannot directly access its content from the parent page due to browser security policies.\n\nThis doesn't appear to be a chat interface like T3 - it's a career counseling tool. Are you perhaps looking at a different window or tab? Or is there a chat widget somewhere on this page that I should look for?", - }, - ], - api: "anthropic-messages", - provider: "anthropic", - model: "claude-sonnet-4-5-20250929", - usage: { - input: 12519, - output: 104, - cacheRead: 0, - cacheWrite: 0, - totalTokens: 0, - cost: { - input: 0.037557, - output: 0.00156, - cacheRead: 0, - cacheWrite: 0, - totalTokens: 0, - total: 0.039117, - }, - }, - stopReason: "stop", - }, - { - role: "user", - content: [ - { - type: "text", - text: "can you open that iframe url in a new tab?", - }, - ], - }, - { - role: "assistant", - content: [ - { - type: "toolCall", - id: "toolu_015qgN7Jjw16r43gKXBEeBFN", - name: "browser_javascript", - arguments: { - code: "const iframeUrl = 'https://berufsinfomat.prod.portal.ams.at/client/6453a57358480fb76ddc0a43/overlay?lc=de&ss=min';\n\n// Open the iframe URL in a new tab\nwindow.open(iframeUrl, '_blank');\n\nconsole.log('Opened iframe URL in new tab:', iframeUrl);\n", - }, - }, - ], - api: "anthropic-messages", - provider: "anthropic", - model: "claude-sonnet-4-5-20250929", - usage: { - input: 12637, - output: 146, - cacheRead: 0, - cacheWrite: 0, - totalTokens: 0, - cost: { - input: 0.037911, - output: 0.00219, - cacheRead: 0, - cacheWrite: 0, - totalTokens: 0, - total: 0.040101, - }, - }, - stopReason: "toolUse", - }, - { - role: "toolResult", - toolCallId: "toolu_015qgN7Jjw16r43gKXBEeBFN", - toolName: "browser_javascript", - output: - "Opened iframe URL in new tab: https://berufsinfomat.prod.portal.ams.at/client/6453a57358480fb76ddc0a43/overlay?lc=de&ss=min", - details: { - files: [], - }, - isError: false, - }, - { - role: "assistant", - content: [ - { - type: "text", - text: "\n\nI've opened the iframe URL in a new tab! Once you switch to that tab, I'll be able to access the content directly and help you interact with the chat interface there. Let me know when you're on the new tab and I can look for the chat input and other elements.", - }, - ], - api: "anthropic-messages", - provider: "anthropic", - model: "claude-sonnet-4-5-20250929", - usage: { - input: 12845, - output: 63, - cacheRead: 0, - cacheWrite: 0, - totalTokens: 0, - cost: { - input: 0.038535, - output: 0.000945, - cacheRead: 0, - cacheWrite: 0, - totalTokens: 0, - total: 0.03948, - }, - }, - stopReason: "stop", - }, - { - role: "user", - content: [ - { - type: "text", - text: "ok, i'm on a news page now, can you tell me what's news today?", - }, - ], - }, - { - role: "assistant", - content: [], - api: "anthropic-messages", - provider: "anthropic", - model: "claude-sonnet-4-5-20250929", - usage: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - totalTokens: 0, - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - totalTokens: 0, - total: 0, - }, - }, - stopReason: "aborted", - errorMessage: "Request was aborted.", - }, - { - role: "user", - content: [ - { - type: "text", - text: "write a markdown doc, and include images if possible", - }, - ], - }, - { - role: "assistant", - content: [ - { - type: "text", - text: "I'll check what's on the current page and extract the news for you:", - }, - { - type: "toolCall", - id: "toolu_01XaoCN6tM5NGdQXy2SjssXZ", - name: "browser_javascript", - arguments: { - code: "// Get the current page info\nconsole.log('Current URL:', window.location.href);\nconsole.log('Page title:', document.title);\n\n// Look for news articles\nconst articles = document.querySelectorAll('article, .article, .news-item, [class*=\"article\"], [class*=\"story\"]');\nconsole.log('Found articles:', articles.length);\n\n// Look for headlines\nconst headlines = document.querySelectorAll('h1, h2, h3, .headline, [class*=\"headline\"], [class*=\"title\"]');\nconsole.log('Found headlines:', headlines.length);\n\n// Look for images\nconst images = document.querySelectorAll('img');\nconsole.log('Found images:', images.length);\n\n// Get main content\nconst mainContent = document.querySelector('main, #main, .main-content, [role=\"main\"]');\nconsole.log('Found main content:', mainContent ? 'Yes' : 'No');\n\n// Let's get a sample of what's on the page\nconsole.log('Body text preview:', document.body.innerText.substring(0, 500));\n", - }, - }, - ], - api: "anthropic-messages", - provider: "anthropic", - model: "claude-sonnet-4-5-20250929", - usage: { - input: 12941, - output: 324, - cacheRead: 0, - cacheWrite: 0, - totalTokens: 0, - cost: { - input: 0.038823, - output: 0.00486, - cacheRead: 0, - cacheWrite: 0, - totalTokens: 0, - total: 0.043683, - }, - }, - stopReason: "toolUse", - }, - { - role: "toolResult", - toolCallId: "toolu_01XaoCN6tM5NGdQXy2SjssXZ", - toolName: "browser_javascript", - output: - 'Current URL: https://www.derstandard.at/\nPage title: derStandard.at | Nachrichten, Kommentare & Community\nFound articles: 231\nFound headlines: 365\nFound images: 154\nFound main content: Yes\nBody text preview: Fr, 3.10.2025\nStartseite\nÜberblick\nNACHRICHTEN IN ECHTZEIT\nRussland greift ukrainische Energieinfrastruktur an \nSchikanen gegen Arbeitsmigranten in Russland \nFall Anna: Gerichtspräsidentin übt scharfe Kritik an Drohungen gegen Richter \nZiel Playoff: Maskenmann Pöltl vor zehnter NBA-Saison zuversichtlich \n\nZUKUNFT MEDIENKOMPETENZ\n\n106\nPostings\nNetzexpertin Brodnig: "Propaganda ist erfolgreich, wenn sich Leute nicht mehr auskennen"\n\nGezielt Verwirrung stiften – das ist die Strategie hinter vielen', - details: { - files: [], - }, - isError: false, - }, - { - role: "assistant", - content: [ - { - type: "text", - text: "\n\nGreat! You're on derStandard.at, an Austrian news site. Let me extract the news articles with images:", - }, - { - type: "toolCall", - id: "toolu_01FDif6VsohQR8Eint1TSZDi", - name: "browser_javascript", - arguments: { - code: "// Extract news articles with their details\nconst newsData = [];\n\n// Find article containers\nconst articles = document.querySelectorAll('article, .article-teaser, [class*=\"teaser\"]');\n\narticles.forEach((article, index) => {\n if (index < 15) { // Get top 15 articles\n const headline = article.querySelector('h1, h2, h3, h4, .headline, [class*=\"headline\"]');\n const link = article.querySelector('a');\n const img = article.querySelector('img');\n const description = article.querySelector('p, .description, .teaser-text, [class*=\"description\"]');\n \n if (headline && headline.textContent.trim()) {\n newsData.push({\n title: headline.textContent.trim(),\n link: link ? link.href : '',\n image: img ? img.src : '',\n imageAlt: img ? img.alt : '',\n description: description ? description.textContent.trim() : ''\n });\n }\n }\n});\n\nconsole.log('Extracted news articles:', newsData.length);\nconsole.log(JSON.stringify(newsData.slice(0, 5), null, 2)); // Show first 5 in console\n\n// Return the data\nnewsData;\n", - }, - }, - ], - api: "anthropic-messages", - provider: "anthropic", - model: "claude-sonnet-4-5-20250929", - usage: { - input: 13535, - output: 385, - cacheRead: 0, - cacheWrite: 0, - totalTokens: 0, - cost: { - input: 0.040605, - output: 0.005775, - cacheRead: 0, - cacheWrite: 0, - totalTokens: 0, - total: 0.046380000000000005, - }, - }, - stopReason: "toolUse", - }, - { - role: "toolResult", - toolCallId: "toolu_01FDif6VsohQR8Eint1TSZDi", - toolName: "browser_javascript", - output: - 'Extracted news articles: 6\n[\n {\n "title": "Russland greift ukrainische Energieinfrastruktur an",\n "link": "https://www.derstandard.at/jetzt/livebericht/3000000290448/lage-an-akw-saporischschja-laut-selenskyj-kritisch?ref=niewidget",\n "image": "",\n "imageAlt": "",\n "description": ""\n },\n {\n "title": "Schikanen gegen Arbeitsmigranten in Russland",\n "link": "https://www.derstandard.at/story/3000000290399/schikanen-gegen-arbeitsmigranten-in-russland?ref=niewidget",\n "image": "",\n "imageAlt": "",\n "description": ""\n },\n {\n "title": "Fall Anna: Gerichtspräsidentin übt scharfe Kritik an Drohungen gegen Richter",\n "link": "https://www.derstandard.at/story/3000000290483/fall-anna-gerichtspraesidentin-uebt-scharfe-kritik-an-drohungen-gegen-richter?ref=niewidget",\n "image": "",\n "imageAlt": "",\n "description": ""\n },\n {\n "title": "Ziel Playoff: Maskenmann Pöltl vor zehnter NBA-Saison zuversichtlich",\n "link": "https://www.derstandard.at/story/3000000290494/basketball-maskenmann-p246ltl-vor-zehnter-nba-saison-zuversichtlich?ref=niewidget",\n "image": "",\n "imageAlt": "",\n "description": ""\n },\n {\n "title": "Netzexpertin Brodnig: \\"Propaganda ist erfolgreich, wenn sich Leute nicht mehr auskennen\\"",\n "link": "https://www.derstandard.at/story/3000000290377/netzexpertin-brodnig-propaganda-ist-erfolgreich-wenn-sich-leute-nicht-mehr-auskennen",\n "image": "https://i.ds.at/C_Le6w/rs:fill:600:400/plain/lido-images/2025/10/01/57d7d9b6-77f7-43fc-871a-c8ed93e22930.jpeg",\n "imageAlt": "",\n "description": "Zukunft Medienkompetenz"\n }\n]', - details: { - files: [], - }, - isError: false, - }, - { - role: "assistant", - content: [ - { - type: "toolCall", - id: "toolu_018YgtrBC5eankNyqWxJW9D2", - name: "browser_javascript", - arguments: { - code: "// Let's get more comprehensive news data\nconst newsArticles = [];\n\n// Try different selectors for better coverage\nconst mainArticles = document.querySelectorAll('article');\n\nmainArticles.forEach((article, index) => {\n if (index < 20) {\n const headline = article.querySelector('h1, h2, h3, h4, h5, a[class*=\"headline\"], span[class*=\"headline\"]');\n const links = article.querySelectorAll('a');\n const mainLink = links[0];\n const img = article.querySelector('img');\n const textElements = article.querySelectorAll('p, span, div');\n \n let description = '';\n textElements.forEach(el => {\n const text = el.textContent.trim();\n if (text.length > 20 && text.length < 300 && !description) {\n description = text;\n }\n });\n \n if (headline && headline.textContent.trim().length > 5) {\n const title = headline.textContent.trim();\n // Avoid duplicates\n if (!newsArticles.find(a => a.title === title)) {\n newsArticles.push({\n title: title,\n link: mainLink ? mainLink.href : '',\n image: img ? img.src : '',\n imageAlt: img ? (img.alt || img.title || '') : '',\n description: description\n });\n }\n }\n }\n});\n\nconsole.log('Total articles extracted:', newsArticles.length);\nnewsArticles;\n", - }, - }, - ], - api: "anthropic-messages", - provider: "anthropic", - model: "claude-sonnet-4-5-20250929", - usage: { - input: 14583, - output: 436, - cacheRead: 0, - cacheWrite: 0, - totalTokens: 0, - cost: { - input: 0.043749, - output: 0.00654, - cacheRead: 0, - cacheWrite: 0, - totalTokens: 0, - total: 0.050289, - }, - }, - stopReason: "toolUse", - }, - { - role: "toolResult", - toolCallId: "toolu_018YgtrBC5eankNyqWxJW9D2", - toolName: "browser_javascript", - output: "Total articles extracted: 18", - details: { - files: [], - }, - isError: false, - }, - { - role: "assistant", - content: [ - { - type: "toolCall", - id: "toolu_01LPxKs8zBbMvWdygvP9JxHF", - name: "browser_javascript", - arguments: { - code: "// Get the extracted news data and format it for markdown\nconst newsArticles = [];\nconst mainArticles = document.querySelectorAll('article');\n\nmainArticles.forEach((article, index) => {\n if (index < 20) {\n const headline = article.querySelector('h1, h2, h3, h4, h5');\n const links = article.querySelectorAll('a');\n const mainLink = links[0];\n const img = article.querySelector('img');\n const textElements = article.querySelectorAll('p');\n \n let description = '';\n textElements.forEach(el => {\n const text = el.textContent.trim();\n if (text.length > 20 && text.length < 300 && !description) {\n description = text;\n }\n });\n \n if (headline && headline.textContent.trim().length > 5) {\n const title = headline.textContent.trim();\n if (!newsArticles.find(a => a.title === title)) {\n newsArticles.push({\n title: title,\n link: mainLink ? mainLink.href : '',\n image: img ? img.src : '',\n imageAlt: img ? (img.alt || img.title || '') : '',\n description: description\n });\n }\n }\n }\n});\n\n// Get today's date\nconst today = new Date().toLocaleDateString('de-AT', { \n weekday: 'long', \n year: 'numeric', \n month: 'long', \n day: 'numeric' \n});\n\n// Create markdown content\nlet markdown = `# Der Standard - Nachrichten\\n## ${today}\\n\\n`;\nmarkdown += `*Quelle: [derStandard.at](${window.location.href})*\\n\\n---\\n\\n`;\n\nnewsArticles.forEach((article, index) => {\n markdown += `### ${index + 1}. ${article.title}\\n\\n`;\n \n if (article.image) {\n markdown += `![${article.imageAlt}](${article.image})\\n\\n`;\n }\n \n if (article.description) {\n markdown += `${article.description}\\n\\n`;\n }\n \n if (article.link) {\n markdown += `[Artikel lesen →](${article.link})\\n\\n`;\n }\n \n markdown += `---\\n\\n`;\n});\n\nconsole.log('Markdown created with', newsArticles.length, 'articles');\nmarkdown;\n", - }, - }, - ], - api: "anthropic-messages", - provider: "anthropic", - model: "claude-sonnet-4-5-20250929", - usage: { - input: 15035, - output: 685, - cacheRead: 0, - cacheWrite: 0, - totalTokens: 0, - cost: { - input: 0.045105, - output: 0.010275, - cacheRead: 0, - cacheWrite: 0, - totalTokens: 0, - total: 0.05538, - }, - }, - stopReason: "toolUse", - }, - { - role: "toolResult", - toolCallId: "toolu_01LPxKs8zBbMvWdygvP9JxHF", - toolName: "browser_javascript", - output: "Markdown created with 18 articles", - details: { - files: [], - }, - isError: false, - }, - { - role: "assistant", - content: [ - { - type: "text", - text: "\n\nNow let me create the markdown document as an artifact:", - }, - { - type: "toolCall", - id: "toolu_013YSiSucUcbfdwXqgHUfy9T", - name: "browser_javascript", - arguments: { - code: "// Generate the final markdown\nconst newsArticles = [];\nconst mainArticles = document.querySelectorAll('article');\n\nmainArticles.forEach((article, index) => {\n if (index < 20) {\n const headline = article.querySelector('h1, h2, h3, h4, h5');\n const links = article.querySelectorAll('a');\n const mainLink = links[0];\n const img = article.querySelector('img');\n const textElements = article.querySelectorAll('p');\n \n let description = '';\n textElements.forEach(el => {\n const text = el.textContent.trim();\n if (text.length > 20 && text.length < 300 && !description) {\n description = text;\n }\n });\n \n if (headline && headline.textContent.trim().length > 5) {\n const title = headline.textContent.trim();\n if (!newsArticles.find(a => a.title === title)) {\n newsArticles.push({\n title: title,\n link: mainLink ? mainLink.href : '',\n image: img ? img.src : '',\n imageAlt: img ? (img.alt || img.title || '') : '',\n description: description\n });\n }\n }\n }\n});\n\nconst today = new Date().toLocaleDateString('de-AT', { \n weekday: 'long', \n year: 'numeric', \n month: 'long', \n day: 'numeric' \n});\n\nlet markdown = `# Der Standard - Nachrichten des Tages\\n## ${today}\\n\\n`;\nmarkdown += `*Quelle: [derStandard.at](${window.location.href})*\\n\\n`;\nmarkdown += `---\\n\\n`;\n\nnewsArticles.forEach((article, index) => {\n markdown += `### ${index + 1}. ${article.title}\\n\\n`;\n \n if (article.image) {\n markdown += `![${article.imageAlt || 'News Image'}](${article.image})\\n\\n`;\n }\n \n if (article.description) {\n markdown += `${article.description}\\n\\n`;\n }\n \n if (article.link) {\n markdown += `[📰 Artikel lesen](${article.link})\\n\\n`;\n }\n \n markdown += `---\\n\\n`;\n});\n\nconsole.log(markdown);\nmarkdown;\n", - }, - }, - ], - api: "anthropic-messages", - provider: "anthropic", - model: "claude-sonnet-4-5-20250929", - usage: { - input: 15738, - output: 683, - cacheRead: 0, - cacheWrite: 0, - totalTokens: 0, - cost: { - input: 0.047214, - output: 0.010245, - cacheRead: 0, - cacheWrite: 0, - totalTokens: 0, - total: 0.057458999999999996, - }, - }, - stopReason: "toolUse", - }, - { - role: "toolResult", - toolCallId: "toolu_013YSiSucUcbfdwXqgHUfy9T", - toolName: "browser_javascript", - output: - '# Der Standard - Nachrichten des Tages\n## Freitag, 3. Oktober 2025\n\n*Quelle: [derStandard.at](https://www.derstandard.at/)*\n\n---\n\n### 1. Russland greift ukrainische Energieinfrastruktur an\n\n[📰 Artikel lesen](https://www.derstandard.at/jetzt/livebericht/3000000290448/lage-an-akw-saporischschja-laut-selenskyj-kritisch?ref=niewidget)\n\n---\n\n### 2. Schikanen gegen Arbeitsmigranten in Russland\n\n[📰 Artikel lesen](https://www.derstandard.at/story/3000000290399/schikanen-gegen-arbeitsmigranten-in-russland?ref=niewidget)\n\n---\n\n### 3. Fall Anna: Gerichtspräsidentin übt scharfe Kritik an Drohungen gegen Richter\n\n[📰 Artikel lesen](https://www.derstandard.at/story/3000000290483/fall-anna-gerichtspraesidentin-uebt-scharfe-kritik-an-drohungen-gegen-richter?ref=niewidget)\n\n---\n\n### 4. Ziel Playoff: Maskenmann Pöltl vor zehnter NBA-Saison zuversichtlich\n\n[📰 Artikel lesen](https://www.derstandard.at/story/3000000290494/basketball-maskenmann-p246ltl-vor-zehnter-nba-saison-zuversichtlich?ref=niewidget)\n\n---\n\n### 5. Netzexpertin Brodnig: "Propaganda ist erfolgreich, wenn sich Leute nicht mehr auskennen"\n\n![News Image](https://i.ds.at/C_Le6w/rs:fill:600:400/plain/lido-images/2025/10/01/57d7d9b6-77f7-43fc-871a-c8ed93e22930.jpeg)\n\nZukunft Medienkompetenz\n\n[📰 Artikel lesen](https://www.derstandard.at/story/3000000290377/netzexpertin-brodnig-propaganda-ist-erfolgreich-wenn-sich-leute-nicht-mehr-auskennen)\n\n---\n\n### 6. Flughafen München nach Drohnensichtung zwischenzeitlich geschlossen, zahlreiche Ausfälle\n\n![News Image](https://i.ds.at/fp3AhQ/rs:fill:600:400/plain/lido-images/2025/10/03/ef80089b-300d-4fcb-96b1-7b65cac270a4.jpeg)\n\nDer Flugbetrieb ist seit den frühen Morgenstunden wieder aufgenommen. Rund 3.000 Passagiere waren von den Ausfällen betroffen\n\n[📰 Artikel lesen](https://www.derstandard.at/story/3000000290460/flughafen-m252nchen-nach-schlie223ung-wegen-drohnensichtung-wieder-offen)\n\n---\n\n### 7. Wie stark werden Onlinekäufer manipuliert? Sozialministerium klagt Billigriesen Temu\n\n![News Image](https://i.ds.at/UQ7LBg/rs:fill:600:400/plain/lido-images/2025/10/02/7febc4a4-6c5a-473c-b28c-ce89d151db0b.jpeg)\n\nUnlauterer Wettbewerb\n\n[📰 Artikel lesen](https://www.derstandard.at/story/3000000290329/wie-stark-werden-onlinekaeufer-manipuliert-sozialministerium-klagt-billigriesen-temu)\n\n---\n\n### 8. Teslas Freude über Verkaufsrekord dürfte von kurzer Dauer sein\n\n![News Image](https://i.ds.at/ryC6hQ/rs:fill:600:400/plain/lido-images/2025/10/03/ecc3c7a6-7d2d-453f-b97d-002034b4d86e.jpeg)\n\nDie aktuell wieder besseren Zahlen dürften auf die Streichung einer Verkaufsprämie zurückzuführen sein. Parallel dazu wollen Investoren Musks Billionen-Dollar-Gehaltspaket kippen\n\n[📰 Artikel lesen](https://www.derstandard.at/story/3000000290473/teslas-freude-ueber-verkaufsrekord-duerfte-von-kurzer-dauer-sein)\n\n---\n\n### 9. Bis zu 17 Euro: Das kosten Eggs Benedict in der Wiener Gastronomie\n\n![News Image](https://i.ds.at/8Zh7zA/rs:fill:600:400/plain/lido-images/2025/10/02/fae3b15e-3ff7-4912-938a-2de53b7e33ff.jpeg)\n\nDen modernen Frühstücksklassiker findet man auf der Speisekarte zahlreicher Wiener Lokale. So viel muss man dafür hinblättern\n\n[📰 Artikel lesen](https://www.derstandard.at/story/3000000290304/bis-zu-17-euro-das-kosten-eggs-benedict-in-der-wiener-gastronomie)\n\n---\n\n### 10. Georg Dornauer lässt die SPÖ ganz alt aussehen\n\n![News Image](https://i.ds.at/cU2jUQ/rs:fill:600:400/plain/lido-images/2025/10/03/c77523eb-b3bb-4a66-8e32-fa29a441452b.jpeg)\n\nDer Ex-Chef der Tiroler Sozialdemokraten ist ein schwieriger Genosse. Dass seine Partei aber keine andere Konfliktlösung als den Ausschluss gefunden hat, ist ein Armutszeugnis\n\n[📰 Artikel lesen](https://www.derstandard.at/story/3000000290496/georg-dornauer-laesst-die-spoe-ganz-alt-aussehen)\n\n---\n\n### 11. Wir sollten die Krise in Österreichs Wirtschaft nicht größer reden, als sie ist\n\n![News Image](https://i.ds.at/QFlU-w/rs:fill:600:400/plain/lido-images/2025/08/01/6c3bcbb1-eca4-4237-ad84-d77e39bc3545.jpeg)\n\nOb Wachstum oder Jobmarkt: Zuletzt gab es nur schlechte Nachrichten vom heimischen Standort. Dabei gibt es gute Gründe, nicht zu verzagen. Vier Beispiele dafür\n\n[📰 Artikel lesen](https://www.derstandard.at/story/3000000290078/wir-sollten-die-krise-in-oesterreichs-wirtschaft-nicht-groesser-reden-als-sie-ist)\n\n---\n\n### 12. Drohnen über Dänemark: Festnahmen auf verdächtigem Schiff\n\n![AFP/DAMIEN MEYER](https://i.ds.at/E8GGfA/c:1200:800:fp:0.5:0.5/rs:fill:220:165:1/plain/lido-images/2025/10/01/423305df-5d12-48f4-9b28-517363b0fd8e.jpeg)\n\n[📰 Artikel lesen](https://www.derstandard.at/story/3000000290261/drohnen-ueber-daenemark-franzoesisches-militaer-entert-schiff-der-russischen-schattenflotte?ref=seite1_entdecken)\n\n---\n\n### 13. Anschlagspläne: Mutmaßliche Hamas-Mitglieder in Deutschland festgenommen\n\n![AFP/POOL/JOHN MACDOUGALL](https://i.ds.at/m-jE6g/c:1200:800:fp:0.5:0.5/rs:fill:220:165:1/plain/lido-images/2025/10/01/fc351fe4-cdac-4102-8332-95828658bff0.jpeg)\n\nJüdische Einrichtungen im Visier\n\n[📰 Artikel lesen](https://www.derstandard.at/story/3000000290245/anschlagsplaene-mutmassliche-hamas-mitglieder-in-deutschland-festgenommen?ref=seite1_entdecken)\n\n---\n\n### 14. Kinderfilm-Klassiker "Momo" bekommt zeitgemäßes Update\n\n![Constantin Film](https://i.ds.at/yibsqA/c:1200:800:fp:0.5:0.5/rs:fill:220:165:1/g:fp:0.48:0.19/plain/lido-images/2025/10/02/a0cfd114-6460-4370-9ea5-1ad60e89a843.jpeg)\n\n[📰 Artikel lesen](https://www.derstandard.at/story/3000000290391/kinderfilm-klassiker-momo-bekommt-zeitgemaesses-update?ref=seite1_entdecken)\n\n---\n\n### 15. Generalstreik in Italien nach Stopp von Gaza-Flottille\n\n![REUTERS/Claudia Greco](https://i.ds.at/wee2ag/c:1200:800:fp:0.5:0.5/rs:fill:220:165:1/plain/lido-images/2025/10/03/70939333-974c-44f4-ab81-20f49f555332.jpeg)\n\n[📰 Artikel lesen](https://www.derstandard.at/story/3000000290474/generalstreik-in-italien-nach-stopp-von-gaza-flottille?ref=seite1_entdecken)\n\n---\n\n### 16. Saturn-Mond Enceladus hat alle Zutaten, die es für Leben braucht\n\n![Illustr.: NASA/JPL-Caltech](https://i.ds.at/ae1Jkw/c:1013:675:fp:0.5:0.5/rs:fill:220:165:1/plain/lido-images/2025/10/02/53df0dc2-7990-40a9-8da3-4f5b8e21688f.jpeg)\n\n[📰 Artikel lesen](https://www.derstandard.at/story/3000000290229/saturn-mond-enceladus-hat-alle-zutaten-die-es-fuer-leben-braucht?ref=seite1_entdecken)\n\n---\n\n### 17. Welches Verhalten treibt euch in den Wahnsinn?\n\n![News Image](https://i.ds.at/hfeNIw/rs:fill:600:400/plain/lido-images/2025/04/16/1d6a93f4-5754-4ffc-88b6-954be5c2c396.jpeg)\n\nMein Forum: Diskutieren Sie dieses Thema mit der STANDARD-Community\n\n[📰 Artikel lesen](https://www.derstandard.at/story/3000000290297/welches-verhalten-treibt-euch-in-den-wahnsinn?ref=meinforum_widget)\n\n---\n\n### 18. Wohin kann man im Ernstfall als LGBTQIA Person sicher auswandern?\n\n![News Image](https://i.ds.at/JWzXOQ/rs:fill:600:400/plain/lido-images/2025/07/07/0d04eba0-7f8d-4156-88d1-151962852150.jpeg)\n\nMein Forum: Diskutieren Sie dieses Thema mit der STANDARD-Community\n\n[📰 Artikel lesen](https://www.derstandard.at/story/3000000290175/wohin-kann-man-im-ernstfall-als-lgbtqia-person-sicher-auswandern?ref=meinforum_widget)\n\n---', - details: { - files: [], - }, - isError: false, - }, - { - role: "assistant", - content: [ - { - type: "text", - text: "\n\nPerfect! Now let me create the markdown file as an artifact:", - }, - { - type: "toolCall", - id: "toolu_01BtH9H2BvwxvKjLw5iHXcZC", - name: "artifacts", - arguments: { - command: "create", - filename: "news_today.md", - title: "Der Standard News - Today", - content: - '# Der Standard - Nachrichten des Tages\n## Freitag, 3. Oktober 2025\n\n*Quelle: [derStandard.at](https://www.derstandard.at/)*\n\n---\n\n### 1. Russland greift ukrainische Energieinfrastruktur an\n\n[📰 Artikel lesen](https://www.derstandard.at/jetzt/livebericht/3000000290448/lage-an-akw-saporischschja-laut-selenskyj-kritisch?ref=niewidget)\n\n---\n\n### 2. Schikanen gegen Arbeitsmigranten in Russland\n\n[📰 Artikel lesen](https://www.derstandard.at/story/3000000290399/schikanen-gegen-arbeitsmigranten-in-russland?ref=niewidget)\n\n---\n\n### 3. Fall Anna: Gerichtspräsidentin übt scharfe Kritik an Drohungen gegen Richter\n\n[📰 Artikel lesen](https://www.derstandard.at/story/3000000290483/fall-anna-gerichtspraesidentin-uebt-scharfe-kritik-an-drohungen-gegen-richter?ref=niewidget)\n\n---\n\n### 4. Ziel Playoff: Maskenmann Pöltl vor zehnter NBA-Saison zuversichtlich\n\n[📰 Artikel lesen](https://www.derstandard.at/story/3000000290494/basketball-maskenmann-p246ltl-vor-zehnter-nba-saison-zuversichtlich?ref=niewidget)\n\n---\n\n### 5. Netzexpertin Brodnig: "Propaganda ist erfolgreich, wenn sich Leute nicht mehr auskennen"\n\n![News Image](https://i.ds.at/C_Le6w/rs:fill:600:400/plain/lido-images/2025/10/01/57d7d9b6-77f7-43fc-871a-c8ed93e22930.jpeg)\n\nZukunft Medienkompetenz\n\n[📰 Artikel lesen](https://www.derstandard.at/story/3000000290377/netzexpertin-brodnig-propaganda-ist-erfolgreich-wenn-sich-leute-nicht-mehr-auskennen)\n\n---\n\n### 6. Flughafen München nach Drohnensichtung zwischenzeitlich geschlossen, zahlreiche Ausfälle\n\n![News Image](https://i.ds.at/fp3AhQ/rs:fill:600:400/plain/lido-images/2025/10/03/ef80089b-300d-4fcb-96b1-7b65cac270a4.jpeg)\n\nDer Flugbetrieb ist seit den frühen Morgenstunden wieder aufgenommen. Rund 3.000 Passagiere waren von den Ausfällen betroffen\n\n[📰 Artikel lesen](https://www.derstandard.at/story/3000000290460/flughafen-m252nchen-nach-schlie223ung-wegen-drohnensichtung-wieder-offen)\n\n---\n\n### 7. Wie stark werden Onlinekäufer manipuliert? Sozialministerium klagt Billigriesen Temu\n\n![News Image](https://i.ds.at/UQ7LBg/rs:fill:600:400/plain/lido-images/2025/10/02/7febc4a4-6c5a-473c-b28c-ce89d151db0b.jpeg)\n\nUnlauterer Wettbewerb\n\n[📰 Artikel lesen](https://www.derstandard.at/story/3000000290329/wie-stark-werden-onlinekaeufer-manipuliert-sozialministerium-klagt-billigriesen-temu)\n\n---\n\n### 8. Teslas Freude über Verkaufsrekord dürfte von kurzer Dauer sein\n\n![News Image](https://i.ds.at/ryC6hQ/rs:fill:600:400/plain/lido-images/2025/10/03/ecc3c7a6-7d2d-453f-b97d-002034b4d86e.jpeg)\n\nDie aktuell wieder besseren Zahlen dürften auf die Streichung einer Verkaufsprämie zurückzuführen sein. Parallel dazu wollen Investoren Musks Billionen-Dollar-Gehaltspaket kippen\n\n[📰 Artikel lesen](https://www.derstandard.at/story/3000000290473/teslas-freude-ueber-verkaufsrekord-duerfte-von-kurzer-dauer-sein)\n\n---\n\n### 9. Bis zu 17 Euro: Das kosten Eggs Benedict in der Wiener Gastronomie\n\n![News Image](https://i.ds.at/8Zh7zA/rs:fill:600:400/plain/lido-images/2025/10/02/fae3b15e-3ff7-4912-938a-2de53b7e33ff.jpeg)\n\nDen modernen Frühstücksklassiker findet man auf der Speisekarte zahlreicher Wiener Lokale. So viel muss man dafür hinblättern\n\n[📰 Artikel lesen](https://www.derstandard.at/story/3000000290304/bis-zu-17-euro-das-kosten-eggs-benedict-in-der-wiener-gastronomie)\n\n---\n\n### 10. Georg Dornauer lässt die SPÖ ganz alt aussehen\n\n![News Image](https://i.ds.at/cU2jUQ/rs:fill:600:400/plain/lido-images/2025/10/03/c77523eb-b3bb-4a66-8e32-fa29a441452b.jpeg)\n\nDer Ex-Chef der Tiroler Sozialdemokraten ist ein schwieriger Genosse. Dass seine Partei aber keine andere Konfliktlösung als den Ausschluss gefunden hat, ist ein Armutszeugnis\n\n[📰 Artikel lesen](https://www.derstandard.at/story/3000000290496/georg-dornauer-laesst-die-spoe-ganz-alt-aussehen)\n\n---\n\n### 11. Wir sollten die Krise in Österreichs Wirtschaft nicht größer reden, als sie ist\n\n![News Image](https://i.ds.at/QFlU-w/rs:fill:600:400/plain/lido-images/2025/08/01/6c3bcbb1-eca4-4237-ad84-d77e39bc3545.jpeg)\n\nOb Wachstum oder Jobmarkt: Zuletzt gab es nur schlechte Nachrichten vom heimischen Standort. Dabei gibt es gute Gründe, nicht zu verzagen. Vier Beispiele dafür\n\n[📰 Artikel lesen](https://www.derstandard.at/story/3000000290078/wir-sollten-die-krise-in-oesterreichs-wirtschaft-nicht-groesser-reden-als-sie-ist)\n\n---\n\n### 12. Drohnen über Dänemark: Festnahmen auf verdächtigem Schiff\n\n![AFP/DAMIEN MEYER](https://i.ds.at/E8GGfA/c:1200:800:fp:0.5:0.5/rs:fill:220:165:1/plain/lido-images/2025/10/01/423305df-5d12-48f4-9b28-517363b0fd8e.jpeg)\n\n[📰 Artikel lesen](https://www.derstandard.at/story/3000000290261/drohnen-ueber-daenemark-franzoesisches-militaer-entert-schiff-der-russischen-schattenflotte?ref=seite1_entdecken)\n\n---\n\n### 13. Anschlagspläne: Mutmaßliche Hamas-Mitglieder in Deutschland festgenommen\n\n![AFP/POOL/JOHN MACDOUGALL](https://i.ds.at/m-jE6g/c:1200:800:fp:0.5:0.5/rs:fill:220:165:1/plain/lido-images/2025/10/01/fc351fe4-cdac-4102-8332-95828658bff0.jpeg)\n\nJüdische Einrichtungen im Visier\n\n[📰 Artikel lesen](https://www.derstandard.at/story/3000000290245/anschlagsplaene-mutmassliche-hamas-mitglieder-in-deutschland-festgenommen?ref=seite1_entdecken)\n\n---\n\n### 14. Kinderfilm-Klassiker "Momo" bekommt zeitgemäßes Update\n\n![Constantin Film](https://i.ds.at/yibsqA/c:1200:800:fp:0.5:0.5/rs:fill:220:165:1/g:fp:0.48:0.19/plain/lido-images/2025/10/02/a0cfd114-6460-4370-9ea5-1ad60e89a843.jpeg)\n\n[📰 Artikel lesen](https://www.derstandard.at/story/3000000290391/kinderfilm-klassiker-momo-bekommt-zeitgemaesses-update?ref=seite1_entdecken)\n\n---\n\n### 15. Generalstreik in Italien nach Stopp von Gaza-Flottille\n\n![REUTERS/Claudia Greco](https://i.ds.at/wee2ag/c:1200:800:fp:0.5:0.5/rs:fill:220:165:1/plain/lido-images/2025/10/03/70939333-974c-44f4-ab81-20f49f555332.jpeg)\n\n[📰 Artikel lesen](https://www.derstandard.at/story/3000000290474/generalstreik-in-italien-nach-stopp-von-gaza-flottille?ref=seite1_entdecken)\n\n---\n\n### 16. Saturn-Mond Enceladus hat alle Zutaten, die es für Leben braucht\n\n![Illustr.: NASA/JPL-Caltech](https://i.ds.at/ae1Jkw/c:1013:675:fp:0.5:0.5/rs:fill:220:165:1/plain/lido-images/2025/10/02/53df0dc2-7990-40a9-8da3-4f5b8e21688f.jpeg)\n\n[📰 Artikel lesen](https://www.derstandard.at/story/3000000290229/saturn-mond-enceladus-hat-alle-zutaten-die-es-fuer-leben-braucht?ref=seite1_entdecken)\n\n---\n\n### 17. Welches Verhalten treibt euch in den Wahnsinn?\n\n![News Image](https://i.ds.at/hfeNIw/rs:fill:600:400/plain/lido-images/2025/04/16/1d6a93f4-5754-4ffc-88b6-954be5c2c396.jpeg)\n\nMein Forum: Diskutieren Sie dieses Thema mit der STANDARD-Community\n\n[📰 Artikel lesen](https://www.derstandard.at/story/3000000290297/welches-verhalten-treibt-euch-in-den-wahnsinn?ref=meinforum_widget)\n\n---\n\n### 18. Wohin kann man im Ernstfall als LGBTQIA Person sicher auswandern?\n\n![News Image](https://i.ds.at/JWzXOQ/rs:fill:600:400/plain/lido-images/2025/07/07/0d04eba0-7f8d-4156-88d1-151962852150.jpeg)\n\nMein Forum: Diskutieren Sie dieses Thema mit der STANDARD-Community\n\n[📰 Artikel lesen](https://www.derstandard.at/story/3000000290175/wohin-kann-man-im-ernstfall-als-lgbtqia-person-sicher-auswandern?ref=meinforum_widget)\n\n---\n\n## Top Themen des Tages\n\n- **Ukraine-Krieg**: Russland greift Energieinfrastruktur an\n- **Sicherheit**: Flughafen München wegen Drohne geschlossen, Drohnen über Dänemark\n- **Österreich**: SPÖ-Konflikt um Georg Dornauer, Diskussion über Wirtschaftskrise\n- **International**: Hamas-Festnahmen in Deutschland, Generalstreik in Italien\n- **Wirtschaft**: Tesla-Verkaufszahlen, Klage gegen Temu\n- **Wissenschaft**: Saturn-Mond Enceladus könnte Leben beherbergen\n- **Kultur**: Neuverfilmung von "Momo"', - }, - }, - ], - api: "anthropic-messages", - provider: "anthropic", - model: "claude-sonnet-4-5-20250929", - usage: { - input: 19586, - output: 3462, - cacheRead: 0, - cacheWrite: 0, - totalTokens: 0, - cost: { - input: 0.058758000000000005, - output: 0.051930000000000004, - cacheRead: 0, - cacheWrite: 0, - totalTokens: 0, - total: 0.11068800000000001, - }, - }, - stopReason: "toolUse", - }, - { - role: "toolResult", - toolCallId: "toolu_01BtH9H2BvwxvKjLw5iHXcZC", - toolName: "artifacts", - output: "Created file news_today.md", - isError: false, - }, - { - role: "assistant", - content: [ - { - type: "text", - text: "\n\nPerfect! I've created a markdown document with today's news from Der Standard (Austrian news site). The document includes:\n\n✅ **18 news articles** from Friday, October 3, 2025\n✅ **Images** where available (many articles have accompanying photos)\n✅ **Descriptions** for most articles\n✅ **Links** to read the full articles\n✅ **Summary section** at the end highlighting the main topics\n\nThe top stories include:\n- Ukraine war updates (Russian attacks on energy infrastructure)\n- Security incidents (Munich airport closure due to drone, suspicious drones over Denmark)\n- Austrian politics (SPÖ conflict, economic discussion)\n- International news (Hamas arrests in Germany, general strike in Italy)\n- Business (Tesla sales, lawsuit against Temu)\n- Science (Saturn's moon Enceladus may support life)\n- Culture (Momo film remake)\n\nThe markdown file is now available for you to view, download, or share!", - }, - ], - api: "anthropic-messages", - provider: "anthropic", - model: "claude-sonnet-4-5-20250929", - usage: { - input: 23065, - output: 223, - cacheRead: 0, - cacheWrite: 0, - totalTokens: 0, - cost: { - input: 0.069195, - output: 0.003345, - cacheRead: 0, - cacheWrite: 0, - totalTokens: 0, - total: 0.07254000000000001, - }, - }, - stopReason: "stop", - }, - { - role: "user", - content: [ - { - type: "text", - text: "wonder if you can read the curent page contents it's a weird ui", - }, - ], - }, - { - role: "assistant", - content: [ - { - type: "text", - text: "Let me investigate the current page structure and see what's there:", - }, - { - type: "toolCall", - id: "toolu_016BX1izZEsfXenpcMCioKSA", - name: "browser_javascript", - arguments: { - code: "// Get basic page info\nconsole.log('Current URL:', window.location.href);\nconsole.log('Page title:', document.title);\n\n// Get the full text content\nconst bodyText = document.body.innerText;\nconsole.log('Page text length:', bodyText.length);\nconsole.log('First 1000 characters:', bodyText.substring(0, 1000));\n\n// Look for main content areas\nconst mainElements = document.querySelectorAll('main, #main, .main, [role=\"main\"]');\nconsole.log('Main elements found:', mainElements.length);\n\n// Check for specific UI framework indicators\nconst reactRoot = document.querySelector('#root, [id*=\"root\"], [id*=\"app\"]');\nconsole.log('React/App root found:', reactRoot ? 'Yes' : 'No');\n\n// Look for chat-like elements\nconst messages = document.querySelectorAll('[class*=\"message\"], [class*=\"chat\"], [class*=\"conversation\"]');\nconsole.log('Message-like elements:', messages.length);\n\n// Get all visible text in chunks\nconsole.log('\\n--- Full visible text ---');\nconsole.log(bodyText);\n", - }, - }, - ], - api: "anthropic-messages", - provider: "anthropic", - model: "claude-sonnet-4-5-20250929", - usage: { - input: 23306, - output: 335, - cacheRead: 0, - cacheWrite: 0, - totalTokens: 0, - cost: { - input: 0.06991800000000001, - output: 0.005025, - cacheRead: 0, - cacheWrite: 0, - totalTokens: 0, - total: 0.07494300000000001, - }, - }, - stopReason: "toolUse", - }, - { - role: "toolResult", - toolCallId: "toolu_016BX1izZEsfXenpcMCioKSA", - toolName: "browser_javascript", - output: - 'Current URL: https://www.derstandard.at/jetzt/livebericht/3000000290448/lage-an-akw-saporischschja-laut-selenskyj-kritisch?ref=niewidget\nPage title: Russland greift ukrainische Energieinfrastruktur an - Livebericht - Ukraine\nPage text length: 2314\nFirst 1000 characters: International\nEuropa\nUkraine\n \nLIVE\n299 POSTINGS\nSie sind hier, ‘wischen‘ für ersten Beitrag\nVibration ein Hilfe\nEinstellungen\n3. Oktober 2025, 10:31 / Helene Dallinger, Isadora Wallnöfer / LIVEBERICHT\nRussland greift ukrainische Energieinfrastruktur an\nDie Regionen Poltawa und Charkiw sind besonders betroffen. Nach Wladimir Putins Auftritt beim Waldai-Forum in Sotschi wurde in der Stadt Drohnenalarm ausgelöst\nJETZT LIVE MITLESEN & POSTEN \n\nDas Wichtigste in Kürze:\n\nDas russische Militär hat ukrainische Energieanlagen in mehreren Gebieten mit Drohnen und Raketen angegriffen – besonders in den Regionen Poltawa und Charkiw.\nNach Wladimir Putins Auftritt beim Waldai-Forum in Sotschi wurde Drohnenalarm ausgelöst, zwei Flughäfen mussten den Betrieb einstellen.\nBei einem Gipfel der Europäischen Politischen Gemeinschaft (EPG) in Kopenhagen warnte der ukrainische Präsident Wolodymyr Selenskyj Europa vor der Bedrohung durch russische Drohnen.\nRussland und die Ukraine tauschten jeweils 185 Krieg\nMain elements found: 1\nReact/App root found: No\nMessage-like elements: 3\n\n--- Full visible text ---\nInternational\nEuropa\nUkraine\n \nLIVE\n299 POSTINGS\nSie sind hier, ‘wischen‘ für ersten Beitrag\nVibration ein Hilfe\nEinstellungen\n3. Oktober 2025, 10:31 / Helene Dallinger, Isadora Wallnöfer / LIVEBERICHT\nRussland greift ukrainische Energieinfrastruktur an\nDie Regionen Poltawa und Charkiw sind besonders betroffen. Nach Wladimir Putins Auftritt beim Waldai-Forum in Sotschi wurde in der Stadt Drohnenalarm ausgelöst\nJETZT LIVE MITLESEN & POSTEN \n\nDas Wichtigste in Kürze:\n\nDas russische Militär hat ukrainische Energieanlagen in mehreren Gebieten mit Drohnen und Raketen angegriffen – besonders in den Regionen Poltawa und Charkiw.\nNach Wladimir Putins Auftritt beim Waldai-Forum in Sotschi wurde Drohnenalarm ausgelöst, zwei Flughäfen mussten den Betrieb einstellen.\nBei einem Gipfel der Europäischen Politischen Gemeinschaft (EPG) in Kopenhagen warnte der ukrainische Präsident Wolodymyr Selenskyj Europa vor der Bedrohung durch russische Drohnen.\nRussland und die Ukraine tauschten jeweils 185 Kriegsgefangene.\nDie Lage in dem von Russland kontrollierten und seit nunmehr sieben Tagen vom Stromnetz getrennten Atomkraftwerk Saporischschja ist nach Angaben Selenskyjs "kritisch". Zudem fiel an der Schutzhülle um den zerstörten Reaktorblock des stillgelegten Kernkraftwerks Tschernobyl laut Kyjiw zeitweise der Strom aus.\nDen Livebericht von Donnerstag finden Sie hier.\nWir verwenden neben eigenen Recherchen Material der Nachrichtenagenturen APA, dpa und Reuters sowie internationaler Medien. Angaben über den Kriegsverlauf und Opferzahlen stammen von den Behörden der Kriegsparteien und lassen sich oft nicht unabhängig überprüfen. (red, 3.10.2025)\nHelene Dallinger vor 4 Stunden\nGuten Morgen, liebe Leser:innen!\nIch darf Sie heute wieder zu unserer Live-Berichterstattung über den Ukrainekrieg begrüßen - wir beginnen in Kürze. Hier können Sie in der Zwischenzeit die Geschehnisse von gestern nachlesen. Ich wünsche Ihnen einen angenehmen Start in den Freitag!\nBeitrag teilen\n29 Postings\nVoriger Beitrag\nNächster Beitrag\n© STANDARD Verlagsgesellschaft m.b.H. 2025\n\nAlle Rechte vorbehalten. Nutzung ausschließlich für den privaten Eigenbedarf.\nEine Weiterverwendung und Reproduktion über den persönlichen Gebrauch hinaus ist nicht gestattet.\n\nderStandard.de\nderStandard.at\nImpressum & Offenlegung\nDatenschutz\nAGB', - details: { - files: [], - }, - isError: false, - }, - { - role: "assistant", - content: [ - { - type: "text", - text: "\n\nInteresting! You're on a live blog (\"Livebericht\") about the Ukraine war. The UI is indeed unique - it's a live ticker/blog format. Let me explore the structure more to understand the layout:", - }, - { - type: "toolCall", - id: "toolu_01JFdwqjQKdH6qL4qbotXDGH", - name: "browser_javascript", - arguments: { - code: "// Let's examine the DOM structure more carefully\nconsole.log('=== DOM STRUCTURE ===');\n\n// Look for the main container\nconst body = document.body;\nconsole.log('Body children:', body.children.length);\n\n// Get all elements with classes\nconst allElements = document.querySelectorAll('*');\nconsole.log('Total elements:', allElements.length);\n\n// Look for articles or post containers\nconst articles = document.querySelectorAll('article');\nconst sections = document.querySelectorAll('section');\nconst divs = document.querySelectorAll('div');\n\nconsole.log('Articles:', articles.length);\nconsole.log('Sections:', sections.length);\nconsole.log('Divs:', divs.length);\n\n// Try to find the live blog posts\nconst liveElements = document.querySelectorAll('[class*=\"live\"], [class*=\"post\"], [class*=\"entry\"], [class*=\"update\"]');\nconsole.log('Live/post elements:', liveElements.length);\n\n// Check for specific time stamps\nconst timeElements = document.querySelectorAll('time, [class*=\"time\"], [class*=\"date\"]');\nconsole.log('Time elements:', timeElements.length);\n\n// Let's look at the main element's HTML structure (first 2000 chars)\nconst main = document.querySelector('main');\nif (main) {\n console.log('\\n=== MAIN ELEMENT HTML ===');\n console.log(main.innerHTML.substring(0, 2000));\n}\n\n// Check body classes and IDs\nconsole.log('\\n=== BODY INFO ===');\nconsole.log('Body class:', body.className);\nconsole.log('Body id:', body.id);\n", - }, - }, - ], - api: "anthropic-messages", - provider: "anthropic", - model: "claude-sonnet-4-5-20250929", - usage: { - input: 25012, - output: 499, - cacheRead: 0, - cacheWrite: 0, - totalTokens: 0, - cost: { - input: 0.075036, - output: 0.007485, - cacheRead: 0, - cacheWrite: 0, - totalTokens: 0, - total: 0.08252100000000001, - }, - }, - stopReason: "toolUse", - }, - { - role: "toolResult", - toolCallId: "toolu_01JFdwqjQKdH6qL4qbotXDGH", - toolName: "browser_javascript", - output: - '=== DOM STRUCTURE ===\nBody children: 24\nTotal elements: 368\nArticles: 0\nSections: 0\nDivs: 68\nLive/post elements: 31\nTime elements: 5\n\n=== MAIN ELEMENT HTML ===\n\n \n\n\n\n\n", - }, - }, - ], - api: "anthropic-messages", - provider: "anthropic", - model: "claude-sonnet-4-5-20250929", - usage: { - input: 30699, - output: 162, - cacheRead: 0, - cacheWrite: 0, - totalTokens: 0, - cost: { - input: 0.092097, - output: 0.00243, - cacheRead: 0, - cacheWrite: 0, - totalTokens: 0, - total: 0.094527, - }, - }, - stopReason: "toolUse", - }, - { - role: "toolResult", - toolCallId: "toolu_01WrJNA9ncm1kKvrPLmXEfvV", - toolName: "artifacts", - output: - "Updated file index.html\n\nExecution timed out. Partial logs:\n[log] Page loaded successfully!\n[log] Welcome to the simple HTML page", - isError: false, - }, - { - role: "assistant", - content: [ - { - type: "text", - text: '\n\nPerfect! I\'ve added 2 console log statements to index.html. The logs show they\'re working:\n1. "Page loaded successfully!"\n2. "Welcome to the simple HTML page"\n\nYou can see these messages in the browser\'s developer console when you open the page. ✅', - }, - ], - api: "anthropic-messages", - provider: "anthropic", - model: "claude-sonnet-4-5-20250929", - usage: { - input: 30906, - output: 67, - cacheRead: 0, - cacheWrite: 0, - totalTokens: 0, - cost: { - input: 0.09271800000000001, - output: 0.001005, - cacheRead: 0, - cacheWrite: 0, - totalTokens: 0, - total: 0.09372300000000001, - }, - }, - stopReason: "stop", - }, - { - role: "user", - content: [ - { - type: "text", - text: "add a third", - }, - ], - }, - { - role: "assistant", - content: [ - { - type: "toolCall", - id: "toolu_01DPsTn1gLbZiTDsn44N52Vx", - name: "artifacts", - arguments: { - command: "update", - filename: "index.html", - old_str: - " ", - new_str: - " ", - }, - }, - ], - api: "anthropic-messages", - provider: "anthropic", - model: "claude-sonnet-4-5-20250929", - usage: { - input: 30979, - output: 182, - cacheRead: 0, - cacheWrite: 0, - totalTokens: 0, - cost: { - input: 0.092937, - output: 0.0027300000000000002, - cacheRead: 0, - cacheWrite: 0, - totalTokens: 0, - total: 0.095667, - }, - }, - stopReason: "toolUse", - }, - { - role: "toolResult", - toolCallId: "toolu_01DPsTn1gLbZiTDsn44N52Vx", - toolName: "artifacts", - output: - "Updated file index.html\n\nExecution timed out. Partial logs:\n[log] Page loaded successfully!\n[log] Welcome to the simple HTML page\n[log] Third console log added!", - isError: false, - }, - { - role: "assistant", - content: [ - { - type: "text", - text: '\n\nDone! Added a third console log statement: "Third console log added!" \n\nAll three logs are now firing successfully! 🎉', - }, - ], - api: "anthropic-messages", - provider: "anthropic", - model: "claude-sonnet-4-5-20250929", - usage: { - input: 31214, - output: 33, - cacheRead: 0, - cacheWrite: 0, - totalTokens: 0, - cost: { - input: 0.093642, - output: 0.000495, - cacheRead: 0, - cacheWrite: 0, - totalTokens: 0, - total: 0.094137, - }, - }, - stopReason: "stop", - }, - ], -}; diff --git a/packages/web-ui/tsconfig.build.json b/packages/web-ui/tsconfig.build.json deleted file mode 100644 index 3b548741..00000000 --- a/packages/web-ui/tsconfig.build.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2022", - "module": "ES2022", - "lib": ["ES2022", "DOM", "DOM.Iterable"], - "moduleResolution": "bundler", - "declaration": true, - "declarationMap": true, - "sourceMap": true, - "strict": true, - "skipLibCheck": true, - "esModuleInterop": true, - "allowSyntheticDefaultImports": true, - "allowImportingTsExtensions": true, - "rewriteRelativeImportExtensions": true, - "experimentalDecorators": true, - "useDefineForClassFields": false, - "rootDir": "./src", - "outDir": "./dist" - }, - "include": ["src/**/*"] -} diff --git a/packages/web-ui/tsconfig.json b/packages/web-ui/tsconfig.json deleted file mode 100644 index 25e8cea9..00000000 --- a/packages/web-ui/tsconfig.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "extends": "../../tsconfig.base.json", - "compilerOptions": { - "noEmit": true - }, - "include": ["src/**/*"] -} diff --git a/scripts/browser-smoke-entry.ts b/scripts/browser-smoke-entry.ts index a4057c93..066a6099 100644 --- a/scripts/browser-smoke-entry.ts +++ b/scripts/browser-smoke-entry.ts @@ -1,4 +1,60 @@ -import { complete, getModel } from "@earendil-works/pi-ai"; +import { complete, createAssistantMessageEventStream, getModel, getProviders, Type } from "@earendil-works/pi-ai"; +import { + Agent, + bashExecutionToText, + convertToLlm, + createCustomMessage, + FileError, + formatPromptTemplateInvocation, + formatSkillInvocation, + formatSkillsForSystemPrompt, + getOrThrow, + InMemorySessionRepo, + ok, + parseCommandArgs, + streamProxy, + toError, + truncateHead, +} from "@earendil-works/pi-agent-core"; +// Keep this entry browser-safe. It is bundled by scripts/check-browser-smoke.mjs +// to catch accidental Node-only runtime imports in browser-facing package exports. const model = getModel("google", "gemini-2.5-flash"); -console.log(model.id, typeof complete); +const schema = Type.Object({ prompt: Type.String() }); +const stream = createAssistantMessageEventStream(); + +const agent = new Agent({ initialState: { model } }); +agent.steer({ role: "user", content: [{ type: "text", text: "queued" }], timestamp: 0 }); +const repo = new InMemorySessionRepo(); +const result = getOrThrow(ok({ value: 1 })); +const customMessage = createCustomMessage("note", "hello", true, undefined, "2026-01-01T00:00:00.000Z"); +const llmMessages = convertToLlm([customMessage]); +const skill = { name: "browser-safe", description: "Smoke test", content: "Use browser APIs.", filePath: "/skills/browser-safe/SKILL.md" }; + +console.log( + model.id, + getProviders().length, + typeof complete, + schema.type, + typeof stream.push, + agent.hasQueuedMessages(), + typeof repo.create, + result.value, + llmMessages.length, + bashExecutionToText({ + role: "bashExecution", + command: "echo ok", + output: "ok", + exitCode: 0, + cancelled: false, + truncated: false, + timestamp: 0, + }), + formatSkillsForSystemPrompt([skill]).length, + formatSkillInvocation(skill).length, + formatPromptTemplateInvocation({ name: "example", content: "$1 $@" }, parseCommandArgs('one "two three"')), + truncateHead("a\nb", { maxLines: 1 }).content, + new FileError("not_found", "missing").code, + toError("boom").message, + typeof streamProxy, +); diff --git a/tsconfig.json b/tsconfig.json index ff748f9e..4dfbf3d2 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -16,12 +16,10 @@ "typebox": ["./node_modules/typebox"], "@earendil-works/pi-tui": ["./packages/tui/src/index.ts"], "@earendil-works/pi-tui/*": ["./packages/tui/src/*"], - "@earendil-works/pi-web-ui": ["./packages/web-ui/src/index.ts"], - "@earendil-works/pi-web-ui/*": ["./packages/web-ui/src/*"], "@earendil-works/pi-agent-old": ["./packages/agent-old/src/index.ts"], "@earendil-works/pi-agent-old/*": ["./packages/agent-old/src/*"] } }, "include": ["packages/*/src/**/*", "packages/*/test/**/*", "packages/coding-agent/examples/**/*"], - "exclude": ["packages/web-ui/**/*", "**/dist/**"] + "exclude": ["**/dist/**"] } From 04e93af5c4fcee5ab3be35177940c720ac6f8b53 Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Wed, 20 May 2026 02:26:01 +0200 Subject: [PATCH 131/166] chore: run pi test script with node --- pi-test.sh | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/pi-test.sh b/pi-test.sh index ece44929..37ff0114 100755 --- a/pi-test.sh +++ b/pi-test.sh @@ -54,10 +54,4 @@ if [[ "$NO_ENV" == "true" ]]; then echo "Running without API keys..." fi -TSX_BIN="$SCRIPT_DIR/node_modules/.bin/tsx" -if [[ ! -x "$TSX_BIN" ]]; then - echo "tsx not found at $TSX_BIN. Run npm install from the repo root first." >&2 - exit 1 -fi - -"$TSX_BIN" "$SCRIPT_DIR/packages/coding-agent/src/cli.ts" ${ARGS[@]+"${ARGS[@]}"} +node "$SCRIPT_DIR/packages/coding-agent/src/cli.ts" ${ARGS[@]+"${ARGS[@]}"} From 9fdc54b6f09a10f7e67aa5c6c465e52981463ee4 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Wed, 20 May 2026 11:40:46 +0200 Subject: [PATCH 132/166] fix(package-manager): ignore managed npm folders in cloud sync closes #4763 --- packages/coding-agent/CHANGELOG.md | 1 + .../coding-agent/src/core/package-manager.ts | 3 ++- packages/coding-agent/src/utils/paths.ts | 18 ++++++++++++++++++ 3 files changed, 21 insertions(+), 1 deletion(-) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 479b5560..582015c0 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -14,6 +14,7 @@ - Fixed AgentSession retry, compaction, and event settlement to use the awaited agent lifecycle instead of a separate event queue, and added `willRetry` to `agent_end` session events. - Fixed the subagent extension's parallel mode to return useful per-task output and failed-task diagnostics to the parent model instead of 100-character previews ([#4710](https://github.com/earendil-works/pi/issues/4710)). - Fixed Windows local bash execution to hide helper console windows when launched from background SDK processes ([#4699](https://github.com/earendil-works/pi/issues/4699)). +- Fixed managed npm extension folders to set cloud-sync ignore metadata where supported ([#4763](https://github.com/earendil-works/pi/issues/4763)). ## [0.75.3] - 2026-05-18 diff --git a/packages/coding-agent/src/core/package-manager.ts b/packages/coding-agent/src/core/package-manager.ts index 59d53427..90c41942 100644 --- a/packages/coding-agent/src/core/package-manager.ts +++ b/packages/coding-agent/src/core/package-manager.ts @@ -30,7 +30,7 @@ import { minimatch } from "minimatch"; import { CONFIG_DIR_NAME } from "../config.ts"; import { spawnProcess, spawnProcessSync } from "../utils/child-process.ts"; import { type GitSource, parseGitUrl } from "../utils/git.ts"; -import { canonicalizePath, isLocalPath } from "../utils/paths.ts"; +import { canonicalizePath, isLocalPath, markPathIgnoredByCloudSync } from "../utils/paths.ts"; import { isStdoutTakenOver } from "./output-guard.ts"; import type { PackageSource, SettingsManager } from "./settings-manager.ts"; @@ -1822,6 +1822,7 @@ export class DefaultPackageManager implements PackageManager { if (!existsSync(installRoot)) { mkdirSync(installRoot, { recursive: true }); } + markPathIgnoredByCloudSync(installRoot); this.ensureGitIgnore(installRoot); const packageJsonPath = join(installRoot, "package.json"); if (!existsSync(packageJsonPath)) { diff --git a/packages/coding-agent/src/utils/paths.ts b/packages/coding-agent/src/utils/paths.ts index ef36de7a..fd8d176e 100644 --- a/packages/coding-agent/src/utils/paths.ts +++ b/packages/coding-agent/src/utils/paths.ts @@ -1,5 +1,6 @@ import { realpathSync } from "node:fs"; import { isAbsolute, relative, resolve as resolvePath, sep } from "node:path"; +import { spawnProcessSync } from "./child-process.ts"; /** * Resolve a path to its canonical (real) form, following symlinks. @@ -55,3 +56,20 @@ export function formatPathRelativeToCwdOrAbsolute(filePath: string, cwd: string) const absolutePath = resolveAgainstCwd(filePath, cwd); return (getCwdRelativePath(absolutePath, cwd) ?? absolutePath).split(sep).join("/"); } + +export function markPathIgnoredByCloudSync(path: string): void { + const attrs = + process.platform === "darwin" + ? ["com.dropbox.ignored", "com.apple.fileprovider.ignore#P"] + : process.platform === "linux" + ? ["user.com.dropbox.ignored"] + : []; + + for (const attr of attrs) { + if (process.platform === "darwin") { + spawnProcessSync("xattr", ["-w", attr, "1", path], { encoding: "utf-8", stdio: "ignore" }); + } else { + spawnProcessSync("setfattr", ["-n", attr, "-v", "1", path], { encoding: "utf-8", stdio: "ignore" }); + } + } +} From ac58c6f44926fbe82c3431b5e71ccd756511dae9 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 20 May 2026 09:54:27 +0000 Subject: [PATCH 133/166] chore: approve contributor mbazso --- .github/APPROVED_CONTRIBUTORS | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/APPROVED_CONTRIBUTORS b/.github/APPROVED_CONTRIBUTORS index 8ddcd120..faefe5ae 100644 --- a/.github/APPROVED_CONTRIBUTORS +++ b/.github/APPROVED_CONTRIBUTORS @@ -213,3 +213,5 @@ abhinavmathur-atlan pr mattiacerutti pr josephyoung pr + +mbazso pr From f1f7cd50c35dbcb922ea074e8a7f141e20c2952a Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Wed, 20 May 2026 12:37:36 +0200 Subject: [PATCH 134/166] test: speed up provider payload coverage --- .../test/anthropic-thinking-disable.test.ts | 15 ++- .../ai/test/bedrock-thinking-payload.test.ts | 19 ++-- packages/ai/test/cache-retention.test.ts | 102 +++++++++--------- packages/ai/test/openai-codex-stream.test.ts | 28 +++-- test.sh | 14 ++- 5 files changed, 106 insertions(+), 72 deletions(-) diff --git a/packages/ai/test/anthropic-thinking-disable.test.ts b/packages/ai/test/anthropic-thinking-disable.test.ts index 8f871514..ea53043c 100644 --- a/packages/ai/test/anthropic-thinking-disable.test.ts +++ b/packages/ai/test/anthropic-thinking-disable.test.ts @@ -1,13 +1,20 @@ import { describe, expect, it } from "vitest"; -import { getModel } from "../src/models.js"; -import { streamSimple } from "../src/stream.js"; -import type { Context, Model, SimpleStreamOptions } from "../src/types.js"; +import { getModel } from "../src/models.ts"; +import { streamSimple } from "../src/stream.ts"; +import type { Context, Model, SimpleStreamOptions } from "../src/types.ts"; interface AnthropicThinkingPayload { thinking?: { type: string; budget_tokens?: number; display?: string }; output_config?: { effort?: string }; } +class PayloadCaptured extends Error { + constructor() { + super("payload captured"); + this.name = "PayloadCaptured"; + } +} + function makePayloadCaptureContext(): Context { return { messages: [{ role: "user", content: "Hello", timestamp: Date.now() }], @@ -29,7 +36,7 @@ async function capturePayload( apiKey: "fake-key", onPayload: (payload) => { capturedPayload = payload as AnthropicThinkingPayload; - return payload; + throw new PayloadCaptured(); }, }); diff --git a/packages/ai/test/bedrock-thinking-payload.test.ts b/packages/ai/test/bedrock-thinking-payload.test.ts index 63ed8845..55b9e82b 100644 --- a/packages/ai/test/bedrock-thinking-payload.test.ts +++ b/packages/ai/test/bedrock-thinking-payload.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it } from "vitest"; -import { getModel } from "../src/models.js"; -import { type BedrockOptions, streamBedrock } from "../src/providers/amazon-bedrock.js"; -import type { Context, Model } from "../src/types.js"; +import { getModel } from "../src/models.ts"; +import { type BedrockOptions, streamBedrock } from "../src/providers/amazon-bedrock.ts"; +import type { Context, Model } from "../src/types.ts"; interface BedrockThinkingPayload { additionalModelRequestFields?: { @@ -11,6 +11,13 @@ interface BedrockThinkingPayload { }; } +class PayloadCaptured extends Error { + constructor() { + super("payload captured"); + this.name = "PayloadCaptured"; + } +} + function makeContext(): Context { return { messages: [{ role: "user", content: "Hello", timestamp: Date.now() }], @@ -25,10 +32,9 @@ async function capturePayload( const s = streamBedrock(model, makeContext(), { ...options, reasoning: options?.reasoning ?? "high", - signal: AbortSignal.abort(), onPayload: (payload) => { capturedPayload = payload as BedrockThinkingPayload; - return payload; + throw new PayloadCaptured(); }, }); @@ -137,10 +143,9 @@ describe("Application inference profile support", () => { messages: [{ role: "user", content: "Hello", timestamp: Date.now() }], }, { - signal: AbortSignal.abort(), onPayload: (payload) => { capturedPayload = payload; - return payload; + throw new PayloadCaptured(); }, }, ); diff --git a/packages/ai/test/cache-retention.test.ts b/packages/ai/test/cache-retention.test.ts index 2f234ab0..8828ba86 100644 --- a/packages/ai/test/cache-retention.test.ts +++ b/packages/ai/test/cache-retention.test.ts @@ -1,7 +1,24 @@ import { afterEach, beforeEach, describe, expect, it } from "vitest"; -import { getModel } from "../src/models.js"; -import { stream } from "../src/stream.js"; -import type { Context, Model } from "../src/types.js"; +import { getModel } from "../src/models.ts"; +import { streamAnthropic } from "../src/providers/anthropic.ts"; +import { streamOpenAICompletions } from "../src/providers/openai-completions.ts"; +import { streamOpenAIResponses } from "../src/providers/openai-responses.ts"; +import { stream } from "../src/stream.ts"; +import type { Context, Model } from "../src/types.ts"; + +class PayloadCaptured extends Error { + constructor() { + super("payload captured"); + this.name = "PayloadCaptured"; + } +} + +function stopAfterPayload(capture: (payload: TPayload) => void): (payload: unknown) => never { + return (payload: unknown): never => { + capture(payload as TPayload); + throw new PayloadCaptured(); + }; +} describe("Cache Retention (PI_CACHE_RETENTION)", () => { const originalEnv = process.env.PI_CACHE_RETENTION; @@ -31,9 +48,9 @@ describe("Cache Retention (PI_CACHE_RETENTION)", () => { let capturedPayload: any = null; const s = stream(model, context, { - onPayload: (payload) => { + onPayload: stopAfterPayload((payload) => { capturedPayload = payload; - }, + }), }); // Consume the stream to trigger the request @@ -54,9 +71,9 @@ describe("Cache Retention (PI_CACHE_RETENTION)", () => { let capturedPayload: any = null; const s = stream(model, context, { - onPayload: (payload) => { + onPayload: stopAfterPayload((payload) => { capturedPayload = payload; - }, + }), }); // Consume the stream to trigger the request @@ -88,14 +105,13 @@ describe("Cache Retention (PI_CACHE_RETENTION)", () => { // Since we can't easily test this without mocking, we'll skip the actual API call // and just verify the helper logic works correctly - const { streamAnthropic } = await import("../src/providers/anthropic.js"); try { const s = streamAnthropic(proxyModel, context, { apiKey: "fake-key", - onPayload: (payload) => { + onPayload: stopAfterPayload((payload) => { capturedPayload = payload; - }, + }), }); // This will fail since we're using a fake key and fake proxy, but the payload should be captured @@ -119,15 +135,13 @@ describe("Cache Retention (PI_CACHE_RETENTION)", () => { }; let capturedPayload: any = null; - const { streamAnthropic } = await import("../src/providers/anthropic.js"); - try { const s = streamAnthropic(proxyModel, context, { apiKey: "fake-key", cacheRetention: "long", - onPayload: (payload) => { + onPayload: stopAfterPayload((payload) => { capturedPayload = payload; - }, + }), }); for await (const event of s) { @@ -145,15 +159,13 @@ describe("Cache Retention (PI_CACHE_RETENTION)", () => { const baseModel = getModel("anthropic", "claude-haiku-4-5"); let capturedPayload: any = null; - const { streamAnthropic } = await import("../src/providers/anthropic.js"); - try { const s = streamAnthropic(baseModel, context, { apiKey: "fake-key", cacheRetention: "none", - onPayload: (payload) => { + onPayload: stopAfterPayload((payload) => { capturedPayload = payload; - }, + }), }); for await (const event of s) { @@ -171,14 +183,12 @@ describe("Cache Retention (PI_CACHE_RETENTION)", () => { const baseModel = getModel("anthropic", "claude-haiku-4-5"); let capturedPayload: any = null; - const { streamAnthropic } = await import("../src/providers/anthropic.js"); - try { const s = streamAnthropic(baseModel, context, { apiKey: "fake-key", - onPayload: (payload) => { + onPayload: stopAfterPayload((payload) => { capturedPayload = payload; - }, + }), }); for await (const event of s) { @@ -199,15 +209,13 @@ describe("Cache Retention (PI_CACHE_RETENTION)", () => { const baseModel = getModel("anthropic", "claude-haiku-4-5"); let capturedPayload: any = null; - const { streamAnthropic } = await import("../src/providers/anthropic.js"); - try { const s = streamAnthropic(baseModel, context, { apiKey: "fake-key", cacheRetention: "long", - onPayload: (payload) => { + onPayload: stopAfterPayload((payload) => { capturedPayload = payload; - }, + }), }); for await (const event of s) { @@ -230,9 +238,9 @@ describe("Cache Retention (PI_CACHE_RETENTION)", () => { let capturedPayload: any = null; const s = stream(model, context, { - onPayload: (payload) => { + onPayload: stopAfterPayload((payload) => { capturedPayload = payload; - }, + }), }); // Consume the stream to trigger the request @@ -253,9 +261,9 @@ describe("Cache Retention (PI_CACHE_RETENTION)", () => { let capturedPayload: any = null; const s = stream(model, context, { - onPayload: (payload) => { + onPayload: stopAfterPayload((payload) => { capturedPayload = payload; - }, + }), }); // Consume the stream to trigger the request @@ -280,14 +288,12 @@ describe("Cache Retention (PI_CACHE_RETENTION)", () => { let capturedPayload: any = null; - const { streamOpenAIResponses } = await import("../src/providers/openai-responses.js"); - try { const s = streamOpenAIResponses(proxyModel, context, { apiKey: "fake-key", - onPayload: (payload) => { + onPayload: stopAfterPayload((payload) => { capturedPayload = payload; - }, + }), }); // This will fail since we're using a fake key and fake proxy, but the payload should be captured @@ -309,16 +315,14 @@ describe("Cache Retention (PI_CACHE_RETENTION)", () => { }; let capturedPayload: any = null; - const { streamOpenAIResponses } = await import("../src/providers/openai-responses.js"); - try { const s = streamOpenAIResponses(model, context, { apiKey: "fake-key", cacheRetention: "long", sessionId: "session-compat-false", - onPayload: (payload) => { + onPayload: stopAfterPayload((payload) => { capturedPayload = payload; - }, + }), }); for await (const event of s) { @@ -336,16 +340,14 @@ describe("Cache Retention (PI_CACHE_RETENTION)", () => { const model = getModel("openai", "gpt-4o-mini"); let capturedPayload: any = null; - const { streamOpenAIResponses } = await import("../src/providers/openai-responses.js"); - try { const s = streamOpenAIResponses(model, context, { apiKey: "fake-key", cacheRetention: "none", sessionId: "session-1", - onPayload: (payload) => { + onPayload: stopAfterPayload((payload) => { capturedPayload = payload; - }, + }), }); for await (const event of s) { @@ -364,16 +366,14 @@ describe("Cache Retention (PI_CACHE_RETENTION)", () => { const model = getModel("openai", "gpt-4o-mini"); let capturedPayload: any = null; - const { streamOpenAIResponses } = await import("../src/providers/openai-responses.js"); - try { const s = streamOpenAIResponses(model, context, { apiKey: "fake-key", cacheRetention: "long", sessionId: "session-2", - onPayload: (payload) => { + onPayload: stopAfterPayload((payload) => { capturedPayload = payload; - }, + }), }); for await (const event of s) { @@ -408,16 +408,15 @@ describe("Cache Retention (PI_CACHE_RETENTION)", () => { it("should set prompt_cache_retention for non-api.openai.com baseUrl by default", async () => { let capturedPayload: any = null; - const { streamOpenAICompletions } = await import("../src/providers/openai-completions.js"); try { const s = streamOpenAICompletions(createCompletionsModel(), context, { apiKey: "fake-key", cacheRetention: "long", sessionId: "session-completions", - onPayload: (payload) => { + onPayload: stopAfterPayload((payload) => { capturedPayload = payload; - }, + }), }); for await (const event of s) { @@ -434,16 +433,15 @@ describe("Cache Retention (PI_CACHE_RETENTION)", () => { it("should omit prompt_cache_retention when supportsLongCacheRetention is false", async () => { let capturedPayload: any = null; - const { streamOpenAICompletions } = await import("../src/providers/openai-completions.js"); try { const s = streamOpenAICompletions(createCompletionsModel({ supportsLongCacheRetention: false }), context, { apiKey: "fake-key", cacheRetention: "long", sessionId: "session-completions-false", - onPayload: (payload) => { + onPayload: stopAfterPayload((payload) => { capturedPayload = payload; - }, + }), }); for await (const event of s) { diff --git a/packages/ai/test/openai-codex-stream.test.ts b/packages/ai/test/openai-codex-stream.test.ts index 5c54fdb4..e50111c9 100644 --- a/packages/ai/test/openai-codex-stream.test.ts +++ b/packages/ai/test/openai-codex-stream.test.ts @@ -7,8 +7,8 @@ import { resetOpenAICodexWebSocketDebugStats, streamOpenAICodexResponses, streamSimpleOpenAICodexResponses, -} from "../src/providers/openai-codex-responses.js"; -import type { Context, Model } from "../src/types.js"; +} from "../src/providers/openai-codex-responses.ts"; +import type { Context, Model } from "../src/types.ts"; const originalAgentDir = process.env.PI_CODING_AGENT_DIR; @@ -173,7 +173,7 @@ describe("openai-codex streaming", () => { messages: [{ role: "user", content: "Say hello", timestamp: Date.now() }], }; - const streamResult = streamOpenAICodexResponses(model, context, { apiKey: token }); + const streamResult = streamOpenAICodexResponses(model, context, { apiKey: token, transport: "sse" }); let sawTextDelta = false; let sawDone = false; @@ -407,7 +407,7 @@ describe("openai-codex streaming", () => { messages: [{ role: "user", content: "Say hello", timestamp: Date.now() }], }; - const streamResult = streamOpenAICodexResponses(model, context, { apiKey: token, sessionId }); + const streamResult = streamOpenAICodexResponses(model, context, { apiKey: token, sessionId, transport: "sse" }); await streamResult.result(); }); @@ -513,7 +513,11 @@ describe("openai-codex streaming", () => { messages: [{ role: "user", content: "Say hello", timestamp: Date.now() }], }; - await streamSimpleOpenAICodexResponses(model, context, { apiKey: token, reasoning: "xhigh" }).result(); + await streamSimpleOpenAICodexResponses(model, context, { + apiKey: token, + reasoning: "xhigh", + transport: "sse", + }).result(); expect(requestedReasoning).toEqual({ effort: "xhigh", summary: "auto" }); }); @@ -559,6 +563,7 @@ describe("openai-codex streaming", () => { })}`, ].join("\n\n")}\n\n`; + let requestedReasoning: unknown; const encoder = new TextEncoder(); const stream = new ReadableStream({ start(controller) { @@ -577,7 +582,7 @@ describe("openai-codex streaming", () => { } if (url === "https://chatgpt.com/backend-api/codex/responses") { const body = typeof init?.body === "string" ? (JSON.parse(init.body) as Record) : null; - expect(body?.reasoning).toEqual({ effort: "low", summary: "auto" }); + requestedReasoning = body?.reasoning; return new Response(stream, { status: 200, @@ -596,6 +601,7 @@ describe("openai-codex streaming", () => { provider: "openai-codex", baseUrl: "https://chatgpt.com/backend-api", reasoning: true, + thinkingLevelMap: { minimal: "low" }, input: ["text"], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: 400000, @@ -610,8 +616,10 @@ describe("openai-codex streaming", () => { const streamResult = streamOpenAICodexResponses(model, context, { apiKey: token, reasoningEffort: "minimal", + transport: "sse", }); await streamResult.result(); + expect(requestedReasoning).toEqual({ effort: "low", summary: "auto" }); }); it.each([ @@ -701,7 +709,11 @@ describe("openai-codex streaming", () => { messages: [{ role: "user", content: "Say hello", timestamp: Date.now() }], }; - const result = await streamOpenAICodexResponses(model, context, { apiKey: token, serviceTier }).result(); + const result = await streamOpenAICodexResponses(model, context, { + apiKey: token, + serviceTier, + transport: "sse", + }).result(); expect(result.usage.cost.input).toBe(1 * multiplier); expect(result.usage.cost.output).toBe(2 * multiplier); @@ -801,7 +813,7 @@ describe("openai-codex streaming", () => { }; // No sessionId provided - const streamResult = streamOpenAICodexResponses(model, context, { apiKey: token }); + const streamResult = streamOpenAICodexResponses(model, context, { apiKey: token, transport: "sse" }); await streamResult.result(); }); it("forwards auto transport from streamSimple options and uses cached websocket context", async () => { diff --git a/test.sh b/test.sh index 67aaaeaf..5f231845 100755 --- a/test.sh +++ b/test.sh @@ -26,7 +26,10 @@ export PI_NO_LOCAL_LLM=1 unset ANTHROPIC_API_KEY unset ANTHROPIC_OAUTH_TOKEN unset OPENAI_API_KEY +unset AZURE_OPENAI_API_KEY +unset DEEPSEEK_API_KEY unset GEMINI_API_KEY +unset GOOGLE_CLOUD_API_KEY unset GROQ_API_KEY unset CEREBRAS_API_KEY unset XAI_API_KEY @@ -35,10 +38,20 @@ unset ZAI_API_KEY unset MISTRAL_API_KEY unset MINIMAX_API_KEY unset MINIMAX_CN_API_KEY +unset MOONSHOT_API_KEY unset KIMI_API_KEY unset HF_TOKEN +unset FIREWORKS_API_KEY +unset TOGETHER_API_KEY unset AI_GATEWAY_API_KEY unset OPENCODE_API_KEY +unset CLOUDFLARE_API_KEY +unset CLOUDFLARE_ACCOUNT_ID +unset CLOUDFLARE_GATEWAY_ID +unset XIAOMI_API_KEY +unset XIAOMI_TOKEN_PLAN_CN_API_KEY +unset XIAOMI_TOKEN_PLAN_AMS_API_KEY +unset XIAOMI_TOKEN_PLAN_SGP_API_KEY unset COPILOT_GITHUB_TOKEN unset GH_TOKEN unset GITHUB_TOKEN @@ -57,7 +70,6 @@ unset AWS_CONTAINER_CREDENTIALS_RELATIVE_URI unset AWS_CONTAINER_CREDENTIALS_FULL_URI unset AWS_WEB_IDENTITY_TOKEN_FILE unset BEDROCK_EXTENSIVE_MODEL_TEST -unset FIREWORKS_API_KEY echo "Running tests without API keys..." npm test From 849f9d9c5af90ec61371f97648ef40fd4a0a0479 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Wed, 20 May 2026 12:45:00 +0200 Subject: [PATCH 135/166] fix(coding-agent): configure HTTP idle timeout (#4759) --- packages/coding-agent/src/cli.ts | 15 ++--- .../coding-agent/src/core/http-dispatcher.ts | 55 +++++++++++++++++++ .../coding-agent/src/core/settings-manager.ts | 23 ++++++++ packages/coding-agent/src/main.ts | 2 + .../components/settings-selector.ts | 18 ++++++ .../src/modes/interactive/interactive-mode.ts | 9 +++ .../test/settings-manager.test.ts | 26 ++++++++- 7 files changed, 136 insertions(+), 12 deletions(-) create mode 100644 packages/coding-agent/src/core/http-dispatcher.ts diff --git a/packages/coding-agent/src/cli.ts b/packages/coding-agent/src/cli.ts index 4f09675e..b0b11ca5 100644 --- a/packages/coding-agent/src/cli.ts +++ b/packages/coding-agent/src/cli.ts @@ -5,23 +5,16 @@ * * Test with: npx tsx src/cli-new.ts [args...] */ -import * as undici from "undici"; import { APP_NAME } from "./config.ts"; +import { configureHttpDispatcher } from "./core/http-dispatcher.ts"; import { main } from "./main.ts"; process.title = APP_NAME; process.env.PI_CODING_AGENT = "true"; process.emitWarning = (() => {}) as typeof process.emitWarning; -// bodyTimeout/headersTimeout default to 300s in undici; long local-LLM stalls -// (e.g. vLLM buffering a large tool call) exceed that and abort the SSE stream -// with UND_ERR_BODY_TIMEOUT. Disable both — provider SDKs enforce their own -// AbortController-based deadlines via retry.provider.timeoutMs. -undici.setGlobalDispatcher(new undici.EnvHttpProxyAgent({ allowH2: false, bodyTimeout: 0, headersTimeout: 0 })); - -// Keep fetch and the dispatcher on the same undici implementation. Node 26.0's -// bundled fetch can otherwise consume compressed responses through npm undici's -// dispatcher without decompressing them, causing response.json() failures. -undici.install?.(); +// Configure undici's global dispatcher before provider SDKs issue requests. +// Runtime settings are applied once SettingsManager has loaded global/project settings. +configureHttpDispatcher(); main(process.argv.slice(2)); diff --git a/packages/coding-agent/src/core/http-dispatcher.ts b/packages/coding-agent/src/core/http-dispatcher.ts new file mode 100644 index 00000000..12ce9fb0 --- /dev/null +++ b/packages/coding-agent/src/core/http-dispatcher.ts @@ -0,0 +1,55 @@ +import * as undici from "undici"; + +export const DEFAULT_HTTP_IDLE_TIMEOUT_MS = 300_000; + +export const HTTP_IDLE_TIMEOUT_CHOICES = [ + { label: "30 sec", timeoutMs: 30_000 }, + { label: "1 min", timeoutMs: 60_000 }, + { label: "2 min", timeoutMs: 120_000 }, + { label: "5 min", timeoutMs: 300_000 }, + { label: "disabled", timeoutMs: 0 }, +] as const; + +export function parseHttpIdleTimeoutMs(value: unknown): number | undefined { + if (typeof value === "string") { + const trimmed = value.trim(); + if (trimmed.toLowerCase() === "disabled") { + return 0; + } + if (trimmed.length === 0) { + return undefined; + } + return parseHttpIdleTimeoutMs(Number(trimmed)); + } + + if (typeof value !== "number" || !Number.isFinite(value) || value < 0) { + return undefined; + } + return Math.floor(value); +} + +export function formatHttpIdleTimeoutMs(timeoutMs: number): string { + const choice = HTTP_IDLE_TIMEOUT_CHOICES.find((item) => item.timeoutMs === timeoutMs); + if (choice) { + return choice.label; + } + return `${timeoutMs / 1000} sec`; +} + +export function configureHttpDispatcher(timeoutMs: number = DEFAULT_HTTP_IDLE_TIMEOUT_MS): void { + const normalizedTimeoutMs = parseHttpIdleTimeoutMs(timeoutMs); + if (normalizedTimeoutMs === undefined) { + throw new Error(`Invalid HTTP idle timeout: ${String(timeoutMs)}`); + } + undici.setGlobalDispatcher( + new undici.EnvHttpProxyAgent({ + allowH2: false, + bodyTimeout: normalizedTimeoutMs, + headersTimeout: normalizedTimeoutMs, + }), + ); + // Keep fetch and the dispatcher on the same undici implementation. Node 26.0's + // bundled fetch can otherwise consume compressed responses through npm undici's + // dispatcher without decompressing them, causing response.json() failures. + undici.install?.(); +} diff --git a/packages/coding-agent/src/core/settings-manager.ts b/packages/coding-agent/src/core/settings-manager.ts index 7dc6931e..2fedad35 100644 --- a/packages/coding-agent/src/core/settings-manager.ts +++ b/packages/coding-agent/src/core/settings-manager.ts @@ -4,6 +4,7 @@ import { homedir } from "os"; import { dirname, join } from "path"; import lockfile from "proper-lockfile"; import { CONFIG_DIR_NAME, getAgentDir } from "../config.ts"; +import { DEFAULT_HTTP_IDLE_TIMEOUT_MS, parseHttpIdleTimeoutMs } from "./http-dispatcher.ts"; export interface CompactionSettings { enabled?: boolean; // default: true @@ -110,6 +111,7 @@ export interface Settings { markdown?: MarkdownSettings; warnings?: WarningSettings; sessionDir?: string; // Custom session storage directory (same format as --session-dir CLI flag) + httpIdleTimeoutMs?: number; // HTTP header/body idle timeout in milliseconds; 0 disables it } /** Deep merge settings: project/overrides take precedence, nested objects merge recursively */ @@ -726,6 +728,27 @@ export class SettingsManager { }; } + getHttpIdleTimeoutMs(): number { + const value = this.settings.httpIdleTimeoutMs; + const timeoutMs = parseHttpIdleTimeoutMs(value); + if (timeoutMs !== undefined) { + return timeoutMs; + } + if (value !== undefined) { + throw new Error(`Invalid httpIdleTimeoutMs setting: ${String(value)}`); + } + return DEFAULT_HTTP_IDLE_TIMEOUT_MS; + } + + setHttpIdleTimeoutMs(timeoutMs: number): void { + if (!Number.isFinite(timeoutMs) || timeoutMs < 0) { + throw new Error(`Invalid httpIdleTimeoutMs setting: ${String(timeoutMs)}`); + } + this.globalSettings.httpIdleTimeoutMs = Math.floor(timeoutMs); + this.markModified("httpIdleTimeoutMs"); + this.save(); + } + getProviderRetrySettings(): { timeoutMs?: number; maxRetries?: number; maxRetryDelayMs: number } { return { timeoutMs: this.settings.retry?.provider?.timeoutMs, diff --git a/packages/coding-agent/src/main.ts b/packages/coding-agent/src/main.ts index 141a34e7..47f53c3d 100644 --- a/packages/coding-agent/src/main.ts +++ b/packages/coding-agent/src/main.ts @@ -26,6 +26,7 @@ import { formatNoModelsAvailableMessage } from "./core/auth-guidance.ts"; import { AuthStorage } from "./core/auth-storage.ts"; import { exportFromFile } from "./core/export-html/index.ts"; import type { ExtensionFactory } from "./core/extensions/types.ts"; +import { configureHttpDispatcher } from "./core/http-dispatcher.ts"; import { KeybindingsManager } from "./core/keybindings.ts"; import type { ModelRegistry } from "./core/model-registry.ts"; import { resolveCliModel, resolveModelScope, type ScopedModel } from "./core/model-resolver.ts"; @@ -617,6 +618,7 @@ export async function main(args: string[], options?: MainOptions) { }); const { services, session, modelFallbackMessage } = runtime; const { settingsManager, modelRegistry, resourceLoader } = services; + configureHttpDispatcher(settingsManager.getHttpIdleTimeoutMs()); if (parsed.help) { const extensionFlags = resourceLoader diff --git a/packages/coding-agent/src/modes/interactive/components/settings-selector.ts b/packages/coding-agent/src/modes/interactive/components/settings-selector.ts index f99d7381..7d210028 100644 --- a/packages/coding-agent/src/modes/interactive/components/settings-selector.ts +++ b/packages/coding-agent/src/modes/interactive/components/settings-selector.ts @@ -11,6 +11,7 @@ import { Spacer, Text, } from "@earendil-works/pi-tui"; +import { formatHttpIdleTimeoutMs, HTTP_IDLE_TIMEOUT_CHOICES } from "../../../core/http-dispatcher.ts"; import type { WarningSettings } from "../../../core/settings-manager.ts"; import { getSelectListTheme, getSettingsListTheme, theme } from "../theme/theme.ts"; import { DynamicBorder } from "./dynamic-border.ts"; @@ -40,6 +41,7 @@ export interface SettingsConfig { steeringMode: "all" | "one-at-a-time"; followUpMode: "all" | "one-at-a-time"; transport: Transport; + httpIdleTimeoutMs: number; thinkingLevel: ThinkingLevel; availableThinkingLevels: ThinkingLevel[]; currentTheme: string; @@ -68,6 +70,7 @@ export interface SettingsCallbacks { onSteeringModeChange: (mode: "all" | "one-at-a-time") => void; onFollowUpModeChange: (mode: "all" | "one-at-a-time") => void; onTransportChange: (transport: Transport) => void; + onHttpIdleTimeoutMsChange: (timeoutMs: number) => void; onThinkingLevelChange: (level: ThinkingLevel) => void; onThemeChange: (theme: string) => void; onThemePreview?: (theme: string) => void; @@ -238,6 +241,14 @@ export class SettingsSelectorComponent extends Container { currentValue: config.transport, values: ["sse", "websocket", "websocket-cached", "auto"], }, + { + id: "http-idle-timeout", + label: "HTTP idle timeout", + description: + "Maximum idle gap while waiting for HTTP headers or body chunks. Disable for local models that pause longer than five minutes.", + currentValue: formatHttpIdleTimeoutMs(config.httpIdleTimeoutMs), + values: HTTP_IDLE_TIMEOUT_CHOICES.map((choice) => choice.label), + }, { id: "hide-thinking", label: "Hide thinking", @@ -482,6 +493,13 @@ export class SettingsSelectorComponent extends Container { case "transport": callbacks.onTransportChange(newValue as Transport); break; + case "http-idle-timeout": { + const choice = HTTP_IDLE_TIMEOUT_CHOICES.find((item) => item.label === newValue); + if (choice) { + callbacks.onHttpIdleTimeoutMsChange(choice.timeoutMs); + } + break; + } case "hide-thinking": callbacks.onHideThinkingBlockChange(newValue === "true"); break; diff --git a/packages/coding-agent/src/modes/interactive/interactive-mode.ts b/packages/coding-agent/src/modes/interactive/interactive-mode.ts index c93beb93..aa8db52b 100644 --- a/packages/coding-agent/src/modes/interactive/interactive-mode.ts +++ b/packages/coding-agent/src/modes/interactive/interactive-mode.ts @@ -71,6 +71,7 @@ import type { ExtensionWidgetOptions, } from "../../core/extensions/index.ts"; import { FooterDataProvider, type ReadonlyFooterDataProvider } from "../../core/footer-data-provider.ts"; +import { configureHttpDispatcher, formatHttpIdleTimeoutMs } from "../../core/http-dispatcher.ts"; import { type AppKeybinding, KeybindingsManager } from "../../core/keybindings.ts"; import { createCompactionSummaryMessage } from "../../core/messages.ts"; import { defaultModelPerProvider, findExactModelReferenceMatch, resolveModelScope } from "../../core/model-resolver.ts"; @@ -1566,6 +1567,7 @@ export class InteractiveMode { } private applyRuntimeSettings(): void { + configureHttpDispatcher(this.settingsManager.getHttpIdleTimeoutMs()); this.footer.setSession(this.session); this.footer.setAutoCompactEnabled(this.session.autoCompactionEnabled); this.footerDataProvider.setCwd(this.sessionManager.getCwd()); @@ -3846,6 +3848,7 @@ export class InteractiveMode { steeringMode: this.session.steeringMode, followUpMode: this.session.followUpMode, transport: this.settingsManager.getTransport(), + httpIdleTimeoutMs: this.settingsManager.getHttpIdleTimeoutMs(), thinkingLevel: this.session.thinkingLevel, availableThinkingLevels: this.session.getAvailableThinkingLevels(), currentTheme: this.settingsManager.getTheme() || "dark", @@ -3904,6 +3907,11 @@ export class InteractiveMode { this.settingsManager.setTransport(transport); this.session.agent.transport = transport; }, + onHttpIdleTimeoutMsChange: (timeoutMs) => { + this.settingsManager.setHttpIdleTimeoutMs(timeoutMs); + configureHttpDispatcher(timeoutMs); + this.showStatus(`HTTP idle timeout: ${formatHttpIdleTimeoutMs(timeoutMs)}`); + }, onThinkingLevelChange: (level) => { this.session.setThinkingLevel(level); this.footer.invalidate(); @@ -4891,6 +4899,7 @@ export class InteractiveMode { try { await this.session.reload(); + configureHttpDispatcher(this.settingsManager.getHttpIdleTimeoutMs()); this.keybindings.reload(); const activeHeader = this.customHeader ?? this.builtInHeader; if (isExpandable(activeHeader)) { diff --git a/packages/coding-agent/test/settings-manager.test.ts b/packages/coding-agent/test/settings-manager.test.ts index d01b1363..a503e3cc 100644 --- a/packages/coding-agent/test/settings-manager.test.ts +++ b/packages/coding-agent/test/settings-manager.test.ts @@ -2,7 +2,8 @@ import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "fs"; import { homedir } from "os"; import { join } from "path"; import { afterEach, beforeEach, describe, expect, it } from "vitest"; -import { SettingsManager } from "../src/core/settings-manager.js"; +import { DEFAULT_HTTP_IDLE_TIMEOUT_MS } from "../src/core/http-dispatcher.ts"; +import { SettingsManager } from "../src/core/settings-manager.ts"; describe("SettingsManager", () => { const testDir = join(process.cwd(), "test-settings-tmp"); @@ -257,6 +258,29 @@ describe("SettingsManager", () => { }); }); + describe("httpIdleTimeoutMs", () => { + it("should default to 5 minutes", () => { + const manager = SettingsManager.create(projectDir, agentDir); + expect(manager.getHttpIdleTimeoutMs()).toBe(DEFAULT_HTTP_IDLE_TIMEOUT_MS); + }); + + it("should use merged global and project settings", () => { + writeFileSync(join(agentDir, "settings.json"), JSON.stringify({ httpIdleTimeoutMs: 300000 })); + writeFileSync(join(projectDir, ".pi", "settings.json"), JSON.stringify({ httpIdleTimeoutMs: 0 })); + + const manager = SettingsManager.create(projectDir, agentDir); + + expect(manager.getHttpIdleTimeoutMs()).toBe(0); + }); + + it("should reject invalid timeout values", () => { + writeFileSync(join(agentDir, "settings.json"), JSON.stringify({ httpIdleTimeoutMs: -1 })); + const manager = SettingsManager.create(projectDir, agentDir); + + expect(() => manager.getHttpIdleTimeoutMs()).toThrow("Invalid httpIdleTimeoutMs setting"); + }); + }); + describe("shellCommandPrefix", () => { it("should load shellCommandPrefix from settings", () => { const settingsPath = join(agentDir, "settings.json"); From 2e02c74dcb9787e3feca7fad67082faed4a1b3cd Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Wed, 20 May 2026 12:44:04 +0200 Subject: [PATCH 136/166] chore: pin dependencies and use native TypeScript --- package-lock.json | 140 ++++++++---------- package.json | 17 +-- packages/agent/package.json | 14 +- packages/agent/test/agent-loop.test.ts | 4 +- packages/agent/test/agent.test.ts | 2 +- packages/agent/test/e2e.test.ts | 4 +- .../test/harness/agent-harness-stream.test.ts | 10 +- .../agent/test/harness/agent-harness.test.ts | 16 +- .../agent/test/harness/compaction.test.ts | 10 +- .../agent/test/harness/nodejs-env.test.ts | 8 +- .../test/harness/prompt-templates.test.ts | 6 +- packages/agent/test/harness/repo.test.ts | 8 +- .../test/harness/resource-formatting.test.ts | 4 +- .../agent/test/harness/session-uuid.test.ts | 2 +- packages/agent/test/harness/session.test.ts | 12 +- packages/agent/test/harness/skills.test.ts | 6 +- packages/agent/test/harness/storage.test.ts | 10 +- .../agent/test/harness/system-prompt.test.ts | 2 +- packages/agent/test/harness/truncate.test.ts | 2 +- packages/agent/test/scratch/simple.ts | 7 +- packages/agent/test/utils/calculate.ts | 2 +- packages/agent/test/utils/get-current-time.ts | 2 +- packages/ai/package.json | 26 ++-- packages/ai/scripts/generate-image-models.ts | 4 +- packages/ai/scripts/generate-models.ts | 12 +- packages/ai/scripts/generate-test-image.ts | 5 +- packages/ai/test/abort.test.ts | 12 +- .../anthropic-eager-tool-input-compat.test.ts | 4 +- .../anthropic-eager-tool-input-e2e.test.ts | 10 +- ...anthropic-long-cache-retention-e2e.test.ts | 10 +- packages/ai/test/anthropic-oauth.test.ts | 2 +- .../ai/test/anthropic-opus-4-7-smoke.test.ts | 6 +- .../ai/test/anthropic-sse-parsing.test.ts | 6 +- .../anthropic-tool-name-normalization.test.ts | 8 +- .../ai/test/azure-openai-base-url.test.ts | 6 +- .../ai/test/bedrock-convert-messages.test.ts | 6 +- .../test/bedrock-endpoint-resolution.test.ts | 6 +- packages/ai/test/bedrock-models.test.ts | 8 +- .../ai/test/codex-websocket-cached-probe.ts | 12 +- packages/ai/test/context-overflow.test.ts | 14 +- .../ai/test/cross-provider-handoff.test.ts | 12 +- packages/ai/test/empty.test.ts | 14 +- packages/ai/test/env-api-keys.test.ts | 2 +- packages/ai/test/faux-provider.test.ts | 4 +- packages/ai/test/fireworks-models.test.ts | 8 +- .../ai/test/github-copilot-anthropic.test.ts | 6 +- packages/ai/test/github-copilot-oauth.test.ts | 2 +- .../test/google-shared-convert-tools.test.ts | 4 +- ...-shared-gemini3-unsigned-tool-call.test.ts | 4 +- ...e-shared-image-tool-result-routing.test.ts | 4 +- .../ai/test/google-thinking-disable.test.ts | 6 +- .../ai/test/google-thinking-signature.test.ts | 2 +- .../google-vertex-api-key-resolution.test.ts | 6 +- packages/ai/test/image-tool-result.test.ts | 12 +- packages/ai/test/images.test.ts | 6 +- packages/ai/test/interleaved-thinking.test.ts | 12 +- packages/ai/test/lazy-module-load.test.ts | 5 +- .../ai/test/mistral-reasoning-mode.test.ts | 6 +- packages/ai/test/mistral-tool-schema.test.ts | 6 +- packages/ai/test/node-http-proxy.test.ts | 2 +- packages/ai/test/oauth.ts | 4 +- .../openai-codex-cache-affinity-e2e.test.ts | 8 +- packages/ai/test/openai-codex-oauth.test.ts | 2 +- ...i-completions-cache-control-format.test.ts | 6 +- .../openai-completions-empty-tools.test.ts | 4 +- .../openai-completions-prompt-cache.test.ts | 6 +- .../openai-completions-response-model.test.ts | 4 +- ...penai-completions-thinking-as-text.test.ts | 4 +- .../openai-completions-tool-choice.test.ts | 8 +- ...nai-completions-tool-result-images.test.ts | 6 +- ...penai-responses-cache-affinity-e2e.test.ts | 6 +- .../openai-responses-copilot-provider.test.ts | 6 +- ...enai-responses-foreign-toolcall-id.test.ts | 8 +- ...nai-responses-partial-json-cleanup.test.ts | 6 +- ...nai-responses-reasoning-replay-e2e.test.ts | 6 +- ...penai-responses-tool-result-images.test.ts | 8 +- .../test/openrouter-cache-write-repro.test.ts | 4 +- packages/ai/test/openrouter-images.test.ts | 4 +- packages/ai/test/overflow.test.ts | 4 +- packages/ai/test/responseid.test.ts | 10 +- packages/ai/test/stream.test.ts | 16 +- packages/ai/test/supports-xhigh.test.ts | 2 +- packages/ai/test/together-models.test.ts | 4 +- packages/ai/test/tokens.test.ts | 14 +- .../test/tool-call-id-normalization.test.ts | 8 +- .../ai/test/tool-call-without-result.test.ts | 14 +- packages/ai/test/total-tokens.test.ts | 14 +- ...ssages-copilot-openai-to-anthropic.test.ts | 4 +- packages/ai/test/unicode-surrogate.test.ts | 14 +- packages/ai/test/validation.test.ts | 4 +- packages/ai/test/xhigh.test.ts | 6 +- packages/ai/test/zen.test.ts | 6 +- .../custom-provider-anthropic/package.json | 2 +- .../custom-provider-gitlab-duo/test.ts | 2 +- .../extensions/doom-overlay/doom-component.ts | 4 +- .../examples/extensions/doom-overlay/index.ts | 6 +- .../examples/extensions/plan-mode/index.ts | 2 +- .../examples/extensions/sandbox/package.json | 2 +- .../examples/extensions/subagent/index.ts | 2 +- .../extensions/with-deps/package.json | 4 +- packages/coding-agent/package.json | 48 +++--- ...gent-session-auto-compaction-queue.test.ts | 12 +- .../test/agent-session-branching.test.ts | 10 +- .../test/agent-session-compaction.test.ts | 14 +- .../test/agent-session-concurrent.test.ts | 14 +- .../agent-session-dynamic-provider.test.ts | 12 +- .../test/agent-session-dynamic-tools.test.ts | 8 +- .../test/agent-session-retry.test.ts | 12 +- .../test/agent-session-runtime-events.test.ts | 8 +- .../test/agent-session-stats.test.ts | 12 +- .../agent-session-tree-navigation.test.ts | 2 +- packages/coding-agent/test/ansi-utils.test.ts | 2 +- packages/coding-agent/test/args.test.ts | 2 +- .../test/assistant-message.test.ts | 4 +- .../coding-agent/test/auth-storage.test.ts | 4 +- .../test/bash-close-hang-windows.test.ts | 4 +- .../test/bash-execution-width.test.ts | 4 +- .../coding-agent/test/block-images.test.ts | 6 +- .../clipboard-image-bmp-conversion.test.ts | 2 +- .../coding-agent/test/clipboard-image.test.ts | 10 +- packages/coding-agent/test/clipboard.test.ts | 2 +- .../compaction-extensions-example.test.ts | 2 +- .../test/compaction-extensions.test.ts | 18 +-- .../test/compaction-serialization.test.ts | 2 +- .../test/compaction-summary-reasoning.test.ts | 2 +- packages/coding-agent/test/compaction.test.ts | 4 +- packages/coding-agent/test/config.test.ts | 2 +- .../test/edit-tool-legacy-input.test.ts | 4 +- .../test/edit-tool-no-full-redraw.test.ts | 8 +- .../test/export-html-whitespace.test.ts | 8 +- .../test/extensions-discovery.test.ts | 6 +- .../test/extensions-input-event.test.ts | 10 +- .../test/extensions-runner.test.ts | 14 +- .../test/file-mutation-queue.test.ts | 6 +- .../test/footer-data-provider.test.ts | 2 +- .../coding-agent/test/footer-width.test.ts | 8 +- .../coding-agent/test/frontmatter.test.ts | 2 +- .../coding-agent/test/git-ssh-url.test.ts | 2 +- packages/coding-agent/test/git-update.test.ts | 4 +- .../test/image-processing.test.ts | 4 +- .../test/image-resize-callers.test.ts | 6 +- .../coding-agent/test/initial-message.test.ts | 4 +- ...interactive-mode-anthropic-warning.test.ts | 2 +- .../interactive-mode-clone-command.test.ts | 2 +- .../test/interactive-mode-compaction.test.ts | 2 +- .../interactive-mode-import-command.test.ts | 4 +- .../test/interactive-mode-status.test.ts | 8 +- .../test/interactive-mode-suspend.test.ts | 2 +- .../test/keybindings-migration.test.ts | 6 +- .../coding-agent/test/model-registry.test.ts | 4 +- .../coding-agent/test/model-resolver.test.ts | 2 +- .../coding-agent/test/oauth-selector.test.ts | 14 +- .../test/package-command-paths.test.ts | 4 +- .../test/package-manager-ssh.test.ts | 4 +- .../coding-agent/test/package-manager.test.ts | 8 +- packages/coding-agent/test/path-utils.test.ts | 2 +- packages/coding-agent/test/paths.test.ts | 2 +- .../coding-agent/test/pi-user-agent.test.ts | 2 +- .../coding-agent/test/plan-mode-utils.test.ts | 2 +- packages/coding-agent/test/print-mode.test.ts | 4 +- .../test/prompt-templates.test.ts | 4 +- .../coding-agent/test/resource-loader.test.ts | 16 +- .../test/restore-sandbox-env.test.ts | 2 +- .../test/rpc-client-clone.test.ts | 2 +- packages/coding-agent/test/rpc-example.ts | 2 +- packages/coding-agent/test/rpc-jsonl.test.ts | 2 +- .../rpc-prompt-response-semantics.test.ts | 16 +- packages/coding-agent/test/rpc.test.ts | 2 +- .../test/sdk-codex-cache-probe-tool-loop.ts | 22 +-- .../test/sdk-openrouter-attribution.test.ts | 10 +- .../test/sdk-session-manager.test.ts | 4 +- packages/coding-agent/test/sdk-skills.test.ts | 10 +- .../coding-agent/test/session-cwd.test.ts | 6 +- .../session-info-modified-timestamp.test.ts | 6 +- .../session-manager/build-context.test.ts | 2 +- .../session-manager/custom-session-id.test.ts | 2 +- .../session-manager/file-operations.test.ts | 2 +- .../test/session-manager/labels.test.ts | 2 +- .../test/session-manager/migration.test.ts | 2 +- .../test/session-manager/save-entry.test.ts | 2 +- .../session-manager/tree-traversal.test.ts | 4 +- .../test/session-selector-path-delete.test.ts | 8 +- .../test/session-selector-rename.test.ts | 8 +- .../test/session-selector-search.test.ts | 4 +- .../test/settings-manager-bug.test.ts | 2 +- packages/coding-agent/test/skills.test.ts | 6 +- .../test/stdout-cleanliness.test.ts | 5 +- .../test/streaming-render-debug.ts | 4 +- .../agent-session-bash-persistence.test.ts | 4 +- .../suite/agent-session-compaction.test.ts | 2 +- .../agent-session-model-extension.test.ts | 4 +- .../test/suite/agent-session-prompt.test.ts | 8 +- .../test/suite/agent-session-queue.test.ts | 2 +- .../suite/agent-session-retry-events.test.ts | 2 +- .../test/suite/agent-session-runtime.test.ts | 8 +- packages/coding-agent/test/suite/harness.ts | 20 +-- ...113-agent-session-event-settlement.test.ts | 2 +- ...2023-queued-slash-command-followup.test.ts | 2 +- ...753-reload-stale-resource-settings.test.ts | 6 +- .../2781-skill-collision-precedence.test.ts | 2 +- .../2791-fswatch-error-crash.test.ts | 4 +- ...-allowlist-filters-extension-tools.test.ts | 8 +- .../2860-replaced-session-context.test.ts | 10 +- .../3217-scoped-model-order.test.ts | 12 +- .../regressions/3302-find-path-glob.test.ts | 2 +- .../3303-find-nested-gitignore.test.ts | 2 +- ...3317-network-connection-lost-retry.test.ts | 2 +- ...uiltin-tools-keeps-extension-tools.test.ts | 10 +- .../3616-settings-inmemory-reload.test.ts | 4 +- .../3686-session-name-event.test.ts | 4 +- .../3688-tree-cancel-compacting.test.ts | 4 +- .../3982-message-end-cost-override.test.ts | 2 +- ...hinking-toggle-pending-tool-render.test.ts | 12 +- .../test/syntax-highlight.test.ts | 2 +- .../coding-agent/test/system-prompt.test.ts | 2 +- .../coding-agent/test/test-harness.test.ts | 2 +- packages/coding-agent/test/test-harness.ts | 16 +- .../coding-agent/test/test-theme-colors.ts | 2 +- .../coding-agent/test/theme-detection.test.ts | 2 +- .../coding-agent/test/theme-export.test.ts | 2 +- .../test/tool-execution-component.test.ts | 16 +- packages/coding-agent/test/tools.test.ts | 10 +- .../coding-agent/test/tree-selector.test.ts | 8 +- .../test/trigger-compact-extension.test.ts | 4 +- .../coding-agent/test/user-message.test.ts | 4 +- packages/coding-agent/test/utilities.ts | 20 +-- .../coding-agent/test/version-check.test.ts | 2 +- packages/tui/package.json | 14 +- packages/tui/test/autocomplete.test.ts | 2 +- ...ression-isimageline-startswith-bug.test.ts | 20 +-- packages/tui/test/chat-simple.ts | 16 +- packages/tui/test/editor.test.ts | 12 +- packages/tui/test/fuzzy.test.ts | 2 +- packages/tui/test/image-test.ts | 12 +- packages/tui/test/input.test.ts | 4 +- packages/tui/test/key-tester.ts | 6 +- packages/tui/test/keybindings.test.ts | 2 +- packages/tui/test/keys.test.ts | 2 +- packages/tui/test/markdown.test.ts | 10 +- .../tui/test/overlay-non-capturing.test.ts | 6 +- packages/tui/test/overlay-options.test.ts | 6 +- .../tui/test/overlay-short-content.test.ts | 4 +- ...egression-regional-indicator-width.test.ts | 2 +- packages/tui/test/select-list.test.ts | 4 +- packages/tui/test/stdin-buffer.test.ts | 2 +- packages/tui/test/terminal-image.test.ts | 4 +- packages/tui/test/terminal.test.ts | 2 +- packages/tui/test/test-themes.ts | 2 +- packages/tui/test/truncate-to-width.test.ts | 2 +- packages/tui/test/truncated-text.test.ts | 4 +- packages/tui/test/tui-cell-size-input.test.ts | 6 +- .../tui/test/tui-overlay-style-leak.test.ts | 4 +- packages/tui/test/tui-render.test.ts | 6 +- packages/tui/test/viewport-overwrite-repro.ts | 4 +- packages/tui/test/virtual-terminal.ts | 2 +- packages/tui/test/wrap-ansi.test.ts | 2 +- scripts/cost.ts | 2 +- scripts/session-transcripts.ts | 6 +- scripts/stats.ts | 2 +- scripts/tool-stats.ts | 2 +- 260 files changed, 862 insertions(+), 897 deletions(-) diff --git a/package-lock.json b/package-lock.json index 38e1232f..9af10d5b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,19 +14,16 @@ "packages/coding-agent/examples/extensions/custom-provider-gitlab-duo", "packages/coding-agent/examples/extensions/sandbox" ], - "dependencies": { - "@earendil-works/pi-coding-agent": "^0.30.2" - }, "devDependencies": { - "@anthropic-ai/sandbox-runtime": "^0.0.26", + "@anthropic-ai/sandbox-runtime": "0.0.26", "@biomejs/biome": "2.3.5", - "@types/node": "^22.10.5", + "@types/node": "22.19.19", "@typescript/native-preview": "7.0.0-dev.20260120.1", - "husky": "^9.1.7", - "jiti": "^2.7.0", - "shx": "^0.4.0", - "tsx": "^4.20.3", - "typescript": "^5.9.2" + "esbuild": "0.28.0", + "husky": "9.1.7", + "jiti": "2.7.0", + "shx": "0.4.0", + "typescript": "5.9.3" }, "engines": { "node": ">=22.19.0" @@ -5089,25 +5086,6 @@ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD" }, - "node_modules/tsx": { - "version": "4.22.1", - "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.22.1.tgz", - "integrity": "sha512-TvncJykhxAzFCk0VQZKBTClall4Pm7qXDSodb6uxi8QFa8X8mT6ABjxxsQ2opDRYxG7AzcRWXaFtruz5HJKuWg==", - "dev": true, - "license": "MIT", - "dependencies": { - "esbuild": "~0.28.0" - }, - "bin": { - "tsx": "dist/cli.mjs" - }, - "engines": { - "node": ">=18.0.0" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - } - }, "node_modules/tunnel-agent": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", @@ -6004,15 +5982,15 @@ "license": "MIT", "dependencies": { "@earendil-works/pi-ai": "^0.75.3", - "ignore": "^7.0.5", - "typebox": "^1.1.24", - "yaml": "^2.8.2" + "ignore": "7.0.5", + "typebox": "1.1.38", + "yaml": "2.9.0" }, "devDependencies": { - "@types/node": "^24.3.0", - "@vitest/coverage-v8": "^3.2.4", - "typescript": "^5.7.3", - "vitest": "^3.2.4" + "@types/node": "24.12.4", + "@vitest/coverage-v8": "3.2.4", + "typescript": "5.9.3", + "vitest": "3.2.4" }, "engines": { "node": ">=22.19.0" @@ -6040,23 +6018,23 @@ "version": "0.75.3", "license": "MIT", "dependencies": { - "@anthropic-ai/sdk": "^0.91.1", - "@aws-sdk/client-bedrock-runtime": "^3.1030.0", - "@google/genai": "^1.40.0", - "@mistralai/mistralai": "^2.2.0", - "http-proxy-agent": "^7.0.2", - "https-proxy-agent": "^7.0.6", + "@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.24" + "partial-json": "0.1.7", + "typebox": "1.1.38" }, "bin": { "pi-ai": "dist/cli.js" }, "devDependencies": { - "@types/node": "^24.3.0", - "canvas": "^3.2.0", - "vitest": "^3.2.4" + "@types/node": "24.12.4", + "canvas": "3.2.3", + "vitest": "3.2.4" }, "engines": { "node": ">=22.19.0" @@ -6087,47 +6065,47 @@ "@earendil-works/pi-agent-core": "^0.75.3", "@earendil-works/pi-ai": "^0.75.3", "@earendil-works/pi-tui": "^0.75.3", - "@silvia-odwyer/photon-node": "^0.3.4", - "chalk": "^5.5.0", - "cross-spawn": "^7.0.6", - "diff": "^8.0.2", - "glob": "^13.0.1", - "highlight.js": "^10.7.3", - "hosted-git-info": "^9.0.2", - "ignore": "^7.0.5", - "jiti": "^2.7.0", - "minimatch": "^10.2.3", - "proper-lockfile": "^4.1.2", - "typebox": "^1.1.24", - "undici": "^8.3.0", - "yaml": "^2.8.2" + "@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" }, "bin": { "pi": "dist/cli.js" }, "devDependencies": { - "@types/cross-spawn": "^6.0.6", - "@types/diff": "^7.0.2", - "@types/hosted-git-info": "^3.0.5", - "@types/ms": "^2.1.0", - "@types/node": "^24.3.0", - "@types/proper-lockfile": "^4.1.4", - "shx": "^0.4.0", - "typescript": "^5.7.3", - "vitest": "^3.2.4" + "@types/cross-spawn": "6.0.6", + "@types/diff": "7.0.2", + "@types/hosted-git-info": "3.0.5", + "@types/ms": "2.1.0", + "@types/node": "24.12.4", + "@types/proper-lockfile": "4.1.4", + "shx": "0.4.0", + "typescript": "5.9.3", + "vitest": "3.2.4" }, "engines": { "node": ">=22.19.0" }, "optionalDependencies": { - "@mariozechner/clipboard": "^0.3.6" + "@mariozechner/clipboard": "0.3.6" } }, "packages/coding-agent/examples/extensions/custom-provider-anthropic": { "name": "pi-extension-custom-provider-anthropic", "version": "0.75.3", "dependencies": { - "@anthropic-ai/sdk": "^0.52.0" + "@anthropic-ai/sdk": "0.52.0" } }, "packages/coding-agent/examples/extensions/custom-provider-gitlab-duo": { @@ -6138,17 +6116,17 @@ "name": "pi-extension-sandbox", "version": "1.5.3", "dependencies": { - "@anthropic-ai/sandbox-runtime": "^0.0.26" + "@anthropic-ai/sandbox-runtime": "0.0.26" } }, "packages/coding-agent/examples/extensions/with-deps": { "name": "pi-extension-with-deps", "version": "0.75.3", "dependencies": { - "ms": "^2.1.3" + "ms": "2.1.3" }, "devDependencies": { - "@types/ms": "^2.1.0" + "@types/ms": "2.1.0" } }, "packages/coding-agent/node_modules/@anthropic-ai/sdk": { @@ -6182,19 +6160,19 @@ "version": "0.75.3", "license": "MIT", "dependencies": { - "get-east-asian-width": "^1.3.0", - "marked": "^15.0.12" + "get-east-asian-width": "1.6.0", + "marked": "15.0.12" }, "devDependencies": { - "@xterm/headless": "^5.5.0", - "@xterm/xterm": "^5.5.0", - "chalk": "^5.5.0" + "@xterm/headless": "5.5.0", + "@xterm/xterm": "5.5.0", + "chalk": "5.6.2" }, "engines": { "node": ">=22.19.0" }, "optionalDependencies": { - "koffi": "^2.9.0" + "koffi": "2.16.2" } } } diff --git a/package.json b/package.json index c0b10fe0..cd0fed1a 100644 --- a/package.json +++ b/package.json @@ -30,23 +30,20 @@ "prepare": "husky" }, "devDependencies": { - "@anthropic-ai/sandbox-runtime": "^0.0.26", + "@anthropic-ai/sandbox-runtime": "0.0.26", "@biomejs/biome": "2.3.5", - "@types/node": "^22.10.5", + "@types/node": "22.19.19", "@typescript/native-preview": "7.0.0-dev.20260120.1", - "husky": "^9.1.7", - "jiti": "^2.7.0", - "tsx": "^4.20.3", - "typescript": "^5.9.2", - "shx": "^0.4.0" + "esbuild": "0.28.0", + "husky": "9.1.7", + "jiti": "2.7.0", + "shx": "0.4.0", + "typescript": "5.9.3" }, "engines": { "node": ">=22.19.0" }, "version": "0.0.3", - "dependencies": { - "@earendil-works/pi-coding-agent": "^0.30.2" - }, "overrides": { "rimraf": "6.1.2", "gaxios": { diff --git a/packages/agent/package.json b/packages/agent/package.json index 37e666ba..d8bf0656 100644 --- a/packages/agent/package.json +++ b/packages/agent/package.json @@ -30,9 +30,9 @@ }, "dependencies": { "@earendil-works/pi-ai": "^0.75.3", - "ignore": "^7.0.5", - "typebox": "^1.1.24", - "yaml": "^2.8.2" + "ignore": "7.0.5", + "typebox": "1.1.38", + "yaml": "2.9.0" }, "keywords": [ "ai", @@ -52,9 +52,9 @@ "node": ">=22.19.0" }, "devDependencies": { - "@types/node": "^24.3.0", - "@vitest/coverage-v8": "^3.2.4", - "typescript": "^5.7.3", - "vitest": "^3.2.4" + "@types/node": "24.12.4", + "@vitest/coverage-v8": "3.2.4", + "typescript": "5.9.3", + "vitest": "3.2.4" } } diff --git a/packages/agent/test/agent-loop.test.ts b/packages/agent/test/agent-loop.test.ts index 261b42fc..62eadf81 100644 --- a/packages/agent/test/agent-loop.test.ts +++ b/packages/agent/test/agent-loop.test.ts @@ -8,8 +8,8 @@ import { } from "@earendil-works/pi-ai"; import { Type } from "typebox"; import { describe, expect, it } from "vitest"; -import { agentLoop, agentLoopContinue } from "../src/agent-loop.js"; -import type { AgentContext, AgentEvent, AgentLoopConfig, AgentMessage, AgentTool } from "../src/types.js"; +import { agentLoop, agentLoopContinue } from "../src/agent-loop.ts"; +import type { AgentContext, AgentEvent, AgentLoopConfig, AgentMessage, AgentTool } from "../src/types.ts"; // Mock stream for testing - mimics MockAssistantStream class MockAssistantStream extends EventStream { diff --git a/packages/agent/test/agent.test.ts b/packages/agent/test/agent.test.ts index 9c38ae8c..82cc58de 100644 --- a/packages/agent/test/agent.test.ts +++ b/packages/agent/test/agent.test.ts @@ -1,6 +1,6 @@ import { type AssistantMessage, type AssistantMessageEvent, EventStream, getModel } from "@earendil-works/pi-ai"; import { describe, expect, it } from "vitest"; -import { Agent } from "../src/index.js"; +import { Agent } from "../src/index.ts"; // Mock stream that mimics AssistantMessageEventStream class MockAssistantStream extends EventStream { diff --git a/packages/agent/test/e2e.test.ts b/packages/agent/test/e2e.test.ts index b75645b0..309536ad 100644 --- a/packages/agent/test/e2e.test.ts +++ b/packages/agent/test/e2e.test.ts @@ -11,8 +11,8 @@ import { type UserMessage, } from "@earendil-works/pi-ai"; import { afterEach, describe, expect, it } from "vitest"; -import { Agent, type AgentEvent } from "../src/index.js"; -import { calculateTool } from "./utils/calculate.js"; +import { Agent, type AgentEvent } from "../src/index.ts"; +import { calculateTool } from "./utils/calculate.ts"; const registrations: FauxProviderRegistration[] = []; diff --git a/packages/agent/test/harness/agent-harness-stream.test.ts b/packages/agent/test/harness/agent-harness-stream.test.ts index dba98f91..ee79564b 100644 --- a/packages/agent/test/harness/agent-harness-stream.test.ts +++ b/packages/agent/test/harness/agent-harness-stream.test.ts @@ -1,10 +1,10 @@ import { fauxAssistantMessage, fauxToolCall, registerFauxProvider, type StreamOptions } from "@earendil-works/pi-ai"; import { afterEach, describe, expect, it } from "vitest"; -import { AgentHarness } from "../../src/harness/agent-harness.js"; -import { NodeExecutionEnv } from "../../src/harness/env/nodejs.js"; -import { InMemorySessionStorage } from "../../src/harness/session/memory-storage.js"; -import { Session } from "../../src/harness/session/session.js"; -import { calculateTool } from "../utils/calculate.js"; +import { AgentHarness } from "../../src/harness/agent-harness.ts"; +import { NodeExecutionEnv } from "../../src/harness/env/nodejs.ts"; +import { InMemorySessionStorage } from "../../src/harness/session/memory-storage.ts"; +import { Session } from "../../src/harness/session/session.ts"; +import { calculateTool } from "../utils/calculate.ts"; const registrations: Array<{ unregister(): void }> = []; diff --git a/packages/agent/test/harness/agent-harness.test.ts b/packages/agent/test/harness/agent-harness.test.ts index 6ded341c..8a395532 100644 --- a/packages/agent/test/harness/agent-harness.test.ts +++ b/packages/agent/test/harness/agent-harness.test.ts @@ -1,13 +1,13 @@ import { fauxAssistantMessage, fauxToolCall, getModel, registerFauxProvider } from "@earendil-works/pi-ai"; import { afterEach, describe, expect, it } from "vitest"; -import { AgentHarness } from "../../src/harness/agent-harness.js"; -import { NodeExecutionEnv } from "../../src/harness/env/nodejs.js"; -import { InMemorySessionStorage } from "../../src/harness/session/memory-storage.js"; -import { Session } from "../../src/harness/session/session.js"; -import type { PromptTemplate, Skill } from "../../src/harness/types.js"; -import type { AgentMessage, AgentTool } from "../../src/types.js"; -import { calculateTool } from "../utils/calculate.js"; -import { getCurrentTimeTool } from "../utils/get-current-time.js"; +import { AgentHarness } from "../../src/harness/agent-harness.ts"; +import { NodeExecutionEnv } from "../../src/harness/env/nodejs.ts"; +import { InMemorySessionStorage } from "../../src/harness/session/memory-storage.ts"; +import { Session } from "../../src/harness/session/session.ts"; +import type { PromptTemplate, Skill } from "../../src/harness/types.ts"; +import type { AgentMessage, AgentTool } from "../../src/types.ts"; +import { calculateTool } from "../utils/calculate.ts"; +import { getCurrentTimeTool } from "../utils/get-current-time.ts"; interface AppSkill extends Skill { source: "project" | "user"; diff --git a/packages/agent/test/harness/compaction.test.ts b/packages/agent/test/harness/compaction.test.ts index 25d740ca..b694d9f5 100644 --- a/packages/agent/test/harness/compaction.test.ts +++ b/packages/agent/test/harness/compaction.test.ts @@ -22,8 +22,8 @@ import { prepareCompaction, serializeConversation, shouldCompact, -} from "../../src/harness/compaction/compaction.js"; -import { buildSessionContext } from "../../src/harness/session/session.js"; +} from "../../src/harness/compaction/compaction.ts"; +import { buildSessionContext } from "../../src/harness/session/session.ts"; import type { BranchSummaryEntry, CompactionEntry, @@ -33,9 +33,9 @@ import type { ModelChangeEntry, SessionTreeEntry, ThinkingLevelChangeEntry, -} from "../../src/harness/types.js"; -import { getOrThrow } from "../../src/harness/types.js"; -import type { AgentMessage } from "../../src/types.js"; +} from "../../src/harness/types.ts"; +import { getOrThrow } from "../../src/harness/types.ts"; +import type { AgentMessage } from "../../src/types.ts"; let nextId = 0; function createId(): string { diff --git a/packages/agent/test/harness/nodejs-env.test.ts b/packages/agent/test/harness/nodejs-env.test.ts index aef098be..758d5f59 100644 --- a/packages/agent/test/harness/nodejs-env.test.ts +++ b/packages/agent/test/harness/nodejs-env.test.ts @@ -1,10 +1,10 @@ import { access, chmod, realpath, symlink } from "node:fs/promises"; import { join } from "node:path"; import { afterEach, describe, expect, it } from "vitest"; -import { NodeExecutionEnv } from "../../src/harness/env/nodejs.js"; -import { FileError, getOrThrow } from "../../src/harness/types.js"; -import { executeShellWithCapture } from "../../src/harness/utils/shell-output.js"; -import { createTempDir } from "./session-test-utils.js"; +import { NodeExecutionEnv } from "../../src/harness/env/nodejs.ts"; +import { FileError, getOrThrow } from "../../src/harness/types.ts"; +import { executeShellWithCapture } from "../../src/harness/utils/shell-output.ts"; +import { createTempDir } from "./session-test-utils.ts"; const chmodRestorePaths: string[] = []; diff --git a/packages/agent/test/harness/prompt-templates.test.ts b/packages/agent/test/harness/prompt-templates.test.ts index adb11006..8e118ad4 100644 --- a/packages/agent/test/harness/prompt-templates.test.ts +++ b/packages/agent/test/harness/prompt-templates.test.ts @@ -1,13 +1,13 @@ import { symlink } from "node:fs/promises"; import { join } from "node:path"; import { describe, expect, it } from "vitest"; -import { NodeExecutionEnv } from "../../src/harness/env/nodejs.js"; +import { NodeExecutionEnv } from "../../src/harness/env/nodejs.ts"; import { formatPromptTemplateInvocation, loadPromptTemplates, loadSourcedPromptTemplates, -} from "../../src/harness/prompt-templates.js"; -import { createTempDir } from "./session-test-utils.js"; +} from "../../src/harness/prompt-templates.ts"; +import { createTempDir } from "./session-test-utils.ts"; describe("loadPromptTemplates", () => { it("loads markdown templates non-recursively from one or more dirs", async () => { diff --git a/packages/agent/test/harness/repo.test.ts b/packages/agent/test/harness/repo.test.ts index fda51b03..b768fc1a 100644 --- a/packages/agent/test/harness/repo.test.ts +++ b/packages/agent/test/harness/repo.test.ts @@ -1,9 +1,9 @@ import { existsSync } from "node:fs"; import { describe, expect, it } from "vitest"; -import { NodeExecutionEnv } from "../../src/harness/env/nodejs.js"; -import { JsonlSessionRepo } from "../../src/harness/session/jsonl-repo.js"; -import { InMemorySessionRepo } from "../../src/harness/session/memory-repo.js"; -import { createAssistantMessage, createTempDir, createUserMessage } from "./session-test-utils.js"; +import { NodeExecutionEnv } from "../../src/harness/env/nodejs.ts"; +import { JsonlSessionRepo } from "../../src/harness/session/jsonl-repo.ts"; +import { InMemorySessionRepo } from "../../src/harness/session/memory-repo.ts"; +import { createAssistantMessage, createTempDir, createUserMessage } from "./session-test-utils.ts"; describe("InMemorySessionRepo", () => { it("opens, deletes, and forks by metadata", async () => { diff --git a/packages/agent/test/harness/resource-formatting.test.ts b/packages/agent/test/harness/resource-formatting.test.ts index fe3fed40..45d4553f 100644 --- a/packages/agent/test/harness/resource-formatting.test.ts +++ b/packages/agent/test/harness/resource-formatting.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest"; -import { formatPromptTemplateInvocation } from "../../src/harness/prompt-templates.js"; -import { formatSkillInvocation } from "../../src/harness/skills.js"; +import { formatPromptTemplateInvocation } from "../../src/harness/prompt-templates.ts"; +import { formatSkillInvocation } from "../../src/harness/skills.ts"; describe("resource formatting helpers", () => { it("formats skill invocations with additional instructions", () => { diff --git a/packages/agent/test/harness/session-uuid.test.ts b/packages/agent/test/harness/session-uuid.test.ts index 247f3177..b8d571f4 100644 --- a/packages/agent/test/harness/session-uuid.test.ts +++ b/packages/agent/test/harness/session-uuid.test.ts @@ -1,5 +1,5 @@ import { afterEach, describe, expect, it, vi } from "vitest"; -import { uuidv7 } from "../../src/harness/session/uuid.js"; +import { uuidv7 } from "../../src/harness/session/uuid.ts"; const UUID_V7_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/; const TIMESTAMP = 0x0123456789ab; diff --git a/packages/agent/test/harness/session.test.ts b/packages/agent/test/harness/session.test.ts index 96510ea4..c9598da8 100644 --- a/packages/agent/test/harness/session.test.ts +++ b/packages/agent/test/harness/session.test.ts @@ -1,12 +1,12 @@ import { readFileSync } from "node:fs"; import { join } from "node:path"; import { describe, expect, it } from "vitest"; -import { NodeExecutionEnv } from "../../src/harness/env/nodejs.js"; -import { JsonlSessionStorage } from "../../src/harness/session/jsonl-storage.js"; -import { InMemorySessionStorage } from "../../src/harness/session/memory-storage.js"; -import { Session } from "../../src/harness/session/session.js"; -import type { SessionStorage } from "../../src/harness/types.js"; -import { createAssistantMessage, createTempDir, createUserMessage, getLatestTempDir } from "./session-test-utils.js"; +import { NodeExecutionEnv } from "../../src/harness/env/nodejs.ts"; +import { JsonlSessionStorage } from "../../src/harness/session/jsonl-storage.ts"; +import { InMemorySessionStorage } from "../../src/harness/session/memory-storage.ts"; +import { Session } from "../../src/harness/session/session.ts"; +import type { SessionStorage } from "../../src/harness/types.ts"; +import { createAssistantMessage, createTempDir, createUserMessage, getLatestTempDir } from "./session-test-utils.ts"; async function runSessionSuite( name: string, diff --git a/packages/agent/test/harness/skills.test.ts b/packages/agent/test/harness/skills.test.ts index 41840a3e..bd769ea9 100644 --- a/packages/agent/test/harness/skills.test.ts +++ b/packages/agent/test/harness/skills.test.ts @@ -1,9 +1,9 @@ import { symlink } from "node:fs/promises"; import { join } from "node:path"; import { describe, expect, it } from "vitest"; -import { NodeExecutionEnv } from "../../src/harness/env/nodejs.js"; -import { loadSkills, loadSourcedSkills } from "../../src/harness/skills.js"; -import { createTempDir } from "./session-test-utils.js"; +import { NodeExecutionEnv } from "../../src/harness/env/nodejs.ts"; +import { loadSkills, loadSourcedSkills } from "../../src/harness/skills.ts"; +import { createTempDir } from "./session-test-utils.ts"; describe("loadSkills", () => { it("loads SKILL.md files through the execution environment", async () => { diff --git a/packages/agent/test/harness/storage.test.ts b/packages/agent/test/harness/storage.test.ts index ff6a6a95..e8383ce4 100644 --- a/packages/agent/test/harness/storage.test.ts +++ b/packages/agent/test/harness/storage.test.ts @@ -1,11 +1,11 @@ import { existsSync, readFileSync, writeFileSync } from "node:fs"; import { join } from "node:path"; import { describe, expect, it } from "vitest"; -import { NodeExecutionEnv } from "../../src/harness/env/nodejs.js"; -import { JsonlSessionStorage, loadJsonlSessionMetadata } from "../../src/harness/session/jsonl-storage.js"; -import { InMemorySessionStorage } from "../../src/harness/session/memory-storage.js"; -import { type MessageEntry, ok, type SessionMetadata } from "../../src/harness/types.js"; -import { createAssistantMessage, createTempDir, createUserMessage } from "./session-test-utils.js"; +import { NodeExecutionEnv } from "../../src/harness/env/nodejs.ts"; +import { JsonlSessionStorage, loadJsonlSessionMetadata } from "../../src/harness/session/jsonl-storage.ts"; +import { InMemorySessionStorage } from "../../src/harness/session/memory-storage.ts"; +import { type MessageEntry, ok, type SessionMetadata } from "../../src/harness/types.ts"; +import { createAssistantMessage, createTempDir, createUserMessage } from "./session-test-utils.ts"; describe("InMemorySessionStorage", () => { it("returns configured session metadata", async () => { diff --git a/packages/agent/test/harness/system-prompt.test.ts b/packages/agent/test/harness/system-prompt.test.ts index 46ce884b..f20fbd16 100644 --- a/packages/agent/test/harness/system-prompt.test.ts +++ b/packages/agent/test/harness/system-prompt.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "vitest"; -import { formatSkillsForSystemPrompt } from "../../src/harness/system-prompt.js"; +import { formatSkillsForSystemPrompt } from "../../src/harness/system-prompt.ts"; const visibleSkill = { name: "visible", diff --git a/packages/agent/test/harness/truncate.test.ts b/packages/agent/test/harness/truncate.test.ts index f712cc2c..3784a52c 100644 --- a/packages/agent/test/harness/truncate.test.ts +++ b/packages/agent/test/harness/truncate.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "vitest"; -import { truncateHead, truncateTail } from "../../src/harness/utils/truncate.js"; +import { truncateHead, truncateTail } from "../../src/harness/utils/truncate.ts"; const encoder = new TextEncoder(); diff --git a/packages/agent/test/scratch/simple.ts b/packages/agent/test/scratch/simple.ts index 2b8fed11..de6ba3a2 100644 --- a/packages/agent/test/scratch/simple.ts +++ b/packages/agent/test/scratch/simple.ts @@ -1,8 +1,8 @@ import { homedir } from "node:os"; import { join } from "node:path"; import { getModel } from "@earendil-works/pi-ai"; -import { NodeExecutionEnv } from "../../src/harness/env/nodejs.js"; -import { InMemorySessionStorage } from "../../src/harness/session/memory-storage.js"; +import { NodeExecutionEnv } from "../../src/harness/env/nodejs.ts"; +import { InMemorySessionStorage } from "../../src/harness/session/memory-storage.ts"; import { AgentHarness, formatSkillsForSystemPrompt, @@ -11,13 +11,14 @@ import { type PromptTemplate, Session, type Skill, -} from "../../src/index.js"; +} from "../../src/index.ts"; type Source = { type: "project" | "user" | "path"; dir: string }; type SourcedSkill = Skill & { source: Source }; type SourcedPromptTemplate = PromptTemplate & { source: Source }; const env = new NodeExecutionEnv({ cwd: process.cwd() }); + const source = (type: Source["type"], dir: string) => ({ path: dir, source: { type, dir } }); const { skills: sourcedSkills } = await loadSourcedSkills( env, diff --git a/packages/agent/test/utils/calculate.ts b/packages/agent/test/utils/calculate.ts index cd2a3777..6158efbd 100644 --- a/packages/agent/test/utils/calculate.ts +++ b/packages/agent/test/utils/calculate.ts @@ -1,5 +1,5 @@ import { type Static, Type } from "typebox"; -import type { AgentTool, AgentToolResult } from "../../src/types.js"; +import type { AgentTool, AgentToolResult } from "../../src/types.ts"; export interface CalculateResult extends AgentToolResult { content: Array<{ type: "text"; text: string }>; diff --git a/packages/agent/test/utils/get-current-time.ts b/packages/agent/test/utils/get-current-time.ts index b7075d9b..a83a82af 100644 --- a/packages/agent/test/utils/get-current-time.ts +++ b/packages/agent/test/utils/get-current-time.ts @@ -1,5 +1,5 @@ import { type Static, Type } from "typebox"; -import type { AgentTool, AgentToolResult } from "../../src/types.js"; +import type { AgentTool, AgentToolResult } from "../../src/types.ts"; export interface GetCurrentTimeResult extends AgentToolResult<{ utcTimestamp: number }> {} diff --git a/packages/ai/package.json b/packages/ai/package.json index 2fa04afc..16fa3be7 100644 --- a/packages/ai/package.json +++ b/packages/ai/package.json @@ -60,22 +60,22 @@ ], "scripts": { "clean": "shx rm -rf dist", - "generate-models": "npx tsx scripts/generate-models.ts", - "generate-image-models": "npx tsx scripts/generate-image-models.ts", + "generate-models": "node scripts/generate-models.ts", + "generate-image-models": "node scripts/generate-image-models.ts", "build": "npm run generate-models && npm run generate-image-models && tsgo -p tsconfig.build.json", "test": "vitest --run", "prepublishOnly": "npm run clean && npm run build" }, "dependencies": { - "@anthropic-ai/sdk": "^0.91.1", - "@aws-sdk/client-bedrock-runtime": "^3.1030.0", - "@google/genai": "^1.40.0", - "@mistralai/mistralai": "^2.2.0", - "http-proxy-agent": "^7.0.2", - "https-proxy-agent": "^7.0.6", + "@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.24" + "partial-json": "0.1.7", + "typebox": "1.1.38" }, "keywords": [ "ai", @@ -98,8 +98,8 @@ "node": ">=22.19.0" }, "devDependencies": { - "@types/node": "^24.3.0", - "canvas": "^3.2.0", - "vitest": "^3.2.4" + "@types/node": "24.12.4", + "canvas": "3.2.3", + "vitest": "3.2.4" } } diff --git a/packages/ai/scripts/generate-image-models.ts b/packages/ai/scripts/generate-image-models.ts index c312f3e4..2744da48 100644 --- a/packages/ai/scripts/generate-image-models.ts +++ b/packages/ai/scripts/generate-image-models.ts @@ -1,9 +1,9 @@ -#!/usr/bin/env tsx +#!/usr/bin/env node import { writeFileSync } from "fs"; import { dirname, join } from "path"; import { fileURLToPath } from "url"; -import type { ImagesModel } from "../src/types.js"; +import type { ImagesModel } from "../src/types.ts"; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); diff --git a/packages/ai/scripts/generate-models.ts b/packages/ai/scripts/generate-models.ts index e896726c..0c025c68 100644 --- a/packages/ai/scripts/generate-models.ts +++ b/packages/ai/scripts/generate-models.ts @@ -1,4 +1,4 @@ -#!/usr/bin/env tsx +#!/usr/bin/env node import { writeFileSync } from "fs"; import { join, dirname } from "path"; @@ -8,14 +8,8 @@ import { CLOUDFLARE_AI_GATEWAY_COMPAT_BASE_URL, CLOUDFLARE_AI_GATEWAY_OPENAI_BASE_URL, CLOUDFLARE_WORKERS_AI_BASE_URL, -} from "../src/providers/cloudflare.js"; -import { - Api, - type AnthropicMessagesCompat, - KnownProvider, - Model, - type OpenAICompletionsCompat, -} from "../src/types.js"; +} from "../src/providers/cloudflare.ts"; +import type { AnthropicMessagesCompat, Api, KnownProvider, Model, OpenAICompletionsCompat } from "../src/types.ts"; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); diff --git a/packages/ai/scripts/generate-test-image.ts b/packages/ai/scripts/generate-test-image.ts index 29a473d4..da3df266 100644 --- a/packages/ai/scripts/generate-test-image.ts +++ b/packages/ai/scripts/generate-test-image.ts @@ -1,7 +1,7 @@ -#!/usr/bin/env tsx +#!/usr/bin/env node import { createCanvas } from "canvas"; -import { writeFileSync } from "fs"; +import { mkdirSync, writeFileSync } from "fs"; import { join, dirname } from "path"; import { fileURLToPath } from "url"; @@ -27,7 +27,6 @@ const buffer = canvas.toBuffer("image/png"); const outputPath = join(__dirname, "..", "test", "data", "red-circle.png"); // Ensure the directory exists -import { mkdirSync } from "fs"; mkdirSync(join(__dirname, "..", "test", "data"), { recursive: true }); writeFileSync(outputPath, buffer); diff --git a/packages/ai/test/abort.test.ts b/packages/ai/test/abort.test.ts index cd46a629..27c274aa 100644 --- a/packages/ai/test/abort.test.ts +++ b/packages/ai/test/abort.test.ts @@ -1,13 +1,13 @@ import { describe, expect, it } from "vitest"; -import { getModel } from "../src/models.js"; -import { complete, stream } from "../src/stream.js"; -import type { Api, Context, Model, StreamOptions } from "../src/types.js"; +import { getModel } from "../src/models.ts"; +import { complete, stream } from "../src/stream.ts"; +import type { Api, Context, Model, StreamOptions } from "../src/types.ts"; type StreamOptionsWithExtras = StreamOptions & Record; -import { hasAzureOpenAICredentials, resolveAzureDeploymentName } from "./azure-utils.js"; -import { hasBedrockCredentials } from "./bedrock-utils.js"; -import { resolveApiKey } from "./oauth.js"; +import { hasAzureOpenAICredentials, resolveAzureDeploymentName } from "./azure-utils.ts"; +import { hasBedrockCredentials } from "./bedrock-utils.ts"; +import { resolveApiKey } from "./oauth.ts"; // Resolve OAuth tokens at module level (async, runs before tests) const [openaiCodexToken] = await Promise.all([resolveApiKey("openai-codex")]); diff --git a/packages/ai/test/anthropic-eager-tool-input-compat.test.ts b/packages/ai/test/anthropic-eager-tool-input-compat.test.ts index e73a00a0..c6df9c3e 100644 --- a/packages/ai/test/anthropic-eager-tool-input-compat.test.ts +++ b/packages/ai/test/anthropic-eager-tool-input-compat.test.ts @@ -2,8 +2,8 @@ import { createServer, type IncomingMessage, type ServerResponse } from "node:ht import type { AddressInfo } from "node:net"; import { Type } from "typebox"; import { describe, expect, it } from "vitest"; -import { streamAnthropic } from "../src/providers/anthropic.js"; -import type { Context, Model, Tool } from "../src/types.js"; +import { streamAnthropic } from "../src/providers/anthropic.ts"; +import type { Context, Model, Tool } from "../src/types.ts"; interface CapturedRequest { headers: IncomingMessage["headers"]; diff --git a/packages/ai/test/anthropic-eager-tool-input-e2e.test.ts b/packages/ai/test/anthropic-eager-tool-input-e2e.test.ts index 3fed7ce2..2483e1a4 100644 --- a/packages/ai/test/anthropic-eager-tool-input-e2e.test.ts +++ b/packages/ai/test/anthropic-eager-tool-input-e2e.test.ts @@ -1,10 +1,10 @@ import { Type } from "typebox"; import { describe, expect, it } from "vitest"; -import { getEnvApiKey } from "../src/env-api-keys.js"; -import { getModels, getProviders } from "../src/models.js"; -import { complete } from "../src/stream.js"; -import type { Api, KnownProvider, Model, ProviderStreamOptions, Tool } from "../src/types.js"; -import { resolveApiKey } from "./oauth.js"; +import { getEnvApiKey } from "../src/env-api-keys.ts"; +import { getModels, getProviders } from "../src/models.ts"; +import { complete } from "../src/stream.ts"; +import type { Api, KnownProvider, Model, ProviderStreamOptions, Tool } from "../src/types.ts"; +import { resolveApiKey } from "./oauth.ts"; const githubCopilotToken = await resolveApiKey("github-copilot"); diff --git a/packages/ai/test/anthropic-long-cache-retention-e2e.test.ts b/packages/ai/test/anthropic-long-cache-retention-e2e.test.ts index d900aa71..2b7667d7 100644 --- a/packages/ai/test/anthropic-long-cache-retention-e2e.test.ts +++ b/packages/ai/test/anthropic-long-cache-retention-e2e.test.ts @@ -1,9 +1,9 @@ import { describe, expect, it } from "vitest"; -import { getEnvApiKey } from "../src/env-api-keys.js"; -import { getModels, getProviders } from "../src/models.js"; -import { complete } from "../src/stream.js"; -import type { Api, KnownProvider, Model, ProviderStreamOptions } from "../src/types.js"; -import { resolveApiKey } from "./oauth.js"; +import { getEnvApiKey } from "../src/env-api-keys.ts"; +import { getModels, getProviders } from "../src/models.ts"; +import { complete } from "../src/stream.ts"; +import type { Api, KnownProvider, Model, ProviderStreamOptions } from "../src/types.ts"; +import { resolveApiKey } from "./oauth.ts"; const githubCopilotToken = await resolveApiKey("github-copilot"); diff --git a/packages/ai/test/anthropic-oauth.test.ts b/packages/ai/test/anthropic-oauth.test.ts index f8e066c6..36585a5f 100644 --- a/packages/ai/test/anthropic-oauth.test.ts +++ b/packages/ai/test/anthropic-oauth.test.ts @@ -1,5 +1,5 @@ import { afterEach, describe, expect, it, vi } from "vitest"; -import { loginAnthropic, refreshAnthropicToken } from "../src/utils/oauth/anthropic.js"; +import { loginAnthropic, refreshAnthropicToken } from "../src/utils/oauth/anthropic.ts"; function jsonResponse(body: unknown, status: number = 200): Response { return new Response(JSON.stringify(body), { diff --git a/packages/ai/test/anthropic-opus-4-7-smoke.test.ts b/packages/ai/test/anthropic-opus-4-7-smoke.test.ts index 29626d1f..7314f8af 100644 --- a/packages/ai/test/anthropic-opus-4-7-smoke.test.ts +++ b/packages/ai/test/anthropic-opus-4-7-smoke.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it } from "vitest"; -import { getModel } from "../src/models.js"; -import { streamSimple } from "../src/stream.js"; -import type { Context } from "../src/types.js"; +import { getModel } from "../src/models.ts"; +import { streamSimple } from "../src/stream.ts"; +import type { Context } from "../src/types.ts"; interface AnthropicThinkingPayload { thinking?: { type: string }; diff --git a/packages/ai/test/anthropic-sse-parsing.test.ts b/packages/ai/test/anthropic-sse-parsing.test.ts index 5bbc7d72..03024966 100644 --- a/packages/ai/test/anthropic-sse-parsing.test.ts +++ b/packages/ai/test/anthropic-sse-parsing.test.ts @@ -1,9 +1,9 @@ import type Anthropic from "@anthropic-ai/sdk"; import { Type } from "typebox"; import { describe, expect, it } from "vitest"; -import { getModel } from "../src/models.js"; -import { streamAnthropic } from "../src/providers/anthropic.js"; -import type { Context, ToolCall } from "../src/types.js"; +import { getModel } from "../src/models.ts"; +import { streamAnthropic } from "../src/providers/anthropic.ts"; +import type { Context, ToolCall } from "../src/types.ts"; function createSseResponse(events: Array<{ event: string; data: string }>): Response { const body = events.map(({ event, data }) => `event: ${event}\ndata: ${data}\n`).join("\n"); diff --git a/packages/ai/test/anthropic-tool-name-normalization.test.ts b/packages/ai/test/anthropic-tool-name-normalization.test.ts index 1ea6bdb2..bb454081 100644 --- a/packages/ai/test/anthropic-tool-name-normalization.test.ts +++ b/packages/ai/test/anthropic-tool-name-normalization.test.ts @@ -1,9 +1,9 @@ import { Type } from "typebox"; import { describe, expect, it } from "vitest"; -import { getModel } from "../src/models.js"; -import { stream } from "../src/stream.js"; -import type { Context, Tool } from "../src/types.js"; -import { resolveApiKey } from "./oauth.js"; +import { getModel } from "../src/models.ts"; +import { stream } from "../src/stream.ts"; +import type { Context, Tool } from "../src/types.ts"; +import { resolveApiKey } from "./oauth.ts"; const oauthToken = await resolveApiKey("anthropic"); diff --git a/packages/ai/test/azure-openai-base-url.test.ts b/packages/ai/test/azure-openai-base-url.test.ts index cdaebc4e..530c5f47 100644 --- a/packages/ai/test/azure-openai-base-url.test.ts +++ b/packages/ai/test/azure-openai-base-url.test.ts @@ -1,7 +1,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import { getModel } from "../src/models.js"; -import { streamAzureOpenAIResponses } from "../src/providers/azure-openai-responses.js"; -import type { Context } from "../src/types.js"; +import { getModel } from "../src/models.ts"; +import { streamAzureOpenAIResponses } from "../src/providers/azure-openai-responses.ts"; +import type { Context } from "../src/types.ts"; interface CapturedAzureClientOptions { apiKey: string; diff --git a/packages/ai/test/bedrock-convert-messages.test.ts b/packages/ai/test/bedrock-convert-messages.test.ts index d9608763..d62abd1b 100644 --- a/packages/ai/test/bedrock-convert-messages.test.ts +++ b/packages/ai/test/bedrock-convert-messages.test.ts @@ -44,9 +44,9 @@ vi.mock("@aws-sdk/client-bedrock-runtime", () => { }; }); -import { getModel } from "../src/models.js"; -import { streamBedrock } from "../src/providers/amazon-bedrock.js"; -import type { Context, Message } from "../src/types.js"; +import { getModel } from "../src/models.ts"; +import { streamBedrock } from "../src/providers/amazon-bedrock.ts"; +import type { Context, Message } from "../src/types.ts"; const baseModel = getModel("amazon-bedrock", "us.anthropic.claude-sonnet-4-5-20250929-v1:0"); diff --git a/packages/ai/test/bedrock-endpoint-resolution.test.ts b/packages/ai/test/bedrock-endpoint-resolution.test.ts index 374599a4..a45bc6cf 100644 --- a/packages/ai/test/bedrock-endpoint-resolution.test.ts +++ b/packages/ai/test/bedrock-endpoint-resolution.test.ts @@ -44,9 +44,9 @@ vi.mock("@aws-sdk/client-bedrock-runtime", () => { }; }); -import { getModel } from "../src/models.js"; -import { streamBedrock } from "../src/providers/amazon-bedrock.js"; -import type { Context, Model } from "../src/types.js"; +import { getModel } from "../src/models.ts"; +import { streamBedrock } from "../src/providers/amazon-bedrock.ts"; +import type { Context, Model } from "../src/types.ts"; const context: Context = { messages: [{ role: "user", content: "hello", timestamp: Date.now() }], diff --git a/packages/ai/test/bedrock-models.test.ts b/packages/ai/test/bedrock-models.test.ts index 453a3953..2cfd8fb9 100644 --- a/packages/ai/test/bedrock-models.test.ts +++ b/packages/ai/test/bedrock-models.test.ts @@ -17,10 +17,10 @@ */ import { describe, expect, it } from "vitest"; -import { getModels } from "../src/models.js"; -import { complete } from "../src/stream.js"; -import type { Context } from "../src/types.js"; -import { hasBedrockCredentials } from "./bedrock-utils.js"; +import { getModels } from "../src/models.ts"; +import { complete } from "../src/stream.ts"; +import type { Context } from "../src/types.ts"; +import { hasBedrockCredentials } from "./bedrock-utils.ts"; describe("Amazon Bedrock Models", () => { const models = getModels("amazon-bedrock"); diff --git a/packages/ai/test/codex-websocket-cached-probe.ts b/packages/ai/test/codex-websocket-cached-probe.ts index ccc875f8..7317035c 100644 --- a/packages/ai/test/codex-websocket-cached-probe.ts +++ b/packages/ai/test/codex-websocket-cached-probe.ts @@ -1,4 +1,4 @@ -#!/usr/bin/env tsx +#!/usr/bin/env node /** * Live probe for OpenAI Codex Responses websocket-cached mode. * @@ -9,15 +9,15 @@ import { tmpdir } from "node:os"; import { join, resolve } from "node:path"; import { Type } from "typebox"; -import { AuthStorage } from "../../coding-agent/src/core/auth-storage.js"; -import { getModel } from "../src/models.js"; +import { AuthStorage } from "../../coding-agent/src/core/auth-storage.ts"; +import { getModel } from "../src/models.ts"; import { closeOpenAICodexWebSocketSessions, getOpenAICodexWebSocketDebugStats, resetOpenAICodexWebSocketDebugStats, streamOpenAICodexResponses, -} from "../src/providers/openai-codex-responses.js"; -import type { AssistantMessage, Context, Message, Model, Tool, ToolResultMessage, Transport } from "../src/types.js"; +} from "../src/providers/openai-codex-responses.ts"; +import type { AssistantMessage, Context, Message, Model, Tool, ToolResultMessage, Transport } from "../src/types.ts"; type ThinkingLevel = "minimal" | "low" | "medium" | "high" | "xhigh"; @@ -85,7 +85,7 @@ function required(value: string | undefined, flag: string): string { } function printHelp(): void { - console.log(`Usage: npx tsx test/codex-websocket-cached-probe.ts [options] + console.log(`Usage: node test/codex-websocket-cached-probe.ts [options] Options: --turns Number of user turns. Default: ${DEFAULT_TURNS} diff --git a/packages/ai/test/context-overflow.test.ts b/packages/ai/test/context-overflow.test.ts index 718800bf..164ca75d 100644 --- a/packages/ai/test/context-overflow.test.ts +++ b/packages/ai/test/context-overflow.test.ts @@ -14,13 +14,13 @@ import type { ChildProcess } from "child_process"; import { execSync, spawn } from "child_process"; import { afterAll, beforeAll, describe, expect, it } from "vitest"; -import { getModel } from "../src/models.js"; -import { complete } from "../src/stream.js"; -import type { AssistantMessage, Context, Model, Usage } from "../src/types.js"; -import { isContextOverflow } from "../src/utils/overflow.js"; -import { hasAzureOpenAICredentials } from "./azure-utils.js"; -import { hasBedrockCredentials } from "./bedrock-utils.js"; -import { resolveApiKey } from "./oauth.js"; +import { getModel } from "../src/models.ts"; +import { complete } from "../src/stream.ts"; +import type { AssistantMessage, Context, Model, Usage } from "../src/types.ts"; +import { isContextOverflow } from "../src/utils/overflow.ts"; +import { hasAzureOpenAICredentials } from "./azure-utils.ts"; +import { hasBedrockCredentials } from "./bedrock-utils.ts"; +import { resolveApiKey } from "./oauth.ts"; // Resolve OAuth tokens at module level (async, runs before tests) const oauthTokens = await Promise.all([resolveApiKey("github-copilot"), resolveApiKey("openai-codex")]); diff --git a/packages/ai/test/cross-provider-handoff.test.ts b/packages/ai/test/cross-provider-handoff.test.ts index a133241a..23593394 100644 --- a/packages/ai/test/cross-provider-handoff.test.ts +++ b/packages/ai/test/cross-provider-handoff.test.ts @@ -25,12 +25,12 @@ import { writeFileSync } from "fs"; import { Type } from "typebox"; import { beforeAll, describe, expect, it } from "vitest"; -import { getModel } from "../src/models.js"; -import { completeSimple, getEnvApiKey } from "../src/stream.js"; -import type { Api, AssistantMessage, Message, Model, Tool, ToolResultMessage } from "../src/types.js"; -import { hasAzureOpenAICredentials } from "./azure-utils.js"; -import { hasCloudflareAiGatewayCredentials, hasCloudflareWorkersAICredentials } from "./cloudflare-utils.js"; -import { resolveApiKey } from "./oauth.js"; +import { getModel } from "../src/models.ts"; +import { completeSimple, getEnvApiKey } from "../src/stream.ts"; +import type { Api, AssistantMessage, Message, Model, Tool, ToolResultMessage } from "../src/types.ts"; +import { hasAzureOpenAICredentials } from "./azure-utils.ts"; +import { hasCloudflareAiGatewayCredentials, hasCloudflareWorkersAICredentials } from "./cloudflare-utils.ts"; +import { resolveApiKey } from "./oauth.ts"; // Simple tool for testing const testToolSchema = Type.Object({ diff --git a/packages/ai/test/empty.test.ts b/packages/ai/test/empty.test.ts index 121c07b9..7df7de2c 100644 --- a/packages/ai/test/empty.test.ts +++ b/packages/ai/test/empty.test.ts @@ -1,14 +1,14 @@ import { describe, expect, it } from "vitest"; -import { getModel } from "../src/models.js"; -import { complete } from "../src/stream.js"; -import type { Api, AssistantMessage, Context, Model, StreamOptions, UserMessage } from "../src/types.js"; +import { getModel } from "../src/models.ts"; +import { complete } from "../src/stream.ts"; +import type { Api, AssistantMessage, Context, Model, StreamOptions, UserMessage } from "../src/types.ts"; type StreamOptionsWithExtras = StreamOptions & Record; -import { hasAzureOpenAICredentials, resolveAzureDeploymentName } from "./azure-utils.js"; -import { hasBedrockCredentials } from "./bedrock-utils.js"; -import { hasCloudflareAiGatewayCredentials, hasCloudflareWorkersAICredentials } from "./cloudflare-utils.js"; -import { resolveApiKey } from "./oauth.js"; +import { hasAzureOpenAICredentials, resolveAzureDeploymentName } from "./azure-utils.ts"; +import { hasBedrockCredentials } from "./bedrock-utils.ts"; +import { hasCloudflareAiGatewayCredentials, hasCloudflareWorkersAICredentials } from "./cloudflare-utils.ts"; +import { resolveApiKey } from "./oauth.ts"; // Resolve OAuth tokens at module level (async, runs before tests) const oauthTokens = await Promise.all([ diff --git a/packages/ai/test/env-api-keys.test.ts b/packages/ai/test/env-api-keys.test.ts index 6889743d..61fec912 100644 --- a/packages/ai/test/env-api-keys.test.ts +++ b/packages/ai/test/env-api-keys.test.ts @@ -1,5 +1,5 @@ import { afterEach, describe, expect, it } from "vitest"; -import { findEnvKeys, getEnvApiKey } from "../src/env-api-keys.js"; +import { findEnvKeys, getEnvApiKey } from "../src/env-api-keys.ts"; const originalCopilotGitHubToken = process.env.COPILOT_GITHUB_TOKEN; const originalGhToken = process.env.GH_TOKEN; diff --git a/packages/ai/test/faux-provider.test.ts b/packages/ai/test/faux-provider.test.ts index 16f33883..4f110c54 100644 --- a/packages/ai/test/faux-provider.test.ts +++ b/packages/ai/test/faux-provider.test.ts @@ -8,8 +8,8 @@ import { registerFauxProvider, stream, Type, -} from "../src/index.js"; -import type { AssistantMessageEvent, Context } from "../src/types.js"; +} from "../src/index.ts"; +import type { AssistantMessageEvent, Context } from "../src/types.ts"; async function collectEvents(streamResult: ReturnType): Promise { const events: AssistantMessageEvent[] = []; diff --git a/packages/ai/test/fireworks-models.test.ts b/packages/ai/test/fireworks-models.test.ts index 35427c10..0260a910 100644 --- a/packages/ai/test/fireworks-models.test.ts +++ b/packages/ai/test/fireworks-models.test.ts @@ -2,10 +2,10 @@ import { createServer, type IncomingMessage, type ServerResponse } from "node:ht import type { AddressInfo } from "node:net"; import { Type } from "typebox"; import { afterEach, describe, expect, it } from "vitest"; -import { findEnvKeys, getEnvApiKey } from "../src/env-api-keys.js"; -import { getModel } from "../src/models.js"; -import { streamAnthropic } from "../src/providers/anthropic.js"; -import type { Context, Model, Tool } from "../src/types.js"; +import { findEnvKeys, getEnvApiKey } from "../src/env-api-keys.ts"; +import { getModel } from "../src/models.ts"; +import { streamAnthropic } from "../src/providers/anthropic.ts"; +import type { Context, Model, Tool } from "../src/types.ts"; const originalFireworksApiKey = process.env.FIREWORKS_API_KEY; diff --git a/packages/ai/test/github-copilot-anthropic.test.ts b/packages/ai/test/github-copilot-anthropic.test.ts index 7fda4a45..ee3de3e8 100644 --- a/packages/ai/test/github-copilot-anthropic.test.ts +++ b/packages/ai/test/github-copilot-anthropic.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it, vi } from "vitest"; -import { getModel } from "../src/models.js"; -import { streamAnthropic } from "../src/providers/anthropic.js"; -import type { Context } from "../src/types.js"; +import { getModel } from "../src/models.ts"; +import { streamAnthropic } from "../src/providers/anthropic.ts"; +import type { Context } from "../src/types.ts"; const mockState = vi.hoisted(() => ({ constructorOpts: undefined as Record | undefined, diff --git a/packages/ai/test/github-copilot-oauth.test.ts b/packages/ai/test/github-copilot-oauth.test.ts index 0367892c..085b1cf0 100644 --- a/packages/ai/test/github-copilot-oauth.test.ts +++ b/packages/ai/test/github-copilot-oauth.test.ts @@ -1,5 +1,5 @@ import { afterEach, describe, expect, it, vi } from "vitest"; -import { loginGitHubCopilot } from "../src/utils/oauth/github-copilot.js"; +import { loginGitHubCopilot } from "../src/utils/oauth/github-copilot.ts"; function jsonResponse(body: unknown, status: number = 200): Response { return new Response(JSON.stringify(body), { diff --git a/packages/ai/test/google-shared-convert-tools.test.ts b/packages/ai/test/google-shared-convert-tools.test.ts index 6cce3175..c7b10b41 100644 --- a/packages/ai/test/google-shared-convert-tools.test.ts +++ b/packages/ai/test/google-shared-convert-tools.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest"; -import { convertTools } from "../src/providers/google-shared.js"; -import type { Tool } from "../src/types.js"; +import { convertTools } from "../src/providers/google-shared.ts"; +import type { Tool } from "../src/types.ts"; function makeTool(parameters: Record): Tool { return { diff --git a/packages/ai/test/google-shared-gemini3-unsigned-tool-call.test.ts b/packages/ai/test/google-shared-gemini3-unsigned-tool-call.test.ts index 1822e9dd..920c6a4e 100644 --- a/packages/ai/test/google-shared-gemini3-unsigned-tool-call.test.ts +++ b/packages/ai/test/google-shared-gemini3-unsigned-tool-call.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest"; -import { convertMessages } from "../src/providers/google-shared.js"; -import type { Context, Model } from "../src/types.js"; +import { convertMessages } from "../src/providers/google-shared.ts"; +import type { Context, Model } from "../src/types.ts"; function makeGemini3Model( api: TApi, diff --git a/packages/ai/test/google-shared-image-tool-result-routing.test.ts b/packages/ai/test/google-shared-image-tool-result-routing.test.ts index 27150dba..1430a084 100644 --- a/packages/ai/test/google-shared-image-tool-result-routing.test.ts +++ b/packages/ai/test/google-shared-image-tool-result-routing.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest"; -import { convertMessages } from "../src/providers/google-shared.js"; -import type { Context, Model } from "../src/types.js"; +import { convertMessages } from "../src/providers/google-shared.ts"; +import type { Context, Model } from "../src/types.ts"; function makeModel( api: TApi, diff --git a/packages/ai/test/google-thinking-disable.test.ts b/packages/ai/test/google-thinking-disable.test.ts index e8720ff8..30df3638 100644 --- a/packages/ai/test/google-thinking-disable.test.ts +++ b/packages/ai/test/google-thinking-disable.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it } from "vitest"; -import { getModel } from "../src/models.js"; -import { streamSimple } from "../src/stream.js"; -import type { Api, Context, Model, SimpleStreamOptions } from "../src/types.js"; +import { getModel } from "../src/models.ts"; +import { streamSimple } from "../src/stream.ts"; +import type { Api, Context, Model, SimpleStreamOptions } from "../src/types.ts"; type SimpleOptionsWithExtras = SimpleStreamOptions & Record; diff --git a/packages/ai/test/google-thinking-signature.test.ts b/packages/ai/test/google-thinking-signature.test.ts index c90f7caa..83b17ccc 100644 --- a/packages/ai/test/google-thinking-signature.test.ts +++ b/packages/ai/test/google-thinking-signature.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "vitest"; -import { isThinkingPart, retainThoughtSignature } from "../src/providers/google-shared.js"; +import { isThinkingPart, retainThoughtSignature } from "../src/providers/google-shared.ts"; describe("Google thinking detection (thoughtSignature)", () => { it("treats part.thought === true as thinking", () => { diff --git a/packages/ai/test/google-vertex-api-key-resolution.test.ts b/packages/ai/test/google-vertex-api-key-resolution.test.ts index d05702c3..5f66649c 100644 --- a/packages/ai/test/google-vertex-api-key-resolution.test.ts +++ b/packages/ai/test/google-vertex-api-key-resolution.test.ts @@ -45,9 +45,9 @@ vi.mock("@google/genai", () => { }; }); -import { getModel } from "../src/models.js"; -import { streamGoogleVertex } from "../src/providers/google-vertex.js"; -import type { Context, Model } from "../src/types.js"; +import { getModel } from "../src/models.ts"; +import { streamGoogleVertex } from "../src/providers/google-vertex.ts"; +import type { Context, Model } from "../src/types.ts"; const model = getModel("google-vertex", "gemini-3-flash-preview"); const context: Context = { diff --git a/packages/ai/test/image-tool-result.test.ts b/packages/ai/test/image-tool-result.test.ts index 9d1276d5..542b2385 100644 --- a/packages/ai/test/image-tool-result.test.ts +++ b/packages/ai/test/image-tool-result.test.ts @@ -2,15 +2,15 @@ import { readFileSync } from "node:fs"; import { join } from "node:path"; import { Type } from "typebox"; import { describe, expect, it } from "vitest"; -import type { Api, Context, Model, Tool, ToolResultMessage } from "../src/index.js"; -import { complete, getModel } from "../src/index.js"; -import type { StreamOptions } from "../src/types.js"; +import type { Api, Context, Model, Tool, ToolResultMessage } from "../src/index.ts"; +import { complete, getModel } from "../src/index.ts"; +import type { StreamOptions } from "../src/types.ts"; type StreamOptionsWithExtras = StreamOptions & Record; -import { hasAzureOpenAICredentials, resolveAzureDeploymentName } from "./azure-utils.js"; -import { hasBedrockCredentials } from "./bedrock-utils.js"; -import { resolveApiKey } from "./oauth.js"; +import { hasAzureOpenAICredentials, resolveAzureDeploymentName } from "./azure-utils.ts"; +import { hasBedrockCredentials } from "./bedrock-utils.ts"; +import { resolveApiKey } from "./oauth.ts"; // Resolve OAuth tokens at module level (async, runs before tests) const oauthTokens = await Promise.all([ diff --git a/packages/ai/test/images.test.ts b/packages/ai/test/images.test.ts index 59912c70..c45cc215 100644 --- a/packages/ai/test/images.test.ts +++ b/packages/ai/test/images.test.ts @@ -2,9 +2,9 @@ import { readFileSync } from "node:fs"; import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; import { describe, expect, it } from "vitest"; -import { getImageModel } from "../src/image-models.js"; -import { generateImages } from "../src/images.js"; -import type { ImageContent, ImagesContext, ImagesModel, ProviderImagesOptions } from "../src/types.js"; +import { getImageModel } from "../src/image-models.ts"; +import { generateImages } from "../src/images.ts"; +import type { ImageContent, ImagesContext, ImagesModel, ProviderImagesOptions } from "../src/types.ts"; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); diff --git a/packages/ai/test/interleaved-thinking.test.ts b/packages/ai/test/interleaved-thinking.test.ts index fabc3c40..4cf387ce 100644 --- a/packages/ai/test/interleaved-thinking.test.ts +++ b/packages/ai/test/interleaved-thinking.test.ts @@ -1,11 +1,11 @@ import { Type } from "typebox"; import { describe, expect, it } from "vitest"; -import { getEnvApiKey } from "../src/env-api-keys.js"; -import { getModel } from "../src/models.js"; -import { completeSimple } from "../src/stream.js"; -import type { Api, Context, Model, StopReason, Tool, ToolCall, ToolResultMessage } from "../src/types.js"; -import { StringEnum } from "../src/utils/typebox-helpers.js"; -import { hasBedrockCredentials } from "./bedrock-utils.js"; +import { getEnvApiKey } from "../src/env-api-keys.ts"; +import { getModel } from "../src/models.ts"; +import { completeSimple } from "../src/stream.ts"; +import type { Api, Context, Model, StopReason, Tool, ToolCall, ToolResultMessage } from "../src/types.ts"; +import { StringEnum } from "../src/utils/typebox-helpers.ts"; +import { hasBedrockCredentials } from "./bedrock-utils.ts"; const calculatorSchema = Type.Object({ a: Type.Number({ description: "First number" }), diff --git a/packages/ai/test/lazy-module-load.test.ts b/packages/ai/test/lazy-module-load.test.ts index c94669ab..e21f0d12 100644 --- a/packages/ai/test/lazy-module-load.test.ts +++ b/packages/ai/test/lazy-module-load.test.ts @@ -1,11 +1,8 @@ import { spawnSync } from "node:child_process"; -import { createRequire } from "node:module"; import { dirname, resolve } from "node:path"; import { fileURLToPath } from "node:url"; import { describe, expect, it } from "vitest"; -const require = createRequire(import.meta.url); -const tsxLoader = require.resolve("tsx/esm"); const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), ".."); const aiEntryUrl = new URL("../src/index.ts", import.meta.url).href; @@ -42,7 +39,7 @@ function runProbe(action: string): ProbeResult { console.log(JSON.stringify({ loadedSpecifiers: [...new Set(loaded)] })); `; - const result = spawnSync(process.execPath, ["--import", tsxLoader, "--input-type=module", "--eval", script], { + const result = spawnSync(process.execPath, ["--input-type=module", "--eval", script], { cwd: packageRoot, encoding: "utf8", }); diff --git a/packages/ai/test/mistral-reasoning-mode.test.ts b/packages/ai/test/mistral-reasoning-mode.test.ts index 28ff5208..35a5b96b 100644 --- a/packages/ai/test/mistral-reasoning-mode.test.ts +++ b/packages/ai/test/mistral-reasoning-mode.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it } from "vitest"; -import { getModel } from "../src/models.js"; -import { streamSimple } from "../src/stream.js"; -import type { Context, Model, SimpleStreamOptions } from "../src/types.js"; +import { getModel } from "../src/models.ts"; +import { streamSimple } from "../src/stream.ts"; +import type { Context, Model, SimpleStreamOptions } from "../src/types.ts"; interface MistralPayload { promptMode?: "reasoning"; diff --git a/packages/ai/test/mistral-tool-schema.test.ts b/packages/ai/test/mistral-tool-schema.test.ts index 623e9a3a..c6898fc6 100644 --- a/packages/ai/test/mistral-tool-schema.test.ts +++ b/packages/ai/test/mistral-tool-schema.test.ts @@ -1,8 +1,8 @@ import { Type } from "typebox"; import { describe, expect, it } from "vitest"; -import { getModel } from "../src/models.js"; -import { complete } from "../src/stream.js"; -import type { Context, Model } from "../src/types.js"; +import { getModel } from "../src/models.ts"; +import { complete } from "../src/stream.ts"; +import type { Context, Model } from "../src/types.ts"; interface MistralToolPayload { tools?: Array<{ diff --git a/packages/ai/test/node-http-proxy.test.ts b/packages/ai/test/node-http-proxy.test.ts index 0057d9bd..f4c9b735 100644 --- a/packages/ai/test/node-http-proxy.test.ts +++ b/packages/ai/test/node-http-proxy.test.ts @@ -1,5 +1,5 @@ import { afterEach, describe, expect, it } from "vitest"; -import { resolveHttpProxyUrlForTarget, UNSUPPORTED_PROXY_PROTOCOL_MESSAGE } from "../src/utils/node-http-proxy.js"; +import { resolveHttpProxyUrlForTarget, UNSUPPORTED_PROXY_PROTOCOL_MESSAGE } from "../src/utils/node-http-proxy.ts"; const PROXY_ENV_KEYS = [ "HTTP_PROXY", diff --git a/packages/ai/test/oauth.ts b/packages/ai/test/oauth.ts index 8dd302a7..8ee7c94d 100644 --- a/packages/ai/test/oauth.ts +++ b/packages/ai/test/oauth.ts @@ -8,8 +8,8 @@ import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "fs"; import { homedir } from "os"; import { dirname, join } from "path"; -import { getOAuthApiKey } from "../src/utils/oauth/index.js"; -import type { OAuthCredentials, OAuthProvider } from "../src/utils/oauth/types.js"; +import { getOAuthApiKey } from "../src/utils/oauth/index.ts"; +import type { OAuthCredentials, OAuthProvider } from "../src/utils/oauth/types.ts"; const AUTH_PATH = join(homedir(), ".pi", "agent", "auth.json"); diff --git a/packages/ai/test/openai-codex-cache-affinity-e2e.test.ts b/packages/ai/test/openai-codex-cache-affinity-e2e.test.ts index 0d929724..0abb46f6 100644 --- a/packages/ai/test/openai-codex-cache-affinity-e2e.test.ts +++ b/packages/ai/test/openai-codex-cache-affinity-e2e.test.ts @@ -1,8 +1,8 @@ import { describe, expect, it } from "vitest"; -import { getModel } from "../src/models.js"; -import { complete } from "../src/stream.js"; -import type { Context } from "../src/types.js"; -import { resolveApiKey } from "./oauth.js"; +import { getModel } from "../src/models.ts"; +import { complete } from "../src/stream.ts"; +import type { Context } from "../src/types.ts"; +import { resolveApiKey } from "./oauth.ts"; const codexToken = await resolveApiKey("openai-codex"); diff --git a/packages/ai/test/openai-codex-oauth.test.ts b/packages/ai/test/openai-codex-oauth.test.ts index 276abe1b..0157f6f6 100644 --- a/packages/ai/test/openai-codex-oauth.test.ts +++ b/packages/ai/test/openai-codex-oauth.test.ts @@ -1,5 +1,5 @@ import { afterEach, describe, expect, it, vi } from "vitest"; -import { refreshOpenAICodexToken } from "../src/utils/oauth/openai-codex.js"; +import { refreshOpenAICodexToken } from "../src/utils/oauth/openai-codex.ts"; describe("OpenAI Codex OAuth", () => { afterEach(() => { diff --git a/packages/ai/test/openai-completions-cache-control-format.test.ts b/packages/ai/test/openai-completions-cache-control-format.test.ts index de69d02a..7d8ee9c4 100644 --- a/packages/ai/test/openai-completions-cache-control-format.test.ts +++ b/packages/ai/test/openai-completions-cache-control-format.test.ts @@ -1,8 +1,8 @@ import { Type } from "typebox"; import { beforeEach, describe, expect, it, vi } from "vitest"; -import { getModel } from "../src/models.js"; -import { streamOpenAICompletions } from "../src/providers/openai-completions.js"; -import type { Model } from "../src/types.js"; +import { getModel } from "../src/models.ts"; +import { streamOpenAICompletions } from "../src/providers/openai-completions.ts"; +import type { Model } from "../src/types.ts"; interface CacheControl { type: "ephemeral"; diff --git a/packages/ai/test/openai-completions-empty-tools.test.ts b/packages/ai/test/openai-completions-empty-tools.test.ts index 8e5d49c7..a743351c 100644 --- a/packages/ai/test/openai-completions-empty-tools.test.ts +++ b/packages/ai/test/openai-completions-empty-tools.test.ts @@ -1,6 +1,6 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; -import { getModel } from "../src/models.js"; -import { streamSimple } from "../src/stream.js"; +import { getModel } from "../src/models.ts"; +import { streamSimple } from "../src/stream.ts"; // Empty tools arrays must NOT be serialized as `tools: []` — some OpenAI-compatible // backends (e.g. DashScope / Aliyun Qwen via compatible-mode) reject the request with diff --git a/packages/ai/test/openai-completions-prompt-cache.test.ts b/packages/ai/test/openai-completions-prompt-cache.test.ts index b1846421..75098fc1 100644 --- a/packages/ai/test/openai-completions-prompt-cache.test.ts +++ b/packages/ai/test/openai-completions-prompt-cache.test.ts @@ -1,7 +1,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import { getModel } from "../src/models.js"; -import { streamOpenAICompletions } from "../src/providers/openai-completions.js"; -import type { Model } from "../src/types.js"; +import { getModel } from "../src/models.ts"; +import { streamOpenAICompletions } from "../src/providers/openai-completions.ts"; +import type { Model } from "../src/types.ts"; interface FakeOpenAIClientOptions { apiKey: string; diff --git a/packages/ai/test/openai-completions-response-model.test.ts b/packages/ai/test/openai-completions-response-model.test.ts index 32d4edf7..aab20dea 100644 --- a/packages/ai/test/openai-completions-response-model.test.ts +++ b/packages/ai/test/openai-completions-response-model.test.ts @@ -1,6 +1,6 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; -import { complete } from "../src/stream.js"; -import type { Model } from "../src/types.js"; +import { complete } from "../src/stream.ts"; +import type { Model } from "../src/types.ts"; // Router/virtual ids (e.g. OpenRouter `auto`) keep `model` pinned to the // requested id and surface the routed concrete id on `responseModel`. diff --git a/packages/ai/test/openai-completions-thinking-as-text.test.ts b/packages/ai/test/openai-completions-thinking-as-text.test.ts index a0bb6519..1c49aad3 100644 --- a/packages/ai/test/openai-completions-thinking-as-text.test.ts +++ b/packages/ai/test/openai-completions-thinking-as-text.test.ts @@ -2,7 +2,7 @@ import { once } from "node:events"; import http from "node:http"; import type { AddressInfo } from "node:net"; import { afterEach, describe, expect, it } from "vitest"; -import { convertMessages, streamOpenAICompletions } from "../src/providers/openai-completions.js"; +import { convertMessages, streamOpenAICompletions } from "../src/providers/openai-completions.ts"; import type { AssistantMessage, AssistantMessageEvent, @@ -10,7 +10,7 @@ import type { Model, OpenAICompletionsCompat, Usage, -} from "../src/types.js"; +} from "../src/types.ts"; const emptyUsage: Usage = { input: 0, diff --git a/packages/ai/test/openai-completions-tool-choice.test.ts b/packages/ai/test/openai-completions-tool-choice.test.ts index 5401ac83..ec98427b 100644 --- a/packages/ai/test/openai-completions-tool-choice.test.ts +++ b/packages/ai/test/openai-completions-tool-choice.test.ts @@ -1,9 +1,9 @@ import { Type } from "typebox"; import { beforeEach, describe, expect, it, vi } from "vitest"; -import { getModel } from "../src/models.js"; -import { convertMessages } from "../src/providers/openai-completions.js"; -import { streamSimple } from "../src/stream.js"; -import type { AssistantMessage, Model, Tool, ToolResultMessage } from "../src/types.js"; +import { getModel } from "../src/models.ts"; +import { convertMessages } from "../src/providers/openai-completions.ts"; +import { streamSimple } from "../src/stream.ts"; +import type { AssistantMessage, Model, Tool, ToolResultMessage } from "../src/types.ts"; const mockState = vi.hoisted(() => ({ lastParams: undefined as unknown, diff --git a/packages/ai/test/openai-completions-tool-result-images.test.ts b/packages/ai/test/openai-completions-tool-result-images.test.ts index cd4d6436..3510f396 100644 --- a/packages/ai/test/openai-completions-tool-result-images.test.ts +++ b/packages/ai/test/openai-completions-tool-result-images.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest"; -import { getModel } from "../src/models.js"; -import { convertMessages } from "../src/providers/openai-completions.js"; +import { getModel } from "../src/models.ts"; +import { convertMessages } from "../src/providers/openai-completions.ts"; import type { AssistantMessage, Context, @@ -8,7 +8,7 @@ import type { OpenAICompletionsCompat, ToolResultMessage, Usage, -} from "../src/types.js"; +} from "../src/types.ts"; const emptyUsage: Usage = { input: 0, diff --git a/packages/ai/test/openai-responses-cache-affinity-e2e.test.ts b/packages/ai/test/openai-responses-cache-affinity-e2e.test.ts index a2fd0d0e..d173694f 100644 --- a/packages/ai/test/openai-responses-cache-affinity-e2e.test.ts +++ b/packages/ai/test/openai-responses-cache-affinity-e2e.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it } from "vitest"; -import { getModel } from "../src/models.js"; -import { complete } from "../src/stream.js"; -import type { Context } from "../src/types.js"; +import { getModel } from "../src/models.ts"; +import { complete } from "../src/stream.ts"; +import type { Context } from "../src/types.ts"; describe.skipIf(!process.env.OPENAI_API_KEY)("openai responses cache affinity e2e", () => { it("handles direct OpenAI Responses requests with aligned cache-affinity identifiers", { retry: 2 }, async () => { diff --git a/packages/ai/test/openai-responses-copilot-provider.test.ts b/packages/ai/test/openai-responses-copilot-provider.test.ts index c956030d..04236fed 100644 --- a/packages/ai/test/openai-responses-copilot-provider.test.ts +++ b/packages/ai/test/openai-responses-copilot-provider.test.ts @@ -1,7 +1,7 @@ import { afterEach, describe, expect, it, vi } from "vitest"; -import { getModel } from "../src/models.js"; -import { streamOpenAIResponses } from "../src/providers/openai-responses.js"; -import type { Model } from "../src/types.js"; +import { getModel } from "../src/models.ts"; +import { streamOpenAIResponses } from "../src/providers/openai-responses.ts"; +import type { Model } from "../src/types.ts"; type CapturedHeaders = Headers | string[][] | Record | undefined; diff --git a/packages/ai/test/openai-responses-foreign-toolcall-id.test.ts b/packages/ai/test/openai-responses-foreign-toolcall-id.test.ts index 52de262b..10f06377 100644 --- a/packages/ai/test/openai-responses-foreign-toolcall-id.test.ts +++ b/packages/ai/test/openai-responses-foreign-toolcall-id.test.ts @@ -1,8 +1,8 @@ import { describe, expect, it } from "vitest"; -import { getModel } from "../src/models.js"; -import { convertResponsesMessages } from "../src/providers/openai-responses-shared.js"; -import type { AssistantMessage, Context, ToolResultMessage, Usage } from "../src/types.js"; -import { shortHash } from "../src/utils/hash.js"; +import { getModel } from "../src/models.ts"; +import { convertResponsesMessages } from "../src/providers/openai-responses-shared.ts"; +import type { AssistantMessage, Context, ToolResultMessage, Usage } from "../src/types.ts"; +import { shortHash } from "../src/utils/hash.ts"; const COPILOT_RAW_TOOL_CALL_ID = "call_4VnzVawQXPB9MgYib7CiQFEY|I9b95oN1wD/cHXKTw3PpRkL6KkCtzTJhUxMouMWYwHeTo2j3htzfSk7YPx2vifiIM4g3A8XXyOj8q4Bt6SLUG7gqY1E3ELkrkVQNHglRfUmWj84lqxJY+Puieb3VKyX0FB+83TUzn91cDMF/4gzt990IzqVrc+nIb9RRscRD070Du16q1glydVjWR0SBJsE6TbY/esOjFpqplogQqrajm1eI++f3eLi73R6q7hVusY0QbeFySVxABCjhN0lXB04caBe1rzHjYzul6MAXj7uq+0r17VLq+yrtyYhN12wkmFqHeqTyEei6EFPbMy24Nc+IbJlkP0OCg02W+gOnyBFcbi2ctvJFSOhSjt1CqBdqCnnhwUqXjbWiT0wh3DmLScRgTHmGkaI+oAcQQjfic65nxj+TnEkReA=="; diff --git a/packages/ai/test/openai-responses-partial-json-cleanup.test.ts b/packages/ai/test/openai-responses-partial-json-cleanup.test.ts index c34350de..76b16ad3 100644 --- a/packages/ai/test/openai-responses-partial-json-cleanup.test.ts +++ b/packages/ai/test/openai-responses-partial-json-cleanup.test.ts @@ -1,8 +1,8 @@ import type { ResponseStreamEvent } from "openai/resources/responses/responses.js"; import { describe, expect, it, vi } from "vitest"; -import { processResponsesStream } from "../src/providers/openai-responses-shared.js"; -import type { AssistantMessage, AssistantMessageEvent, Model } from "../src/types.js"; -import { AssistantMessageEventStream } from "../src/utils/event-stream.js"; +import { processResponsesStream } from "../src/providers/openai-responses-shared.ts"; +import type { AssistantMessage, AssistantMessageEvent, Model } from "../src/types.ts"; +import { AssistantMessageEventStream } from "../src/utils/event-stream.ts"; function createOutput(model: Model<"openai-responses">): AssistantMessage { return { diff --git a/packages/ai/test/openai-responses-reasoning-replay-e2e.test.ts b/packages/ai/test/openai-responses-reasoning-replay-e2e.test.ts index 2f78ddc7..aa6f2e23 100644 --- a/packages/ai/test/openai-responses-reasoning-replay-e2e.test.ts +++ b/packages/ai/test/openai-responses-reasoning-replay-e2e.test.ts @@ -1,8 +1,8 @@ import { Type } from "typebox"; import { describe, expect, it } from "vitest"; -import { getModel } from "../src/models.js"; -import { complete, getEnvApiKey } from "../src/stream.js"; -import type { AssistantMessage, Context, Message, Tool, ToolCall } from "../src/types.js"; +import { getModel } from "../src/models.ts"; +import { complete, getEnvApiKey } from "../src/stream.ts"; +import type { AssistantMessage, Context, Message, Tool, ToolCall } from "../src/types.ts"; const testToolSchema = Type.Object({ value: Type.Number({ description: "A number to double" }), diff --git a/packages/ai/test/openai-responses-tool-result-images.test.ts b/packages/ai/test/openai-responses-tool-result-images.test.ts index 48ab8d08..c6131d91 100644 --- a/packages/ai/test/openai-responses-tool-result-images.test.ts +++ b/packages/ai/test/openai-responses-tool-result-images.test.ts @@ -4,10 +4,10 @@ import { fileURLToPath } from "node:url"; import type { ResponseFunctionCallOutputItemList } from "openai/resources/responses/responses.js"; import { Type } from "typebox"; import { describe, expect, it } from "vitest"; -import type { Api, Context, Model, StreamOptions, Tool, ToolResultMessage } from "../src/index.js"; -import { complete, getModel } from "../src/index.js"; -import { hasAzureOpenAICredentials, resolveAzureDeploymentName } from "./azure-utils.js"; -import { resolveApiKey } from "./oauth.js"; +import type { Api, Context, Model, StreamOptions, Tool, ToolResultMessage } from "../src/index.ts"; +import { complete, getModel } from "../src/index.ts"; +import { hasAzureOpenAICredentials, resolveAzureDeploymentName } from "./azure-utils.ts"; +import { resolveApiKey } from "./oauth.ts"; type StreamOptionsWithExtras = StreamOptions & Record; diff --git a/packages/ai/test/openrouter-cache-write-repro.test.ts b/packages/ai/test/openrouter-cache-write-repro.test.ts index ca492967..4bdeb286 100644 --- a/packages/ai/test/openrouter-cache-write-repro.test.ts +++ b/packages/ai/test/openrouter-cache-write-repro.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest"; -import { getModel } from "../src/models.js"; -import { completeSimple } from "../src/stream.js"; +import { getModel } from "../src/models.ts"; +import { completeSimple } from "../src/stream.ts"; function createLongSystemPrompt(): string { const nonce = `${Date.now()}-${Math.random()}`; diff --git a/packages/ai/test/openrouter-images.test.ts b/packages/ai/test/openrouter-images.test.ts index de2ddbba..31882bec 100644 --- a/packages/ai/test/openrouter-images.test.ts +++ b/packages/ai/test/openrouter-images.test.ts @@ -1,6 +1,6 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; -import { generateImages } from "../src/images.js"; -import type { ImagesContext, ImagesModel } from "../src/types.js"; +import { generateImages } from "../src/images.ts"; +import type { ImagesContext, ImagesModel } from "../src/types.ts"; const mockState = vi.hoisted(() => ({ lastParams: undefined as unknown, diff --git a/packages/ai/test/overflow.test.ts b/packages/ai/test/overflow.test.ts index ca6fde24..b26948e2 100644 --- a/packages/ai/test/overflow.test.ts +++ b/packages/ai/test/overflow.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest"; -import type { AssistantMessage } from "../src/types.js"; -import { isContextOverflow } from "../src/utils/overflow.js"; +import type { AssistantMessage } from "../src/types.ts"; +import { isContextOverflow } from "../src/utils/overflow.ts"; function createErrorMessage(errorMessage: string): AssistantMessage { return { diff --git a/packages/ai/test/responseid.test.ts b/packages/ai/test/responseid.test.ts index 61df764b..d250f5f4 100644 --- a/packages/ai/test/responseid.test.ts +++ b/packages/ai/test/responseid.test.ts @@ -1,9 +1,9 @@ import { describe, expect, it } from "vitest"; -import { getModel } from "../src/models.js"; -import { complete } from "../src/stream.js"; -import type { Api, Context, Model, StreamOptions } from "../src/types.js"; -import { hasAzureOpenAICredentials, resolveAzureDeploymentName } from "./azure-utils.js"; -import { resolveApiKey } from "./oauth.js"; +import { getModel } from "../src/models.ts"; +import { complete } from "../src/stream.ts"; +import type { Api, Context, Model, StreamOptions } from "../src/types.ts"; +import { hasAzureOpenAICredentials, resolveAzureDeploymentName } from "./azure-utils.ts"; +import { resolveApiKey } from "./oauth.ts"; type StreamOptionsWithExtras = StreamOptions & Record; diff --git a/packages/ai/test/stream.test.ts b/packages/ai/test/stream.test.ts index 21199d38..298104dd 100644 --- a/packages/ai/test/stream.test.ts +++ b/packages/ai/test/stream.test.ts @@ -4,17 +4,17 @@ import { dirname, join } from "path"; import { Type } from "typebox"; import { fileURLToPath } from "url"; import { afterAll, beforeAll, describe, expect, it } from "vitest"; -import { getModel } from "../src/models.js"; -import { complete, stream } from "../src/stream.js"; -import type { Api, Context, ImageContent, Model, StreamOptions, Tool, ToolResultMessage } from "../src/types.js"; +import { getModel } from "../src/models.ts"; +import { complete, stream } from "../src/stream.ts"; +import type { Api, Context, ImageContent, Model, StreamOptions, Tool, ToolResultMessage } from "../src/types.ts"; type StreamOptionsWithExtras = StreamOptions & Record; -import { StringEnum } from "../src/utils/typebox-helpers.js"; -import { hasAzureOpenAICredentials, resolveAzureDeploymentName } from "./azure-utils.js"; -import { hasBedrockCredentials } from "./bedrock-utils.js"; -import { hasCloudflareAiGatewayCredentials, hasCloudflareWorkersAICredentials } from "./cloudflare-utils.js"; -import { resolveApiKey } from "./oauth.js"; +import { StringEnum } from "../src/utils/typebox-helpers.ts"; +import { hasAzureOpenAICredentials, resolveAzureDeploymentName } from "./azure-utils.ts"; +import { hasBedrockCredentials } from "./bedrock-utils.ts"; +import { hasCloudflareAiGatewayCredentials, hasCloudflareWorkersAICredentials } from "./cloudflare-utils.ts"; +import { resolveApiKey } from "./oauth.ts"; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); diff --git a/packages/ai/test/supports-xhigh.test.ts b/packages/ai/test/supports-xhigh.test.ts index 6a66ee94..4fb6f3b2 100644 --- a/packages/ai/test/supports-xhigh.test.ts +++ b/packages/ai/test/supports-xhigh.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "vitest"; -import { getModel, getSupportedThinkingLevels } from "../src/models.js"; +import { getModel, getSupportedThinkingLevels } from "../src/models.ts"; describe("getSupportedThinkingLevels", () => { it("includes xhigh for Anthropic Opus 4.6 on anthropic-messages API", () => { diff --git a/packages/ai/test/together-models.test.ts b/packages/ai/test/together-models.test.ts index 14031447..cb5ea943 100644 --- a/packages/ai/test/together-models.test.ts +++ b/packages/ai/test/together-models.test.ts @@ -1,6 +1,6 @@ import { afterEach, describe, expect, it } from "vitest"; -import { findEnvKeys, getEnvApiKey } from "../src/env-api-keys.js"; -import { getModel } from "../src/models.js"; +import { findEnvKeys, getEnvApiKey } from "../src/env-api-keys.ts"; +import { getModel } from "../src/models.ts"; const originalTogetherApiKey = process.env.TOGETHER_API_KEY; diff --git a/packages/ai/test/tokens.test.ts b/packages/ai/test/tokens.test.ts index 86a70cd2..f8da26a8 100644 --- a/packages/ai/test/tokens.test.ts +++ b/packages/ai/test/tokens.test.ts @@ -1,14 +1,14 @@ import { describe, expect, it } from "vitest"; -import { getModel } from "../src/models.js"; -import { stream } from "../src/stream.js"; -import type { Api, Context, Model, StreamOptions } from "../src/types.js"; +import { getModel } from "../src/models.ts"; +import { stream } from "../src/stream.ts"; +import type { Api, Context, Model, StreamOptions } from "../src/types.ts"; type StreamOptionsWithExtras = StreamOptions & Record; -import { hasAzureOpenAICredentials, resolveAzureDeploymentName } from "./azure-utils.js"; -import { hasBedrockCredentials } from "./bedrock-utils.js"; -import { hasCloudflareAiGatewayCredentials, hasCloudflareWorkersAICredentials } from "./cloudflare-utils.js"; -import { resolveApiKey } from "./oauth.js"; +import { hasAzureOpenAICredentials, resolveAzureDeploymentName } from "./azure-utils.ts"; +import { hasBedrockCredentials } from "./bedrock-utils.ts"; +import { hasCloudflareAiGatewayCredentials, hasCloudflareWorkersAICredentials } from "./cloudflare-utils.ts"; +import { resolveApiKey } from "./oauth.ts"; // Resolve OAuth tokens at module level (async, runs before tests) const oauthTokens = await Promise.all([ diff --git a/packages/ai/test/tool-call-id-normalization.test.ts b/packages/ai/test/tool-call-id-normalization.test.ts index 33ea327f..0672181b 100644 --- a/packages/ai/test/tool-call-id-normalization.test.ts +++ b/packages/ai/test/tool-call-id-normalization.test.ts @@ -12,10 +12,10 @@ import { Type } from "typebox"; import { describe, expect, it } from "vitest"; -import { getModel } from "../src/models.js"; -import { completeSimple, getEnvApiKey } from "../src/stream.js"; -import type { AssistantMessage, Message, Tool, ToolResultMessage } from "../src/types.js"; -import { resolveApiKey } from "./oauth.js"; +import { getModel } from "../src/models.ts"; +import { completeSimple, getEnvApiKey } from "../src/stream.ts"; +import type { AssistantMessage, Message, Tool, ToolResultMessage } from "../src/types.ts"; +import { resolveApiKey } from "./oauth.ts"; // Resolve API keys const copilotToken = await resolveApiKey("github-copilot"); diff --git a/packages/ai/test/tool-call-without-result.test.ts b/packages/ai/test/tool-call-without-result.test.ts index fdab9394..2b1950b9 100644 --- a/packages/ai/test/tool-call-without-result.test.ts +++ b/packages/ai/test/tool-call-without-result.test.ts @@ -1,15 +1,15 @@ import { Type } from "typebox"; import { describe, expect, it } from "vitest"; -import { getModel } from "../src/models.js"; -import { complete } from "../src/stream.js"; -import type { Api, Context, Model, StreamOptions, Tool } from "../src/types.js"; +import { getModel } from "../src/models.ts"; +import { complete } from "../src/stream.ts"; +import type { Api, Context, Model, StreamOptions, Tool } from "../src/types.ts"; type StreamOptionsWithExtras = StreamOptions & Record; -import { hasAzureOpenAICredentials, resolveAzureDeploymentName } from "./azure-utils.js"; -import { hasBedrockCredentials } from "./bedrock-utils.js"; -import { hasCloudflareAiGatewayCredentials, hasCloudflareWorkersAICredentials } from "./cloudflare-utils.js"; -import { resolveApiKey } from "./oauth.js"; +import { hasAzureOpenAICredentials, resolveAzureDeploymentName } from "./azure-utils.ts"; +import { hasBedrockCredentials } from "./bedrock-utils.ts"; +import { hasCloudflareAiGatewayCredentials, hasCloudflareWorkersAICredentials } from "./cloudflare-utils.ts"; +import { resolveApiKey } from "./oauth.ts"; // Resolve OAuth tokens at module level (async, runs before tests) const oauthTokens = await Promise.all([ diff --git a/packages/ai/test/total-tokens.test.ts b/packages/ai/test/total-tokens.test.ts index 47303da4..bd782c2c 100644 --- a/packages/ai/test/total-tokens.test.ts +++ b/packages/ai/test/total-tokens.test.ts @@ -13,16 +13,16 @@ */ import { describe, expect, it } from "vitest"; -import { getModel } from "../src/models.js"; -import { complete } from "../src/stream.js"; -import type { Api, Context, Model, StreamOptions, Usage } from "../src/types.js"; +import { getModel } from "../src/models.ts"; +import { complete } from "../src/stream.ts"; +import type { Api, Context, Model, StreamOptions, Usage } from "../src/types.ts"; type StreamOptionsWithExtras = StreamOptions & Record; -import { hasAzureOpenAICredentials, resolveAzureDeploymentName } from "./azure-utils.js"; -import { hasBedrockCredentials } from "./bedrock-utils.js"; -import { hasCloudflareAiGatewayCredentials, hasCloudflareWorkersAICredentials } from "./cloudflare-utils.js"; -import { resolveApiKey } from "./oauth.js"; +import { hasAzureOpenAICredentials, resolveAzureDeploymentName } from "./azure-utils.ts"; +import { hasBedrockCredentials } from "./bedrock-utils.ts"; +import { hasCloudflareAiGatewayCredentials, hasCloudflareWorkersAICredentials } from "./cloudflare-utils.ts"; +import { resolveApiKey } from "./oauth.ts"; // Resolve OAuth tokens at module level (async, runs before tests) const oauthTokens = await Promise.all([ diff --git a/packages/ai/test/transform-messages-copilot-openai-to-anthropic.test.ts b/packages/ai/test/transform-messages-copilot-openai-to-anthropic.test.ts index 1db3bad6..4d858835 100644 --- a/packages/ai/test/transform-messages-copilot-openai-to-anthropic.test.ts +++ b/packages/ai/test/transform-messages-copilot-openai-to-anthropic.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest"; -import { transformMessages } from "../src/providers/transform-messages.js"; -import type { AssistantMessage, Message, Model, ToolCall } from "../src/types.js"; +import { transformMessages } from "../src/providers/transform-messages.ts"; +import type { AssistantMessage, Message, Model, ToolCall } from "../src/types.ts"; // Normalize function matching what anthropic.ts uses function anthropicNormalizeToolCallId( diff --git a/packages/ai/test/unicode-surrogate.test.ts b/packages/ai/test/unicode-surrogate.test.ts index 568de174..afe48d5c 100644 --- a/packages/ai/test/unicode-surrogate.test.ts +++ b/packages/ai/test/unicode-surrogate.test.ts @@ -1,15 +1,15 @@ import { Type } from "typebox"; import { describe, expect, it } from "vitest"; -import { getModel } from "../src/models.js"; -import { complete } from "../src/stream.js"; -import type { Api, Context, Model, StreamOptions, ToolResultMessage } from "../src/types.js"; +import { getModel } from "../src/models.ts"; +import { complete } from "../src/stream.ts"; +import type { Api, Context, Model, StreamOptions, ToolResultMessage } from "../src/types.ts"; type StreamOptionsWithExtras = StreamOptions & Record; -import { hasAzureOpenAICredentials, resolveAzureDeploymentName } from "./azure-utils.js"; -import { hasBedrockCredentials } from "./bedrock-utils.js"; -import { hasCloudflareAiGatewayCredentials, hasCloudflareWorkersAICredentials } from "./cloudflare-utils.js"; -import { resolveApiKey } from "./oauth.js"; +import { hasAzureOpenAICredentials, resolveAzureDeploymentName } from "./azure-utils.ts"; +import { hasBedrockCredentials } from "./bedrock-utils.ts"; +import { hasCloudflareAiGatewayCredentials, hasCloudflareWorkersAICredentials } from "./cloudflare-utils.ts"; +import { resolveApiKey } from "./oauth.ts"; // Empty schema for test tools - must be proper OBJECT type for Cloud Code Assist const emptySchema = Type.Object({}); diff --git a/packages/ai/test/validation.test.ts b/packages/ai/test/validation.test.ts index fd9d0164..dcbe9ebe 100644 --- a/packages/ai/test/validation.test.ts +++ b/packages/ai/test/validation.test.ts @@ -1,7 +1,7 @@ import { Type } from "typebox"; import { describe, expect, it } from "vitest"; -import type { Tool, ToolCall } from "../src/types.js"; -import { validateToolArguments } from "../src/utils/validation.js"; +import type { Tool, ToolCall } from "../src/types.ts"; +import { validateToolArguments } from "../src/utils/validation.ts"; function createToolCallWithPlainSchema( schema: Tool["parameters"], diff --git a/packages/ai/test/xhigh.test.ts b/packages/ai/test/xhigh.test.ts index ef487369..3c279863 100644 --- a/packages/ai/test/xhigh.test.ts +++ b/packages/ai/test/xhigh.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it } from "vitest"; -import { getModel } from "../src/models.js"; -import { stream } from "../src/stream.js"; -import type { Context, Model } from "../src/types.js"; +import { getModel } from "../src/models.ts"; +import { stream } from "../src/stream.ts"; +import type { Context, Model } from "../src/types.ts"; function makeContext(): Context { return { diff --git a/packages/ai/test/zen.test.ts b/packages/ai/test/zen.test.ts index 567af861..8ca80014 100644 --- a/packages/ai/test/zen.test.ts +++ b/packages/ai/test/zen.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it } from "vitest"; -import { MODELS } from "../src/models.generated.js"; -import { complete } from "../src/stream.js"; -import type { Model } from "../src/types.js"; +import { MODELS } from "../src/models.generated.ts"; +import { complete } from "../src/stream.ts"; +import type { Model } from "../src/types.ts"; describe.skipIf(!process.env.OPENCODE_API_KEY)("OpenCode Models Smoke Test", () => { const providers = [ diff --git a/packages/coding-agent/examples/extensions/custom-provider-anthropic/package.json b/packages/coding-agent/examples/extensions/custom-provider-anthropic/package.json index d8e358c5..a2f796cf 100644 --- a/packages/coding-agent/examples/extensions/custom-provider-anthropic/package.json +++ b/packages/coding-agent/examples/extensions/custom-provider-anthropic/package.json @@ -14,6 +14,6 @@ ] }, "dependencies": { - "@anthropic-ai/sdk": "^0.52.0" + "@anthropic-ai/sdk": "0.52.0" } } diff --git a/packages/coding-agent/examples/extensions/custom-provider-gitlab-duo/test.ts b/packages/coding-agent/examples/extensions/custom-provider-gitlab-duo/test.ts index c7963732..0077aca8 100644 --- a/packages/coding-agent/examples/extensions/custom-provider-gitlab-duo/test.ts +++ b/packages/coding-agent/examples/extensions/custom-provider-gitlab-duo/test.ts @@ -12,7 +12,7 @@ import { type Api, type Context, type Model, registerApiProvider, streamSimple } import { readFileSync } from "fs"; import { getAgentDir } from "packages/coding-agent/src/config.js"; import { join } from "path"; -import { MODELS, streamGitLabDuo } from "./index.js"; +import { MODELS, streamGitLabDuo } from "./index.ts"; const MODEL_MAP = new Map(MODELS.map((m) => [m.id, m])); diff --git a/packages/coding-agent/examples/extensions/doom-overlay/doom-component.ts b/packages/coding-agent/examples/extensions/doom-overlay/doom-component.ts index dbc795b9..ddc2e98f 100644 --- a/packages/coding-agent/examples/extensions/doom-overlay/doom-component.ts +++ b/packages/coding-agent/examples/extensions/doom-overlay/doom-component.ts @@ -7,8 +7,8 @@ import type { Component } from "@earendil-works/pi-tui"; import { isKeyRelease, type TUI } from "@earendil-works/pi-tui"; -import type { DoomEngine } from "./doom-engine.js"; -import { DoomKeys, mapKeyToDoom } from "./doom-keys.js"; +import type { DoomEngine } from "./doom-engine.ts"; +import { DoomKeys, mapKeyToDoom } from "./doom-keys.ts"; function renderHalfBlock( rgba: Uint8Array, diff --git a/packages/coding-agent/examples/extensions/doom-overlay/index.ts b/packages/coding-agent/examples/extensions/doom-overlay/index.ts index 434f0be1..9d9dad13 100644 --- a/packages/coding-agent/examples/extensions/doom-overlay/index.ts +++ b/packages/coding-agent/examples/extensions/doom-overlay/index.ts @@ -10,9 +10,9 @@ */ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"; -import { DoomOverlayComponent } from "./doom-component.js"; -import { DoomEngine } from "./doom-engine.js"; -import { ensureWadFile } from "./wad-finder.js"; +import { DoomOverlayComponent } from "./doom-component.ts"; +import { DoomEngine } from "./doom-engine.ts"; +import { ensureWadFile } from "./wad-finder.ts"; // Persistent engine instance - survives between invocations let activeEngine: DoomEngine | null = null; diff --git a/packages/coding-agent/examples/extensions/plan-mode/index.ts b/packages/coding-agent/examples/extensions/plan-mode/index.ts index 709741d1..40db408c 100644 --- a/packages/coding-agent/examples/extensions/plan-mode/index.ts +++ b/packages/coding-agent/examples/extensions/plan-mode/index.ts @@ -16,7 +16,7 @@ import type { AgentMessage } from "@earendil-works/pi-agent-core"; import type { AssistantMessage, TextContent } from "@earendil-works/pi-ai"; import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent"; import { Key } from "@earendil-works/pi-tui"; -import { extractTodoItems, isSafeCommand, markCompletedSteps, type TodoItem } from "./utils.js"; +import { extractTodoItems, isSafeCommand, markCompletedSteps, type TodoItem } from "./utils.ts"; // Tools const PLAN_MODE_TOOLS = ["read", "bash", "grep", "find", "ls", "questionnaire"]; diff --git a/packages/coding-agent/examples/extensions/sandbox/package.json b/packages/coding-agent/examples/extensions/sandbox/package.json index 785ca407..1fd94b35 100644 --- a/packages/coding-agent/examples/extensions/sandbox/package.json +++ b/packages/coding-agent/examples/extensions/sandbox/package.json @@ -14,6 +14,6 @@ ] }, "dependencies": { - "@anthropic-ai/sandbox-runtime": "^0.0.26" + "@anthropic-ai/sandbox-runtime": "0.0.26" } } diff --git a/packages/coding-agent/examples/extensions/subagent/index.ts b/packages/coding-agent/examples/extensions/subagent/index.ts index 2853ad78..92b471a3 100644 --- a/packages/coding-agent/examples/extensions/subagent/index.ts +++ b/packages/coding-agent/examples/extensions/subagent/index.ts @@ -22,7 +22,7 @@ import { StringEnum } from "@earendil-works/pi-ai"; import { type ExtensionAPI, getMarkdownTheme, withFileMutationQueue } from "@earendil-works/pi-coding-agent"; import { Container, Markdown, Spacer, Text } from "@earendil-works/pi-tui"; import { Type } from "typebox"; -import { type AgentConfig, type AgentScope, discoverAgents } from "./agents.js"; +import { type AgentConfig, type AgentScope, discoverAgents } from "./agents.ts"; const MAX_PARALLEL_TASKS = 8; const MAX_CONCURRENCY = 4; diff --git a/packages/coding-agent/examples/extensions/with-deps/package.json b/packages/coding-agent/examples/extensions/with-deps/package.json index eb66a8a9..2d7cc728 100644 --- a/packages/coding-agent/examples/extensions/with-deps/package.json +++ b/packages/coding-agent/examples/extensions/with-deps/package.json @@ -14,9 +14,9 @@ ] }, "dependencies": { - "ms": "^2.1.3" + "ms": "2.1.3" }, "devDependencies": { - "@types/ms": "^2.1.0" + "@types/ms": "2.1.0" } } diff --git a/packages/coding-agent/package.json b/packages/coding-agent/package.json index 82b5ec76..cd83c380 100644 --- a/packages/coding-agent/package.json +++ b/packages/coding-agent/package.json @@ -40,20 +40,20 @@ "@earendil-works/pi-agent-core": "^0.75.3", "@earendil-works/pi-ai": "^0.75.3", "@earendil-works/pi-tui": "^0.75.3", - "@silvia-odwyer/photon-node": "^0.3.4", - "chalk": "^5.5.0", - "cross-spawn": "^7.0.6", - "diff": "^8.0.2", - "glob": "^13.0.1", - "highlight.js": "^10.7.3", - "hosted-git-info": "^9.0.2", - "ignore": "^7.0.5", - "jiti": "^2.7.0", - "minimatch": "^10.2.3", - "proper-lockfile": "^4.1.2", - "typebox": "^1.1.24", - "undici": "^8.3.0", - "yaml": "^2.8.2" + "@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" }, "overrides": { "rimraf": "6.1.2", @@ -62,18 +62,18 @@ } }, "optionalDependencies": { - "@mariozechner/clipboard": "^0.3.6" + "@mariozechner/clipboard": "0.3.6" }, "devDependencies": { - "@types/cross-spawn": "^6.0.6", - "@types/diff": "^7.0.2", - "@types/hosted-git-info": "^3.0.5", - "@types/ms": "^2.1.0", - "@types/node": "^24.3.0", - "@types/proper-lockfile": "^4.1.4", - "shx": "^0.4.0", - "typescript": "^5.7.3", - "vitest": "^3.2.4" + "@types/cross-spawn": "6.0.6", + "@types/diff": "7.0.2", + "@types/hosted-git-info": "3.0.5", + "@types/ms": "2.1.0", + "@types/node": "24.12.4", + "@types/proper-lockfile": "4.1.4", + "shx": "0.4.0", + "typescript": "5.9.3", + "vitest": "3.2.4" }, "keywords": [ "coding-agent", diff --git a/packages/coding-agent/test/agent-session-auto-compaction-queue.test.ts b/packages/coding-agent/test/agent-session-auto-compaction-queue.test.ts index 1dd01c9b..1abe3e28 100644 --- a/packages/coding-agent/test/agent-session-auto-compaction-queue.test.ts +++ b/packages/coding-agent/test/agent-session-auto-compaction-queue.test.ts @@ -4,12 +4,12 @@ import { join } from "node:path"; import { Agent } from "@earendil-works/pi-agent-core"; import { type AssistantMessage, getModel } from "@earendil-works/pi-ai"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import { AgentSession } from "../src/core/agent-session.js"; -import { AuthStorage } from "../src/core/auth-storage.js"; -import { ModelRegistry } from "../src/core/model-registry.js"; -import { SessionManager } from "../src/core/session-manager.js"; -import { SettingsManager } from "../src/core/settings-manager.js"; -import { createTestResourceLoader } from "./utilities.js"; +import { AgentSession } from "../src/core/agent-session.ts"; +import { AuthStorage } from "../src/core/auth-storage.ts"; +import { ModelRegistry } from "../src/core/model-registry.ts"; +import { SessionManager } from "../src/core/session-manager.ts"; +import { SettingsManager } from "../src/core/settings-manager.ts"; +import { createTestResourceLoader } from "./utilities.ts"; vi.mock("../src/core/compaction/index.js", () => ({ calculateContextTokens: (usage: { diff --git a/packages/coding-agent/test/agent-session-branching.test.ts b/packages/coding-agent/test/agent-session-branching.test.ts index 8590ad74..f516e6d6 100644 --- a/packages/coding-agent/test/agent-session-branching.test.ts +++ b/packages/coding-agent/test/agent-session-branching.test.ts @@ -12,17 +12,17 @@ import { tmpdir } from "node:os"; import { join } from "node:path"; import { getModel } from "@earendil-works/pi-ai"; import { afterEach, beforeEach, describe, expect, it } from "vitest"; -import type { AgentSession } from "../src/core/agent-session.js"; +import type { AgentSession } from "../src/core/agent-session.ts"; import { type AgentSessionRuntime, type CreateAgentSessionRuntimeFactory, createAgentSessionFromServices, createAgentSessionRuntime, createAgentSessionServices, -} from "../src/core/agent-session-runtime.js"; -import { AuthStorage } from "../src/core/auth-storage.js"; -import { SessionManager } from "../src/core/session-manager.js"; -import { API_KEY } from "./utilities.js"; +} from "../src/core/agent-session-runtime.ts"; +import { AuthStorage } from "../src/core/auth-storage.ts"; +import { SessionManager } from "../src/core/session-manager.ts"; +import { API_KEY } from "./utilities.ts"; describe.skipIf(!API_KEY)("AgentSession forking", () => { let session: AgentSession; diff --git a/packages/coding-agent/test/agent-session-compaction.test.ts b/packages/coding-agent/test/agent-session-compaction.test.ts index da52aeca..5724d684 100644 --- a/packages/coding-agent/test/agent-session-compaction.test.ts +++ b/packages/coding-agent/test/agent-session-compaction.test.ts @@ -13,13 +13,13 @@ import { join } from "node:path"; import { Agent } from "@earendil-works/pi-agent-core"; import { getModel } from "@earendil-works/pi-ai"; import { afterEach, beforeEach, describe, expect, it } from "vitest"; -import { AgentSession, type AgentSessionEvent } from "../src/core/agent-session.js"; -import { AuthStorage } from "../src/core/auth-storage.js"; -import { ModelRegistry } from "../src/core/model-registry.js"; -import { SessionManager } from "../src/core/session-manager.js"; -import { SettingsManager } from "../src/core/settings-manager.js"; -import { createCodingTools } from "../src/index.js"; -import { API_KEY, createTestResourceLoader } from "./utilities.js"; +import { AgentSession, type AgentSessionEvent } from "../src/core/agent-session.ts"; +import { AuthStorage } from "../src/core/auth-storage.ts"; +import { ModelRegistry } from "../src/core/model-registry.ts"; +import { SessionManager } from "../src/core/session-manager.ts"; +import { SettingsManager } from "../src/core/settings-manager.ts"; +import { createCodingTools } from "../src/index.ts"; +import { API_KEY, createTestResourceLoader } from "./utilities.ts"; describe.skipIf(!API_KEY)("AgentSession compaction e2e", () => { let session: AgentSession; diff --git a/packages/coding-agent/test/agent-session-concurrent.test.ts b/packages/coding-agent/test/agent-session-concurrent.test.ts index 876883f2..81f400ea 100644 --- a/packages/coding-agent/test/agent-session-concurrent.test.ts +++ b/packages/coding-agent/test/agent-session-concurrent.test.ts @@ -16,13 +16,13 @@ import { } from "@earendil-works/pi-ai"; import { Type } from "typebox"; import { afterEach, beforeEach, describe, expect, it } from "vitest"; -import { AgentSession } from "../src/core/agent-session.js"; -import { AuthStorage } from "../src/core/auth-storage.js"; -import { ModelRegistry } from "../src/core/model-registry.js"; -import { SessionManager } from "../src/core/session-manager.js"; -import { SettingsManager } from "../src/core/settings-manager.js"; -import type { BuildSystemPromptOptions } from "../src/core/system-prompt.js"; -import { createTestExtensionsResult, createTestResourceLoader } from "./utilities.js"; +import { AgentSession } from "../src/core/agent-session.ts"; +import { AuthStorage } from "../src/core/auth-storage.ts"; +import { ModelRegistry } from "../src/core/model-registry.ts"; +import { SessionManager } from "../src/core/session-manager.ts"; +import { SettingsManager } from "../src/core/settings-manager.ts"; +import type { BuildSystemPromptOptions } from "../src/core/system-prompt.ts"; +import { createTestExtensionsResult, createTestResourceLoader } from "./utilities.ts"; // Mock stream that mimics AssistantMessageEventStream class MockAssistantStream extends EventStream { diff --git a/packages/coding-agent/test/agent-session-dynamic-provider.test.ts b/packages/coding-agent/test/agent-session-dynamic-provider.test.ts index e527ca35..a6da8c9e 100644 --- a/packages/coding-agent/test/agent-session-dynamic-provider.test.ts +++ b/packages/coding-agent/test/agent-session-dynamic-provider.test.ts @@ -3,12 +3,12 @@ import { tmpdir } from "node:os"; import { join } from "node:path"; import { getModel } from "@earendil-works/pi-ai"; import { afterEach, beforeEach, describe, expect, it } from "vitest"; -import { AuthStorage } from "../src/core/auth-storage.js"; -import { DefaultResourceLoader } from "../src/core/resource-loader.js"; -import type { ExtensionFactory } from "../src/core/sdk.js"; -import { createAgentSession } from "../src/core/sdk.js"; -import { SessionManager } from "../src/core/session-manager.js"; -import { SettingsManager } from "../src/core/settings-manager.js"; +import { AuthStorage } from "../src/core/auth-storage.ts"; +import { DefaultResourceLoader } from "../src/core/resource-loader.ts"; +import type { ExtensionFactory } from "../src/core/sdk.ts"; +import { createAgentSession } from "../src/core/sdk.ts"; +import { SessionManager } from "../src/core/session-manager.ts"; +import { SettingsManager } from "../src/core/settings-manager.ts"; describe("AgentSession dynamic provider registration", () => { let tempDir: string; diff --git a/packages/coding-agent/test/agent-session-dynamic-tools.test.ts b/packages/coding-agent/test/agent-session-dynamic-tools.test.ts index 6c020def..e58ea22f 100644 --- a/packages/coding-agent/test/agent-session-dynamic-tools.test.ts +++ b/packages/coding-agent/test/agent-session-dynamic-tools.test.ts @@ -4,10 +4,10 @@ import { join } from "node:path"; import { getModel } from "@earendil-works/pi-ai"; import { Type } from "typebox"; import { afterEach, beforeEach, describe, expect, it } from "vitest"; -import { DefaultResourceLoader } from "../src/core/resource-loader.js"; -import { createAgentSession } from "../src/core/sdk.js"; -import { SessionManager } from "../src/core/session-manager.js"; -import { SettingsManager } from "../src/core/settings-manager.js"; +import { DefaultResourceLoader } from "../src/core/resource-loader.ts"; +import { createAgentSession } from "../src/core/sdk.ts"; +import { SessionManager } from "../src/core/session-manager.ts"; +import { SettingsManager } from "../src/core/settings-manager.ts"; describe("AgentSession dynamic tool registration", () => { let tempDir: string; diff --git a/packages/coding-agent/test/agent-session-retry.test.ts b/packages/coding-agent/test/agent-session-retry.test.ts index 25dfd209..ebba143b 100644 --- a/packages/coding-agent/test/agent-session-retry.test.ts +++ b/packages/coding-agent/test/agent-session-retry.test.ts @@ -5,12 +5,12 @@ import { Agent, type AgentEvent, type AgentTool } from "@earendil-works/pi-agent import { type AssistantMessage, type AssistantMessageEvent, EventStream, getModel } from "@earendil-works/pi-ai"; import { Type } from "typebox"; import { afterEach, beforeEach, describe, expect, it } from "vitest"; -import { AgentSession } from "../src/core/agent-session.js"; -import { AuthStorage } from "../src/core/auth-storage.js"; -import { ModelRegistry } from "../src/core/model-registry.js"; -import { SessionManager } from "../src/core/session-manager.js"; -import { SettingsManager } from "../src/core/settings-manager.js"; -import { createTestResourceLoader } from "./utilities.js"; +import { AgentSession } from "../src/core/agent-session.ts"; +import { AuthStorage } from "../src/core/auth-storage.ts"; +import { ModelRegistry } from "../src/core/model-registry.ts"; +import { SessionManager } from "../src/core/session-manager.ts"; +import { SettingsManager } from "../src/core/settings-manager.ts"; +import { createTestResourceLoader } from "./utilities.ts"; class MockAssistantStream extends EventStream { constructor() { diff --git a/packages/coding-agent/test/agent-session-runtime-events.test.ts b/packages/coding-agent/test/agent-session-runtime-events.test.ts index dcd70577..42b01fe1 100644 --- a/packages/coding-agent/test/agent-session-runtime-events.test.ts +++ b/packages/coding-agent/test/agent-session-runtime-events.test.ts @@ -8,16 +8,16 @@ import { createAgentSessionFromServices, createAgentSessionRuntime, createAgentSessionServices, -} from "../src/core/agent-session-runtime.js"; -import { AuthStorage } from "../src/core/auth-storage.js"; -import { SessionManager } from "../src/core/session-manager.js"; +} from "../src/core/agent-session-runtime.ts"; +import { AuthStorage } from "../src/core/auth-storage.ts"; +import { SessionManager } from "../src/core/session-manager.ts"; import type { ExtensionFactory, SessionBeforeForkEvent, SessionBeforeSwitchEvent, SessionShutdownEvent, SessionStartEvent, -} from "../src/index.js"; +} from "../src/index.ts"; type RecordedSessionEvent = | SessionBeforeSwitchEvent diff --git a/packages/coding-agent/test/agent-session-stats.test.ts b/packages/coding-agent/test/agent-session-stats.test.ts index 27788b8f..b435246e 100644 --- a/packages/coding-agent/test/agent-session-stats.test.ts +++ b/packages/coding-agent/test/agent-session-stats.test.ts @@ -1,12 +1,12 @@ import { Agent } from "@earendil-works/pi-agent-core"; import { type AssistantMessage, getModel, type Usage } from "@earendil-works/pi-ai"; import { describe, expect, it } from "vitest"; -import { AgentSession } from "../src/core/agent-session.js"; -import { AuthStorage } from "../src/core/auth-storage.js"; -import { ModelRegistry } from "../src/core/model-registry.js"; -import { SessionManager } from "../src/core/session-manager.js"; -import { SettingsManager } from "../src/core/settings-manager.js"; -import { createTestResourceLoader } from "./utilities.js"; +import { AgentSession } from "../src/core/agent-session.ts"; +import { AuthStorage } from "../src/core/auth-storage.ts"; +import { ModelRegistry } from "../src/core/model-registry.ts"; +import { SessionManager } from "../src/core/session-manager.ts"; +import { SettingsManager } from "../src/core/settings-manager.ts"; +import { createTestResourceLoader } from "./utilities.ts"; const model = getModel("anthropic", "claude-sonnet-4-5")!; diff --git a/packages/coding-agent/test/agent-session-tree-navigation.test.ts b/packages/coding-agent/test/agent-session-tree-navigation.test.ts index 35abae1e..361e5bff 100644 --- a/packages/coding-agent/test/agent-session-tree-navigation.test.ts +++ b/packages/coding-agent/test/agent-session-tree-navigation.test.ts @@ -10,7 +10,7 @@ */ import { afterEach, beforeEach, describe, expect, it } from "vitest"; -import { API_KEY, createTestSession, type TestSessionContext } from "./utilities.js"; +import { API_KEY, createTestSession, type TestSessionContext } from "./utilities.ts"; describe.skipIf(!API_KEY)("AgentSession tree navigation e2e", () => { let ctx: TestSessionContext; diff --git a/packages/coding-agent/test/ansi-utils.test.ts b/packages/coding-agent/test/ansi-utils.test.ts index 862f0401..15bb4314 100644 --- a/packages/coding-agent/test/ansi-utils.test.ts +++ b/packages/coding-agent/test/ansi-utils.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "vitest"; -import { stripAnsi } from "../src/utils/ansi.js"; +import { stripAnsi } from "../src/utils/ansi.ts"; function referenceAnsiRegex(): RegExp { const ST = "(?:\\u0007|\\u001B\\u005C|\\u009C)"; diff --git a/packages/coding-agent/test/args.test.ts b/packages/coding-agent/test/args.test.ts index ab3ce2a5..3bf41473 100644 --- a/packages/coding-agent/test/args.test.ts +++ b/packages/coding-agent/test/args.test.ts @@ -1,5 +1,5 @@ import { describe, expect, test } from "vitest"; -import { parseArgs } from "../src/cli/args.js"; +import { parseArgs } from "../src/cli/args.ts"; describe("parseArgs", () => { describe("--version flag", () => { diff --git a/packages/coding-agent/test/assistant-message.test.ts b/packages/coding-agent/test/assistant-message.test.ts index 9af2a4a2..137376a5 100644 --- a/packages/coding-agent/test/assistant-message.test.ts +++ b/packages/coding-agent/test/assistant-message.test.ts @@ -1,7 +1,7 @@ import type { AssistantMessage } from "@earendil-works/pi-ai"; import { describe, expect, test } from "vitest"; -import { AssistantMessageComponent } from "../src/modes/interactive/components/assistant-message.js"; -import { initTheme } from "../src/modes/interactive/theme/theme.js"; +import { AssistantMessageComponent } from "../src/modes/interactive/components/assistant-message.ts"; +import { initTheme } from "../src/modes/interactive/theme/theme.ts"; const OSC133_ZONE_START = "\x1b]133;A\x07"; const OSC133_ZONE_END = "\x1b]133;B\x07"; diff --git a/packages/coding-agent/test/auth-storage.test.ts b/packages/coding-agent/test/auth-storage.test.ts index ddaef67e..9ec97100 100644 --- a/packages/coding-agent/test/auth-storage.test.ts +++ b/packages/coding-agent/test/auth-storage.test.ts @@ -4,8 +4,8 @@ import { join } from "node:path"; import { registerOAuthProvider } from "@earendil-works/pi-ai/oauth"; import lockfile from "proper-lockfile"; import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; -import { AuthStorage } from "../src/core/auth-storage.js"; -import { clearConfigValueCache } from "../src/core/resolve-config-value.js"; +import { AuthStorage } from "../src/core/auth-storage.ts"; +import { clearConfigValueCache } from "../src/core/resolve-config-value.ts"; describe("AuthStorage", () => { let tempDir: string; diff --git a/packages/coding-agent/test/bash-close-hang-windows.test.ts b/packages/coding-agent/test/bash-close-hang-windows.test.ts index 10ae6414..a17834e2 100644 --- a/packages/coding-agent/test/bash-close-hang-windows.test.ts +++ b/packages/coding-agent/test/bash-close-hang-windows.test.ts @@ -3,8 +3,8 @@ import { existsSync, mkdirSync, readFileSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { afterEach, beforeEach, describe, expect, it } from "vitest"; -import { executeBashWithOperations } from "../src/core/bash-executor.js"; -import { createBashTool, createLocalBashOperations } from "../src/core/tools/bash.js"; +import { executeBashWithOperations } from "../src/core/bash-executor.ts"; +import { createBashTool, createLocalBashOperations } from "../src/core/tools/bash.ts"; function toBashSingleQuotedArg(value: string): string { return `'${value.replace(/\\/g, "/").replace(/'/g, `'"'"'`)}'`; diff --git a/packages/coding-agent/test/bash-execution-width.test.ts b/packages/coding-agent/test/bash-execution-width.test.ts index 12a8bb51..9cc64e8c 100644 --- a/packages/coding-agent/test/bash-execution-width.test.ts +++ b/packages/coding-agent/test/bash-execution-width.test.ts @@ -4,8 +4,8 @@ */ import { visibleWidth } from "@earendil-works/pi-tui"; import { beforeAll, describe, expect, it } from "vitest"; -import { BashExecutionComponent } from "../src/modes/interactive/components/bash-execution.js"; -import { initTheme } from "../src/modes/interactive/theme/theme.js"; +import { BashExecutionComponent } from "../src/modes/interactive/components/bash-execution.ts"; +import { initTheme } from "../src/modes/interactive/theme/theme.ts"; /** Minimal TUI stub that only exposes terminal.columns */ function createTuiStub(columns: number): { columns: number; stub: any } { diff --git a/packages/coding-agent/test/block-images.test.ts b/packages/coding-agent/test/block-images.test.ts index 2c599276..19b43459 100644 --- a/packages/coding-agent/test/block-images.test.ts +++ b/packages/coding-agent/test/block-images.test.ts @@ -2,9 +2,9 @@ import { mkdirSync, rmSync, writeFileSync } from "fs"; import { tmpdir } from "os"; import { join } from "path"; import { afterEach, beforeEach, describe, expect, it } from "vitest"; -import { processFileArguments } from "../src/cli/file-processor.js"; -import { SettingsManager } from "../src/core/settings-manager.js"; -import { createReadTool } from "../src/core/tools/read.js"; +import { processFileArguments } from "../src/cli/file-processor.ts"; +import { SettingsManager } from "../src/core/settings-manager.ts"; +import { createReadTool } from "../src/core/tools/read.ts"; // 1x1 red PNG image as base64 (smallest valid PNG) const TINY_PNG_BASE64 = diff --git a/packages/coding-agent/test/clipboard-image-bmp-conversion.test.ts b/packages/coding-agent/test/clipboard-image-bmp-conversion.test.ts index 95107506..be3a9d95 100644 --- a/packages/coding-agent/test/clipboard-image-bmp-conversion.test.ts +++ b/packages/coding-agent/test/clipboard-image-bmp-conversion.test.ts @@ -68,7 +68,7 @@ vi.mock("@mariozechner/clipboard", () => ({ describe("readClipboardImage BMP conversion", () => { test("converts BMP to PNG on Wayland/WSLg", async () => { - const { readClipboardImage } = await import("../src/utils/clipboard-image.js"); + const { readClipboardImage } = await import("../src/utils/clipboard-image.ts"); // Simulate Wayland session (WSLg) const image = await readClipboardImage({ diff --git a/packages/coding-agent/test/clipboard-image.test.ts b/packages/coding-agent/test/clipboard-image.test.ts index eda61a29..b7416a4c 100644 --- a/packages/coding-agent/test/clipboard-image.test.ts +++ b/packages/coding-agent/test/clipboard-image.test.ts @@ -70,7 +70,7 @@ describe("readClipboardImage", () => { throw new Error(`Unexpected spawnSync call: ${command} ${args.join(" ")}`); }); - const { readClipboardImage } = await import("../src/utils/clipboard-image.js"); + const { readClipboardImage } = await import("../src/utils/clipboard-image.ts"); const result = await readClipboardImage({ platform: "linux", env: { WAYLAND_DISPLAY: "1" } }); expect(result).not.toBeNull(); expect(result?.mimeType).toBe("image/png"); @@ -101,7 +101,7 @@ describe("readClipboardImage", () => { return spawnOk(Buffer.alloc(0)); }); - const { readClipboardImage } = await import("../src/utils/clipboard-image.js"); + const { readClipboardImage } = await import("../src/utils/clipboard-image.ts"); const result = await readClipboardImage({ platform: "linux", env: { XDG_SESSION_TYPE: "wayland" } }); expect(result).not.toBeNull(); expect(result?.mimeType).toBe("image/png"); @@ -138,7 +138,7 @@ describe("readClipboardImage", () => { throw new Error(`Unexpected spawnSync call: ${command} ${args.join(" ")}`); }); - const { readClipboardImage } = await import("../src/utils/clipboard-image.js"); + const { readClipboardImage } = await import("../src/utils/clipboard-image.ts"); const result = await readClipboardImage({ platform: "linux", env: { WSL_DISTRO_NAME: "Ubuntu" } }); expect(result).not.toBeNull(); expect(result?.mimeType).toBe("image/png"); @@ -153,7 +153,7 @@ describe("readClipboardImage", () => { mocks.clipboard.hasImage.mockReturnValue(true); mocks.clipboard.getImageBinary.mockResolvedValue(new Uint8Array([7])); - const { readClipboardImage } = await import("../src/utils/clipboard-image.js"); + const { readClipboardImage } = await import("../src/utils/clipboard-image.ts"); const result = await readClipboardImage({ platform: "linux", env: {} }); expect(result).not.toBeNull(); expect(result?.mimeType).toBe("image/png"); @@ -167,7 +167,7 @@ describe("readClipboardImage", () => { mocks.clipboard.hasImage.mockReturnValue(false); - const { readClipboardImage } = await import("../src/utils/clipboard-image.js"); + const { readClipboardImage } = await import("../src/utils/clipboard-image.ts"); const result = await readClipboardImage({ platform: "linux", env: {} }); expect(result).toBeNull(); }); diff --git a/packages/coding-agent/test/clipboard.test.ts b/packages/coding-agent/test/clipboard.test.ts index ea231b57..6ea3a685 100644 --- a/packages/coding-agent/test/clipboard.test.ts +++ b/packages/coding-agent/test/clipboard.test.ts @@ -1,7 +1,7 @@ import { execSync, spawn } from "child_process"; import { platform } from "os"; import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; -import { copyToClipboard } from "../src/utils/clipboard.js"; +import { copyToClipboard } from "../src/utils/clipboard.ts"; const mocks = vi.hoisted(() => { return { diff --git a/packages/coding-agent/test/compaction-extensions-example.test.ts b/packages/coding-agent/test/compaction-extensions-example.test.ts index 4f5e3359..db2f4082 100644 --- a/packages/coding-agent/test/compaction-extensions-example.test.ts +++ b/packages/coding-agent/test/compaction-extensions-example.test.ts @@ -3,7 +3,7 @@ */ import { describe, expect, it } from "vitest"; -import type { ExtensionAPI, SessionBeforeCompactEvent, SessionCompactEvent } from "../src/core/extensions/index.js"; +import type { ExtensionAPI, SessionBeforeCompactEvent, SessionCompactEvent } from "../src/core/extensions/index.ts"; describe("Documentation example", () => { it("custom compaction example should type-check correctly", () => { diff --git a/packages/coding-agent/test/compaction-extensions.test.ts b/packages/coding-agent/test/compaction-extensions.test.ts index 1a134e7b..2c41210e 100644 --- a/packages/coding-agent/test/compaction-extensions.test.ts +++ b/packages/coding-agent/test/compaction-extensions.test.ts @@ -8,21 +8,21 @@ import { join } from "node:path"; import { Agent } from "@earendil-works/pi-agent-core"; import { getModel } from "@earendil-works/pi-ai"; import { afterEach, beforeEach, describe, expect, it } from "vitest"; -import { AgentSession } from "../src/core/agent-session.js"; -import { AuthStorage } from "../src/core/auth-storage.js"; +import { AgentSession } from "../src/core/agent-session.ts"; +import { AuthStorage } from "../src/core/auth-storage.ts"; import { createExtensionRuntime, type Extension, type SessionBeforeCompactEvent, type SessionCompactEvent, type SessionEvent, -} from "../src/core/extensions/index.js"; -import { ModelRegistry } from "../src/core/model-registry.js"; -import { SessionManager } from "../src/core/session-manager.js"; -import { SettingsManager } from "../src/core/settings-manager.js"; -import { createSyntheticSourceInfo } from "../src/core/source-info.js"; -import { createCodingTools } from "../src/index.js"; -import { createTestResourceLoader } from "./utilities.js"; +} from "../src/core/extensions/index.ts"; +import { ModelRegistry } from "../src/core/model-registry.ts"; +import { SessionManager } from "../src/core/session-manager.ts"; +import { SettingsManager } from "../src/core/settings-manager.ts"; +import { createSyntheticSourceInfo } from "../src/core/source-info.ts"; +import { createCodingTools } from "../src/index.ts"; +import { createTestResourceLoader } from "./utilities.ts"; const API_KEY = process.env.ANTHROPIC_OAUTH_TOKEN || process.env.ANTHROPIC_API_KEY; diff --git a/packages/coding-agent/test/compaction-serialization.test.ts b/packages/coding-agent/test/compaction-serialization.test.ts index 9b11bb0f..91ca9251 100644 --- a/packages/coding-agent/test/compaction-serialization.test.ts +++ b/packages/coding-agent/test/compaction-serialization.test.ts @@ -1,6 +1,6 @@ import type { Message } from "@earendil-works/pi-ai"; import { describe, expect, it } from "vitest"; -import { serializeConversation } from "../src/core/compaction/utils.js"; +import { serializeConversation } from "../src/core/compaction/utils.ts"; describe("serializeConversation", () => { it("should truncate long tool results", () => { diff --git a/packages/coding-agent/test/compaction-summary-reasoning.test.ts b/packages/coding-agent/test/compaction-summary-reasoning.test.ts index ea52016b..6609aca1 100644 --- a/packages/coding-agent/test/compaction-summary-reasoning.test.ts +++ b/packages/coding-agent/test/compaction-summary-reasoning.test.ts @@ -1,7 +1,7 @@ import type { AgentMessage } from "@earendil-works/pi-agent-core"; import type { AssistantMessage, Model } from "@earendil-works/pi-ai"; import { beforeEach, describe, expect, it, vi } from "vitest"; -import { type CompactionPreparation, compact, generateSummary } from "../src/core/compaction/index.js"; +import { type CompactionPreparation, compact, generateSummary } from "../src/core/compaction/index.ts"; const { completeSimpleMock } = vi.hoisted(() => ({ completeSimpleMock: vi.fn(), diff --git a/packages/coding-agent/test/compaction.test.ts b/packages/coding-agent/test/compaction.test.ts index ca665543..929d06ec 100644 --- a/packages/coding-agent/test/compaction.test.ts +++ b/packages/coding-agent/test/compaction.test.ts @@ -14,7 +14,7 @@ import { getLastAssistantUsage, prepareCompaction, shouldCompact, -} from "../src/core/compaction/index.js"; +} from "../src/core/compaction/index.ts"; import { buildSessionContext, type CompactionEntry, @@ -24,7 +24,7 @@ import { type SessionEntry, type SessionMessageEntry, type ThinkingLevelChangeEntry, -} from "../src/core/session-manager.js"; +} from "../src/core/session-manager.ts"; // ============================================================================ // Test fixtures diff --git a/packages/coding-agent/test/config.test.ts b/packages/coding-agent/test/config.test.ts index 71c4b48b..c22ff12b 100644 --- a/packages/coding-agent/test/config.test.ts +++ b/packages/coding-agent/test/config.test.ts @@ -7,7 +7,7 @@ import { getSelfUpdateCommand, getSelfUpdateUnavailableInstruction, getUpdateInstruction, -} from "../src/config.js"; +} from "../src/config.ts"; const execPathDescriptor = Object.getOwnPropertyDescriptor(process, "execPath"); const originalPath = process.env.PATH; diff --git a/packages/coding-agent/test/edit-tool-legacy-input.test.ts b/packages/coding-agent/test/edit-tool-legacy-input.test.ts index 13a8bbeb..26a550d2 100644 --- a/packages/coding-agent/test/edit-tool-legacy-input.test.ts +++ b/packages/coding-agent/test/edit-tool-legacy-input.test.ts @@ -2,8 +2,8 @@ import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { afterEach, describe, expect, it } from "vitest"; -import type { ExtensionContext } from "../src/core/extensions/types.js"; -import { createEditToolDefinition } from "../src/core/tools/edit.js"; +import type { ExtensionContext } from "../src/core/extensions/types.ts"; +import { createEditToolDefinition } from "../src/core/tools/edit.ts"; const tempDirs: string[] = []; diff --git a/packages/coding-agent/test/edit-tool-no-full-redraw.test.ts b/packages/coding-agent/test/edit-tool-no-full-redraw.test.ts index 21d4e7d5..1da9f0df 100644 --- a/packages/coding-agent/test/edit-tool-no-full-redraw.test.ts +++ b/packages/coding-agent/test/edit-tool-no-full-redraw.test.ts @@ -3,10 +3,10 @@ import { tmpdir } from "node:os"; import { join } from "node:path"; import { Container, type Terminal, Text, TUI } from "@earendil-works/pi-tui"; import { afterEach, beforeAll, describe, expect, it } from "vitest"; -import { createEditToolDefinition } from "../src/core/tools/edit.js"; -import { computeEditsDiff, type Edit } from "../src/core/tools/edit-diff.js"; -import { ToolExecutionComponent } from "../src/modes/interactive/components/tool-execution.js"; -import { initTheme } from "../src/modes/interactive/theme/theme.js"; +import { createEditToolDefinition } from "../src/core/tools/edit.ts"; +import { computeEditsDiff, type Edit } from "../src/core/tools/edit-diff.ts"; +import { ToolExecutionComponent } from "../src/modes/interactive/components/tool-execution.ts"; +import { initTheme } from "../src/modes/interactive/theme/theme.ts"; class FakeTerminal implements Terminal { columns = 80; diff --git a/packages/coding-agent/test/export-html-whitespace.test.ts b/packages/coding-agent/test/export-html-whitespace.test.ts index 4336c63c..9d2a0e84 100644 --- a/packages/coding-agent/test/export-html-whitespace.test.ts +++ b/packages/coding-agent/test/export-html-whitespace.test.ts @@ -1,10 +1,10 @@ import type { Component } from "@earendil-works/pi-tui"; import { readFileSync } from "fs"; import { describe, expect, it } from "vitest"; -import { ansiLinesToHtml } from "../src/core/export-html/ansi-to-html.js"; -import { createToolHtmlRenderer } from "../src/core/export-html/tool-renderer.js"; -import type { ToolDefinition } from "../src/core/extensions/types.js"; -import type { Theme } from "../src/modes/interactive/theme/theme.js"; +import { ansiLinesToHtml } from "../src/core/export-html/ansi-to-html.ts"; +import { createToolHtmlRenderer } from "../src/core/export-html/tool-renderer.ts"; +import type { ToolDefinition } from "../src/core/extensions/types.ts"; +import type { Theme } from "../src/modes/interactive/theme/theme.ts"; describe("export HTML tool output whitespace", () => { it("preserves whitespace for plain-text tool output lines without preserving template whitespace", () => { diff --git a/packages/coding-agent/test/extensions-discovery.test.ts b/packages/coding-agent/test/extensions-discovery.test.ts index 52029d2c..3613e902 100644 --- a/packages/coding-agent/test/extensions-discovery.test.ts +++ b/packages/coding-agent/test/extensions-discovery.test.ts @@ -3,7 +3,7 @@ import * as os from "node:os"; import * as path from "node:path"; import { fileURLToPath } from "node:url"; import { afterEach, beforeEach, describe, expect, it } from "vitest"; -import { discoverAndLoadExtensions } from "../src/core/extensions/loader.js"; +import { discoverAndLoadExtensions } from "../src/core/extensions/loader.ts"; const __dirname = path.dirname(fileURLToPath(import.meta.url)); @@ -440,7 +440,7 @@ describe("extensions discovery", () => { fs.writeFileSync(explicitPath, extensionCodeWithTool("explicit")); // Use loadExtensions directly to skip discovery - const { loadExtensions } = await import("../src/core/extensions/loader.js"); + const { loadExtensions } = await import("../src/core/extensions/loader.ts"); const result = await loadExtensions([explicitPath], tempDir); expect(result.errors).toHaveLength(0); @@ -454,7 +454,7 @@ describe("extensions discovery", () => { fs.writeFileSync(path.join(extensionsDir, "discovered.ts"), extensionCode); // Use loadExtensions directly with empty paths - const { loadExtensions } = await import("../src/core/extensions/loader.js"); + const { loadExtensions } = await import("../src/core/extensions/loader.ts"); const result = await loadExtensions([], tempDir); expect(result.errors).toHaveLength(0); diff --git a/packages/coding-agent/test/extensions-input-event.test.ts b/packages/coding-agent/test/extensions-input-event.test.ts index 5c08fd2c..3d5ae5cc 100644 --- a/packages/coding-agent/test/extensions-input-event.test.ts +++ b/packages/coding-agent/test/extensions-input-event.test.ts @@ -2,11 +2,11 @@ import * as fs from "node:fs"; import * as os from "node:os"; import * as path from "node:path"; import { afterEach, beforeEach, describe, expect, it } from "vitest"; -import { AuthStorage } from "../src/core/auth-storage.js"; -import { discoverAndLoadExtensions } from "../src/core/extensions/loader.js"; -import { ExtensionRunner } from "../src/core/extensions/runner.js"; -import { ModelRegistry } from "../src/core/model-registry.js"; -import { SessionManager } from "../src/core/session-manager.js"; +import { AuthStorage } from "../src/core/auth-storage.ts"; +import { discoverAndLoadExtensions } from "../src/core/extensions/loader.ts"; +import { ExtensionRunner } from "../src/core/extensions/runner.ts"; +import { ModelRegistry } from "../src/core/model-registry.ts"; +import { SessionManager } from "../src/core/session-manager.ts"; describe("Input Event", () => { let tempDir: string; diff --git a/packages/coding-agent/test/extensions-runner.test.ts b/packages/coding-agent/test/extensions-runner.test.ts index 67c46235..03de2d77 100644 --- a/packages/coding-agent/test/extensions-runner.test.ts +++ b/packages/coding-agent/test/extensions-runner.test.ts @@ -6,13 +6,13 @@ import * as fs from "node:fs"; import * as os from "node:os"; import * as path from "node:path"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import { AuthStorage } from "../src/core/auth-storage.js"; -import { createExtensionRuntime, discoverAndLoadExtensions } from "../src/core/extensions/loader.js"; -import { ExtensionRunner } from "../src/core/extensions/runner.js"; -import type { ExtensionActions, ExtensionContextActions, ProviderConfig } from "../src/core/extensions/types.js"; -import { KeybindingsManager, type KeyId } from "../src/core/keybindings.js"; -import { ModelRegistry } from "../src/core/model-registry.js"; -import { SessionManager } from "../src/core/session-manager.js"; +import { AuthStorage } from "../src/core/auth-storage.ts"; +import { createExtensionRuntime, discoverAndLoadExtensions } from "../src/core/extensions/loader.ts"; +import { ExtensionRunner } from "../src/core/extensions/runner.ts"; +import type { ExtensionActions, ExtensionContextActions, ProviderConfig } from "../src/core/extensions/types.ts"; +import { KeybindingsManager, type KeyId } from "../src/core/keybindings.ts"; +import { ModelRegistry } from "../src/core/model-registry.ts"; +import { SessionManager } from "../src/core/session-manager.ts"; describe("ExtensionRunner", () => { let tempDir: string; diff --git a/packages/coding-agent/test/file-mutation-queue.test.ts b/packages/coding-agent/test/file-mutation-queue.test.ts index 8b839fe2..73d20c6d 100644 --- a/packages/coding-agent/test/file-mutation-queue.test.ts +++ b/packages/coding-agent/test/file-mutation-queue.test.ts @@ -2,9 +2,9 @@ import { access, mkdtemp, readFile, rm, symlink, writeFile } from "node:fs/promi import { tmpdir } from "node:os"; import { join } from "node:path"; import { afterEach, describe, expect, it } from "vitest"; -import { createEditTool } from "../src/core/tools/edit.js"; -import { withFileMutationQueue } from "../src/core/tools/file-mutation-queue.js"; -import { createWriteTool } from "../src/core/tools/write.js"; +import { createEditTool } from "../src/core/tools/edit.ts"; +import { withFileMutationQueue } from "../src/core/tools/file-mutation-queue.ts"; +import { createWriteTool } from "../src/core/tools/write.ts"; function delay(ms: number): Promise { return new Promise((resolve) => setTimeout(resolve, ms)); diff --git a/packages/coding-agent/test/footer-data-provider.test.ts b/packages/coding-agent/test/footer-data-provider.test.ts index 3a6e1d47..4ad9a23a 100644 --- a/packages/coding-agent/test/footer-data-provider.test.ts +++ b/packages/coding-agent/test/footer-data-provider.test.ts @@ -37,7 +37,7 @@ vi.mock("child_process", () => ({ }), })); -import { FooterDataProvider } from "../src/core/footer-data-provider.js"; +import { FooterDataProvider } from "../src/core/footer-data-provider.ts"; type WorktreeFixture = { worktreeDir: string; diff --git a/packages/coding-agent/test/footer-width.test.ts b/packages/coding-agent/test/footer-width.test.ts index dd158f36..2d912647 100644 --- a/packages/coding-agent/test/footer-width.test.ts +++ b/packages/coding-agent/test/footer-width.test.ts @@ -1,9 +1,9 @@ import { visibleWidth } from "@earendil-works/pi-tui"; import { beforeAll, describe, expect, it } from "vitest"; -import type { AgentSession } from "../src/core/agent-session.js"; -import type { ReadonlyFooterDataProvider } from "../src/core/footer-data-provider.js"; -import { FooterComponent } from "../src/modes/interactive/components/footer.js"; -import { initTheme } from "../src/modes/interactive/theme/theme.js"; +import type { AgentSession } from "../src/core/agent-session.ts"; +import type { ReadonlyFooterDataProvider } from "../src/core/footer-data-provider.ts"; +import { FooterComponent } from "../src/modes/interactive/components/footer.ts"; +import { initTheme } from "../src/modes/interactive/theme/theme.ts"; type AssistantUsage = { input: number; diff --git a/packages/coding-agent/test/frontmatter.test.ts b/packages/coding-agent/test/frontmatter.test.ts index 3879fedf..43a92985 100644 --- a/packages/coding-agent/test/frontmatter.test.ts +++ b/packages/coding-agent/test/frontmatter.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "vitest"; -import { parseFrontmatter, stripFrontmatter } from "../src/utils/frontmatter.js"; +import { parseFrontmatter, stripFrontmatter } from "../src/utils/frontmatter.ts"; describe("parseFrontmatter", () => { it("parses keys, strips quotes, and returns body", () => { diff --git a/packages/coding-agent/test/git-ssh-url.test.ts b/packages/coding-agent/test/git-ssh-url.test.ts index a7d24267..6913b70c 100644 --- a/packages/coding-agent/test/git-ssh-url.test.ts +++ b/packages/coding-agent/test/git-ssh-url.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "vitest"; -import { parseGitUrl } from "../src/utils/git.js"; +import { parseGitUrl } from "../src/utils/git.ts"; describe("Git URL Parsing", () => { describe("protocol URLs (accepted without git: prefix)", () => { diff --git a/packages/coding-agent/test/git-update.test.ts b/packages/coding-agent/test/git-update.test.ts index 2cfcb404..0f574ba6 100644 --- a/packages/coding-agent/test/git-update.test.ts +++ b/packages/coding-agent/test/git-update.test.ts @@ -12,8 +12,8 @@ import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node import { tmpdir } from "node:os"; import { join } from "node:path"; import { afterEach, beforeEach, describe, expect, it } from "vitest"; -import { DefaultPackageManager } from "../src/core/package-manager.js"; -import { SettingsManager } from "../src/core/settings-manager.js"; +import { DefaultPackageManager } from "../src/core/package-manager.ts"; +import { SettingsManager } from "../src/core/settings-manager.ts"; // Helper to run git commands in a directory function git(args: string[], cwd: string): string { diff --git a/packages/coding-agent/test/image-processing.test.ts b/packages/coding-agent/test/image-processing.test.ts index 4dba6f2d..40f264a8 100644 --- a/packages/coding-agent/test/image-processing.test.ts +++ b/packages/coding-agent/test/image-processing.test.ts @@ -3,8 +3,8 @@ */ import { describe, expect, it } from "vitest"; -import { convertToPng } from "../src/utils/image-convert.js"; -import { formatDimensionNote, resizeImage } from "../src/utils/image-resize.js"; +import { convertToPng } from "../src/utils/image-convert.ts"; +import { formatDimensionNote, resizeImage } from "../src/utils/image-resize.ts"; // Small 2x2 red PNG image (base64) - generated with ImageMagick const TINY_PNG = diff --git a/packages/coding-agent/test/image-resize-callers.test.ts b/packages/coding-agent/test/image-resize-callers.test.ts index 5d1c1a79..2337ae8c 100644 --- a/packages/coding-agent/test/image-resize-callers.test.ts +++ b/packages/coding-agent/test/image-resize-callers.test.ts @@ -8,9 +8,9 @@ vi.mock("../src/utils/image-resize.js", () => ({ formatDimensionNote: vi.fn(() => undefined), })); -import { processFileArguments } from "../src/cli/file-processor.js"; -import { createReadTool } from "../src/core/tools/read.js"; -import { resizeImage } from "../src/utils/image-resize.js"; +import { processFileArguments } from "../src/cli/file-processor.ts"; +import { createReadTool } from "../src/core/tools/read.ts"; +import { resizeImage } from "../src/utils/image-resize.ts"; const TINY_PNG_BASE64 = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8DwHwAFBQIAX8jx0gAAAABJRU5ErkJggg=="; diff --git a/packages/coding-agent/test/initial-message.test.ts b/packages/coding-agent/test/initial-message.test.ts index 22b1612d..fae479f7 100644 --- a/packages/coding-agent/test/initial-message.test.ts +++ b/packages/coding-agent/test/initial-message.test.ts @@ -1,6 +1,6 @@ import { describe, expect, test } from "vitest"; -import type { Args } from "../src/cli/args.js"; -import { buildInitialMessage } from "../src/cli/initial-message.js"; +import type { Args } from "../src/cli/args.ts"; +import { buildInitialMessage } from "../src/cli/initial-message.ts"; function createArgs(messages: string[] = []): Args { return { diff --git a/packages/coding-agent/test/interactive-mode-anthropic-warning.test.ts b/packages/coding-agent/test/interactive-mode-anthropic-warning.test.ts index 16ca930a..10e35192 100644 --- a/packages/coding-agent/test/interactive-mode-anthropic-warning.test.ts +++ b/packages/coding-agent/test/interactive-mode-anthropic-warning.test.ts @@ -1,5 +1,5 @@ import { describe, expect, test, vi } from "vitest"; -import { InteractiveMode } from "../src/modes/interactive/interactive-mode.js"; +import { InteractiveMode } from "../src/modes/interactive/interactive-mode.ts"; function createSettingsManager(warnings: { anthropicExtraUsage?: boolean } = {}) { return { diff --git a/packages/coding-agent/test/interactive-mode-clone-command.test.ts b/packages/coding-agent/test/interactive-mode-clone-command.test.ts index 9388b89f..fea69c5d 100644 --- a/packages/coding-agent/test/interactive-mode-clone-command.test.ts +++ b/packages/coding-agent/test/interactive-mode-clone-command.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it, vi } from "vitest"; -import { InteractiveMode } from "../src/modes/interactive/interactive-mode.js"; +import { InteractiveMode } from "../src/modes/interactive/interactive-mode.ts"; type CloneCommandContext = { sessionManager: { getLeafId: () => string | null }; diff --git a/packages/coding-agent/test/interactive-mode-compaction.test.ts b/packages/coding-agent/test/interactive-mode-compaction.test.ts index 85e8e919..0fc90454 100644 --- a/packages/coding-agent/test/interactive-mode-compaction.test.ts +++ b/packages/coding-agent/test/interactive-mode-compaction.test.ts @@ -1,5 +1,5 @@ import { describe, expect, test, vi } from "vitest"; -import { InteractiveMode } from "../src/modes/interactive/interactive-mode.js"; +import { InteractiveMode } from "../src/modes/interactive/interactive-mode.ts"; describe("InteractiveMode compaction events", () => { test("rebuilds chat and appends a synthetic compaction summary at the bottom", async () => { diff --git a/packages/coding-agent/test/interactive-mode-import-command.test.ts b/packages/coding-agent/test/interactive-mode-import-command.test.ts index 85575ec0..c2e5ae1f 100644 --- a/packages/coding-agent/test/interactive-mode-import-command.test.ts +++ b/packages/coding-agent/test/interactive-mode-import-command.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it, vi } from "vitest"; -import { SessionImportFileNotFoundError } from "../src/core/agent-session-runtime.js"; -import { InteractiveMode } from "../src/modes/interactive/interactive-mode.js"; +import { SessionImportFileNotFoundError } from "../src/core/agent-session-runtime.ts"; +import { InteractiveMode } from "../src/modes/interactive/interactive-mode.ts"; type PathCommand = "/export" | "/import"; diff --git a/packages/coding-agent/test/interactive-mode-status.test.ts b/packages/coding-agent/test/interactive-mode-status.test.ts index ba2b46c5..d5a2c159 100644 --- a/packages/coding-agent/test/interactive-mode-status.test.ts +++ b/packages/coding-agent/test/interactive-mode-status.test.ts @@ -2,10 +2,10 @@ import { homedir } from "node:os"; import * as path from "node:path"; import { type AutocompleteProvider, CombinedAutocompleteProvider, Container } from "@earendil-works/pi-tui"; import { beforeAll, describe, expect, test, vi } from "vitest"; -import type { AutocompleteProviderFactory } from "../src/core/extensions/types.js"; -import type { SourceInfo } from "../src/core/source-info.js"; -import { InteractiveMode } from "../src/modes/interactive/interactive-mode.js"; -import { initTheme } from "../src/modes/interactive/theme/theme.js"; +import type { AutocompleteProviderFactory } from "../src/core/extensions/types.ts"; +import type { SourceInfo } from "../src/core/source-info.ts"; +import { InteractiveMode } from "../src/modes/interactive/interactive-mode.ts"; +import { initTheme } from "../src/modes/interactive/theme/theme.ts"; function renderLastLine(container: Container, width = 120): string { const last = container.children[container.children.length - 1]; diff --git a/packages/coding-agent/test/interactive-mode-suspend.test.ts b/packages/coding-agent/test/interactive-mode-suspend.test.ts index 3f41fe2e..996d2f1a 100644 --- a/packages/coding-agent/test/interactive-mode-suspend.test.ts +++ b/packages/coding-agent/test/interactive-mode-suspend.test.ts @@ -1,5 +1,5 @@ import { afterEach, describe, expect, test, vi } from "vitest"; -import { InteractiveMode } from "../src/modes/interactive/interactive-mode.js"; +import { InteractiveMode } from "../src/modes/interactive/interactive-mode.ts"; type FakeUi = { start: () => void; diff --git a/packages/coding-agent/test/keybindings-migration.test.ts b/packages/coding-agent/test/keybindings-migration.test.ts index 0799ff25..f393687c 100644 --- a/packages/coding-agent/test/keybindings-migration.test.ts +++ b/packages/coding-agent/test/keybindings-migration.test.ts @@ -2,9 +2,9 @@ import * as fs from "node:fs"; import * as os from "node:os"; import * as path from "node:path"; import { afterEach, describe, expect, it } from "vitest"; -import { ENV_AGENT_DIR } from "../src/config.js"; -import { KeybindingsManager } from "../src/core/keybindings.js"; -import { runMigrations } from "../src/migrations.js"; +import { ENV_AGENT_DIR } from "../src/config.ts"; +import { KeybindingsManager } from "../src/core/keybindings.ts"; +import { runMigrations } from "../src/migrations.ts"; describe("keybindings migration", () => { const tempDirs: string[] = []; diff --git a/packages/coding-agent/test/model-registry.test.ts b/packages/coding-agent/test/model-registry.test.ts index 03434ea8..ad706ab5 100644 --- a/packages/coding-agent/test/model-registry.test.ts +++ b/packages/coding-agent/test/model-registry.test.ts @@ -5,8 +5,8 @@ import type { AnthropicMessagesCompat, Api, Context, Model, OpenAICompletionsCom import { getApiProvider } from "@earendil-works/pi-ai"; import { getOAuthProvider } from "@earendil-works/pi-ai/oauth"; import { afterEach, beforeEach, describe, expect, test } from "vitest"; -import { AuthStorage } from "../src/core/auth-storage.js"; -import { clearApiKeyCache, ModelRegistry, type ProviderConfigInput } from "../src/core/model-registry.js"; +import { AuthStorage } from "../src/core/auth-storage.ts"; +import { clearApiKeyCache, ModelRegistry, type ProviderConfigInput } from "../src/core/model-registry.ts"; describe("ModelRegistry", () => { let tempDir: string; diff --git a/packages/coding-agent/test/model-resolver.test.ts b/packages/coding-agent/test/model-resolver.test.ts index 69afd756..7339200b 100644 --- a/packages/coding-agent/test/model-resolver.test.ts +++ b/packages/coding-agent/test/model-resolver.test.ts @@ -5,7 +5,7 @@ import { findInitialModel, parseModelPattern, resolveCliModel, -} from "../src/core/model-resolver.js"; +} from "../src/core/model-resolver.ts"; // Mock models for testing const mockModels: Model<"anthropic-messages">[] = [ diff --git a/packages/coding-agent/test/oauth-selector.test.ts b/packages/coding-agent/test/oauth-selector.test.ts index 26fd993b..7164db54 100644 --- a/packages/coding-agent/test/oauth-selector.test.ts +++ b/packages/coding-agent/test/oauth-selector.test.ts @@ -1,12 +1,12 @@ import { setKeybindings } from "@earendil-works/pi-tui"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; -import { AuthStorage } from "../src/core/auth-storage.js"; -import { KeybindingsManager } from "../src/core/keybindings.js"; -import { BUILT_IN_PROVIDER_DISPLAY_NAMES } from "../src/core/provider-display-names.js"; -import { OAuthSelectorComponent } from "../src/modes/interactive/components/oauth-selector.js"; -import { isApiKeyLoginProvider } from "../src/modes/interactive/interactive-mode.js"; -import { initTheme } from "../src/modes/interactive/theme/theme.js"; -import { stripAnsi } from "../src/utils/ansi.js"; +import { AuthStorage } from "../src/core/auth-storage.ts"; +import { KeybindingsManager } from "../src/core/keybindings.ts"; +import { BUILT_IN_PROVIDER_DISPLAY_NAMES } from "../src/core/provider-display-names.ts"; +import { OAuthSelectorComponent } from "../src/modes/interactive/components/oauth-selector.ts"; +import { isApiKeyLoginProvider } from "../src/modes/interactive/interactive-mode.ts"; +import { initTheme } from "../src/modes/interactive/theme/theme.ts"; +import { stripAnsi } from "../src/utils/ansi.ts"; const originalOpenAiApiKey = process.env.OPENAI_API_KEY; diff --git a/packages/coding-agent/test/package-command-paths.test.ts b/packages/coding-agent/test/package-command-paths.test.ts index 03c8f4ca..5e4a1a48 100644 --- a/packages/coding-agent/test/package-command-paths.test.ts +++ b/packages/coding-agent/test/package-command-paths.test.ts @@ -2,8 +2,8 @@ import { mkdirSync, readFileSync, realpathSync, rmSync, writeFileSync } from "no import { tmpdir } from "node:os"; import { join } from "node:path"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import { ENV_AGENT_DIR, PACKAGE_NAME, VERSION } from "../src/config.js"; -import { main } from "../src/main.js"; +import { ENV_AGENT_DIR, PACKAGE_NAME, VERSION } from "../src/config.ts"; +import { main } from "../src/main.ts"; describe("package commands", () => { let tempDir: string; diff --git a/packages/coding-agent/test/package-manager-ssh.test.ts b/packages/coding-agent/test/package-manager-ssh.test.ts index 3dc04ab1..1fa9b917 100644 --- a/packages/coding-agent/test/package-manager-ssh.test.ts +++ b/packages/coding-agent/test/package-manager-ssh.test.ts @@ -2,8 +2,8 @@ import { mkdirSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { afterEach, beforeEach, describe, expect, it } from "vitest"; -import { DefaultPackageManager } from "../src/core/package-manager.js"; -import { SettingsManager } from "../src/core/settings-manager.js"; +import { DefaultPackageManager } from "../src/core/package-manager.ts"; +import { SettingsManager } from "../src/core/settings-manager.ts"; describe("Package Manager git source parsing", () => { let tempDir: string; diff --git a/packages/coding-agent/test/package-manager.test.ts b/packages/coding-agent/test/package-manager.test.ts index cc6afbc4..9466d733 100644 --- a/packages/coding-agent/test/package-manager.test.ts +++ b/packages/coding-agent/test/package-manager.test.ts @@ -4,8 +4,8 @@ import { tmpdir } from "node:os"; import { join, relative } from "node:path"; import { PassThrough } from "node:stream"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import { DefaultPackageManager, type ProgressEvent, type ResolvedResource } from "../src/core/package-manager.js"; -import { SettingsManager } from "../src/core/settings-manager.js"; +import { DefaultPackageManager, type ProgressEvent, type ResolvedResource } from "../src/core/package-manager.ts"; +import { SettingsManager } from "../src/core/settings-manager.ts"; function normalizeForMatch(value: string): string { return value.replace(/\\/g, "/"); @@ -1747,7 +1747,7 @@ Content`, // Main entry point writeFileSync( join(pkgDir, "extensions", "subagent", "index.ts"), - `import { helper } from "./agents.js"; + `import { helper } from "./agents.ts"; export default function(api) { api.registerTool({ name: "test", description: "test", execute: async () => helper() }); }`, ); // Helper module (should NOT be loaded as standalone extension) @@ -1803,7 +1803,7 @@ export default function(api) { api.registerTool({ name: "test", description: "te // Subdirectory with index.ts + helpers writeFileSync( join(pkgDir, "extensions", "complex", "index.ts"), - "import { a } from './a.js'; export default function(api) {}", + "import { a } from './a.ts'; export default function(api) {}", ); writeFileSync(join(pkgDir, "extensions", "complex", "a.ts"), "export const a = 1;"); writeFileSync(join(pkgDir, "extensions", "complex", "b.ts"), "export const b = 2;"); diff --git a/packages/coding-agent/test/path-utils.test.ts b/packages/coding-agent/test/path-utils.test.ts index 2ddf127c..85c2605f 100644 --- a/packages/coding-agent/test/path-utils.test.ts +++ b/packages/coding-agent/test/path-utils.test.ts @@ -2,7 +2,7 @@ import { mkdtempSync, readdirSync, rmdirSync, unlinkSync, writeFileSync } from " import { tmpdir } from "node:os"; import { join, resolve } from "node:path"; import { afterEach, beforeEach, describe, expect, it } from "vitest"; -import { expandPath, resolveReadPath, resolveToCwd } from "../src/core/tools/path-utils.js"; +import { expandPath, resolveReadPath, resolveToCwd } from "../src/core/tools/path-utils.ts"; describe("path-utils", () => { describe("expandPath", () => { diff --git a/packages/coding-agent/test/paths.test.ts b/packages/coding-agent/test/paths.test.ts index 208da3f9..01151635 100644 --- a/packages/coding-agent/test/paths.test.ts +++ b/packages/coding-agent/test/paths.test.ts @@ -2,7 +2,7 @@ import { mkdirSync, mkdtempSync, realpathSync, rmSync, symlinkSync, writeFileSyn import { tmpdir } from "node:os"; import { join } from "node:path"; import { afterEach, describe, expect, it } from "vitest"; -import { canonicalizePath, getCwdRelativePath, isLocalPath } from "../src/utils/paths.js"; +import { canonicalizePath, getCwdRelativePath, isLocalPath } from "../src/utils/paths.ts"; let tempDir: string; diff --git a/packages/coding-agent/test/pi-user-agent.test.ts b/packages/coding-agent/test/pi-user-agent.test.ts index 64439b21..1c49ca29 100644 --- a/packages/coding-agent/test/pi-user-agent.test.ts +++ b/packages/coding-agent/test/pi-user-agent.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "vitest"; -import { getPiUserAgent } from "../src/utils/pi-user-agent.js"; +import { getPiUserAgent } from "../src/utils/pi-user-agent.ts"; describe("getPiUserAgent", () => { it("formats the user agent expected by pi.dev", () => { diff --git a/packages/coding-agent/test/plan-mode-utils.test.ts b/packages/coding-agent/test/plan-mode-utils.test.ts index 8d71ba95..db4b31ec 100644 --- a/packages/coding-agent/test/plan-mode-utils.test.ts +++ b/packages/coding-agent/test/plan-mode-utils.test.ts @@ -6,7 +6,7 @@ import { isSafeCommand, markCompletedSteps, type TodoItem, -} from "../examples/extensions/plan-mode/utils.js"; +} from "../examples/extensions/plan-mode/utils.ts"; describe("isSafeCommand", () => { describe("safe commands", () => { diff --git a/packages/coding-agent/test/print-mode.test.ts b/packages/coding-agent/test/print-mode.test.ts index bbf6756e..30d7251d 100644 --- a/packages/coding-agent/test/print-mode.test.ts +++ b/packages/coding-agent/test/print-mode.test.ts @@ -1,7 +1,7 @@ import type { AssistantMessage, ImageContent } from "@earendil-works/pi-ai"; import { afterEach, describe, expect, it, vi } from "vitest"; -import type { SessionShutdownEvent } from "../src/index.js"; -import { runPrintMode } from "../src/modes/print-mode.js"; +import type { SessionShutdownEvent } from "../src/index.ts"; +import { runPrintMode } from "../src/modes/print-mode.ts"; type EmitEvent = SessionShutdownEvent; diff --git a/packages/coding-agent/test/prompt-templates.test.ts b/packages/coding-agent/test/prompt-templates.test.ts index 10cecaca..1e4bcf5c 100644 --- a/packages/coding-agent/test/prompt-templates.test.ts +++ b/packages/coding-agent/test/prompt-templates.test.ts @@ -12,13 +12,13 @@ import { mkdirSync, rmSync, writeFileSync } from "fs"; import { tmpdir } from "os"; import { join } from "path"; import { afterAll, describe, expect, test } from "vitest"; -import { getAgentDir } from "../src/config.js"; +import { getAgentDir } from "../src/config.ts"; import { expandPromptTemplate, loadPromptTemplates, parseCommandArgs, substituteArgs, -} from "../src/core/prompt-templates.js"; +} from "../src/core/prompt-templates.ts"; // ============================================================================ // substituteArgs diff --git a/packages/coding-agent/test/resource-loader.test.ts b/packages/coding-agent/test/resource-loader.test.ts index 01adc9b3..f023855a 100644 --- a/packages/coding-agent/test/resource-loader.test.ts +++ b/packages/coding-agent/test/resource-loader.test.ts @@ -2,14 +2,14 @@ import { mkdirSync, readFileSync, rmSync, symlinkSync, writeFileSync } from "nod import { tmpdir } from "node:os"; import { join } from "node:path"; import { afterEach, beforeEach, describe, expect, it } from "vitest"; -import { AuthStorage } from "../src/core/auth-storage.js"; -import { ExtensionRunner } from "../src/core/extensions/runner.js"; -import { ModelRegistry } from "../src/core/model-registry.js"; -import { DefaultResourceLoader } from "../src/core/resource-loader.js"; -import { SessionManager } from "../src/core/session-manager.js"; -import { SettingsManager } from "../src/core/settings-manager.js"; -import type { Skill } from "../src/core/skills.js"; -import { createSyntheticSourceInfo } from "../src/core/source-info.js"; +import { AuthStorage } from "../src/core/auth-storage.ts"; +import { ExtensionRunner } from "../src/core/extensions/runner.ts"; +import { ModelRegistry } from "../src/core/model-registry.ts"; +import { DefaultResourceLoader } from "../src/core/resource-loader.ts"; +import { SessionManager } from "../src/core/session-manager.ts"; +import { SettingsManager } from "../src/core/settings-manager.ts"; +import type { Skill } from "../src/core/skills.ts"; +import { createSyntheticSourceInfo } from "../src/core/source-info.ts"; describe("DefaultResourceLoader", () => { let tempDir: string; diff --git a/packages/coding-agent/test/restore-sandbox-env.test.ts b/packages/coding-agent/test/restore-sandbox-env.test.ts index fa9036eb..2fb2767f 100644 --- a/packages/coding-agent/test/restore-sandbox-env.test.ts +++ b/packages/coding-agent/test/restore-sandbox-env.test.ts @@ -6,7 +6,7 @@ vi.mock("node:fs", () => ({ readFileSync, })); -const { restoreSandboxEnv } = await import("../src/bun/restore-sandbox-env.js"); +const { restoreSandboxEnv } = await import("../src/bun/restore-sandbox-env.ts"); describe("restoreSandboxEnv", () => { it("does nothing when not running under bun", () => { diff --git a/packages/coding-agent/test/rpc-client-clone.test.ts b/packages/coding-agent/test/rpc-client-clone.test.ts index f05caa4a..083f20c9 100644 --- a/packages/coding-agent/test/rpc-client-clone.test.ts +++ b/packages/coding-agent/test/rpc-client-clone.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it, vi } from "vitest"; -import { RpcClient } from "../src/modes/rpc/rpc-client.js"; +import { RpcClient } from "../src/modes/rpc/rpc-client.ts"; type RpcClientPrivate = { send: (command: { type: string }) => Promise; diff --git a/packages/coding-agent/test/rpc-example.ts b/packages/coding-agent/test/rpc-example.ts index d82bd934..aaa24fb1 100644 --- a/packages/coding-agent/test/rpc-example.ts +++ b/packages/coding-agent/test/rpc-example.ts @@ -1,7 +1,7 @@ import { dirname, join } from "node:path"; import * as readline from "node:readline"; import { fileURLToPath } from "node:url"; -import { RpcClient } from "../src/modes/rpc/rpc-client.js"; +import { RpcClient } from "../src/modes/rpc/rpc-client.ts"; const __dirname = dirname(fileURLToPath(import.meta.url)); diff --git a/packages/coding-agent/test/rpc-jsonl.test.ts b/packages/coding-agent/test/rpc-jsonl.test.ts index f8ca4538..c8fc89e1 100644 --- a/packages/coding-agent/test/rpc-jsonl.test.ts +++ b/packages/coding-agent/test/rpc-jsonl.test.ts @@ -1,6 +1,6 @@ import { Readable } from "node:stream"; import { describe, expect, test } from "vitest"; -import { attachJsonlLineReader, serializeJsonLine } from "../src/modes/rpc/jsonl.js"; +import { attachJsonlLineReader, serializeJsonLine } from "../src/modes/rpc/jsonl.ts"; describe("RPC JSONL framing", () => { test("serializes strict JSONL records without escaping Unicode separators", () => { diff --git a/packages/coding-agent/test/rpc-prompt-response-semantics.test.ts b/packages/coding-agent/test/rpc-prompt-response-semantics.test.ts index 07fac54d..1504a622 100644 --- a/packages/coding-agent/test/rpc-prompt-response-semantics.test.ts +++ b/packages/coding-agent/test/rpc-prompt-response-semantics.test.ts @@ -10,14 +10,14 @@ import { type Model, } from "@earendil-works/pi-ai"; import { afterEach, describe, expect, it, vi } from "vitest"; -import { AgentSession } from "../src/core/agent-session.js"; -import type { AgentSessionRuntime } from "../src/core/agent-session-runtime.js"; -import { AuthStorage } from "../src/core/auth-storage.js"; -import { ModelRegistry } from "../src/core/model-registry.js"; -import { SessionManager } from "../src/core/session-manager.js"; -import { SettingsManager } from "../src/core/settings-manager.js"; -import { runRpcMode } from "../src/modes/rpc/rpc-mode.js"; -import { createTestResourceLoader } from "./utilities.js"; +import { AgentSession } from "../src/core/agent-session.ts"; +import type { AgentSessionRuntime } from "../src/core/agent-session-runtime.ts"; +import { AuthStorage } from "../src/core/auth-storage.ts"; +import { ModelRegistry } from "../src/core/model-registry.ts"; +import { SessionManager } from "../src/core/session-manager.ts"; +import { SettingsManager } from "../src/core/settings-manager.ts"; +import { runRpcMode } from "../src/modes/rpc/rpc-mode.ts"; +import { createTestResourceLoader } from "./utilities.ts"; const rpcIo = vi.hoisted(() => ({ outputLines: [] as string[], diff --git a/packages/coding-agent/test/rpc.test.ts b/packages/coding-agent/test/rpc.test.ts index 9d4a97d4..87447d3b 100644 --- a/packages/coding-agent/test/rpc.test.ts +++ b/packages/coding-agent/test/rpc.test.ts @@ -4,7 +4,7 @@ import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; import type { AgentEvent } from "@earendil-works/pi-agent-core"; import { afterEach, beforeEach, describe, expect, test } from "vitest"; -import { RpcClient } from "../src/modes/rpc/rpc-client.js"; +import { RpcClient } from "../src/modes/rpc/rpc-client.ts"; const __dirname = dirname(fileURLToPath(import.meta.url)); diff --git a/packages/coding-agent/test/sdk-codex-cache-probe-tool-loop.ts b/packages/coding-agent/test/sdk-codex-cache-probe-tool-loop.ts index fe349646..a6338d14 100644 --- a/packages/coding-agent/test/sdk-codex-cache-probe-tool-loop.ts +++ b/packages/coding-agent/test/sdk-codex-cache-probe-tool-loop.ts @@ -1,4 +1,4 @@ -#!/usr/bin/env tsx +#!/usr/bin/env node /** * Manual SDK probe for OpenAI Codex prompt caching through the tool loop. * @@ -24,15 +24,15 @@ import { import { getOpenAICodexWebSocketDebugStats, streamSimpleOpenAICodexResponses, -} from "../../ai/src/providers/openai-codex-responses.js"; -import { AuthStorage } from "../src/core/auth-storage.js"; -import { createExtensionRuntime } from "../src/core/extensions/loader.js"; -import type { ToolDefinition } from "../src/core/extensions/types.js"; -import { ModelRegistry } from "../src/core/model-registry.js"; -import type { ResourceLoader } from "../src/core/resource-loader.js"; -import { createAgentSession } from "../src/core/sdk.js"; -import { SessionManager } from "../src/core/session-manager.js"; -import { SettingsManager } from "../src/core/settings-manager.js"; +} from "../../ai/src/providers/openai-codex-responses.ts"; +import { AuthStorage } from "../src/core/auth-storage.ts"; +import { createExtensionRuntime } from "../src/core/extensions/loader.ts"; +import type { ToolDefinition } from "../src/core/extensions/types.ts"; +import { ModelRegistry } from "../src/core/model-registry.ts"; +import type { ResourceLoader } from "../src/core/resource-loader.ts"; +import { createAgentSession } from "../src/core/sdk.ts"; +import { SessionManager } from "../src/core/session-manager.ts"; +import { SettingsManager } from "../src/core/settings-manager.ts"; type Transport = "sse" | "websocket" | "websocket-cached" | "auto"; @@ -123,7 +123,7 @@ function parseArgs(argv: string[]): Args { } function printHelp(): void { - console.log(`Usage: npx tsx test/sdk-codex-cache-probe-tool-loop.ts [options] + console.log(`Usage: node test/sdk-codex-cache-probe-tool-loop.ts [options] Options: --turns Number of turns to run. Must be between ${MIN_TURNS} and ${MAX_TURNS}. Default: ${DEFAULT_TURNS} diff --git a/packages/coding-agent/test/sdk-openrouter-attribution.test.ts b/packages/coding-agent/test/sdk-openrouter-attribution.test.ts index e667accf..70bb8709 100644 --- a/packages/coding-agent/test/sdk-openrouter-attribution.test.ts +++ b/packages/coding-agent/test/sdk-openrouter-attribution.test.ts @@ -9,11 +9,11 @@ import { type SimpleStreamOptions, } from "@earendil-works/pi-ai"; import { afterEach, beforeEach, describe, expect, it } from "vitest"; -import { AuthStorage } from "../src/core/auth-storage.js"; -import { ModelRegistry } from "../src/core/model-registry.js"; -import { createAgentSession } from "../src/core/sdk.js"; -import { SessionManager } from "../src/core/session-manager.js"; -import { SettingsManager } from "../src/core/settings-manager.js"; +import { AuthStorage } from "../src/core/auth-storage.ts"; +import { ModelRegistry } from "../src/core/model-registry.ts"; +import { createAgentSession } from "../src/core/sdk.ts"; +import { SessionManager } from "../src/core/session-manager.ts"; +import { SettingsManager } from "../src/core/settings-manager.ts"; describe("createAgentSession OpenRouter attribution headers", () => { let tempDir: string; diff --git a/packages/coding-agent/test/sdk-session-manager.test.ts b/packages/coding-agent/test/sdk-session-manager.test.ts index 51498f50..cb7e505c 100644 --- a/packages/coding-agent/test/sdk-session-manager.test.ts +++ b/packages/coding-agent/test/sdk-session-manager.test.ts @@ -3,8 +3,8 @@ import { tmpdir } from "node:os"; import { join } from "node:path"; import { getModel } from "@earendil-works/pi-ai"; import { afterEach, beforeEach, describe, expect, it } from "vitest"; -import { createAgentSession } from "../src/core/sdk.js"; -import { SessionManager } from "../src/core/session-manager.js"; +import { createAgentSession } from "../src/core/sdk.ts"; +import { SessionManager } from "../src/core/session-manager.ts"; describe("createAgentSession session manager defaults", () => { let tempDir: string; diff --git a/packages/coding-agent/test/sdk-skills.test.ts b/packages/coding-agent/test/sdk-skills.test.ts index 5f95e67b..49ade057 100644 --- a/packages/coding-agent/test/sdk-skills.test.ts +++ b/packages/coding-agent/test/sdk-skills.test.ts @@ -2,11 +2,11 @@ import { mkdirSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { afterEach, beforeEach, describe, expect, it } from "vitest"; -import { createExtensionRuntime } from "../src/core/extensions/loader.js"; -import type { ResourceLoader } from "../src/core/resource-loader.js"; -import { createAgentSession } from "../src/core/sdk.js"; -import { SessionManager } from "../src/core/session-manager.js"; -import { createSyntheticSourceInfo } from "../src/core/source-info.js"; +import { createExtensionRuntime } from "../src/core/extensions/loader.ts"; +import type { ResourceLoader } from "../src/core/resource-loader.ts"; +import { createAgentSession } from "../src/core/sdk.ts"; +import { SessionManager } from "../src/core/session-manager.ts"; +import { createSyntheticSourceInfo } from "../src/core/source-info.ts"; describe("createAgentSession skills option", () => { let tempDir: string; diff --git a/packages/coding-agent/test/session-cwd.test.ts b/packages/coding-agent/test/session-cwd.test.ts index 72ce6014..8a7d23fc 100644 --- a/packages/coding-agent/test/session-cwd.test.ts +++ b/packages/coding-agent/test/session-cwd.test.ts @@ -2,9 +2,9 @@ import { mkdirSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { afterEach, describe, expect, it } from "vitest"; -import { type CreateAgentSessionRuntimeFactory, createAgentSessionRuntime } from "../src/core/agent-session-runtime.js"; -import { getMissingSessionCwdIssue, MissingSessionCwdError } from "../src/core/session-cwd.js"; -import { SessionManager } from "../src/core/session-manager.js"; +import { type CreateAgentSessionRuntimeFactory, createAgentSessionRuntime } from "../src/core/agent-session-runtime.ts"; +import { getMissingSessionCwdIssue, MissingSessionCwdError } from "../src/core/session-cwd.ts"; +import { SessionManager } from "../src/core/session-manager.ts"; function createTempDir(name: string): string { const dir = join(tmpdir(), `${name}-${Date.now()}-${Math.random().toString(36).slice(2)}`); diff --git a/packages/coding-agent/test/session-info-modified-timestamp.test.ts b/packages/coding-agent/test/session-info-modified-timestamp.test.ts index 1dd1cb41..a4322e46 100644 --- a/packages/coding-agent/test/session-info-modified-timestamp.test.ts +++ b/packages/coding-agent/test/session-info-modified-timestamp.test.ts @@ -3,9 +3,9 @@ import { stat } from "node:fs/promises"; import { tmpdir } from "node:os"; import { dirname, join } from "node:path"; import { afterEach, beforeAll, describe, expect, it, vi } from "vitest"; -import type { SessionHeader } from "../src/core/session-manager.js"; -import { SessionManager } from "../src/core/session-manager.js"; -import { initTheme } from "../src/modes/interactive/theme/theme.js"; +import type { SessionHeader } from "../src/core/session-manager.ts"; +import { SessionManager } from "../src/core/session-manager.ts"; +import { initTheme } from "../src/modes/interactive/theme/theme.ts"; function createSessionFile(path: string): void { const header: SessionHeader = { diff --git a/packages/coding-agent/test/session-manager/build-context.test.ts b/packages/coding-agent/test/session-manager/build-context.test.ts index 5bc336a6..a0d8c8e8 100644 --- a/packages/coding-agent/test/session-manager/build-context.test.ts +++ b/packages/coding-agent/test/session-manager/build-context.test.ts @@ -7,7 +7,7 @@ import { type SessionEntry, type SessionMessageEntry, type ThinkingLevelChangeEntry, -} from "../../src/core/session-manager.js"; +} from "../../src/core/session-manager.ts"; function msg(id: string, parentId: string | null, role: "user" | "assistant", text: string): SessionMessageEntry { const base = { type: "message" as const, id, parentId, timestamp: "2025-01-01T00:00:00Z" }; diff --git a/packages/coding-agent/test/session-manager/custom-session-id.test.ts b/packages/coding-agent/test/session-manager/custom-session-id.test.ts index 72eed465..f9c6ad0d 100644 --- a/packages/coding-agent/test/session-manager/custom-session-id.test.ts +++ b/packages/coding-agent/test/session-manager/custom-session-id.test.ts @@ -2,7 +2,7 @@ import { mkdtempSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { describe, expect, it } from "vitest"; -import { SessionManager } from "../../src/core/session-manager.js"; +import { SessionManager } from "../../src/core/session-manager.ts"; const UUID_V7_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/; diff --git a/packages/coding-agent/test/session-manager/file-operations.test.ts b/packages/coding-agent/test/session-manager/file-operations.test.ts index 16ef89e6..c744c3c3 100644 --- a/packages/coding-agent/test/session-manager/file-operations.test.ts +++ b/packages/coding-agent/test/session-manager/file-operations.test.ts @@ -2,7 +2,7 @@ import { mkdirSync, readFileSync, rmSync, writeFileSync } from "fs"; import { tmpdir } from "os"; import { join } from "path"; import { afterEach, beforeEach, describe, expect, it } from "vitest"; -import { findMostRecentSession, loadEntriesFromFile, SessionManager } from "../../src/core/session-manager.js"; +import { findMostRecentSession, loadEntriesFromFile, SessionManager } from "../../src/core/session-manager.ts"; describe("loadEntriesFromFile", () => { let tempDir: string; diff --git a/packages/coding-agent/test/session-manager/labels.test.ts b/packages/coding-agent/test/session-manager/labels.test.ts index e703abf8..7500746e 100644 --- a/packages/coding-agent/test/session-manager/labels.test.ts +++ b/packages/coding-agent/test/session-manager/labels.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "vitest"; -import { type LabelEntry, SessionManager } from "../../src/core/session-manager.js"; +import { type LabelEntry, SessionManager } from "../../src/core/session-manager.ts"; describe("SessionManager labels", () => { it("sets and gets labels", () => { diff --git a/packages/coding-agent/test/session-manager/migration.test.ts b/packages/coding-agent/test/session-manager/migration.test.ts index 1142da37..05774231 100644 --- a/packages/coding-agent/test/session-manager/migration.test.ts +++ b/packages/coding-agent/test/session-manager/migration.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "vitest"; -import { type FileEntry, migrateSessionEntries } from "../../src/core/session-manager.js"; +import { type FileEntry, migrateSessionEntries } from "../../src/core/session-manager.ts"; describe("migrateSessionEntries", () => { it("should add id/parentId to v1 entries", () => { diff --git a/packages/coding-agent/test/session-manager/save-entry.test.ts b/packages/coding-agent/test/session-manager/save-entry.test.ts index a384a074..91019b93 100644 --- a/packages/coding-agent/test/session-manager/save-entry.test.ts +++ b/packages/coding-agent/test/session-manager/save-entry.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "vitest"; -import { type CustomEntry, SessionManager } from "../../src/core/session-manager.js"; +import { type CustomEntry, SessionManager } from "../../src/core/session-manager.ts"; describe("SessionManager.saveCustomEntry", () => { it("saves custom entries and includes them in tree traversal", () => { diff --git a/packages/coding-agent/test/session-manager/tree-traversal.test.ts b/packages/coding-agent/test/session-manager/tree-traversal.test.ts index 2cfb10bc..e47ec909 100644 --- a/packages/coding-agent/test/session-manager/tree-traversal.test.ts +++ b/packages/coding-agent/test/session-manager/tree-traversal.test.ts @@ -2,8 +2,8 @@ import { existsSync, mkdirSync, readFileSync, rmSync } from "fs"; import { tmpdir } from "os"; import { join } from "path"; import { describe, expect, it } from "vitest"; -import { type CustomEntry, SessionManager } from "../../src/core/session-manager.js"; -import { assistantMsg, userMsg } from "../utilities.js"; +import { type CustomEntry, SessionManager } from "../../src/core/session-manager.ts"; +import { assistantMsg, userMsg } from "../utilities.ts"; describe("SessionManager append and tree traversal", () => { describe("append operations", () => { diff --git a/packages/coding-agent/test/session-selector-path-delete.test.ts b/packages/coding-agent/test/session-selector-path-delete.test.ts index 618ceef7..52cae486 100644 --- a/packages/coding-agent/test/session-selector-path-delete.test.ts +++ b/packages/coding-agent/test/session-selector-path-delete.test.ts @@ -3,10 +3,10 @@ import { tmpdir } from "node:os"; import { join } from "node:path"; import { setKeybindings } from "@earendil-works/pi-tui"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; -import { KeybindingsManager } from "../src/core/keybindings.js"; -import type { SessionInfo } from "../src/core/session-manager.js"; -import { SessionSelectorComponent } from "../src/modes/interactive/components/session-selector.js"; -import { initTheme } from "../src/modes/interactive/theme/theme.js"; +import { KeybindingsManager } from "../src/core/keybindings.ts"; +import type { SessionInfo } from "../src/core/session-manager.ts"; +import { SessionSelectorComponent } from "../src/modes/interactive/components/session-selector.ts"; +import { initTheme } from "../src/modes/interactive/theme/theme.ts"; type Deferred = { promise: Promise; diff --git a/packages/coding-agent/test/session-selector-rename.test.ts b/packages/coding-agent/test/session-selector-rename.test.ts index f91d6ebf..d535496c 100644 --- a/packages/coding-agent/test/session-selector-rename.test.ts +++ b/packages/coding-agent/test/session-selector-rename.test.ts @@ -1,9 +1,9 @@ import { setKeybindings } from "@earendil-works/pi-tui"; import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; -import { KeybindingsManager } from "../src/core/keybindings.js"; -import type { SessionInfo } from "../src/core/session-manager.js"; -import { SessionSelectorComponent } from "../src/modes/interactive/components/session-selector.js"; -import { initTheme } from "../src/modes/interactive/theme/theme.js"; +import { KeybindingsManager } from "../src/core/keybindings.ts"; +import type { SessionInfo } from "../src/core/session-manager.ts"; +import { SessionSelectorComponent } from "../src/modes/interactive/components/session-selector.ts"; +import { initTheme } from "../src/modes/interactive/theme/theme.ts"; async function flushPromises(): Promise { await new Promise((resolve) => { diff --git a/packages/coding-agent/test/session-selector-search.test.ts b/packages/coding-agent/test/session-selector-search.test.ts index f2ffbe1c..c1aed713 100644 --- a/packages/coding-agent/test/session-selector-search.test.ts +++ b/packages/coding-agent/test/session-selector-search.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest"; -import type { SessionInfo } from "../src/core/session-manager.js"; -import { filterAndSortSessions } from "../src/modes/interactive/components/session-selector-search.js"; +import type { SessionInfo } from "../src/core/session-manager.ts"; +import { filterAndSortSessions } from "../src/modes/interactive/components/session-selector-search.ts"; function makeSession( overrides: Partial & { id: string; modified: Date; allMessagesText: string }, diff --git a/packages/coding-agent/test/settings-manager-bug.test.ts b/packages/coding-agent/test/settings-manager-bug.test.ts index b79fd9f6..92cc3a82 100644 --- a/packages/coding-agent/test/settings-manager-bug.test.ts +++ b/packages/coding-agent/test/settings-manager-bug.test.ts @@ -1,7 +1,7 @@ import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "fs"; import { join } from "path"; import { afterEach, beforeEach, describe, expect, it } from "vitest"; -import { SettingsManager } from "../src/core/settings-manager.js"; +import { SettingsManager } from "../src/core/settings-manager.ts"; /** * Tests for the fix to a bug where external file changes to arrays were overwritten. diff --git a/packages/coding-agent/test/skills.test.ts b/packages/coding-agent/test/skills.test.ts index 36c02e87..03980199 100644 --- a/packages/coding-agent/test/skills.test.ts +++ b/packages/coding-agent/test/skills.test.ts @@ -1,9 +1,9 @@ import { homedir } from "os"; import { join, resolve } from "path"; import { describe, expect, it } from "vitest"; -import type { ResourceDiagnostic } from "../src/core/diagnostics.js"; -import { formatSkillsForPrompt, loadSkills, loadSkillsFromDir, type Skill } from "../src/core/skills.js"; -import { createSyntheticSourceInfo } from "../src/core/source-info.js"; +import type { ResourceDiagnostic } from "../src/core/diagnostics.ts"; +import { formatSkillsForPrompt, loadSkills, loadSkillsFromDir, type Skill } from "../src/core/skills.ts"; +import { createSyntheticSourceInfo } from "../src/core/source-info.ts"; const fixturesDir = resolve(__dirname, "fixtures/skills"); const collisionFixturesDir = resolve(__dirname, "fixtures/skills-collision"); diff --git a/packages/coding-agent/test/stdout-cleanliness.test.ts b/packages/coding-agent/test/stdout-cleanliness.test.ts index f9455f89..f6c426a1 100644 --- a/packages/coding-agent/test/stdout-cleanliness.test.ts +++ b/packages/coding-agent/test/stdout-cleanliness.test.ts @@ -3,10 +3,9 @@ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join, resolve } from "node:path"; import { afterEach, describe, expect, it } from "vitest"; -import { ENV_AGENT_DIR } from "../src/config.js"; +import { ENV_AGENT_DIR } from "../src/config.ts"; const cliPath = resolve(__dirname, "../src/cli.ts"); -const tsxPath = resolve(__dirname, "../../../node_modules/tsx/dist/cli.mjs"); const tempDirs: string[] = []; @@ -55,7 +54,7 @@ async function runCli(args: string[]): Promise<{ stdout: string; stderr: string; ); return await new Promise((resolvePromise, reject) => { - const child = spawn(process.execPath, [tsxPath, cliPath, ...args], { + const child = spawn(process.execPath, [cliPath, ...args], { cwd: projectDir, env: { ...process.env, diff --git a/packages/coding-agent/test/streaming-render-debug.ts b/packages/coding-agent/test/streaming-render-debug.ts index ffd4cc89..315c664d 100644 --- a/packages/coding-agent/test/streaming-render-debug.ts +++ b/packages/coding-agent/test/streaming-render-debug.ts @@ -9,8 +9,8 @@ import { ProcessTerminal, TUI } from "@earendil-works/pi-tui"; import { readFileSync } from "fs"; import { dirname, join } from "path"; import { fileURLToPath } from "url"; -import { AssistantMessageComponent } from "../src/modes/interactive/components/assistant-message.js"; -import { initTheme } from "../src/modes/interactive/theme/theme.js"; +import { AssistantMessageComponent } from "../src/modes/interactive/components/assistant-message.ts"; +import { initTheme } from "../src/modes/interactive/theme/theme.ts"; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); diff --git a/packages/coding-agent/test/suite/agent-session-bash-persistence.test.ts b/packages/coding-agent/test/suite/agent-session-bash-persistence.test.ts index f59146b4..efd51cfc 100644 --- a/packages/coding-agent/test/suite/agent-session-bash-persistence.test.ts +++ b/packages/coding-agent/test/suite/agent-session-bash-persistence.test.ts @@ -3,8 +3,8 @@ import type { AgentTool } from "@earendil-works/pi-agent-core"; import { fauxAssistantMessage, fauxToolCall } from "@earendil-works/pi-ai"; import { Type } from "typebox"; import { afterEach, describe, expect, it } from "vitest"; -import type { BashOperations } from "../../src/core/tools/bash.js"; -import { createHarness, type Harness } from "./harness.js"; +import type { BashOperations } from "../../src/core/tools/bash.ts"; +import { createHarness, type Harness } from "./harness.ts"; function getEntryTypes(harness: Harness): string[] { return harness.sessionManager.getEntries().map((entry) => entry.type); diff --git a/packages/coding-agent/test/suite/agent-session-compaction.test.ts b/packages/coding-agent/test/suite/agent-session-compaction.test.ts index 8ca5d0e5..e04732d0 100644 --- a/packages/coding-agent/test/suite/agent-session-compaction.test.ts +++ b/packages/coding-agent/test/suite/agent-session-compaction.test.ts @@ -5,7 +5,7 @@ import { type Model, } from "@earendil-works/pi-ai"; import { afterEach, describe, expect, it, vi } from "vitest"; -import { createHarness, type Harness } from "./harness.js"; +import { createHarness, type Harness } from "./harness.ts"; type SessionWithCompactionInternals = { _checkCompaction: (assistantMessage: AssistantMessage, skipAbortedCheck?: boolean) => Promise; diff --git a/packages/coding-agent/test/suite/agent-session-model-extension.test.ts b/packages/coding-agent/test/suite/agent-session-model-extension.test.ts index 55271147..f6469884 100644 --- a/packages/coding-agent/test/suite/agent-session-model-extension.test.ts +++ b/packages/coding-agent/test/suite/agent-session-model-extension.test.ts @@ -2,8 +2,8 @@ import type { AgentTool, ThinkingLevel } from "@earendil-works/pi-agent-core"; import { fauxAssistantMessage, fauxToolCall, type Model } from "@earendil-works/pi-ai"; import { Type } from "typebox"; import { afterEach, describe, expect, it } from "vitest"; -import type { ExtensionAPI } from "../../src/index.js"; -import { createHarness, getAssistantTexts, type Harness } from "./harness.js"; +import type { ExtensionAPI } from "../../src/index.ts"; +import { createHarness, getAssistantTexts, type Harness } from "./harness.ts"; describe("AgentSession model and extension characterization", () => { const harnesses: Harness[] = []; diff --git a/packages/coding-agent/test/suite/agent-session-prompt.test.ts b/packages/coding-agent/test/suite/agent-session-prompt.test.ts index e40a4fe3..dde25f61 100644 --- a/packages/coding-agent/test/suite/agent-session-prompt.test.ts +++ b/packages/coding-agent/test/suite/agent-session-prompt.test.ts @@ -5,10 +5,10 @@ import type { AgentTool } from "@earendil-works/pi-agent-core"; import { fauxAssistantMessage, fauxToolCall, type Model } from "@earendil-works/pi-ai"; import { Type } from "typebox"; import { afterEach, describe, expect, it } from "vitest"; -import type { PromptTemplate } from "../../src/core/prompt-templates.js"; -import { createSyntheticSourceInfo } from "../../src/core/source-info.js"; -import { createTestResourceLoader } from "../utilities.js"; -import { createHarness, getMessageText, type Harness } from "./harness.js"; +import type { PromptTemplate } from "../../src/core/prompt-templates.ts"; +import { createSyntheticSourceInfo } from "../../src/core/source-info.ts"; +import { createTestResourceLoader } from "../utilities.ts"; +import { createHarness, getMessageText, type Harness } from "./harness.ts"; describe("AgentSession prompt characterization", () => { const harnesses: Harness[] = []; diff --git a/packages/coding-agent/test/suite/agent-session-queue.test.ts b/packages/coding-agent/test/suite/agent-session-queue.test.ts index 06aebcf8..3db7c14b 100644 --- a/packages/coding-agent/test/suite/agent-session-queue.test.ts +++ b/packages/coding-agent/test/suite/agent-session-queue.test.ts @@ -3,7 +3,7 @@ import { fauxAssistantMessage, fauxToolCall } from "@earendil-works/pi-ai"; import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"; import { Type } from "typebox"; import { afterEach, describe, expect, it } from "vitest"; -import { createHarness, getAssistantTexts, getMessageText, getUserTexts, type Harness } from "./harness.js"; +import { createHarness, getAssistantTexts, getMessageText, getUserTexts, type Harness } from "./harness.ts"; async function createWaitingHarness( options: { diff --git a/packages/coding-agent/test/suite/agent-session-retry-events.test.ts b/packages/coding-agent/test/suite/agent-session-retry-events.test.ts index ff3e5e95..946f4da9 100644 --- a/packages/coding-agent/test/suite/agent-session-retry-events.test.ts +++ b/packages/coding-agent/test/suite/agent-session-retry-events.test.ts @@ -2,7 +2,7 @@ import type { AgentTool } from "@earendil-works/pi-agent-core"; import { fauxAssistantMessage, fauxThinking, fauxToolCall } from "@earendil-works/pi-ai"; import { Type } from "typebox"; import { afterEach, describe, expect, it } from "vitest"; -import { createHarness, type Harness } from "./harness.js"; +import { createHarness, type Harness } from "./harness.ts"; function normalizeEventOrder(events: Harness["events"]): string[] { const normalized: string[] = []; diff --git a/packages/coding-agent/test/suite/agent-session-runtime.test.ts b/packages/coding-agent/test/suite/agent-session-runtime.test.ts index fb8b2167..4d839ecf 100644 --- a/packages/coding-agent/test/suite/agent-session-runtime.test.ts +++ b/packages/coding-agent/test/suite/agent-session-runtime.test.ts @@ -8,9 +8,9 @@ import { createAgentSessionFromServices, createAgentSessionRuntime, createAgentSessionServices, -} from "../../src/core/agent-session-runtime.js"; -import { AuthStorage } from "../../src/core/auth-storage.js"; -import { SessionManager } from "../../src/core/session-manager.js"; +} from "../../src/core/agent-session-runtime.ts"; +import { AuthStorage } from "../../src/core/auth-storage.ts"; +import { SessionManager } from "../../src/core/session-manager.ts"; import type { ExtensionAPI, ExtensionFactory, @@ -18,7 +18,7 @@ import type { SessionBeforeSwitchEvent, SessionShutdownEvent, SessionStartEvent, -} from "../../src/index.js"; +} from "../../src/index.ts"; type RecordedSessionEvent = | SessionBeforeSwitchEvent diff --git a/packages/coding-agent/test/suite/harness.ts b/packages/coding-agent/test/suite/harness.ts index 16a8182e..090d5683 100644 --- a/packages/coding-agent/test/suite/harness.ts +++ b/packages/coding-agent/test/suite/harness.ts @@ -9,20 +9,20 @@ import type { AgentMessage, AgentTool } from "@earendil-works/pi-agent-core"; import { Agent } from "@earendil-works/pi-agent-core"; import type { FauxModelDefinition, FauxProviderRegistration, FauxResponseStep, Model } from "@earendil-works/pi-ai"; import { registerFauxProvider } from "@earendil-works/pi-ai"; -import { AgentSession, type AgentSessionEvent } from "../../src/core/agent-session.js"; -import { AuthStorage } from "../../src/core/auth-storage.js"; -import type { ExtensionRunner } from "../../src/core/extensions/index.js"; -import { convertToLlm } from "../../src/core/messages.js"; -import { ModelRegistry } from "../../src/core/model-registry.js"; -import { SessionManager } from "../../src/core/session-manager.js"; -import type { Settings } from "../../src/core/settings-manager.js"; -import { SettingsManager } from "../../src/core/settings-manager.js"; -import type { ExtensionFactory, ResourceLoader } from "../../src/index.js"; +import { AgentSession, type AgentSessionEvent } from "../../src/core/agent-session.ts"; +import { AuthStorage } from "../../src/core/auth-storage.ts"; +import type { ExtensionRunner } from "../../src/core/extensions/index.ts"; +import { convertToLlm } from "../../src/core/messages.ts"; +import { ModelRegistry } from "../../src/core/model-registry.ts"; +import { SessionManager } from "../../src/core/session-manager.ts"; +import type { Settings } from "../../src/core/settings-manager.ts"; +import { SettingsManager } from "../../src/core/settings-manager.ts"; +import type { ExtensionFactory, ResourceLoader } from "../../src/index.ts"; import { type CreateTestExtensionsResultInput, createTestExtensionsResult, createTestResourceLoader, -} from "../utilities.js"; +} from "../utilities.ts"; type MessageTextPart = { type: "text"; text: string }; diff --git a/packages/coding-agent/test/suite/regressions/1717-2113-agent-session-event-settlement.test.ts b/packages/coding-agent/test/suite/regressions/1717-2113-agent-session-event-settlement.test.ts index bea38c9e..e8658b1b 100644 --- a/packages/coding-agent/test/suite/regressions/1717-2113-agent-session-event-settlement.test.ts +++ b/packages/coding-agent/test/suite/regressions/1717-2113-agent-session-event-settlement.test.ts @@ -2,7 +2,7 @@ import type { AgentTool } from "@earendil-works/pi-agent-core"; import { fauxAssistantMessage, fauxToolCall } from "@earendil-works/pi-ai"; import { Type } from "typebox"; import { afterEach, describe, expect, it } from "vitest"; -import { createHarness, type Harness } from "../harness.js"; +import { createHarness, type Harness } from "../harness.ts"; function createEchoTool(): AgentTool { return { diff --git a/packages/coding-agent/test/suite/regressions/2023-queued-slash-command-followup.test.ts b/packages/coding-agent/test/suite/regressions/2023-queued-slash-command-followup.test.ts index d4adadfa..cf3f0752 100644 --- a/packages/coding-agent/test/suite/regressions/2023-queued-slash-command-followup.test.ts +++ b/packages/coding-agent/test/suite/regressions/2023-queued-slash-command-followup.test.ts @@ -3,7 +3,7 @@ import { fauxAssistantMessage, fauxToolCall } from "@earendil-works/pi-ai"; import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"; import { Type } from "typebox"; import { afterEach, describe, expect, it } from "vitest"; -import { createHarness, getAssistantTexts, getUserTexts, type Harness } from "../harness.js"; +import { createHarness, getAssistantTexts, getUserTexts, type Harness } from "../harness.ts"; describe("issue #2023 queued slash-command follow-up", () => { const harnesses: Harness[] = []; diff --git a/packages/coding-agent/test/suite/regressions/2753-reload-stale-resource-settings.test.ts b/packages/coding-agent/test/suite/regressions/2753-reload-stale-resource-settings.test.ts index a95dc7d0..17d9b302 100644 --- a/packages/coding-agent/test/suite/regressions/2753-reload-stale-resource-settings.test.ts +++ b/packages/coding-agent/test/suite/regressions/2753-reload-stale-resource-settings.test.ts @@ -8,9 +8,9 @@ import { createAgentSessionFromServices, createAgentSessionRuntime, createAgentSessionServices, -} from "../../../src/core/agent-session-runtime.js"; -import { AuthStorage } from "../../../src/core/auth-storage.js"; -import { SessionManager } from "../../../src/core/session-manager.js"; +} from "../../../src/core/agent-session-runtime.ts"; +import { AuthStorage } from "../../../src/core/auth-storage.ts"; +import { SessionManager } from "../../../src/core/session-manager.ts"; describe("issue #2753 reload stale resource settings", () => { const cleanups: Array<() => void> = []; diff --git a/packages/coding-agent/test/suite/regressions/2781-skill-collision-precedence.test.ts b/packages/coding-agent/test/suite/regressions/2781-skill-collision-precedence.test.ts index e7e17f58..cf36e771 100644 --- a/packages/coding-agent/test/suite/regressions/2781-skill-collision-precedence.test.ts +++ b/packages/coding-agent/test/suite/regressions/2781-skill-collision-precedence.test.ts @@ -2,7 +2,7 @@ import { mkdirSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { afterEach, beforeEach, describe, expect, it } from "vitest"; -import { DefaultResourceLoader } from "../../../src/core/resource-loader.js"; +import { DefaultResourceLoader } from "../../../src/core/resource-loader.ts"; describe("issue #2781 skill collision precedence: user skills should override package skills", () => { let tempDir: string; diff --git a/packages/coding-agent/test/suite/regressions/2791-fswatch-error-crash.test.ts b/packages/coding-agent/test/suite/regressions/2791-fswatch-error-crash.test.ts index 007ba580..ed450c5a 100644 --- a/packages/coding-agent/test/suite/regressions/2791-fswatch-error-crash.test.ts +++ b/packages/coding-agent/test/suite/regressions/2791-fswatch-error-crash.test.ts @@ -39,7 +39,7 @@ describe("issue #2791 fs.watch error event crashes process", () => { }); it("process should survive an error event on the theme FSWatcher", () => { - const themeModulePath = join(__dirname, "../../../src/modes/interactive/theme/theme.js").replace(/\\/g, "/"); + const themeModulePath = join(__dirname, "../../../src/modes/interactive/theme/theme.ts").replace(/\\/g, "/"); const agentDir = join(tempRoot, "agent").replace(/\\/g, "/"); // Script that sets up the watcher and emits a synthetic error on it. @@ -87,7 +87,7 @@ process.exit(0); let stderr = ""; let exitCode: number; try { - _stdout = execFileSync("npx", ["tsx", scriptPath], { + _stdout = execFileSync(process.execPath, [scriptPath], { timeout: 10000, encoding: "utf-8", env: { ...process.env, PI_CODING_AGENT_DIR: agentDir }, diff --git a/packages/coding-agent/test/suite/regressions/2835-tools-allowlist-filters-extension-tools.test.ts b/packages/coding-agent/test/suite/regressions/2835-tools-allowlist-filters-extension-tools.test.ts index 7d69c244..e0003aa6 100644 --- a/packages/coding-agent/test/suite/regressions/2835-tools-allowlist-filters-extension-tools.test.ts +++ b/packages/coding-agent/test/suite/regressions/2835-tools-allowlist-filters-extension-tools.test.ts @@ -4,10 +4,10 @@ import { join } from "node:path"; import { getModel } from "@earendil-works/pi-ai"; import { Type } from "typebox"; import { afterEach, beforeEach, describe, expect, it } from "vitest"; -import { DefaultResourceLoader } from "../../../src/core/resource-loader.js"; -import { createAgentSession } from "../../../src/core/sdk.js"; -import { SessionManager } from "../../../src/core/session-manager.js"; -import { SettingsManager } from "../../../src/core/settings-manager.js"; +import { DefaultResourceLoader } from "../../../src/core/resource-loader.ts"; +import { createAgentSession } from "../../../src/core/sdk.ts"; +import { SessionManager } from "../../../src/core/session-manager.ts"; +import { SettingsManager } from "../../../src/core/settings-manager.ts"; describe("regression #2835: tool allowlists filter extension tools", () => { let tempDir: string; diff --git a/packages/coding-agent/test/suite/regressions/2860-replaced-session-context.test.ts b/packages/coding-agent/test/suite/regressions/2860-replaced-session-context.test.ts index 21539919..0e4c061e 100644 --- a/packages/coding-agent/test/suite/regressions/2860-replaced-session-context.test.ts +++ b/packages/coding-agent/test/suite/regressions/2860-replaced-session-context.test.ts @@ -3,16 +3,16 @@ import { tmpdir } from "node:os"; import { join } from "node:path"; import { fauxAssistantMessage, registerFauxProvider } from "@earendil-works/pi-ai"; import { afterEach, describe, expect, it } from "vitest"; -import type { AgentSession } from "../../../src/core/agent-session.js"; +import type { AgentSession } from "../../../src/core/agent-session.ts"; import { type CreateAgentSessionRuntimeFactory, createAgentSessionFromServices, createAgentSessionRuntime, createAgentSessionServices, -} from "../../../src/core/agent-session-runtime.js"; -import { AuthStorage } from "../../../src/core/auth-storage.js"; -import { SessionManager } from "../../../src/core/session-manager.js"; -import type { ExtensionAPI, ExtensionCommandContext, ExtensionFactory } from "../../../src/index.js"; +} from "../../../src/core/agent-session-runtime.ts"; +import { AuthStorage } from "../../../src/core/auth-storage.ts"; +import { SessionManager } from "../../../src/core/session-manager.ts"; +import type { ExtensionAPI, ExtensionCommandContext, ExtensionFactory } from "../../../src/index.ts"; function getText(message: AgentSession["messages"][number]): string { if (!("content" in message)) { diff --git a/packages/coding-agent/test/suite/regressions/3217-scoped-model-order.test.ts b/packages/coding-agent/test/suite/regressions/3217-scoped-model-order.test.ts index e1b7cadd..b32535ba 100644 --- a/packages/coding-agent/test/suite/regressions/3217-scoped-model-order.test.ts +++ b/packages/coding-agent/test/suite/regressions/3217-scoped-model-order.test.ts @@ -1,11 +1,11 @@ import { setKeybindings, type TUI } from "@earendil-works/pi-tui"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; -import { KeybindingsManager } from "../../../src/core/keybindings.js"; -import { ModelSelectorComponent } from "../../../src/modes/interactive/components/model-selector.js"; -import { ScopedModelsSelectorComponent } from "../../../src/modes/interactive/components/scoped-models-selector.js"; -import { initTheme } from "../../../src/modes/interactive/theme/theme.js"; -import { stripAnsi } from "../../../src/utils/ansi.js"; -import { createHarness, type Harness } from "../harness.js"; +import { KeybindingsManager } from "../../../src/core/keybindings.ts"; +import { ModelSelectorComponent } from "../../../src/modes/interactive/components/model-selector.ts"; +import { ScopedModelsSelectorComponent } from "../../../src/modes/interactive/components/scoped-models-selector.ts"; +import { initTheme } from "../../../src/modes/interactive/theme/theme.ts"; +import { stripAnsi } from "../../../src/utils/ansi.ts"; +import { createHarness, type Harness } from "../harness.ts"; function createFakeTui(): TUI { return { diff --git a/packages/coding-agent/test/suite/regressions/3302-find-path-glob.test.ts b/packages/coding-agent/test/suite/regressions/3302-find-path-glob.test.ts index 05cf8008..825b4a05 100644 --- a/packages/coding-agent/test/suite/regressions/3302-find-path-glob.test.ts +++ b/packages/coding-agent/test/suite/regressions/3302-find-path-glob.test.ts @@ -2,7 +2,7 @@ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { afterEach, beforeEach, describe, expect, it } from "vitest"; -import { createFindToolDefinition } from "../../../src/core/tools/find.js"; +import { createFindToolDefinition } from "../../../src/core/tools/find.ts"; /** * Regression test for https://github.com/earendil-works/pi-mono/issues/3302 diff --git a/packages/coding-agent/test/suite/regressions/3303-find-nested-gitignore.test.ts b/packages/coding-agent/test/suite/regressions/3303-find-nested-gitignore.test.ts index d22b0c18..bd986cc9 100644 --- a/packages/coding-agent/test/suite/regressions/3303-find-nested-gitignore.test.ts +++ b/packages/coding-agent/test/suite/regressions/3303-find-nested-gitignore.test.ts @@ -2,7 +2,7 @@ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { afterEach, beforeEach, describe, expect, it } from "vitest"; -import { createFindToolDefinition } from "../../../src/core/tools/find.js"; +import { createFindToolDefinition } from "../../../src/core/tools/find.ts"; /** * Regression test for https://github.com/earendil-works/pi-mono/issues/3303 diff --git a/packages/coding-agent/test/suite/regressions/3317-network-connection-lost-retry.test.ts b/packages/coding-agent/test/suite/regressions/3317-network-connection-lost-retry.test.ts index 74276253..1be6154e 100644 --- a/packages/coding-agent/test/suite/regressions/3317-network-connection-lost-retry.test.ts +++ b/packages/coding-agent/test/suite/regressions/3317-network-connection-lost-retry.test.ts @@ -1,6 +1,6 @@ import { fauxAssistantMessage } from "@earendil-works/pi-ai"; import { afterEach, describe, expect, it } from "vitest"; -import { createHarness, getAssistantTexts, type Harness } from "../harness.js"; +import { createHarness, getAssistantTexts, type Harness } from "../harness.ts"; describe("issue #3317 network connection lost retry", () => { const harnesses: Harness[] = []; diff --git a/packages/coding-agent/test/suite/regressions/3592-no-builtin-tools-keeps-extension-tools.test.ts b/packages/coding-agent/test/suite/regressions/3592-no-builtin-tools-keeps-extension-tools.test.ts index 27b186f7..d7d1376c 100644 --- a/packages/coding-agent/test/suite/regressions/3592-no-builtin-tools-keeps-extension-tools.test.ts +++ b/packages/coding-agent/test/suite/regressions/3592-no-builtin-tools-keeps-extension-tools.test.ts @@ -7,11 +7,11 @@ import { afterEach, beforeEach, describe, expect, it } from "vitest"; import { createAgentSessionFromServices, createAgentSessionServices, -} from "../../../src/core/agent-session-services.js"; -import { DefaultResourceLoader } from "../../../src/core/resource-loader.js"; -import { createAgentSession } from "../../../src/core/sdk.js"; -import { SessionManager } from "../../../src/core/session-manager.js"; -import { SettingsManager } from "../../../src/core/settings-manager.js"; +} from "../../../src/core/agent-session-services.ts"; +import { DefaultResourceLoader } from "../../../src/core/resource-loader.ts"; +import { createAgentSession } from "../../../src/core/sdk.ts"; +import { SessionManager } from "../../../src/core/session-manager.ts"; +import { SettingsManager } from "../../../src/core/settings-manager.ts"; describe("regression #3592: no-builtin-tools keeps extension tools enabled", () => { let tempDir: string; diff --git a/packages/coding-agent/test/suite/regressions/3616-settings-inmemory-reload.test.ts b/packages/coding-agent/test/suite/regressions/3616-settings-inmemory-reload.test.ts index daa2c030..5f92baeb 100644 --- a/packages/coding-agent/test/suite/regressions/3616-settings-inmemory-reload.test.ts +++ b/packages/coding-agent/test/suite/regressions/3616-settings-inmemory-reload.test.ts @@ -2,8 +2,8 @@ import { existsSync, mkdirSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { afterEach, beforeEach, describe, expect, it } from "vitest"; -import { DefaultResourceLoader } from "../../../src/core/resource-loader.js"; -import { SettingsManager } from "../../../src/core/settings-manager.js"; +import { DefaultResourceLoader } from "../../../src/core/resource-loader.ts"; +import { SettingsManager } from "../../../src/core/settings-manager.ts"; describe("regression #3616: in-memory settings survive reload", () => { let tempDir: string; diff --git a/packages/coding-agent/test/suite/regressions/3686-session-name-event.test.ts b/packages/coding-agent/test/suite/regressions/3686-session-name-event.test.ts index 3614c09c..6265ce96 100644 --- a/packages/coding-agent/test/suite/regressions/3686-session-name-event.test.ts +++ b/packages/coding-agent/test/suite/regressions/3686-session-name-event.test.ts @@ -1,6 +1,6 @@ import { afterEach, describe, expect, it } from "vitest"; -import type { ExtensionAPI } from "../../../src/index.js"; -import { createHarness, type Harness } from "../harness.js"; +import type { ExtensionAPI } from "../../../src/index.ts"; +import { createHarness, type Harness } from "../harness.ts"; describe("regression #3686: session name changes emit an event", () => { const harnesses: Harness[] = []; diff --git a/packages/coding-agent/test/suite/regressions/3688-tree-cancel-compacting.test.ts b/packages/coding-agent/test/suite/regressions/3688-tree-cancel-compacting.test.ts index 67262e2c..7d0f172e 100644 --- a/packages/coding-agent/test/suite/regressions/3688-tree-cancel-compacting.test.ts +++ b/packages/coding-agent/test/suite/regressions/3688-tree-cancel-compacting.test.ts @@ -1,6 +1,6 @@ import { afterEach, describe, expect, it } from "vitest"; -import { assistantMsg, userMsg } from "../../utilities.js"; -import { createHarness, type Harness } from "../harness.js"; +import { assistantMsg, userMsg } from "../../utilities.ts"; +import { createHarness, type Harness } from "../harness.ts"; describe("issue #3688 tree cancellation compaction state", () => { const harnesses: Harness[] = []; diff --git a/packages/coding-agent/test/suite/regressions/3982-message-end-cost-override.test.ts b/packages/coding-agent/test/suite/regressions/3982-message-end-cost-override.test.ts index 99a47b33..757a08ed 100644 --- a/packages/coding-agent/test/suite/regressions/3982-message-end-cost-override.test.ts +++ b/packages/coding-agent/test/suite/regressions/3982-message-end-cost-override.test.ts @@ -1,6 +1,6 @@ import { fauxAssistantMessage } from "@earendil-works/pi-ai"; import { afterEach, describe, expect, it } from "vitest"; -import { createHarness, type Harness } from "../harness.js"; +import { createHarness, type Harness } from "../harness.ts"; describe("regression #3982: message_end cost override", () => { const harnesses: Harness[] = []; diff --git a/packages/coding-agent/test/suite/regressions/4167-thinking-toggle-pending-tool-render.test.ts b/packages/coding-agent/test/suite/regressions/4167-thinking-toggle-pending-tool-render.test.ts index 0dcd8a95..307c71de 100644 --- a/packages/coding-agent/test/suite/regressions/4167-thinking-toggle-pending-tool-render.test.ts +++ b/packages/coding-agent/test/suite/regressions/4167-thinking-toggle-pending-tool-render.test.ts @@ -2,12 +2,12 @@ import type { AgentMessage } from "@earendil-works/pi-agent-core"; import type { AssistantMessage, ToolResultMessage, Usage } from "@earendil-works/pi-ai"; import { Container, Text, type TUI } from "@earendil-works/pi-tui"; import { beforeAll, describe, expect, test, vi } from "vitest"; -import type { AgentSessionEvent } from "../../../src/core/agent-session.js"; -import type { SessionContext } from "../../../src/core/session-manager.js"; -import type { ToolExecutionComponent } from "../../../src/modes/interactive/components/tool-execution.js"; -import { InteractiveMode } from "../../../src/modes/interactive/interactive-mode.js"; -import { initTheme } from "../../../src/modes/interactive/theme/theme.js"; -import { stripAnsi } from "../../../src/utils/ansi.js"; +import type { AgentSessionEvent } from "../../../src/core/agent-session.ts"; +import type { SessionContext } from "../../../src/core/session-manager.ts"; +import type { ToolExecutionComponent } from "../../../src/modes/interactive/components/tool-execution.ts"; +import { InteractiveMode } from "../../../src/modes/interactive/interactive-mode.ts"; +import { initTheme } from "../../../src/modes/interactive/theme/theme.ts"; +import { stripAnsi } from "../../../src/utils/ansi.ts"; const TOOL_CALL_ID = "tool-4167"; const TOOL_NAME = "slow_tool"; diff --git a/packages/coding-agent/test/syntax-highlight.test.ts b/packages/coding-agent/test/syntax-highlight.test.ts index c5cc7ab1..92d8aa39 100644 --- a/packages/coding-agent/test/syntax-highlight.test.ts +++ b/packages/coding-agent/test/syntax-highlight.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "vitest"; -import { highlight, renderHighlightedHtml, supportsLanguage } from "../src/utils/syntax-highlight.js"; +import { highlight, renderHighlightedHtml, supportsLanguage } from "../src/utils/syntax-highlight.ts"; describe("syntax highlight renderer", () => { it("renders highlighted spans with the provided theme", () => { diff --git a/packages/coding-agent/test/system-prompt.test.ts b/packages/coding-agent/test/system-prompt.test.ts index bdc19056..82def227 100644 --- a/packages/coding-agent/test/system-prompt.test.ts +++ b/packages/coding-agent/test/system-prompt.test.ts @@ -1,5 +1,5 @@ import { describe, expect, test } from "vitest"; -import { buildSystemPrompt } from "../src/core/system-prompt.js"; +import { buildSystemPrompt } from "../src/core/system-prompt.ts"; describe("buildSystemPrompt", () => { describe("empty tools", () => { diff --git a/packages/coding-agent/test/test-harness.test.ts b/packages/coding-agent/test/test-harness.test.ts index f9cafa51..2d6e7866 100644 --- a/packages/coding-agent/test/test-harness.test.ts +++ b/packages/coding-agent/test/test-harness.test.ts @@ -7,7 +7,7 @@ import type { AgentTool } from "@earendil-works/pi-agent-core"; import type { AssistantMessage } from "@earendil-works/pi-ai"; import { Type } from "typebox"; import { afterEach, describe, expect, it } from "vitest"; -import { createHarness, createHarnessWithExtensions, type Harness } from "./test-harness.js"; +import { createHarness, createHarnessWithExtensions, type Harness } from "./test-harness.ts"; describe("test harness", () => { let harness: Harness; diff --git a/packages/coding-agent/test/test-harness.ts b/packages/coding-agent/test/test-harness.ts index 269c5146..0ae601ef 100644 --- a/packages/coding-agent/test/test-harness.ts +++ b/packages/coding-agent/test/test-harness.ts @@ -26,18 +26,18 @@ import type { Usage, } from "@earendil-works/pi-ai"; import { createAssistantMessageEventStream } from "@earendil-works/pi-ai"; -import { AgentSession, type AgentSessionEvent } from "../src/core/agent-session.js"; -import { AuthStorage } from "../src/core/auth-storage.js"; -import { ModelRegistry } from "../src/core/model-registry.js"; -import { SessionManager } from "../src/core/session-manager.js"; -import type { Settings } from "../src/core/settings-manager.js"; -import { SettingsManager } from "../src/core/settings-manager.js"; -import type { ExtensionFactory, ResourceLoader } from "../src/index.js"; +import { AgentSession, type AgentSessionEvent } from "../src/core/agent-session.ts"; +import { AuthStorage } from "../src/core/auth-storage.ts"; +import { ModelRegistry } from "../src/core/model-registry.ts"; +import { SessionManager } from "../src/core/session-manager.ts"; +import type { Settings } from "../src/core/settings-manager.ts"; +import { SettingsManager } from "../src/core/settings-manager.ts"; +import type { ExtensionFactory, ResourceLoader } from "../src/index.ts"; import { type CreateTestExtensionsResultInput, createTestExtensionsResult, createTestResourceLoader, -} from "./utilities.js"; +} from "./utilities.ts"; // ============================================================================ // Faux model diff --git a/packages/coding-agent/test/test-theme-colors.ts b/packages/coding-agent/test/test-theme-colors.ts index a3317026..70da7b18 100644 --- a/packages/coding-agent/test/test-theme-colors.ts +++ b/packages/coding-agent/test/test-theme-colors.ts @@ -1,5 +1,5 @@ import fs from "fs"; -import { initTheme, theme } from "../src/modes/interactive/theme/theme.js"; +import { initTheme, theme } from "../src/modes/interactive/theme/theme.ts"; // --- Color utilities --- diff --git a/packages/coding-agent/test/theme-detection.test.ts b/packages/coding-agent/test/theme-detection.test.ts index 582358bb..7bb280b2 100644 --- a/packages/coding-agent/test/theme-detection.test.ts +++ b/packages/coding-agent/test/theme-detection.test.ts @@ -5,7 +5,7 @@ import { getThemeByName, getThemeForRgbColor, parseOsc11BackgroundColor, -} from "../src/modes/interactive/theme/theme.js"; +} from "../src/modes/interactive/theme/theme.ts"; afterEach(() => { resetCapabilitiesCache(); diff --git a/packages/coding-agent/test/theme-export.test.ts b/packages/coding-agent/test/theme-export.test.ts index 84dd649a..155a07a4 100644 --- a/packages/coding-agent/test/theme-export.test.ts +++ b/packages/coding-agent/test/theme-export.test.ts @@ -2,7 +2,7 @@ import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "fs" import { tmpdir } from "os"; import { join } from "path"; import { afterEach, beforeEach, describe, expect, it } from "vitest"; -import { getThemeExportColors } from "../src/modes/interactive/theme/theme.js"; +import { getThemeExportColors } from "../src/modes/interactive/theme/theme.ts"; type ThemeFile = { name: string; diff --git a/packages/coding-agent/test/tool-execution-component.test.ts b/packages/coding-agent/test/tool-execution-component.test.ts index 7013ba2a..54c75806 100644 --- a/packages/coding-agent/test/tool-execution-component.test.ts +++ b/packages/coding-agent/test/tool-execution-component.test.ts @@ -2,14 +2,14 @@ import { join, resolve } from "node:path"; import { Text, type TUI } from "@earendil-works/pi-tui"; import { Type } from "typebox"; import { beforeAll, describe, expect, test } from "vitest"; -import { getReadmePath } from "../src/config.js"; -import type { ToolDefinition } from "../src/core/extensions/types.js"; -import { type BashOperations, createBashToolDefinition } from "../src/core/tools/bash.js"; -import { createReadTool, createReadToolDefinition } from "../src/core/tools/read.js"; -import { createWriteToolDefinition } from "../src/core/tools/write.js"; -import { ToolExecutionComponent } from "../src/modes/interactive/components/tool-execution.js"; -import { initTheme } from "../src/modes/interactive/theme/theme.js"; -import { stripAnsi } from "../src/utils/ansi.js"; +import { getReadmePath } from "../src/config.ts"; +import type { ToolDefinition } from "../src/core/extensions/types.ts"; +import { type BashOperations, createBashToolDefinition } from "../src/core/tools/bash.ts"; +import { createReadTool, createReadToolDefinition } from "../src/core/tools/read.ts"; +import { createWriteToolDefinition } from "../src/core/tools/write.ts"; +import { ToolExecutionComponent } from "../src/modes/interactive/components/tool-execution.ts"; +import { initTheme } from "../src/modes/interactive/theme/theme.ts"; +import { stripAnsi } from "../src/utils/ansi.ts"; function createBaseToolDefinition(name = "custom_tool"): ToolDefinition { return { diff --git a/packages/coding-agent/test/tools.test.ts b/packages/coding-agent/test/tools.test.ts index 70e4e54f..70d4ec2d 100644 --- a/packages/coding-agent/test/tools.test.ts +++ b/packages/coding-agent/test/tools.test.ts @@ -2,9 +2,9 @@ import { chmodSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } import { tmpdir } from "os"; import { join } from "path"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import { executeBashWithOperations } from "../src/core/bash-executor.js"; -import { type BashOperations, createBashTool, createLocalBashOperations } from "../src/core/tools/bash.js"; -import { computeEditsDiff } from "../src/core/tools/edit-diff.js"; +import { executeBashWithOperations } from "../src/core/bash-executor.ts"; +import { type BashOperations, createBashTool, createLocalBashOperations } from "../src/core/tools/bash.ts"; +import { computeEditsDiff } from "../src/core/tools/edit-diff.ts"; import { createEditTool, createFindTool, @@ -12,8 +12,8 @@ import { createLsTool, createReadTool, createWriteTool, -} from "../src/index.js"; -import * as shellModule from "../src/utils/shell.js"; +} from "../src/index.ts"; +import * as shellModule from "../src/utils/shell.ts"; const readTool = createReadTool(process.cwd()); const writeTool = createWriteTool(process.cwd()); diff --git a/packages/coding-agent/test/tree-selector.test.ts b/packages/coding-agent/test/tree-selector.test.ts index 3b29d7e0..8f423dc5 100644 --- a/packages/coding-agent/test/tree-selector.test.ts +++ b/packages/coding-agent/test/tree-selector.test.ts @@ -1,14 +1,14 @@ import { setKeybindings } from "@earendil-works/pi-tui"; import { beforeAll, beforeEach, describe, expect, test } from "vitest"; -import { KeybindingsManager } from "../src/core/keybindings.js"; +import { KeybindingsManager } from "../src/core/keybindings.ts"; import type { ModelChangeEntry, SessionEntry, SessionMessageEntry, SessionTreeNode, -} from "../src/core/session-manager.js"; -import { TreeSelectorComponent } from "../src/modes/interactive/components/tree-selector.js"; -import { initTheme } from "../src/modes/interactive/theme/theme.js"; +} from "../src/core/session-manager.ts"; +import { TreeSelectorComponent } from "../src/modes/interactive/components/tree-selector.ts"; +import { initTheme } from "../src/modes/interactive/theme/theme.ts"; beforeAll(() => { initTheme("dark"); diff --git a/packages/coding-agent/test/trigger-compact-extension.test.ts b/packages/coding-agent/test/trigger-compact-extension.test.ts index 92d2ce95..a8bdbb2a 100644 --- a/packages/coding-agent/test/trigger-compact-extension.test.ts +++ b/packages/coding-agent/test/trigger-compact-extension.test.ts @@ -1,6 +1,6 @@ import { describe, expect, test, vi } from "vitest"; -import triggerCompactExtension from "../examples/extensions/trigger-compact.js"; -import type { ExtensionAPI, ExtensionCommandContext, ExtensionContext } from "../src/core/extensions/index.js"; +import triggerCompactExtension from "../examples/extensions/trigger-compact.ts"; +import type { ExtensionAPI, ExtensionCommandContext, ExtensionContext } from "../src/core/extensions/index.ts"; function createContext(tokens: number | null, compact = vi.fn()): ExtensionContext { return { diff --git a/packages/coding-agent/test/user-message.test.ts b/packages/coding-agent/test/user-message.test.ts index 7c756810..1e62e5a8 100644 --- a/packages/coding-agent/test/user-message.test.ts +++ b/packages/coding-agent/test/user-message.test.ts @@ -1,6 +1,6 @@ import { describe, expect, test } from "vitest"; -import { UserMessageComponent } from "../src/modes/interactive/components/user-message.js"; -import { initTheme } from "../src/modes/interactive/theme/theme.js"; +import { UserMessageComponent } from "../src/modes/interactive/components/user-message.ts"; +import { initTheme } from "../src/modes/interactive/theme/theme.ts"; const OSC133_ZONE_START = "\x1b]133;A\x07"; const OSC133_ZONE_END = "\x1b]133;B\x07"; diff --git a/packages/coding-agent/test/utilities.ts b/packages/coding-agent/test/utilities.ts index 058ce889..5ebf6813 100644 --- a/packages/coding-agent/test/utilities.ts +++ b/packages/coding-agent/test/utilities.ts @@ -8,16 +8,16 @@ import { dirname, join } from "node:path"; import { Agent } from "@earendil-works/pi-agent-core"; import { getModel, type OAuthCredentials, type OAuthProvider } from "@earendil-works/pi-ai"; import { getOAuthApiKey } from "@earendil-works/pi-ai/oauth"; -import { AgentSession } from "../src/core/agent-session.js"; -import { AuthStorage } from "../src/core/auth-storage.js"; -import { createEventBus } from "../src/core/event-bus.js"; -import type { Extension, ExtensionFactory, LoadExtensionsResult } from "../src/core/extensions/index.js"; -import { createExtensionRuntime, loadExtensionFromFactory } from "../src/core/extensions/loader.js"; -import { ModelRegistry } from "../src/core/model-registry.js"; -import type { ResourceLoader } from "../src/core/resource-loader.js"; -import { SessionManager } from "../src/core/session-manager.js"; -import { SettingsManager } from "../src/core/settings-manager.js"; -import { createCodingTools } from "../src/index.js"; +import { AgentSession } from "../src/core/agent-session.ts"; +import { AuthStorage } from "../src/core/auth-storage.ts"; +import { createEventBus } from "../src/core/event-bus.ts"; +import type { Extension, ExtensionFactory, LoadExtensionsResult } from "../src/core/extensions/index.ts"; +import { createExtensionRuntime, loadExtensionFromFactory } from "../src/core/extensions/loader.ts"; +import { ModelRegistry } from "../src/core/model-registry.ts"; +import type { ResourceLoader } from "../src/core/resource-loader.ts"; +import { SessionManager } from "../src/core/session-manager.ts"; +import { SettingsManager } from "../src/core/settings-manager.ts"; +import { createCodingTools } from "../src/index.ts"; /** * API key for authenticated tests. Tests using this should be wrapped in diff --git a/packages/coding-agent/test/version-check.test.ts b/packages/coding-agent/test/version-check.test.ts index f05bfd2e..70197fa5 100644 --- a/packages/coding-agent/test/version-check.test.ts +++ b/packages/coding-agent/test/version-check.test.ts @@ -5,7 +5,7 @@ import { getLatestPiRelease, getLatestPiVersion, isNewerPackageVersion, -} from "../src/utils/version-check.js"; +} from "../src/utils/version-check.ts"; const originalSkipVersionCheck = process.env.PI_SKIP_VERSION_CHECK; const originalOffline = process.env.PI_OFFLINE; diff --git a/packages/tui/package.json b/packages/tui/package.json index fd678612..8248d33f 100644 --- a/packages/tui/package.json +++ b/packages/tui/package.json @@ -7,7 +7,7 @@ "scripts": { "clean": "shx rm -rf dist", "build": "tsgo -p tsconfig.build.json", - "test": "node --test --import tsx test/*.test.ts", + "test": "node --test test/*.test.ts", "prepublishOnly": "npm run clean && npm run build" }, "files": [ @@ -35,15 +35,15 @@ }, "types": "./dist/index.d.ts", "dependencies": { - "get-east-asian-width": "^1.3.0", - "marked": "^15.0.12" + "get-east-asian-width": "1.6.0", + "marked": "15.0.12" }, "optionalDependencies": { - "koffi": "^2.9.0" + "koffi": "2.16.2" }, "devDependencies": { - "@xterm/headless": "^5.5.0", - "@xterm/xterm": "^5.5.0", - "chalk": "^5.5.0" + "@xterm/headless": "5.5.0", + "@xterm/xterm": "5.5.0", + "chalk": "5.6.2" } } diff --git a/packages/tui/test/autocomplete.test.ts b/packages/tui/test/autocomplete.test.ts index 3e5c8240..be27d152 100644 --- a/packages/tui/test/autocomplete.test.ts +++ b/packages/tui/test/autocomplete.test.ts @@ -4,7 +4,7 @@ import { mkdirSync, mkdtempSync, rmSync, symlinkSync, writeFileSync } from "node import { tmpdir } from "node:os"; import { dirname, join } from "node:path"; import { afterEach, beforeEach, describe, it, test } from "node:test"; -import { CombinedAutocompleteProvider } from "../src/autocomplete.js"; +import { CombinedAutocompleteProvider } from "../src/autocomplete.ts"; const resolveFdPath = (): string | null => { const command = process.platform === "win32" ? "where" : "which"; diff --git a/packages/tui/test/bug-regression-isimageline-startswith-bug.test.ts b/packages/tui/test/bug-regression-isimageline-startswith-bug.test.ts index ff15d18f..b3d25e83 100644 --- a/packages/tui/test/bug-regression-isimageline-startswith-bug.test.ts +++ b/packages/tui/test/bug-regression-isimageline-startswith-bug.test.ts @@ -56,7 +56,7 @@ describe("Bug regression: isImageLine() crash with image escape sequences", () = }); it("new implementation returns true correctly", async () => { - const { isImageLine } = await import("../src/terminal-image.js"); + const { isImageLine } = await import("../src/terminal-image.ts"); // Line containing image escape sequence with text before it const lineWithImageSequence = @@ -68,7 +68,7 @@ describe("Bug regression: isImageLine() crash with image escape sequences", () = }); it("new implementation detects Kitty sequences in any position", async () => { - const { isImageLine } = await import("../src/terminal-image.js"); + const { isImageLine } = await import("../src/terminal-image.ts"); const scenarios = [ "At start: \x1b_Ga=T,f=100,data...\x1b\\", @@ -85,7 +85,7 @@ describe("Bug regression: isImageLine() crash with image escape sequences", () = }); it("new implementation detects iTerm2 sequences in any position", async () => { - const { isImageLine } = await import("../src/terminal-image.js"); + const { isImageLine } = await import("../src/terminal-image.ts"); const scenarios = [ "At start: \x1b]1337;File=size=100,100:base64...\x07", @@ -121,7 +121,7 @@ describe("Bug regression: isImageLine() crash with image escape sequences", () = */ it("detects image sequences in read tool output", async () => { - const { isImageLine } = await import("../src/terminal-image.js"); + const { isImageLine } = await import("../src/terminal-image.ts"); // Simulate output when read tool processes an image // The line might have text from the read result plus the image escape sequence @@ -131,7 +131,7 @@ describe("Bug regression: isImageLine() crash with image escape sequences", () = }); it("detects Kitty sequences from Image component", async () => { - const { isImageLine } = await import("../src/terminal-image.js"); + const { isImageLine } = await import("../src/terminal-image.ts"); // Kitty image component creates multi-line output with escape sequences const kittyLine = "\x1b_Ga=T,f=100,t=f,d=base64data...\x1b\\\x1b_Gm=i=1;\x1b\\"; @@ -140,7 +140,7 @@ describe("Bug regression: isImageLine() crash with image escape sequences", () = }); it("handles ANSI codes before image sequences", async () => { - const { isImageLine } = await import("../src/terminal-image.js"); + const { isImageLine } = await import("../src/terminal-image.ts"); // Line might have styling (error, warning, etc.) before image data const lines = [ @@ -161,7 +161,7 @@ describe("Bug regression: isImageLine() crash with image escape sequences", () = describe("Crash scenario simulation", () => { it("does NOT crash on very long lines with image sequences", async () => { - const { isImageLine } = await import("../src/terminal-image.js"); + const { isImageLine } = await import("../src/terminal-image.ts"); /** * Simulate the exact crash scenario: @@ -190,7 +190,7 @@ describe("Bug regression: isImageLine() crash with image escape sequences", () = }); it("handles lines exactly matching crash log dimensions", async () => { - const { isImageLine } = await import("../src/terminal-image.js"); + const { isImageLine } = await import("../src/terminal-image.ts"); /** * Crash log showed: line 58649 chars wide, terminal width 115 @@ -211,7 +211,7 @@ describe("Bug regression: isImageLine() crash with image escape sequences", () = describe("Negative cases: Don't false positive", () => { it("does not detect images in regular long text", async () => { - const { isImageLine } = await import("../src/terminal-image.js"); + const { isImageLine } = await import("../src/terminal-image.ts"); // Very long line WITHOUT image sequences const longText = "A".repeat(100000); @@ -220,7 +220,7 @@ describe("Bug regression: isImageLine() crash with image escape sequences", () = }); it("does not detect images in lines with file paths", async () => { - const { isImageLine } = await import("../src/terminal-image.js"); + const { isImageLine } = await import("../src/terminal-image.ts"); const filePaths = [ "/path/to/1337/image.jpg", diff --git a/packages/tui/test/chat-simple.ts b/packages/tui/test/chat-simple.ts index 836d1f53..b6ccd1a8 100644 --- a/packages/tui/test/chat-simple.ts +++ b/packages/tui/test/chat-simple.ts @@ -3,14 +3,14 @@ */ import chalk from "chalk"; -import { CombinedAutocompleteProvider } from "../src/autocomplete.js"; -import { Editor } from "../src/components/editor.js"; -import { Loader } from "../src/components/loader.js"; -import { Markdown } from "../src/components/markdown.js"; -import { Text } from "../src/components/text.js"; -import { ProcessTerminal } from "../src/terminal.js"; -import { TUI } from "../src/tui.js"; -import { defaultEditorTheme, defaultMarkdownTheme } from "./test-themes.js"; +import { CombinedAutocompleteProvider } from "../src/autocomplete.ts"; +import { Editor } from "../src/components/editor.ts"; +import { Loader } from "../src/components/loader.ts"; +import { Markdown } from "../src/components/markdown.ts"; +import { Text } from "../src/components/text.ts"; +import { ProcessTerminal } from "../src/terminal.ts"; +import { TUI } from "../src/tui.ts"; +import { defaultEditorTheme, defaultMarkdownTheme } from "./test-themes.ts"; // Create terminal const terminal = new ProcessTerminal(); diff --git a/packages/tui/test/editor.test.ts b/packages/tui/test/editor.test.ts index 36f7ee7b..e7e74ec3 100644 --- a/packages/tui/test/editor.test.ts +++ b/packages/tui/test/editor.test.ts @@ -1,12 +1,12 @@ import assert from "node:assert"; import { describe, it } from "node:test"; import { stripVTControlCharacters } from "node:util"; -import { type AutocompleteProvider, CombinedAutocompleteProvider } from "../src/autocomplete.js"; -import { Editor, wordWrapLine } from "../src/components/editor.js"; -import { TUI } from "../src/tui.js"; -import { visibleWidth } from "../src/utils.js"; -import { defaultEditorTheme } from "./test-themes.js"; -import { VirtualTerminal } from "./virtual-terminal.js"; +import { type AutocompleteProvider, CombinedAutocompleteProvider } from "../src/autocomplete.ts"; +import { Editor, wordWrapLine } from "../src/components/editor.ts"; +import { TUI } from "../src/tui.ts"; +import { visibleWidth } from "../src/utils.ts"; +import { defaultEditorTheme } from "./test-themes.ts"; +import { VirtualTerminal } from "./virtual-terminal.ts"; /** Create a TUI with a virtual terminal for testing */ function createTestTUI(cols = 80, rows = 24): TUI { diff --git a/packages/tui/test/fuzzy.test.ts b/packages/tui/test/fuzzy.test.ts index e784e49c..7415e963 100644 --- a/packages/tui/test/fuzzy.test.ts +++ b/packages/tui/test/fuzzy.test.ts @@ -1,6 +1,6 @@ import assert from "node:assert"; import { describe, it } from "node:test"; -import { fuzzyFilter, fuzzyMatch } from "../src/fuzzy.js"; +import { fuzzyFilter, fuzzyMatch } from "../src/fuzzy.ts"; describe("fuzzyMatch", () => { it("empty query matches everything with score 0", () => { diff --git a/packages/tui/test/image-test.ts b/packages/tui/test/image-test.ts index 6a29d16d..116c97d0 100644 --- a/packages/tui/test/image-test.ts +++ b/packages/tui/test/image-test.ts @@ -1,10 +1,10 @@ import { readFileSync } from "fs"; -import { Image } from "../src/components/image.js"; -import { Spacer } from "../src/components/spacer.js"; -import { Text } from "../src/components/text.js"; -import { ProcessTerminal } from "../src/terminal.js"; -import { getCapabilities, getImageDimensions } from "../src/terminal-image.js"; -import { TUI } from "../src/tui.js"; +import { Image } from "../src/components/image.ts"; +import { Spacer } from "../src/components/spacer.ts"; +import { Text } from "../src/components/text.ts"; +import { ProcessTerminal } from "../src/terminal.ts"; +import { getCapabilities, getImageDimensions } from "../src/terminal-image.ts"; +import { TUI } from "../src/tui.ts"; const testImagePath = process.argv[2] || "/tmp/test-image.png"; diff --git a/packages/tui/test/input.test.ts b/packages/tui/test/input.test.ts index 3edd73ca..e980fb05 100644 --- a/packages/tui/test/input.test.ts +++ b/packages/tui/test/input.test.ts @@ -1,7 +1,7 @@ import assert from "node:assert"; import { describe, it } from "node:test"; -import { Input } from "../src/components/input.js"; -import { visibleWidth } from "../src/utils.js"; +import { Input } from "../src/components/input.ts"; +import { visibleWidth } from "../src/utils.ts"; describe("Input component", () => { it("submits value including backslash on Enter", () => { diff --git a/packages/tui/test/key-tester.ts b/packages/tui/test/key-tester.ts index 443ab3ac..650b98cb 100755 --- a/packages/tui/test/key-tester.ts +++ b/packages/tui/test/key-tester.ts @@ -1,7 +1,7 @@ #!/usr/bin/env node -import { matchesKey } from "../src/keys.js"; -import { ProcessTerminal } from "../src/terminal.js"; -import { type Component, TUI } from "../src/tui.js"; +import { matchesKey } from "../src/keys.ts"; +import { ProcessTerminal } from "../src/terminal.ts"; +import { type Component, TUI } from "../src/tui.ts"; /** * Simple key code logger component diff --git a/packages/tui/test/keybindings.test.ts b/packages/tui/test/keybindings.test.ts index 7e255b0a..37c67442 100644 --- a/packages/tui/test/keybindings.test.ts +++ b/packages/tui/test/keybindings.test.ts @@ -1,6 +1,6 @@ import assert from "node:assert"; import { describe, it } from "node:test"; -import { KeybindingsManager, TUI_KEYBINDINGS } from "../src/keybindings.js"; +import { KeybindingsManager, TUI_KEYBINDINGS } from "../src/keybindings.ts"; describe("KeybindingsManager", () => { it("does not evict selector confirm when input submit is rebound", () => { diff --git a/packages/tui/test/keys.test.ts b/packages/tui/test/keys.test.ts index 392214cd..e06844e4 100644 --- a/packages/tui/test/keys.test.ts +++ b/packages/tui/test/keys.test.ts @@ -11,7 +11,7 @@ import { matchesKey, parseKey, setKittyProtocolActive, -} from "../src/keys.js"; +} from "../src/keys.ts"; function withEnv(name: string, value: string | undefined, fn: () => void): void { const previous = process.env[name]; diff --git a/packages/tui/test/markdown.test.ts b/packages/tui/test/markdown.test.ts index 2e1ed022..584c583c 100644 --- a/packages/tui/test/markdown.test.ts +++ b/packages/tui/test/markdown.test.ts @@ -2,11 +2,11 @@ import assert from "node:assert"; import { afterEach, describe, it } from "node:test"; import type { Terminal as XtermTerminalType } from "@xterm/headless"; import { Chalk } from "chalk"; -import { Markdown } from "../src/components/markdown.js"; -import { resetCapabilitiesCache, setCapabilities } from "../src/terminal-image.js"; -import { type Component, TUI } from "../src/tui.js"; -import { defaultMarkdownTheme } from "./test-themes.js"; -import { VirtualTerminal } from "./virtual-terminal.js"; +import { Markdown } from "../src/components/markdown.ts"; +import { resetCapabilitiesCache, setCapabilities } from "../src/terminal-image.ts"; +import { type Component, TUI } from "../src/tui.ts"; +import { defaultMarkdownTheme } from "./test-themes.ts"; +import { VirtualTerminal } from "./virtual-terminal.ts"; // Force full color in CI so ANSI assertions are deterministic const chalk = new Chalk({ level: 3 }); diff --git a/packages/tui/test/overlay-non-capturing.test.ts b/packages/tui/test/overlay-non-capturing.test.ts index a0a7a676..cb91e5e4 100644 --- a/packages/tui/test/overlay-non-capturing.test.ts +++ b/packages/tui/test/overlay-non-capturing.test.ts @@ -1,8 +1,8 @@ import assert from "node:assert"; import { describe, it } from "node:test"; -import type { Component, Focusable } from "../src/tui.js"; -import { TUI } from "../src/tui.js"; -import { VirtualTerminal } from "./virtual-terminal.js"; +import type { Component, Focusable } from "../src/tui.ts"; +import { TUI } from "../src/tui.ts"; +import { VirtualTerminal } from "./virtual-terminal.ts"; class StaticOverlay implements Component { private lines: string[]; diff --git a/packages/tui/test/overlay-options.test.ts b/packages/tui/test/overlay-options.test.ts index 942bddd3..c93f26cb 100644 --- a/packages/tui/test/overlay-options.test.ts +++ b/packages/tui/test/overlay-options.test.ts @@ -1,8 +1,8 @@ import assert from "node:assert"; import { describe, it } from "node:test"; -import type { Component } from "../src/tui.js"; -import { TUI } from "../src/tui.js"; -import { VirtualTerminal } from "./virtual-terminal.js"; +import type { Component } from "../src/tui.ts"; +import { TUI } from "../src/tui.ts"; +import { VirtualTerminal } from "./virtual-terminal.ts"; class StaticOverlay implements Component { private lines: string[]; diff --git a/packages/tui/test/overlay-short-content.test.ts b/packages/tui/test/overlay-short-content.test.ts index 7721ea00..135d8cb9 100644 --- a/packages/tui/test/overlay-short-content.test.ts +++ b/packages/tui/test/overlay-short-content.test.ts @@ -1,7 +1,7 @@ import assert from "node:assert"; import { describe, it } from "node:test"; -import { type Component, TUI } from "../src/tui.js"; -import { VirtualTerminal } from "./virtual-terminal.js"; +import { type Component, TUI } from "../src/tui.ts"; +import { VirtualTerminal } from "./virtual-terminal.ts"; class SimpleContent implements Component { private lines: string[]; diff --git a/packages/tui/test/regression-regional-indicator-width.test.ts b/packages/tui/test/regression-regional-indicator-width.test.ts index 675ce97f..6803e2b1 100644 --- a/packages/tui/test/regression-regional-indicator-width.test.ts +++ b/packages/tui/test/regression-regional-indicator-width.test.ts @@ -1,6 +1,6 @@ import assert from "node:assert"; import { describe, it } from "node:test"; -import { visibleWidth, wrapTextWithAnsi } from "../src/utils.js"; +import { visibleWidth, wrapTextWithAnsi } from "../src/utils.ts"; describe("regional indicator width regression", () => { it("treats partial flag grapheme as full-width to avoid streaming render drift", () => { diff --git a/packages/tui/test/select-list.test.ts b/packages/tui/test/select-list.test.ts index 053690fe..b5c24212 100644 --- a/packages/tui/test/select-list.test.ts +++ b/packages/tui/test/select-list.test.ts @@ -1,7 +1,7 @@ import assert from "node:assert"; import { describe, it } from "node:test"; -import { SelectList } from "../src/components/select-list.js"; -import { visibleWidth } from "../src/utils.js"; +import { SelectList } from "../src/components/select-list.ts"; +import { visibleWidth } from "../src/utils.ts"; const testTheme = { selectedPrefix: (text: string) => text, diff --git a/packages/tui/test/stdin-buffer.test.ts b/packages/tui/test/stdin-buffer.test.ts index 4f4a0061..e72c1496 100644 --- a/packages/tui/test/stdin-buffer.test.ts +++ b/packages/tui/test/stdin-buffer.test.ts @@ -7,7 +7,7 @@ import assert from "node:assert"; import { beforeEach, describe, it } from "node:test"; -import { StdinBuffer } from "../src/stdin-buffer.js"; +import { StdinBuffer } from "../src/stdin-buffer.ts"; describe("StdinBuffer", () => { let buffer: StdinBuffer; diff --git a/packages/tui/test/terminal-image.test.ts b/packages/tui/test/terminal-image.test.ts index 4f7c238b..70fbf9eb 100644 --- a/packages/tui/test/terminal-image.test.ts +++ b/packages/tui/test/terminal-image.test.ts @@ -4,7 +4,7 @@ import assert from "node:assert"; import { describe, it } from "node:test"; -import { Image } from "../src/components/image.js"; +import { Image } from "../src/components/image.ts"; import { deleteAllKittyImages, deleteKittyImage, @@ -16,7 +16,7 @@ import { resetCapabilitiesCache, setCapabilities, setCellDimensions, -} from "../src/terminal-image.js"; +} from "../src/terminal-image.ts"; const ENV_KEYS = [ "TERM", diff --git a/packages/tui/test/terminal.test.ts b/packages/tui/test/terminal.test.ts index 7099b3df..ab2027df 100644 --- a/packages/tui/test/terminal.test.ts +++ b/packages/tui/test/terminal.test.ts @@ -1,6 +1,6 @@ import assert from "node:assert"; import { describe, it } from "node:test"; -import { ProcessTerminal } from "../src/terminal.js"; +import { ProcessTerminal } from "../src/terminal.ts"; describe("ProcessTerminal dimensions", () => { it("falls back to COLUMNS and LINES before default dimensions", () => { diff --git a/packages/tui/test/test-themes.ts b/packages/tui/test/test-themes.ts index ff68b2bb..33e3c39e 100644 --- a/packages/tui/test/test-themes.ts +++ b/packages/tui/test/test-themes.ts @@ -3,7 +3,7 @@ */ import { Chalk } from "chalk"; -import type { EditorTheme, MarkdownTheme, SelectListTheme } from "../src/index.js"; +import type { EditorTheme, MarkdownTheme, SelectListTheme } from "../src/index.ts"; const chalk = new Chalk({ level: 3 }); diff --git a/packages/tui/test/truncate-to-width.test.ts b/packages/tui/test/truncate-to-width.test.ts index 42d93ffa..321ba898 100644 --- a/packages/tui/test/truncate-to-width.test.ts +++ b/packages/tui/test/truncate-to-width.test.ts @@ -1,6 +1,6 @@ import assert from "node:assert"; import { describe, it } from "node:test"; -import { normalizeTerminalOutput, truncateToWidth, visibleWidth } from "../src/utils.js"; +import { normalizeTerminalOutput, truncateToWidth, visibleWidth } from "../src/utils.ts"; describe("truncateToWidth", () => { it("keeps output within width for very large unicode input", () => { diff --git a/packages/tui/test/truncated-text.test.ts b/packages/tui/test/truncated-text.test.ts index ac139179..a25034e2 100644 --- a/packages/tui/test/truncated-text.test.ts +++ b/packages/tui/test/truncated-text.test.ts @@ -1,8 +1,8 @@ import assert from "node:assert"; import { describe, it } from "node:test"; import { Chalk } from "chalk"; -import { TruncatedText } from "../src/components/truncated-text.js"; -import { visibleWidth } from "../src/utils.js"; +import { TruncatedText } from "../src/components/truncated-text.ts"; +import { visibleWidth } from "../src/utils.ts"; // Force full color in CI so ANSI assertions are deterministic const chalk = new Chalk({ level: 3 }); diff --git a/packages/tui/test/tui-cell-size-input.test.ts b/packages/tui/test/tui-cell-size-input.test.ts index 0ca515d4..fab915ca 100644 --- a/packages/tui/test/tui-cell-size-input.test.ts +++ b/packages/tui/test/tui-cell-size-input.test.ts @@ -1,8 +1,8 @@ import assert from "node:assert"; import { describe, it } from "node:test"; -import { getCellDimensions, resetCapabilitiesCache, setCellDimensions } from "../src/terminal-image.js"; -import { type Component, TUI } from "../src/tui.js"; -import { VirtualTerminal } from "./virtual-terminal.js"; +import { getCellDimensions, resetCapabilitiesCache, setCellDimensions } from "../src/terminal-image.ts"; +import { type Component, TUI } from "../src/tui.ts"; +import { VirtualTerminal } from "./virtual-terminal.ts"; class InputRecorder implements Component { readonly inputs: string[] = []; diff --git a/packages/tui/test/tui-overlay-style-leak.test.ts b/packages/tui/test/tui-overlay-style-leak.test.ts index df93deef..a44b699e 100644 --- a/packages/tui/test/tui-overlay-style-leak.test.ts +++ b/packages/tui/test/tui-overlay-style-leak.test.ts @@ -1,8 +1,8 @@ import assert from "node:assert"; import { describe, it } from "node:test"; import type { Terminal as XtermTerminalType } from "@xterm/headless"; -import { type Component, TUI } from "../src/tui.js"; -import { VirtualTerminal } from "./virtual-terminal.js"; +import { type Component, TUI } from "../src/tui.ts"; +import { VirtualTerminal } from "./virtual-terminal.ts"; class StaticLines implements Component { private readonly lines: string[]; diff --git a/packages/tui/test/tui-render.test.ts b/packages/tui/test/tui-render.test.ts index 91cb9695..230a0005 100644 --- a/packages/tui/test/tui-render.test.ts +++ b/packages/tui/test/tui-render.test.ts @@ -1,9 +1,9 @@ import assert from "node:assert"; import { describe, it } from "node:test"; import type { Terminal as XtermTerminalType } from "@xterm/headless"; -import { deleteKittyImage, encodeKitty } from "../src/terminal-image.js"; -import { type Component, TUI } from "../src/tui.js"; -import { VirtualTerminal } from "./virtual-terminal.js"; +import { deleteKittyImage, encodeKitty } from "../src/terminal-image.ts"; +import { type Component, TUI } from "../src/tui.ts"; +import { VirtualTerminal } from "./virtual-terminal.ts"; class TestComponent implements Component { lines: string[] = []; diff --git a/packages/tui/test/viewport-overwrite-repro.ts b/packages/tui/test/viewport-overwrite-repro.ts index 5a0ac4f9..1ef432a0 100644 --- a/packages/tui/test/viewport-overwrite-repro.ts +++ b/packages/tui/test/viewport-overwrite-repro.ts @@ -17,8 +17,8 @@ * - When content exceeds the viewport and new lines arrive after a tool-call pause, * some earlier PRE-TOOL lines near the bottom are overwritten by POST-TOOL lines. */ -import { ProcessTerminal } from "../src/terminal.js"; -import { type Component, TUI } from "../src/tui.js"; +import { ProcessTerminal } from "../src/terminal.ts"; +import { type Component, TUI } from "../src/tui.ts"; const sleep = (ms: number): Promise => new Promise((resolve) => setTimeout(resolve, ms)); diff --git a/packages/tui/test/virtual-terminal.ts b/packages/tui/test/virtual-terminal.ts index c7ca4510..4e067f4e 100644 --- a/packages/tui/test/virtual-terminal.ts +++ b/packages/tui/test/virtual-terminal.ts @@ -1,6 +1,6 @@ import type { Terminal as XtermTerminalType } from "@xterm/headless"; import xterm from "@xterm/headless"; -import type { Terminal } from "../src/terminal.js"; +import type { Terminal } from "../src/terminal.ts"; // Extract Terminal class from the module const XtermTerminal = xterm.Terminal; diff --git a/packages/tui/test/wrap-ansi.test.ts b/packages/tui/test/wrap-ansi.test.ts index bd24e815..52d59148 100644 --- a/packages/tui/test/wrap-ansi.test.ts +++ b/packages/tui/test/wrap-ansi.test.ts @@ -1,6 +1,6 @@ import assert from "node:assert"; import { describe, it } from "node:test"; -import { visibleWidth, wrapTextWithAnsi } from "../src/utils.js"; +import { visibleWidth, wrapTextWithAnsi } from "../src/utils.ts"; describe("wrapTextWithAnsi", () => { describe("underline styling", () => { diff --git a/scripts/cost.ts b/scripts/cost.ts index 2774b8d8..865fbed7 100755 --- a/scripts/cost.ts +++ b/scripts/cost.ts @@ -1,4 +1,4 @@ -#!/usr/bin/env npx tsx +#!/usr/bin/env node import * as fs from "fs"; import * as path from "path"; diff --git a/scripts/session-transcripts.ts b/scripts/session-transcripts.ts index e10dbff9..4dca6a1d 100644 --- a/scripts/session-transcripts.ts +++ b/scripts/session-transcripts.ts @@ -1,9 +1,9 @@ -#!/usr/bin/env npx tsx +#!/usr/bin/env node /** * Extracts session transcripts for a given cwd, splits into context-sized files, * optionally spawns subagents to analyze patterns. * - * Usage: npx tsx scripts/session-transcripts.ts [--analyze] [--output ] [cwd] + * Usage: node scripts/session-transcripts.ts [--analyze] [--output ] [cwd] * --analyze Spawn pi subagents to analyze each transcript file * --output Output directory for transcript files (defaults to ./session-transcripts) * cwd Working directory to extract sessions for (defaults to current) @@ -14,7 +14,7 @@ import { spawn } from "child_process"; import { createInterface } from "node:readline"; import { homedir } from "os"; import { join, resolve } from "path"; -import { parseSessionEntries, type SessionMessageEntry } from "../packages/coding-agent/src/core/session-manager.js"; +import { parseSessionEntries, type SessionMessageEntry } from "../packages/coding-agent/src/core/session-manager.ts"; import chalk from "chalk"; const MAX_CHARS_PER_FILE = 100_000; // ~20k tokens, leaving room for prompt + analysis + output diff --git a/scripts/stats.ts b/scripts/stats.ts index 15c353e3..653f4bfb 100644 --- a/scripts/stats.ts +++ b/scripts/stats.ts @@ -1,4 +1,4 @@ -#!/usr/bin/env npx tsx +#!/usr/bin/env node import { existsSync, readdirSync, readFileSync } from "node:fs"; import { homedir } from "node:os"; diff --git a/scripts/tool-stats.ts b/scripts/tool-stats.ts index 8d39b868..78a69418 100755 --- a/scripts/tool-stats.ts +++ b/scripts/tool-stats.ts @@ -1,4 +1,4 @@ -#!/usr/bin/env npx tsx +#!/usr/bin/env node import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs"; import { homedir, tmpdir } from "node:os"; From ea4eab15cf101995c8bb1a56ba842f2d9b1fbdf7 Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Wed, 20 May 2026 13:06:28 +0200 Subject: [PATCH 137/166] chore: add local release and dependency guards --- package.json | 5 +- scripts/check-pinned-deps.mjs | 63 +++++++++ scripts/check-ts-relative-imports.mjs | 74 ++++++++++ scripts/local-release.mjs | 187 ++++++++++++++++++++++++++ 4 files changed, 328 insertions(+), 1 deletion(-) create mode 100644 scripts/check-pinned-deps.mjs create mode 100644 scripts/check-ts-relative-imports.mjs create mode 100644 scripts/local-release.mjs diff --git a/package.json b/package.json index cd0fed1a..07a7fd3c 100644 --- a/package.json +++ b/package.json @@ -12,8 +12,10 @@ "scripts": { "clean": "npm run clean --workspaces", "build": "cd packages/tui && npm run build && cd ../ai && npm run build && cd ../agent && npm run build && cd ../coding-agent && npm run build", - "check": "biome check --write --error-on-warnings . && tsgo --noEmit && npm run check:browser-smoke", + "check": "biome check --write --error-on-warnings . && npm run check:pinned-deps && npm run check:ts-imports && tsgo --noEmit && npm run check:browser-smoke", "check:browser-smoke": "node scripts/check-browser-smoke.mjs", + "check:pinned-deps": "node scripts/check-pinned-deps.mjs", + "check:ts-imports": "node scripts/check-ts-relative-imports.mjs", "profile:tui": "node scripts/profile-coding-agent-node.mjs --mode tui", "profile:rpc": "node scripts/profile-coding-agent-node.mjs --mode rpc", "test": "npm run test --workspaces --if-present", @@ -24,6 +26,7 @@ "prepublishOnly": "npm run clean && npm run build && npm run check", "publish": "npm run prepublishOnly && npm publish -ws --access public", "publish:dry": "npm run prepublishOnly && npm publish -ws --access public --dry-run", + "release:local": "node scripts/local-release.mjs", "release:patch": "node scripts/release.mjs patch", "release:minor": "node scripts/release.mjs minor", "release:major": "node scripts/release.mjs major", diff --git a/scripts/check-pinned-deps.mjs b/scripts/check-pinned-deps.mjs new file mode 100644 index 00000000..a2b38942 --- /dev/null +++ b/scripts/check-pinned-deps.mjs @@ -0,0 +1,63 @@ +import { readdirSync, readFileSync } from "node:fs"; +import { join } from "node:path"; + +const dependencySections = ["dependencies", "devDependencies", "optionalDependencies"]; +const exactVersionPattern = /^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/; +const ignoredDirectories = new Set([".git", "dist", "node_modules"]); +const packageJsonFiles = []; + +function collectPackageJsonFiles(directory) { + for (const entry of readdirSync(directory, { withFileTypes: true })) { + if (entry.isDirectory()) { + if (!ignoredDirectories.has(entry.name)) { + collectPackageJsonFiles(join(directory, entry.name)); + } + continue; + } + + if (entry.isFile() && entry.name === "package.json") { + packageJsonFiles.push(join(directory, entry.name)); + } + } +} + +function isInternalWorkspaceDependency(name) { + return name.startsWith("@earendil-works/pi-"); +} + +function isNonRegistrySpecifier(specifier) { + return /^(?:workspace:|file:|link:|portal:|git\+|github:|git:|https?:|ssh:|git:\/\/)/.test(specifier); +} + +function getVersionSpecifier(specifier) { + if (!specifier.startsWith("npm:")) return specifier; + const aliasTarget = specifier.slice("npm:".length); + const versionSeparator = aliasTarget.lastIndexOf("@"); + if (versionSeparator <= 0) return specifier; + return aliasTarget.slice(versionSeparator + 1); +} + +const failures = []; + +collectPackageJsonFiles("."); + +for (const file of packageJsonFiles.sort()) { + const packageJson = JSON.parse(readFileSync(file, "utf8")); + + for (const section of dependencySections) { + const dependencies = packageJson[section]; + if (!dependencies) continue; + + for (const [name, specifier] of Object.entries(dependencies)) { + if (isInternalWorkspaceDependency(name) || isNonRegistrySpecifier(specifier)) continue; + if (exactVersionPattern.test(getVersionSpecifier(specifier))) continue; + failures.push(`${file}: ${section}.${name} must be pinned, found ${specifier}`); + } + } +} + +if (failures.length > 0) { + console.error("Direct external dependencies must use exact versions:"); + for (const failure of failures) console.error(` ${failure}`); + process.exit(1); +} diff --git a/scripts/check-ts-relative-imports.mjs b/scripts/check-ts-relative-imports.mjs new file mode 100644 index 00000000..bf21a5d0 --- /dev/null +++ b/scripts/check-ts-relative-imports.mjs @@ -0,0 +1,74 @@ +import { readdirSync, readFileSync } from "node:fs"; +import { join } from "node:path"; +import ts from "typescript"; + +const ignoredDirectories = new Set([".git", "coverage", "dist", "node_modules"]); +const files = []; + +function collectTypescriptFiles(directory) { + for (const entry of readdirSync(directory, { withFileTypes: true })) { + if (entry.isDirectory()) { + if (!ignoredDirectories.has(entry.name)) { + collectTypescriptFiles(join(directory, entry.name)); + } + continue; + } + + if (entry.isFile() && entry.name.endsWith(".ts") && !entry.name.endsWith(".d.ts")) { + files.push(join(directory, entry.name)); + } + } +} + +function isRelativeJavaScriptSpecifier(specifier) { + return /^\.\.?\//.test(specifier) && /\.js(?:[?#].*)?$/.test(specifier); +} + +function getImportTypeSpecifier(node) { + if (!ts.isLiteralTypeNode(node.argument)) return undefined; + if (!ts.isStringLiteralLike(node.argument.literal)) return undefined; + return node.argument.literal; +} + +const failures = []; + +collectTypescriptFiles("."); + +for (const file of files.sort()) { + const sourceText = readFileSync(file, "utf8"); + const sourceFile = ts.createSourceFile(file, sourceText, ts.ScriptTarget.Latest, true); + + function checkSpecifier(node) { + if (!isRelativeJavaScriptSpecifier(node.text)) return; + const { line, character } = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile)); + failures.push(`${file}:${line + 1}:${character + 1}: ${node.text}`); + } + + function visit(node) { + if (ts.isImportDeclaration(node) && ts.isStringLiteralLike(node.moduleSpecifier)) { + checkSpecifier(node.moduleSpecifier); + } else if (ts.isExportDeclaration(node) && node.moduleSpecifier && ts.isStringLiteralLike(node.moduleSpecifier)) { + checkSpecifier(node.moduleSpecifier); + } else if ( + ts.isCallExpression(node) && + node.expression.kind === ts.SyntaxKind.ImportKeyword && + node.arguments[0] && + ts.isStringLiteralLike(node.arguments[0]) + ) { + checkSpecifier(node.arguments[0]); + } else if (ts.isImportTypeNode(node)) { + const specifier = getImportTypeSpecifier(node); + if (specifier) checkSpecifier(specifier); + } + + ts.forEachChild(node, visit); + } + + visit(sourceFile); +} + +if (failures.length > 0) { + console.error("Relative .js imports are not allowed in non-declaration .ts files:"); + for (const failure of failures) console.error(` ${failure}`); + process.exit(1); +} diff --git a/scripts/local-release.mjs b/scripts/local-release.mjs new file mode 100644 index 00000000..31e07901 --- /dev/null +++ b/scripts/local-release.mjs @@ -0,0 +1,187 @@ +#!/usr/bin/env node + +import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, symlinkSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { dirname, isAbsolute, join, relative, resolve } from "node:path"; +import { spawnSync } from "node:child_process"; + +const packages = [ + { directory: "packages/ai", name: "@earendil-works/pi-ai" }, + { directory: "packages/tui", name: "@earendil-works/pi-tui" }, + { directory: "packages/agent", name: "@earendil-works/pi-agent-core" }, + { directory: "packages/coding-agent", name: "@earendil-works/pi-coding-agent" }, +]; + +function printUsage() { + console.log(`Usage: node scripts/local-release.mjs [options] + +Builds and packs the publishable packages, then installs the tarballs into an +isolated directory outside the repository for local release testing. + +Options: + --out Output directory. Defaults to a new directory under ${tmpdir()} + --force Remove --out first if it already exists + --skip-check Do not run npm run check before building + --skip-install Only create tarballs; do not create the isolated install + --help Show this help +`); +} + +function parseArgs() { + const options = { force: false, outDir: undefined, skipCheck: false, skipInstall: false }; + const args = process.argv.slice(2); + + for (let i = 0; i < args.length; i++) { + const arg = args[i]; + if (arg === "--help") { + printUsage(); + process.exit(0); + } + if (arg === "--force") { + options.force = true; + continue; + } + if (arg === "--skip-check") { + options.skipCheck = true; + continue; + } + if (arg === "--skip-install") { + options.skipInstall = true; + continue; + } + if (arg === "--out") { + const value = args[++i]; + if (!value) { + throw new Error("--out requires a directory"); + } + options.outDir = value; + continue; + } + throw new Error(`Unknown option: ${arg}`); + } + + return options; +} + +function run(command, args, options = {}) { + console.log(`$ ${[command, ...args].join(" ")}`); + const result = spawnSync(command, args, { + cwd: options.cwd, + encoding: "utf8", + stdio: options.capture ? ["inherit", "pipe", "inherit"] : "inherit", + }); + + if (result.status !== 0) { + throw new Error(`Command failed: ${[command, ...args].join(" ")}`); + } + + return result.stdout ?? ""; +} + +function readPackageJson(directory) { + return JSON.parse(readFileSync(join(directory, "package.json"), "utf8")); +} + +function isInsidePath(child, parent) { + const relativePath = relative(parent, child); + return relativePath === "" || (!relativePath.startsWith("..") && !isAbsolute(relativePath)); +} + +function prepareOutputDirectory(options, repoRoot) { + if (!options.outDir) { + return mkdtempSync(join(tmpdir(), "pi-local-release-")); + } + + const outDir = resolve(options.outDir); + + if (isInsidePath(outDir, repoRoot)) { + throw new Error(`Output directory must be outside the repository: ${outDir}`); + } + + if (existsSync(outDir)) { + if (!options.force) { + throw new Error(`Output directory already exists. Use --force to replace it: ${outDir}`); + } + rmSync(outDir, { force: true, recursive: true }); + } + + mkdirSync(outDir, { recursive: true }); + return outDir; +} + +function fileSpecifier(fromDirectory, file) { + const relativePath = relative(fromDirectory, file).replaceAll("\\", "/"); + return `file:${relativePath.startsWith(".") ? relativePath : `./${relativePath}`}`; +} + +function packPackage(pkg, tarballDirectory) { + const packageJson = readPackageJson(pkg.directory); + if (packageJson.name !== pkg.name) { + throw new Error(`${pkg.directory}/package.json has name ${packageJson.name}, expected ${pkg.name}`); + } + + const output = run("npm", ["pack", "--json", "--pack-destination", tarballDirectory], { + capture: true, + cwd: pkg.directory, + }); + const packed = JSON.parse(output)[0]; + return join(tarballDirectory, packed.filename); +} + +const options = parseArgs(); +const repoRoot = process.cwd(); +const rootPackageJson = readPackageJson(repoRoot); + +if (rootPackageJson.name !== "pi-monorepo") { + throw new Error("Run this script from the repository root"); +} + +const outDir = prepareOutputDirectory(options, repoRoot); +const tarballDirectory = join(outDir, "tarballs"); +const installDirectory = join(outDir, "install"); +const binDirectory = join(outDir, "bin"); +mkdirSync(tarballDirectory, { recursive: true }); + +if (!options.skipCheck) { + run("npm", ["run", "check"], { cwd: repoRoot }); +} + +for (const pkg of packages) { + run("npm", ["run", "clean"], { cwd: pkg.directory }); + run("npm", ["run", "build"], { cwd: pkg.directory }); +} + +const tarballs = new Map(); +for (const pkg of packages) { + const tarball = packPackage(pkg, tarballDirectory); + tarballs.set(pkg.name, tarball); +} + +if (!options.skipInstall) { + mkdirSync(installDirectory, { recursive: true }); + const dependencies = Object.fromEntries( + packages.map((pkg) => [pkg.name, fileSpecifier(installDirectory, tarballs.get(pkg.name))]), + ); + writeFileSync( + join(installDirectory, "package.json"), + `${JSON.stringify({ private: true, dependencies }, undefined, "\t")}\n`, + ); + + run("npm", ["install", "--omit=dev"], { cwd: installDirectory }); + mkdirSync(binDirectory, { recursive: true }); + symlinkSync(join(installDirectory, "node_modules", ".bin", "pi"), join(binDirectory, "pi")); +} + +console.log("\nLocal release artifacts created:"); +console.log(` ${outDir}`); +console.log("\nTarballs:"); +for (const tarball of tarballs.values()) { + console.log(` ${tarball}`); +} + +if (!options.skipInstall) { + console.log("\nIsolated install:"); + console.log(` ${installDirectory}`); + console.log("\nRun the locally packed CLI from outside the repository:"); + console.log(` ${join(binDirectory, "pi")} --help`); +} From fec3871432b45602c38dd2f85a8788951a0f5e80 Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Wed, 20 May 2026 13:38:40 +0200 Subject: [PATCH 138/166] docs: require local release smoke test --- AGENTS.md | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 3e752675..a05fab61 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -190,13 +190,23 @@ Create provider file exporting: 1. **Update CHANGELOGs**: Ensure all changes since last release are documented in the `[Unreleased]` section of each affected package's CHANGELOG.md -2. **Run release script**: +2. **Soft gate: local release smoke test**: Before running the real release script, build an unpublished local release and manually smoke test it from outside the repository so it cannot accidentally resolve workspace files: + ```bash + npm run release:local -- --out /tmp/pi-local-release --force + cd /tmp + /tmp/pi-local-release/bin/pi --help + /tmp/pi-local-release/bin/pi --version + /tmp/pi-local-release/bin/pi + ``` + In the interactive smoke test, verify startup, model/account listing, and at least one real prompt with the intended default provider. Treat failures as release blockers unless the user explicitly accepts the risk. + +3. **Run release script**: ```bash npm run release:patch # Fixes and additions npm run release:minor # API breaking changes ``` -The script handles: version bump, CHANGELOG finalization, commit, tag, publish, and adding new `[Unreleased]` sections. +The release script handles: version bump, CHANGELOG finalization, commit, tag, publish, and adding new `[Unreleased]` sections. ## **CRITICAL** Git Rules for Parallel Agents **CRITICAL** From dce24ac9ff98d4d4df7a7a6129468932f2c50508 Mon Sep 17 00:00:00 2001 From: Sviatoslav Abakumov Date: Wed, 20 May 2026 15:54:11 +0400 Subject: [PATCH 139/166] fix(coding-agent): keep fork session id aligned --- packages/coding-agent/src/core/agent-session-runtime.ts | 5 ++--- .../coding-agent/test/suite/agent-session-runtime.test.ts | 4 +++- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/coding-agent/src/core/agent-session-runtime.ts b/packages/coding-agent/src/core/agent-session-runtime.ts index a89fef83..b4756d4c 100644 --- a/packages/coding-agent/src/core/agent-session-runtime.ts +++ b/packages/coding-agent/src/core/agent-session-runtime.ts @@ -292,12 +292,11 @@ export class AgentSessionRuntime { return { cancelled: false, selectedText }; } - const sourceManager = SessionManager.open(currentSessionFile, sessionDir); - const forkedSessionPath = sourceManager.createBranchedSession(targetLeafId); + const sessionManager = SessionManager.open(currentSessionFile, sessionDir); + const forkedSessionPath = sessionManager.createBranchedSession(targetLeafId); if (!forkedSessionPath) { throw new Error("Failed to create forked session"); } - const sessionManager = SessionManager.open(forkedSessionPath, sessionDir); await this.teardownCurrent("fork", sessionManager.getSessionFile()); this.apply( await this.createRuntime({ diff --git a/packages/coding-agent/test/suite/agent-session-runtime.test.ts b/packages/coding-agent/test/suite/agent-session-runtime.test.ts index fb8b2167..a4d6e04d 100644 --- a/packages/coding-agent/test/suite/agent-session-runtime.test.ts +++ b/packages/coding-agent/test/suite/agent-session-runtime.test.ts @@ -1,6 +1,6 @@ import { existsSync, mkdirSync, realpathSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; -import { join } from "node:path"; +import { join, parse } from "node:path"; import { fauxAssistantMessage, registerFauxProvider } from "@earendil-works/pi-ai"; import { afterEach, describe, expect, it } from "vitest"; import { @@ -274,6 +274,8 @@ describe("AgentSessionRuntime characterization", () => { { type: "session_shutdown", reason: "fork", targetSessionFile: runtime.session.sessionFile }, { type: "session_start", reason: "fork", previousSessionFile }, ]); + const sessionFileName = parse(runtime.session.sessionFile!).name; + expect(sessionFileName.endsWith(`_${runtime.session.sessionId}`)).toBe(true); events.length = 0; cancelNextFork = true; From 715c82ce0454b4fa9d6791bca22d6d694a2a0b28 Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Wed, 20 May 2026 14:19:40 +0200 Subject: [PATCH 140/166] chore: shrinkwrap coding agent release deps --- package.json | 10 +- packages/coding-agent/CHANGELOG.md | 1 + packages/coding-agent/npm-shrinkwrap.json | 1803 ++++++++++++++++++ packages/coding-agent/package.json | 6 +- scripts/build-binaries.sh | 2 +- scripts/generate-coding-agent-shrinkwrap.mjs | 340 ++++ scripts/release.mjs | 28 +- 7 files changed, 2171 insertions(+), 19 deletions(-) create mode 100644 packages/coding-agent/npm-shrinkwrap.json create mode 100644 scripts/generate-coding-agent-shrinkwrap.mjs diff --git a/package.json b/package.json index 07a7fd3c..daa4b4e5 100644 --- a/package.json +++ b/package.json @@ -12,21 +12,23 @@ "scripts": { "clean": "npm run clean --workspaces", "build": "cd packages/tui && npm run build && cd ../ai && npm run build && cd ../agent && npm run build && cd ../coding-agent && npm run build", - "check": "biome check --write --error-on-warnings . && npm run check:pinned-deps && npm run check:ts-imports && tsgo --noEmit && npm run check:browser-smoke", + "check": "biome check --write --error-on-warnings . && npm run check:pinned-deps && npm run check:ts-imports && npm run check:shrinkwrap && tsgo --noEmit && npm run check:browser-smoke", "check:browser-smoke": "node scripts/check-browser-smoke.mjs", "check:pinned-deps": "node scripts/check-pinned-deps.mjs", + "check:shrinkwrap": "node scripts/generate-coding-agent-shrinkwrap.mjs --check", "check:ts-imports": "node scripts/check-ts-relative-imports.mjs", "profile:tui": "node scripts/profile-coding-agent-node.mjs --mode tui", "profile:rpc": "node scripts/profile-coding-agent-node.mjs --mode rpc", "test": "npm run test --workspaces --if-present", - "version:patch": "npm version patch -ws --no-git-tag-version && node scripts/sync-versions.js && shx rm -rf node_modules packages/*/node_modules package-lock.json && npm install", - "version:minor": "npm version minor -ws --no-git-tag-version && node scripts/sync-versions.js && shx rm -rf node_modules packages/*/node_modules package-lock.json && npm install", - "version:major": "npm version major -ws --no-git-tag-version && node scripts/sync-versions.js && shx rm -rf node_modules packages/*/node_modules package-lock.json && npm install", + "version:patch": "npm version patch -ws --no-git-tag-version && node scripts/sync-versions.js && npm install --package-lock-only", + "version:minor": "npm version minor -ws --no-git-tag-version && node scripts/sync-versions.js && npm install --package-lock-only", + "version:major": "npm version major -ws --no-git-tag-version && node scripts/sync-versions.js && npm install --package-lock-only", "version:set": "npm version -ws", "prepublishOnly": "npm run clean && npm run build && npm run check", "publish": "npm run prepublishOnly && npm publish -ws --access public", "publish:dry": "npm run prepublishOnly && npm publish -ws --access public --dry-run", "release:local": "node scripts/local-release.mjs", + "shrinkwrap:coding-agent": "node scripts/generate-coding-agent-shrinkwrap.mjs", "release:patch": "node scripts/release.mjs patch", "release:minor": "node scripts/release.mjs minor", "release:major": "node scripts/release.mjs major", diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 582015c0..f902be1e 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -6,6 +6,7 @@ - Changed source syntax to avoid TypeScript constructs that require JavaScript emit, keeping core sources compatible with Node.js strip-only TypeScript checks. - Removed web UI workspace references from the CLI package and dropped the package-level development watch script. +- Published npm installs now include an `npm-shrinkwrap.json` to lock transitive dependencies for the CLI package. ### Fixed diff --git a/packages/coding-agent/npm-shrinkwrap.json b/packages/coding-agent/npm-shrinkwrap.json new file mode 100644 index 00000000..174e6d17 --- /dev/null +++ b/packages/coding-agent/npm-shrinkwrap.json @@ -0,0 +1,1803 @@ +{ + "name": "@earendil-works/pi-coding-agent", + "version": "0.75.3", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@earendil-works/pi-coding-agent", + "version": "0.75.3", + "license": "MIT", + "dependencies": { + "@earendil-works/pi-agent-core": "^0.75.3", + "@earendil-works/pi-ai": "^0.75.3", + "@earendil-works/pi-tui": "^0.75.3", + "@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" + }, + "engines": { + "node": ">=22.19.0" + } + }, + "node_modules/@anthropic-ai/sdk": { + "version": "0.91.1", + "resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.91.1.tgz", + "integrity": "sha512-LAmu761tSN9r66ixvmciswUj/ZC+1Q4iAfpedTfSVLeswRwnY3n2Nb6Tsk+cLPP28aLOPWeMgIuTuCcMC6W/iw==", + "license": "MIT", + "dependencies": { + "json-schema-to-ts": "^3.1.1" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + }, + "peerDependenciesMeta": { + "zod": { + "optional": true + } + }, + "bin": { + "anthropic-ai-sdk": "bin/cli" + } + }, + "node_modules/@aws-crypto/crc32": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/crc32/-/crc32-5.2.0.tgz", + "integrity": "sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-crypto/sha256-browser": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-browser/-/sha256-browser-5.2.0.tgz", + "integrity": "sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==", + "license": "Apache-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" + } + }, + "node_modules/@aws-crypto/sha256-js": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-5.2.0.tgz", + "integrity": "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-crypto/supports-web-crypto": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/supports-web-crypto/-/supports-web-crypto-5.2.0.tgz", + "integrity": "sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/util": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-5.2.0.tgz", + "integrity": "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.222.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/client-bedrock-runtime": { + "version": "3.1048.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-bedrock-runtime/-/client-bedrock-runtime-3.1048.0.tgz", + "integrity": "sha512-u+NT61JZEkRFtpL0CAw1N1dwxnaLgwVXQl/zjJxTGgLyS/jTIdg2SdoEoCTHxgDyCnqa1HEi9QOoE9/pYRNpOQ==", + "license": "Apache-2.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" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/core": { + "version": "3.974.11", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.974.11.tgz", + "integrity": "sha512-QpnINq5FZH6EOaDEkmHdT7eUunbvD27pDNQypaWjFyYz7Zl1q3UCMQErBZxpmfGfI7MvI2TlK8KTkgNpv8b1ug==", + "license": "Apache-2.0", + "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" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-env": { + "version": "3.972.37", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.37.tgz", + "integrity": "sha512-/jpPvEh6f7ntmIzf7dNxoNX6Q8vt8UpesCjbW6mFfk4V1NW6bIy9qxcQ6WbA8As5yQhsZOe+xeNd4xHX8kdY2Q==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.11", + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.2", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-http": { + "version": "3.972.39", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.39.tgz", + "integrity": "sha512-pIgTpisWyWg7X1bUbzSjuUYosYTD0Ghz2M0hkSTmb3a6i3qV3uU+NYJPI/E2XSC0HcsZh5rsLPzeXrkb2DS0Cg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.11", + "@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" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.972.41", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.972.41.tgz", + "integrity": "sha512-u2tyjaxJJzW8UtW4SM1ZcPMDwO6y+kV+llvou+Adts0FAKyzes5jG4izQN+KX3yE8ZROpS5y1LJ//xL2iSf76w==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.11", + "@aws-sdk/credential-provider-env": "^3.972.37", + "@aws-sdk/credential-provider-http": "^3.972.39", + "@aws-sdk/credential-provider-login": "^3.972.41", + "@aws-sdk/credential-provider-process": "^3.972.37", + "@aws-sdk/credential-provider-sso": "^3.972.41", + "@aws-sdk/credential-provider-web-identity": "^3.972.41", + "@aws-sdk/nested-clients": "^3.997.9", + "@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" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-login": { + "version": "3.972.41", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.41.tgz", + "integrity": "sha512-0LBitxXiAiaE5nlFPfpNIww/8FRY/I7WIndWsc9GmNFOM7cE1wNpVNQEGEk9Outg5l8xl+3vybxFyUy4l9q/LQ==", + "license": "Apache-2.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" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-node": { + "version": "3.972.42", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.42.tgz", + "integrity": "sha512-D4oon2zbqqsWOJUM99Gm3/ZyJ0IJvTXVN3PyloGb3kQEyI36fjCZheZj422lAgTWWd6TSHgiImLt3RIaLdv3dQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/credential-provider-env": "^3.972.37", + "@aws-sdk/credential-provider-http": "^3.972.39", + "@aws-sdk/credential-provider-ini": "^3.972.41", + "@aws-sdk/credential-provider-process": "^3.972.37", + "@aws-sdk/credential-provider-sso": "^3.972.41", + "@aws-sdk/credential-provider-web-identity": "^3.972.41", + "@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" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-process": { + "version": "3.972.37", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.37.tgz", + "integrity": "sha512-7nVaHBUaWIddASYfVaA9O4D5ZVjewU3sCol9WqZPGfW0nR+0WqE0xHZnD/U2L33PlOB8KNXGKZ6wOES/QijKzg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.11", + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.2", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.972.41", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.972.41.tgz", + "integrity": "sha512-IOWAWEHe5LkjSKkkUUX9ciV6Y1scHTsnfEkdt5yyC4Slrc7AGbkLPrpntjqh18ksJAMOaVhoBsO8p2WyTcY2wQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.11", + "@aws-sdk/nested-clients": "^3.997.9", + "@aws-sdk/token-providers": "3.1048.0", + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.2", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.972.41", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.41.tgz", + "integrity": "sha512-mbACk9Yypa8nm4iGZLs0PofOXEcTDOUw6wDnsPXNDNSd2WNXs1tSo+6nc/fh0jLYdfVZThhBL98PHW4aXFsG5A==", + "license": "Apache-2.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" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/eventstream-handler-node": { + "version": "3.972.16", + "resolved": "https://registry.npmjs.org/@aws-sdk/eventstream-handler-node/-/eventstream-handler-node-3.972.16.tgz", + "integrity": "sha512-yedpPgKftqjU5SlPFHfqWpOw6xSCRieWRG1euWOlXn4WJxt2VX92VprCa2PpSOXjVCAeK6dTjW9eJRXVig9yGA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.2", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-eventstream": { + "version": "3.972.12", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-eventstream/-/middleware-eventstream-3.972.12.tgz", + "integrity": "sha512-tHTHHCHNrq6XklQvlzHBDJG4Iuhh7NVPRdtmvP+nHFA+5sxPlIDzlAHHgfoYHGvT3NXP1yVP/L5c3opUn6T3Qg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.2", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-websocket": { + "version": "3.972.19", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-websocket/-/middleware-websocket-3.972.19.tgz", + "integrity": "sha512-mkEhOGYozqKQkbFaVrjwr0faiwwZza1v5/jSY6Tucm3bD+uKTazIUH/4Yo6aMnQD2ua2W9cMP6s8mvwTcjtqHw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.11", + "@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" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@aws-sdk/nested-clients": { + "version": "3.997.9", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.997.9.tgz", + "integrity": "sha512-jPR3rnmRI4hWYyzfmTGBr7NblMp8QYYeflHXba1H6+7CGrWVqWKQzaXFQ4qbExqPRsXN3T3L3JxFhr6aouXUGQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "^3.974.11", + "@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" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/signature-v4-multi-region": { + "version": "3.996.27", + "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.996.27.tgz", + "integrity": "sha512-0Phbz4t6HI3D3skxvG2uI+VWU034/nSIw1T8d+FPzzQG9EQTrw94o9mOKO2Gv3n3Oc8P7JD7RAUxkoneLWv5Eg==", + "license": "Apache-2.0", + "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" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/token-providers": { + "version": "3.1048.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1048.0.tgz", + "integrity": "sha512-k0y/GcuesuSfWyUM0WamrGyeZmltRYaPbHO82UDA6mZ/doB+FOHKutikPAtSXMn/hDz970cF+iRuuiYO9VEbAA==", + "license": "Apache-2.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" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/types": { + "version": "3.973.8", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.973.8.tgz", + "integrity": "sha512-gjlAdtHMbtR9X5iIhVUvbVcy55KnznpC6bkDUWW9z915bi0ckdUr5cjf16Kp6xq0bP5HBD2xzgbL9F9Quv5vUw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/util-locate-window": { + "version": "3.965.5", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.965.5.tgz", + "integrity": "sha512-WhlJNNINQB+9qtLtZJcpQdgZw3SCDCpXdUJP7cToGwHbCWCnRckGlc6Bx/OhWwIYFNAn+FIydY8SZ0QmVu3xTQ==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/xml-builder": { + "version": "3.972.24", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.24.tgz", + "integrity": "sha512-V8z5YcDPfsvzrBlj0xR1vhRtocblhYbqdreCJB/voGd4Sr5zjNAeWxexbnqVtskTJe0vFb5KMqbSL++ePl+zRw==", + "license": "Apache-2.0", + "dependencies": { + "@nodable/entities": "2.1.0", + "@smithy/types": "^4.14.1", + "fast-xml-parser": "5.7.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws/lambda-invoke-store": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.2.4.tgz", + "integrity": "sha512-iY8yvjE0y651BixKNPgmv1WrQc+GZ142sb0z4gYnChDDY2YqI4P/jsSopBWrKfAt7LOJAkOXt7rC/hms+WclQQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz", + "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@earendil-works/pi-agent-core": { + "version": "0.75.3", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-agent-core/-/pi-agent-core-0.75.3.tgz", + "license": "MIT", + "dependencies": { + "@earendil-works/pi-ai": "^0.75.3", + "ignore": "7.0.5", + "typebox": "1.1.38", + "yaml": "2.9.0" + }, + "engines": { + "node": ">=22.19.0" + } + }, + "node_modules/@earendil-works/pi-ai": { + "version": "0.75.3", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-ai/-/pi-ai-0.75.3.tgz", + "license": "MIT", + "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" + }, + "engines": { + "node": ">=22.19.0" + } + }, + "node_modules/@earendil-works/pi-tui": { + "version": "0.75.3", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-tui/-/pi-tui-0.75.3.tgz", + "license": "MIT", + "dependencies": { + "get-east-asian-width": "1.6.0", + "marked": "15.0.12" + }, + "optionalDependencies": { + "koffi": "2.16.2" + }, + "engines": { + "node": ">=22.19.0" + } + }, + "node_modules/@google/genai": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/@google/genai/-/genai-1.52.0.tgz", + "integrity": "sha512-gwSvbpiN/17O9TbsqSsE/OzZcpv5Fo4RQjdngGgogtuB9RsyJ8ZHhX5KjHj1bp5N9snN2eK8LDGXSaWW2hof8Q==", + "license": "Apache-2.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" + }, + "peerDependenciesMeta": { + "@modelcontextprotocol/sdk": { + "optional": true + } + }, + "engines": { + "node": ">=20.0.0" + }, + "hasInstallScript": true + }, + "node_modules/@mariozechner/clipboard": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard/-/clipboard-0.3.6.tgz", + "integrity": "sha512-MXdtr+6+ntlIVHdrZYuZNQydu6o8yZswFJ2Ln81j2O/Y9B/LDHvEaIm95xWNPkjGTWriSOeLnQJRFs6dYb60bg==", + "license": "MIT", + "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" + }, + "engines": { + "node": ">= 10" + }, + "optional": true + }, + "node_modules/@mariozechner/clipboard-darwin-arm64": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-darwin-arm64/-/clipboard-darwin-arm64-0.3.6.tgz", + "integrity": "sha512-HjaisYCAbHi/1+N1yDAQHc8ZXGffufIUT5NSOSVR3f3AuMDusxTtnbK8tZ7JFDkShua1oNGZoNwQHsc8MPtE0Q==", + "license": "MIT", + "engines": { + "node": ">= 10" + }, + "os": [ + "darwin" + ], + "cpu": [ + "arm64" + ], + "optional": true + }, + "node_modules/@mariozechner/clipboard-darwin-universal": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-darwin-universal/-/clipboard-darwin-universal-0.3.6.tgz", + "integrity": "sha512-8BWtPjOtJOJoykml3w0fx0zRrfWP31mXrJwfoA7xzNprkZw1uolCNfgmjDiVBseoKjp16EGITz7bN+61qn8dWA==", + "license": "MIT", + "engines": { + "node": ">= 10" + }, + "os": [ + "darwin" + ], + "optional": true + }, + "node_modules/@mariozechner/clipboard-darwin-x64": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-darwin-x64/-/clipboard-darwin-x64-0.3.6.tgz", + "integrity": "sha512-p9syiZD1kU4I+1ya7f7g+zD1GiUvR8fdlRlNmgsZNWlyjtc8rlV2EjTLd/35x1LsdBq020GVvtzp0ZmPgBI09Q==", + "license": "MIT", + "engines": { + "node": ">= 10" + }, + "os": [ + "darwin" + ], + "cpu": [ + "x64" + ], + "optional": true + }, + "node_modules/@mariozechner/clipboard-linux-arm64-gnu": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-linux-arm64-gnu/-/clipboard-linux-arm64-gnu-0.3.6.tgz", + "integrity": "sha512-5JFf5rGofrm+V29HNF+wLthXphHdQpMbKDUYJ5tML6/Z5DLlLOV/9Ak4kDPtYyZ+Dzf+kAusE0VsFg4+tfP1IA==", + "license": "MIT", + "engines": { + "node": ">= 10" + }, + "os": [ + "linux" + ], + "cpu": [ + "arm64" + ], + "optional": true + }, + "node_modules/@mariozechner/clipboard-linux-arm64-musl": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-linux-arm64-musl/-/clipboard-linux-arm64-musl-0.3.6.tgz", + "integrity": "sha512-JlVjxxw0GbGC0djXYWRIqyteO3J1KZ/QG3udlEFaOD5TLOM1FnmXXAPDQBqr+aBVr720ef9K00dirYnJ0LDCtw==", + "license": "MIT", + "engines": { + "node": ">= 10" + }, + "os": [ + "linux" + ], + "cpu": [ + "arm64" + ], + "optional": true + }, + "node_modules/@mariozechner/clipboard-linux-riscv64-gnu": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-linux-riscv64-gnu/-/clipboard-linux-riscv64-gnu-0.3.6.tgz", + "integrity": "sha512-4t8BUi5zZ+L77otFQVnVSlaTyAX4TVk9EqQm4syMrEQp96trFEHEwwNHcNEBGzYv5+K7mxay50TthYkz47OWzQ==", + "license": "MIT", + "engines": { + "node": ">= 10" + }, + "os": [ + "linux" + ], + "cpu": [ + "riscv64" + ], + "optional": true + }, + "node_modules/@mariozechner/clipboard-linux-x64-gnu": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-linux-x64-gnu/-/clipboard-linux-x64-gnu-0.3.6.tgz", + "integrity": "sha512-trtPwcNLW37irwQCJLtCxLw757jjJZk3TSnY/MU9bhtWtA3K9b/eLW0e4RGhUXDoFRds9opNWWaUDuFLa8dm0w==", + "license": "MIT", + "engines": { + "node": ">= 10" + }, + "os": [ + "linux" + ], + "cpu": [ + "x64" + ], + "optional": true + }, + "node_modules/@mariozechner/clipboard-linux-x64-musl": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-linux-x64-musl/-/clipboard-linux-x64-musl-0.3.6.tgz", + "integrity": "sha512-WfnzIvOCCWQiN0MmltCEo6cLceUDbYe+I7xyFZjaps5A+2Op/M2CY7Rey+C4ucQhrvmpoHmTSFgY9ODWk7snoA==", + "license": "MIT", + "engines": { + "node": ">= 10" + }, + "os": [ + "linux" + ], + "cpu": [ + "x64" + ], + "optional": true + }, + "node_modules/@mariozechner/clipboard-win32-arm64-msvc": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-win32-arm64-msvc/-/clipboard-win32-arm64-msvc-0.3.6.tgz", + "integrity": "sha512-+8+1aHYsBPUjmW3otmWlg+Hijt0iJvoBBs5e0mxFeUd4gDaKMB8Bn6x7c6KVtscg7E5j5NFXnwQqNSIAO4p8zQ==", + "license": "MIT", + "engines": { + "node": ">= 10" + }, + "os": [ + "win32" + ], + "cpu": [ + "arm64" + ], + "optional": true + }, + "node_modules/@mariozechner/clipboard-win32-x64-msvc": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-win32-x64-msvc/-/clipboard-win32-x64-msvc-0.3.6.tgz", + "integrity": "sha512-S4xfPmERC8ZkiLHe3vekZCjdDwNEETCuvCgQK2kP6/TnvmUkq1y2Pk+DjM4t8uh9KMX9bH4zs5ePcKa8GTXmfg==", + "license": "MIT", + "engines": { + "node": ">= 10" + }, + "os": [ + "win32" + ], + "cpu": [ + "x64" + ], + "optional": true + }, + "node_modules/@mistralai/mistralai": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@mistralai/mistralai/-/mistralai-2.2.1.tgz", + "integrity": "sha512-uKU8CZmL2RzYKmplsU01hii4p3pe4HqJefpWNRWXm1Tcm0Sm4xXfwSLIy4k7ZCPlbETCGcp69E7hZs+WOJ5itQ==", + "license": "Apache-2.0", + "dependencies": { + "ws": "^8.18.0", + "zod": "^3.25.0 || ^4.0.0", + "zod-to-json-schema": "^3.25.0" + } + }, + "node_modules/@nodable/entities": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@nodable/entities/-/entities-2.1.0.tgz", + "integrity": "sha512-nyT7T3nbMyBI/lvr6L5TyWbFJAI9FTgVRakNoBqCD+PmID8DzFrrNdLLtHMwMszOtqZa8PAOV24ZqDnQrhQINA==", + "license": "MIT", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/nodable" + } + ] + }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.5.tgz", + "integrity": "sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.1.tgz", + "integrity": "sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw==", + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/inquire": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.2.tgz", + "integrity": "sha512-pa0vFRuws4wkvaXKK1uXZMAwAX4/t8ANaJo45iw/oQHNQ9q5xUzwgFmVJGXiga2BeN+zpX7Vf9vmsiIa2J+MUw==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.1.tgz", + "integrity": "sha512-oOAWABowe8EAbMyWKM0tYDKi8Yaox52D+HWZhAIJqQXbqe0xI/GV7FhLWqlEKreMkfDjshR5FKgi3mnle0h6Eg==", + "license": "BSD-3-Clause" + }, + "node_modules/@silvia-odwyer/photon-node": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@silvia-odwyer/photon-node/-/photon-node-0.3.4.tgz", + "integrity": "sha512-bnly4BKB3KDTFxrUIcgCLbaeVVS8lrAkri1pEzskpmxu9MdfGQTy8b8EgcD83ywD3RPMsIulY8xJH5Awa+t9fA==", + "license": "Apache-2.0" + }, + "node_modules/@smithy/core": { + "version": "3.24.3", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.24.3.tgz", + "integrity": "sha512-Ep/7tPamGY8mgESE3LyLKtxJyy6U52WWAqr/3wial47Sj4u3PiIF73AOGI27UyLy9duTkhZbgzodOfLV4TduZg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/crc32": "5.2.0", + "@smithy/types": "^4.14.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/credential-provider-imds": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.3.3.tgz", + "integrity": "sha512-I2Bti0DKFo2IJyN28ijCsx51BAumEYR4/1yZ1FXyBygy9MqbnMqCev4JPth/MbpRfBSRAX35hITSnAdJRo1u5w==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.24.3", + "@smithy/types": "^4.14.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/fetch-http-handler": { + "version": "5.4.3", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.4.3.tgz", + "integrity": "sha512-F+DRf8IJazRJgYog2A/yJK7eYVc0rqTlRzO+5ZxjJd4WkZoKz0IJRncf7G6t1pdVT3kryJcwuTFhN1c5m6N47A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.24.3", + "@smithy/types": "^4.14.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@smithy/node-http-handler": { + "version": "4.7.3", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.7.3.tgz", + "integrity": "sha512-/jPhevcTFPMVl6KNjbaI47iOg1zxC7IsnX4PQDGVZKMFceOXtB8IEYaB7a9VvkP/3oC60WzTeKocvSI7vLT0vA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.24.3", + "@smithy/types": "^4.14.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/signature-v4": { + "version": "5.4.3", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.4.3.tgz", + "integrity": "sha512-53+75QuPl6DL+ct6vVEB51FDO5oulXr20TPV46VvJZg76lIlXNWfxi8j+G2V/t0I2qxCBOa3vX/8bmjrpFVo9g==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.24.3", + "@smithy/types": "^4.14.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/types": { + "version": "4.14.2", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.14.2.tgz", + "integrity": "sha512-P+otAxbV4CqBybp7EkcJCrig63yE2E7PuNVOmilVMRcx/O+QDzGULTrKsq4DV13gSfak9ObPrWaHl/9bL5YcWw==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@types/node": { + "version": "22.19.19", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.19.tgz", + "integrity": "sha512-dyh/xO2Fh5bYrfWaaqGrRQQGkNdmYw6AmaAUvYeUMNTWQtvb796ikLdmTchRmOlOiIJ1TDXfWgVx1QkUlQ6Hew==", + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "license": "MIT", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/bignumber.js": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz", + "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/bowser": { + "version": "2.14.1", + "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.14.1.tgz", + "integrity": "sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==", + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, + "node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + }, + "engines": { + "node": ">=6.0" + } + }, + "node_modules/diff": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/diff/-/diff-8.0.4.tgz", + "integrity": "sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT" + }, + "node_modules/fast-xml-builder": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.2.0.tgz", + "integrity": "sha512-00aAWieqff+ZJhsXA4g1g7M8k+7AYoMUUHF+/zFb5U6Uv/P0Vl4QZo84/IcufzYalLuEj9928bXN9PbbFzMF0Q==", + "license": "MIT", + "dependencies": { + "path-expression-matcher": "^1.5.0", + "xml-naming": "^0.1.0" + }, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ] + }, + "node_modules/fast-xml-parser": { + "version": "5.7.3", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.7.3.tgz", + "integrity": "sha512-C0AaNuC+mscy6vrAQKAc/rMq+zAPHodfHGZu4sGVehvAQt/JLG1O5zEcYcXSY5zSqr4YVgxsB+pHXTq0i7eDlg==", + "license": "MIT", + "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" + }, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ] + }, + "node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "license": "MIT", + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + }, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ] + }, + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "license": "MIT", + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/gaxios": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.1.4.tgz", + "integrity": "sha512-bTIgTsM2bWn3XklZISBTQX7ZSddGW+IO3bMdGaemHZ3tbqExMENHLx6kKZ/KlejgrMtj8q7wBItt51yegqalrA==", + "license": "Apache-2.0", + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "node-fetch": "^3.3.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/gcp-metadata": { + "version": "8.1.2", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-8.1.2.tgz", + "integrity": "sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg==", + "license": "Apache-2.0", + "dependencies": { + "gaxios": "^7.0.0", + "google-logging-utils": "^1.0.0", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/get-east-asian-width": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.6.0.tgz", + "integrity": "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob": { + "version": "13.0.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", + "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", + "license": "BlueOak-1.0.0", + "dependencies": { + "minimatch": "^10.2.2", + "minipass": "^7.1.3", + "path-scurry": "^2.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/google-auth-library": { + "version": "10.6.2", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-10.6.2.tgz", + "integrity": "sha512-e27Z6EThmVNNvtYASwQxose/G57rkRuaRbQyxM2bvYLLX/GqWZ5chWq2EBoUchJbCc57eC9ArzO5wMsEmWftCw==", + "license": "Apache-2.0", + "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" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/google-logging-utils": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-1.1.3.tgz", + "integrity": "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "node_modules/highlight.js": { + "version": "10.7.3", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz", + "integrity": "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==", + "license": "BSD-3-Clause", + "engines": { + "node": "*" + } + }, + "node_modules/hosted-git-info": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-9.0.3.tgz", + "integrity": "sha512-Hc+ghLoSt6QaYZUv0WBiIvmMDZuZZ7oaDvdH8MbfOO4lOsxdXLEvuC6ePoGs9H1X9oCLyq6+NVN0MKqD+ydxyg==", + "license": "ISC", + "dependencies": { + "lru-cache": "^11.1.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/jiti": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", + "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/json-bigint": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", + "license": "MIT", + "dependencies": { + "bignumber.js": "^9.0.0" + } + }, + "node_modules/json-schema-to-ts": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/json-schema-to-ts/-/json-schema-to-ts-3.1.1.tgz", + "integrity": "sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.3", + "ts-algebra": "^2.0.0" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/koffi": { + "version": "2.16.2", + "resolved": "https://registry.npmjs.org/koffi/-/koffi-2.16.2.tgz", + "integrity": "sha512-owU0MRwv6xkrVqCd+33uw6BaYppkTRXbO/rVdJNI2dvZG0gzyRhYwW25eWtc5pauwK8TGh3AbkFONSezdykfSA==", + "license": "MIT", + "optional": true, + "hasInstallScript": true, + "funding": { + "url": "https://liberapay.com/Koromix" + } + }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0" + }, + "node_modules/lru-cache": { + "version": "11.4.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.4.0.tgz", + "integrity": "sha512-W+R+kFL4HgVxONq2bhXPi3bGpzGe/yEhVOp233qw9wCRtgncJ15P3bC+e4zZMu4Cq7d+WAJjXGW0uUkifhcatA==", + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/marked": { + "version": "15.0.12", + "resolved": "https://registry.npmjs.org/marked/-/marked-15.0.12.tgz", + "integrity": "sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA==", + "license": "MIT", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "license": "MIT", + "engines": { + "node": ">=10.5.0" + }, + "deprecated": "Use your platform's native DOMException instead", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ] + }, + "node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "license": "MIT", + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, + "node_modules/openai": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/openai/-/openai-6.26.0.tgz", + "integrity": "sha512-zd23dbWTjiJ6sSAX6s0HrCZi41JwTA1bQVs0wLQPZ2/5o2gxOJA5wh7yOAUgwYybfhDXyhwlpeQf7Mlgx8EOCA==", + "license": "Apache-2.0", + "peerDependencies": { + "ws": "^8.18.0", + "zod": "^3.25 || ^4.0" + }, + "peerDependenciesMeta": { + "ws": { + "optional": true + }, + "zod": { + "optional": true + } + }, + "bin": { + "openai": "bin/cli" + } + }, + "node_modules/p-retry": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz", + "integrity": "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==", + "license": "MIT", + "dependencies": { + "@types/retry": "0.12.0", + "retry": "^0.13.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-retry/node_modules/@types/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==", + "license": "MIT" + }, + "node_modules/partial-json": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/partial-json/-/partial-json-0.1.7.tgz", + "integrity": "sha512-Njv/59hHaokb/hRUjce3Hdv12wd60MtM9Z5Olmn+nehe0QDAsRtRbJPvJ0Z91TusF0SuZRIvnM+S4l6EIP8leA==", + "license": "MIT" + }, + "node_modules/path-expression-matcher": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.5.0.tgz", + "integrity": "sha512-cbrerZV+6rvdQrrD+iGMcZFEiiSrbv9Tfdkvnusy6y0x0GKBXREFg/Y65GhIfm0tnLntThhzCnfKwp1WRjeCyQ==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + }, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ] + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", + "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/proper-lockfile": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz", + "integrity": "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "retry": "^0.12.0", + "signal-exit": "^3.0.2" + } + }, + "node_modules/proper-lockfile/node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/protobufjs": { + "version": "7.5.9", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.9.tgz", + "integrity": "sha512-Od4muIm3HW1AouyHF5lONOf1FWo3hY1NbFDoy191X9GzhpgW1clCoaFjfVs2rKJNFYpTNJbje4cbAIDBZJ63ZA==", + "license": "BSD-3-Clause", + "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.0.0" + }, + "engines": { + "node": ">=12.0.0" + }, + "hasInstallScript": true + }, + "node_modules/retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "license": "MIT", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "license": "ISC" + }, + "node_modules/strnum": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.3.0.tgz", + "integrity": "sha512-ums3KNd42PGyx5xaoVTO1mjU1bH3NpY4vsrVlnv9PNGqQj8wd7rJ6nEypLrJ7z5vxK5RP0yMLo6J/Gsm62DI5Q==", + "license": "MIT", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ] + }, + "node_modules/ts-algebra": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ts-algebra/-/ts-algebra-2.0.0.tgz", + "integrity": "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==", + "license": "MIT" + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/typebox": { + "version": "1.1.38", + "resolved": "https://registry.npmjs.org/typebox/-/typebox-1.1.38.tgz", + "integrity": "sha512-pZ0aQPmMmXoUvSbeuWf/Hzsc+avNw/Zd6VeE8CFgkVGWyuHPJvqeJJDeJqLve+K70LvjYIoleGcoJHPT17cWoA==", + "license": "MIT" + }, + "node_modules/undici": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-8.3.0.tgz", + "integrity": "sha512-TkUDgb6tl7KOGZ+7e8E3d2FYgUQgF6z5YypqjWmixVQSQERFcVrVg0ySADm2LVLRh5ljAaHTCR5Fmz3Q34rB7Q==", + "license": "MIT", + "engines": { + "node": ">=22.19.0" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "license": "MIT" + }, + "node_modules/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/ws": { + "version": "8.20.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.1.tgz", + "integrity": "sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w==", + "license": "MIT", + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/xml-naming": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/xml-naming/-/xml-naming-0.1.0.tgz", + "integrity": "sha512-k8KO9hrMyNk6tUWqUfkTEZbezRRpONVOzUTnc97VnCvyj6Tf9lyUR9EDAIeiVLv56jsMcoXEwjW8Kv5yPY52lw==", + "license": "MIT", + "engines": { + "node": ">=16.0.0" + }, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ] + }, + "node_modules/yaml": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", + "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-to-json-schema": { + "version": "3.25.2", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.2.tgz", + "integrity": "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==", + "license": "ISC", + "peerDependencies": { + "zod": "^3.25.28 || ^4" + } + } + } +} diff --git a/packages/coding-agent/package.json b/packages/coding-agent/package.json index cd83c380..23a82422 100644 --- a/packages/coding-agent/package.json +++ b/packages/coding-agent/package.json @@ -25,7 +25,8 @@ "dist", "docs", "examples", - "CHANGELOG.md" + "CHANGELOG.md", + "npm-shrinkwrap.json" ], "scripts": { "clean": "shx rm -rf dist", @@ -34,7 +35,8 @@ "copy-assets": "shx mkdir -p dist/modes/interactive/theme && shx cp src/modes/interactive/theme/*.json dist/modes/interactive/theme/ && shx mkdir -p dist/modes/interactive/assets && shx cp src/modes/interactive/assets/*.png dist/modes/interactive/assets/ && shx mkdir -p dist/core/export-html/vendor && shx cp src/core/export-html/template.html src/core/export-html/template.css src/core/export-html/template.js dist/core/export-html/ && shx cp src/core/export-html/vendor/*.js dist/core/export-html/vendor/", "copy-binary-assets": "shx cp package.json dist/ && shx cp README.md dist/ && shx cp CHANGELOG.md dist/ && shx mkdir -p dist/theme && shx cp src/modes/interactive/theme/*.json dist/theme/ && shx mkdir -p dist/assets && shx cp src/modes/interactive/assets/*.png dist/assets/ && shx mkdir -p dist/export-html/vendor && shx cp src/core/export-html/template.html dist/export-html/ && shx cp src/core/export-html/vendor/*.js dist/export-html/vendor/ && shx cp -r docs dist/ && shx cp -r examples dist/ && shx cp ../../node_modules/@silvia-odwyer/photon-node/photon_rs_bg.wasm dist/", "test": "vitest --run", - "prepublishOnly": "npm run clean && npm run build" + "shrinkwrap": "node ../../scripts/generate-coding-agent-shrinkwrap.mjs", + "prepublishOnly": "npm run clean && npm run build && npm run shrinkwrap" }, "dependencies": { "@earendil-works/pi-agent-core": "^0.75.3", diff --git a/scripts/build-binaries.sh b/scripts/build-binaries.sh index e66e4390..1c1bf076 100755 --- a/scripts/build-binaries.sh +++ b/scripts/build-binaries.sh @@ -65,7 +65,7 @@ if [[ "$SKIP_DEPS" == "false" ]]; then # We need all platform bindings for bun cross-compilation # Use --force to bypass platform checks (os/cpu restrictions in package.json) # Install all in one command to avoid npm removing packages from previous installs - npm install --no-save --force --ignore-scripts \ + npm install --no-save --package-lock=false --force --ignore-scripts \ @mariozechner/clipboard-darwin-arm64@0.3.2 \ @mariozechner/clipboard-darwin-x64@0.3.2 \ @mariozechner/clipboard-linux-x64-gnu@0.3.2 \ diff --git a/scripts/generate-coding-agent-shrinkwrap.mjs b/scripts/generate-coding-agent-shrinkwrap.mjs new file mode 100644 index 00000000..563b0528 --- /dev/null +++ b/scripts/generate-coding-agent-shrinkwrap.mjs @@ -0,0 +1,340 @@ +#!/usr/bin/env node + +import { existsSync, readFileSync, writeFileSync } from "node:fs"; +import { dirname, join, posix, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; + +const scriptDir = dirname(fileURLToPath(import.meta.url)); +const repoRoot = resolve(scriptDir, ".."); +const codingAgentDir = join(repoRoot, "packages/coding-agent"); +const rootLockfilePath = join(repoRoot, "package-lock.json"); +const shrinkwrapPath = join(codingAgentDir, "npm-shrinkwrap.json"); +const internalPackagePrefix = "@earendil-works/pi-"; + +const args = new Set(process.argv.slice(2)); +const checkOnly = args.has("--check"); + +for (const arg of args) { + if (arg !== "--check") { + console.error(`Unknown argument: ${arg}`); + process.exit(1); + } +} + +function readJson(path) { + return JSON.parse(readFileSync(path, "utf8")); +} + +function packageDependencies(entry) { + return { + ...(entry.dependencies ?? {}), + ...(entry.optionalDependencies ?? {}), + }; +} + +function sortedObject(object) { + return Object.fromEntries(Object.entries(object).sort(([a], [b]) => a.localeCompare(b))); +} + +function sortedPackageEntry(entry) { + const fieldOrder = [ + "name", + "version", + "resolved", + "integrity", + "license", + "dependencies", + "optionalDependencies", + "peerDependencies", + "peerDependenciesMeta", + "bin", + "engines", + "os", + "cpu", + "libc", + "optional", + "hasInstallScript", + "deprecated", + "funding", + ]; + const sorted = {}; + + for (const field of fieldOrder) { + if (entry[field] !== undefined) { + sorted[field] = entry[field]; + } + } + for (const [field, value] of Object.entries(entry).sort(([a], [b]) => a.localeCompare(b))) { + if (sorted[field] === undefined) { + sorted[field] = value; + } + } + return sorted; +} + +function copyLockEntry(entry) { + const copied = { ...entry }; + delete copied.dev; + delete copied.devOptional; + delete copied.extraneous; + delete copied.link; + return sortedPackageEntry(copied); +} + +function copyPackageJsonEntry(packageJson, options) { + const entry = options.includeName + ? { name: packageJson.name, version: packageJson.version } + : { version: packageJson.version }; + + for (const field of [ + "license", + "dependencies", + "optionalDependencies", + "peerDependencies", + "peerDependenciesMeta", + "bin", + "engines", + "os", + "cpu", + "libc", + ]) { + if (packageJson[field] !== undefined) { + entry[field] = packageJson[field]; + } + } + + return sortedPackageEntry(entry); +} + +function packageNameFromLockPath(lockPath) { + const marker = "node_modules/"; + const index = lockPath.lastIndexOf(marker); + if (index === -1) { + return undefined; + } + + const parts = lockPath.slice(index + marker.length).split("/"); + if (parts[0]?.startsWith("@")) { + return `${parts[0]}/${parts[1]}`; + } + return parts[0]; +} + +function registryTarballUrl(packageName, version) { + const tarballName = packageName.startsWith("@") ? packageName.split("/")[1] : packageName; + return `https://registry.npmjs.org/${packageName}/-/${tarballName}-${version}.tgz`; +} + +function getInternalWorkspaces(lockPackages) { + const workspaces = new Map(); + + for (const [lockPath, entry] of Object.entries(lockPackages)) { + if (!lockPath.startsWith("packages/") || lockPath.includes("/node_modules/") || !entry.name || !entry.version) { + continue; + } + if (!entry.name.startsWith(internalPackagePrefix)) { + continue; + } + + workspaces.set(entry.name, { + lockPath, + packageJson: readJson(join(repoRoot, lockPath, "package.json")), + }); + } + + return workspaces; +} + +function resolveExternalDependency(lockPackages, packageName, fromLockPath) { + const candidateDirs = []; + let current = fromLockPath; + + while (current) { + candidateDirs.push(current); + const parent = posix.dirname(current); + if (parent === "." || parent === current) { + break; + } + current = parent; + } + candidateDirs.push(""); + + const tried = new Set(); + for (const directory of candidateDirs) { + const candidate = directory ? `${directory}/node_modules/${packageName}` : `node_modules/${packageName}`; + if (tried.has(candidate)) { + continue; + } + tried.add(candidate); + + const entry = lockPackages[candidate]; + if (entry && !entry.link) { + return candidate; + } + } + + const suffix = `node_modules/${packageName}`; + const matches = Object.entries(lockPackages) + .filter(([lockPath, entry]) => !entry.link && (lockPath === suffix || lockPath.endsWith(`/${suffix}`))) + .map(([lockPath]) => lockPath); + + if (matches.length === 1) { + return matches[0]; + } + + throw new Error( + `Cannot resolve ${packageName} from ${fromLockPath || "root"}. ` + + (matches.length > 1 ? `Matches: ${matches.join(", ")}` : "No matching lockfile entry found."), + ); +} + +function addInternalWorkspace(shrinkwrapPackages, addedPaths, queue, name, workspace) { + const packageJson = workspace.packageJson; + const outputPath = `node_modules/${name}`; + const entry = copyPackageJsonEntry(packageJson, { includeName: false }); + entry.resolved = registryTarballUrl(name, packageJson.version); + + shrinkwrapPackages[outputPath] = sortedPackageEntry(entry); + addedPaths.add(outputPath); + + for (const dependencyName of Object.keys(packageDependencies(packageJson))) { + queue.push({ name: dependencyName, from: outputPath }); + } +} + +function addExternalPackage(lockPackages, shrinkwrapPackages, addedPaths, queue, name, from) { + const lockPath = resolveExternalDependency(lockPackages, name, from); + if (addedPaths.has(lockPath)) { + return; + } + + const entry = lockPackages[lockPath]; + shrinkwrapPackages[lockPath] = copyLockEntry(entry); + addedPaths.add(lockPath); + + for (const dependencyName of Object.keys(packageDependencies(entry))) { + queue.push({ name: dependencyName, from: lockPath }); + } +} + +function validateShrinkwrap(shrinkwrap, internalNames) { + const errors = []; + const includedPaths = new Set(Object.keys(shrinkwrap.packages)); + const includedPackageNames = new Set(); + + for (const [lockPath, entry] of Object.entries(shrinkwrap.packages)) { + const packageName = packageNameFromLockPath(lockPath); + if (packageName) { + includedPackageNames.add(packageName); + } + if (entry.link) { + errors.push(`${lockPath} is a link entry`); + } + if (typeof entry.resolved === "string" && /^(file:|link:|workspace:|\.\.?\/|\/)/.test(entry.resolved)) { + errors.push(`${lockPath} has a local resolved value: ${entry.resolved}`); + } + } + + for (const name of internalNames) { + if (!includedPackageNames.has(name)) { + errors.push(`internal dependency ${name} is missing`); + } + } + + for (const [lockPath, entry] of Object.entries(shrinkwrap.packages)) { + for (const dependencyName of Object.keys(packageDependencies(entry))) { + const dependencyIncluded = [...includedPaths].some( + (candidate) => candidate === `node_modules/${dependencyName}` || candidate.endsWith(`/node_modules/${dependencyName}`), + ); + if (!dependencyIncluded) { + errors.push(`${lockPath || "root"} dependency ${dependencyName} is missing`); + } + } + } + + const platformPackageCount = Object.values(shrinkwrap.packages).filter((entry) => entry.os || entry.cpu || entry.libc).length; + if (platformPackageCount === 0) { + errors.push("no platform-specific optional dependency entries found"); + } + + if (errors.length > 0) { + throw new Error(`Generated shrinkwrap failed validation:\n${errors.map((error) => ` - ${error}`).join("\n")}`); + } +} + +function generateShrinkwrap() { + const rootLock = readJson(rootLockfilePath); + if (rootLock.lockfileVersion !== 3 || !rootLock.packages) { + throw new Error("package-lock.json must be lockfileVersion 3 and contain a packages map"); + } + + const lockPackages = rootLock.packages; + const codingAgentPackage = readJson(join(codingAgentDir, "package.json")); + const internalWorkspaces = getInternalWorkspaces(lockPackages); + const shrinkwrapPackages = { + "": copyPackageJsonEntry(codingAgentPackage, { includeName: true }), + }; + const addedPaths = new Set([""]); + const internalNames = new Set(); + const queue = Object.keys(packageDependencies(codingAgentPackage)).map((name) => ({ name, from: "" })); + + while (queue.length > 0) { + const item = queue.shift(); + if (!item) { + break; + } + + const workspace = internalWorkspaces.get(item.name); + if (workspace) { + const outputPath = `node_modules/${item.name}`; + internalNames.add(item.name); + if (!addedPaths.has(outputPath)) { + addInternalWorkspace(shrinkwrapPackages, addedPaths, queue, item.name, workspace); + } + continue; + } + + addExternalPackage(lockPackages, shrinkwrapPackages, addedPaths, queue, item.name, item.from); + } + + const shrinkwrap = { + name: codingAgentPackage.name, + version: codingAgentPackage.version, + lockfileVersion: 3, + requires: true, + packages: sortedObject(shrinkwrapPackages), + }; + + validateShrinkwrap(shrinkwrap, internalNames); + return shrinkwrap; +} + +try { + const shrinkwrap = generateShrinkwrap(); + const content = `${JSON.stringify(shrinkwrap, null, "\t")}\n`; + + if (checkOnly) { + if (!existsSync(shrinkwrapPath)) { + console.error("packages/coding-agent/npm-shrinkwrap.json is missing."); + console.error("Run: npm run shrinkwrap:coding-agent"); + process.exit(1); + } + const current = readFileSync(shrinkwrapPath, "utf8"); + if (current !== content) { + console.error("packages/coding-agent/npm-shrinkwrap.json is out of date."); + console.error("Run: npm run shrinkwrap:coding-agent"); + process.exit(1); + } + console.log("packages/coding-agent/npm-shrinkwrap.json is up to date."); + } else { + writeFileSync(shrinkwrapPath, content); + const packageCount = Object.keys(shrinkwrap.packages).length - 1; + const platformPackageCount = Object.values(shrinkwrap.packages).filter((entry) => entry.os || entry.cpu || entry.libc).length; + console.log( + `Wrote packages/coding-agent/npm-shrinkwrap.json (${packageCount} packages, ${platformPackageCount} platform-specific).`, + ); + } +} catch (error) { + console.error(error instanceof Error ? error.message : String(error)); + process.exit(1); +} diff --git a/scripts/release.mjs b/scripts/release.mjs index 73ae06f9..59b51509 100755 --- a/scripts/release.mjs +++ b/scripts/release.mjs @@ -10,10 +10,11 @@ * 1. Check for uncommitted changes * 2. Bump version via npm run version:xxx or set an explicit version * 3. Update CHANGELOG.md files: [Unreleased] -> [version] - date - * 4. Commit and tag - * 5. Publish to npm - * 6. Add new [Unreleased] section to changelogs - * 7. Commit + * 4. Generate the coding-agent npm-shrinkwrap.json + * 5. Commit and tag + * 6. Publish to npm + * 7. Add new [Unreleased] section to changelogs + * 8. Commit */ import { execSync } from "child_process"; @@ -90,9 +91,7 @@ function bumpOrSetVersion(target) { } console.log(`Setting explicit version (${target})...`); - run( - `npm version ${target} -ws --no-git-tag-version && node scripts/sync-versions.js && npx shx rm -rf node_modules packages/*/node_modules package-lock.json && npm install`, - ); + run(`npm version ${target} -ws --no-git-tag-version && node scripts/sync-versions.js && npm install --package-lock-only`); return getVersion(); } @@ -164,30 +163,35 @@ console.log("Updating CHANGELOG.md files..."); updateChangelogsForRelease(version); console.log(); -// 4. Commit and tag +// 4. Generate publish shrinkwrap +console.log("Generating coding-agent shrinkwrap..."); +run("npm run shrinkwrap:coding-agent"); +console.log(); + +// 5. Commit and tag console.log("Committing and tagging..."); stageChangedFiles(); run(`git commit -m "Release v${version}"`); run(`git tag v${version}`); console.log(); -// 5. Publish +// 6. Publish console.log("Publishing to npm..."); run("npm run publish"); console.log(); -// 6. Add new [Unreleased] sections +// 7. Add new [Unreleased] sections console.log("Adding [Unreleased] sections for next cycle..."); addUnreleasedSection(); console.log(); -// 7. Commit +// 8. Commit console.log("Committing changelog updates..."); stageChangedFiles(); run(`git commit -m "Add [Unreleased] section for next cycle"`); console.log(); -// 8. Push +// 9. Push console.log("Pushing to remote..."); run("git push origin main"); run(`git push origin v${version}`); From a3ebcd2322be07187a69a188b6af21051d2a359a Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Wed, 20 May 2026 14:42:45 +0200 Subject: [PATCH 141/166] fix(coding-agent): disable scripts during self-update --- packages/coding-agent/src/config.ts | 14 +++-- packages/coding-agent/test/config.test.ts | 65 +++++++++++++---------- 2 files changed, 47 insertions(+), 32 deletions(-) diff --git a/packages/coding-agent/src/config.ts b/packages/coding-agent/src/config.ts index 1a94ea02..e55fa5f3 100644 --- a/packages/coding-agent/src/config.ts +++ b/packages/coding-agent/src/config.ts @@ -110,21 +110,21 @@ function getSelfUpdateCommandForMethod( return undefined; case "pnpm": return makeSelfUpdateCommand( - makeSelfUpdateCommandStep("pnpm", ["install", "-g", updatePackageName]), + makeSelfUpdateCommandStep("pnpm", ["install", "-g", "--ignore-scripts", updatePackageName]), updatePackageName === installedPackageName ? undefined : makeSelfUpdateCommandStep("pnpm", ["remove", "-g", installedPackageName]), ); case "yarn": return makeSelfUpdateCommand( - makeSelfUpdateCommandStep("yarn", ["global", "add", updatePackageName]), + makeSelfUpdateCommandStep("yarn", ["global", "add", "--ignore-scripts", updatePackageName]), updatePackageName === installedPackageName ? undefined : makeSelfUpdateCommandStep("yarn", ["global", "remove", installedPackageName]), ); case "bun": return makeSelfUpdateCommand( - makeSelfUpdateCommandStep("bun", ["install", "-g", updatePackageName]), + makeSelfUpdateCommandStep("bun", ["install", "-g", "--ignore-scripts", updatePackageName]), updatePackageName === installedPackageName ? undefined : makeSelfUpdateCommandStep("bun", ["uninstall", "-g", installedPackageName]), @@ -133,7 +133,13 @@ function getSelfUpdateCommandForMethod( const [command = "npm", ...npmArgs] = npmCommand ?? []; const inferred = npmCommand?.length ? undefined : getInferredNpmInstall(); const prefixArgs = [...npmArgs, ...(inferred ? ["--prefix", inferred.prefix] : [])]; - const installStep = makeSelfUpdateCommandStep(command, [...prefixArgs, "install", "-g", updatePackageName]); + const installStep = makeSelfUpdateCommandStep(command, [ + ...prefixArgs, + "install", + "-g", + "--ignore-scripts", + updatePackageName, + ]); const uninstallStep = updatePackageName === installedPackageName ? undefined diff --git a/packages/coding-agent/test/config.test.ts b/packages/coding-agent/test/config.test.ts index c22ff12b..6b90778a 100644 --- a/packages/coding-agent/test/config.test.ts +++ b/packages/coding-agent/test/config.test.ts @@ -153,7 +153,7 @@ describe("detectInstallMethod", () => { expect(detectInstallMethod()).toBe("pnpm"); expect(getUpdateInstruction("@earendil-works/pi-coding-agent")).toBe( - "Run: pnpm install -g @earendil-works/pi-coding-agent", + "Run: pnpm install -g --ignore-scripts @earendil-works/pi-coding-agent", ); }); @@ -175,8 +175,8 @@ describe("detectInstallMethod", () => { expect(detectInstallMethod()).toBe("npm"); expect(command).toEqual({ command: "npm", - args: ["--prefix", prefix, "install", "-g", "@earendil-works/pi-coding-agent"], - display: `npm --prefix ${prefix} install -g @earendil-works/pi-coding-agent`, + args: ["--prefix", prefix, "install", "-g", "--ignore-scripts", "@earendil-works/pi-coding-agent"], + display: `npm --prefix ${prefix} install -g --ignore-scripts @earendil-works/pi-coding-agent`, }); }); @@ -187,8 +187,8 @@ describe("detectInstallMethod", () => { expect(command).toEqual({ command: "npm", - args: ["--prefix", prefix, "install", "-g", "@new-scope/pi"], - display: `npm --prefix ${prefix} uninstall -g @mariozechner/pi-coding-agent && npm --prefix ${prefix} install -g @new-scope/pi`, + args: ["--prefix", prefix, "install", "-g", "--ignore-scripts", "@new-scope/pi"], + display: `npm --prefix ${prefix} uninstall -g @mariozechner/pi-coding-agent && npm --prefix ${prefix} install -g --ignore-scripts @new-scope/pi`, steps: [ { command: "npm", @@ -197,8 +197,8 @@ describe("detectInstallMethod", () => { }, { command: "npm", - args: ["--prefix", prefix, "install", "-g", "@new-scope/pi"], - display: `npm --prefix ${prefix} install -g @new-scope/pi`, + args: ["--prefix", prefix, "install", "-g", "--ignore-scripts", "@new-scope/pi"], + display: `npm --prefix ${prefix} install -g --ignore-scripts @new-scope/pi`, }, ], }); @@ -211,8 +211,8 @@ describe("detectInstallMethod", () => { expect(command).toEqual({ command: "npm", - args: ["--prefix", prefix, "install", "-g", "@earendil-works/pi-coding-agent"], - display: `npm --prefix ${prefix} install -g @earendil-works/pi-coding-agent`, + args: ["--prefix", prefix, "install", "-g", "--ignore-scripts", "@earendil-works/pi-coding-agent"], + display: `npm --prefix ${prefix} install -g --ignore-scripts @earendil-works/pi-coding-agent`, }); }); @@ -221,7 +221,14 @@ describe("detectInstallMethod", () => { const command = getSelfUpdateCommand("@earendil-works/pi-coding-agent", []); - expect(command?.args).toEqual(["--prefix", prefix, "install", "-g", "@earendil-works/pi-coding-agent"]); + expect(command?.args).toEqual([ + "--prefix", + prefix, + "install", + "-g", + "--ignore-scripts", + "@earendil-works/pi-coding-agent", + ]); }); test("quotes npm self-update display paths", () => { @@ -229,7 +236,9 @@ describe("detectInstallMethod", () => { const command = getSelfUpdateCommand("@earendil-works/pi-coding-agent"); - expect(command?.display).toBe(`npm --prefix "${prefix}" install -g @earendil-works/pi-coding-agent`); + expect(command?.display).toBe( + `npm --prefix "${prefix}" install -g --ignore-scripts @earendil-works/pi-coding-agent`, + ); }); test("does not infer Windows npm custom prefixes from package paths", () => { @@ -239,7 +248,7 @@ describe("detectInstallMethod", () => { expect(detectInstallMethod()).toBe("npm"); expect(getUpdateInstruction("@earendil-works/pi-coding-agent")).toBe( - "Run: npm install -g @earendil-works/pi-coding-agent", + "Run: npm install -g --ignore-scripts @earendil-works/pi-coding-agent", ); }); @@ -251,8 +260,8 @@ describe("detectInstallMethod", () => { expect(detectInstallMethod()).toBe("bun"); expect(command).toEqual({ command: "bun", - args: ["install", "-g", "@earendil-works/pi-coding-agent"], - display: "bun install -g @earendil-works/pi-coding-agent", + args: ["install", "-g", "--ignore-scripts", "@earendil-works/pi-coding-agent"], + display: "bun install -g --ignore-scripts @earendil-works/pi-coding-agent", }); }); @@ -264,8 +273,8 @@ describe("detectInstallMethod", () => { expect(detectInstallMethod()).toBe("pnpm"); expect(command).toEqual({ command: "pnpm", - args: ["install", "-g", "@new-scope/pi"], - display: "pnpm remove -g @mariozechner/pi-coding-agent && pnpm install -g @new-scope/pi", + args: ["install", "-g", "--ignore-scripts", "@new-scope/pi"], + display: "pnpm remove -g @mariozechner/pi-coding-agent && pnpm install -g --ignore-scripts @new-scope/pi", steps: [ { command: "pnpm", @@ -274,8 +283,8 @@ describe("detectInstallMethod", () => { }, { command: "pnpm", - args: ["install", "-g", "@new-scope/pi"], - display: "pnpm install -g @new-scope/pi", + args: ["install", "-g", "--ignore-scripts", "@new-scope/pi"], + display: "pnpm install -g --ignore-scripts @new-scope/pi", }, ], }); @@ -319,8 +328,8 @@ describe("detectInstallMethod", () => { expect(detectInstallMethod()).toBe("pnpm"); expect(command).toEqual({ command: "pnpm", - args: ["install", "-g", packageName], - display: `pnpm install -g ${packageName}`, + args: ["install", "-g", "--ignore-scripts", packageName], + display: `pnpm install -g --ignore-scripts ${packageName}`, }); }); @@ -332,8 +341,8 @@ describe("detectInstallMethod", () => { expect(detectInstallMethod()).toBe("yarn"); expect(command).toEqual({ command: "yarn", - args: ["global", "add", "@new-scope/pi"], - display: "yarn global remove @mariozechner/pi-coding-agent && yarn global add @new-scope/pi", + args: ["global", "add", "--ignore-scripts", "@new-scope/pi"], + display: "yarn global remove @mariozechner/pi-coding-agent && yarn global add --ignore-scripts @new-scope/pi", steps: [ { command: "yarn", @@ -342,8 +351,8 @@ describe("detectInstallMethod", () => { }, { command: "yarn", - args: ["global", "add", "@new-scope/pi"], - display: "yarn global add @new-scope/pi", + args: ["global", "add", "--ignore-scripts", "@new-scope/pi"], + display: "yarn global add --ignore-scripts @new-scope/pi", }, ], }); @@ -357,8 +366,8 @@ describe("detectInstallMethod", () => { expect(detectInstallMethod()).toBe("bun"); expect(command).toEqual({ command: "bun", - args: ["install", "-g", "@new-scope/pi"], - display: "bun uninstall -g @mariozechner/pi-coding-agent && bun install -g @new-scope/pi", + args: ["install", "-g", "--ignore-scripts", "@new-scope/pi"], + display: "bun uninstall -g @mariozechner/pi-coding-agent && bun install -g --ignore-scripts @new-scope/pi", steps: [ { command: "bun", @@ -367,8 +376,8 @@ describe("detectInstallMethod", () => { }, { command: "bun", - args: ["install", "-g", "@new-scope/pi"], - display: "bun install -g @new-scope/pi", + args: ["install", "-g", "--ignore-scripts", "@new-scope/pi"], + display: "bun install -g --ignore-scripts @new-scope/pi", }, ], }); From 5b7c5d21572e63aed4626b167e5ea9a2c736bb2d Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Wed, 20 May 2026 14:47:18 +0200 Subject: [PATCH 142/166] docs: recommend scriptless npm installs --- packages/coding-agent/README.md | 8 +++++--- packages/coding-agent/docs/index.md | 16 +++++++++------- packages/coding-agent/docs/quickstart.md | 4 +++- packages/coding-agent/docs/termux.md | 2 +- 4 files changed, 18 insertions(+), 12 deletions(-) diff --git a/packages/coding-agent/README.md b/packages/coding-agent/README.md index fedc53d2..79f284a0 100644 --- a/packages/coding-agent/README.md +++ b/packages/coding-agent/README.md @@ -68,13 +68,15 @@ I regularly publish my own `pi-mono` work sessions here: ## Quick Start ```bash -curl -fsSL https://pi.dev/install.sh | sh +npm install -g --ignore-scripts @earendil-works/pi-coding-agent ``` -Or with npm: +`--ignore-scripts` disables dependency lifecycle scripts during install. Pi does not require install scripts for normal npm installs. + +Installer alternative: ```bash -npm install -g @earendil-works/pi-coding-agent +curl -fsSL https://pi.dev/install.sh | sh ``` Authenticate with an API key: diff --git a/packages/coding-agent/docs/index.md b/packages/coding-agent/docs/index.md index ffe97865..75a527d6 100644 --- a/packages/coding-agent/docs/index.md +++ b/packages/coding-agent/docs/index.md @@ -4,18 +4,20 @@ Pi is a minimal terminal coding harness. It is designed to stay small at the cor ## Quick start -On linux or mac you can install Pi with curl: +Install Pi with npm: + +```bash +npm install -g --ignore-scripts @earendil-works/pi-coding-agent +``` + +`--ignore-scripts` disables dependency lifecycle scripts during install. Pi does not require install scripts for normal npm installs. + +On Linux or macOS, you can also use the installer: ```bash curl -fsSL https://pi.dev/install.sh | sh ``` -Or alternatively with npm: - -```bash -npm install -g @earendil-works/pi-coding-agent -``` - To uninstall pi itself, use npm for curl and npm installs: ```bash diff --git a/packages/coding-agent/docs/quickstart.md b/packages/coding-agent/docs/quickstart.md index 29f9896a..c7266346 100644 --- a/packages/coding-agent/docs/quickstart.md +++ b/packages/coding-agent/docs/quickstart.md @@ -7,9 +7,11 @@ This page gets you from install to a useful first pi session. Pi is distributed as an npm package: ```bash -npm install -g @earendil-works/pi-coding-agent +npm install -g --ignore-scripts @earendil-works/pi-coding-agent ``` +`--ignore-scripts` disables dependency lifecycle scripts during install. Pi does not require install scripts for normal npm installs. + ### Uninstall Use the package manager that installed pi. The curl installer uses npm globally, so curl and npm installs are removed with npm: diff --git a/packages/coding-agent/docs/termux.md b/packages/coding-agent/docs/termux.md index c9b9973c..aba6e292 100644 --- a/packages/coding-agent/docs/termux.md +++ b/packages/coding-agent/docs/termux.md @@ -17,7 +17,7 @@ pkg update && pkg upgrade pkg install nodejs termux-api git # Install pi -npm install -g @earendil-works/pi-coding-agent +npm install -g --ignore-scripts @earendil-works/pi-coding-agent # Create config directory mkdir -p ~/.pi/agent From 29851d01e42a14fa35714e3ccfff2dcd1f63f252 Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Wed, 20 May 2026 14:55:38 +0200 Subject: [PATCH 143/166] chore: enforce npm dependency age gate --- .npmrc | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 .npmrc diff --git a/.npmrc b/.npmrc new file mode 100644 index 00000000..d9b69800 --- /dev/null +++ b/.npmrc @@ -0,0 +1,2 @@ +save-exact=true +min-release-age=14 From aa4adac766d44c94a47a9552c400906cc9d42aae Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Wed, 20 May 2026 15:51:38 +0200 Subject: [PATCH 144/166] chore: run pi-test through tsx --- package-lock.json | 20 ++++++++++++++++++++ package.json | 1 + pi-test.sh | 2 +- 3 files changed, 22 insertions(+), 1 deletion(-) diff --git a/package-lock.json b/package-lock.json index 9af10d5b..4bb2dfe8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,6 +23,7 @@ "husky": "9.1.7", "jiti": "2.7.0", "shx": "0.4.0", + "tsx": "4.22.1", "typescript": "5.9.3" }, "engines": { @@ -5086,6 +5087,25 @@ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD" }, + "node_modules/tsx": { + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.22.1.tgz", + "integrity": "sha512-TvncJykhxAzFCk0VQZKBTClall4Pm7qXDSodb6uxi8QFa8X8mT6ABjxxsQ2opDRYxG7AzcRWXaFtruz5HJKuWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.28.0" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, "node_modules/tunnel-agent": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", diff --git a/package.json b/package.json index daa4b4e5..7a62003d 100644 --- a/package.json +++ b/package.json @@ -43,6 +43,7 @@ "husky": "9.1.7", "jiti": "2.7.0", "shx": "0.4.0", + "tsx": "4.22.1", "typescript": "5.9.3" }, "engines": { diff --git a/pi-test.sh b/pi-test.sh index 37ff0114..b726dd41 100755 --- a/pi-test.sh +++ b/pi-test.sh @@ -54,4 +54,4 @@ if [[ "$NO_ENV" == "true" ]]; then echo "Running without API keys..." fi -node "$SCRIPT_DIR/packages/coding-agent/src/cli.ts" ${ARGS[@]+"${ARGS[@]}"} +"$SCRIPT_DIR/node_modules/.bin/tsx" --tsconfig "$SCRIPT_DIR/tsconfig.json" "$SCRIPT_DIR/packages/coding-agent/src/cli.ts" ${ARGS[@]+"${ARGS[@]}"} From 17cc86a4793bedd13ba17ebec556a0a366816b6d Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Wed, 20 May 2026 15:53:18 +0200 Subject: [PATCH 145/166] chore: harden dependency workflows --- .github/workflows/ci.yml | 2 +- .github/workflows/npm-audit.yml | 31 +++++++ .husky/pre-commit | 5 ++ .npmrc | 2 +- README.md | 14 +++ scripts/build-binaries.sh | 2 +- scripts/check-lockfile-commit.mjs | 94 ++++++++++++++++++++ scripts/generate-coding-agent-shrinkwrap.mjs | 26 ++++++ scripts/local-release.mjs | 2 +- 9 files changed, 174 insertions(+), 4 deletions(-) create mode 100644 .github/workflows/npm-audit.yml create mode 100644 scripts/check-lockfile-commit.mjs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ef5b18da..a40a894c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -30,7 +30,7 @@ jobs: sudo ln -s $(which fdfind) /usr/local/bin/fd - name: Install dependencies - run: npm ci + run: npm ci --ignore-scripts - name: Build run: npm run build diff --git a/.github/workflows/npm-audit.yml b/.github/workflows/npm-audit.yml new file mode 100644 index 00000000..021b81ca --- /dev/null +++ b/.github/workflows/npm-audit.yml @@ -0,0 +1,31 @@ +name: npm audit + +on: + schedule: + - cron: '37 7 * * *' + workflow_dispatch: + +permissions: + contents: read + +jobs: + audit: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + + - name: Install dependencies without lifecycle scripts + run: npm ci --ignore-scripts --no-audit --no-fund + + - name: Audit production vulnerabilities + run: npm audit --omit=dev --audit-level=moderate + + - name: Verify registry signatures + run: npm audit signatures --omit=dev diff --git a/.husky/pre-commit b/.husky/pre-commit index 27d18fbc..2b21ec96 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -3,6 +3,11 @@ # Get list of staged files before running check STAGED_FILES=$(git diff --cached --name-only) +node scripts/check-lockfile-commit.mjs +if [ $? -ne 0 ]; then + exit 1 +fi + # Run the check script (formatting, linting, and type checking) echo "Running formatting, linting, and type checking..." npm run check diff --git a/.npmrc b/.npmrc index d9b69800..72acb9ad 100644 --- a/.npmrc +++ b/.npmrc @@ -1,2 +1,2 @@ save-exact=true -min-release-age=14 +min-release-age=2 diff --git a/README.md b/README.md index 6bdda547..d7e73cf0 100644 --- a/README.md +++ b/README.md @@ -70,6 +70,20 @@ npm run check # Lint, format, and type check ./pi-test.sh # Run pi from sources (can be run from any directory) ``` +## Supply-chain hardening + +We treat npm dependency changes as reviewed code changes. + +- Direct external dependencies are pinned to exact versions. Internal workspace packages remain version-ranged. +- `.npmrc` sets `save-exact=true` and `min-release-age=2` to avoid same-day dependency releases during npm resolution. +- `package-lock.json` is the dependency ground truth. Pre-commit blocks accidental lockfile commits unless `PI_ALLOW_LOCKFILE_CHANGE=1` is set. +- `npm run check` verifies pinned direct deps, native TypeScript import compatibility, and the generated coding-agent shrinkwrap. +- The published CLI package includes `packages/coding-agent/npm-shrinkwrap.json`, generated from the root lockfile, to pin transitive deps for npm users. +- Release smoke tests use `npm run release:local` to build, pack, and install outside the repo before publishing. +- Local release installs, documented npm installs, and `pi update --self` use `--ignore-scripts` where supported. +- CI installs with `npm ci --ignore-scripts`, and a scheduled GitHub workflow runs `npm audit --omit=dev` plus `npm audit signatures --omit=dev`. +- Shrinkwrap generation has an explicit allowlist for dependency lifecycle scripts; new lifecycle-script deps fail checks until reviewed. + ## License MIT diff --git a/scripts/build-binaries.sh b/scripts/build-binaries.sh index 1c1bf076..4b94de97 100755 --- a/scripts/build-binaries.sh +++ b/scripts/build-binaries.sh @@ -57,7 +57,7 @@ if [[ -n "$PLATFORM" ]]; then fi echo "==> Installing dependencies..." -npm ci +npm ci --ignore-scripts if [[ "$SKIP_DEPS" == "false" ]]; then echo "==> Installing cross-platform native bindings..." diff --git a/scripts/check-lockfile-commit.mjs b/scripts/check-lockfile-commit.mjs new file mode 100644 index 00000000..abf78947 --- /dev/null +++ b/scripts/check-lockfile-commit.mjs @@ -0,0 +1,94 @@ +#!/usr/bin/env node + +import { execFileSync } from "node:child_process"; + +const allowValue = process.env.PI_ALLOW_LOCKFILE_CHANGE; +const allowed = allowValue === "1" || allowValue === "true" || allowValue === "yes"; + +function git(args) { + return execFileSync("git", args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }); +} + +function readJsonFromGit(ref) { + try { + return JSON.parse(git(["show", ref])); + } catch { + return undefined; + } +} + +function packageNameFromLockPath(lockPath) { + const marker = "node_modules/"; + const index = lockPath.lastIndexOf(marker); + if (index === -1) return lockPath || ""; + const parts = lockPath.slice(index + marker.length).split("/"); + return parts[0]?.startsWith("@") ? `${parts[0]}/${parts[1]}` : parts[0]; +} + +function packageLabel(lockPath, entry) { + const name = entry?.name ?? packageNameFromLockPath(lockPath); + return entry?.version ? `${name}@${entry.version}` : name; +} + +function summarizeLockfileChange() { + const before = readJsonFromGit("HEAD:package-lock.json"); + const after = readJsonFromGit(":package-lock.json"); + if (!before?.packages || !after?.packages) return []; + + const changes = []; + const paths = new Set([...Object.keys(before.packages), ...Object.keys(after.packages)]); + for (const lockPath of [...paths].sort()) { + if (!lockPath.includes("node_modules/")) continue; + const oldEntry = before.packages[lockPath]; + const newEntry = after.packages[lockPath]; + if (!oldEntry && newEntry) { + changes.push(`added ${packageLabel(lockPath, newEntry)}`); + } else if (oldEntry && !newEntry) { + changes.push(`removed ${packageLabel(lockPath, oldEntry)}`); + } else if (oldEntry?.version !== newEntry?.version) { + changes.push( + `changed ${packageNameFromLockPath(lockPath)} ${oldEntry?.version ?? ""} -> ${newEntry?.version ?? ""}`, + ); + } + } + return changes; +} + +const stagedFiles = git(["diff", "--cached", "--name-only"]) + .split("\n") + .map((line) => line.trim()) + .filter(Boolean); + +if (!stagedFiles.includes("package-lock.json")) { + process.exit(0); +} + +if (allowed) { + console.error("package-lock.json is staged; PI_ALLOW_LOCKFILE_CHANGE is set, allowing commit."); + process.exit(0); +} + +console.error("package-lock.json is staged."); +console.error(""); +console.error("Review lockfile changes before committing:"); +console.error(" - confirm every new/updated package is intentional"); +console.error(" - confirm npm age gates were active for resolution"); +console.error(" - review any new lifecycle scripts in the dependency tree"); +console.error(" - regenerate/check coding-agent shrinkwrap if release deps changed"); + +const changes = summarizeLockfileChange(); +if (changes.length > 0) { + console.error(""); + console.error("Detected package version changes:"); + for (const change of changes.slice(0, 40)) { + console.error(` - ${change}`); + } + if (changes.length > 40) { + console.error(` ... ${changes.length - 40} more`); + } +} + +console.error(""); +console.error("If this lockfile change is intentional, commit with:"); +console.error(" PI_ALLOW_LOCKFILE_CHANGE=1 git commit ..."); +process.exit(1); diff --git a/scripts/generate-coding-agent-shrinkwrap.mjs b/scripts/generate-coding-agent-shrinkwrap.mjs index 563b0528..a122f183 100644 --- a/scripts/generate-coding-agent-shrinkwrap.mjs +++ b/scripts/generate-coding-agent-shrinkwrap.mjs @@ -10,6 +10,11 @@ const codingAgentDir = join(repoRoot, "packages/coding-agent"); const rootLockfilePath = join(repoRoot, "package-lock.json"); const shrinkwrapPath = join(codingAgentDir, "npm-shrinkwrap.json"); const internalPackagePrefix = "@earendil-works/pi-"; +const allowedInstallScriptPackages = new Map([ + ["@google/genai@1.52.0", "preinstall is a no-op in the published package"], + ["koffi@2.16.2", "optional native package ships prebuilt modules used without install scripts"], + ["protobufjs@7.5.9", "postinstall only warns about protobufjs version scheme mismatches"], +]); const args = new Set(process.argv.slice(2)); const checkOnly = args.has("--check"); @@ -221,6 +226,7 @@ function validateShrinkwrap(shrinkwrap, internalNames) { const errors = []; const includedPaths = new Set(Object.keys(shrinkwrap.packages)); const includedPackageNames = new Set(); + const seenAllowedInstallScriptPackages = new Set(); for (const [lockPath, entry] of Object.entries(shrinkwrap.packages)) { const packageName = packageNameFromLockPath(lockPath); @@ -233,6 +239,26 @@ function validateShrinkwrap(shrinkwrap, internalNames) { if (typeof entry.resolved === "string" && /^(file:|link:|workspace:|\.\.?\/|\/)/.test(entry.resolved)) { errors.push(`${lockPath} has a local resolved value: ${entry.resolved}`); } + if (entry.hasInstallScript) { + if (!packageName || !entry.version) { + errors.push(`${lockPath || "root"} has install scripts but no package name/version`); + } else { + const packageId = `${packageName}@${entry.version}`; + if (allowedInstallScriptPackages.has(packageId)) { + seenAllowedInstallScriptPackages.add(packageId); + } else { + errors.push( + `${lockPath} has install scripts (${packageId}). Review it and add it to allowedInstallScriptPackages if intentional.`, + ); + } + } + } + } + + for (const packageId of allowedInstallScriptPackages.keys()) { + if (!seenAllowedInstallScriptPackages.has(packageId)) { + errors.push(`allowed install-script package ${packageId} is no longer present; remove it from the allowlist`); + } } for (const name of internalNames) { diff --git a/scripts/local-release.mjs b/scripts/local-release.mjs index 31e07901..d5b4b416 100644 --- a/scripts/local-release.mjs +++ b/scripts/local-release.mjs @@ -167,7 +167,7 @@ if (!options.skipInstall) { `${JSON.stringify({ private: true, dependencies }, undefined, "\t")}\n`, ); - run("npm", ["install", "--omit=dev"], { cwd: installDirectory }); + run("npm", ["install", "--omit=dev", "--ignore-scripts"], { cwd: installDirectory }); mkdirSync(binDirectory, { recursive: true }); symlinkSync(join(installDirectory, "node_modules", ".bin", "pi"), join(binDirectory, "pi")); } From beefa6a4fed88422219b125153e82f7404353839 Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Wed, 20 May 2026 16:02:30 +0200 Subject: [PATCH 146/166] chore: add bun local release smoke install --- AGENTS.md | 8 +++-- README.md | 2 +- scripts/local-release.mjs | 68 +++++++++++++++++++++++++++------------ 3 files changed, 53 insertions(+), 25 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index a05fab61..4f1d1ccb 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -194,9 +194,11 @@ Create provider file exporting: ```bash npm run release:local -- --out /tmp/pi-local-release --force cd /tmp - /tmp/pi-local-release/bin/pi --help - /tmp/pi-local-release/bin/pi --version - /tmp/pi-local-release/bin/pi + /tmp/pi-local-release/node/pi --help + /tmp/pi-local-release/node/pi --version + /tmp/pi-local-release/node/pi + /tmp/pi-local-release/bun/pi --help + /tmp/pi-local-release/bun/pi --version ``` In the interactive smoke test, verify startup, model/account listing, and at least one real prompt with the intended default provider. Treat failures as release blockers unless the user explicitly accepts the risk. diff --git a/README.md b/README.md index d7e73cf0..750b3a61 100644 --- a/README.md +++ b/README.md @@ -79,7 +79,7 @@ We treat npm dependency changes as reviewed code changes. - `package-lock.json` is the dependency ground truth. Pre-commit blocks accidental lockfile commits unless `PI_ALLOW_LOCKFILE_CHANGE=1` is set. - `npm run check` verifies pinned direct deps, native TypeScript import compatibility, and the generated coding-agent shrinkwrap. - The published CLI package includes `packages/coding-agent/npm-shrinkwrap.json`, generated from the root lockfile, to pin transitive deps for npm users. -- Release smoke tests use `npm run release:local` to build, pack, and install outside the repo before publishing. +- Release smoke tests use `npm run release:local` to build, pack, and create isolated npm and Bun installs outside the repo before publishing. - Local release installs, documented npm installs, and `pi update --self` use `--ignore-scripts` where supported. - CI installs with `npm ci --ignore-scripts`, and a scheduled GitHub workflow runs `npm audit --omit=dev` plus `npm audit signatures --omit=dev`. - Shrinkwrap generation has an explicit allowlist for dependency lifecycle scripts; new lifecycle-script deps fail checks until reviewed. diff --git a/scripts/local-release.mjs b/scripts/local-release.mjs index d5b4b416..5cdc67bc 100644 --- a/scripts/local-release.mjs +++ b/scripts/local-release.mjs @@ -19,16 +19,17 @@ Builds and packs the publishable packages, then installs the tarballs into an isolated directory outside the repository for local release testing. Options: - --out Output directory. Defaults to a new directory under ${tmpdir()} - --force Remove --out first if it already exists - --skip-check Do not run npm run check before building - --skip-install Only create tarballs; do not create the isolated install - --help Show this help + --out Output directory. Defaults to a new directory under ${tmpdir()} + --force Remove --out first if it already exists + --skip-check Do not run npm run check before building + --skip-install Only create tarballs; do not create isolated installs + --skip-bun-install Do not create the isolated Bun install + --help Show this help `); } function parseArgs() { - const options = { force: false, outDir: undefined, skipCheck: false, skipInstall: false }; + const options = { force: false, outDir: undefined, skipBunInstall: false, skipCheck: false, skipInstall: false }; const args = process.argv.slice(2); for (let i = 0; i < args.length; i++) { @@ -49,6 +50,10 @@ function parseArgs() { options.skipInstall = true; continue; } + if (arg === "--skip-bun-install") { + options.skipBunInstall = true; + continue; + } if (arg === "--out") { const value = args[++i]; if (!value) { @@ -82,6 +87,10 @@ function readPackageJson(directory) { return JSON.parse(readFileSync(join(directory, "package.json"), "utf8")); } +function commandExists(command) { + return spawnSync(command, ["--version"], { stdio: "ignore" }).status === 0; +} + function isInsidePath(child, parent) { const relativePath = relative(parent, child); return relativePath === "" || (!relativePath.startsWith("..") && !isAbsolute(relativePath)); @@ -138,8 +147,8 @@ if (rootPackageJson.name !== "pi-monorepo") { const outDir = prepareOutputDirectory(options, repoRoot); const tarballDirectory = join(outDir, "tarballs"); -const installDirectory = join(outDir, "install"); -const binDirectory = join(outDir, "bin"); +const nodeInstallDirectory = join(outDir, "node"); +const bunInstallDirectory = join(outDir, "bun"); mkdirSync(tarballDirectory, { recursive: true }); if (!options.skipCheck) { @@ -158,18 +167,28 @@ for (const pkg of packages) { } if (!options.skipInstall) { - mkdirSync(installDirectory, { recursive: true }); + mkdirSync(nodeInstallDirectory, { recursive: true }); const dependencies = Object.fromEntries( - packages.map((pkg) => [pkg.name, fileSpecifier(installDirectory, tarballs.get(pkg.name))]), - ); - writeFileSync( - join(installDirectory, "package.json"), - `${JSON.stringify({ private: true, dependencies }, undefined, "\t")}\n`, + packages.map((pkg) => [pkg.name, fileSpecifier(nodeInstallDirectory, tarballs.get(pkg.name))]), ); + const installPackageJson = `${JSON.stringify({ private: true, dependencies }, undefined, "\t")}\n`; + writeFileSync(join(nodeInstallDirectory, "package.json"), installPackageJson); - run("npm", ["install", "--omit=dev", "--ignore-scripts"], { cwd: installDirectory }); - mkdirSync(binDirectory, { recursive: true }); - symlinkSync(join(installDirectory, "node_modules", ".bin", "pi"), join(binDirectory, "pi")); + run("npm", ["install", "--omit=dev", "--ignore-scripts"], { cwd: nodeInstallDirectory }); + symlinkSync(join("node_modules", ".bin", "pi"), join(nodeInstallDirectory, "pi")); + + if (!options.skipBunInstall) { + if (!commandExists("bun")) { + throw new Error("Bun is required for the isolated Bun install. Use --skip-bun-install to skip it."); + } + mkdirSync(bunInstallDirectory, { recursive: true }); + const bunDependencies = Object.fromEntries( + packages.map((pkg) => [pkg.name, fileSpecifier(bunInstallDirectory, tarballs.get(pkg.name))]), + ); + writeFileSync(join(bunInstallDirectory, "package.json"), `${JSON.stringify({ private: true, dependencies: bunDependencies }, undefined, "\t")}\n`); + run("bun", ["install", "--production", "--ignore-scripts"], { cwd: bunInstallDirectory }); + symlinkSync(join("node_modules", ".bin", "pi"), join(bunInstallDirectory, "pi")); + } } console.log("\nLocal release artifacts created:"); @@ -180,8 +199,15 @@ for (const tarball of tarballs.values()) { } if (!options.skipInstall) { - console.log("\nIsolated install:"); - console.log(` ${installDirectory}`); - console.log("\nRun the locally packed CLI from outside the repository:"); - console.log(` ${join(binDirectory, "pi")} --help`); + console.log("\nIsolated npm install:"); + console.log(` ${nodeInstallDirectory}`); + console.log("\nRun the locally packed npm CLI from outside the repository:"); + console.log(` ${join(nodeInstallDirectory, "pi")} --help`); + + if (!options.skipBunInstall) { + console.log("\nIsolated Bun install:"); + console.log(` ${bunInstallDirectory}`); + console.log("\nRun the locally packed Bun CLI from outside the repository:"); + console.log(` ${join(bunInstallDirectory, "pi")} --help`); + } } From cd2ae1b5448822b3af2b905316eb53de37715b18 Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Wed, 20 May 2026 16:05:33 +0200 Subject: [PATCH 147/166] docs(coding-agent): document fnm npm shim fix closes #4793 --- packages/coding-agent/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index f902be1e..9a2fb04e 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -16,6 +16,7 @@ - Fixed the subagent extension's parallel mode to return useful per-task output and failed-task diagnostics to the parent model instead of 100-character previews ([#4710](https://github.com/earendil-works/pi/issues/4710)). - Fixed Windows local bash execution to hide helper console windows when launched from background SDK processes ([#4699](https://github.com/earendil-works/pi/issues/4699)). - Fixed managed npm extension folders to set cloud-sync ignore metadata where supported ([#4763](https://github.com/earendil-works/pi/issues/4763)). +- Fixed Windows npm-family package commands for fnm-managed Node.js installs that expose both extensionless Unix scripts and `.cmd` shims ([#4793](https://github.com/earendil-works/pi/issues/4793)). ## [0.75.3] - 2026-05-18 From 144b93861f339ce353531f6873d377a1e4b2f5c4 Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Wed, 20 May 2026 16:10:23 +0200 Subject: [PATCH 148/166] docs: audit unreleased changelog entries --- packages/coding-agent/CHANGELOG.md | 20 ++++++++++++++++++++ packages/tui/CHANGELOG.md | 5 +++++ 2 files changed, 25 insertions(+) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 9a2fb04e..5e9053cd 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -2,20 +2,40 @@ ## [Unreleased] +### New Features + +- **Hardened npm install and release path** - Pi now ships the CLI with a generated shrinkwrap for transitive dependencies, blocks accidental lockfile changes, verifies dependency pinning and lifecycle-script allowlists in checks, disables lifecycle scripts for self-update and local release installs where supported, and smoke-tests isolated npm and Bun installs before release. See [Supply-chain hardening](../../README.md#supply-chain-hardening). + +### Added + +- Added interactive update notes after `pi update` runs, so users can see the installed version's changelog before continuing ([#4724](https://github.com/earendil-works/pi-mono/pull/4724) by [@mitsuhiko](https://github.com/mitsuhiko)). +- Exported image resize utilities from the package root for SDK consumers ([#4775](https://github.com/earendil-works/pi-mono/pull/4775) by [@xl0](https://github.com/xl0)). + ### Changed - Changed source syntax to avoid TypeScript constructs that require JavaScript emit, keeping core sources compatible with Node.js strip-only TypeScript checks. - Removed web UI workspace references from the CLI package and dropped the package-level development watch script. - Published npm installs now include an `npm-shrinkwrap.json` to lock transitive dependencies for the CLI package. +- Improved terminal theme detection for light/dark and truecolor handling. +- Changed self-update package-manager commands to disable lifecycle scripts during reinstall. ### Fixed - Fixed the system prompt to tell models to resolve pi docs and examples under the absolute package paths before reading topic-specific relative references ([#4752](https://github.com/earendil-works/pi/issues/4752)). - Fixed extension `ctx.abort()` during tool-call preflight to stop later confirmations and restore queued interactive input like Escape ([#4276](https://github.com/earendil-works/pi/issues/4276)). - Fixed AgentSession retry, compaction, and event settlement to use the awaited agent lifecycle instead of a separate event queue, and added `willRetry` to `agent_end` session events. +- Fixed forked session runtime state to keep the active session id aligned with the fork target ([#4799](https://github.com/earendil-works/pi-mono/pull/4799) by [@Perlence](https://github.com/Perlence)). - Fixed the subagent extension's parallel mode to return useful per-task output and failed-task diagnostics to the parent model instead of 100-character previews ([#4710](https://github.com/earendil-works/pi/issues/4710)). - Fixed Windows local bash execution to hide helper console windows when launched from background SDK processes ([#4699](https://github.com/earendil-works/pi/issues/4699)). - Fixed managed npm extension folders to set cloud-sync ignore metadata where supported ([#4763](https://github.com/earendil-works/pi/issues/4763)). +- Fixed HTTP idle timeout configuration so long-running provider streams can avoid premature idle disconnects ([#4759](https://github.com/earendil-works/pi-mono/pull/4759) by [@mitsuhiko](https://github.com/mitsuhiko)). +- Fixed default system prompt boundaries to use explicit XML tags for clearer file separation ([#4709](https://github.com/earendil-works/pi-mono/pull/4709) by [@herrnel](https://github.com/herrnel)). +- Fixed HTML share/export sidebar clicks for shared tool entries to scroll to the rendered tool call ([#4664](https://github.com/earendil-works/pi-mono/pull/4664) by [@yzhg1983](https://github.com/yzhg1983)). +- Fixed theme palettes to set explicit text colors and avoid terminal-default color drift. +- Fixed truecolor detection to align terminal image rendering and interactive theme decisions. +- Fixed loader indicator startup inherited from `@earendil-works/pi-tui` so initialization cannot run before frames are available. +- Fixed OpenAI-compatible default output token requests inherited from `@earendil-works/pi-ai` to avoid reserving impossible context windows on servers such as vLLM ([#4675](https://github.com/earendil-works/pi/issues/4675)). +- Fixed OpenAI prompt cache keys inherited from `@earendil-works/pi-ai` to stay within the 64-character provider limit ([#4720](https://github.com/earendil-works/pi/issues/4720)). - Fixed Windows npm-family package commands for fnm-managed Node.js installs that expose both extensionless Unix scripts and `.cmd` shims ([#4793](https://github.com/earendil-works/pi/issues/4793)). ## [0.75.3] - 2026-05-18 diff --git a/packages/tui/CHANGELOG.md b/packages/tui/CHANGELOG.md index 8cae4810..2a235c74 100644 --- a/packages/tui/CHANGELOG.md +++ b/packages/tui/CHANGELOG.md @@ -6,6 +6,11 @@ - Removed the package-level development watch script now that the root TypeScript check validates strip-only-compatible sources. +### Fixed + +- Fixed loader initialization so indicator startup cannot run before frames are initialized. +- Fixed truecolor capability detection to align terminal image rendering with the interactive theme detector. + ## [0.75.3] - 2026-05-18 ## [0.75.2] - 2026-05-18 From 3533843dd781dcd233f51854fc883ec246a6a919 Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Wed, 20 May 2026 16:11:46 +0200 Subject: [PATCH 149/166] Release v0.75.4 --- package-lock.json | 24 +++++++++---------- packages/agent/CHANGELOG.md | 2 +- packages/agent/package.json | 4 ++-- packages/ai/CHANGELOG.md | 2 +- packages/ai/package.json | 2 +- packages/coding-agent/CHANGELOG.md | 2 +- .../package-lock.json | 4 ++-- .../custom-provider-anthropic/package.json | 2 +- .../custom-provider-gitlab-duo/package.json | 2 +- .../extensions/sandbox/package-lock.json | 4 ++-- .../examples/extensions/sandbox/package.json | 2 +- .../extensions/with-deps/package-lock.json | 4 ++-- .../extensions/with-deps/package.json | 2 +- packages/coding-agent/npm-shrinkwrap.json | 24 +++++++++---------- packages/coding-agent/package.json | 8 +++---- packages/tui/CHANGELOG.md | 2 +- packages/tui/package.json | 2 +- 17 files changed, 46 insertions(+), 46 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4bb2dfe8..fb54cf29 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5998,10 +5998,10 @@ }, "packages/agent": { "name": "@earendil-works/pi-agent-core", - "version": "0.75.3", + "version": "0.75.4", "license": "MIT", "dependencies": { - "@earendil-works/pi-ai": "^0.75.3", + "@earendil-works/pi-ai": "^0.75.4", "ignore": "7.0.5", "typebox": "1.1.38", "yaml": "2.9.0" @@ -6035,7 +6035,7 @@ }, "packages/ai": { "name": "@earendil-works/pi-ai", - "version": "0.75.3", + "version": "0.75.4", "license": "MIT", "dependencies": { "@anthropic-ai/sdk": "0.91.1", @@ -6079,12 +6079,12 @@ }, "packages/coding-agent": { "name": "@earendil-works/pi-coding-agent", - "version": "0.75.3", + "version": "0.75.4", "license": "MIT", "dependencies": { - "@earendil-works/pi-agent-core": "^0.75.3", - "@earendil-works/pi-ai": "^0.75.3", - "@earendil-works/pi-tui": "^0.75.3", + "@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", @@ -6123,25 +6123,25 @@ }, "packages/coding-agent/examples/extensions/custom-provider-anthropic": { "name": "pi-extension-custom-provider-anthropic", - "version": "0.75.3", + "version": "0.75.4", "dependencies": { "@anthropic-ai/sdk": "0.52.0" } }, "packages/coding-agent/examples/extensions/custom-provider-gitlab-duo": { "name": "pi-extension-custom-provider-gitlab-duo", - "version": "0.75.3" + "version": "0.75.4" }, "packages/coding-agent/examples/extensions/sandbox": { "name": "pi-extension-sandbox", - "version": "1.5.3", + "version": "1.5.4", "dependencies": { "@anthropic-ai/sandbox-runtime": "0.0.26" } }, "packages/coding-agent/examples/extensions/with-deps": { "name": "pi-extension-with-deps", - "version": "0.75.3", + "version": "0.75.4", "dependencies": { "ms": "2.1.3" }, @@ -6177,7 +6177,7 @@ }, "packages/tui": { "name": "@earendil-works/pi-tui", - "version": "0.75.3", + "version": "0.75.4", "license": "MIT", "dependencies": { "get-east-asian-width": "1.6.0", diff --git a/packages/agent/CHANGELOG.md b/packages/agent/CHANGELOG.md index 1fbe2915..872b8979 100644 --- a/packages/agent/CHANGELOG.md +++ b/packages/agent/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## [Unreleased] +## [0.75.4] - 2026-05-20 ### Changed diff --git a/packages/agent/package.json b/packages/agent/package.json index d8bf0656..f5d823e7 100644 --- a/packages/agent/package.json +++ b/packages/agent/package.json @@ -1,6 +1,6 @@ { "name": "@earendil-works/pi-agent-core", - "version": "0.75.3", + "version": "0.75.4", "description": "General-purpose agent with transport abstraction, state management, and attachment support", "type": "module", "main": "./dist/index.js", @@ -29,7 +29,7 @@ "prepublishOnly": "npm run clean && npm run build" }, "dependencies": { - "@earendil-works/pi-ai": "^0.75.3", + "@earendil-works/pi-ai": "^0.75.4", "ignore": "7.0.5", "typebox": "1.1.38", "yaml": "2.9.0" diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index d4560aa0..40e1a33e 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## [Unreleased] +## [0.75.4] - 2026-05-20 ### Changed diff --git a/packages/ai/package.json b/packages/ai/package.json index 16fa3be7..a10a23df 100644 --- a/packages/ai/package.json +++ b/packages/ai/package.json @@ -1,6 +1,6 @@ { "name": "@earendil-works/pi-ai", - "version": "0.75.3", + "version": "0.75.4", "description": "Unified LLM API with automatic model discovery and provider configuration", "type": "module", "main": "./dist/index.js", diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 5e9053cd..58aa9fb0 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## [Unreleased] +## [0.75.4] - 2026-05-20 ### New Features diff --git a/packages/coding-agent/examples/extensions/custom-provider-anthropic/package-lock.json b/packages/coding-agent/examples/extensions/custom-provider-anthropic/package-lock.json index 8ca5ae51..c78ae920 100644 --- a/packages/coding-agent/examples/extensions/custom-provider-anthropic/package-lock.json +++ b/packages/coding-agent/examples/extensions/custom-provider-anthropic/package-lock.json @@ -1,12 +1,12 @@ { "name": "pi-extension-custom-provider", - "version": "0.75.3", + "version": "0.75.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pi-extension-custom-provider", - "version": "0.75.3", + "version": "0.75.4", "dependencies": { "@anthropic-ai/sdk": "^0.52.0" } diff --git a/packages/coding-agent/examples/extensions/custom-provider-anthropic/package.json b/packages/coding-agent/examples/extensions/custom-provider-anthropic/package.json index a2f796cf..89aa420a 100644 --- a/packages/coding-agent/examples/extensions/custom-provider-anthropic/package.json +++ b/packages/coding-agent/examples/extensions/custom-provider-anthropic/package.json @@ -1,7 +1,7 @@ { "name": "pi-extension-custom-provider-anthropic", "private": true, - "version": "0.75.3", + "version": "0.75.4", "type": "module", "scripts": { "clean": "echo 'nothing to clean'", diff --git a/packages/coding-agent/examples/extensions/custom-provider-gitlab-duo/package.json b/packages/coding-agent/examples/extensions/custom-provider-gitlab-duo/package.json index cda287b8..2db431cf 100644 --- a/packages/coding-agent/examples/extensions/custom-provider-gitlab-duo/package.json +++ b/packages/coding-agent/examples/extensions/custom-provider-gitlab-duo/package.json @@ -1,7 +1,7 @@ { "name": "pi-extension-custom-provider-gitlab-duo", "private": true, - "version": "0.75.3", + "version": "0.75.4", "type": "module", "scripts": { "clean": "echo 'nothing to clean'", diff --git a/packages/coding-agent/examples/extensions/sandbox/package-lock.json b/packages/coding-agent/examples/extensions/sandbox/package-lock.json index 80c45099..ef29b150 100644 --- a/packages/coding-agent/examples/extensions/sandbox/package-lock.json +++ b/packages/coding-agent/examples/extensions/sandbox/package-lock.json @@ -1,12 +1,12 @@ { "name": "pi-extension-sandbox", - "version": "1.5.3", + "version": "1.5.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pi-extension-sandbox", - "version": "1.5.3", + "version": "1.5.4", "dependencies": { "@anthropic-ai/sandbox-runtime": "^0.0.26" } diff --git a/packages/coding-agent/examples/extensions/sandbox/package.json b/packages/coding-agent/examples/extensions/sandbox/package.json index 1fd94b35..c33d2d60 100644 --- a/packages/coding-agent/examples/extensions/sandbox/package.json +++ b/packages/coding-agent/examples/extensions/sandbox/package.json @@ -1,7 +1,7 @@ { "name": "pi-extension-sandbox", "private": true, - "version": "1.5.3", + "version": "1.5.4", "type": "module", "scripts": { "clean": "echo 'nothing to clean'", diff --git a/packages/coding-agent/examples/extensions/with-deps/package-lock.json b/packages/coding-agent/examples/extensions/with-deps/package-lock.json index a1cdd835..526356f4 100644 --- a/packages/coding-agent/examples/extensions/with-deps/package-lock.json +++ b/packages/coding-agent/examples/extensions/with-deps/package-lock.json @@ -1,12 +1,12 @@ { "name": "pi-extension-with-deps", - "version": "0.75.3", + "version": "0.75.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pi-extension-with-deps", - "version": "0.75.3", + "version": "0.75.4", "dependencies": { "ms": "^2.1.3" }, diff --git a/packages/coding-agent/examples/extensions/with-deps/package.json b/packages/coding-agent/examples/extensions/with-deps/package.json index 2d7cc728..45f1d3f2 100644 --- a/packages/coding-agent/examples/extensions/with-deps/package.json +++ b/packages/coding-agent/examples/extensions/with-deps/package.json @@ -1,7 +1,7 @@ { "name": "pi-extension-with-deps", "private": true, - "version": "0.75.3", + "version": "0.75.4", "type": "module", "scripts": { "clean": "echo 'nothing to clean'", diff --git a/packages/coding-agent/npm-shrinkwrap.json b/packages/coding-agent/npm-shrinkwrap.json index 174e6d17..453f236f 100644 --- a/packages/coding-agent/npm-shrinkwrap.json +++ b/packages/coding-agent/npm-shrinkwrap.json @@ -1,17 +1,17 @@ { "name": "@earendil-works/pi-coding-agent", - "version": "0.75.3", + "version": "0.75.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@earendil-works/pi-coding-agent", - "version": "0.75.3", + "version": "0.75.4", "license": "MIT", "dependencies": { - "@earendil-works/pi-agent-core": "^0.75.3", - "@earendil-works/pi-ai": "^0.75.3", - "@earendil-works/pi-tui": "^0.75.3", + "@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", @@ -473,11 +473,11 @@ } }, "node_modules/@earendil-works/pi-agent-core": { - "version": "0.75.3", - "resolved": "https://registry.npmjs.org/@earendil-works/pi-agent-core/-/pi-agent-core-0.75.3.tgz", + "version": "0.75.4", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-agent-core/-/pi-agent-core-0.75.4.tgz", "license": "MIT", "dependencies": { - "@earendil-works/pi-ai": "^0.75.3", + "@earendil-works/pi-ai": "^0.75.4", "ignore": "7.0.5", "typebox": "1.1.38", "yaml": "2.9.0" @@ -487,8 +487,8 @@ } }, "node_modules/@earendil-works/pi-ai": { - "version": "0.75.3", - "resolved": "https://registry.npmjs.org/@earendil-works/pi-ai/-/pi-ai-0.75.3.tgz", + "version": "0.75.4", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-ai/-/pi-ai-0.75.4.tgz", "license": "MIT", "dependencies": { "@anthropic-ai/sdk": "0.91.1", @@ -509,8 +509,8 @@ } }, "node_modules/@earendil-works/pi-tui": { - "version": "0.75.3", - "resolved": "https://registry.npmjs.org/@earendil-works/pi-tui/-/pi-tui-0.75.3.tgz", + "version": "0.75.4", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-tui/-/pi-tui-0.75.4.tgz", "license": "MIT", "dependencies": { "get-east-asian-width": "1.6.0", diff --git a/packages/coding-agent/package.json b/packages/coding-agent/package.json index 23a82422..0c19d304 100644 --- a/packages/coding-agent/package.json +++ b/packages/coding-agent/package.json @@ -1,6 +1,6 @@ { "name": "@earendil-works/pi-coding-agent", - "version": "0.75.3", + "version": "0.75.4", "description": "Coding agent CLI with read, bash, edit, write tools and session management", "type": "module", "piConfig": { @@ -39,9 +39,9 @@ "prepublishOnly": "npm run clean && npm run build && npm run shrinkwrap" }, "dependencies": { - "@earendil-works/pi-agent-core": "^0.75.3", - "@earendil-works/pi-ai": "^0.75.3", - "@earendil-works/pi-tui": "^0.75.3", + "@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 --git a/packages/tui/CHANGELOG.md b/packages/tui/CHANGELOG.md index 2a235c74..8d074fed 100644 --- a/packages/tui/CHANGELOG.md +++ b/packages/tui/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## [Unreleased] +## [0.75.4] - 2026-05-20 ### Changed diff --git a/packages/tui/package.json b/packages/tui/package.json index 8248d33f..213e51bb 100644 --- a/packages/tui/package.json +++ b/packages/tui/package.json @@ -1,6 +1,6 @@ { "name": "@earendil-works/pi-tui", - "version": "0.75.3", + "version": "0.75.4", "description": "Terminal User Interface library with differential rendering for efficient text-based applications", "type": "module", "main": "dist/index.js", From b8326ca93598baaf6222ab6b3922963f859edfe3 Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Wed, 20 May 2026 16:26:05 +0200 Subject: [PATCH 150/166] Add [Unreleased] section for next cycle --- AGENTS.md | 11 +++++++++-- packages/agent/CHANGELOG.md | 2 ++ packages/ai/CHANGELOG.md | 2 ++ packages/coding-agent/CHANGELOG.md | 2 ++ packages/tui/CHANGELOG.md | 2 ++ 5 files changed, 17 insertions(+), 2 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 4f1d1ccb..67cdf06d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -202,13 +202,20 @@ Create provider file exporting: ``` In the interactive smoke test, verify startup, model/account listing, and at least one real prompt with the intended default provider. Treat failures as release blockers unless the user explicitly accepts the risk. -3. **Run release script**: +3. **Run release script until npm publish**: ```bash npm run release:patch # Fixes and additions npm run release:minor # API breaking changes ``` -The release script handles: version bump, CHANGELOG finalization, commit, tag, publish, and adding new `[Unreleased]` sections. + npm publishing requires the maintainer's npm WebAuthn/security-key 2FA and cannot be completed by an agent alone. If the release script stops at `npm publish` with an npm browser authentication URL, the maintainer must run or approve `npm run publish` locally. Do not rerun the version bump. + +4. **After publish succeeds, finish release bookkeeping**: + - Add fresh `## [Unreleased]` sections to package changelogs. + - Commit with `Add [Unreleased] section for next cycle`. + - Push `main` and the release tag. + +The release script handles the full flow when npm publish auth is already satisfied. If npm requires WebAuthn during publish, continue manually from the existing release commit/tag using the steps above. ## **CRITICAL** Git Rules for Parallel Agents **CRITICAL** diff --git a/packages/agent/CHANGELOG.md b/packages/agent/CHANGELOG.md index 872b8979..4ec02d9a 100644 --- a/packages/agent/CHANGELOG.md +++ b/packages/agent/CHANGELOG.md @@ -1,5 +1,7 @@ # Changelog +## [Unreleased] + ## [0.75.4] - 2026-05-20 ### Changed diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index 40e1a33e..3d20b95a 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -1,5 +1,7 @@ # Changelog +## [Unreleased] + ## [0.75.4] - 2026-05-20 ### Changed diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 58aa9fb0..468c4d84 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -1,5 +1,7 @@ # Changelog +## [Unreleased] + ## [0.75.4] - 2026-05-20 ### New Features diff --git a/packages/tui/CHANGELOG.md b/packages/tui/CHANGELOG.md index 8d074fed..56ea936e 100644 --- a/packages/tui/CHANGELOG.md +++ b/packages/tui/CHANGELOG.md @@ -1,5 +1,7 @@ # Changelog +## [Unreleased] + ## [0.75.4] - 2026-05-20 ### Changed From 3b37c9e0a1f8ee751df062e26f364ff47585e421 Mon Sep 17 00:00:00 2001 From: Julien Chaumond Date: Wed, 20 May 2026 18:12:00 +0200 Subject: [PATCH 151/166] chore: add HF_TOKEN to pi-test.ps1 --no-env unset list Aligns the PowerShell script with pi-test.sh, which already unsets HF_TOKEN. Co-Authored-By: julien-agent --- pi-test.ps1 | 1 + 1 file changed, 1 insertion(+) diff --git a/pi-test.ps1 b/pi-test.ps1 index 2bbe35bd..610aeaeb 100644 --- a/pi-test.ps1 +++ b/pi-test.ps1 @@ -31,6 +31,7 @@ if ($noEnv) { "COPILOT_GITHUB_TOKEN", "GH_TOKEN", "GITHUB_TOKEN", + "HF_TOKEN", "GOOGLE_APPLICATION_CREDENTIALS", "GOOGLE_CLOUD_PROJECT", "GCLOUD_PROJECT", From 088987b200c214f5ee11b45781b3eee10b7ef6cd Mon Sep 17 00:00:00 2001 From: Sviatoslav Abakumov Date: Wed, 20 May 2026 23:48:03 +0400 Subject: [PATCH 152/166] fix(coding-agent): list themes by content name --- .../src/modes/interactive/theme/theme.ts | 66 +++++++++++-------- .../coding-agent/test/theme-picker.test.ts | 51 ++++++++++++++ 2 files changed, 89 insertions(+), 28 deletions(-) create mode 100644 packages/coding-agent/test/theme-picker.test.ts diff --git a/packages/coding-agent/src/modes/interactive/theme/theme.ts b/packages/coding-agent/src/modes/interactive/theme/theme.ts index 0560f014..d9bcdf8c 100644 --- a/packages/coding-agent/src/modes/interactive/theme/theme.ts +++ b/packages/coding-agent/src/modes/interactive/theme/theme.ts @@ -440,20 +440,7 @@ function getBuiltinThemes(): Record { } export function getAvailableThemes(): string[] { - const themes = new Set(Object.keys(getBuiltinThemes())); - const customThemesDir = getCustomThemesDir(); - if (fs.existsSync(customThemesDir)) { - const files = fs.readdirSync(customThemesDir); - for (const file of files) { - if (file.endsWith(".json")) { - themes.add(file.slice(0, -5)); - } - } - } - for (const name of registeredThemes.keys()) { - themes.add(name); - } - return Array.from(themes).sort(); + return getAvailableThemesWithPaths().map(({ name }) => name); } export interface ThemeInfo { @@ -463,35 +450,58 @@ export interface ThemeInfo { export function getAvailableThemesWithPaths(): ThemeInfo[] { const themesDir = getThemesDir(); - const customThemesDir = getCustomThemesDir(); const result: ThemeInfo[] = []; + const seen = new Set(); + const addTheme = (themeInfo: ThemeInfo) => { + if (seen.has(themeInfo.name)) { + return; + } + seen.add(themeInfo.name); + result.push(themeInfo); + }; // Built-in themes for (const name of Object.keys(getBuiltinThemes())) { - result.push({ name, path: path.join(themesDir, `${name}.json`) }); + addTheme({ name, path: path.join(themesDir, `${name}.json`) }); } // Custom themes - if (fs.existsSync(customThemesDir)) { - for (const file of fs.readdirSync(customThemesDir)) { - if (file.endsWith(".json")) { - const name = file.slice(0, -5); - if (!result.some((t) => t.name === name)) { - result.push({ name, path: path.join(customThemesDir, file) }); - } - } - } + for (const themeInfo of getCustomThemeInfos()) { + addTheme(themeInfo); } for (const [name, theme] of registeredThemes.entries()) { - if (!result.some((t) => t.name === name)) { - result.push({ name, path: theme.sourcePath }); - } + addTheme({ name, path: theme.sourcePath }); } return result.sort((a, b) => a.name.localeCompare(b.name)); } +function getCustomThemeInfos(): ThemeInfo[] { + const customThemesDir = getCustomThemesDir(); + const result: ThemeInfo[] = []; + if (!fs.existsSync(customThemesDir)) { + return result; + } + + for (const file of fs.readdirSync(customThemesDir)) { + if (!file.endsWith(".json")) { + continue; + } + const themePath = path.join(customThemesDir, file); + try { + const customTheme = loadThemeFromPath(themePath); + if (customTheme.name) { + result.push({ name: customTheme.name, path: themePath }); + } + } catch { + // Invalid themes are ignored here; the resource loader reports them + // during normal startup/reload. + } + } + return result; +} + function parseThemeJson(label: string, json: unknown): ThemeJson { if (!validateThemeJson.Check(json)) { const errors = Array.from(validateThemeJson.Errors(json)); diff --git a/packages/coding-agent/test/theme-picker.test.ts b/packages/coding-agent/test/theme-picker.test.ts new file mode 100644 index 00000000..7b31854e --- /dev/null +++ b/packages/coding-agent/test/theme-picker.test.ts @@ -0,0 +1,51 @@ +import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { + getAvailableThemes, + getAvailableThemesWithPaths, + setRegisteredThemes, +} from "../src/modes/interactive/theme/theme.ts"; + +type ThemeFile = { + name: string; + vars?: Record; + colors: Record; +}; + +describe("theme picker", () => { + let tempRoot: string; + + beforeEach(() => { + tempRoot = mkdtempSync(join(tmpdir(), "pi-theme-picker-")); + const agentDir = join(tempRoot, "agent"); + vi.stubEnv("PI_CODING_AGENT_DIR", agentDir); + mkdirSync(join(agentDir, "themes"), { recursive: true }); + setRegisteredThemes([]); + }); + + afterEach(() => { + setRegisteredThemes([]); + rmSync(tempRoot, { recursive: true, force: true }); + vi.unstubAllEnvs(); + }); + + it("uses custom theme content names instead of file names", () => { + const darkTheme = JSON.parse( + readFileSync(new URL("../src/modes/interactive/theme/dark.json", import.meta.url), "utf-8"), + ) as ThemeFile; + const customTheme: ThemeFile = { + ...darkTheme, + name: "bar", + }; + + const themePath = join(process.env.PI_CODING_AGENT_DIR!, "themes", "foo.json"); + writeFileSync(themePath, JSON.stringify(customTheme, null, 2)); + + expect(getAvailableThemes()).toContain("bar"); + expect(getAvailableThemes()).not.toContain("foo"); + expect(getAvailableThemesWithPaths()).toContainEqual({ name: "bar", path: themePath }); + expect(getAvailableThemesWithPaths().some((theme) => theme.name === "foo")).toBe(false); + }); +}); From 385a11bf531cf34f0b5c74085ed1b49625263f0f Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Wed, 20 May 2026 22:53:38 +0200 Subject: [PATCH 153/166] docs: document dependency install security --- AGENTS.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/AGENTS.md b/AGENTS.md index 67cdf06d..10f28ef5 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -36,6 +36,15 @@ - For ad-hoc scripts, write the script to a temporary file (for example under `/tmp`) using `write`, run that file, edit it if needed, and remove it when it is no longer needed. Do not embed multi-line scripts directly in `bash` commands. - NEVER commit unless user asks +## Dependency and Install Security + +- Treat npm dependency and lockfile changes as reviewed code changes. Direct external dependencies must stay pinned to exact versions. +- Use `npm install --ignore-scripts` to hydrate/update `node_modules` locally. Use `npm ci --ignore-scripts` for clean installs/CI-style verification. Do not run lifecycle scripts unless the user explicitly asks. +- If dependency metadata changes, run `npm install --package-lock-only --ignore-scripts` to update `package-lock.json` without installing or running scripts. +- If `packages/coding-agent/npm-shrinkwrap.json` needs regeneration, run `node scripts/generate-coding-agent-shrinkwrap.mjs`; verify with `node scripts/generate-coding-agent-shrinkwrap.mjs --check` or `npm run check`. +- Pre-commit blocks accidental lockfile commits unless `PI_ALLOW_LOCKFILE_CHANGE=1` is set. Do not bypass it unless the user explicitly wants the lockfile change committed. +- New dependencies with lifecycle scripts require review and an explicit allowlist entry in `scripts/generate-coding-agent-shrinkwrap.mjs`; do not add one silently. + ## Contribution Gate - New issues from new contributors are auto-closed by `.github/workflows/issue-gate.yml` From 4868222e3414554987bf4b05fbb393fc65080aa0 Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Thu, 21 May 2026 00:21:21 +0200 Subject: [PATCH 154/166] chore(tui): replace koffi with Windows VT input helper closes #4480 --- package-lock.json | 14 --- packages/coding-agent/npm-shrinkwrap.json | 14 --- packages/tui/CHANGELOG.md | 4 + .../win32-arm64/win32-console-mode.node | Bin 0 -> 3072 bytes .../win32-x64/win32-console-mode.node | Bin 0 -> 3072 bytes .../tui/native/win32/src/win32-console-mode.c | 53 ++++++++++ packages/tui/package.json | 4 +- packages/tui/src/terminal.ts | 38 ++++--- scripts/build-binaries.sh | 20 ++-- scripts/generate-coding-agent-shrinkwrap.mjs | 1 - scripts/local-release.mjs | 98 ++++++++++++++++-- 11 files changed, 176 insertions(+), 70 deletions(-) create mode 100644 packages/tui/native/win32/prebuilds/win32-arm64/win32-console-mode.node create mode 100644 packages/tui/native/win32/prebuilds/win32-x64/win32-console-mode.node create mode 100644 packages/tui/native/win32/src/win32-console-mode.c diff --git a/package-lock.json b/package-lock.json index fb54cf29..3e936140 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3773,17 +3773,6 @@ "safe-buffer": "^5.0.1" } }, - "node_modules/koffi": { - "version": "2.16.2", - "resolved": "https://registry.npmjs.org/koffi/-/koffi-2.16.2.tgz", - "integrity": "sha512-owU0MRwv6xkrVqCd+33uw6BaYppkTRXbO/rVdJNI2dvZG0gzyRhYwW25eWtc5pauwK8TGh3AbkFONSezdykfSA==", - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "funding": { - "url": "https://liberapay.com/Koromix" - } - }, "node_modules/lodash-es": { "version": "4.18.1", "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.18.1.tgz", @@ -6190,9 +6179,6 @@ }, "engines": { "node": ">=22.19.0" - }, - "optionalDependencies": { - "koffi": "2.16.2" } } } diff --git a/packages/coding-agent/npm-shrinkwrap.json b/packages/coding-agent/npm-shrinkwrap.json index 453f236f..85a22b1a 100644 --- a/packages/coding-agent/npm-shrinkwrap.json +++ b/packages/coding-agent/npm-shrinkwrap.json @@ -516,9 +516,6 @@ "get-east-asian-width": "1.6.0", "marked": "15.0.12" }, - "optionalDependencies": { - "koffi": "2.16.2" - }, "engines": { "node": ">=22.19.0" } @@ -1369,17 +1366,6 @@ "safe-buffer": "^5.0.1" } }, - "node_modules/koffi": { - "version": "2.16.2", - "resolved": "https://registry.npmjs.org/koffi/-/koffi-2.16.2.tgz", - "integrity": "sha512-owU0MRwv6xkrVqCd+33uw6BaYppkTRXbO/rVdJNI2dvZG0gzyRhYwW25eWtc5pauwK8TGh3AbkFONSezdykfSA==", - "license": "MIT", - "optional": true, - "hasInstallScript": true, - "funding": { - "url": "https://liberapay.com/Koromix" - } - }, "node_modules/long": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", diff --git a/packages/tui/CHANGELOG.md b/packages/tui/CHANGELOG.md index 56ea936e..b1f8a8dc 100644 --- a/packages/tui/CHANGELOG.md +++ b/packages/tui/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Changed + +- Replaced the optional `koffi` dependency for Windows VT input with a tiny vendored native helper, reducing install size while preserving Shift+Tab handling ([#4480](https://github.com/earendil-works/pi/issues/4480)). + ## [0.75.4] - 2026-05-20 ### Changed diff --git a/packages/tui/native/win32/prebuilds/win32-arm64/win32-console-mode.node b/packages/tui/native/win32/prebuilds/win32-arm64/win32-console-mode.node new file mode 100644 index 0000000000000000000000000000000000000000..42b2c77ccadb03f7ad745fda39583b2e6711d10f GIT binary patch literal 3072 zcmeHIU1%It6#ni^o1_T|tzE22(MeoE#4a^IJ_(&zgB4p1+2BKc*v#%+lhNJTb#}Ja z1Z2UN(t>0}TJ*(MeCRF+5hJ7pCAA8^6x4@2`LOW^^58@5whar?^_)99c2lV$_CX<+ z+&%Yv=V!iq?#;P}4$ngeu!Ux`2`rK1VXEK1{`rQ?eGi=P!{W9}{Y&EDrT)nor)U(s z{IqA~jEv>FdEZD&!z;Ol;~GyrpEPoLTPAvYyW_3YpWOS?SL;WQpN!_4jT5J?D*eRC zD|eWq04pk?k@Dj2^A;QD}s8L@3GaYF!pA zcun>W<#bc3LU3`Ed5T=aph)~#50Pbqcu>q*KkzUIM`JH=Pqc269~!{!HvMmxXUVeH`m&mI^?zp1DHy9w&h#ES*{HM%BQ_X1ex@gD1?5BJAat{yJ@x!Q@OSQ zo6G-v1?tJqD>r)3Xzs_iUBE)u4zp3$Q8h&QE{J_AO&#a1lKy@NKCTH&DxYm;V^v4M zc2i(@O*hZg^jKAhU4zQrYc|+61q=_PJV93_79+_v2oa0`@2pYIW+xgz`Ma-y#UCC- zu&T!%sp&hb99!!&nNx71hxjDFIJ>N;R=Ak;y$_3wM1}T7uEYI z_q7PSAcFfyXa2O0#)yW+7)|n@*9C%|#K#)Yf}d$GTZo5-^4m3iSM{{EZ)F)6`B66; zX|k{CcUKuN6JDb=^ID&Kl6Z_(>dA|W8*{18p~76cL|8-K8}Eg3@i!N`wsPIV{hb#0 zRPnxmAd0!2FV~NA)#ps!a}d>n>+x^L_&}J!ZpkApd8{SPmW;mRa{soo**|ov;H0Ld zpGxQRS!uZ&@Qf!dU#4Cvxf$QdE1S#yMe=v8oU~K)RurV?zYghI>8yOg@%)mNos?eA zajon#ZlT0Yj7HSYdujb8%{cYdcqd!<-Qt%ExMBd@?xMLy`Y$vu(_Ek)*iA9OYmPfK zxF?f$i$r*jdXIQhP|Q3z?G%0Kr3f!&W$MUd9KwU*xtFTG8qbCMG{PR7aJVn|L5GZ1u0dMjw~g2kW83LG_L6M*4Kv1P zG;e$uxOSgxIwz!{D7L#lWoGzxsr*tnO^ zjM}y*i$yv)M)rqCK+Y8R#Mb03$g^V;&y5`%8f?F98@X<3cc21vuRp>Eg8x6>#Y5VG R9l)zNa|aS{ruaWp;CE*fda3{b literal 0 HcmV?d00001 diff --git a/packages/tui/native/win32/prebuilds/win32-x64/win32-console-mode.node b/packages/tui/native/win32/prebuilds/win32-x64/win32-console-mode.node new file mode 100644 index 0000000000000000000000000000000000000000..2c6d86d8725cb88e742a168f0fd9db2e200e4766 GIT binary patch literal 3072 zcmeHIQD_`h6urCK#&qNErmzc93Qp>L1ZkIbH;RHtstJA7t%fA1h`62XjLFFC>@quB zV+4sV5tCshh_w9```-`!sQn7GVhd>vh#J%ng?isadnBG@Sa3 zqt%qEX4`dFnKl%sWh{5L8_C0mjNq4V1aXo(9~{R>Vsy!f4DAALoB#R> zh@}_QUa{n={m+W_6-9VgKi={oxdbjv=^No)L%@h=U-RAjL}v=7cizu^nIT$Sbq72N ze3m)$!!AE}0bu8~*y3s{?)@e@sqf)$ZlD5uh7gF~UG9vk!uv{im;80%G3vSML&7`D zPC>NKDV4KpYau5~9bjS#6Dxq2@V>^bAMmbk@~)lKDcHi>+-$P@DYoWvgR)rs(LKlq zJ|_6UL+$gvSQ=G_%AM4m$W3%|9bSnQO0eRR{{?7g%U)R(<<8E!-1l>)39gnp9x_Do zuKFBk7MNzhGc4Qn(giE{A$6)V=ox_-g0}9+A!cTeqwmzSb}+GlpB4uGcL?vv0zyj-k0m<)xNgb + +#ifndef ENABLE_VIRTUAL_TERMINAL_INPUT +#define ENABLE_VIRTUAL_TERMINAL_INPUT 0x0200 +#endif + +#define NAPI_AUTO_LENGTH ((unsigned long long)-1) + +typedef void* napi_env; +typedef void* napi_value; +typedef void* napi_callback_info; +typedef napi_value (__cdecl *napi_callback)(napi_env, napi_callback_info); +typedef int (__cdecl *napi_create_function_fn)(napi_env, const char*, unsigned long long, napi_callback, void*, napi_value*); +typedef int (__cdecl *napi_set_named_property_fn)(napi_env, napi_value, const char*, napi_value); +typedef int (__cdecl *napi_get_boolean_fn)(napi_env, int, napi_value*); + +static void* node_symbol(const char* name) { + HMODULE module = GetModuleHandleA(0); + void* proc = module ? (void*)GetProcAddress(module, name) : 0; + if (proc) return proc; + + module = GetModuleHandleA("node.dll"); + return module ? (void*)GetProcAddress(module, name) : 0; +} + +static napi_value __cdecl enable_virtual_terminal_input(napi_env env, napi_callback_info info) { + (void)info; + + HANDLE handle = GetStdHandle(STD_INPUT_HANDLE); + DWORD mode = 0; + int enabled = handle != INVALID_HANDLE_VALUE && + GetConsoleMode(handle, &mode) && + SetConsoleMode(handle, mode | ENABLE_VIRTUAL_TERMINAL_INPUT); + + napi_get_boolean_fn napi_get_boolean = (napi_get_boolean_fn)node_symbol("napi_get_boolean"); + napi_value result = 0; + if (napi_get_boolean) napi_get_boolean(env, enabled, &result); + return result; +} + +__declspec(dllexport) napi_value __cdecl napi_register_module_v1(napi_env env, napi_value exports) { + napi_create_function_fn napi_create_function = (napi_create_function_fn)node_symbol("napi_create_function"); + napi_set_named_property_fn napi_set_named_property = (napi_set_named_property_fn)node_symbol("napi_set_named_property"); + + napi_value fn = 0; + if (napi_create_function && + napi_set_named_property && + napi_create_function(env, "enableVirtualTerminalInput", NAPI_AUTO_LENGTH, enable_virtual_terminal_input, 0, &fn) == 0) { + napi_set_named_property(env, exports, "enableVirtualTerminalInput", fn); + } + + return exports; +} diff --git a/packages/tui/package.json b/packages/tui/package.json index 213e51bb..c684eae7 100644 --- a/packages/tui/package.json +++ b/packages/tui/package.json @@ -12,6 +12,7 @@ }, "files": [ "dist/**/*", + "native/win32/prebuilds/**/*.node", "README.md" ], "keywords": [ @@ -38,9 +39,6 @@ "get-east-asian-width": "1.6.0", "marked": "15.0.12" }, - "optionalDependencies": { - "koffi": "2.16.2" - }, "devDependencies": { "@xterm/headless": "5.5.0", "@xterm/xterm": "5.5.0", diff --git a/packages/tui/src/terminal.ts b/packages/tui/src/terminal.ts index 9d24dcdd..94c56ff6 100644 --- a/packages/tui/src/terminal.ts +++ b/packages/tui/src/terminal.ts @@ -1,6 +1,7 @@ import * as fs from "node:fs"; import { createRequire } from "node:module"; import * as path from "node:path"; +import { fileURLToPath } from "node:url"; import { setKittyProtocolActive } from "./keys.ts"; import { StdinBuffer } from "./stdin-buffer.ts"; @@ -210,23 +211,30 @@ export class ProcessTerminal implements Terminal { private enableWindowsVTInput(): void { if (process.platform !== "win32") return; try { - // Dynamic require to avoid bundling koffi's 74MB of cross-platform - // native binaries into every compiled binary. Koffi is only needed - // on Windows for VT input support. - const koffi = cjsRequire("koffi"); - const k32 = koffi.load("kernel32.dll"); - const GetStdHandle = k32.func("void* __stdcall GetStdHandle(int)"); - const GetConsoleMode = k32.func("bool __stdcall GetConsoleMode(void*, _Out_ uint32_t*)"); - const SetConsoleMode = k32.func("bool __stdcall SetConsoleMode(void*, uint32_t)"); + const arch = process.arch; + if (arch !== "x64" && arch !== "arm64") return; - const STD_INPUT_HANDLE = -10; - const ENABLE_VIRTUAL_TERMINAL_INPUT = 0x0200; - const handle = GetStdHandle(STD_INPUT_HANDLE); - const mode = new Uint32Array(1); - GetConsoleMode(handle, mode); - SetConsoleMode(handle, mode[0]! | ENABLE_VIRTUAL_TERMINAL_INPUT); + // Dynamic require so non-Windows and bundled/browser paths never load the + // native helper. In the npm package native/ is next to dist/; in compiled + // binary archives native/ is copied next to the executable. + const moduleDir = path.dirname(fileURLToPath(import.meta.url)); + const nativePath = path.join("native", "win32", "prebuilds", `win32-${arch}`, "win32-console-mode.node"); + const candidates = [ + path.join(moduleDir, "..", nativePath), + path.join(moduleDir, nativePath), + path.join(path.dirname(process.execPath), nativePath), + ]; + for (const modulePath of candidates) { + try { + const helper = cjsRequire(modulePath) as { enableVirtualTerminalInput?: () => boolean }; + helper.enableVirtualTerminalInput?.(); + return; + } catch { + // Try the next possible packaging location. + } + } } catch { - // koffi not available — Shift+Tab won't be distinguishable from Tab + // Native helper not available — Shift+Tab won't be distinguishable from Tab. } } diff --git a/scripts/build-binaries.sh b/scripts/build-binaries.sh index 4b94de97..c1b7b868 100755 --- a/scripts/build-binaries.sh +++ b/scripts/build-binaries.sh @@ -105,14 +105,10 @@ fi for platform in "${PLATFORMS[@]}"; do echo "Building for $platform..." - # Externalize koffi to avoid embedding all 18 platform .node files (~74MB) - # into every binary. Koffi is only used on Windows for VT input and the - # call site has a try/catch fallback. For Windows builds, we copy the - # appropriate .node file alongside the binary below. if [[ "$platform" == windows-* ]]; then - bun build --compile --external koffi --target=bun-$platform ./dist/bun/cli.js --outfile binaries/$platform/pi.exe + bun build --compile --target=bun-$platform ./dist/bun/cli.js --outfile binaries/$platform/pi.exe else - bun build --compile --external koffi --target=bun-$platform ./dist/bun/cli.js --outfile binaries/$platform/pi + bun build --compile --target=bun-$platform ./dist/bun/cli.js --outfile binaries/$platform/pi fi done @@ -132,17 +128,15 @@ for platform in "${PLATFORMS[@]}"; do cp -r docs binaries/$platform/ cp -r examples binaries/$platform/ - # Copy koffi native module for Windows (needed for VT input support) + # Copy Windows VT input native helper next to compiled Windows binaries. if [[ "$platform" == windows-* ]]; then if [[ "$platform" == "windows-arm64" ]]; then - koffi_arch_dir="win32_arm64" + win32_arch_dir="win32-arm64" else - koffi_arch_dir="win32_x64" + win32_arch_dir="win32-x64" fi - mkdir -p binaries/$platform/node_modules/koffi/build/koffi/$koffi_arch_dir - cp ../../node_modules/koffi/index.js binaries/$platform/node_modules/koffi/ - cp ../../node_modules/koffi/package.json binaries/$platform/node_modules/koffi/ - cp ../../node_modules/koffi/build/koffi/$koffi_arch_dir/koffi.node binaries/$platform/node_modules/koffi/build/koffi/$koffi_arch_dir/ + mkdir -p binaries/$platform/native/win32/prebuilds/$win32_arch_dir + cp ../tui/native/win32/prebuilds/$win32_arch_dir/win32-console-mode.node binaries/$platform/native/win32/prebuilds/$win32_arch_dir/ fi done diff --git a/scripts/generate-coding-agent-shrinkwrap.mjs b/scripts/generate-coding-agent-shrinkwrap.mjs index a122f183..7bcf5681 100644 --- a/scripts/generate-coding-agent-shrinkwrap.mjs +++ b/scripts/generate-coding-agent-shrinkwrap.mjs @@ -12,7 +12,6 @@ const shrinkwrapPath = join(codingAgentDir, "npm-shrinkwrap.json"); const internalPackagePrefix = "@earendil-works/pi-"; const allowedInstallScriptPackages = new Map([ ["@google/genai@1.52.0", "preinstall is a no-op in the published package"], - ["koffi@2.16.2", "optional native package ships prebuilt modules used without install scripts"], ["protobufjs@7.5.9", "postinstall only warns about protobufjs version scheme mismatches"], ]); diff --git a/scripts/local-release.mjs b/scripts/local-release.mjs index 5cdc67bc..782318bd 100644 --- a/scripts/local-release.mjs +++ b/scripts/local-release.mjs @@ -1,6 +1,6 @@ #!/usr/bin/env node -import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, symlinkSync, writeFileSync } from "node:fs"; +import { cpSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, symlinkSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { dirname, isAbsolute, join, relative, resolve } from "node:path"; import { spawnSync } from "node:child_process"; @@ -73,6 +73,7 @@ function run(command, args, options = {}) { const result = spawnSync(command, args, { cwd: options.cwd, encoding: "utf8", + shell: process.platform === "win32", stdio: options.capture ? ["inherit", "pipe", "inherit"] : "inherit", }); @@ -123,6 +124,73 @@ function fileSpecifier(fromDirectory, file) { return `file:${relativePath.startsWith(".") ? relativePath : `./${relativePath}`}`; } +function currentBinaryPlatform() { + if (process.platform === "win32") return process.arch === "arm64" ? "windows-arm64" : "windows-x64"; + if (process.platform === "darwin") return process.arch === "arm64" ? "darwin-arm64" : "darwin-x64"; + if (process.platform === "linux") return process.arch === "arm64" ? "linux-arm64" : "linux-x64"; + throw new Error(`Unsupported binary platform: ${process.platform} ${process.arch}`); +} + +function copyBinaryAssets(targetDirectory) { + const codingAgentDirectory = join(repoRoot, "packages", "coding-agent"); + const distDirectory = join(codingAgentDirectory, "dist"); + cpSync(join(codingAgentDirectory, "package.json"), join(targetDirectory, "package.json")); + cpSync(join(codingAgentDirectory, "README.md"), join(targetDirectory, "README.md")); + cpSync(join(codingAgentDirectory, "CHANGELOG.md"), join(targetDirectory, "CHANGELOG.md")); + cpSync(join(repoRoot, "node_modules", "@silvia-odwyer", "photon-node", "photon_rs_bg.wasm"), join(targetDirectory, "photon_rs_bg.wasm")); + cpSync(join(distDirectory, "modes", "interactive", "theme"), join(targetDirectory, "theme"), { recursive: true }); + cpSync(join(distDirectory, "modes", "interactive", "assets"), join(targetDirectory, "assets"), { recursive: true }); + cpSync(join(distDirectory, "core", "export-html"), join(targetDirectory, "export-html"), { recursive: true }); + cpSync(join(codingAgentDirectory, "docs"), join(targetDirectory, "docs"), { recursive: true }); + cpSync(join(codingAgentDirectory, "examples"), join(targetDirectory, "examples"), { recursive: true }); +} + +function copyWindowsConsoleModeHelper(targetDirectory, platform) { + if (!platform.startsWith("windows-")) return; + const win32Arch = platform === "windows-arm64" ? "win32-arm64" : "win32-x64"; + const relativeNativePath = join("native", "win32", "prebuilds", win32Arch); + mkdirSync(join(targetDirectory, relativeNativePath), { recursive: true }); + cpSync( + join(repoRoot, "packages", "tui", "native", "win32", "prebuilds", win32Arch, "win32-console-mode.node"), + join(targetDirectory, relativeNativePath, "win32-console-mode.node"), + ); +} + +function buildBunBinaryRelease(targetDirectory, archiveDirectory) { + if (!commandExists("bun")) { + throw new Error("Bun is required for the local binary release build."); + } + const platform = currentBinaryPlatform(); + mkdirSync(targetDirectory, { recursive: true }); + const executableName = platform.startsWith("windows-") ? "pi.exe" : "pi"; + const executablePath = join(targetDirectory, executableName); + const target = `bun-${platform}`; + run("bun", ["build", "--compile", `--target=${target}`, join(repoRoot, "packages", "coding-agent", "dist", "bun", "cli.js"), "--outfile", executablePath]); + copyBinaryAssets(targetDirectory); + copyWindowsConsoleModeHelper(targetDirectory, platform); + if (platform.startsWith("windows-")) { + run("powershell", ["-NoProfile", "-Command", `Compress-Archive -Path '${join(targetDirectory, "*").replaceAll("'", "''")}' -DestinationPath '${join(archiveDirectory, `pi-${platform}.zip`).replaceAll("'", "''")}' -Force`]); + } else { + run("tar", ["-czf", join(archiveDirectory, `pi-${platform}.tar.gz`), "-C", targetDirectory, "."]); + } + return platform; +} + +function createPiShim(installDirectory) { + const binDirectory = join(installDirectory, "node_modules", ".bin"); + if (process.platform === "win32") { + if (existsSync(join(binDirectory, "pi.cmd"))) { + writeFileSync(join(installDirectory, "pi.cmd"), '@ECHO off\r\n"%~dp0node_modules\\.bin\\pi.cmd" %*\r\n'); + writeFileSync(join(installDirectory, "pi.ps1"), '& "$PSScriptRoot/node_modules/.bin/pi.ps1" @args\n'); + return; + } + writeFileSync(join(installDirectory, "pi.cmd"), '@ECHO off\r\n"%~dp0node_modules\\.bin\\pi.exe" %*\r\n'); + writeFileSync(join(installDirectory, "pi.ps1"), '& "$PSScriptRoot/node_modules/.bin/pi.exe" @args\n'); + return; + } + symlinkSync(join("node_modules", ".bin", "pi"), join(installDirectory, "pi")); +} + function packPackage(pkg, tarballDirectory) { const packageJson = readPackageJson(pkg.directory); if (packageJson.name !== pkg.name) { @@ -148,7 +216,8 @@ if (rootPackageJson.name !== "pi-monorepo") { const outDir = prepareOutputDirectory(options, repoRoot); const tarballDirectory = join(outDir, "tarballs"); const nodeInstallDirectory = join(outDir, "node"); -const bunInstallDirectory = join(outDir, "bun"); +const bunInstallDirectory = join(outDir, "bun-install"); +const binaryDirectory = join(outDir, "bun"); mkdirSync(tarballDirectory, { recursive: true }); if (!options.skipCheck) { @@ -166,16 +235,19 @@ for (const pkg of packages) { tarballs.set(pkg.name, tarball); } +let binaryPlatform; if (!options.skipInstall) { + binaryPlatform = buildBunBinaryRelease(binaryDirectory, outDir); + mkdirSync(nodeInstallDirectory, { recursive: true }); const dependencies = Object.fromEntries( packages.map((pkg) => [pkg.name, fileSpecifier(nodeInstallDirectory, tarballs.get(pkg.name))]), ); - const installPackageJson = `${JSON.stringify({ private: true, dependencies }, undefined, "\t")}\n`; + const installPackageJson = `${JSON.stringify({ private: true, dependencies, overrides: dependencies }, undefined, "\t")}\n`; writeFileSync(join(nodeInstallDirectory, "package.json"), installPackageJson); run("npm", ["install", "--omit=dev", "--ignore-scripts"], { cwd: nodeInstallDirectory }); - symlinkSync(join("node_modules", ".bin", "pi"), join(nodeInstallDirectory, "pi")); + createPiShim(nodeInstallDirectory); if (!options.skipBunInstall) { if (!commandExists("bun")) { @@ -185,9 +257,9 @@ if (!options.skipInstall) { const bunDependencies = Object.fromEntries( packages.map((pkg) => [pkg.name, fileSpecifier(bunInstallDirectory, tarballs.get(pkg.name))]), ); - writeFileSync(join(bunInstallDirectory, "package.json"), `${JSON.stringify({ private: true, dependencies: bunDependencies }, undefined, "\t")}\n`); + writeFileSync(join(bunInstallDirectory, "package.json"), `${JSON.stringify({ private: true, dependencies: bunDependencies, overrides: bunDependencies }, undefined, "\t")}\n`); run("bun", ["install", "--production", "--ignore-scripts"], { cwd: bunInstallDirectory }); - symlinkSync(join("node_modules", ".bin", "pi"), join(bunInstallDirectory, "pi")); + createPiShim(bunInstallDirectory); } } @@ -199,15 +271,21 @@ for (const tarball of tarballs.values()) { } if (!options.skipInstall) { + console.log("\nLocal Bun binary release:"); + console.log(` ${binaryDirectory}`); + console.log(` ${join(outDir, `pi-${binaryPlatform}.${String(binaryPlatform).startsWith("windows-") ? "zip" : "tar.gz"}`)}`); + console.log("\nRun the local Bun binary release from outside the repository:"); + console.log(` ${join(binaryDirectory, String(binaryPlatform).startsWith("windows-") ? "pi.exe" : "pi")} --help`); + console.log("\nIsolated npm install:"); console.log(` ${nodeInstallDirectory}`); console.log("\nRun the locally packed npm CLI from outside the repository:"); - console.log(` ${join(nodeInstallDirectory, "pi")} --help`); + console.log(` ${join(nodeInstallDirectory, process.platform === "win32" ? "pi.cmd" : "pi")} --help`); if (!options.skipBunInstall) { - console.log("\nIsolated Bun install:"); + console.log("\nIsolated Bun package install:"); console.log(` ${bunInstallDirectory}`); - console.log("\nRun the locally packed Bun CLI from outside the repository:"); - console.log(` ${join(bunInstallDirectory, "pi")} --help`); + console.log("\nRun the locally packed Bun package CLI from outside the repository:"); + console.log(` ${join(bunInstallDirectory, process.platform === "win32" ? "pi.cmd" : "pi")} --help`); } } From 7dad27e5f2eaa30f25992a6ae400519586e8b568 Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Thu, 21 May 2026 12:03:55 +0200 Subject: [PATCH 155/166] fix(coding-agent): avoid duplicate bash truncation path closes #4819 --- packages/coding-agent/CHANGELOG.md | 4 ++ packages/coding-agent/src/core/tools/bash.ts | 12 ++++-- .../test/tool-execution-component.test.ts | 37 +++++++++++++++++++ 3 files changed, 50 insertions(+), 3 deletions(-) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 468c4d84..61f7ad6e 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Fixed + +- Fixed final bash tool cards to avoid rendering duplicate full-output truncation paths ([#4819](https://github.com/earendil-works/pi/issues/4819)). + ## [0.75.4] - 2026-05-20 ### New Features diff --git a/packages/coding-agent/src/core/tools/bash.ts b/packages/coding-agent/src/core/tools/bash.ts index f70c9470..b9e55142 100644 --- a/packages/coding-agent/src/core/tools/bash.ts +++ b/packages/coding-agent/src/core/tools/bash.ts @@ -200,7 +200,15 @@ function rebuildBashResultRenderComponent( const state = component.state; component.clear(); - const output = getTextOutput(result as any, showImages).trim(); + let output = getTextOutput(result as any, showImages).trim(); + const truncation = result.details?.truncation; + const fullOutputPath = result.details?.fullOutputPath; + if (!options.isPartial && truncation?.truncated && fullOutputPath && output.endsWith("]")) { + const footerStart = output.lastIndexOf("\n\n["); + if (footerStart !== -1 && output.slice(footerStart).includes(fullOutputPath)) { + output = output.slice(0, footerStart).trimEnd(); + } + } if (output) { const styledOutput = output @@ -236,8 +244,6 @@ function rebuildBashResultRenderComponent( } } - const truncation = result.details?.truncation; - const fullOutputPath = result.details?.fullOutputPath; if (truncation?.truncated || fullOutputPath) { const warnings: string[] = []; if (fullOutputPath) { diff --git a/packages/coding-agent/test/tool-execution-component.test.ts b/packages/coding-agent/test/tool-execution-component.test.ts index 54c75806..c227515b 100644 --- a/packages/coding-agent/test/tool-execution-component.test.ts +++ b/packages/coding-agent/test/tool-execution-component.test.ts @@ -123,6 +123,43 @@ describe("ToolExecutionComponent parity", () => { await promise; }); + test("bash renderer does not duplicate final full output truncation details", async () => { + const operations: BashOperations = { + exec: async (_command, _cwd, { onData }) => { + for (let i = 1; i <= 4000; i++) { + onData(Buffer.from(`line-${String(i).padStart(4, "0")}\n`)); + } + return { exitCode: 0 }; + }, + }; + const tool = createBashToolDefinition(process.cwd(), { operations }); + const result = await tool.execute( + "tool-bash-1b", + { command: "generate output" }, + undefined, + undefined, + {} as never, + ); + const component = new ToolExecutionComponent( + "bash", + "tool-bash-1b", + { command: "generate output" }, + {}, + tool, + createFakeTui(), + process.cwd(), + ); + component.setExpanded(true); + component.updateResult({ ...result, isError: false }, false); + + const rendered = stripAnsi(component.render(200).join("\n")); + expect(rendered.match(/Full output:/g)?.length ?? 0).toBe(1); + expect(rendered).toMatch(/line-4000[^\n]*\n[^\S\n]*\n \[Full output:/); + expect(rendered).not.toMatch(/line-4000[^\n]*\n[^\S\n]*\n[^\S\n]*\n \[Full output:/); + expect(rendered).toContain("Truncated: showing 2000 of 4001 lines"); + expect(rendered).not.toContain("[Showing lines 2002-4001 of 4001. Full output:"); + }); + test("does not duplicate built-in headers when passed the active built-in definition", () => { const component = new ToolExecutionComponent( "read", From 60a55a23991a1033243cb6f6d6e29670b9964c11 Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Thu, 21 May 2026 12:15:39 +0200 Subject: [PATCH 156/166] feat(coding-agent): expose edit tool unified patch closes #4821 --- packages/coding-agent/CHANGELOG.md | 4 ++++ packages/coding-agent/docs/sdk.md | 2 ++ packages/coding-agent/src/core/tools/edit-diff.ts | 10 +++++++++- packages/coding-agent/src/core/tools/edit.ts | 8 ++++++-- packages/coding-agent/test/tools.test.ts | 7 +++++++ 5 files changed, 28 insertions(+), 3 deletions(-) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 61f7ad6e..5d14b69a 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Added + +- Added a standard unified patch to edit tool result details for SDK consumers ([#4821](https://github.com/earendil-works/pi/issues/4821)). + ### Fixed - Fixed final bash tool cards to avoid rendering duplicate full-output truncation paths ([#4819](https://github.com/earendil-works/pi/issues/4819)). diff --git a/packages/coding-agent/docs/sdk.md b/packages/coding-agent/docs/sdk.md index 2fa85cd0..8377185e 100644 --- a/packages/coding-agent/docs/sdk.md +++ b/packages/coding-agent/docs/sdk.md @@ -473,6 +473,8 @@ Specify which built-in tools to enable: - `noTools: "all"` disables all tools - `noTools: "builtin"` disables default built-ins while keeping extension and custom tools enabled +The `edit` tool returns `details.diff` for Pi's TUI display and `details.patch` as a standard unified patch for SDK consumers. + ```typescript import { createAgentSession } from "@earendil-works/pi-coding-agent"; diff --git a/packages/coding-agent/src/core/tools/edit-diff.ts b/packages/coding-agent/src/core/tools/edit-diff.ts index 691d882a..f280bf19 100644 --- a/packages/coding-agent/src/core/tools/edit-diff.ts +++ b/packages/coding-agent/src/core/tools/edit-diff.ts @@ -259,8 +259,16 @@ export function applyEditsToNormalizedContent( return { baseContent, newContent }; } +/** Generate a standard unified patch. */ +export function generateUnifiedPatch(path: string, oldContent: string, newContent: string, contextLines = 4): string { + return Diff.createTwoFilesPatch(path, path, oldContent, newContent, undefined, undefined, { + context: contextLines, + headerOptions: Diff.FILE_HEADERS_ONLY, + }); +} + /** - * Generate a unified diff string with line numbers and context. + * Generate a display-oriented diff string with line numbers and context. * Returns both the diff string and the first changed line number (in the new file). */ export function generateDiffString( diff --git a/packages/coding-agent/src/core/tools/edit.ts b/packages/coding-agent/src/core/tools/edit.ts index e5656378..5d915d45 100644 --- a/packages/coding-agent/src/core/tools/edit.ts +++ b/packages/coding-agent/src/core/tools/edit.ts @@ -13,6 +13,7 @@ import { type EditDiffError, type EditDiffResult, generateDiffString, + generateUnifiedPatch, normalizeToLF, restoreLineEndings, stripBom, @@ -57,8 +58,10 @@ type LegacyEditToolInput = EditToolInput & { }; export interface EditToolDetails { - /** Unified diff of the changes made */ + /** Display-oriented diff of the changes made */ diff: string; + /** Standard unified patch of the changes made */ + patch: string; /** Line number of the first change in the new file (for editor navigation) */ firstChangedLine?: number; } @@ -394,6 +397,7 @@ export function createEditToolDefinition( } const diffResult = generateDiffString(baseContent, newContent); + const patch = generateUnifiedPatch(path, baseContent, newContent); resolve({ content: [ { @@ -401,7 +405,7 @@ export function createEditToolDefinition( text: `Successfully replaced ${edits.length} block(s) in ${path}.`, }, ], - details: { diff: diffResult.diff, firstChangedLine: diffResult.firstChangedLine }, + details: { diff: diffResult.diff, patch, firstChangedLine: diffResult.firstChangedLine }, }); } catch (error: unknown) { // Clean up abort handler. diff --git a/packages/coding-agent/test/tools.test.ts b/packages/coding-agent/test/tools.test.ts index 70d4ec2d..1d850e49 100644 --- a/packages/coding-agent/test/tools.test.ts +++ b/packages/coding-agent/test/tools.test.ts @@ -1,3 +1,4 @@ +import { applyPatch } from "diff"; import { chmodSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "fs"; import { tmpdir } from "os"; import { join } from "path"; @@ -239,6 +240,12 @@ describe("Coding Agent Tools", () => { expect(result.details.diff).toBeDefined(); expect(typeof result.details.diff).toBe("string"); expect(result.details.diff).toContain("testing"); + expect(result.details.patch).toContain("--- "); + expect(result.details.patch).toContain("+++ "); + expect(result.details.patch).toContain("@@"); + expect(result.details.patch).toContain("-Hello, world!"); + expect(result.details.patch).toContain("+Hello, testing!"); + expect(applyPatch(originalContent, result.details.patch)).toBe("Hello, testing!"); }); it("should fail if text not found", async () => { From f953067814ceed5fc9366db577333c460c9c5e4f Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Thu, 21 May 2026 16:54:56 +0200 Subject: [PATCH 157/166] fix(coding-agent): correct bash truncation line count closes #4818 --- packages/coding-agent/CHANGELOG.md | 1 + .../src/core/tools/output-accumulator.ts | 12 +++++++--- .../coding-agent/src/core/tools/truncate.ts | 15 +++++++++++-- packages/coding-agent/test/tools.test.ts | 22 +++++++++++++++++++ 4 files changed, 45 insertions(+), 5 deletions(-) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 5d14b69a..611f82e2 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -9,6 +9,7 @@ ### Fixed - Fixed final bash tool cards to avoid rendering duplicate full-output truncation paths ([#4819](https://github.com/earendil-works/pi/issues/4819)). +- Fixed bash tool truncation line counts to ignore the trailing newline as an extra output line ([#4818](https://github.com/earendil-works/pi/issues/4818)). ## [0.75.4] - 2026-05-20 diff --git a/packages/coding-agent/src/core/tools/output-accumulator.ts b/packages/coding-agent/src/core/tools/output-accumulator.ts index 78fcfe9e..e13fe458 100644 --- a/packages/coding-agent/src/core/tools/output-accumulator.ts +++ b/packages/coding-agent/src/core/tools/output-accumulator.ts @@ -45,8 +45,10 @@ export class OutputAccumulator { private tailStartsAtLineBoundary = true; private totalRawBytes = 0; private totalDecodedBytes = 0; - private totalLines = 1; + private completedLines = 0; + private totalLines = 0; private currentLineBytes = 0; + private hasOpenLine = false; private finished = false; private tempFilePath: string | undefined; @@ -164,10 +166,14 @@ export class OutputAccumulator { } if (newlines === 0) { this.currentLineBytes += bytes; + this.hasOpenLine = true; } else { - this.totalLines += newlines; - this.currentLineBytes = byteLength(text.slice(lastNewline + 1)); + this.completedLines += newlines; + const tail = text.slice(lastNewline + 1); + this.currentLineBytes = byteLength(tail); + this.hasOpenLine = tail.length > 0; } + this.totalLines = this.completedLines + (this.hasOpenLine ? 1 : 0); } private trimTail(): void { diff --git a/packages/coding-agent/src/core/tools/truncate.ts b/packages/coding-agent/src/core/tools/truncate.ts index 18ac5d74..c638ee48 100644 --- a/packages/coding-agent/src/core/tools/truncate.ts +++ b/packages/coding-agent/src/core/tools/truncate.ts @@ -44,6 +44,17 @@ export interface TruncationOptions { maxBytes?: number; } +function splitLinesForCounting(content: string): string[] { + if (content.length === 0) { + return []; + } + const lines = content.split("\n"); + if (content.endsWith("\n")) { + lines.pop(); + } + return lines; +} + /** * Format bytes as human-readable size. */ @@ -69,7 +80,7 @@ export function truncateHead(content: string, options: TruncationOptions = {}): const maxBytes = options.maxBytes ?? DEFAULT_MAX_BYTES; const totalBytes = Buffer.byteLength(content, "utf-8"); - const lines = content.split("\n"); + const lines = splitLinesForCounting(content); const totalLines = lines.length; // Check if no truncation needed @@ -159,7 +170,7 @@ export function truncateTail(content: string, options: TruncationOptions = {}): const maxBytes = options.maxBytes ?? DEFAULT_MAX_BYTES; const totalBytes = Buffer.byteLength(content, "utf-8"); - const lines = content.split("\n"); + const lines = splitLinesForCounting(content); const totalLines = lines.length; // Check if no truncation needed diff --git a/packages/coding-agent/test/tools.test.ts b/packages/coding-agent/test/tools.test.ts index 1d850e49..c5e8fa6f 100644 --- a/packages/coding-agent/test/tools.test.ts +++ b/packages/coding-agent/test/tools.test.ts @@ -580,6 +580,28 @@ describe("Coding Agent Tools", () => { expect(getTextOutput(result)).toContain("line 4999"); }); + it("should not count a trailing newline as an extra truncated bash output line", async () => { + const operations: BashOperations = { + exec: async (_command, _cwd, { onData }) => { + for (let i = 1; i <= 4000; i++) { + onData(Buffer.from(`line-${String(i).padStart(4, "0")}\n`, "utf-8")); + } + return { exitCode: 0 }; + }, + }; + const bash = createBashTool(testDir, { operations }); + + const result = await bash.execute("test-call-trailing-newline-line-count", { command: "many-lines" }); + const output = getTextOutput(result); + + expect(result.details?.truncation?.totalLines).toBe(4000); + expect(result.details?.truncation?.outputLines).toBe(2000); + expect(output).toContain("line-2001"); + expect(output).toContain("line-4000"); + expect(output).toMatch(/\[Showing lines 2001-4000 of 4000\. Full output: /); + expect(output).not.toContain("4001"); + }); + it("should decode UTF-8 characters split across output chunks", async () => { const euro = Buffer.from("€\n", "utf-8"); const operations: BashOperations = { From 2171cefbaa92c414f6567377f8febebea4078624 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Thu, 21 May 2026 18:35:48 +0200 Subject: [PATCH 158/166] test(coding-agent): update bash truncation expectation --- packages/coding-agent/test/tool-execution-component.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/coding-agent/test/tool-execution-component.test.ts b/packages/coding-agent/test/tool-execution-component.test.ts index c227515b..9a3d1639 100644 --- a/packages/coding-agent/test/tool-execution-component.test.ts +++ b/packages/coding-agent/test/tool-execution-component.test.ts @@ -156,8 +156,8 @@ describe("ToolExecutionComponent parity", () => { expect(rendered.match(/Full output:/g)?.length ?? 0).toBe(1); expect(rendered).toMatch(/line-4000[^\n]*\n[^\S\n]*\n \[Full output:/); expect(rendered).not.toMatch(/line-4000[^\n]*\n[^\S\n]*\n[^\S\n]*\n \[Full output:/); - expect(rendered).toContain("Truncated: showing 2000 of 4001 lines"); - expect(rendered).not.toContain("[Showing lines 2002-4001 of 4001. Full output:"); + expect(rendered).toContain("Truncated: showing 2000 of 4000 lines"); + expect(rendered).not.toContain("[Showing lines 2001-4000 of 4000. Full output:"); }); test("does not duplicate built-in headers when passed the active built-in definition", () => { From 136716420471a32f04958f186618c5659f3cc90b Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 21 May 2026 18:34:39 +0000 Subject: [PATCH 159/166] chore: approve contributor AJM10565 --- .github/APPROVED_CONTRIBUTORS | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/APPROVED_CONTRIBUTORS b/.github/APPROVED_CONTRIBUTORS index faefe5ae..f53db6a6 100644 --- a/.github/APPROVED_CONTRIBUTORS +++ b/.github/APPROVED_CONTRIBUTORS @@ -215,3 +215,5 @@ mattiacerutti pr josephyoung pr mbazso pr + +AJM10565 pr From b0c5554902ac94edc36b99e62318a327aa88d2da Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Thu, 21 May 2026 20:43:48 +0200 Subject: [PATCH 160/166] docs: document safe development install closes #4868 --- README.md | 2 +- packages/coding-agent/CHANGELOG.md | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 750b3a61..b9a45021 100644 --- a/README.md +++ b/README.md @@ -63,7 +63,7 @@ See [CONTRIBUTING.md](CONTRIBUTING.md) for contribution guidelines and [AGENTS.m ## Development ```bash -npm install # Install all dependencies +npm install --ignore-scripts # Install all dependencies without running lifecycle scripts npm run build # Build all packages npm run check # Lint, format, and type check ./test.sh # Run tests (skips LLM-dependent tests without API keys) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 611f82e2..1a3bb51c 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -6,6 +6,10 @@ - Added a standard unified patch to edit tool result details for SDK consumers ([#4821](https://github.com/earendil-works/pi/issues/4821)). +### Changed + +- Changed the root development install documentation to use `npm install --ignore-scripts` ([#4868](https://github.com/earendil-works/pi/issues/4868)). + ### Fixed - Fixed final bash tool cards to avoid rendering duplicate full-output truncation paths ([#4819](https://github.com/earendil-works/pi/issues/4819)). From 11c3da4f74bbd4580fbc8e0ca9caaf038c801655 Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Thu, 21 May 2026 23:27:40 +0200 Subject: [PATCH 161/166] fix(ai): set bedrock claude default max tokens closes #4848 --- packages/ai/CHANGELOG.md | 4 +++ packages/ai/src/providers/amazon-bedrock.ts | 3 +- .../ai/test/bedrock-thinking-payload.test.ts | 34 +++++++++++++++++++ 3 files changed, 40 insertions(+), 1 deletion(-) diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index 3d20b95a..841f3fed 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Fixed + +- Fixed Amazon Bedrock Claude requests to send the model output token cap by default, matching Anthropic requests and avoiding Bedrock's 4096-token default truncation ([#4848](https://github.com/earendil-works/pi/issues/4848)). + ## [0.75.4] - 2026-05-20 ### Changed diff --git a/packages/ai/src/providers/amazon-bedrock.ts b/packages/ai/src/providers/amazon-bedrock.ts index 5d89ac56..429098a6 100644 --- a/packages/ai/src/providers/amazon-bedrock.ts +++ b/packages/ai/src/providers/amazon-bedrock.ts @@ -183,12 +183,13 @@ export const streamBedrock: StreamFunction<"bedrock-converse-stream", BedrockOpt try { const client = new BedrockRuntimeClient(config); const cacheRetention = resolveCacheRetention(options.cacheRetention); + const inferenceMaxTokens = options.maxTokens ?? (isAnthropicClaudeModel(model) ? model.maxTokens : undefined); let commandInput = { modelId: model.id, messages: convertMessages(context, model, cacheRetention), system: buildSystemPrompt(context.systemPrompt, model, cacheRetention), inferenceConfig: { - ...(options.maxTokens !== undefined && { maxTokens: options.maxTokens }), + ...(inferenceMaxTokens !== undefined && { maxTokens: inferenceMaxTokens }), ...(options.temperature !== undefined && { temperature: options.temperature }), }, toolConfig: convertToolConfig(context.tools, options.toolChoice), diff --git a/packages/ai/test/bedrock-thinking-payload.test.ts b/packages/ai/test/bedrock-thinking-payload.test.ts index 55b9e82b..46a5277b 100644 --- a/packages/ai/test/bedrock-thinking-payload.test.ts +++ b/packages/ai/test/bedrock-thinking-payload.test.ts @@ -2,6 +2,7 @@ import { describe, expect, it } from "vitest"; import { getModel } from "../src/models.ts"; import { type BedrockOptions, streamBedrock } from "../src/providers/amazon-bedrock.ts"; import type { Context, Model } from "../src/types.ts"; +import { hasBedrockCredentials } from "./bedrock-utils.ts"; interface BedrockThinkingPayload { additionalModelRequestFields?: { @@ -112,6 +113,39 @@ describe("Bedrock thinking payload", () => { }); }); +describe.skipIf(!hasBedrockCredentials())("Bedrock Claude max tokens E2E", () => { + it( + "uses the model maxTokens cap instead of Bedrock's 4096-token default for adaptive Claude models", + { retry: 2, timeout: 180000 }, + async () => { + const baseModel = getModel("amazon-bedrock", "global.anthropic.claude-sonnet-4-6"); + const model: Model<"bedrock-converse-stream"> = { + ...baseModel, + maxTokens: 6000, + }; + + const response = await streamBedrock( + model, + { + systemPrompt: "You are a deterministic text generator. Follow the requested output format exactly.", + messages: [ + { + role: "user", + content: + "Output exactly 5200 repetitions of the token alpha, separated by single spaces. Do not number them. Do not use markdown. Do not add any other text.", + timestamp: Date.now(), + }, + ], + }, + { reasoning: "low" }, + ).result(); + + expect(response.stopReason, response.errorMessage).not.toBe("error"); + expect(response.usage.output).toBeGreaterThan(4096); + }, + ); +}); + describe("Application inference profile support", () => { it("uses adaptive thinking when model.name contains the model name but ARN does not", async () => { const baseModel = getModel("amazon-bedrock", "global.anthropic.claude-opus-4-6-v1"); From ced73b396a280facbecc89d52259a88f9e0815ad Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Fri, 22 May 2026 00:28:49 +0200 Subject: [PATCH 162/166] docs: note Node 20 rescue release Closes #4876 --- packages/coding-agent/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 1a3bb51c..32fcca39 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -12,6 +12,7 @@ ### Fixed +- Published a 0.74.2 rescue release that tells Node 20 users to upgrade Node before updating to newer Pi versions ([#4876](https://github.com/earendil-works/pi/issues/4876)). - Fixed final bash tool cards to avoid rendering duplicate full-output truncation paths ([#4819](https://github.com/earendil-works/pi/issues/4819)). - Fixed bash tool truncation line counts to ignore the trailing newline as an extra output line ([#4818](https://github.com/earendil-works/pi/issues/4818)). From 1a2a536dbaf929d6c4e8a999f6ffd0967d8b09f6 Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Fri, 22 May 2026 11:11:38 +0200 Subject: [PATCH 163/166] chore: update PR prompt template --- .pi/prompts/pr.md | 28 +- packages/ai/src/models.generated.ts | 650 ++++++++++------------------ 2 files changed, 246 insertions(+), 432 deletions(-) diff --git a/.pi/prompts/pr.md b/.pi/prompts/pr.md index 800d4524..b1b2617e 100644 --- a/.pi/prompts/pr.md +++ b/.pi/prompts/pr.md @@ -8,22 +8,20 @@ For each PR URL, do the following in order: 1. Add the `inprogress` label to the PR via GitHub CLI before analysis starts. If adding the label fails, report that explicitly and continue. 2. Read the PR page in full. Include description, all comments, all commits, and all changed files. 3. Identify any linked issues referenced in the PR body, comments, commit messages, or cross links. Read each issue in full, including all comments. -4. Analyze the PR diff. Read all relevant code files in full with no truncation from the current main branch and compare against the diff. Do not fetch PR file blobs unless a file is missing on main or the diff context is insufficient. Include related code paths that are not in the diff but are required to validate behavior. -5. Check for a changelog entry in the relevant `packages/*/CHANGELOG.md` files. Report whether an entry exists. If missing, state that a changelog entry is required before merge and that you will add it if the user decides to merge. Follow the changelog format rules in AGENTS.md. Verify: - - Entry uses correct section (`### Breaking Changes`, `### Added`, `### Fixed`, etc.) - - External contributions include PR link and author: `Fixed foo ([#123](https://github.com/earendil-works/pi-mono/pull/123) by [@user](https://github.com/user))` - - Breaking changes are in `### Breaking Changes`, not just `### Fixed` +4. Analyze the PR diff. Read all relevant code files in full with no truncation and compare against the diff. Do not fetch PR file blobs unless a file is missing on main or the diff context is insufficient. Include related code paths that are not in the diff but are required to validate behavior. +5. Do not check for a changelog entry. Per CONTRIBUTING.md, contributor PRs must not edit `CHANGELOG.md` — the maintainer adds the entry when merging. 6. Check if packages/coding-agent/README.md, packages/coding-agent/docs/*.md, packages/coding-agent/examples/**/*.md require modification. This is usually the case when existing features have been changed, or new features have been added. 7. Provide a structured review with these sections: - - Good: solid choices or improvements - - Bad: concrete issues, regressions, missing tests, or risks - - Ugly: subtle or high impact problems -8. Add Questions or Assumptions if anything is unclear. -9. Add Change summary and Tests. + - What it does: one short paragraph describing the change and its intent. + - Good: solid choices or improvements. + - Bad: concrete issues, regressions, missing tests, or risks. + - Ugly: subtle or high impact problems. + - Tests: what is covered, what is missing, and whether existing tests are adequate. + - Open questions for you: only things blocking a merge decision that need the user's input. Omit the section entirely if there are none. Output format per PR: PR: -Changelog: +What it does: - ... Good: - ... @@ -31,11 +29,9 @@ Bad: - ... Ugly: - ... -Questions or Assumptions: -- ... -Change summary: -- ... Tests: - ... +Open questions for you: +- ... -If no issues are found, say so under Bad and Ugly. \ No newline at end of file +If no issues are found, say so under Bad and Ugly. diff --git a/packages/ai/src/models.generated.ts b/packages/ai/src/models.generated.ts index dbd7d495..782c4b97 100644 --- a/packages/ai/src/models.generated.ts +++ b/packages/ai/src/models.generated.ts @@ -3278,6 +3278,42 @@ export const MODELS = { contextWindow: 256000, maxTokens: 16384, } satisfies Model<"openai-completions">, + "@cf/ibm-granite/granite-4.0-h-micro": { + id: "@cf/ibm-granite/granite-4.0-h-micro", + name: "Granite 4.0 H Micro", + api: "openai-completions", + provider: "cloudflare-workers-ai", + baseUrl: "https://api.cloudflare.com/client/v4/accounts/{CLOUDFLARE_ACCOUNT_ID}/ai/v1", + compat: {"sendSessionAffinityHeaders":true}, + reasoning: false, + input: ["text"], + cost: { + input: 0.017, + output: 0.112, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 131000, + maxTokens: 131000, + } satisfies Model<"openai-completions">, + "@cf/meta/llama-3.3-70b-instruct-fp8-fast": { + id: "@cf/meta/llama-3.3-70b-instruct-fp8-fast", + name: "Llama 3.3 70B Instruct fp8 Fast", + api: "openai-completions", + provider: "cloudflare-workers-ai", + baseUrl: "https://api.cloudflare.com/client/v4/accounts/{CLOUDFLARE_ACCOUNT_ID}/ai/v1", + compat: {"sendSessionAffinityHeaders":true}, + reasoning: false, + input: ["text"], + cost: { + input: 0.293, + output: 2.253, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 24000, + maxTokens: 24000, + } satisfies Model<"openai-completions">, "@cf/meta/llama-4-scout-17b-16e-instruct": { id: "@cf/meta/llama-4-scout-17b-16e-instruct", name: "Llama 4 Scout 17B 16E Instruct", @@ -3293,9 +3329,27 @@ export const MODELS = { cacheRead: 0, cacheWrite: 0, }, - contextWindow: 128000, + contextWindow: 131000, maxTokens: 16384, } satisfies Model<"openai-completions">, + "@cf/mistralai/mistral-small-3.1-24b-instruct": { + id: "@cf/mistralai/mistral-small-3.1-24b-instruct", + name: "Mistral Small 3.1 24B Instruct", + api: "openai-completions", + provider: "cloudflare-workers-ai", + baseUrl: "https://api.cloudflare.com/client/v4/accounts/{CLOUDFLARE_ACCOUNT_ID}/ai/v1", + compat: {"sendSessionAffinityHeaders":true}, + reasoning: false, + input: ["text"], + cost: { + input: 0.351, + output: 0.555, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 128000, + } satisfies Model<"openai-completions">, "@cf/moonshotai/kimi-k2.5": { id: "@cf/moonshotai/kimi-k2.5", name: "Kimi K2.5", @@ -3329,7 +3383,7 @@ export const MODELS = { cacheRead: 0.16, cacheWrite: 0, }, - contextWindow: 256000, + contextWindow: 262144, maxTokens: 256000, } satisfies Model<"openai-completions">, "@cf/nvidia/nemotron-3-120b-a12b": { @@ -3386,6 +3440,24 @@ export const MODELS = { contextWindow: 128000, maxTokens: 16384, } satisfies Model<"openai-completions">, + "@cf/qwen/qwen3-30b-a3b-fp8": { + id: "@cf/qwen/qwen3-30b-a3b-fp8", + name: "Qwen3 30B A3b fp8", + api: "openai-completions", + provider: "cloudflare-workers-ai", + baseUrl: "https://api.cloudflare.com/client/v4/accounts/{CLOUDFLARE_ACCOUNT_ID}/ai/v1", + compat: {"sendSessionAffinityHeaders":true}, + reasoning: true, + input: ["text"], + cost: { + input: 0.0509, + output: 0.335, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 32768, + maxTokens: 32768, + } satisfies Model<"openai-completions">, "@cf/zai-org/glm-4.7-flash": { id: "@cf/zai-org/glm-4.7-flash", name: "GLM-4.7-Flash", @@ -3396,7 +3468,7 @@ export const MODELS = { reasoning: true, input: ["text"], cost: { - input: 0.06, + input: 0.0605, output: 0.4, cacheRead: 0, cacheWrite: 0, @@ -4169,57 +4241,6 @@ export const MODELS = { } satisfies Model<"openai-completions">, }, "google": { - "gemini-1.5-flash": { - id: "gemini-1.5-flash", - name: "Gemini 1.5 Flash", - api: "google-generative-ai", - provider: "google", - baseUrl: "https://generativelanguage.googleapis.com/v1beta", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.075, - output: 0.3, - cacheRead: 0.01875, - cacheWrite: 0, - }, - contextWindow: 1000000, - maxTokens: 8192, - } satisfies Model<"google-generative-ai">, - "gemini-1.5-flash-8b": { - id: "gemini-1.5-flash-8b", - name: "Gemini 1.5 Flash-8B", - api: "google-generative-ai", - provider: "google", - baseUrl: "https://generativelanguage.googleapis.com/v1beta", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.0375, - output: 0.15, - cacheRead: 0.01, - cacheWrite: 0, - }, - contextWindow: 1000000, - maxTokens: 8192, - } satisfies Model<"google-generative-ai">, - "gemini-1.5-pro": { - id: "gemini-1.5-pro", - name: "Gemini 1.5 Pro", - api: "google-generative-ai", - provider: "google", - baseUrl: "https://generativelanguage.googleapis.com/v1beta", - reasoning: false, - input: ["text", "image"], - cost: { - input: 1.25, - output: 5, - cacheRead: 0.3125, - cacheWrite: 0, - }, - contextWindow: 1000000, - maxTokens: 8192, - } satisfies Model<"google-generative-ai">, "gemini-2.0-flash": { id: "gemini-2.0-flash", name: "Gemini 2.0 Flash", @@ -4239,7 +4260,7 @@ export const MODELS = { } satisfies Model<"google-generative-ai">, "gemini-2.0-flash-lite": { id: "gemini-2.0-flash-lite", - name: "Gemini 2.0 Flash Lite", + name: "Gemini 2.0 Flash-Lite", api: "google-generative-ai", provider: "google", baseUrl: "https://generativelanguage.googleapis.com/v1beta", @@ -4273,7 +4294,7 @@ export const MODELS = { } satisfies Model<"google-generative-ai">, "gemini-2.5-flash-lite": { id: "gemini-2.5-flash-lite", - name: "Gemini 2.5 Flash Lite", + name: "Gemini 2.5 Flash-Lite", api: "google-generative-ai", provider: "google", baseUrl: "https://generativelanguage.googleapis.com/v1beta", @@ -4288,91 +4309,6 @@ export const MODELS = { contextWindow: 1048576, maxTokens: 65536, } satisfies Model<"google-generative-ai">, - "gemini-2.5-flash-lite-preview-06-17": { - id: "gemini-2.5-flash-lite-preview-06-17", - name: "Gemini 2.5 Flash Lite Preview 06-17", - api: "google-generative-ai", - provider: "google", - baseUrl: "https://generativelanguage.googleapis.com/v1beta", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.1, - output: 0.4, - cacheRead: 0.025, - cacheWrite: 0, - }, - contextWindow: 1048576, - maxTokens: 65536, - } satisfies Model<"google-generative-ai">, - "gemini-2.5-flash-lite-preview-09-2025": { - id: "gemini-2.5-flash-lite-preview-09-2025", - name: "Gemini 2.5 Flash Lite Preview 09-25", - api: "google-generative-ai", - provider: "google", - baseUrl: "https://generativelanguage.googleapis.com/v1beta", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.1, - output: 0.4, - cacheRead: 0.025, - cacheWrite: 0, - }, - contextWindow: 1048576, - maxTokens: 65536, - } satisfies Model<"google-generative-ai">, - "gemini-2.5-flash-preview-04-17": { - id: "gemini-2.5-flash-preview-04-17", - name: "Gemini 2.5 Flash Preview 04-17", - api: "google-generative-ai", - provider: "google", - baseUrl: "https://generativelanguage.googleapis.com/v1beta", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.15, - output: 0.6, - cacheRead: 0.0375, - cacheWrite: 0, - }, - contextWindow: 1048576, - maxTokens: 65536, - } satisfies Model<"google-generative-ai">, - "gemini-2.5-flash-preview-05-20": { - id: "gemini-2.5-flash-preview-05-20", - name: "Gemini 2.5 Flash Preview 05-20", - api: "google-generative-ai", - provider: "google", - baseUrl: "https://generativelanguage.googleapis.com/v1beta", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.15, - output: 0.6, - cacheRead: 0.0375, - cacheWrite: 0, - }, - contextWindow: 1048576, - maxTokens: 65536, - } satisfies Model<"google-generative-ai">, - "gemini-2.5-flash-preview-09-2025": { - id: "gemini-2.5-flash-preview-09-2025", - name: "Gemini 2.5 Flash Preview 09-25", - api: "google-generative-ai", - provider: "google", - baseUrl: "https://generativelanguage.googleapis.com/v1beta", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.3, - output: 2.5, - cacheRead: 0.075, - cacheWrite: 0, - }, - contextWindow: 1048576, - maxTokens: 65536, - } satisfies Model<"google-generative-ai">, "gemini-2.5-pro": { id: "gemini-2.5-pro", name: "Gemini 2.5 Pro", @@ -4390,40 +4326,6 @@ export const MODELS = { contextWindow: 1048576, maxTokens: 65536, } satisfies Model<"google-generative-ai">, - "gemini-2.5-pro-preview-05-06": { - id: "gemini-2.5-pro-preview-05-06", - name: "Gemini 2.5 Pro Preview 05-06", - api: "google-generative-ai", - provider: "google", - baseUrl: "https://generativelanguage.googleapis.com/v1beta", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1.25, - output: 10, - cacheRead: 0.31, - cacheWrite: 0, - }, - contextWindow: 1048576, - maxTokens: 65536, - } satisfies Model<"google-generative-ai">, - "gemini-2.5-pro-preview-06-05": { - id: "gemini-2.5-pro-preview-06-05", - name: "Gemini 2.5 Pro Preview 06-05", - api: "google-generative-ai", - provider: "google", - baseUrl: "https://generativelanguage.googleapis.com/v1beta", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1.25, - output: 10, - cacheRead: 0.31, - cacheWrite: 0, - }, - contextWindow: 1048576, - maxTokens: 65536, - } satisfies Model<"google-generative-ai">, "gemini-3-flash-preview": { id: "gemini-3-flash-preview", name: "Gemini 3 Flash Preview", @@ -4457,8 +4359,8 @@ export const MODELS = { cacheRead: 0.2, cacheWrite: 0, }, - contextWindow: 1000000, - maxTokens: 64000, + contextWindow: 1048576, + maxTokens: 65536, } satisfies Model<"google-generative-ai">, "gemini-3.1-flash-lite": { id: "gemini-3.1-flash-lite", @@ -4584,60 +4486,9 @@ export const MODELS = { contextWindow: 1048576, maxTokens: 65536, } satisfies Model<"google-generative-ai">, - "gemini-live-2.5-flash": { - id: "gemini-live-2.5-flash", - name: "Gemini Live 2.5 Flash", - api: "google-generative-ai", - provider: "google", - baseUrl: "https://generativelanguage.googleapis.com/v1beta", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.5, - output: 2, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 8000, - } satisfies Model<"google-generative-ai">, - "gemini-live-2.5-flash-preview-native-audio": { - id: "gemini-live-2.5-flash-preview-native-audio", - name: "Gemini Live 2.5 Flash Preview Native Audio", - api: "google-generative-ai", - provider: "google", - baseUrl: "https://generativelanguage.googleapis.com/v1beta", - reasoning: true, - input: ["text"], - cost: { - input: 0.5, - output: 2, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 65536, - } satisfies Model<"google-generative-ai">, - "gemma-3-27b-it": { - id: "gemma-3-27b-it", - name: "Gemma 3 27B", - api: "google-generative-ai", - provider: "google", - baseUrl: "https://generativelanguage.googleapis.com/v1beta", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 8192, - } satisfies Model<"google-generative-ai">, "gemma-4-26b-a4b-it": { id: "gemma-4-26b-a4b-it", - name: "Gemma 4 26B", + name: "Gemma 4 26B A4B IT", api: "google-generative-ai", provider: "google", baseUrl: "https://generativelanguage.googleapis.com/v1beta", @@ -4650,12 +4501,12 @@ export const MODELS = { cacheRead: 0, cacheWrite: 0, }, - contextWindow: 256000, - maxTokens: 8192, + contextWindow: 262144, + maxTokens: 32768, } satisfies Model<"google-generative-ai">, "gemma-4-31b-it": { id: "gemma-4-31b-it", - name: "Gemma 4 31B", + name: "Gemma 4 31B IT", api: "google-generative-ai", provider: "google", baseUrl: "https://generativelanguage.googleapis.com/v1beta", @@ -4668,8 +4519,8 @@ export const MODELS = { cacheRead: 0, cacheWrite: 0, }, - contextWindow: 256000, - maxTokens: 8192, + contextWindow: 262144, + maxTokens: 32768, } satisfies Model<"google-generative-ai">, }, "google-vertex": { @@ -7511,6 +7362,24 @@ export const MODELS = { contextWindow: 1048576, maxTokens: 65536, } satisfies Model<"google-generative-ai">, + "gemini-3.5-flash": { + id: "gemini-3.5-flash", + name: "Gemini 3.5 Flash", + api: "google-generative-ai", + provider: "opencode", + baseUrl: "https://opencode.ai/zen/v1", + reasoning: true, + thinkingLevelMap: {"off":null}, + input: ["text", "image"], + cost: { + input: 1.5, + output: 9, + cacheRead: 0.15, + cacheWrite: 0, + }, + contextWindow: 1048576, + maxTokens: 65536, + } satisfies Model<"google-generative-ai">, "glm-5": { id: "glm-5", name: "GLM-5", @@ -7833,6 +7702,23 @@ export const MODELS = { contextWindow: 1050000, maxTokens: 128000, } satisfies Model<"openai-responses">, + "grok-build-0.1": { + id: "grok-build-0.1", + name: "Grok Build 0.1", + api: "openai-completions", + provider: "opencode", + baseUrl: "https://opencode.ai/zen/v1", + reasoning: true, + input: ["text", "image"], + cost: { + input: 1, + output: 2, + cacheRead: 0.2, + cacheWrite: 0, + }, + contextWindow: 256000, + maxTokens: 256000, + } satisfies Model<"openai-completions">, "kimi-k2.5": { id: "kimi-k2.5", name: "Kimi K2.5", @@ -7884,23 +7770,6 @@ export const MODELS = { contextWindow: 204800, maxTokens: 131072, } satisfies Model<"openai-completions">, - "minimax-m2.5-free": { - id: "minimax-m2.5-free", - name: "MiniMax M2.5 Free", - api: "anthropic-messages", - provider: "opencode", - baseUrl: "https://opencode.ai/zen", - reasoning: true, - input: ["text"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 204800, - maxTokens: 131072, - } satisfies Model<"anthropic-messages">, "minimax-m2.7": { id: "minimax-m2.7", name: "MiniMax M2.7", @@ -8544,23 +8413,6 @@ export const MODELS = { contextWindow: 1000000, maxTokens: 128000, } satisfies Model<"openai-completions">, - "arcee-ai/trinity-large-preview": { - id: "arcee-ai/trinity-large-preview", - name: "Arcee AI: Trinity Large Preview", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 0.15, - output: 0.44999999999999996, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131000, - maxTokens: 4096, - } satisfies Model<"openai-completions">, "arcee-ai/trinity-large-thinking": { id: "arcee-ai/trinity-large-thinking", name: "Arcee AI: Trinity Large Thinking", @@ -9023,7 +8875,7 @@ export const MODELS = { cacheRead: 0.024999999999999998, cacheWrite: 0.08333333333333334, }, - contextWindow: 1048576, + contextWindow: 1000000, maxTokens: 8192, } satisfies Model<"openai-completions">, "google/gemini-2.0-flash-lite-001": { @@ -9393,9 +9245,9 @@ export const MODELS = { reasoning: false, input: ["text"], cost: { - input: 0.3, - output: 2.5, - cacheRead: 0.06, + input: 0.075, + output: 0.625, + cacheRead: 0.015, cacheWrite: 0, }, contextWindow: 262144, @@ -11940,7 +11792,7 @@ export const MODELS = { cost: { input: 0.39, output: 2.34, - cacheRead: 0.195, + cacheRead: 0, cacheWrite: 0, }, contextWindow: 262144, @@ -12023,13 +11875,13 @@ export const MODELS = { reasoning: true, input: ["text", "image"], cost: { - input: 0.32, + input: 0.317, output: 3.1999999999999997, cacheRead: 0, cacheWrite: 0, }, contextWindow: 262144, - maxTokens: 81920, + maxTokens: 262140, } satisfies Model<"openai-completions">, "qwen/qwen3.6-35b-a3b": { id: "qwen/qwen3.6-35b-a3b", @@ -12099,6 +11951,23 @@ export const MODELS = { contextWindow: 1000000, maxTokens: 65536, } satisfies Model<"openai-completions">, + "qwen/qwen3.7-max": { + id: "qwen/qwen3.7-max", + name: "Qwen: Qwen3.7 Max", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + input: ["text"], + cost: { + input: 2.5, + output: 7.5, + cacheRead: 0, + cacheWrite: 3.125, + }, + contextWindow: 1000000, + maxTokens: 65536, + } satisfies Model<"openai-completions">, "rekaai/reka-edge": { id: "rekaai/reka-edge", name: "Reka Edge", @@ -12176,13 +12045,13 @@ export const MODELS = { reasoning: true, input: ["text"], cost: { - input: 0.09999999999999999, + input: 0.09, output: 0.3, - cacheRead: 0, + cacheRead: 0.02, cacheWrite: 0, }, contextWindow: 262144, - maxTokens: 65536, + maxTokens: 16384, } satisfies Model<"openai-completions">, "tencent/hy3-preview": { id: "tencent/hy3-preview", @@ -12286,6 +12155,23 @@ export const MODELS = { contextWindow: 1000000, maxTokens: 4096, } satisfies Model<"openai-completions">, + "x-ai/grok-build-0.1": { + id: "x-ai/grok-build-0.1", + name: "xAI: Grok Build 0.1", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + input: ["text", "image"], + cost: { + input: 1, + output: 2, + cacheRead: 0.19999999999999998, + cacheWrite: 0, + }, + contextWindow: 256000, + maxTokens: 4096, + } satisfies Model<"openai-completions">, "xiaomi/mimo-v2-flash": { id: "xiaomi/mimo-v2-flash", name: "Xiaomi: MiMo-V2-Flash", @@ -12567,13 +12453,13 @@ export const MODELS = { reasoning: true, input: ["text"], cost: { - input: 0, - output: 0, - cacheRead: 0, + input: 0.98, + output: 3.08, + cacheRead: 0.182, cacheWrite: 0, }, - contextWindow: 202800, - maxTokens: 202800, + contextWindow: 202752, + maxTokens: 4096, } satisfies Model<"openai-completions">, "z-ai/glm-5v-turbo": { id: "z-ai/glm-5v-turbo", @@ -13358,6 +13244,23 @@ export const MODELS = { contextWindow: 1000000, maxTokens: 64000, } satisfies Model<"anthropic-messages">, + "alibaba/qwen3.7-max": { + id: "alibaba/qwen3.7-max", + name: "Qwen 3.7 Max", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text", "image"], + cost: { + input: 2.5, + output: 7.5, + cacheRead: 0.5, + cacheWrite: 3.125, + }, + contextWindow: 991000, + maxTokens: 64000, + } satisfies Model<"anthropic-messages">, "anthropic/claude-3-haiku": { id: "anthropic/claude-3-haiku", name: "Claude 3 Haiku", @@ -14397,6 +14300,23 @@ export const MODELS = { contextWindow: 128000, maxTokens: 64000, } satisfies Model<"anthropic-messages">, + "mistral/mistral-medium-3.5": { + id: "mistral/mistral-medium-3.5", + name: "Mistral Medium Latest", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text"], + cost: { + input: 1.5, + output: 7.5, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 256000, + maxTokens: 256000, + } satisfies Model<"anthropic-messages">, "mistral/mistral-small": { id: "mistral/mistral-small", name: "Mistral Small", @@ -15412,6 +15332,23 @@ export const MODELS = { contextWindow: 1000000, maxTokens: 1000000, } satisfies Model<"anthropic-messages">, + "xai/grok-build-0.1": { + id: "xai/grok-build-0.1", + name: "Grok Build 0.1", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text", "image"], + cost: { + input: 1, + output: 2, + cacheRead: 0.19999999999999998, + cacheWrite: 0, + }, + contextWindow: 256000, + maxTokens: 256000, + } satisfies Model<"anthropic-messages">, "xiaomi/mimo-v2-flash": { id: "xiaomi/mimo-v2-flash", name: "MiMo V2 Flash", @@ -15703,108 +15640,6 @@ export const MODELS = { } satisfies Model<"anthropic-messages">, }, "xai": { - "grok-2": { - id: "grok-2", - name: "Grok 2", - api: "openai-completions", - provider: "xai", - baseUrl: "https://api.x.ai/v1", - reasoning: false, - input: ["text"], - cost: { - input: 2, - output: 10, - cacheRead: 2, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 8192, - } satisfies Model<"openai-completions">, - "grok-2-1212": { - id: "grok-2-1212", - name: "Grok 2 (1212)", - api: "openai-completions", - provider: "xai", - baseUrl: "https://api.x.ai/v1", - reasoning: false, - input: ["text"], - cost: { - input: 2, - output: 10, - cacheRead: 2, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 8192, - } satisfies Model<"openai-completions">, - "grok-2-latest": { - id: "grok-2-latest", - name: "Grok 2 Latest", - api: "openai-completions", - provider: "xai", - baseUrl: "https://api.x.ai/v1", - reasoning: false, - input: ["text"], - cost: { - input: 2, - output: 10, - cacheRead: 2, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 8192, - } satisfies Model<"openai-completions">, - "grok-2-vision": { - id: "grok-2-vision", - name: "Grok 2 Vision", - api: "openai-completions", - provider: "xai", - baseUrl: "https://api.x.ai/v1", - reasoning: false, - input: ["text", "image"], - cost: { - input: 2, - output: 10, - cacheRead: 2, - cacheWrite: 0, - }, - contextWindow: 8192, - maxTokens: 4096, - } satisfies Model<"openai-completions">, - "grok-2-vision-1212": { - id: "grok-2-vision-1212", - name: "Grok 2 Vision (1212)", - api: "openai-completions", - provider: "xai", - baseUrl: "https://api.x.ai/v1", - reasoning: false, - input: ["text", "image"], - cost: { - input: 2, - output: 10, - cacheRead: 2, - cacheWrite: 0, - }, - contextWindow: 8192, - maxTokens: 4096, - } satisfies Model<"openai-completions">, - "grok-2-vision-latest": { - id: "grok-2-vision-latest", - name: "Grok 2 Vision Latest", - api: "openai-completions", - provider: "xai", - baseUrl: "https://api.x.ai/v1", - reasoning: false, - input: ["text", "image"], - cost: { - input: 2, - output: 10, - cacheRead: 2, - cacheWrite: 0, - }, - contextWindow: 8192, - maxTokens: 4096, - } satisfies Model<"openai-completions">, "grok-3": { id: "grok-3", name: "Grok 3", @@ -15848,8 +15683,8 @@ export const MODELS = { reasoning: false, input: ["text", "image"], cost: { - input: 2, - output: 6, + input: 1.25, + output: 2.5, cacheRead: 0.2, cacheWrite: 0, }, @@ -15865,8 +15700,8 @@ export const MODELS = { reasoning: true, input: ["text", "image"], cost: { - input: 2, - output: 6, + input: 1.25, + output: 2.5, cacheRead: 0.2, cacheWrite: 0, }, @@ -15890,22 +15725,22 @@ export const MODELS = { contextWindow: 1000000, maxTokens: 30000, } satisfies Model<"openai-completions">, - "grok-beta": { - id: "grok-beta", - name: "Grok Beta", + "grok-build-0.1": { + id: "grok-build-0.1", + name: "Grok Build 0.1", api: "openai-completions", provider: "xai", baseUrl: "https://api.x.ai/v1", - reasoning: false, - input: ["text"], + reasoning: true, + input: ["text", "image"], cost: { - input: 5, - output: 15, - cacheRead: 5, + input: 1, + output: 2, + cacheRead: 0.2, cacheWrite: 0, }, - contextWindow: 131072, - maxTokens: 4096, + contextWindow: 256000, + maxTokens: 256000, } satisfies Model<"openai-completions">, "grok-code-fast-1": { id: "grok-code-fast-1", @@ -15924,23 +15759,6 @@ export const MODELS = { contextWindow: 32768, maxTokens: 8192, } satisfies Model<"openai-completions">, - "grok-vision-beta": { - id: "grok-vision-beta", - name: "Grok Vision Beta", - api: "openai-completions", - provider: "xai", - baseUrl: "https://api.x.ai/v1", - reasoning: false, - input: ["text", "image"], - cost: { - input: 5, - output: 15, - cacheRead: 5, - cacheWrite: 0, - }, - contextWindow: 8192, - maxTokens: 4096, - } satisfies Model<"openai-completions">, }, "xiaomi": { "mimo-v2-flash": { From bf56a86e1e8ab6a57c1299fb9b2ab4adc5265d0b Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Fri, 22 May 2026 11:13:26 +0200 Subject: [PATCH 164/166] fix(coding-agent): reconcile git package refs closes #4870 --- packages/coding-agent/CHANGELOG.md | 1 + packages/coding-agent/README.md | 2 +- packages/coding-agent/docs/packages.md | 4 +- .../coding-agent/src/core/package-manager.ts | 35 ++++-- .../coding-agent/test/package-manager.test.ts | 107 ++++++++++++++++++ 5 files changed, 139 insertions(+), 10 deletions(-) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 32fcca39..ce69a3aa 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -12,6 +12,7 @@ ### Fixed +- Fixed git package installs to reconcile existing checkouts to the requested ref and update package settings without losing filters ([#4870](https://github.com/earendil-works/pi/issues/4870)). - Published a 0.74.2 rescue release that tells Node 20 users to upgrade Node before updating to newer Pi versions ([#4876](https://github.com/earendil-works/pi/issues/4876)). - Fixed final bash tool cards to avoid rendering duplicate full-output truncation paths ([#4819](https://github.com/earendil-works/pi/issues/4819)). - Fixed bash tool truncation line counts to ignore the trailing newline as an extra output line ([#4818](https://github.com/earendil-works/pi/issues/4818)). diff --git a/packages/coding-agent/README.md b/packages/coding-agent/README.md index 79f284a0..00fca81e 100644 --- a/packages/coding-agent/README.md +++ b/packages/coding-agent/README.md @@ -408,7 +408,7 @@ pi update npm:@foo/pi-tools # update one package pi config # enable/disable extensions, skills, prompts, themes ``` -Packages install to `~/.pi/agent/git/` (git) or `~/.pi/agent/npm/` (npm). Use `-l` for project-local installs (`.pi/git/`, `.pi/npm/`). Git packages install dependencies with `npm install --omit=dev` by default, so runtime deps must be listed under `dependencies`; when `npmCommand` is configured, git packages use plain `install` for compatibility with wrappers. If you use a Node version manager and want package installs to reuse a stable npm context, set `npmCommand` in `settings.json`, for example `["mise", "exec", "node@20", "--", "npm"]`. +Packages install to `~/.pi/agent/git/` (git) or `~/.pi/agent/npm/` (npm). Use `-l` for project-local installs (`.pi/git/`, `.pi/npm/`). Git `@ref` values are pinned tags or commits; pinned packages are skipped by `pi update`, so use `pi install git:host/user/repo@new-ref` to move an existing package to a new ref. Git packages install dependencies with `npm install --omit=dev` by default, so runtime deps must be listed under `dependencies`; when `npmCommand` is configured, git packages use plain `install` for compatibility with wrappers. If you use a Node version manager and want package installs to reuse a stable npm context, set `npmCommand` in `settings.json`, for example `["mise", "exec", "node@20", "--", "npm"]`. Create a package by adding a `pi` key to `package.json`: diff --git a/packages/coding-agent/docs/packages.md b/packages/coding-agent/docs/packages.md index db5ab6cc..1f3c69a6 100644 --- a/packages/coding-agent/docs/packages.md +++ b/packages/coding-agent/docs/packages.md @@ -85,9 +85,9 @@ ssh://git@github.com/user/repo@v1 - HTTPS and SSH URLs are both supported. - SSH URLs use your configured SSH keys automatically (respects `~/.ssh/config`). - For non-interactive runs (for example CI), you can set `GIT_TERMINAL_PROMPT=0` to disable credential prompts and set `GIT_SSH_COMMAND` (for example `ssh -o BatchMode=yes -o ConnectTimeout=5`) to fail fast. -- Refs pin the package and skip package updates (`pi update`, `pi update --extensions`). +- Refs are pinned tags or commits and skip package updates (`pi update`, `pi update --extensions`). Use `pi install git:host/user/repo@new-ref` to move an existing package to a new pinned ref. - Cloned to `~/.pi/agent/git//` (global) or `.pi/git//` (project). -- Runs `npm install` after clone or pull if `package.json` exists. +- Runs `npm install` after clone, pull, or pinned ref change if `package.json` exists. **SSH examples:** ```bash diff --git a/packages/coding-agent/src/core/package-manager.ts b/packages/coding-agent/src/core/package-manager.ts index 90c41942..b8c333bc 100644 --- a/packages/coding-agent/src/core/package-manager.ts +++ b/packages/coding-agent/src/core/package-manager.ts @@ -778,9 +778,21 @@ export class DefaultPackageManager implements PackageManager { scope === "project" ? this.settingsManager.getProjectSettings() : this.settingsManager.getGlobalSettings(); const currentPackages = currentSettings.packages ?? []; const normalizedSource = this.normalizePackageSourceForSettings(source, scope); - const exists = currentPackages.some((existing) => this.packageSourcesMatch(existing, source, scope)); - if (exists) { - return false; + const matchIndex = currentPackages.findIndex((existing) => this.packageSourcesMatch(existing, source, scope)); + if (matchIndex !== -1) { + const existing = currentPackages[matchIndex]; + if (this.getPackageSourceString(existing) === normalizedSource) { + return false; + } + const nextPackages = [...currentPackages]; + nextPackages[matchIndex] = + typeof existing === "string" ? normalizedSource : { ...existing, source: normalizedSource }; + if (scope === "project") { + this.settingsManager.setProjectPackages(nextPackages); + } else { + this.settingsManager.setPackages(nextPackages); + } + return true; } const nextPackages = [...currentPackages, normalizedSource]; if (scope === "project") { @@ -1723,6 +1735,12 @@ export class DefaultPackageManager implements PackageManager { private async installGit(source: GitSource, scope: SourceScope): Promise { const targetDir = this.getGitInstallPath(source, scope); if (existsSync(targetDir)) { + if (source.ref) { + await this.ensureGitRef(targetDir, ["fetch", "origin", source.ref], "FETCH_HEAD"); + return; + } + const target = await this.getLocalGitUpdateTarget(targetDir); + await this.ensureGitRef(targetDir, target.fetchArgs, target.ref); return; } const gitRoot = this.getGitInstallRoot(scope); @@ -1749,23 +1767,26 @@ export class DefaultPackageManager implements PackageManager { } const target = await this.getLocalGitUpdateTarget(targetDir); + await this.ensureGitRef(targetDir, target.fetchArgs, target.ref); + } + private async ensureGitRef(targetDir: string, fetchArgs: string[], ref: string): Promise { // Fetch only the ref we will reset to, avoiding unrelated branch/tag noise. - await this.runCommand("git", target.fetchArgs, { cwd: targetDir }); + await this.runCommand("git", fetchArgs, { cwd: targetDir }); const localHead = await this.runCommandCapture("git", ["rev-parse", "HEAD"], { cwd: targetDir, timeoutMs: NETWORK_TIMEOUT_MS, }); - const refreshedTargetHead = await this.runCommandCapture("git", ["rev-parse", target.ref], { + const targetHead = await this.runCommandCapture("git", ["rev-parse", ref], { cwd: targetDir, timeoutMs: NETWORK_TIMEOUT_MS, }); - if (localHead.trim() === refreshedTargetHead.trim()) { + if (localHead.trim() === targetHead.trim()) { return; } - await this.runCommand("git", ["reset", "--hard", target.ref], { cwd: targetDir }); + await this.runCommand("git", ["reset", "--hard", ref], { cwd: targetDir }); // Clean untracked files (extensions should be pristine) await this.runCommand("git", ["clean", "-fdx"], { cwd: targetDir }); diff --git a/packages/coding-agent/test/package-manager.test.ts b/packages/coding-agent/test/package-manager.test.ts index 9466d733..9f3476a8 100644 --- a/packages/coding-agent/test/package-manager.test.ts +++ b/packages/coding-agent/test/package-manager.test.ts @@ -25,6 +25,16 @@ class MockSpawnedProcess extends EventEmitter { } } +interface PackageManagerInternals { + runCommand(command: string, args: string[], options?: { cwd?: string }): Promise; + runCommandCapture( + command: string, + args: string[], + options?: { cwd?: string; timeoutMs?: number; env?: Record }, + ): Promise; + getLocalGitUpdateTarget(installedPath: string): Promise<{ ref: string; head: string; fetchArgs: string[] }>; +} + // Helper to check if a resource is enabled const isEnabled = (r: ResolvedResource, pathMatch: string, matchFn: "endsWith" | "includes" = "endsWith") => { const normalizedPath = normalizeForMatch(r.path); @@ -693,6 +703,62 @@ Content`, expect(runCommandSpy).toHaveBeenCalledWith("npm", ["install", "--omit=dev"], { cwd: targetDir }); }); + it("should reconcile an existing git checkout to a pinned ref during install", async () => { + const source = "git:github.com/user/repo@v2"; + const targetDir = join(agentDir, "git", "github.com", "user", "repo"); + mkdirSync(targetDir, { recursive: true }); + writeFileSync(join(targetDir, "package.json"), JSON.stringify({ name: "repo", version: "1.0.0" })); + + const managerWithInternals = packageManager as unknown as PackageManagerInternals; + vi.spyOn(managerWithInternals, "runCommandCapture").mockImplementation(async (_command, args) => { + if (args[0] === "rev-parse" && args[1] === "HEAD") { + return "old-head"; + } + if (args[0] === "rev-parse" && args[1] === "FETCH_HEAD") { + return "new-head"; + } + throw new Error(`Unexpected runCommandCapture args: ${args.join(" ")}`); + }); + const runCommandSpy = vi.spyOn(managerWithInternals, "runCommand").mockResolvedValue(undefined); + + await packageManager.install(source); + + expect(runCommandSpy).toHaveBeenCalledWith("git", ["fetch", "origin", "v2"], { cwd: targetDir }); + expect(runCommandSpy).toHaveBeenCalledWith("git", ["reset", "--hard", "FETCH_HEAD"], { cwd: targetDir }); + expect(runCommandSpy).toHaveBeenCalledWith("git", ["clean", "-fdx"], { cwd: targetDir }); + expect(runCommandSpy).toHaveBeenCalledWith("npm", ["install", "--omit=dev"], { cwd: targetDir }); + }); + + it("should reconcile an existing git checkout to its update target when installing without a ref", async () => { + const source = "git:github.com/user/repo"; + const targetDir = join(agentDir, "git", "github.com", "user", "repo"); + const fetchArgs = ["fetch", "--prune", "--no-tags", "origin", "+refs/heads/main:refs/remotes/origin/main"]; + mkdirSync(targetDir, { recursive: true }); + + const managerWithInternals = packageManager as unknown as PackageManagerInternals; + vi.spyOn(managerWithInternals, "getLocalGitUpdateTarget").mockResolvedValue({ + ref: "origin/HEAD", + head: "new-head", + fetchArgs, + }); + vi.spyOn(managerWithInternals, "runCommandCapture").mockImplementation(async (_command, args) => { + if (args[0] === "rev-parse" && args[1] === "HEAD") { + return "old-head"; + } + if (args[0] === "rev-parse" && args[1] === "origin/HEAD") { + return "new-head"; + } + throw new Error(`Unexpected runCommandCapture args: ${args.join(" ")}`); + }); + const runCommandSpy = vi.spyOn(managerWithInternals, "runCommand").mockResolvedValue(undefined); + + await packageManager.install(source); + + expect(runCommandSpy).toHaveBeenCalledWith("git", fetchArgs, { cwd: targetDir }); + expect(runCommandSpy).toHaveBeenCalledWith("git", ["reset", "--hard", "origin/HEAD"], { cwd: targetDir }); + expect(runCommandSpy).toHaveBeenCalledWith("git", ["clean", "-fdx"], { cwd: targetDir }); + }); + it("should use plain install for git package dependencies when npmCommand is configured", async () => { settingsManager = SettingsManager.inMemory({ npmCommand: ["pnpm"], @@ -1061,6 +1127,47 @@ Content`, expect(removed).toBe(true); expect(settingsManager.getGlobalSettings().packages ?? []).toHaveLength(0); }); + + it("should return false when adding the same git source with the same ref", () => { + const first = packageManager.addSourceToSettings("git:github.com/user/repo@v1"); + expect(first).toBe(true); + + const second = packageManager.addSourceToSettings("git:github.com/user/repo@v1"); + expect(second).toBe(false); + expect(settingsManager.getGlobalSettings().packages).toEqual(["git:github.com/user/repo@v1"]); + }); + + it("should update the ref when adding the same git source with a different ref", () => { + packageManager.addSourceToSettings("git:github.com/user/repo@v1"); + + const updated = packageManager.addSourceToSettings("git:github.com/user/repo@v2"); + expect(updated).toBe(true); + expect(settingsManager.getGlobalSettings().packages).toEqual(["git:github.com/user/repo@v2"]); + }); + + it("should preserve package filters when replacing a package source ref", () => { + settingsManager.setPackages([ + { + source: "git:github.com/user/repo@v1", + extensions: ["extensions/main.ts"], + skills: [], + prompts: ["prompts/review.md"], + themes: ["themes/dark.json"], + }, + ]); + + const updated = packageManager.addSourceToSettings("git:github.com/user/repo@v2"); + expect(updated).toBe(true); + expect(settingsManager.getGlobalSettings().packages).toEqual([ + { + source: "git:github.com/user/repo@v2", + extensions: ["extensions/main.ts"], + skills: [], + prompts: ["prompts/review.md"], + themes: ["themes/dark.json"], + }, + ]); + }); }); describe("HTTPS git URL parsing (old behavior)", () => { From c100620bf447349ae7a4866bc1cb6757cc9f67c4 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 22 May 2026 11:25:15 +0200 Subject: [PATCH 165/166] fix(coding-agent): Clean up Path Handling (#4873) --- packages/coding-agent/src/config.ts | 9 +-- .../src/core/agent-session-runtime.ts | 3 +- .../src/core/agent-session-services.ts | 5 +- .../coding-agent/src/core/agent-session.ts | 8 ++- .../coding-agent/src/core/auth-storage.ts | 3 +- .../src/core/export-html/index.ts | 14 ++-- .../src/core/extensions/loader.ts | 45 ++++--------- .../coding-agent/src/core/model-registry.ts | 3 +- .../coding-agent/src/core/package-manager.ts | 18 ++--- .../coding-agent/src/core/prompt-templates.ts | 25 ++----- .../coding-agent/src/core/resource-loader.ts | 67 +++++++++++-------- packages/coding-agent/src/core/sdk.ts | 5 +- .../coding-agent/src/core/session-manager.ts | 56 +++++++++------- .../coding-agent/src/core/settings-manager.ts | 19 ++---- packages/coding-agent/src/core/skills.ts | 29 +++----- .../coding-agent/src/core/tools/path-utils.ts | 26 +------ packages/coding-agent/src/main.ts | 11 ++- packages/coding-agent/src/utils/paths.ts | 57 ++++++++++++++-- .../test/extensions-discovery.test.ts | 25 +++++++ .../coding-agent/test/package-manager.test.ts | 34 ++++++++++ packages/coding-agent/test/path-utils.test.ts | 16 ++++- packages/coding-agent/test/paths.test.ts | 60 ++++++++++++++++- .../coding-agent/test/resource-loader.test.ts | 39 +++++++++++ 23 files changed, 363 insertions(+), 214 deletions(-) diff --git a/packages/coding-agent/src/config.ts b/packages/coding-agent/src/config.ts index e55fa5f3..dd091804 100644 --- a/packages/coding-agent/src/config.ts +++ b/packages/coding-agent/src/config.ts @@ -3,6 +3,7 @@ import { homedir } from "os"; import { basename, dirname, join, resolve, sep, win32 } from "path"; import { fileURLToPath } from "url"; import { spawnProcessSync } from "./utils/child-process.ts"; +import { normalizePath } from "./utils/paths.ts"; // ============================================================================= // Package Detection @@ -330,9 +331,7 @@ export function getPackageDir(): string { // Allow override via environment variable (useful for Nix/Guix where store paths tokenize poorly) const envDir = process.env.PI_PACKAGE_DIR; if (envDir) { - if (envDir === "~") return homedir(); - if (envDir.startsWith("~/")) return homedir() + envDir.slice(1); - return envDir; + return normalizePath(envDir); } if (isBunBinary) { @@ -454,9 +453,7 @@ export const ENV_AGENT_DIR = `${APP_NAME.toUpperCase()}_CODING_AGENT_DIR`; export const ENV_SESSION_DIR = `${APP_NAME.toUpperCase()}_CODING_AGENT_SESSION_DIR`; export function expandTildePath(path: string): string { - if (path === "~") return homedir(); - if (path.startsWith("~/")) return homedir() + path.slice(1); - return path; + return normalizePath(path); } const DEFAULT_SHARE_VIEWER_URL = "https://pi.dev/session/"; diff --git a/packages/coding-agent/src/core/agent-session-runtime.ts b/packages/coding-agent/src/core/agent-session-runtime.ts index b4756d4c..baab11b0 100644 --- a/packages/coding-agent/src/core/agent-session-runtime.ts +++ b/packages/coding-agent/src/core/agent-session-runtime.ts @@ -1,5 +1,6 @@ import { copyFileSync, existsSync, mkdirSync } from "node:fs"; import { basename, join, resolve } from "node:path"; +import { resolvePath } from "../utils/paths.ts"; import type { AgentSession } from "./agent-session.ts"; import type { AgentSessionRuntimeDiagnostic, AgentSessionServices } from "./agent-session-services.ts"; import type { ReplacedSessionContext, SessionShutdownEvent, SessionStartEvent } from "./extensions/index.ts"; @@ -337,7 +338,7 @@ export class AgentSessionRuntime { * @throws {MissingSessionCwdError} When the imported session cwd cannot be resolved and no override is provided. */ async importFromJsonl(inputPath: string, cwdOverride?: string): Promise<{ cancelled: boolean }> { - const resolvedPath = resolve(inputPath); + const resolvedPath = resolvePath(inputPath); if (!existsSync(resolvedPath)) { throw new SessionImportFileNotFoundError(resolvedPath); } diff --git a/packages/coding-agent/src/core/agent-session-services.ts b/packages/coding-agent/src/core/agent-session-services.ts index 852776a9..adf6f9e0 100644 --- a/packages/coding-agent/src/core/agent-session-services.ts +++ b/packages/coding-agent/src/core/agent-session-services.ts @@ -2,6 +2,7 @@ import { join } from "node:path"; import type { ThinkingLevel } from "@earendil-works/pi-agent-core"; import type { Model } from "@earendil-works/pi-ai"; import { getAgentDir } from "../config.ts"; +import { resolvePath } from "../utils/paths.ts"; import { AuthStorage } from "./auth-storage.ts"; import type { SessionStartEvent, ToolDefinition } from "./extensions/index.ts"; import { ModelRegistry } from "./model-registry.ts"; @@ -129,8 +130,8 @@ function applyExtensionFlagValues( export async function createAgentSessionServices( options: CreateAgentSessionServicesOptions, ): Promise { - const cwd = options.cwd; - const agentDir = options.agentDir ?? getAgentDir(); + const cwd = resolvePath(options.cwd); + const agentDir = options.agentDir ? resolvePath(options.agentDir) : getAgentDir(); const authStorage = options.authStorage ?? AuthStorage.create(join(agentDir, "auth.json")); const settingsManager = options.settingsManager ?? SettingsManager.create(cwd, agentDir); const modelRegistry = options.modelRegistry ?? ModelRegistry.create(authStorage, join(agentDir, "models.json")); diff --git a/packages/coding-agent/src/core/agent-session.ts b/packages/coding-agent/src/core/agent-session.ts index d18591b7..cf13594b 100644 --- a/packages/coding-agent/src/core/agent-session.ts +++ b/packages/coding-agent/src/core/agent-session.ts @@ -14,7 +14,7 @@ */ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; -import { basename, dirname, resolve } from "node:path"; +import { basename, dirname } from "node:path"; import type { Agent, AgentEvent, @@ -35,6 +35,7 @@ import { } from "@earendil-works/pi-ai"; import { theme } from "../modes/interactive/theme/theme.ts"; import { stripFrontmatter } from "../utils/frontmatter.ts"; +import { resolvePath } from "../utils/paths.ts"; import { sleep } from "../utils/sleep.ts"; import { formatNoApiKeyFoundMessage, formatNoModelSelectedMessage } from "./auth-guidance.ts"; import { type BashResult, executeBashWithOperations } from "./bash-executor.ts"; @@ -2993,7 +2994,10 @@ export class AgentSession { * @returns The resolved output file path. */ exportToJsonl(outputPath?: string): string { - const filePath = resolve(outputPath ?? `session-${new Date().toISOString().replace(/[:.]/g, "-")}.jsonl`); + const filePath = resolvePath( + outputPath ?? `session-${new Date().toISOString().replace(/[:.]/g, "-")}.jsonl`, + process.cwd(), + ); const dir = dirname(filePath); if (!existsSync(dir)) { mkdirSync(dir, { recursive: true }); diff --git a/packages/coding-agent/src/core/auth-storage.ts b/packages/coding-agent/src/core/auth-storage.ts index ae2996c4..7630fc80 100644 --- a/packages/coding-agent/src/core/auth-storage.ts +++ b/packages/coding-agent/src/core/auth-storage.ts @@ -18,6 +18,7 @@ import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "f import { dirname, join } from "path"; import lockfile from "proper-lockfile"; import { getAgentDir } from "../config.ts"; +import { normalizePath } from "../utils/paths.ts"; import { resolveConfigValue } from "./resolve-config-value.ts"; export type ApiKeyCredential = { @@ -53,7 +54,7 @@ export class FileAuthStorageBackend implements AuthStorageBackend { private authPath: string; constructor(authPath: string = join(getAgentDir(), "auth.json")) { - this.authPath = authPath; + this.authPath = normalizePath(authPath); } private ensureParentDir(): void { diff --git a/packages/coding-agent/src/core/export-html/index.ts b/packages/coding-agent/src/core/export-html/index.ts index a831ae42..3762c264 100644 --- a/packages/coding-agent/src/core/export-html/index.ts +++ b/packages/coding-agent/src/core/export-html/index.ts @@ -3,6 +3,7 @@ import { existsSync, readFileSync, writeFileSync } from "fs"; import { basename, join } from "path"; import { APP_NAME, getExportTemplateDir } from "../../config.ts"; import { getResolvedThemeColors, getThemeExportColors } from "../../modes/interactive/theme/theme.ts"; +import { normalizePath, resolvePath } from "../../utils/paths.ts"; import type { ToolDefinition } from "../extensions/types.ts"; import type { SessionEntry } from "../session-manager.ts"; import { SessionManager } from "../session-manager.ts"; @@ -270,7 +271,7 @@ export async function exportSessionToHtml( const html = generateHtml(sessionData, opts.themeName); - let outputPath = opts.outputPath; + let outputPath = opts.outputPath ? normalizePath(opts.outputPath) : undefined; if (!outputPath) { const sessionBasename = basename(sessionFile, ".jsonl"); outputPath = `${APP_NAME}-session-${sessionBasename}.html`; @@ -286,12 +287,13 @@ export async function exportSessionToHtml( */ export async function exportFromFile(inputPath: string, options?: ExportOptions | string): Promise { const opts: ExportOptions = typeof options === "string" ? { outputPath: options } : options || {}; + const resolvedInputPath = resolvePath(inputPath); - if (!existsSync(inputPath)) { - throw new Error(`File not found: ${inputPath}`); + if (!existsSync(resolvedInputPath)) { + throw new Error(`File not found: ${resolvedInputPath}`); } - const sm = SessionManager.open(inputPath); + const sm = SessionManager.open(resolvedInputPath); const sessionData: SessionData = { header: sm.getHeader(), @@ -303,9 +305,9 @@ export async function exportFromFile(inputPath: string, options?: ExportOptions const html = generateHtml(sessionData, opts.themeName); - let outputPath = opts.outputPath; + let outputPath = opts.outputPath ? normalizePath(opts.outputPath) : undefined; if (!outputPath) { - const inputBasename = basename(inputPath, ".jsonl"); + const inputBasename = basename(resolvedInputPath, ".jsonl"); outputPath = `${APP_NAME}-session-${inputBasename}.html`; } diff --git a/packages/coding-agent/src/core/extensions/loader.ts b/packages/coding-agent/src/core/extensions/loader.ts index 60ad625d..6a7d8abb 100644 --- a/packages/coding-agent/src/core/extensions/loader.ts +++ b/packages/coding-agent/src/core/extensions/loader.ts @@ -5,7 +5,6 @@ import * as fs from "node:fs"; import { createRequire } from "node:module"; -import * as os from "node:os"; import * as path from "node:path"; import { fileURLToPath } from "node:url"; import * as _bundledPiAgentCore from "@earendil-works/pi-agent-core"; @@ -24,6 +23,7 @@ import { CONFIG_DIR_NAME, getAgentDir, isBunBinary } from "../../config.ts"; // NOTE: This import works because loader.ts exports are NOT re-exported from index.ts, // avoiding a circular dependency. Extensions can import from @earendil-works/pi-coding-agent. import * as _bundledPiCodingAgent from "../../index.ts"; +import { resolvePath } from "../../utils/paths.ts"; import { createEventBus, type EventBus } from "../event-bus.ts"; import type { ExecOptions } from "../exec.ts"; import { execCommand } from "../exec.ts"; @@ -115,31 +115,6 @@ function getAliases(): Record { return _aliases; } -const UNICODE_SPACES = /[\u00A0\u2000-\u200A\u202F\u205F\u3000]/g; - -function normalizeUnicodeSpaces(str: string): string { - return str.replace(UNICODE_SPACES, " "); -} - -function expandPath(p: string): string { - const normalized = normalizeUnicodeSpaces(p); - if (normalized.startsWith("~/")) { - return path.join(os.homedir(), normalized.slice(2)); - } - if (normalized.startsWith("~")) { - return path.join(os.homedir(), normalized.slice(1)); - } - return normalized; -} - -function resolvePath(extPath: string, cwd: string): string { - const expanded = expandPath(extPath); - if (path.isAbsolute(expanded)) { - return expanded; - } - return path.resolve(cwd, expanded); -} - type HandlerFn = (...args: unknown[]) => Promise; /** @@ -396,7 +371,7 @@ async function loadExtension( eventBus: EventBus, runtime: ExtensionRuntime, ): Promise<{ extension: Extension | null; error: string | null }> { - const resolvedPath = resolvePath(extensionPath, cwd); + const resolvedPath = resolvePath(extensionPath, cwd, { normalizeUnicodeSpaces: true }); try { const factory = await loadExtensionModule(resolvedPath); @@ -426,7 +401,8 @@ export async function loadExtensionFromFactory( extensionPath = "", ): Promise { const extension = createExtension(extensionPath, extensionPath); - const api = createExtensionAPI(extension, runtime, cwd, eventBus); + const resolvedCwd = resolvePath(cwd); + const api = createExtensionAPI(extension, runtime, resolvedCwd, eventBus); await factory(api); return extension; } @@ -437,11 +413,12 @@ export async function loadExtensionFromFactory( export async function loadExtensions(paths: string[], cwd: string, eventBus?: EventBus): Promise { const extensions: Extension[] = []; const errors: Array<{ path: string; error: string }> = []; + const resolvedCwd = resolvePath(cwd); const resolvedEventBus = eventBus ?? createEventBus(); const runtime = createExtensionRuntime(); for (const extPath of paths) { - const { extension, error } = await loadExtension(extPath, cwd, resolvedEventBus, runtime); + const { extension, error } = await loadExtension(extPath, resolvedCwd, resolvedEventBus, runtime); if (error) { errors.push({ path: extPath, error }); @@ -578,6 +555,8 @@ export async function discoverAndLoadExtensions( agentDir: string = getAgentDir(), eventBus?: EventBus, ): Promise { + const resolvedCwd = resolvePath(cwd); + const resolvedAgentDir = resolvePath(agentDir); const allPaths: string[] = []; const seen = new Set(); @@ -592,16 +571,16 @@ export async function discoverAndLoadExtensions( }; // 1. Project-local extensions: cwd/${CONFIG_DIR_NAME}/extensions/ - const localExtDir = path.join(cwd, CONFIG_DIR_NAME, "extensions"); + const localExtDir = path.join(resolvedCwd, CONFIG_DIR_NAME, "extensions"); addPaths(discoverExtensionsInDir(localExtDir)); // 2. Global extensions: agentDir/extensions/ - const globalExtDir = path.join(agentDir, "extensions"); + const globalExtDir = path.join(resolvedAgentDir, "extensions"); addPaths(discoverExtensionsInDir(globalExtDir)); // 3. Explicitly configured paths for (const p of configuredPaths) { - const resolved = resolvePath(p, cwd); + const resolved = resolvePath(p, resolvedCwd, { normalizeUnicodeSpaces: true }); if (fs.existsSync(resolved) && fs.statSync(resolved).isDirectory()) { // Check for package.json with pi manifest or index.ts const entries = resolveExtensionEntries(resolved); @@ -617,5 +596,5 @@ export async function discoverAndLoadExtensions( addPaths([resolved]); } - return loadExtensions(allPaths, cwd, eventBus); + return loadExtensions(allPaths, resolvedCwd, eventBus); } diff --git a/packages/coding-agent/src/core/model-registry.ts b/packages/coding-agent/src/core/model-registry.ts index 4d040463..6e3a1031 100644 --- a/packages/coding-agent/src/core/model-registry.ts +++ b/packages/coding-agent/src/core/model-registry.ts @@ -25,6 +25,7 @@ import { type Static, Type } from "typebox"; import { Compile } from "typebox/compile"; import type { TLocalizedValidationError } from "typebox/error"; import { getAgentDir } from "../config.ts"; +import { normalizePath } from "../utils/paths.ts"; import type { AuthStatus, AuthStorage } from "./auth-storage.ts"; import { BUILT_IN_PROVIDER_DISPLAY_NAMES } from "./provider-display-names.ts"; import { @@ -339,7 +340,7 @@ export class ModelRegistry { private constructor(authStorage: AuthStorage, modelsJsonPath: string | undefined) { this.authStorage = authStorage; - this.modelsJsonPath = modelsJsonPath; + this.modelsJsonPath = modelsJsonPath ? normalizePath(modelsJsonPath) : undefined; this.loadModels(); } diff --git a/packages/coding-agent/src/core/package-manager.ts b/packages/coding-agent/src/core/package-manager.ts index b8c333bc..994adb56 100644 --- a/packages/coding-agent/src/core/package-manager.ts +++ b/packages/coding-agent/src/core/package-manager.ts @@ -30,7 +30,7 @@ import { minimatch } from "minimatch"; import { CONFIG_DIR_NAME } from "../config.ts"; import { spawnProcess, spawnProcessSync } from "../utils/child-process.ts"; import { type GitSource, parseGitUrl } from "../utils/git.ts"; -import { canonicalizePath, isLocalPath, markPathIgnoredByCloudSync } from "../utils/paths.ts"; +import { canonicalizePath, isLocalPath, markPathIgnoredByCloudSync, resolvePath } from "../utils/paths.ts"; import { isStdoutTakenOver } from "./output-guard.ts"; import type { PackageSource, SettingsManager } from "./settings-manager.ts"; @@ -763,8 +763,8 @@ export class DefaultPackageManager implements PackageManager { private progressCallback: ProgressCallback | undefined; constructor(options: PackageManagerOptions) { - this.cwd = options.cwd; - this.agentDir = options.agentDir; + this.cwd = resolvePath(options.cwd); + this.agentDir = resolvePath(options.agentDir); this.settingsManager = options.settingsManager; } @@ -1968,19 +1968,11 @@ export class DefaultPackageManager implements PackageManager { } private resolvePath(input: string): string { - const trimmed = input.trim(); - if (trimmed === "~") return getHomeDir(); - if (trimmed.startsWith("~/")) return join(getHomeDir(), trimmed.slice(2)); - if (trimmed.startsWith("~")) return join(getHomeDir(), trimmed.slice(1)); - return resolve(this.cwd, trimmed); + return resolvePath(input, this.cwd, { homeDir: getHomeDir(), trim: true }); } private resolvePathFromBase(input: string, baseDir: string): string { - const trimmed = input.trim(); - if (trimmed === "~") return getHomeDir(); - if (trimmed.startsWith("~/")) return join(getHomeDir(), trimmed.slice(2)); - if (trimmed.startsWith("~")) return join(getHomeDir(), trimmed.slice(1)); - return resolve(baseDir, trimmed); + return resolvePath(input, baseDir, { homeDir: getHomeDir(), trim: true }); } private collectPackageResources( diff --git a/packages/coding-agent/src/core/prompt-templates.ts b/packages/coding-agent/src/core/prompt-templates.ts index b646896d..581a34eb 100644 --- a/packages/coding-agent/src/core/prompt-templates.ts +++ b/packages/coding-agent/src/core/prompt-templates.ts @@ -1,8 +1,8 @@ import { existsSync, readdirSync, readFileSync, statSync } from "fs"; -import { homedir } from "os"; -import { basename, dirname, isAbsolute, join, resolve, sep } from "path"; +import { basename, dirname, join, resolve, sep } from "path"; import { CONFIG_DIR_NAME } from "../config.ts"; import { parseFrontmatter } from "../utils/frontmatter.ts"; +import { resolvePath } from "../utils/paths.ts"; import { createSyntheticSourceInfo, type SourceInfo } from "./source-info.ts"; /** @@ -185,19 +185,6 @@ export interface LoadPromptTemplatesOptions { includeDefaults: boolean; } -function normalizePath(input: string): string { - const trimmed = input.trim(); - if (trimmed === "~") return homedir(); - if (trimmed.startsWith("~/")) return join(homedir(), trimmed.slice(2)); - if (trimmed.startsWith("~")) return join(homedir(), trimmed.slice(1)); - return trimmed; -} - -function resolvePromptPath(p: string, cwd: string): string { - const normalized = normalizePath(p); - return isAbsolute(normalized) ? normalized : resolve(cwd, normalized); -} - /** * Load all prompt templates from: * 1. Global: agentDir/prompts/ @@ -205,14 +192,14 @@ function resolvePromptPath(p: string, cwd: string): string { * 3. Explicit prompt paths */ export function loadPromptTemplates(options: LoadPromptTemplatesOptions): PromptTemplate[] { - const resolvedCwd = options.cwd; - const resolvedAgentDir = options.agentDir; + const resolvedCwd = resolvePath(options.cwd); + const resolvedAgentDir = resolvePath(options.agentDir); const promptPaths = options.promptPaths; const includeDefaults = options.includeDefaults; const templates: PromptTemplate[] = []; - const globalPromptsDir = options.agentDir ? join(options.agentDir, "prompts") : resolvedAgentDir; + const globalPromptsDir = join(resolvedAgentDir, "prompts"); const projectPromptsDir = resolve(resolvedCwd, CONFIG_DIR_NAME, "prompts"); const isUnderPath = (target: string, root: string): boolean => { @@ -252,7 +239,7 @@ export function loadPromptTemplates(options: LoadPromptTemplatesOptions): Prompt // 3. Load explicit prompt paths for (const rawPath of promptPaths) { - const resolvedPath = resolvePromptPath(rawPath, resolvedCwd); + const resolvedPath = resolvePath(rawPath, resolvedCwd, { trim: true }); if (!existsSync(resolvedPath)) { continue; } diff --git a/packages/coding-agent/src/core/resource-loader.ts b/packages/coding-agent/src/core/resource-loader.ts index 57d90f97..eb043cd5 100644 --- a/packages/coding-agent/src/core/resource-loader.ts +++ b/packages/coding-agent/src/core/resource-loader.ts @@ -1,5 +1,4 @@ import { existsSync, readdirSync, readFileSync, statSync } from "node:fs"; -import { homedir } from "node:os"; import { join, resolve, sep } from "node:path"; import chalk from "chalk"; import { CONFIG_DIR_NAME } from "../config.ts"; @@ -8,7 +7,7 @@ import type { ResourceDiagnostic } from "./diagnostics.ts"; export type { ResourceCollision, ResourceDiagnostic } from "./diagnostics.ts"; -import { canonicalizePath, isLocalPath } from "../utils/paths.ts"; +import { canonicalizePath, isLocalPath, resolvePath } from "../utils/paths.ts"; import { createEventBus, type EventBus } from "./event-bus.ts"; import { createExtensionRuntime, loadExtensionFromFactory, loadExtensions } from "./extensions/loader.ts"; import type { Extension, ExtensionFactory, ExtensionRuntime, LoadExtensionsResult } from "./extensions/types.ts"; @@ -77,8 +76,8 @@ export function loadProjectContextFiles(options: { cwd: string; agentDir: string; }): Array<{ path: string; content: string }> { - const resolvedCwd = options.cwd; - const resolvedAgentDir = options.agentDir; + const resolvedCwd = resolvePath(options.cwd); + const resolvedAgentDir = resolvePath(options.agentDir); const contextFiles: Array<{ path: string; content: string }> = []; const seenPaths = new Set(); @@ -205,8 +204,8 @@ export class DefaultResourceLoader implements ResourceLoader { private lastThemePaths: string[]; constructor(options: DefaultResourceLoaderOptions) { - this.cwd = options.cwd; - this.agentDir = options.agentDir; + this.cwd = resolvePath(options.cwd); + this.agentDir = resolvePath(options.agentDir); this.settingsManager = options.settingsManager ?? SettingsManager.create(this.cwd, this.agentDir); this.eventBus = options.eventBus ?? createEventBus(); this.packageManager = new DefaultPackageManager({ @@ -409,8 +408,11 @@ export class DefaultResourceLoader implements ResourceLoader { } for (const p of this.additionalExtensionPaths) { - if (isLocalPath(p) && !existsSync(p)) { - extensionsResult.errors.push({ path: p, error: `Extension path does not exist: ${p}` }); + if (isLocalPath(p)) { + const resolved = this.resolveResourcePath(p); + if (!existsSync(resolved)) { + extensionsResult.errors.push({ path: resolved, error: `Extension path does not exist: ${resolved}` }); + } } } this.extensionsResult = this.extensionsOverride ? this.extensionsOverride(extensionsResult) : extensionsResult; @@ -423,8 +425,11 @@ export class DefaultResourceLoader implements ResourceLoader { this.lastSkillPaths = skillPaths; this.updateSkillsFromPaths(skillPaths, metadataByPath); for (const p of this.additionalSkillPaths) { - if (isLocalPath(p) && !existsSync(p) && !this.skillDiagnostics.some((d) => d.path === p)) { - this.skillDiagnostics.push({ type: "error", message: "Skill path does not exist", path: p }); + if (isLocalPath(p)) { + const resolved = this.resolveResourcePath(p); + if (!existsSync(resolved) && !this.skillDiagnostics.some((d) => d.path === resolved)) { + this.skillDiagnostics.push({ type: "error", message: "Skill path does not exist", path: resolved }); + } } } @@ -435,8 +440,15 @@ export class DefaultResourceLoader implements ResourceLoader { this.lastPromptPaths = promptPaths; this.updatePromptsFromPaths(promptPaths, metadataByPath); for (const p of this.additionalPromptTemplatePaths) { - if (isLocalPath(p) && !existsSync(p) && !this.promptDiagnostics.some((d) => d.path === p)) { - this.promptDiagnostics.push({ type: "error", message: "Prompt template path does not exist", path: p }); + if (isLocalPath(p)) { + const resolved = this.resolveResourcePath(p); + if (!existsSync(resolved) && !this.promptDiagnostics.some((d) => d.path === resolved)) { + this.promptDiagnostics.push({ + type: "error", + message: "Prompt template path does not exist", + path: resolved, + }); + } } } @@ -447,8 +459,9 @@ export class DefaultResourceLoader implements ResourceLoader { this.lastThemePaths = themePaths; this.updateThemesFromPaths(themePaths, metadataByPath); for (const p of this.additionalThemePaths) { - if (!existsSync(p) && !this.themeDiagnostics.some((d) => d.path === p)) { - this.themeDiagnostics.push({ type: "error", message: "Theme path does not exist", path: p }); + const resolved = this.resolveResourcePath(p); + if (!existsSync(resolved) && !this.themeDiagnostics.some((d) => d.path === resolved)) { + this.themeDiagnostics.push({ type: "error", message: "Theme path does not exist", path: resolved }); } } @@ -478,10 +491,15 @@ export class DefaultResourceLoader implements ResourceLoader { private normalizeExtensionPaths( entries: Array<{ path: string; metadata: PathMetadata }>, ): Array<{ path: string; metadata: PathMetadata }> { - return entries.map((entry) => ({ - path: this.resolveResourcePath(entry.path), - metadata: entry.metadata, - })); + return entries.map((entry) => { + const metadata = entry.metadata.baseDir + ? { ...entry.metadata, baseDir: this.resolveResourcePath(entry.metadata.baseDir) } + : entry.metadata; + return { + path: this.resolveResourcePath(entry.path), + metadata, + }; + }); } private updateSkillsFromPaths(skillPaths: string[], metadataByPath?: Map): void { @@ -674,16 +692,7 @@ export class DefaultResourceLoader implements ResourceLoader { } private resolveResourcePath(p: string): string { - const trimmed = p.trim(); - let expanded = trimmed; - if (trimmed === "~") { - expanded = homedir(); - } else if (trimmed.startsWith("~/")) { - expanded = join(homedir(), trimmed.slice(2)); - } else if (trimmed.startsWith("~")) { - expanded = join(homedir(), trimmed.slice(1)); - } - return resolve(this.cwd, expanded); + return resolvePath(p, this.cwd, { trim: true }); } private loadThemes( @@ -704,7 +713,7 @@ export class DefaultResourceLoader implements ResourceLoader { } for (const p of paths) { - const resolved = resolve(this.cwd, p); + const resolved = this.resolveResourcePath(p); if (!existsSync(resolved)) { diagnostics.push({ type: "warning", message: "theme path does not exist", path: resolved }); continue; diff --git a/packages/coding-agent/src/core/sdk.ts b/packages/coding-agent/src/core/sdk.ts index 7caa0fa1..f19581e4 100644 --- a/packages/coding-agent/src/core/sdk.ts +++ b/packages/coding-agent/src/core/sdk.ts @@ -2,6 +2,7 @@ import { join } from "node:path"; import { Agent, type AgentMessage, type ThinkingLevel } from "@earendil-works/pi-agent-core"; import { clampThinkingLevel, type Message, type Model, streamSimple } from "@earendil-works/pi-ai"; import { getAgentDir } from "../config.ts"; +import { resolvePath } from "../utils/paths.ts"; import { AgentSession } from "./agent-session.ts"; import { formatNoModelsAvailableMessage } from "./auth-guidance.ts"; import { AuthStorage } from "./auth-storage.ts"; @@ -191,8 +192,8 @@ function getAttributionHeaders( * ``` */ export async function createAgentSession(options: CreateAgentSessionOptions = {}): Promise { - const cwd = options.cwd ?? options.sessionManager?.getCwd() ?? process.cwd(); - const agentDir = options.agentDir ?? getDefaultAgentDir(); + const cwd = resolvePath(options.cwd ?? options.sessionManager?.getCwd() ?? process.cwd()); + const agentDir = options.agentDir ? resolvePath(options.agentDir) : getDefaultAgentDir(); let resourceLoader = options.resourceLoader; // Use provided or create AuthStorage and ModelRegistry diff --git a/packages/coding-agent/src/core/session-manager.ts b/packages/coding-agent/src/core/session-manager.ts index 533916f3..e7d12314 100644 --- a/packages/coding-agent/src/core/session-manager.ts +++ b/packages/coding-agent/src/core/session-manager.ts @@ -16,6 +16,7 @@ import { import { readdir, readFile, stat } from "fs/promises"; import { join, resolve } from "path"; import { getAgentDir as getDefaultAgentDir, getSessionsDir } from "../config.ts"; +import { normalizePath, resolvePath } from "../utils/paths.ts"; import { type BashExecutionMessage, type CustomMessage, @@ -425,8 +426,10 @@ export function buildSessionContext( * Encodes cwd into a safe directory name under ~/.pi/agent/sessions/. */ export function getDefaultSessionDir(cwd: string, agentDir: string = getDefaultAgentDir()): string { - const safePath = `--${cwd.replace(/^[/\\]/, "").replace(/[/\\:]/g, "-")}--`; - const sessionDir = join(agentDir, "sessions", safePath); + const resolvedCwd = resolvePath(cwd); + const resolvedAgentDir = resolvePath(agentDir); + const safePath = `--${resolvedCwd.replace(/^[/\\]/, "").replace(/[/\\:]/g, "-")}--`; + const sessionDir = join(resolvedAgentDir, "sessions", safePath); if (!existsSync(sessionDir)) { mkdirSync(sessionDir, { recursive: true }); } @@ -435,9 +438,10 @@ export function getDefaultSessionDir(cwd: string, agentDir: string = getDefaultA /** Exported for testing */ export function loadEntriesFromFile(filePath: string): FileEntry[] { - if (!existsSync(filePath)) return []; + const resolvedFilePath = normalizePath(filePath); + if (!existsSync(resolvedFilePath)) return []; - const content = readFileSync(filePath, "utf8"); + const content = readFileSync(resolvedFilePath, "utf8"); const entries: FileEntry[] = []; const lines = content.trim().split("\n"); @@ -478,10 +482,11 @@ function isValidSessionFile(filePath: string): boolean { /** Exported for testing */ export function findMostRecentSession(sessionDir: string): string | null { + const resolvedSessionDir = normalizePath(sessionDir); try { - const files = readdirSync(sessionDir) + const files = readdirSync(resolvedSessionDir) .filter((f) => f.endsWith(".jsonl")) - .map((f) => join(sessionDir, f)) + .map((f) => join(resolvedSessionDir, f)) .filter(isValidSessionFile) .map((path) => ({ path, mtime: statSync(path).mtime })) .sort((a, b) => b.mtime.getTime() - a.mtime.getTime()); @@ -717,11 +722,11 @@ export class SessionManager { private leafId: string | null = null; private constructor(cwd: string, sessionDir: string, sessionFile: string | undefined, persist: boolean) { - this.cwd = cwd; - this.sessionDir = sessionDir; + this.cwd = resolvePath(cwd); + this.sessionDir = normalizePath(sessionDir); this.persist = persist; - if (persist && sessionDir && !existsSync(sessionDir)) { - mkdirSync(sessionDir, { recursive: true }); + if (persist && this.sessionDir && !existsSync(this.sessionDir)) { + mkdirSync(this.sessionDir, { recursive: true }); } if (sessionFile) { @@ -733,7 +738,7 @@ export class SessionManager { /** Switch to a different session file (used for resume and branching) */ setSessionFile(sessionFile: string): void { - this.sessionFile = resolve(sessionFile); + this.sessionFile = resolvePath(sessionFile); if (existsSync(this.sessionFile)) { this.fileEntries = loadEntriesFromFile(this.sessionFile); @@ -1304,7 +1309,7 @@ export class SessionManager { * @param sessionDir Optional session directory. If omitted, uses default (~/.pi/agent/sessions//). */ static create(cwd: string, sessionDir?: string): SessionManager { - const dir = sessionDir ?? getDefaultSessionDir(cwd); + const dir = sessionDir ? normalizePath(sessionDir) : getDefaultSessionDir(cwd); return new SessionManager(cwd, dir, undefined, true); } @@ -1315,13 +1320,14 @@ export class SessionManager { * @param cwdOverride Optional cwd override instead of the session header cwd. */ static open(path: string, sessionDir?: string, cwdOverride?: string): SessionManager { + const resolvedPath = resolvePath(path); // Extract cwd from session header if possible, otherwise use process.cwd() - const entries = loadEntriesFromFile(path); + const entries = loadEntriesFromFile(resolvedPath); const header = entries.find((e) => e.type === "session") as SessionHeader | undefined; const cwd = cwdOverride ?? header?.cwd ?? process.cwd(); // If no sessionDir provided, derive from file's parent directory - const dir = sessionDir ?? resolve(path, ".."); - return new SessionManager(cwd, dir, path, true); + const dir = sessionDir ? normalizePath(sessionDir) : resolve(resolvedPath, ".."); + return new SessionManager(cwd, dir, resolvedPath, true); } /** @@ -1330,7 +1336,7 @@ export class SessionManager { * @param sessionDir Optional session directory. If omitted, uses default (~/.pi/agent/sessions//). */ static continueRecent(cwd: string, sessionDir?: string): SessionManager { - const dir = sessionDir ?? getDefaultSessionDir(cwd); + const dir = sessionDir ? normalizePath(sessionDir) : getDefaultSessionDir(cwd); const mostRecent = findMostRecentSession(dir); if (mostRecent) { return new SessionManager(cwd, dir, mostRecent, true); @@ -1351,17 +1357,19 @@ export class SessionManager { * @param sessionDir Optional session directory. If omitted, uses default for targetCwd. */ static forkFrom(sourcePath: string, targetCwd: string, sessionDir?: string): SessionManager { - const sourceEntries = loadEntriesFromFile(sourcePath); + const resolvedSourcePath = resolvePath(sourcePath); + const resolvedTargetCwd = resolvePath(targetCwd); + const sourceEntries = loadEntriesFromFile(resolvedSourcePath); if (sourceEntries.length === 0) { - throw new Error(`Cannot fork: source session file is empty or invalid: ${sourcePath}`); + throw new Error(`Cannot fork: source session file is empty or invalid: ${resolvedSourcePath}`); } const sourceHeader = sourceEntries.find((e) => e.type === "session") as SessionHeader | undefined; if (!sourceHeader) { - throw new Error(`Cannot fork: source session has no header: ${sourcePath}`); + throw new Error(`Cannot fork: source session has no header: ${resolvedSourcePath}`); } - const dir = sessionDir ?? getDefaultSessionDir(targetCwd); + const dir = sessionDir ? normalizePath(sessionDir) : getDefaultSessionDir(resolvedTargetCwd); if (!existsSync(dir)) { mkdirSync(dir, { recursive: true }); } @@ -1378,8 +1386,8 @@ export class SessionManager { version: CURRENT_SESSION_VERSION, id: newSessionId, timestamp, - cwd: targetCwd, - parentSession: sourcePath, + cwd: resolvedTargetCwd, + parentSession: resolvedSourcePath, }; appendFileSync(newSessionFile, `${JSON.stringify(newHeader)}\n`); @@ -1390,7 +1398,7 @@ export class SessionManager { } } - return new SessionManager(targetCwd, dir, newSessionFile, true); + return new SessionManager(resolvedTargetCwd, dir, newSessionFile, true); } /** @@ -1400,7 +1408,7 @@ export class SessionManager { * @param onProgress Optional callback for progress updates (loaded, total) */ static async list(cwd: string, sessionDir?: string, onProgress?: SessionListProgress): Promise { - const dir = sessionDir ?? getDefaultSessionDir(cwd); + const dir = sessionDir ? normalizePath(sessionDir) : getDefaultSessionDir(cwd); const sessions = await listSessionsFromDir(dir, onProgress); sessions.sort((a, b) => b.modified.getTime() - a.modified.getTime()); return sessions; diff --git a/packages/coding-agent/src/core/settings-manager.ts b/packages/coding-agent/src/core/settings-manager.ts index 2fedad35..40f97cd3 100644 --- a/packages/coding-agent/src/core/settings-manager.ts +++ b/packages/coding-agent/src/core/settings-manager.ts @@ -1,9 +1,9 @@ import type { Transport } from "@earendil-works/pi-ai"; import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs"; -import { homedir } from "os"; import { dirname, join } from "path"; import lockfile from "proper-lockfile"; import { CONFIG_DIR_NAME, getAgentDir } from "../config.ts"; +import { normalizePath, resolvePath } from "../utils/paths.ts"; import { DEFAULT_HTTP_IDLE_TIMEOUT_MS, parseHttpIdleTimeoutMs } from "./http-dispatcher.ts"; export interface CompactionSettings { @@ -161,8 +161,10 @@ export class FileSettingsStorage implements SettingsStorage { private projectSettingsPath: string; constructor(cwd: string, agentDir: string) { - this.globalSettingsPath = join(agentDir, "settings.json"); - this.projectSettingsPath = join(cwd, CONFIG_DIR_NAME, "settings.json"); + const resolvedCwd = resolvePath(cwd); + const resolvedAgentDir = resolvePath(agentDir); + this.globalSettingsPath = join(resolvedAgentDir, "settings.json"); + this.projectSettingsPath = join(resolvedCwd, CONFIG_DIR_NAME, "settings.json"); } private acquireLockSyncWithRetry(path: string): () => void { @@ -577,16 +579,7 @@ export class SettingsManager { getSessionDir(): string | undefined { const sessionDir = this.settings.sessionDir; - if (!sessionDir) { - return sessionDir; - } - if (sessionDir === "~") { - return homedir(); - } - if (sessionDir.startsWith("~/")) { - return join(homedir(), sessionDir.slice(2)); - } - return sessionDir; + return sessionDir ? normalizePath(sessionDir) : sessionDir; } getDefaultProvider(): string | undefined { diff --git a/packages/coding-agent/src/core/skills.ts b/packages/coding-agent/src/core/skills.ts index 675da3a7..c104c856 100644 --- a/packages/coding-agent/src/core/skills.ts +++ b/packages/coding-agent/src/core/skills.ts @@ -1,10 +1,9 @@ import { existsSync, readdirSync, readFileSync, statSync } from "fs"; import ignore from "ignore"; -import { homedir } from "os"; -import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "path"; +import { basename, dirname, join, relative, resolve, sep } from "path"; import { CONFIG_DIR_NAME, getAgentDir } from "../config.ts"; import { parseFrontmatter } from "../utils/frontmatter.ts"; -import { canonicalizePath } from "../utils/paths.ts"; +import { canonicalizePath, resolvePath } from "../utils/paths.ts"; import type { ResourceDiagnostic } from "./diagnostics.ts"; import { createSyntheticSourceInfo, type SourceInfo } from "./source-info.ts"; @@ -381,28 +380,16 @@ export interface LoadSkillsOptions { includeDefaults: boolean; } -function normalizePath(input: string): string { - const trimmed = input.trim(); - if (trimmed === "~") return homedir(); - if (trimmed.startsWith("~/")) return join(homedir(), trimmed.slice(2)); - if (trimmed.startsWith("~")) return join(homedir(), trimmed.slice(1)); - return trimmed; -} - -function resolveSkillPath(p: string, cwd: string): string { - const normalized = normalizePath(p); - return isAbsolute(normalized) ? normalized : resolve(cwd, normalized); -} - /** * Load skills from all configured locations. * Returns skills and any validation diagnostics. */ export function loadSkills(options: LoadSkillsOptions): LoadSkillsResult { - const { cwd, agentDir, skillPaths, includeDefaults } = options; + const { agentDir, skillPaths, includeDefaults } = options; // Resolve agentDir - if not provided, use default from config - const resolvedAgentDir = agentDir ?? getAgentDir(); + const resolvedCwd = resolvePath(options.cwd); + const resolvedAgentDir = resolvePath(agentDir ?? getAgentDir()); const skillMap = new Map(); const realPathSet = new Set(); @@ -442,11 +429,11 @@ export function loadSkills(options: LoadSkillsOptions): LoadSkillsResult { if (includeDefaults) { addSkills(loadSkillsFromDirInternal(join(resolvedAgentDir, "skills"), "user", true)); - addSkills(loadSkillsFromDirInternal(resolve(cwd, CONFIG_DIR_NAME, "skills"), "project", true)); + addSkills(loadSkillsFromDirInternal(resolve(resolvedCwd, CONFIG_DIR_NAME, "skills"), "project", true)); } const userSkillsDir = join(resolvedAgentDir, "skills"); - const projectSkillsDir = resolve(cwd, CONFIG_DIR_NAME, "skills"); + const projectSkillsDir = resolve(resolvedCwd, CONFIG_DIR_NAME, "skills"); const isUnderPath = (target: string, root: string): boolean => { const normalizedRoot = resolve(root); @@ -466,7 +453,7 @@ export function loadSkills(options: LoadSkillsOptions): LoadSkillsResult { }; for (const rawPath of skillPaths) { - const resolvedPath = resolveSkillPath(rawPath, cwd); + const resolvedPath = resolvePath(rawPath, resolvedCwd, { trim: true }); if (!existsSync(resolvedPath)) { allDiagnostics.push({ type: "warning", message: "skill path does not exist", path: resolvedPath }); continue; diff --git a/packages/coding-agent/src/core/tools/path-utils.ts b/packages/coding-agent/src/core/tools/path-utils.ts index 0c10b52e..1ed1e2bc 100644 --- a/packages/coding-agent/src/core/tools/path-utils.ts +++ b/packages/coding-agent/src/core/tools/path-utils.ts @@ -1,12 +1,7 @@ import { accessSync, constants } from "node:fs"; -import * as os from "node:os"; -import { isAbsolute, resolve as resolvePath } from "node:path"; +import { normalizePath, resolvePath } from "../../utils/paths.ts"; -const UNICODE_SPACES = /[\u00A0\u2000-\u200A\u202F\u205F\u3000]/g; const NARROW_NO_BREAK_SPACE = "\u202F"; -function normalizeUnicodeSpaces(str: string): string { - return str.replace(UNICODE_SPACES, " "); -} function tryMacOSScreenshotPath(filePath: string): string { return filePath.replace(/ (AM|PM)\./gi, `${NARROW_NO_BREAK_SPACE}$1.`); @@ -32,19 +27,8 @@ function fileExists(filePath: string): boolean { } } -function normalizeAtPrefix(filePath: string): string { - return filePath.startsWith("@") ? filePath.slice(1) : filePath; -} - export function expandPath(filePath: string): string { - const normalized = normalizeUnicodeSpaces(normalizeAtPrefix(filePath)); - if (normalized === "~") { - return os.homedir(); - } - if (normalized.startsWith("~/")) { - return os.homedir() + normalized.slice(1); - } - return normalized; + return normalizePath(filePath, { normalizeUnicodeSpaces: true, stripAtPrefix: true }); } /** @@ -52,11 +36,7 @@ export function expandPath(filePath: string): string { * Handles ~ expansion and absolute paths. */ export function resolveToCwd(filePath: string, cwd: string): string { - const expanded = expandPath(filePath); - if (isAbsolute(expanded)) { - return expanded; - } - return resolvePath(cwd, expanded); + return resolvePath(filePath, cwd, { normalizeUnicodeSpaces: true, stripAtPrefix: true }); } export function resolveReadPath(filePath: string, cwd: string): string { diff --git a/packages/coding-agent/src/main.ts b/packages/coding-agent/src/main.ts index 47f53c3d..5395e045 100644 --- a/packages/coding-agent/src/main.ts +++ b/packages/coding-agent/src/main.ts @@ -5,7 +5,6 @@ * createAgentSession() options. The SDK does the heavy lifting. */ -import { resolve } from "node:path"; import { createInterface } from "node:readline"; import { type ImageContent, modelsAreEqual } from "@earendil-works/pi-ai"; import { ProcessTerminal, setKeybindings, TUI } from "@earendil-works/pi-tui"; @@ -46,7 +45,7 @@ import { InteractiveMode, runPrintMode, runRpcMode } from "./modes/index.ts"; import { ExtensionSelectorComponent } from "./modes/interactive/components/extension-selector.ts"; import { initTheme, stopThemeWatcher } from "./modes/interactive/theme/theme.ts"; import { handleConfigCommand, handlePackageCommand } from "./package-manager-cli.ts"; -import { isLocalPath } from "./utils/paths.ts"; +import { isLocalPath, normalizePath, resolvePath } from "./utils/paths.ts"; import { cleanupWindowsSelfUpdateQuarantine } from "./utils/windows-self-update.ts"; /** @@ -147,9 +146,9 @@ type ResolvedSession = * If it looks like a path, use as-is. Otherwise try to match as session ID prefix. */ async function resolveSessionPath(sessionArg: string, cwd: string, sessionDir?: string): Promise { - // If it looks like a file path, use as-is + // If it looks like a file path, resolve it before handing it to the session manager. if (sessionArg.includes("/") || sessionArg.includes("\\") || sessionArg.endsWith(".jsonl")) { - return { type: "path", path: sessionArg }; + return { type: "path", path: resolvePath(sessionArg, cwd) }; } // Try to match as session ID in current project first @@ -381,7 +380,7 @@ function buildSessionOptions( } function resolveCliPaths(cwd: string, paths: string[] | undefined): string[] | undefined { - return paths?.map((value) => (isLocalPath(value) ? resolve(cwd, value) : value)); + return paths?.map((value) => (isLocalPath(value) ? resolvePath(value, cwd) : value)); } async function promptForMissingSessionCwd( @@ -501,7 +500,7 @@ export async function main(args: string[], options?: MainOptions) { // sessionDir lookup during session selection. const envSessionDir = process.env[ENV_SESSION_DIR]; const sessionDir = - parsed.sessionDir ?? + (parsed.sessionDir ? normalizePath(parsed.sessionDir) : undefined) ?? (envSessionDir ? expandTildePath(envSessionDir) : undefined) ?? startupSettingsManager.getSessionDir(); let sessionManager = await createSessionManager(parsed, cwd, sessionDir, startupSettingsManager); diff --git a/packages/coding-agent/src/utils/paths.ts b/packages/coding-agent/src/utils/paths.ts index fd8d176e..7f101344 100644 --- a/packages/coding-agent/src/utils/paths.ts +++ b/packages/coding-agent/src/utils/paths.ts @@ -1,7 +1,24 @@ import { realpathSync } from "node:fs"; -import { isAbsolute, relative, resolve as resolvePath, sep } from "node:path"; +import { homedir } from "node:os"; +import { isAbsolute, join, resolve as nodeResolvePath, relative, sep } from "node:path"; +import { fileURLToPath } from "node:url"; import { spawnProcessSync } from "./child-process.ts"; +const UNICODE_SPACES = /[\u00A0\u2000-\u200A\u202F\u205F\u3000]/g; + +export interface PathInputOptions { + /** Trim leading/trailing whitespace before normalization. */ + trim?: boolean; + /** Expand leading `~` to a home directory. Defaults to true. */ + expandTilde?: boolean; + /** Home directory used for `~` expansion. Defaults to `os.homedir()`. */ + homeDir?: string; + /** Strip a leading `@`, used for CLI @file paths. */ + stripAtPrefix?: boolean; + /** Normalize unicode space variants to regular spaces. */ + normalizeUnicodeSpaces?: boolean; +} + /** * Resolve a path to its canonical (real) form, following symlinks. * Falls back to the raw path if resolution fails (e.g. the target does @@ -18,12 +35,12 @@ export function canonicalizePath(path: string): string { /** * Returns true if the value is NOT a package source (npm:, git:, etc.) - * or a URL protocol. Bare names and relative paths without ./ prefix + * or a remote URL protocol. Bare names, relative paths, and file: URLs * are considered local. */ export function isLocalPath(value: string): boolean { const trimmed = value.trim(); - // Known non-local prefixes + // Known non-local prefixes. file: URLs are local paths and are intentionally resolved by resolvePath(). if ( trimmed.startsWith("npm:") || trimmed.startsWith("git:") || @@ -37,13 +54,39 @@ export function isLocalPath(value: string): boolean { return true; } -function resolveAgainstCwd(filePath: string, cwd: string): string { - return isAbsolute(filePath) ? resolvePath(filePath) : resolvePath(cwd, filePath); +export function normalizePath(input: string, options: PathInputOptions = {}): string { + let normalized = options.trim ? input.trim() : input; + if (options.normalizeUnicodeSpaces) { + normalized = normalized.replace(UNICODE_SPACES, " "); + } + if (options.stripAtPrefix && normalized.startsWith("@")) { + normalized = normalized.slice(1); + } + + if (options.expandTilde ?? true) { + const home = options.homeDir ?? homedir(); + if (normalized === "~") return home; + if (normalized.startsWith("~/") || (process.platform === "win32" && normalized.startsWith("~\\"))) { + return join(home, normalized.slice(2)); + } + } + + if (/^file:\/\//.test(normalized)) { + return fileURLToPath(normalized); + } + + return normalized; +} + +export function resolvePath(input: string, baseDir: string = process.cwd(), options: PathInputOptions = {}): string { + const normalized = normalizePath(input, options); + const normalizedBaseDir = normalizePath(baseDir); + return isAbsolute(normalized) ? nodeResolvePath(normalized) : nodeResolvePath(normalizedBaseDir, normalized); } export function getCwdRelativePath(filePath: string, cwd: string): string | undefined { const resolvedCwd = resolvePath(cwd); - const resolvedPath = resolveAgainstCwd(filePath, resolvedCwd); + const resolvedPath = resolvePath(filePath, resolvedCwd); const relativePath = relative(resolvedCwd, resolvedPath); const isInsideCwd = relativePath === "" || @@ -53,7 +96,7 @@ export function getCwdRelativePath(filePath: string, cwd: string): string | unde } export function formatPathRelativeToCwdOrAbsolute(filePath: string, cwd: string): string { - const absolutePath = resolveAgainstCwd(filePath, cwd); + const absolutePath = resolvePath(filePath, cwd); return (getCwdRelativePath(absolutePath, cwd) ?? absolutePath).split(sep).join("/"); } diff --git a/packages/coding-agent/test/extensions-discovery.test.ts b/packages/coding-agent/test/extensions-discovery.test.ts index 3613e902..b1b1232f 100644 --- a/packages/coding-agent/test/extensions-discovery.test.ts +++ b/packages/coding-agent/test/extensions-discovery.test.ts @@ -123,6 +123,31 @@ describe("extensions discovery", () => { expect(result.extensions[0].path).toContain("main.ts"); }); + it("keeps package.json pi extension entries with leading tilde package-relative", async () => { + const subdir = path.join(extensionsDir, "tilde-package"); + const directExtensionPath = path.join(subdir, "~entry.ts"); + const slashExtensionPath = path.join(subdir, "~", "entry.ts"); + fs.mkdirSync(path.join(subdir, "~"), { recursive: true }); + fs.writeFileSync(directExtensionPath, extensionCode); + fs.writeFileSync(slashExtensionPath, extensionCode); + fs.writeFileSync( + path.join(subdir, "package.json"), + JSON.stringify({ + name: "tilde-package", + pi: { + extensions: ["~entry.ts", "~/entry.ts"], + }, + }), + ); + + const result = await discoverAndLoadExtensions([], tempDir, tempDir); + + expect(result.errors).toHaveLength(0); + expect(result.extensions.map((extension) => extension.path).sort()).toEqual( + [directExtensionPath, slashExtensionPath].sort(), + ); + }); + it("package.json can declare multiple extensions", async () => { const subdir = path.join(extensionsDir, "my-package"); fs.mkdirSync(subdir); diff --git a/packages/coding-agent/test/package-manager.test.ts b/packages/coding-agent/test/package-manager.test.ts index 9f3476a8..30068da1 100644 --- a/packages/coding-agent/test/package-manager.test.ts +++ b/packages/coding-agent/test/package-manager.test.ts @@ -584,6 +584,40 @@ Content`, ); }); + it("should keep pi manifest entries with leading tilde package-relative", async () => { + const pkgDir = join(tempDir, "tilde-manifest-package"); + const directExtensionPath = join(pkgDir, "~extensions", "main.ts"); + const slashExtensionPath = join(pkgDir, "~", "extensions", "alt.ts"); + const directSkillPath = join(pkgDir, "~skills", "direct-skill", "SKILL.md"); + const slashSkillPath = join(pkgDir, "~", "skills", "slash-skill", "SKILL.md"); + + mkdirSync(join(pkgDir, "~extensions"), { recursive: true }); + mkdirSync(join(pkgDir, "~", "extensions"), { recursive: true }); + mkdirSync(join(pkgDir, "~skills", "direct-skill"), { recursive: true }); + mkdirSync(join(pkgDir, "~", "skills", "slash-skill"), { recursive: true }); + writeFileSync(directExtensionPath, "export default function() {}"); + writeFileSync(slashExtensionPath, "export default function() {}"); + writeFileSync(directSkillPath, "---\nname: direct-skill\ndescription: Direct\n---\nContent"); + writeFileSync(slashSkillPath, "---\nname: slash-skill\ndescription: Slash\n---\nContent"); + writeFileSync( + join(pkgDir, "package.json"), + JSON.stringify({ + name: "tilde-manifest-package", + pi: { + extensions: ["~extensions/main.ts", "~/extensions/alt.ts"], + skills: ["~skills", "~/skills"], + }, + }), + ); + + const result = await packageManager.resolveExtensionSources([pkgDir]); + + expect(result.extensions.some((r) => r.path === directExtensionPath && r.enabled)).toBe(true); + expect(result.extensions.some((r) => r.path === slashExtensionPath && r.enabled)).toBe(true); + expect(result.skills.some((r) => r.path === directSkillPath && r.enabled)).toBe(true); + expect(result.skills.some((r) => r.path === slashSkillPath && r.enabled)).toBe(true); + }); + it("should handle directories with auto-discovery layout", async () => { const pkgDir = join(tempDir, "auto-pkg"); mkdirSync(join(pkgDir, "extensions"), { recursive: true }); diff --git a/packages/coding-agent/test/path-utils.test.ts b/packages/coding-agent/test/path-utils.test.ts index 85c2605f..77a8345f 100644 --- a/packages/coding-agent/test/path-utils.test.ts +++ b/packages/coding-agent/test/path-utils.test.ts @@ -16,6 +16,11 @@ describe("path-utils", () => { expect(result).not.toContain("~/"); }); + it("should keep tilde-prefixed filenames literal", () => { + expect(expandPath("~draft.md")).toBe("~draft.md"); + expect(expandPath("@~draft.md")).toBe("~draft.md"); + }); + it("should normalize Unicode spaces", () => { // Non-breaking space (U+00A0) should become regular space const withNBSP = "file\u00A0name.txt"; @@ -26,14 +31,21 @@ describe("path-utils", () => { describe("resolveToCwd", () => { it("should resolve absolute paths as-is", () => { - const result = resolveToCwd("/absolute/path/file.txt", "/some/cwd"); - expect(result).toBe("/absolute/path/file.txt"); + const absolutePath = resolve(tmpdir(), "absolute", "path", "file.txt"); + const result = resolveToCwd(absolutePath, resolve(tmpdir(), "some", "cwd")); + expect(result).toBe(absolutePath); }); it("should resolve relative paths against cwd", () => { const result = resolveToCwd("relative/file.txt", "/some/cwd"); expect(result).toBe(resolve("/some/cwd", "relative/file.txt")); }); + + it("should resolve tilde-prefixed filenames against cwd", () => { + const cwd = join(tmpdir(), "pi-path-utils-cwd"); + expect(resolveToCwd("~draft.md", cwd)).toBe(resolve(cwd, "~draft.md")); + expect(resolveToCwd("@~draft.md", cwd)).toBe(resolve(cwd, "~draft.md")); + }); }); describe("resolveReadPath", () => { diff --git a/packages/coding-agent/test/paths.test.ts b/packages/coding-agent/test/paths.test.ts index 01151635..accf9989 100644 --- a/packages/coding-agent/test/paths.test.ts +++ b/packages/coding-agent/test/paths.test.ts @@ -1,8 +1,9 @@ import { mkdirSync, mkdtempSync, realpathSync, rmSync, symlinkSync, writeFileSync } from "node:fs"; -import { tmpdir } from "node:os"; -import { join } from "node:path"; +import { homedir, tmpdir } from "node:os"; +import { join, resolve } from "node:path"; +import { pathToFileURL } from "node:url"; import { afterEach, describe, expect, it } from "vitest"; -import { canonicalizePath, getCwdRelativePath, isLocalPath } from "../src/utils/paths.ts"; +import { canonicalizePath, getCwdRelativePath, isLocalPath, normalizePath, resolvePath } from "../src/utils/paths.ts"; let tempDir: string; @@ -73,6 +74,55 @@ describe("getCwdRelativePath", () => { }); }); +describe("resolvePath", () => { + it("expands only home tilde shortcuts", () => { + const cwd = join(tmpdir(), "pi-paths-cwd"); + expect(normalizePath("~")).toBe(homedir()); + expect(normalizePath("~/file.txt")).toBe(join(homedir(), "file.txt")); + expect(resolvePath("~draft.md", cwd)).toBe(resolve(cwd, "~draft.md")); + expect(normalizePath("~draft.md")).toBe("~draft.md"); + }); + + it("resolves relative paths against the base directory", () => { + const cwd = join(tmpdir(), "pi-paths-cwd"); + expect(resolvePath("subdir/file.txt", cwd)).toBe(resolve(cwd, "subdir/file.txt")); + expect(resolvePath("subdir/file.txt", pathToFileURL(cwd).href)).toBe(resolve(cwd, "subdir/file.txt")); + }); + + it("accepts file URLs", () => { + const dir = createTempDir(); + const filePath = join(dir, "file with spaces.txt"); + expect(resolvePath(pathToFileURL(filePath).href, join(dir, "base"))).toBe(resolve(filePath)); + }); + + it("throws for invalid file URLs", () => { + expect(() => resolvePath("file:///%E0%A4%A")).toThrow(); + }); + + it("preserves POSIX absolute paths with literal percent sequences", () => { + if (process.platform === "win32") { + return; + } + + const dir = createTempDir(); + for (const filePath of [join(dir, "report%2026.md"), join(dir, "foo%2Fbar"), join(dir, "malformed%A.md")]) { + expect(resolvePath(filePath, join(dir, "base"))).toBe(resolve(filePath)); + } + }); + + it("does not treat Windows file URL pathname strings as native paths", () => { + if (process.platform !== "win32") { + return; + } + + const dir = createTempDir(); + const filePath = join(dir, "dir", "SKILL.md"); + const pathname = pathToFileURL(filePath).pathname; + expect(pathname).toMatch(/^\/[A-Za-z]:/); + expect(resolvePath(pathname, "E:\\project")).toBe(resolve(pathname)); + }); +}); + describe("isLocalPath", () => { it("returns true for bare names", () => { expect(isLocalPath("my-package")).toBe(true); @@ -82,6 +132,10 @@ describe("isLocalPath", () => { expect(isLocalPath("./foo")).toBe(true); }); + it("returns true for file URLs", () => { + expect(isLocalPath("file:///tmp/foo")).toBe(true); + }); + it("returns false for npm: protocol", () => { expect(isLocalPath("npm:package")).toBe(false); }); diff --git a/packages/coding-agent/test/resource-loader.test.ts b/packages/coding-agent/test/resource-loader.test.ts index f023855a..693f05c9 100644 --- a/packages/coding-agent/test/resource-loader.test.ts +++ b/packages/coding-agent/test/resource-loader.test.ts @@ -1,6 +1,7 @@ import { mkdirSync, readFileSync, rmSync, symlinkSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; +import { pathToFileURL } from "node:url"; import { afterEach, beforeEach, describe, expect, it } from "vitest"; import { AuthStorage } from "../src/core/auth-storage.ts"; import { ExtensionRunner } from "../src/core/extensions/runner.ts"; @@ -405,6 +406,44 @@ Extra prompt content`, expect(loadedPrompt?.sourceInfo?.source).toBe("extension:extra"); expect(loadedPrompt?.sourceInfo?.path).toBe(promptPath); }); + + it("should load extension resources returned as file URLs", async () => { + const extraSkillDir = join(tempDir, "extra skills", "file-url-skill"); + mkdirSync(extraSkillDir, { recursive: true }); + const skillPath = join(extraSkillDir, "SKILL.md"); + writeFileSync( + skillPath, + `--- +name: file-url-skill +description: File URL skill +--- +Extra content`, + ); + + const loader = new DefaultResourceLoader({ cwd, agentDir }); + await loader.reload(); + + loader.extendResources({ + skillPaths: [ + { + path: pathToFileURL(extraSkillDir).href, + metadata: { + source: "extension:file-url", + scope: "temporary", + origin: "top-level", + baseDir: extraSkillDir, + }, + }, + ], + }); + + const { skills, diagnostics } = loader.getSkills(); + expect(diagnostics).toEqual([]); + const loadedSkill = skills.find((skill) => skill.name === "file-url-skill"); + expect(loadedSkill).toBeDefined(); + expect(loadedSkill?.filePath).toBe(skillPath); + expect(loadedSkill?.sourceInfo?.source).toBe("extension:file-url"); + }); }); describe("noSkills option", () => { From 7426ce977d2bbbf7d289d4a897a5e351ce8dfce7 Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Fri, 22 May 2026 12:04:01 +0200 Subject: [PATCH 166/166] Tighten AGENTS.md and extract LLM provider checklist to skill - Tighten all sections (279 -> 159 lines) without dropping rules. - Reorganize: git rules moved next to issues/PRs. - Defer contributor gate details to CONTRIBUTING.md. - Replace stale npx tsx test command with node (strip-only mode); add ./test.sh + e2e warning. - Releasing: explicit WebAuthn briefing step, no bash timeout, stop on partial publish failure. - Move LLM provider checklist to .pi/skills/add-llm-provider.md. --- .pi/skills/add-llm-provider.md | 57 +++++++ AGENTS.md | 304 ++++++++++----------------------- 2 files changed, 149 insertions(+), 212 deletions(-) create mode 100644 .pi/skills/add-llm-provider.md diff --git a/.pi/skills/add-llm-provider.md b/.pi/skills/add-llm-provider.md new file mode 100644 index 00000000..97cd9a3e --- /dev/null +++ b/.pi/skills/add-llm-provider.md @@ -0,0 +1,57 @@ +--- +name: add-llm-provider +description: Checklist for adding a new LLM provider to packages/ai. Covers core types, provider implementation, lazy registration, model generation, the full test matrix, coding-agent wiring, and docs. +--- + +# Adding a New LLM Provider (packages/ai) + +A new provider touches multiple files. Work through these steps in order. + +## 1. Core Types (`packages/ai/src/types.ts`) + +- Add API identifier to `Api` type union (e.g. `"bedrock-converse-stream"`). +- Create options interface extending `StreamOptions`. +- Add mapping to `ApiOptionsMap`. +- Add provider name to `KnownProvider` type union. + +## 2. Provider Implementation (`packages/ai/src/providers/`) + +Create a provider file exporting: + +- `stream()` returning `AssistantMessageEventStream`. +- `streamSimple()` for `SimpleStreamOptions` mapping. +- Provider-specific options interface. +- Message/tool conversion functions. +- Response parsing that emits standardized events (`text`, `tool_call`, `thinking`, `usage`, `stop`). + +## 3. Provider Exports and Lazy Registration + +- Add a package subpath export in `packages/ai/package.json` pointing at `./dist/providers/.js`. +- Add `export type` re-exports in `packages/ai/src/index.ts` for provider option types that should remain available from the root entry. +- Register the provider in `packages/ai/src/providers/register-builtins.ts` via lazy loader wrappers; do not statically import provider implementation modules there. +- Add credential detection in `packages/ai/src/env-api-keys.ts`. + +## 4. Model Generation (`packages/ai/scripts/generate-models.ts`) + +- Add logic to fetch/parse models from the provider source. +- Map to the standardized `Model` interface. + +## 5. Tests (`packages/ai/test/`) + +- Always add the provider to `stream.test.ts` with at least one representative model, even if it reuses an existing API impl such as `openai-completions`. +- Add the provider to the broader matrix where applicable: `tokens.test.ts`, `abort.test.ts`, `empty.test.ts`, `context-overflow.test.ts`, `unicode-surrogate.test.ts`, `tool-call-without-result.test.ts`, `image-tool-result.test.ts`, `total-tokens.test.ts`, `cross-provider-handoff.test.ts`. +- For `cross-provider-handoff.test.ts`, add at least one provider/model pair. If the provider exposes multiple model families (e.g. GPT and Claude), add at least one pair per family. +- For non-standard auth, create a utility (e.g. `bedrock-utils.ts`) with credential detection. + +## 6. Coding Agent (`packages/coding-agent/`) + +- `src/core/model-resolver.ts`: add default model ID to `defaultModelPerProvider`. +- `src/core/provider-display-names.ts`: add API-key login display name so `/login` and related UI show the provider for built-in API-key auth. +- `src/cli/args.ts`: add env var documentation. +- `README.md`: add provider setup instructions. +- `docs/providers.md`: add setup instructions, env var, and `auth.json` key. + +## 7. Documentation + +- `packages/ai/README.md`: add to providers table, document options/auth, add env vars. +- `packages/ai/CHANGELOG.md`: add entry under `## [Unreleased]`. diff --git a/AGENTS.md b/AGENTS.md index 10f28ef5..144d2c60 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -4,202 +4,118 @@ - Keep answers short and concise - No emojis in commits, issues, PR comments, or code -- No fluff or cheerful filler text -- Technical prose only, be kind but direct (e.g., "Thanks @user" not "Thanks so much @user!") +- No fluff or cheerful filler text (e.g., "Thanks @user" not "Thanks so much @user!") +- Technical prose only, be direct - When the user asks a question, answer it first before making edits or running implementation commands. ## Code Quality -- Read files in full before making wide-ranging changes, before editing files you have not already fully inspected, and when the user asks you to investigate or audit something. Do not rely only on search snippets for broad changes. -- No `any` types unless absolutely necessary -- Single-line helper functions with a single call site are forbidden; inline them instead. -- Check node_modules for external API type definitions instead of guessing -- **NEVER use inline imports** - no `await import("./foo.js")`, no `import("pkg").Type` in type positions, no dynamic imports for types. Always use standard top-level imports. -- NEVER remove or downgrade code to fix type errors from outdated dependencies; upgrade the dependency instead -- Use only erasable TypeScript syntax compatible with Node strip-only mode in TypeScript checked by the root config (`packages/*/src`, `packages/*/test`, and `packages/coding-agent/examples`). Do not use constructor parameter properties, `enum`, `namespace`/`module`, `import =`, `export =`, or other TypeScript constructs that require JavaScript emit. Use explicit fields and constructor assignments instead of parameter properties. -- Always ask before removing functionality or code that appears to be intentional -- Do not preserve backward compatibility unless the user explicitly asks for it -- Never hardcode key checks with, eg. `matchesKey(keyData, "ctrl+x")`. All keybindings must be configurable. Add default to matching object (`DEFAULT_EDITOR_KEYBINDINGS` or `DEFAULT_APP_KEYBINDINGS`) -- NEVER modify `packages/ai/src/models.generated.ts` directly. Update `packages/ai/scripts/generate-models.ts` instead. +- Read files in full before wide-ranging changes, before editing files you have not fully inspected, and when asked to investigate or audit. Do not rely on search snippets for broad changes. +- No `any` unless absolutely necessary. +- Inline single-line helpers that have only one call site. +- Check node_modules for external API types; don't guess. +- **No inline imports** (`await import()`, `import("pkg").Type`, dynamic type imports). Top-level imports only. +- Never remove or downgrade code to fix type errors from outdated deps; upgrade the dep instead. +- Use only erasable TypeScript syntax (Node strip-only mode) in code checked by the root config (`packages/*/src`, `packages/*/test`, `packages/coding-agent/examples`): no parameter properties, `enum`, `namespace`/`module`, `import =`, `export =`, or other constructs needing JS emit. Use explicit fields with constructor assignments. +- Always ask before removing functionality or code that appears intentional. +- Do not preserve backward compatibility unless the user asks for it. +- Never hardcode key checks (e.g. `matchesKey(keyData, "ctrl+x")`). Add defaults to `DEFAULT_EDITOR_KEYBINDINGS` or `DEFAULT_APP_KEYBINDINGS` so they stay configurable. +- Never modify `packages/ai/src/models.generated.ts` directly; update `packages/ai/scripts/generate-models.ts` instead. ## Commands -- After code changes (not documentation changes): `npm run check` (get full output, no tail). Fix all errors, warnings, and infos before committing. -- Note: `npm run check` does not run tests. -- NEVER run: `npm run build`, `npm test` -- Only run specific tests if user instructs: `npx tsx ../../node_modules/vitest/dist/cli.js --run test/specific.test.ts` -- Run tests from the package root, not the repo root. -- If you create or modify a test file, you MUST run that test file and iterate until it passes. -- When writing tests, run them, identify issues in either the test or implementation, and iterate until fixed. -- For `packages/coding-agent/test/suite/`, use `test/suite/harness.ts` plus the faux provider. Do not use real provider APIs, real API keys, or paid tokens. -- Put issue-specific regressions under `packages/coding-agent/test/suite/regressions/` and name them `-.test.ts`. -- For ad-hoc scripts, write the script to a temporary file (for example under `/tmp`) using `write`, run that file, edit it if needed, and remove it when it is no longer needed. Do not embed multi-line scripts directly in `bash` commands. -- NEVER commit unless user asks +- After code changes (not docs): `npm run check` (full output, no tail). Fix all errors, warnings, and infos before committing. Does not run tests. +- Never run `npm run build` or `npm test` unless requested by the user. +- Never run the full vitest suite directly: it includes e2e tests that activate when endpoint/auth env vars are present. For all non-e2e tests, run `./test.sh` from the repo root. Otherwise run specific tests from the package root: `node ../../node_modules/vitest/dist/cli.js --run test/specific.test.ts`. +- If you create or modify a test file, run it and iterate on test or implementation until it passes. +- For `packages/coding-agent/test/suite/`, use `test/suite/harness.ts` + the faux provider. No real provider APIs, keys, or paid tokens. +- Put issue-specific regressions under `packages/coding-agent/test/suite/regressions/` named `-.test.ts`. +- For ad-hoc scripts, `write` them to a temp file (e.g. `/tmp`), run, edit if needed, remove when done. Don't embed multi-line scripts in `bash` commands. +- Never commit unless the user asks. ## Dependency and Install Security -- Treat npm dependency and lockfile changes as reviewed code changes. Direct external dependencies must stay pinned to exact versions. -- Use `npm install --ignore-scripts` to hydrate/update `node_modules` locally. Use `npm ci --ignore-scripts` for clean installs/CI-style verification. Do not run lifecycle scripts unless the user explicitly asks. -- If dependency metadata changes, run `npm install --package-lock-only --ignore-scripts` to update `package-lock.json` without installing or running scripts. -- If `packages/coding-agent/npm-shrinkwrap.json` needs regeneration, run `node scripts/generate-coding-agent-shrinkwrap.mjs`; verify with `node scripts/generate-coding-agent-shrinkwrap.mjs --check` or `npm run check`. -- Pre-commit blocks accidental lockfile commits unless `PI_ALLOW_LOCKFILE_CHANGE=1` is set. Do not bypass it unless the user explicitly wants the lockfile change committed. -- New dependencies with lifecycle scripts require review and an explicit allowlist entry in `scripts/generate-coding-agent-shrinkwrap.mjs`; do not add one silently. +- Treat npm dep and lockfile changes as reviewed code. Direct external deps stay pinned to exact versions. +- Hydrate/update locally with `npm install --ignore-scripts`; clean/CI-style with `npm ci --ignore-scripts`. Don't run lifecycle scripts unless the user asks. +- If dep metadata changes, refresh `package-lock.json` with `npm install --package-lock-only --ignore-scripts`. +- If `packages/coding-agent/npm-shrinkwrap.json` needs regen, run `node scripts/generate-coding-agent-shrinkwrap.mjs` (verify with `--check` or `npm run check`). New deps with lifecycle scripts require review and an explicit allowlist entry in that script; never add one silently. +- Pre-commit blocks lockfile commits unless `PI_ALLOW_LOCKFILE_CHANGE=1`. Don't bypass unless the user wants the lockfile change committed. -## Contribution Gate +## Git -- New issues from new contributors are auto-closed by `.github/workflows/issue-gate.yml` -- New PRs from new contributors without PR rights are auto-closed by `.github/workflows/pr-gate.yml` -- Maintainer approval comments are handled by `.github/workflows/approve-contributor.yml` -- Maintainers review auto-closed issues daily -- Issues that do not meet the quality bar in `CONTRIBUTING.md` are not reopened and do not receive a reply -- `lgtmi` approves future issues -- `lgtm` approves future issues and rights to submit PRs +Multiple pi sessions may be running in this cwd at the same time, each modifying different files. Git operations that touch unstaged, staged, or untracked files outside your own changes will stomp on other sessions' work. Follow these rules: + +Committing: + +- Only commit files YOU changed in THIS session. +- Stage explicit paths (`git add `); never `git add -A` / `git add .`. +- Before committing, run `git status` and verify you are only staging your files. +- `packages/ai/src/models.generated.ts` may always be included alongside your files. + +Never run (destroys other agents' work or bypasses checks): + +- `git reset --hard`, `git checkout .`, `git clean -fd`, `git stash`, `git add -A`, `git add .`, `git commit --no-verify`. + +If rebase conflicts occur: + +- Resolve conflicts only in files you modified. +- If a conflict is in a file you did not modify, abort and ask the user. +- Never force push. + +## Issues and PRs + +See `CONTRIBUTING.md` for the contributor gate (auto-close workflows, `lgtm`/`lgtmi`, quality bar). When creating issues: -- Add `pkg:*` labels to indicate which package(s) the issue affects - - Available labels: `pkg:agent`, `pkg:ai`, `pkg:coding-agent`, `pkg:tui` -- If an issue spans multiple packages, add all relevant labels +- Add `pkg:*` labels for affected packages (`pkg:agent`, `pkg:ai`, `pkg:coding-agent`, `pkg:tui`); use all that apply. When posting issue/PR comments: -- Write the full comment to a temp file and use `gh issue comment --body-file` or `gh pr comment --body-file` -- Never pass multi-line markdown directly via `--body` in shell commands -- Preview the exact comment text before posting -- Post exactly one final comment unless the user explicitly asks for multiple comments -- If a comment is malformed, delete it immediately, then post one corrected comment -- Keep comments concise, technical, and in the user's tone +- Write the comment to a temp file and post with `gh issue/pr comment --body-file` (never multi-line markdown via `--body`). +- Keep comments concise, technical, in the user's tone. +- End every AI-posted comment with the AI-generated disclaimer line specified by the originating prompt (e.g. `This comment is AI-generated by `/wr``). When closing issues via commit: -- Include `fixes #` or `closes #` in the commit message -- This automatically closes the issue when the commit is merged - -## PR Workflow - -- Analyze PRs without pulling locally first -- If the user approves: create a feature branch, pull PR, rebase on main, apply adjustments, commit, merge into main, push, close PR, and leave a comment in the user's tone -- You never open PRs yourself. We work in feature branches until everything is according to the user's requirements, then merge into main, and push. +- Include `fixes #` or `closes #` in the message so merging auto-closes the issue. For multiple issues, repeat the keyword per issue (`closes #1, closes #2`); a shared keyword (`closes #1, #2`) only closes the first. ## Testing pi Interactive Mode with tmux -To test pi's TUI in a controlled terminal environment: +Run the TUI in a controlled terminal (from the repo root): ```bash -# Create tmux session with specific dimensions tmux new-session -d -s pi-test -x 80 -y 24 - -# Start pi from source -tmux send-keys -t pi-test "cd /Users/badlogic/workspaces/pi-mono && ./pi-test.sh" Enter - -# Wait for startup, then capture output -sleep 3 && tmux capture-pane -t pi-test -p - -# Send input +tmux send-keys -t pi-test "./pi-test.sh" Enter +sleep 3 && tmux capture-pane -t pi-test -p # capture after startup tmux send-keys -t pi-test "your prompt here" Enter - -# Send special keys -tmux send-keys -t pi-test Escape -tmux send-keys -t pi-test C-o # ctrl+o - -# Cleanup +tmux send-keys -t pi-test Escape # special keys (also C-o for ctrl+o, etc.) tmux kill-session -t pi-test ``` ## Changelog -Location: `packages/*/CHANGELOG.md` (each package has its own) +Location: `packages/*/CHANGELOG.md` (one per package). -### Format +Sections under `## [Unreleased]`: `### Breaking Changes` (API changes requiring migration), `### Added`, `### Changed`, `### Fixed`, `### Removed`. -Use these sections under `## [Unreleased]`: +Rules: -- `### Breaking Changes` - API changes requiring migration -- `### Added` - New features -- `### Changed` - Changes to existing functionality -- `### Fixed` - Bug fixes -- `### Removed` - Removed features +- All new entries go under `## [Unreleased]`. Read the full section first and append to existing subsections; never duplicate them. +- Released version sections (e.g. `## [0.12.2]`) are immutable; never modify them. -### Rules +Attribution: -- Before adding entries, read the full `[Unreleased]` section to see which subsections already exist -- New entries ALWAYS go under `## [Unreleased]` section -- Append to existing subsections (e.g., `### Fixed`), do not create duplicates -- NEVER modify already-released version sections (e.g., `## [0.12.2]`) -- Each version section is immutable once released - -### Attribution - -- **Internal changes (from issues)**: `Fixed foo bar ([#123](https://github.com/earendil-works/pi-mono/issues/123))` -- **External contributions**: `Added feature X ([#456](https://github.com/earendil-works/pi-mono/pull/456) by [@username](https://github.com/username))` - -## Adding a New LLM Provider (packages/ai) - -Adding a new provider requires changes across multiple files: - -### 1. Core Types (`packages/ai/src/types.ts`) - -- Add API identifier to `Api` type union (e.g., `"bedrock-converse-stream"`) -- Create options interface extending `StreamOptions` -- Add mapping to `ApiOptionsMap` -- Add provider name to `KnownProvider` type union - -### 2. Provider Implementation (`packages/ai/src/providers/`) - -Create provider file exporting: - -- `stream()` function returning `AssistantMessageEventStream` -- `streamSimple()` for `SimpleStreamOptions` mapping -- Provider-specific options interface -- Message/tool conversion functions -- Response parsing emitting standardized events (`text`, `tool_call`, `thinking`, `usage`, `stop`) - -### 3. Provider Exports and Lazy Registration - -- Add a package subpath export in `packages/ai/package.json` pointing at `./dist/providers/.js` -- Add `export type` re-exports in `packages/ai/src/index.ts` for provider option types that should remain available from the root entry -- Register the provider in `packages/ai/src/providers/register-builtins.ts` via lazy loader wrappers, do not statically import provider implementation modules there -- Add credential detection in `packages/ai/src/env-api-keys.ts` - -### 4. Model Generation (`packages/ai/scripts/generate-models.ts`) - -- Add logic to fetch/parse models from provider source -- Map to standardized `Model` interface - -### 5. Tests (`packages/ai/test/`) - -- Always add the provider to `stream.test.ts` with at least one representative model, even if it reuses an existing API implementation such as `openai-completions`. -- Add the provider to the broader provider matrix where applicable: `tokens.test.ts`, `abort.test.ts`, `empty.test.ts`, `context-overflow.test.ts`, `unicode-surrogate.test.ts`, `tool-call-without-result.test.ts`, `image-tool-result.test.ts`, `total-tokens.test.ts`, `cross-provider-handoff.test.ts`. -- For `cross-provider-handoff.test.ts`, add at least one provider/model pair. If the provider exposes multiple model families (for example GPT and Claude), add at least one pair per family. -- For non-standard auth, create utility (e.g., `bedrock-utils.ts`) with credential detection. - -### 6. Coding Agent (`packages/coding-agent/`) - -- `src/core/model-resolver.ts`: Add default model ID to `defaultModelPerProvider` -- `src/core/provider-display-names.ts`: Add API-key login display name so `/login` and related UI show the provider for built-in API-key auth. -- `src/cli/args.ts`: Add env var documentation -- `README.md`: Add provider setup instructions -- `docs/providers.md`: Add setup instructions, env var, and `auth.json` key - -### 7. Documentation - -- `packages/ai/README.md`: Add to providers table, document options/auth, add env vars -- `packages/ai/CHANGELOG.md`: Add entry under `## [Unreleased]` +- Internal (from issues): `Fixed foo bar ([#123](https://github.com/earendil-works/pi-mono/issues/123))` +- External contributions: `Added feature X ([#456](https://github.com/earendil-works/pi-mono/pull/456) by [@username](https://github.com/username))` ## Releasing -**Lockstep versioning**: All packages always share the same version number. Every release updates all packages together. +**Lockstep versioning**: all packages share one version; every release updates all together. `patch` = fixes + additions, `minor` = breaking changes. No major releases. -**Version semantics** (no major releases): +1. **Update CHANGELOGs**: ask the user whether they ran the `/cl` prompt on the latest commit on `main`. If not, they must run `/cl` first to audit and update each package's `[Unreleased]` section before releasing. -- `patch`: Bug fixes and new features -- `minor`: API breaking changes - -### Steps - -1. **Update CHANGELOGs**: Ensure all changes since last release are documented in the `[Unreleased]` section of each affected package's CHANGELOG.md - -2. **Soft gate: local release smoke test**: Before running the real release script, build an unpublished local release and manually smoke test it from outside the repository so it cannot accidentally resolve workspace files: +2. **Local smoke test**: build an unpublished release and smoke test from outside the repo (so it can't resolve workspace files): ```bash npm run release:local -- --out /tmp/pi-local-release --force cd /tmp @@ -209,71 +125,35 @@ Create provider file exporting: /tmp/pi-local-release/bun/pi --help /tmp/pi-local-release/bun/pi --version ``` - In the interactive smoke test, verify startup, model/account listing, and at least one real prompt with the intended default provider. Treat failures as release blockers unless the user explicitly accepts the risk. + Verify startup, model/account listing, and at least one real prompt with the intended default provider. Failures are release blockers unless the user explicitly accepts the risk. -3. **Run release script until npm publish**: - ```bash - npm run release:patch # Fixes and additions - npm run release:minor # API breaking changes +3. **Brief the user on the WebAuthn flow before running anything**. Print exactly the following message and then stop and wait for the user to confirm in their next message: + + ``` + Before I run the release script, read this carefully: + + - `npm publish` uses WebAuthn 2FA. + - A login URL will appear in the live bash output in this TUI. I will NOT see it until the command exits. + - You must watch the bash output, cmd/ctrl-click the URL, log in in the browser, and select the "don't ask again for N minutes" option so publish can continue. + - This may happen more than once during the release. + + Reply "ready" once you have read this and are watching the bash output. I will not run the release script until you do. ``` - npm publishing requires the maintainer's npm WebAuthn/security-key 2FA and cannot be completed by an agent alone. If the release script stops at `npm publish` with an npm browser authentication URL, the maintainer must run or approve `npm run publish` locally. Do not rerun the version bump. + Do not proceed to step 4 until the user explicitly confirms. -4. **After publish succeeds, finish release bookkeeping**: +4. **Run the release script**: + ```bash + npm run release:patch # fixes + additions + npm run release:minor # breaking changes + ``` + Do not pass a `timeout` to the bash tool for this call. If publish fails partway, stop and report to the user what happened (which package failed, the error output) along with possible solutions. Never rerun the version bump on your own. + +5. **After publish succeeds**: - Add fresh `## [Unreleased]` sections to package changelogs. - Commit with `Add [Unreleased] section for next cycle`. - Push `main` and the release tag. -The release script handles the full flow when npm publish auth is already satisfied. If npm requires WebAuthn during publish, continue manually from the existing release commit/tag using the steps above. +## User Override -## **CRITICAL** Git Rules for Parallel Agents **CRITICAL** - -Multiple agents may work on different files in the same worktree simultaneously. You MUST follow these rules: - -### Committing - -- **ONLY commit files YOU changed in THIS session** -- ALWAYS include `fixes #` or `closes #` in the commit message when there is a related issue or PR -- NEVER use `git add -A` or `git add .` - these sweep up changes from other agents -- ALWAYS use `git add ` listing only files you modified -- Before committing, run `git status` and verify you are only staging YOUR files -- Track which files you created/modified/deleted during the session -- It is always fine to include `packages/ai/src/models.generated.ts` in a commit alongside the actual files you want to commit - -### Forbidden Git Operations - -These commands can destroy other agents' work: - -- `git reset --hard` - destroys uncommitted changes -- `git checkout .` - destroys uncommitted changes -- `git clean -fd` - deletes untracked files -- `git stash` - stashes ALL changes including other agents' work -- `git add -A` / `git add .` - stages other agents' uncommitted work -- `git commit --no-verify` - bypasses required checks and is never allowed - -### Safe Workflow - -```bash -# 1. Check status first -git status - -# 2. Add ONLY your specific files -git add packages/ai/src/providers/transform-messages.ts -git add packages/ai/CHANGELOG.md - -# 3. Commit -git commit -m "fix(ai): description" - -# 4. Push (pull --rebase if needed, but NEVER reset/checkout) -git pull --rebase && git push -``` - -### If Rebase Conflicts Occur - -- Resolve conflicts in YOUR files only -- If conflict is in a file you didn't modify, abort and ask the user -- NEVER force push - -### User override - -If the user instructions conflict with rules set out here, ask for confirmation that they want to override the rules. Only then execute their instructions. +If the user's instructions conflict with any rule in this document, ask for explicit confirmation before overriding. Only then execute their instructions.